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 { readProjectConfig, 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('-v, --verbose', 'Show step-by-step details')
25 .action(async (opts) => {
26 try {
27 const gate = requireAgent();
28 if (!gate.ok) {
29 console.error(`${name} skim should be run by a coding agent (e.g. claude code, gemini cli).`);
30 console.error(`open your agent and ask it to run '${name} skim' for you.`);
31 process.exitCode = 1;
32 return;
33 }
34
35 const { verbose } = opts;
36 const did = requireDid(opts);
37 if (!did) return;
38 if (verbose) console.log(`[verbose] DID: ${did}`);
39
40 const projectConfig = readProjectConfig();
41 const beacon = projectConfig.beacon;
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 (!beacon && !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 && beacon) console.log(`[verbose] beacon: ${beacon}`);
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 && beacon) {
95 const res = await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50);
96 const caps = res.records.filter(r => r.value.beacon === beacon);
97 if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} caps, ${caps.length} matching beacon`);
98 for (const cap of caps) {
99 cap._handle = handleMap.get(repoDid) || repoDid;
100 cap._type = 'cap';
101 }
102 items.push(...caps);
103 }
104
105 // Fetch skills (unfiltered — skills are universal)
106 if (wantSkills) {
107 try {
108 const res = await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50);
109 if (verbose) console.log(`[verbose] ${repoDid}: ${res.records.length} skills`);
110 for (const skill of res.records) {
111 skill._handle = handleMap.get(repoDid) || repoDid;
112 skill._type = 'skill';
113 }
114 items.push(...res.records);
115 } catch (err) {
116 if (verbose) console.log(`[verbose] ${repoDid}: error fetching skills: ${err.message}`);
117 }
118 }
119
120 return items;
121 }, { verbose });
122
123 for (const items of batchResults) {
124 allItems.push(...items);
125 }
126
127 // sort by createdAt descending
128 allItems.sort((a, b) => {
129 const ta = a.value.createdAt || '';
130 const tb = b.value.createdAt || '';
131 return tb.localeCompare(ta);
132 });
133
134 // apply limit
135 const limit = parseInt(opts.limit, 10);
136 const capped = allItems.slice(0, limit);
137
138 if (opts.json) {
139 console.log(JSON.stringify(capped, null, 2));
140 } else {
141 if (capped.length === 0) {
142 if (skillsOnly) {
143 console.log('no skills found.');
144 } else if (opts.caps) {
145 console.log('no caps found for this beacon.');
146 } else {
147 console.log('no caps or skills found.');
148 }
149 }
150 for (const rec of capped) {
151 if (rec._type === 'skill') {
152 const skillRef = skillRefFromName(rec.value.name);
153 const skillName = rec.value.name || '';
154 const description = rec.value.description || '';
155 const version = rec.value.version;
156 const tags = rec.value.tags;
157 console.log(`ref: ${skillRef}`);
158 console.log(`by: @${rec._handle}`);
159 console.log(`type: skill${version ? ' v' + version : ''}`);
160 if (skillName) console.log(`title: ${skillName}`);
161 if (description) console.log(`description: ${description}`);
162 if (tags && tags.length > 0) console.log(`tags: ${tags.join(', ')}`);
163 console.log();
164 } else {
165 const ref = resolveRef(rec.value, rec.cid);
166 const title = rec.value.title || '';
167 const description = rec.value.description || '';
168 console.log(`ref: ${ref}`);
169 console.log(`by: @${rec._handle}`);
170 console.log(`type: cap`);
171 if (title) console.log(`title: ${title}`);
172 if (description) console.log(`description: ${description}`);
173 console.log();
174 }
175 }
176 console.log('---');
177 console.log(`hint: tell your user to run '${name} vet <ref>' in another terminal for any item they want to review.`);
178 }
179 } catch (err) {
180 console.error(err instanceof Error ? err.message : String(err));
181 process.exitCode = 1;
182 }
183 });
184}