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 } from '../lib/constants.js';
6import { restoreAgent } from '../lib/oauth.js';
7import { readBeaconSet, readFollowing } from '../lib/vit-dir.js';
8import { requireAgent } from '../lib/agent.js';
9import { resolveRef } from '../lib/cap-ref.js';
10import { skillRefFromName } from '../lib/skill-ref.js';
11import { name } from '../lib/brand.js';
12import { resolvePds, listRecordsFromPds, batchQuery } from '../lib/pds.js';
13
14export default function register(program) {
15 program
16 .command('skim')
17 .description('Read caps and skills from followed accounts')
18 .option('--did <did>', 'DID to use')
19 .option('--handle <handle>', 'Show items from a specific handle only')
20 .option('--limit <n>', 'Max items to display', '25')
21 .option('--json', 'Output as JSON array')
22 .option('--caps', 'Show only caps')
23 .option('--skills', 'Show only skills')
24 .option('--kind <kind>', 'Filter caps by kind (e.g. request, feat, fix)')
25 .option('-v, --verbose', 'Show step-by-step details')
26 .action(async (opts) => {
27 try {
28 const gate = requireAgent();
29 if (!gate.ok) {
30 console.error(`${name} skim should be run by a coding agent (e.g. claude code, gemini cli).`);
31 console.error(`open your agent and ask it to run '${name} skim' for you.`);
32 process.exitCode = 1;
33 return;
34 }
35
36 const { verbose } = opts;
37 const did = requireDid(opts);
38 if (!did) return;
39 if (verbose) console.log(`[verbose] DID: ${did}`);
40
41 const beaconSet = readBeaconSet();
42
43 const wantCaps = !opts.skills;
44 const wantSkills = !opts.caps;
45 const skillsOnly = opts.skills && !opts.caps;
46
47 // Beacon required unless --skills only mode
48 if (beaconSet.size === 0 && !skillsOnly) {
49 console.error(`no beacon set. run '${name} init' in a project directory first.`);
50 process.exitCode = 1;
51 return;
52 }
53
54 if (verbose && beaconSet.size > 0) console.log(`[verbose] beacons: ${[...beaconSet].join(', ')}`);
55
56 const { agent } = await restoreAgent(did);
57 if (verbose) console.log('[verbose] session restored');
58
59 // build list of DIDs to query and DID→handle map
60 const handleMap = new Map();
61 let dids;
62 if (opts.handle) {
63 const handle = opts.handle.replace(/^@/, '');
64 const resolved = await agent.resolveHandle({ handle });
65 dids = [resolved.data.did];
66 handleMap.set(resolved.data.did, handle);
67 if (verbose) console.log(`[verbose] resolved ${handle} to ${resolved.data.did}`);
68 } else {
69 const following = readFollowing();
70 for (const e of following) handleMap.set(e.did, e.handle);
71 dids = following.map(e => e.did);
72 dids.push(did);
73 }
74
75 // resolve own handle if not already known
76 if (!handleMap.has(did)) {
77 try {
78 const desc = await agent.com.atproto.repo.describeRepo({ repo: did });
79 handleMap.set(did, desc.data.handle);
80 } catch {
81 if (verbose) console.log(`[verbose] could not resolve handle for ${did}`);
82 }
83 }
84
85 // fetch from each DID
86 const allItems = [];
87
88 const batchResults = await batchQuery(dids, async (repoDid) => {
89 const pds = await resolvePds(repoDid);
90 if (verbose) console.log(`[verbose] ${repoDid}: resolved PDS ${pds}`);
91 const items = [];
92
93 // Fetch caps (filtered by beacon)
94 if (wantCaps && beaconSet.size > 0) {
95 const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50);
96 let caps = res.records.filter(r => beaconSet.has(r.value.beacon));
97 if (opts.kind) {
98 caps = caps.filter(r => r.value.kind === opts.kind);
99 }
100 if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} caps, ${caps.length} matching beacon`);
101 for (const cap of caps) {
102 cap._handle = handleMap.get(repoDid) || repoDid;
103 cap._type = 'cap';
104 }
105 items.push(...caps);
106 }
107
108 // Fetch skills (unfiltered — skills are universal)
109 if (wantSkills) {
110 try {
111 const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50);
112 if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} skills`);
113 for (const skill of res.records) {
114 skill._handle = handleMap.get(repoDid) || repoDid;
115 skill._type = 'skill';
116 }
117 items.push(...res.records);
118 } catch (err) {
119 if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`);
120 }
121 }
122
123 return items;
124 }, { verbose });
125
126 for (const items of batchResults) {
127 allItems.push(...items);
128 }
129
130 // sort by createdAt descending
131 allItems.sort((a, b) => {
132 const ta = a.value.createdAt || '';
133 const tb = b.value.createdAt || '';
134 return tb.localeCompare(ta);
135 });
136
137 // apply limit
138 const limit = parseInt(opts.limit, 10);
139 const capped = allItems.slice(0, limit);
140
141 if (opts.json) {
142 if (capped.length === 0) {
143 const following = readFollowing();
144 let hint;
145 if (skillsOnly) {
146 hint = "no skills found — try 'vit explore skills' or ship your own with 'vit ship --skill'";
147 } else if (following.length === 0) {
148 hint = "not following anyone — run 'vit follow <handle>'";
149 } else {
150 hint = "no matching caps — try 'vit explore caps' or 'vit ship'";
151 }
152 console.log(JSON.stringify({ ok: true, items: [], hint }, null, 2));
153 } else {
154 console.log(JSON.stringify(capped, null, 2));
155 }
156 } else {
157 if (capped.length === 0) {
158 if (skillsOnly) {
159 console.log('no skills found from followed accounts.');
160 console.log('');
161 console.log("try 'vit explore skills' to discover skills network-wide, or ship your own with 'vit ship --skill'.");
162 } else {
163 const following = readFollowing();
164 if (following.length === 0) {
165 console.log("no caps or skills found. you're not following anyone yet and haven't shipped any caps for this beacon.");
166 console.log('');
167 console.log('next steps:');
168 console.log(' vit scan discover active publishers on the network');
169 console.log(' vit follow <handle> start following someone to see their caps');
170 console.log(' vit ship publish a cap to seed the network');
171 } else {
172 console.log('no caps found for this beacon from your followed accounts.');
173 console.log('');
174 console.log("the network grows when people ship. publish a cap with 'vit ship' to get things started for this project.");
175 console.log("try 'vit explore caps' for network-wide discovery.");
176 }
177 }
178 }
179 for (const rec of capped) {
180 if (rec._type === 'skill') {
181 const skillRef = skillRefFromName(rec.value.name);
182 const skillName = rec.value.name || '';
183 const description = rec.value.description || '';
184 const version = rec.value.version;
185 const tags = rec.value.tags;
186 console.log(`ref: ${skillRef}`);
187 console.log(`by: @${rec._handle}`);
188 console.log(`type: skill${version ? ' v' + version : ''}`);
189 if (skillName) console.log(`title: ${skillName}`);
190 if (description) console.log(`description: ${description}`);
191 if (tags && tags.length > 0) console.log(`tags: ${tags.join(', ')}`);
192 console.log();
193 } else {
194 const ref = resolveRef(rec.value, rec.cid);
195 const title = rec.value.title || '';
196 const description = rec.value.description || '';
197 console.log(`ref: ${ref}`);
198 console.log(`by: @${rec._handle}`);
199 console.log(`type: cap`);
200 if (title) console.log(`title: ${title}`);
201 if (description) console.log(`description: ${description}`);
202 console.log();
203 }
204 }
205 if (capped.length > 0) {
206 console.log('---');
207 console.log(`hint: tell your operator to run '${name} vet <ref>' in another terminal for any item they want to review.`);
208 }
209 }
210 } catch (err) {
211 console.error(err instanceof Error ? err.message : String(err));
212 process.exitCode = 1;
213 }
214 });
215}