open source is social v-it.org
at main 215 lines 9.1 kB view raw
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}