open source is social v-it.org
0
fork

Configure Feed

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

feat(explore): add five subcommands for the explore API

Caps, skills, beacons, vouches, and stats queries against explore.v-it.org.
Supports --json, --explore-url override, VIT_EXPLORE_URL env var, --beacon .
resolution, and --ref-to-URI resolution for vouches.

+415
+12
skills/vit/SKILL.md
··· 133 133 - Output: `beacon: lit <uri>` or `beacon: unlit`. 134 134 - Common errors: invalid target URL or clone/probe failure. 135 135 136 + ### `vit explore` 137 + - Description: Query the public explore index at explore.v-it.org. No authentication required. 138 + - Subcommands: 139 + - `vit explore caps` — list recent caps. Flags: `--beacon <beacon>` (use `.` for current project), `--limit <n>`, `--cursor <id>`, `--json` 140 + - `vit explore skills` — list published skills. Flags: `--tag <tag>`, `--limit <n>`, `--cursor <id>`, `--json` 141 + - `vit explore beacons` — list active beacons. Flags: `--json` 142 + - `vit explore vouches` — list vouches for a cap. Flags: `--cap <uri>` or `--ref <ref>`, `--beacon <beacon>` (narrows ref search), `--json` 143 + - `vit explore stats` — network-wide stats. Flags: `--json` 144 + - All subcommands accept `--explore-url <url>` to override the API endpoint (also via `VIT_EXPLORE_URL` env var). 145 + - Discovery hierarchy: `explore` (indexed, instant) > `scan` (Jetstream replay, slow) > `skim` (followed accounts only). 146 + - Use cases: Find caps by beacon, discover skills by tag, check network health, look up vouches before adopting. 147 + 136 148 ### `vit scan` 137 149 - Description: Discover cap and skill publishers across the network via Jetstream replay. 138 150 - Usage: `vit scan [options]`
+2
src/cli.js
··· 7 7 import registerBeacon from './cmd/beacon.js'; 8 8 import registerConfig from './cmd/config.js'; 9 9 import registerDoctor from './cmd/doctor.js'; 10 + import registerExplore from './cmd/explore.js'; 10 11 import registerInit from './cmd/init.js'; 11 12 import registerLearn from './cmd/learn.js'; 12 13 import registerLogin from './cmd/login.js'; ··· 32 33 registerBeacon(program); 33 34 registerConfig(program); 34 35 registerDoctor(program); 36 + registerExplore(program); 35 37 registerInit(program); 36 38 registerLearn(program); 37 39 registerLogin(program);
+313
src/cmd/explore.js
··· 1 + // SPDX-License-Identifier: MIT 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { DEFAULT_EXPLORE_URL } from '../lib/constants.js'; 5 + import { readProjectConfig } from '../lib/vit-dir.js'; 6 + import { brand } from '../lib/brand.js'; 7 + import { jsonOk, jsonError } from '../lib/json-output.js'; 8 + 9 + function resolveUrl(opts) { 10 + return opts.exploreUrl || process.env.VIT_EXPLORE_URL || DEFAULT_EXPLORE_URL; 11 + } 12 + 13 + function unavailableMessage(baseUrl) { 14 + try { 15 + return `${new URL(baseUrl).host} is unavailable. try 'vit scan' for network-wide discovery or 'vit skim' for your followed accounts.`; 16 + } catch { 17 + return `${baseUrl} is unavailable. try 'vit scan' for network-wide discovery or 'vit skim' for your followed accounts.`; 18 + } 19 + } 20 + 21 + export default function register(program) { 22 + const explore = program 23 + .command('explore') 24 + .description('Query the explore index for caps, skills, beacons, vouches, and stats'); 25 + 26 + explore 27 + .command('caps') 28 + .description('List recent caps from the explore index') 29 + .option('--beacon <beacon>', 'Filter by beacon') 30 + .option('--limit <n>', 'Limit number of caps') 31 + .option('--cursor <id>', 'Pagination cursor') 32 + .option('--json', 'Output as JSON') 33 + .option('--explore-url <url>', 'Explore API base URL') 34 + .action(async (opts) => { 35 + const baseUrl = resolveUrl(opts); 36 + 37 + try { 38 + let beacon = opts.beacon; 39 + if (beacon === '.') { 40 + beacon = readProjectConfig().beacon; 41 + if (!beacon) { 42 + const msg = "no beacon set — run 'vit init' first"; 43 + if (opts.json) { 44 + jsonError(msg); 45 + return; 46 + } 47 + console.error(msg); 48 + process.exitCode = 1; 49 + return; 50 + } 51 + } 52 + 53 + const url = new URL('/api/caps', baseUrl); 54 + if (beacon) url.searchParams.set('beacon', beacon); 55 + if (opts.limit) url.searchParams.set('limit', opts.limit); 56 + if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 57 + 58 + const res = await fetch(url); 59 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 60 + const data = await res.json(); 61 + 62 + if (opts.json) { 63 + jsonOk({ caps: data.caps, cursor: data.cursor }); 64 + return; 65 + } 66 + 67 + console.log(`${brand} explore caps`); 68 + if (!data.caps?.length) { 69 + console.log('no caps found.'); 70 + return; 71 + } 72 + 73 + for (const cap of data.caps) { 74 + console.log(` ${cap.title} (${cap.ref})`); 75 + console.log(` @${cap.handle} ${cap.beacon}`); 76 + console.log(` ${cap.description}`); 77 + } 78 + if (data.cursor) { 79 + console.log(`\nnext: --cursor ${data.cursor}`); 80 + } 81 + } catch (err) { 82 + const msg = err instanceof Error ? err.message : String(err); 83 + const finalMsg = msg.startsWith('explore API returned ') 84 + ? msg 85 + : unavailableMessage(baseUrl); 86 + if (opts.json) { 87 + jsonError(finalMsg); 88 + return; 89 + } 90 + console.error(finalMsg); 91 + process.exitCode = 1; 92 + } 93 + }); 94 + 95 + explore 96 + .command('skills') 97 + .description('List published skills from the explore index') 98 + .option('--tag <tag>', 'Filter by tag') 99 + .option('--limit <n>', 'Limit number of skills') 100 + .option('--cursor <id>', 'Pagination cursor') 101 + .option('--json', 'Output as JSON') 102 + .option('--explore-url <url>', 'Explore API base URL') 103 + .action(async (opts) => { 104 + const baseUrl = resolveUrl(opts); 105 + 106 + try { 107 + const url = new URL('/api/skills', baseUrl); 108 + if (opts.tag) url.searchParams.set('tag', opts.tag); 109 + if (opts.limit) url.searchParams.set('limit', opts.limit); 110 + if (opts.cursor) url.searchParams.set('cursor', opts.cursor); 111 + 112 + const res = await fetch(url); 113 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 114 + const data = await res.json(); 115 + 116 + if (opts.json) { 117 + jsonOk({ skills: data.skills, cursor: data.cursor }); 118 + return; 119 + } 120 + 121 + console.log(`${brand} explore skills`); 122 + if (!data.skills?.length) { 123 + console.log('no skills found.'); 124 + return; 125 + } 126 + 127 + for (const skill of data.skills) { 128 + console.log(` ${skill.name} v${skill.version} (${skill.ref})`); 129 + console.log(` @${skill.handle} ${skill.description}`); 130 + } 131 + if (data.cursor) { 132 + console.log(`\nnext: --cursor ${data.cursor}`); 133 + } 134 + } catch (err) { 135 + const msg = err instanceof Error ? err.message : String(err); 136 + const finalMsg = msg.startsWith('explore API returned ') 137 + ? msg 138 + : unavailableMessage(baseUrl); 139 + if (opts.json) { 140 + jsonError(finalMsg); 141 + return; 142 + } 143 + console.error(finalMsg); 144 + process.exitCode = 1; 145 + } 146 + }); 147 + 148 + explore 149 + .command('beacons') 150 + .description('List active beacons from the explore index') 151 + .option('--json', 'Output as JSON') 152 + .option('--explore-url <url>', 'Explore API base URL') 153 + .action(async (opts) => { 154 + const baseUrl = resolveUrl(opts); 155 + 156 + try { 157 + const url = new URL('/api/beacons', baseUrl); 158 + const res = await fetch(url); 159 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 160 + const data = await res.json(); 161 + 162 + if (opts.json) { 163 + jsonOk({ beacons: data.beacons }); 164 + return; 165 + } 166 + 167 + console.log(`${brand} explore beacons`); 168 + if (!data.beacons?.length) { 169 + console.log('no beacons found.'); 170 + return; 171 + } 172 + 173 + for (const beacon of data.beacons) { 174 + console.log(` ${beacon.name}`); 175 + console.log(` caps: ${beacon.cap_count} vouches: ${beacon.vouch_count} last active: ${beacon.last_activity}`); 176 + } 177 + } catch (err) { 178 + const msg = err instanceof Error ? err.message : String(err); 179 + const finalMsg = msg.startsWith('explore API returned ') 180 + ? msg 181 + : unavailableMessage(baseUrl); 182 + if (opts.json) { 183 + jsonError(finalMsg); 184 + return; 185 + } 186 + console.error(finalMsg); 187 + process.exitCode = 1; 188 + } 189 + }); 190 + 191 + explore 192 + .command('vouches') 193 + .description('List vouches for a cap from the explore index') 194 + .option('--cap <uri>', 'Cap URI') 195 + .option('--ref <ref>', 'Cap ref') 196 + .option('--beacon <beacon>', 'Filter ref lookup by beacon') 197 + .option('--json', 'Output as JSON') 198 + .option('--explore-url <url>', 'Explore API base URL') 199 + .action(async (opts) => { 200 + const baseUrl = resolveUrl(opts); 201 + 202 + try { 203 + if ((!opts.cap && !opts.ref) || (opts.cap && opts.ref)) { 204 + const msg = 'provide --cap <uri> or --ref <ref>'; 205 + if (opts.json) { 206 + jsonError(msg); 207 + return; 208 + } 209 + console.error(msg); 210 + process.exitCode = 1; 211 + return; 212 + } 213 + 214 + let capUri = opts.cap; 215 + if (opts.ref) { 216 + const capsUrl = new URL('/api/caps', baseUrl); 217 + if (opts.beacon) capsUrl.searchParams.set('beacon', opts.beacon); 218 + 219 + const capsRes = await fetch(capsUrl); 220 + if (!capsRes.ok) throw new Error(`explore API returned ${capsRes.status}`); 221 + const capsData = await capsRes.json(); 222 + const match = capsData.caps?.find((cap) => cap.ref === opts.ref); 223 + 224 + if (!match) { 225 + const msg = `no cap found with ref '${opts.ref}'`; 226 + if (opts.json) { 227 + jsonError(msg); 228 + return; 229 + } 230 + console.error(msg); 231 + process.exitCode = 1; 232 + return; 233 + } 234 + 235 + capUri = match.uri; 236 + } 237 + 238 + const url = new URL('/api/vouches', baseUrl); 239 + url.searchParams.set('cap_uri', capUri); 240 + 241 + const res = await fetch(url); 242 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 243 + const data = await res.json(); 244 + 245 + if (opts.json) { 246 + jsonOk({ vouches: data.vouches, cap_uri: capUri }); 247 + return; 248 + } 249 + 250 + console.log(`${brand} explore vouches`); 251 + if (!data.vouches?.length) { 252 + console.log('no vouches found for this cap.'); 253 + return; 254 + } 255 + 256 + for (const vouch of data.vouches) { 257 + const who = vouch.handle ? `@${vouch.handle}` : (vouch.did || 'unknown'); 258 + const createdAt = vouch.created_at || vouch.createdAt || 'unknown'; 259 + const ref = vouch.ref || vouch.cap_ref || vouch.cap_uri || ''; 260 + console.log(` ${who} ${createdAt}`); 261 + if (ref) console.log(` ${ref}`); 262 + } 263 + } catch (err) { 264 + const msg = err instanceof Error ? err.message : String(err); 265 + const finalMsg = msg.startsWith('explore API returned ') 266 + ? msg 267 + : unavailableMessage(baseUrl); 268 + if (opts.json) { 269 + jsonError(finalMsg); 270 + return; 271 + } 272 + console.error(finalMsg); 273 + process.exitCode = 1; 274 + } 275 + }); 276 + 277 + explore 278 + .command('stats') 279 + .description('Show network-wide stats from the explore index') 280 + .option('--json', 'Output as JSON') 281 + .option('--explore-url <url>', 'Explore API base URL') 282 + .action(async (opts) => { 283 + const baseUrl = resolveUrl(opts); 284 + 285 + try { 286 + const url = new URL('/api/stats', baseUrl); 287 + const res = await fetch(url); 288 + if (!res.ok) throw new Error(`explore API returned ${res.status}`); 289 + const data = await res.json(); 290 + 291 + if (opts.json) { 292 + jsonOk(data); 293 + return; 294 + } 295 + 296 + console.log(`${brand} explore stats`); 297 + console.log(` caps: ${data.total_caps} skills: ${data.total_skills}`); 298 + console.log(` vouches: ${data.total_vouches} beacons: ${data.total_beacons}`); 299 + console.log(` active dids: ${data.active_dids} skill publishers: ${data.skill_publishers}`); 300 + } catch (err) { 301 + const msg = err instanceof Error ? err.message : String(err); 302 + const finalMsg = msg.startsWith('explore API returned ') 303 + ? msg 304 + : unavailableMessage(baseUrl); 305 + if (opts.json) { 306 + jsonError(finalMsg); 307 + return; 308 + } 309 + console.error(finalMsg); 310 + process.exitCode = 1; 311 + } 312 + }); 313 + }
+1
src/lib/constants.js
··· 5 5 export const VOUCH_COLLECTION = 'org.v-it.vouch'; 6 6 export const SKILL_COLLECTION = 'org.v-it.skill'; 7 7 export const DEFAULT_JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; 8 + export const DEFAULT_EXPLORE_URL = 'https://explore.v-it.org';
+87
test/explore.test.js
··· 1 + // SPDX-License-Identifier: MIT 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + 7 + describe('vit explore', () => { 8 + test('shows help', () => { 9 + const result = run('explore --help', '/tmp'); 10 + expect(result.exitCode).toBe(0); 11 + expect(result.stdout).toContain('explore'); 12 + }); 13 + 14 + test('stats returns JSON', () => { 15 + const result = run('explore stats --json', '/tmp'); 16 + expect(result.exitCode).toBe(0); 17 + const data = JSON.parse(result.stdout); 18 + expect(data.ok).toBe(true); 19 + expect(typeof data.total_caps).toBe('number'); 20 + }); 21 + 22 + test('caps returns JSON', () => { 23 + const result = run('explore caps --json --limit 2', '/tmp'); 24 + expect(result.exitCode).toBe(0); 25 + const data = JSON.parse(result.stdout); 26 + expect(data.ok).toBe(true); 27 + expect(Array.isArray(data.caps)).toBe(true); 28 + }); 29 + 30 + test('skills returns JSON', () => { 31 + const result = run('explore skills --json --limit 2', '/tmp'); 32 + expect(result.exitCode).toBe(0); 33 + const data = JSON.parse(result.stdout); 34 + expect(data.ok).toBe(true); 35 + expect(Array.isArray(data.skills)).toBe(true); 36 + }); 37 + 38 + test('beacons returns JSON', () => { 39 + const result = run('explore beacons --json', '/tmp'); 40 + expect(result.exitCode).toBe(0); 41 + const data = JSON.parse(result.stdout); 42 + expect(data.ok).toBe(true); 43 + expect(Array.isArray(data.beacons)).toBe(true); 44 + }); 45 + 46 + test('graceful error on unreachable URL', () => { 47 + const result = run('explore stats --explore-url http://localhost:1 --json', '/tmp'); 48 + expect(result.exitCode).not.toBe(0); 49 + const data = JSON.parse(result.stdout); 50 + expect(data.ok).toBe(false); 51 + expect(data.error).toContain('unavailable'); 52 + }); 53 + 54 + test('graceful error on invalid URL', () => { 55 + const result = run('explore stats --explore-url not-a-url --json', '/tmp'); 56 + expect(result.exitCode).not.toBe(0); 57 + const data = JSON.parse(result.stdout); 58 + expect(data.ok).toBe(false); 59 + expect(data.error).toContain('unavailable'); 60 + }); 61 + 62 + test('vouches requires --cap or --ref', () => { 63 + const result = run('explore vouches --json', '/tmp'); 64 + expect(result.exitCode).not.toBe(0); 65 + const data = JSON.parse(result.stdout); 66 + expect(data.ok).toBe(false); 67 + }); 68 + 69 + test('env var override works', () => { 70 + const result = run('explore stats --json', '/tmp', { VIT_EXPLORE_URL: 'http://localhost:1' }); 71 + expect(result.exitCode).not.toBe(0); 72 + const data = JSON.parse(result.stdout); 73 + expect(data.ok).toBe(false); 74 + expect(data.error).toContain('unavailable'); 75 + }); 76 + 77 + test('flag overrides env var', () => { 78 + const result = run( 79 + 'explore stats --json --explore-url https://explore.v-it.org', 80 + '/tmp', 81 + { VIT_EXPLORE_URL: 'http://localhost:1' }, 82 + ); 83 + expect(result.exitCode).toBe(0); 84 + const data = JSON.parse(result.stdout); 85 + expect(data.ok).toBe(true); 86 + }); 87 + });