open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(beacon): add secondary beacon support for upstream cap discovery

readBeaconSet() is the new shared abstraction for beacon matching.
All reading commands (skim, vet, remix, vouch, scan) use it instead
of inline === comparisons. init --secondary stores the upstream
beacon. explore API accepts comma-separated beacons with IN queries.

+173 -37
+8 -4
explore/src/api.js
··· 52 52 const bindings = []; 53 53 54 54 if (beacon) { 55 - conditions.push('c.beacon = ?'); 56 - bindings.push(beacon); 55 + const beacons = beacon.split(',').filter(Boolean); 56 + const placeholders = beacons.map(() => '?').join(', '); 57 + conditions.push(`c.beacon IN (${placeholders})`); 58 + bindings.push(...beacons); 57 59 } 58 60 59 61 if (cursor) { ··· 101 103 bindings.push(ref); 102 104 103 105 if (beacon) { 104 - conditions.push('c.beacon = ?'); 105 - bindings.push(beacon); 106 + const beacons = beacon.split(',').filter(Boolean); 107 + const placeholders = beacons.map(() => '?').join(', '); 108 + conditions.push(`c.beacon IN (${placeholders})`); 109 + bindings.push(...beacons); 106 110 } 107 111 } 108 112
+4 -2
src/cmd/explore.js
··· 37 37 try { 38 38 let beacon = opts.beacon; 39 39 if (beacon === '.') { 40 - beacon = readProjectConfig().beacon; 41 - if (!beacon) { 40 + const config = readProjectConfig(); 41 + const beacons = [config.beacon, config.secondaryBeacon].filter(Boolean); 42 + if (beacons.length === 0) { 42 43 const msg = "no beacon set — run 'vit init' first"; 43 44 if (opts.json) { 44 45 jsonError(msg); ··· 48 49 process.exitCode = 1; 49 50 return; 50 51 } 52 + beacon = beacons.join(','); 51 53 } 52 54 53 55 const url = new URL('/api/caps', baseUrl);
+44 -4
src/cmd/init.js
··· 15 15 .command('init') 16 16 .description('Initialize .vit directory and set project beacon. Use the most official upstream or well-known git URL so all contributors converge on the same beacon.') 17 17 .option('--beacon <url>', 'Git URL (or "." to read from git remote upstream/origin) to derive the beacon URI') 18 + .option('--secondary <url>', 'Secondary beacon URL for upstream cap discovery') 18 19 .option('--json', 'Output as JSON') 19 20 .option('-v, --verbose', 'Show step-by-step details') 20 21 .action(async (opts) => { ··· 36 37 const dir = vitDir(); 37 38 if (verbose) vlog(`[verbose] .vit dir: ${dir}`); 38 39 39 - if (!opts.beacon) { 40 + if (!opts.beacon && !opts.secondary) { 40 41 const config = readProjectConfig(); 41 42 if (config.beacon) { 42 43 if (opts.json) { 43 - jsonOk({ beacon: config.beacon }); 44 + const out = { beacon: config.beacon }; 45 + if (config.secondaryBeacon) out.secondaryBeacon = config.secondaryBeacon; 46 + jsonOk(out); 44 47 return; 45 48 } 46 49 console.log(`${mark} beacon: ${config.beacon}`); 50 + if (config.secondaryBeacon) { 51 + console.log(`${mark} secondary beacon: ${config.secondaryBeacon}`); 52 + } 47 53 console.log(`hint: to change the beacon, run: ${name} init --beacon <git-url>`); 48 54 return; 49 55 } ··· 137 143 return; 138 144 } 139 145 146 + if (opts.secondary && !opts.beacon) { 147 + const config = readProjectConfig(); 148 + if (!config.beacon) { 149 + if (opts.json) { 150 + jsonError("no primary beacon set — run 'vit init --beacon <url>' first"); 151 + return; 152 + } 153 + console.error("no primary beacon set — run 'vit init --beacon <url>' first"); 154 + process.exitCode = 1; 155 + return; 156 + } 157 + 158 + const secondary = 'vit:' + toBeacon(opts.secondary); 159 + const merged = { ...config, secondaryBeacon: secondary }; 160 + writeProjectConfig(merged); 161 + if (opts.json) { 162 + jsonOk({ beacon: merged.beacon, secondaryBeacon: merged.secondaryBeacon }); 163 + return; 164 + } 165 + console.log(`${mark} beacon: ${merged.beacon}`); 166 + console.log(`${mark} secondary beacon: ${merged.secondaryBeacon}`); 167 + return; 168 + } 169 + 140 170 let gitUrl = opts.beacon; 141 171 if (gitUrl === '.') { 142 172 if (verbose) vlog('[verbose] resolving --beacon . via remote.upstream.url then remote.origin.url'); ··· 177 207 178 208 const beacon = 'vit:' + toBeacon(gitUrl); 179 209 if (verbose) vlog(`[verbose] Computed beacon: ${beacon}`); 180 - writeProjectConfig({ beacon }); 210 + const existing = readProjectConfig(); 211 + const merged = { ...existing, beacon }; 212 + if (opts.secondary) { 213 + merged.secondaryBeacon = 'vit:' + toBeacon(opts.secondary); 214 + } 215 + writeProjectConfig(merged); 181 216 if (verbose) vlog(`[verbose] Wrote config.json`); 182 217 const readmePath = join(vitDir(), 'README.md'); 183 218 if (!existsSync(readmePath)) { ··· 185 220 if (verbose) vlog(`[verbose] Wrote .vit/README.md`); 186 221 } 187 222 if (opts.json) { 188 - jsonOk({ beacon }); 223 + const out = { beacon: merged.beacon }; 224 + if (merged.secondaryBeacon) out.secondaryBeacon = merged.secondaryBeacon; 225 + jsonOk(out); 189 226 return; 190 227 } 191 228 console.log(`${mark} beacon: ${beacon}`); 229 + if (merged.secondaryBeacon) { 230 + console.log(`${mark} secondary beacon: ${merged.secondaryBeacon}`); 231 + } 192 232 } catch (err) { 193 233 const msg = err instanceof Error ? err.message : String(err); 194 234 if (opts.json) {
+5 -6
src/cmd/remix.js
··· 4 4 import { requireDid } from '../lib/config.js'; 5 5 import { CAP_COLLECTION } from '../lib/constants.js'; 6 6 import { restoreAgent } from '../lib/oauth.js'; 7 - import { readProjectConfig, readFollowing, readLog } from '../lib/vit-dir.js'; 7 + import { readBeaconSet, readFollowing, readLog } from '../lib/vit-dir.js'; 8 8 import { requireAgent, detectCodingAgent } from '../lib/agent.js'; 9 9 import { shouldBypassVet } from '../lib/trust-gate.js'; 10 10 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; ··· 56 56 if (!did) return; 57 57 if (verbose) vlog(`[verbose] DID: ${did}`); 58 58 59 - const projectConfig = readProjectConfig(); 60 - const beacon = projectConfig.beacon; 61 - if (!beacon) { 59 + const beaconSet = readBeaconSet(); 60 + if (beaconSet.size === 0) { 62 61 if (opts.json) { 63 62 jsonError('no beacon set', "run 'vit init' first"); 64 63 return; ··· 67 66 process.exitCode = 1; 68 67 return; 69 68 } 70 - if (verbose) vlog(`[verbose] beacon: ${beacon}`); 69 + if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 71 70 72 71 const trusted = readLog('trusted.jsonl'); 73 72 const trustedEntry = trusted.find(e => e.ref === ref); ··· 114 113 let match = null; 115 114 for (const records of allRecords) { 116 115 for (const rec of records) { 117 - if (rec.value.beacon !== beacon) continue; 116 + if (!beaconSet.has(rec.value.beacon)) continue; 118 117 const recRef = resolveRef(rec.value, rec.cid); 119 118 if (recRef === ref) { 120 119 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) {
+20 -2
src/cmd/scan.js
··· 6 6 import { resolveHandleFromDid } from '../lib/pds.js'; 7 7 import { brand } from '../lib/brand.js'; 8 8 import { jsonOk, jsonError } from '../lib/json-output.js'; 9 + import { readBeaconSet } from '../lib/vit-dir.js'; 9 10 10 11 export default function register(program) { 11 12 program ··· 37 38 38 39 const wantCaps = !opts.skills; 39 40 const wantSkills = !opts.caps; 41 + let beaconSet = null; 42 + if (opts.beacon) { 43 + if (opts.beacon === '.') { 44 + beaconSet = readBeaconSet(); 45 + if (beaconSet.size === 0) { 46 + if (opts.json) { 47 + jsonError("no beacon set — run 'vit init' first"); 48 + return; 49 + } 50 + console.error("no beacon set — run 'vit init' first"); 51 + process.exitCode = 1; 52 + return; 53 + } 54 + } else { 55 + beaconSet = new Set([opts.beacon]); 56 + } 57 + } 40 58 41 59 const cursor = (Date.now() - days * 86400000) * 1000; 42 60 const timeout = Math.max(120000, Math.min(600000, days * 60000)); ··· 56 74 if (!opts.json) { 57 75 console.log(`${brand} scan`); 58 76 console.log(` Replaying ${days} day${days === 1 ? '' : 's'} of ${scanType} events...`); 59 - if (opts.beacon) console.log(` Beacon filter: ${opts.beacon}`); 77 + if (beaconSet) console.log(` Beacon filter: ${[...beaconSet].join(', ')}`); 60 78 if (opts.tag) console.log(` Tag filter: ${opts.tag}`); 61 79 console.log(` Timeout: ${Math.round(timeout / 1000)}s`); 62 80 console.log(''); ··· 87 105 if (!isCapEvent && !isSkillEvent) return; 88 106 89 107 // Apply filters 90 - if (isCapEvent && opts.beacon && record.beacon !== opts.beacon) return; 108 + if (isCapEvent && beaconSet && !beaconSet.has(record.beacon)) return; 91 109 if (isSkillEvent && opts.tag) { 92 110 const tags = record.tags || []; 93 111 if (!tags.some(t => t.toLowerCase() === opts.tag.toLowerCase())) return;
+6 -7
src/cmd/skim.js
··· 4 4 import { requireDid } from '../lib/config.js'; 5 5 import { CAP_COLLECTION, SKILL_COLLECTION } from '../lib/constants.js'; 6 6 import { restoreAgent } from '../lib/oauth.js'; 7 - import { readProjectConfig, readFollowing } from '../lib/vit-dir.js'; 7 + import { readBeaconSet, readFollowing } from '../lib/vit-dir.js'; 8 8 import { requireAgent } from '../lib/agent.js'; 9 9 import { resolveRef } from '../lib/cap-ref.js'; 10 10 import { skillRefFromName } from '../lib/skill-ref.js'; ··· 37 37 if (!did) return; 38 38 if (verbose) console.log(`[verbose] DID: ${did}`); 39 39 40 - const projectConfig = readProjectConfig(); 41 - const beacon = projectConfig.beacon; 40 + const beaconSet = readBeaconSet(); 42 41 43 42 const wantCaps = !opts.skills; 44 43 const wantSkills = !opts.caps; 45 44 const skillsOnly = opts.skills && !opts.caps; 46 45 47 46 // Beacon required unless --skills only mode 48 - if (!beacon && !skillsOnly) { 47 + if (beaconSet.size === 0 && !skillsOnly) { 49 48 console.error(`no beacon set. run '${name} init' in a project directory first.`); 50 49 process.exitCode = 1; 51 50 return; 52 51 } 53 52 54 - if (verbose && beacon) console.log(`[verbose] beacon: ${beacon}`); 53 + if (verbose && beaconSet.size > 0) console.log(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 55 54 56 55 const { agent } = await restoreAgent(did); 57 56 if (verbose) console.log('[verbose] session restored'); ··· 91 90 const items = []; 92 91 93 92 // Fetch caps (filtered by beacon) 94 - if (wantCaps && beacon) { 93 + if (wantCaps && beaconSet.size > 0) { 95 94 const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50); 96 - const caps = res.records.filter(r => r.value.beacon === beacon); 95 + const caps = res.records.filter(r => beaconSet.has(r.value.beacon)); 97 96 if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} caps, ${caps.length} matching beacon`); 98 97 for (const cap of caps) { 99 98 cap._handle = handleMap.get(repoDid) || repoDid;
+5 -6
src/cmd/vet.js
··· 8 8 import { requireDid } from '../lib/config.js'; 9 9 import { CAP_COLLECTION, SKILL_COLLECTION } from '../lib/constants.js'; 10 10 import { restoreAgent } from '../lib/oauth.js'; 11 - import { appendLog, readProjectConfig, readFollowing, vitDir } from '../lib/vit-dir.js'; 11 + import { appendLog, readBeaconSet, readFollowing, vitDir } from '../lib/vit-dir.js'; 12 12 import { requireNotAgent, detectCodingAgent, toSandboxName } from '../lib/agent.js'; 13 13 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 14 14 import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js'; ··· 259 259 260 260 if (!isSkill) { 261 261 // Cap vet requires beacon 262 - const projectConfig = readProjectConfig(); 263 - const beacon = projectConfig.beacon; 264 - if (!beacon) { 262 + const beaconSet = readBeaconSet(); 263 + if (beaconSet.size === 0) { 265 264 if (opts.json) { 266 265 jsonError('no beacon set', "run 'vit init' first"); 267 266 return; ··· 270 269 process.exitCode = 1; 271 270 return; 272 271 } 273 - if (verbose) vlog(`[verbose] beacon: ${beacon}`); 272 + if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 274 273 275 274 const { agent: oauthAgent } = await restoreAgent(did); 276 275 if (verbose) vlog('[verbose] session restored'); ··· 290 289 let match = null; 291 290 for (const records of allRecords) { 292 291 for (const rec of records) { 293 - if (rec.value.beacon !== beacon) continue; 292 + if (!beaconSet.has(rec.value.beacon)) continue; 294 293 const recRef = resolveRef(rec.value, rec.cid); 295 294 if (recRef === ref) { 296 295 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) {
+5 -6
src/cmd/vouch.js
··· 5 5 import { CAP_COLLECTION, SKILL_COLLECTION, VOUCH_COLLECTION } from '../lib/constants.js'; 6 6 import { TID } from '@atproto/common-web'; 7 7 import { restoreAgent } from '../lib/oauth.js'; 8 - import { appendLog, readProjectConfig, readFollowing, readLog } from '../lib/vit-dir.js'; 8 + import { appendLog, readBeaconSet, readFollowing, readLog } from '../lib/vit-dir.js'; 9 9 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 10 10 import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js'; 11 11 import { mark, name } from '../lib/brand.js'; ··· 156 156 console.log(`${mark} vouched: ${ref} (${match.uri})`); 157 157 } else { 158 158 // Cap vouch — requires beacon (check beacon before trusted, matching original behavior) 159 - const projectConfig = readProjectConfig(); 160 - const beacon = projectConfig.beacon; 161 - if (!beacon) { 159 + const beaconSet = readBeaconSet(); 160 + if (beaconSet.size === 0) { 162 161 if (opts.json) { 163 162 jsonError('no beacon set', "run 'vit init' first"); 164 163 return; ··· 167 166 process.exitCode = 1; 168 167 return; 169 168 } 170 - if (verbose) vlog(`[verbose] beacon: ${beacon}`); 169 + if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 171 170 172 171 const trusted = readLog('trusted.jsonl'); 173 172 const trustedEntry = trusted.find(e => e.ref === ref); ··· 204 203 let match = null; 205 204 for (const records of allRecords) { 206 205 for (const rec of records) { 207 - if (rec.value.beacon !== beacon) continue; 206 + if (!beaconSet.has(rec.value.beacon)) continue; 208 207 const recRef = resolveRef(rec.value, rec.cid); 209 208 if (recRef === ref) { 210 209 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) {
+8
src/lib/vit-dir.js
··· 18 18 } 19 19 } 20 20 21 + export function readBeaconSet() { 22 + const config = readProjectConfig(); 23 + const set = new Set(); 24 + if (config.beacon) set.add(config.beacon); 25 + if (config.secondaryBeacon) set.add(config.secondaryBeacon); 26 + return set; 27 + } 28 + 21 29 export function writeProjectConfig(obj, baseDir) { 22 30 const dir = baseDir ? join(baseDir, '.vit') : vitDir(); 23 31 mkdirSync(dir, { recursive: true });
+44
test/init.test.js
··· 83 83 expect(result.stdout).toContain('hint: to change the beacon, run: vit init --beacon <git-url>'); 84 84 }); 85 85 86 + test('--secondary stores secondaryBeacon with existing primary', () => { 87 + run('init --beacon https://github.com/org/repo.git', tmpDir, { CLAUDECODE: '1' }); 88 + const result = run('init --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' }); 89 + expect(result.exitCode).toBe(0); 90 + 91 + const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 92 + const config = JSON.parse(content); 93 + expect(config.beacon).toBe('vit:github.com/org/repo'); 94 + expect(config.secondaryBeacon).toBe('vit:github.com/upstream/repo'); 95 + }); 96 + 97 + test('--secondary errors without existing primary beacon', () => { 98 + const result = run('init --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' }); 99 + expect(result.exitCode).not.toBe(0); 100 + }); 101 + 102 + test('--beacon preserves existing secondaryBeacon', () => { 103 + run('init --beacon https://github.com/org/repo.git', tmpDir, { CLAUDECODE: '1' }); 104 + run('init --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' }); 105 + run('init --beacon https://github.com/org/newrepo.git', tmpDir, { CLAUDECODE: '1' }); 106 + 107 + const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 108 + const config = JSON.parse(content); 109 + expect(config.beacon).toBe('vit:github.com/org/newrepo'); 110 + expect(config.secondaryBeacon).toBe('vit:github.com/upstream/repo'); 111 + }); 112 + 113 + test('displays secondary beacon when set', () => { 114 + run('init --beacon https://github.com/org/repo.git --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' }); 115 + const result = run('init', tmpDir, { CLAUDECODE: '1' }); 116 + expect(result.exitCode).toBe(0); 117 + expect(result.stdout).toContain('beacon: vit:github.com/org/repo'); 118 + expect(result.stdout).toContain('secondary beacon: vit:github.com/upstream/repo'); 119 + }); 120 + 121 + test('--json includes secondaryBeacon when set', () => { 122 + run('init --beacon https://github.com/org/repo.git --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' }); 123 + const result = run('init --json', tmpDir, { CLAUDECODE: '1' }); 124 + expect(result.exitCode).toBe(0); 125 + const output = JSON.parse(result.stdout); 126 + expect(output.beacon).toBe('vit:github.com/org/repo'); 127 + expect(output.secondaryBeacon).toBe('vit:github.com/upstream/repo'); 128 + }); 129 + 86 130 test('reports no beacon when .vit exists but directory is not a git repo', () => { 87 131 mkdirSync(join(tmpDir, '.vit'), { recursive: true }); 88 132 const result = run('init', tmpDir, { CLAUDECODE: '1' });
+24
test/vit-dir.test.js
··· 45 45 expect(config).toEqual({}); 46 46 }); 47 47 48 + test('readBeaconSet returns empty Set when no config', async () => { 49 + const { readBeaconSet } = await import('../src/lib/vit-dir.js'); 50 + const set = readBeaconSet(); 51 + expect(set).toBeInstanceOf(Set); 52 + expect(set.size).toBe(0); 53 + }); 54 + 55 + test('readBeaconSet returns primary only when no secondary', async () => { 56 + const { writeProjectConfig, readBeaconSet } = await import('../src/lib/vit-dir.js'); 57 + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); 58 + const set = readBeaconSet(); 59 + expect(set.size).toBe(1); 60 + expect(set.has('vit:github.com/org/repo')).toBe(true); 61 + }); 62 + 63 + test('readBeaconSet returns both when secondary is set', async () => { 64 + const { writeProjectConfig, readBeaconSet } = await import('../src/lib/vit-dir.js'); 65 + writeProjectConfig({ beacon: 'vit:github.com/org/repo', secondaryBeacon: 'vit:github.com/upstream/repo' }); 66 + const set = readBeaconSet(); 67 + expect(set.size).toBe(2); 68 + expect(set.has('vit:github.com/org/repo')).toBe(true); 69 + expect(set.has('vit:github.com/upstream/repo')).toBe(true); 70 + }); 71 + 48 72 test('appendLog creates file and appends JSONL line', async () => { 49 73 const { appendLog } = await import('../src/lib/vit-dir.js'); 50 74 appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', did: 'did:plc:test' });