open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { requireDid } from '../lib/config.js';
5import { CAP_COLLECTION, SKILL_COLLECTION, VOUCH_COLLECTION } from '../lib/constants.js';
6import { TID } from '@atproto/common-web';
7import { restoreAgent } from '../lib/oauth.js';
8import { appendLog, readBeaconSet, readFollowing, readLog } from '../lib/vit-dir.js';
9import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js';
10import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js';
11import { mark, name } from '../lib/brand.js';
12import { resolvePds, listRecordsFromPds, batchQuery } from '../lib/pds.js';
13import { loadConfig } from '../lib/config.js';
14import { jsonOk, jsonError } from '../lib/json-output.js';
15
16export default function register(program) {
17 program
18 .command('vouch')
19 .argument('<ref>', 'Cap or skill reference (e.g. fast-cache-invalidation or skill-agent-test-patterns)')
20 .description('Publicly endorse a vetted cap or skill, or signal demand with --kind want')
21 .option('--did <did>', 'DID to use')
22 .option('--kind <kind>', 'Vouch intent: endorse (default, quality signal) or want (demand signal)')
23 .option('--json', 'Output as JSON')
24 .option('-v, --verbose', 'Show step-by-step details')
25 .action(async (ref, opts) => {
26 try {
27 const { verbose } = opts;
28 const vlog = opts.json ? (...a) => console.error(...a) : console.log;
29 const isSkill = isSkillRef(ref);
30
31 // Validate --kind if provided
32 if (opts.kind) {
33 const validVouchKinds = ['endorse', 'want'];
34 if (!validVouchKinds.includes(opts.kind)) {
35 if (opts.json) {
36 jsonError(`--kind must be one of: ${validVouchKinds.join(', ')}`);
37 return;
38 }
39 console.error(`error: --kind must be one of: ${validVouchKinds.join(', ')}`);
40 process.exitCode = 1;
41 return;
42 }
43 }
44
45 // Validate ref format
46 if (isSkill) {
47 if (!isValidSkillRef(ref)) {
48 if (opts.json) {
49 jsonError('invalid skill ref', 'expected format: skill-{name}');
50 return;
51 }
52 console.error('invalid skill ref. expected format: skill-{name} (lowercase letters, numbers, hyphens)');
53 process.exitCode = 1;
54 return;
55 }
56 } else {
57 if (!REF_PATTERN.test(ref)) {
58 if (opts.json) {
59 jsonError('invalid ref', 'expected three lowercase words with dashes');
60 return;
61 }
62 console.error('invalid ref. expected three lowercase words with dashes (e.g. fast-cache-invalidation)');
63 process.exitCode = 1;
64 return;
65 }
66 }
67
68 if (opts.json && !(opts.did || loadConfig().did)) {
69 jsonError('no DID configured', "run 'vit login <handle>' first");
70 return;
71 }
72 const did = requireDid(opts);
73 if (!did) return;
74 if (verbose) vlog(`[verbose] DID: ${did}`);
75
76 if (isSkill) {
77 // Skill vouch — no beacon required, check trusted first
78 const trusted = readLog('trusted.jsonl');
79 const trustedEntry = trusted.find(e => e.ref === ref);
80 if (!trustedEntry) {
81 if (opts.json) {
82 jsonError(`skill '${ref}' is not yet vetted`, `run 'vit vet ${ref}' first`);
83 return;
84 }
85 console.error(`skill '${ref}' is not yet vetted. vet it first:`);
86 console.error('');
87 console.error(` vit vet ${ref}`);
88 console.error('');
89 console.error('after reviewing, trust it with:');
90 console.error('');
91 console.error(` vit vet ${ref} --trust`);
92 process.exitCode = 1;
93 return;
94 }
95 if (verbose) vlog(`[verbose] trusted entry found, uri: ${trustedEntry.uri}`);
96
97 const skillName = nameFromSkillRef(ref);
98
99 const { agent } = await restoreAgent(did);
100 if (verbose) vlog('[verbose] session restored');
101
102 const following = readFollowing();
103 const dids = following.map(e => e.did);
104 dids.push(did);
105
106 const allRecords = await batchQuery(dids, async (repoDid) => {
107 const pds = await resolvePds(repoDid);
108 if (verbose) vlog(`[verbose] ${repoDid}: resolved PDS ${pds}`);
109 return (await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50)).records;
110 }, { verbose });
111
112 let match = null;
113 for (const records of allRecords) {
114 for (const rec of records) {
115 if (rec.value.name === skillName) {
116 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) {
117 match = rec;
118 }
119 }
120 }
121 }
122
123 if (!match) {
124 if (opts.json) {
125 jsonError(`no skill found with ref '${ref}'`);
126 return;
127 }
128 console.error(`no skill found with ref '${ref}' from followed accounts.`);
129 console.error('');
130 console.error('hint: skills appear from accounts you follow and your own.');
131 console.error(` vit following check who you're following`);
132 console.error(` vit explore skills browse skills network-wide`);
133 process.exitCode = 1;
134 return;
135 }
136
137 const now = new Date().toISOString();
138 const vouchRecord = {
139 $type: VOUCH_COLLECTION,
140 subject: {
141 uri: match.uri,
142 cid: match.cid,
143 },
144 createdAt: now,
145 ref,
146 // No beacon for skill vouches
147 };
148 if (verbose) vlog(`[verbose] creating vouch for ${match.uri}`);
149 const rkey = TID.nextStr();
150 const res = await agent.com.atproto.repo.putRecord({
151 repo: did,
152 collection: VOUCH_COLLECTION,
153 rkey,
154 record: vouchRecord,
155 validate: true,
156 });
157
158 try {
159 appendLog('vouched.jsonl', {
160 ref,
161 uri: match.uri,
162 cid: match.cid,
163 vouchUri: res.data.uri,
164 ts: now,
165 });
166 } catch (logErr) {
167 console.error('warning: failed to write vouched.jsonl:', logErr.message);
168 }
169 if (verbose) vlog('[verbose] logged to vouched.jsonl');
170
171 if (opts.json) {
172 jsonOk({ ref, uri: match.uri, vouchUri: res.data.uri });
173 return;
174 }
175 console.log(`${mark} vouched: ${ref} (${match.uri})`);
176 } else {
177 const vouchKind = opts.kind || 'endorse';
178 const isWant = vouchKind === 'want';
179
180 // Cap vouch — beacon required unless want-vouching (demand signal)
181 const beaconSet = readBeaconSet();
182
183 if (!isWant) {
184 if (beaconSet.size === 0) {
185 if (opts.json) {
186 jsonError('no beacon set', "run 'vit init' first");
187 return;
188 }
189 console.error(`no beacon set. run '${name} init' in a project directory first.`);
190 process.exitCode = 1;
191 return;
192 }
193 if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`);
194
195 const trusted = readLog('trusted.jsonl');
196 const trustedEntry = trusted.find(e => e.ref === ref);
197 if (!trustedEntry) {
198 if (opts.json) {
199 jsonError(`cap '${ref}' is not yet vetted`, `run 'vit vet ${ref}' first`);
200 return;
201 }
202 console.error(`cap '${ref}' is not yet vetted. vet it first:`);
203 console.error('');
204 console.error(` vit vet ${ref}`);
205 console.error('');
206 console.error('after reviewing, trust it with:');
207 console.error('');
208 console.error(` vit vet ${ref} --trust`);
209 process.exitCode = 1;
210 return;
211 }
212 if (verbose) vlog(`[verbose] trusted entry found, uri: ${trustedEntry.uri}`);
213 }
214
215 const { agent } = await restoreAgent(did);
216 if (verbose) vlog('[verbose] session restored');
217
218 const following = readFollowing();
219 const dids = following.map(e => e.did);
220 dids.push(did);
221
222 const allRecords = await batchQuery(dids, async (repoDid) => {
223 const pds = await resolvePds(repoDid);
224 if (verbose) vlog(`[verbose] ${repoDid}: resolved PDS ${pds}`);
225 return (await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50)).records;
226 }, { verbose });
227
228 let match = null;
229 for (const records of allRecords) {
230 for (const rec of records) {
231 if (!isWant && !beaconSet.has(rec.value.beacon)) continue;
232 const recRef = resolveRef(rec.value, rec.cid);
233 if (recRef === ref) {
234 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) {
235 match = rec;
236 }
237 }
238 }
239 }
240
241 if (!match) {
242 if (opts.json) {
243 jsonError(`no cap found with ref '${ref}'${!isWant ? ' for this beacon' : ''}`);
244 return;
245 }
246 console.error(`no cap found with ref '${ref}'${!isWant ? ' for this beacon' : ''}.`);
247 console.error('');
248 console.error('hint: caps only appear from accounts you follow and your own.');
249 console.error(` vit following check who you're following`);
250 console.error(` vit explore cap ${ref} search the network-wide index`);
251 process.exitCode = 1;
252 return;
253 }
254
255 const now = new Date().toISOString();
256 const projBeacon = beaconSet.size > 0 ? [...beaconSet][0] : (match.value.beacon || null);
257 const vouchRecord = {
258 $type: VOUCH_COLLECTION,
259 subject: {
260 uri: match.uri,
261 cid: match.cid,
262 },
263 createdAt: now,
264 ref,
265 kind: vouchKind,
266 };
267 if (projBeacon) vouchRecord.beacon = projBeacon;
268 if (verbose) vlog(`[verbose] creating vouch (${vouchKind}) for ${match.uri}`);
269 const rkey = TID.nextStr();
270 const res = await agent.com.atproto.repo.putRecord({
271 repo: did,
272 collection: VOUCH_COLLECTION,
273 rkey,
274 record: vouchRecord,
275 validate: false,
276 });
277
278 try {
279 appendLog('vouched.jsonl', {
280 ref,
281 uri: match.uri,
282 cid: match.cid,
283 vouchUri: res.data.uri,
284 kind: vouchKind,
285 beacon: projBeacon,
286 ts: now,
287 });
288 } catch (logErr) {
289 console.error('warning: failed to write vouched.jsonl:', logErr.message);
290 }
291 if (verbose) vlog('[verbose] logged to vouched.jsonl');
292
293 if (opts.json) {
294 jsonOk({ ref, uri: match.uri, vouchUri: res.data.uri, kind: vouchKind });
295 return;
296 }
297 if (isWant) {
298 console.log(`${mark} vouched (want): ${ref}`);
299 console.log(` demand signal recorded. vouch count visible in vit explore vouches.`);
300 } else {
301 console.log(`${mark} vouched: ${ref} (${match.uri})`);
302 }
303 }
304 } catch (err) {
305 const msg = err instanceof Error ? err.message : String(err);
306 if (opts.json) {
307 jsonError(msg);
308 return;
309 }
310 console.error(msg);
311 process.exitCode = 1;
312 }
313 });
314}