open source is social v-it.org
0
fork

Configure Feed

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

Merge branch 'hopper-h65as5ji-vit-ship-agent'

+177 -30
+19 -5
skills/vit/COMMANDS.md
··· 136 136 Example: 137 137 - `vit following` 138 138 139 - ### `vit ship <text>` 140 - Usage: `vit ship <text> --title <title> --description <description> --ref <ref>` 139 + ### `vit ship` 140 + Usage: `vit ship --title <title> --description <description> --ref <ref> [--recap <ref>]` 141 + 142 + Input: 143 + - Body is required on stdin (pipe or heredoc). 141 144 142 145 Options: 143 146 - `-v, --verbose` - Show step-by-step details ··· 145 148 - `--title <title>` (required) 146 149 - `--description <description>` (required) 147 150 - `--ref <ref>` (required) - must match `^[a-z]+-[a-z]+-[a-z]+$` 151 + - `--recap <ref>` (optional) - ref to derive from; must match `^[a-z]+-[a-z]+-[a-z]+$` 152 + 153 + Gate: 154 + - Agent-only (`requireAgent()`). 148 155 149 156 Output format: 150 157 - JSON object on success with request/response metadata. 151 158 152 159 Error conditions: 160 + - Not running in an agent context (`requireAgent()` gate). 153 161 - No DID configured. 154 162 - Missing required options. 155 - - Invalid ref format. 163 + - Missing stdin body. 164 + - Invalid `--ref` or `--recap` format. 165 + - `--recap` target not found. 156 166 - Session restore/auth failure. 157 167 158 168 Examples: 159 - - `vit ship "add retry helper" --title "Retry helper" --description "Adds bounded retry utility" --ref retry-helper-utility` 160 - - `vit ship "patch" --title "Patch" --description "Details" --ref fast-cache-invalidation --did did:plc:example` 169 + - `vit ship --title "Retry helper" --description "Adds bounded retry utility" --ref retry-helper-utility <<'EOF'` 170 + - `Add bounded retry utility for transient request failures.` 171 + - `EOF` 172 + - `vit ship --title "Patch" --description "Details" --ref fast-cache-invalidation --recap original-cache-ref --did did:plc:example <<'EOF'` 173 + - `Patch details...` 174 + - `EOF` 161 175 162 176 ### `vit beacon <target>` 163 177 Usage: `vit beacon <target>`
+8 -6
skills/vit/SKILL.md
··· 21 21 1. Run `vit init` to initialize `.vit/` directory (derives beacon from git remotes). 22 22 2. Run `vit follow <handle>` to follow accounts whose caps you want to see. 23 23 3. Run `vit skim --json` to read caps from followed accounts filtered by beacon. 24 - 4. Run `vit ship <text> --title <t> --description <d> --ref <ref>` to publish a cap. 24 + 4. Run `vit ship --title <t> --description <d> --ref <ref> <<'EOF' ... EOF` to publish a cap (body on stdin). 25 25 26 26 Handoffs: 27 27 - If no DID is configured, tell the user to run `vit login <handle>`. ··· 89 89 - Output: `handle (did)` lines or `no followings`. 90 90 - Common errors: malformed following file content. 91 91 92 - ### `vit ship <text>` 93 - - Description: Publish a cap to ATProto. 94 - - Usage: `vit ship <text> --title <title> --description <description> --ref <ref>` 95 - - Key flags: required `--title <title>`, `--description <description>`, `--ref <ref>`; optional `--did <did>`, `-v, --verbose` 92 + ### `vit ship` 93 + - Description: Publish a cap to ATProto from stdin body input. 94 + - Usage: `vit ship --title <title> --description <description> --ref <ref> [--recap <ref>] <<'EOF' ... EOF` 95 + - Key flags: required `--title <title>`, `--description <description>`, `--ref <ref>`; optional `--recap <ref>`, `--did <did>`, `-v, --verbose` 96 + - Input: cap body is required via stdin (pipe or heredoc). 97 + - Gate: agent-only (`requireAgent()`). 96 98 - Output: JSON object on success. 97 - - Common errors: no DID, invalid ref, session expired. 99 + - Common errors: not running in an agent context, missing stdin body, no DID, invalid ref, recap ref not found, session expired. 98 100 99 101 ### `vit beacon <target>` 100 102 - Description: Probe a remote repo and report whether its beacon is lit.
+120 -11
src/cmd/ship.js
··· 2 2 // Copyright (c) 2026 sol pbc 3 3 4 4 import { TID } from '@atproto/common-web'; 5 + import { readFileSync } from 'node:fs'; 5 6 import { CAP_COLLECTION } from '../lib/constants.js'; 6 - import { loadConfig } from '../lib/config.js'; 7 + import { requireAgent } from '../lib/agent.js'; 8 + import { requireDid } from '../lib/config.js'; 7 9 import { restoreAgent } from '../lib/oauth.js'; 8 - import { appendLog, readProjectConfig } from '../lib/vit-dir.js'; 9 - import { REF_PATTERN } from '../lib/cap-ref.js'; 10 + import { appendLog, readProjectConfig, readLog, readFollowing } from '../lib/vit-dir.js'; 11 + import { REF_PATTERN, resolveRef } from '../lib/cap-ref.js'; 10 12 11 13 export default function register(program) { 12 14 program 13 15 .command('ship') 14 - .argument('<text>', 'Cap text to publish') 15 16 .description('Publish a cap to your feed') 16 17 .option('-v, --verbose', 'Show step-by-step details') 17 18 .option('--did <did>', 'DID to use (reads saved DID from config if not provided)') 18 19 .requiredOption('--title <title>', 'Short title for the cap') 19 20 .requiredOption('--description <description>', 'Description of the cap') 20 21 .requiredOption('--ref <ref>', 'Three lowercase words with dashes (e.g. fast-cache-invalidation)') 21 - .action(async (text, opts) => { 22 + .option('--recap <ref>', 'Ref of the cap this derives from (quote-post semantics)') 23 + .action(async (opts) => { 22 24 try { 23 - const { verbose } = opts; 24 - const did = opts.did || loadConfig().did; 25 - if (!did) { 26 - console.error("No DID configured. Run 'vit login <handle>' first or pass --did."); 25 + const gate = requireAgent(); 26 + if (!gate.ok) { 27 + console.error('vit ship should be run by a coding agent (e.g. claude code, gemini cli).'); 28 + console.error("open your agent and ask it to run 'vit ship' for you."); 27 29 process.exitCode = 1; 28 30 return; 29 31 } 32 + 33 + const { verbose } = opts; 34 + const did = requireDid(opts); 35 + if (!did) return; 30 36 if (verbose) console.log(`[verbose] DID: ${did}`); 31 37 38 + let text; 39 + try { 40 + text = readFileSync('/dev/stdin', 'utf-8').trim(); 41 + } catch { 42 + text = ''; 43 + } 44 + if (!text) { 45 + console.error('error: cap body is required via stdin (pipe or heredoc)'); 46 + process.exitCode = 1; 47 + return; 48 + } 49 + 32 50 if (!REF_PATTERN.test(opts.ref)) { 33 51 console.error('error: --ref must be exactly three lowercase words separated by dashes (e.g. fast-cache-invalidation)'); 34 52 process.exitCode = 1; 35 53 return; 36 54 } 55 + 56 + let recapUri = null; 57 + if (opts.recap) { 58 + if (!REF_PATTERN.test(opts.recap)) { 59 + console.error('error: --recap must be exactly three lowercase words separated by dashes (e.g. fast-cache-invalidation)'); 60 + process.exitCode = 1; 61 + return; 62 + } 63 + 64 + const caps = readLog('caps.jsonl'); 65 + const localMatch = caps.find(e => e.ref === opts.recap); 66 + if (localMatch) { 67 + recapUri = localMatch.uri; 68 + if (verbose) console.log(`[verbose] recap resolved locally: ${recapUri}`); 69 + } 70 + } 71 + 37 72 const now = new Date().toISOString(); 38 73 39 74 const { agent, session } = await restoreAgent(did); 40 75 if (verbose) console.log(`[verbose] Session restored, PDS: ${session.serverMetadata?.issuer}`); 41 76 42 - const record = { $type: CAP_COLLECTION, text, title: opts.title, description: opts.description, ref: opts.ref, createdAt: now }; 77 + if (opts.recap && !recapUri) { 78 + const following = readFollowing(); 79 + const dids = following.map(e => e.did); 80 + dids.push(did); 81 + if (verbose) console.log(`[verbose] recap: querying ${dids.length} accounts`); 82 + 83 + let match = null; 84 + for (const repoDid of dids) { 85 + try { 86 + const res = await agent.com.atproto.repo.listRecords({ 87 + repo: repoDid, 88 + collection: CAP_COLLECTION, 89 + limit: 50, 90 + }); 91 + for (const rec of res.data.records) { 92 + const recRef = resolveRef(rec.value, rec.cid); 93 + if (recRef === opts.recap) { 94 + if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 95 + match = rec; 96 + } 97 + } 98 + } 99 + } catch (err) { 100 + if (verbose) console.log(`[verbose] ${repoDid}: error fetching caps: ${err.message}`); 101 + } 102 + } 103 + 104 + if (match) { 105 + recapUri = match.uri; 106 + if (verbose) console.log(`[verbose] recap resolved remotely: ${recapUri}`); 107 + } else { 108 + console.error(`error: could not find cap with ref '${opts.recap}' to recap`); 109 + process.exitCode = 1; 110 + return; 111 + } 112 + } 113 + 114 + const record = { 115 + $type: CAP_COLLECTION, 116 + text, 117 + title: opts.title, 118 + description: opts.description, 119 + ref: opts.ref, 120 + createdAt: now, 121 + }; 43 122 const projectConfig = readProjectConfig(); 44 123 if (projectConfig.beacon) record.beacon = projectConfig.beacon; 124 + if (opts.recap) record.recap = { uri: recapUri, ref: opts.recap }; 45 125 if (verbose && projectConfig.beacon) console.log(`[verbose] Beacon: ${projectConfig.beacon}`); 46 126 const rkey = TID.nextStr(); 47 127 if (verbose) console.log(`[verbose] Record built, rkey: ${rkey}`); ··· 59 139 ts: now, 60 140 did, 61 141 rkey, 142 + ref: opts.ref, 62 143 collection: CAP_COLLECTION, 63 144 pds: session.serverMetadata?.issuer, 64 145 uri: putRes.data.uri, ··· 81 162 console.error(err instanceof Error ? err.message : String(err)); 82 163 process.exitCode = 1; 83 164 } 84 - }); 165 + }) 166 + .addHelpText('after', ` 167 + Authoring guidance (for coding agents): 168 + 169 + Fields: 170 + --title Short name for the cap (2-5 words) 171 + --description One sentence explaining what this cap does 172 + --ref Three lowercase words with dashes (your-ref-name) 173 + --recap <ref> Optional. Ref of the cap this derives from (links back to original) 174 + body (stdin) Full cap content, piped or via heredoc 175 + 176 + What makes a good cap: 177 + - Title is a concise noun phrase: "Fast LRU Cache", "JWT Auth Middleware" 178 + - Description explains the value: "Thread-safe LRU cache with O(1) eviction" 179 + - Body contains the complete, self-contained capability text 180 + - Ref is a memorable three-word slug for discovery 181 + 182 + When to use --recap: 183 + Use --recap when this cap is derived from another cap (e.g. after vit remix). 184 + It creates a link back to the original, like a quote-post. 185 + 186 + Example: 187 + vit ship --title "Fast LRU Cache" \\ 188 + --description "Thread-safe LRU cache with O(1) eviction" \\ 189 + --ref "fast-lru-cache" \\ 190 + --recap "original-cache-ref" \\ 191 + <<'EOF' 192 + ... full cap body text ... 193 + EOF`); 85 194 }
+2 -1
test/helpers.js
··· 6 6 7 7 const vitBin = join(import.meta.dir, '..', 'bin', 'vit.js'); 8 8 9 - export function run(args, cwd, env) { 9 + export function run(args, cwd, env, input) { 10 10 try { 11 11 return { 12 12 stdout: execSync(`bun ${vitBin} ${args}`, { ··· 15 15 timeout: 30000, 16 16 stdio: ['pipe', 'pipe', 'pipe'], 17 17 env: env ? { ...process.env, ...env } : undefined, 18 + input, 18 19 }).trim(), 19 20 exitCode: 0, 20 21 };
+28 -7
test/ship.test.js
··· 4 4 import { describe, test, expect } from 'bun:test'; 5 5 import { run } from './helpers.js'; 6 6 7 + const agentEnv = { CLAUDECODE: '1' }; 8 + 7 9 describe('vit ship', () => { 10 + test('rejects when run outside a coding agent', () => { 11 + const r = run('ship --title "Hi" --description "desc" --ref "one-two-three"', '/tmp', { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }, 'body text'); 12 + expect(r.exitCode).not.toBe(0); 13 + expect(r.stderr).toContain('should be run by a coding agent'); 14 + }); 15 + 8 16 test('fails when --title is missing', () => { 9 - const r = run('ship "hello" --description "desc" --ref "one-two-three"'); 17 + const r = run('ship --description "desc" --ref "one-two-three"'); 10 18 expect(r.exitCode).not.toBe(0); 11 19 expect(r.stderr).toMatch(/--title/i); 12 20 }); 13 21 14 22 test('fails when --description is missing', () => { 15 - const r = run('ship "hello" --title "Hi" --ref "one-two-three"'); 23 + const r = run('ship --title "Hi" --ref "one-two-three"'); 16 24 expect(r.exitCode).not.toBe(0); 17 25 expect(r.stderr).toMatch(/--description/i); 18 26 }); 19 27 20 28 test('fails when --ref is missing', () => { 21 - const r = run('ship "hello" --title "Hi" --description "desc"'); 29 + const r = run('ship --title "Hi" --description "desc"'); 22 30 expect(r.exitCode).not.toBe(0); 23 31 expect(r.stderr).toMatch(/--ref/i); 24 32 }); 25 33 34 + test('fails when stdin body is empty', () => { 35 + const r = run('ship --title "Hi" --description "desc" --ref "one-two-three" --did "did:plc:abc"', undefined, agentEnv, ''); 36 + expect(r.exitCode).not.toBe(0); 37 + expect(r.stderr).toMatch(/body is required/i); 38 + }); 39 + 26 40 test('rejects ref with uppercase letters', () => { 27 - const r = run('ship "hello" --title "Hi" --description "desc" --ref "Bad-Ref-Here"'); 41 + const r = run('ship --title "Hi" --description "desc" --ref "Bad-Ref-Here" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 28 42 expect(r.exitCode).not.toBe(0); 29 43 expect(r.stderr).toMatch(/three lowercase words/i); 30 44 }); 31 45 32 46 test('rejects ref with wrong segment count', () => { 33 - const r = run('ship "hello" --title "Hi" --description "desc" --ref "only-two"'); 47 + const r = run('ship --title "Hi" --description "desc" --ref "only-two" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 34 48 expect(r.exitCode).not.toBe(0); 35 49 expect(r.stderr).toMatch(/three lowercase words/i); 36 50 }); 37 51 38 52 test('rejects ref with digits', () => { 39 - const r = run('ship "hello" --title "Hi" --description "desc" --ref "has-num-3bers"'); 53 + const r = run('ship --title "Hi" --description "desc" --ref "has-num-3bers" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 40 54 expect(r.exitCode).not.toBe(0); 41 55 expect(r.stderr).toMatch(/three lowercase words/i); 42 56 }); 43 57 58 + test('rejects --recap with invalid ref format', () => { 59 + const r = run('ship --title "Hi" --description "desc" --ref "one-two-three" --recap "BAD" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 60 + expect(r.exitCode).not.toBe(0); 61 + expect(r.stderr).toMatch(/--recap must be exactly three lowercase words/i); 62 + }); 63 + 44 64 test('accepts valid ref format (fails at auth, not validation)', () => { 45 - const r = run('ship "hello" --title "Hi" --description "desc" --ref "one-two-three" --did "did:plc:abc"'); 65 + const r = run('ship --title "Hi" --description "desc" --ref "one-two-three" --did "did:plc:abc"', undefined, agentEnv, 'body text'); 46 66 expect(r.exitCode).not.toBe(0); 47 67 expect(r.stderr).not.toMatch(/three lowercase words/i); 68 + expect(r.stderr).not.toMatch(/body is required/i); 48 69 }); 49 70 });