open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { DEFAULT_EXPLORE_URL } from '../lib/constants.js';
5import { readBeaconSet } from '../lib/vit-dir.js';
6import { brand } from '../lib/brand.js';
7import { jsonOk, jsonError } from '../lib/json-output.js';
8
9function timeAgo(isoString) {
10 const seconds = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000);
11 if (seconds < 60) return `${seconds}s ago`;
12 const minutes = Math.floor(seconds / 60);
13 if (minutes < 60) return `${minutes}m ago`;
14 const hours = Math.floor(minutes / 60);
15 if (hours < 24) return `${hours}h ago`;
16 const days = Math.floor(hours / 24);
17 return `${days}d ago`;
18}
19
20function resolveUrl(opts) {
21 return opts.exploreUrl || process.env.VIT_EXPLORE_URL || DEFAULT_EXPLORE_URL;
22}
23
24function unavailableMessage(baseUrl) {
25 try {
26 return `${new URL(baseUrl).host} is unavailable. try 'vit explore caps --beacon .' for network-wide discovery.`;
27 } catch {
28 return `${baseUrl} is unavailable. try 'vit explore caps --beacon .' for network-wide discovery.`;
29 }
30}
31
32export default function register(program) {
33 program
34 .command('inbox')
35 .description('Show caps addressed to your project beacon (project-centric view)')
36 .option('--kind <kind>', 'Filter by cap kind (e.g. request)')
37 .option('--sort <sort>', 'Sort order: recent (default) or want-vouches', 'recent')
38 .option('--limit <n>', 'Limit number of caps')
39 .option('--json', 'Output as JSON')
40 .option('--explore-url <url>', 'Explore API base URL')
41 .action(async (opts) => {
42 try {
43 const beaconSet = readBeaconSet();
44 if (beaconSet.size === 0) {
45 const msg = "no beacon set — run 'vit init' first (inbox requires a project beacon)";
46 if (opts.json) {
47 jsonError(msg);
48 return;
49 }
50 console.error(msg);
51 process.exitCode = 1;
52 return;
53 }
54
55 const beacon = [...beaconSet].join(',');
56 const baseUrl = resolveUrl(opts);
57
58 let data;
59 try {
60 const url = new URL('/api/caps', baseUrl);
61 url.searchParams.set('beacon', beacon);
62 if (opts.kind) url.searchParams.set('kind', opts.kind);
63 if (opts.sort === 'want-vouches') url.searchParams.set('sort', 'want-vouches');
64 if (opts.limit) url.searchParams.set('limit', opts.limit);
65
66 const res = await fetch(url);
67 if (!res.ok) throw new Error(`explore API returned ${res.status}`);
68 data = await res.json();
69 } catch (err) {
70 const msg = err instanceof Error ? err.message : String(err);
71 const finalMsg = msg.startsWith('explore API returned ')
72 ? msg
73 : unavailableMessage(baseUrl);
74 if (opts.json) {
75 jsonError(finalMsg);
76 return;
77 }
78 console.error(finalMsg);
79 console.error("fallback: try 'vit explore caps --beacon .' to query the explore index directly.");
80 process.exitCode = 1;
81 return;
82 }
83
84 if (opts.json) {
85 jsonOk({ caps: data.caps || [], cursor: data.cursor || null });
86 return;
87 }
88
89 const caps = data.caps || [];
90 const beaconDisplay = [...beaconSet][0];
91
92 console.log(`${brand} inbox — ${beaconDisplay}`);
93 console.log('');
94
95 if (caps.length === 0) {
96 const kindFilter = opts.kind ? ` (kind: ${opts.kind})` : '';
97 console.log(`no caps found${kindFilter}.`);
98 if (opts.kind) {
99 console.log(`hint: to request a feature, run 'vit ship --kind request --beacon <url>'`);
100 }
101 return;
102 }
103
104 for (const cap of caps) {
105 const wantCount = cap.want_vouch_count ?? 0;
106 const wantStr = wantCount === 1 ? '1 want' : `${wantCount} wants`;
107 const age = cap.created_at ? timeAgo(cap.created_at) : '';
108 const handle = cap.handle ? `@${cap.handle}` : cap.did || 'unknown';
109 const kind = cap.kind || (cap.record_json ? (() => { try { return JSON.parse(cap.record_json).kind || ''; } catch { return ''; } })() : '');
110
111 console.log(` ${cap.ref} ${handle} ${wantStr} ${age}`);
112 if (cap.title) console.log(` ${cap.title}${kind ? ` [${kind}]` : ''}`);
113 if (cap.description) console.log(` ${cap.description}`);
114 console.log('');
115 }
116
117 const kindNote = opts.kind ? ` ${opts.kind}` : '';
118 console.log(`${caps.length} open${kindNote} cap${caps.length === 1 ? '' : 's'}`);
119 console.log(`tip: 'vit vouch <ref> --kind want' to signal demand`);
120 console.log(` 'vit ship --recap <ref>' to ship an implementation`);
121 } catch (err) {
122 const msg = err instanceof Error ? err.message : String(err);
123 if (opts.json) {
124 jsonError(msg);
125 return;
126 }
127 console.error(msg);
128 process.exitCode = 1;
129 }
130 });
131}