open source is social v-it.org
0
fork

Configure Feed

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

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