open source is social v-it.org
0

Configure Feed

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

vit / src / cmd / skim.js
7.3 kB 184 lines
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}