overlord-cli 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,553 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Agent bundle setup commands (setup / doctor).
5
+ *
6
+ * Installs durable Overlord workflow configuration for Claude Code and OpenCode
7
+ * into their respective config directories.
8
+ */
9
+
10
+ import crypto from 'node:crypto';
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+
15
+ const BUNDLE_VERSION = '1.5.0';
16
+ const MD_MARKER_START = '<!-- overlord:managed:start -->';
17
+ const MD_MARKER_END = '<!-- overlord:managed:end -->';
18
+ const MANIFEST_DIR = path.join(os.homedir(), '.ovld');
19
+ const MANIFEST_FILE = path.join(MANIFEST_DIR, 'bundle-manifest.json');
20
+
21
+ const supportedAgents = ['claude', 'opencode'];
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Templates (same content as electron/services/agent-bundle/templates.ts)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const CLAUDE_SKILL_CONTENT = `---
28
+ name: overlord-local
29
+ description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work.
30
+ ---
31
+
32
+ # Overlord Local Workflow
33
+
34
+ If you receive a prompt with a specified ticket ID, adhere to the following. If the prompt does not have a ticket ID, the user may choose to add one later, but otherwise, proceed without it.
35
+
36
+ ## Lifecycle
37
+
38
+ 1. **Attach first** — Always call attach before doing any work:
39
+ \`\`\`bash
40
+ ovld protocol attach --ticket-id $TICKET_ID
41
+ \`\`\`
42
+ Store \`session.sessionKey\` from the response — it is required for all subsequent calls.
43
+
44
+ 2. **Update during work** — Post at least one progress update before delivering:
45
+ \`\`\`bash
46
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
47
+ \`\`\`
48
+ Phases: \`draft\`, \`execute\`, \`review\`, \`deliver\`, \`complete\`, \`blocked\`, \`cancelled\`.
49
+ Use \`execute\` while working.
50
+
51
+ Pass \`--event-type <type>\` to publish a specific activity event (default: \`update\`):
52
+ - \`update\` — standard progress update (default)
53
+ - \`user_follow_up\` — a message or question from the human user (EXCLUDING THE INITIAL TICKET)
54
+ - \`alert\` — surface a warning or non-blocking alert
55
+
56
+ 3. **Ask when blocked** — Stop working after calling:
57
+ \`\`\`bash
58
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
59
+ \`\`\`
60
+
61
+ 4. **Deliver last** — Always deliver when done:
62
+ \`\`\`bash
63
+ ovld protocol deliver --session-key <sessionKey> \\\\
64
+ --ticket-id $TICKET_ID \\\\
65
+ --summary "Narrative: what you did, next steps." \\\\
66
+ --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
67
+ --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
68
+ \`\`\`
69
+
70
+ For larger or quote-sensitive deliveries, prefer a single JSON file:
71
+ \`\`\`bash
72
+ ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --payload-file ./deliver.json
73
+ \`\`\`
74
+
75
+ ## Change Rationales
76
+
77
+ Always include \`changeRationales\` when delivering. Optionally include them on updates during long-running work.
78
+
79
+ Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact.
80
+
81
+ These are structured protocol payloads that Overlord stores as first-class rows in the \`file_changes\` table. Prefer inline JSON or the dedicated command below. For quote-sensitive deliveries, prefer \`--payload-file\` so summary, artifacts, and change rationales stay in one JSON document. Ordinary deliver artifacts should use \`next_steps\`, \`test_results\`, \`migration\`, \`note\`, \`url\`, or \`decision\`.
82
+
83
+ \`\`\`bash
84
+ ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
85
+ --summary "Recorded rationale details for the latest code changes." --phase execute \\\\
86
+ --change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
87
+ \`\`\`
88
+
89
+ Record only meaningful behavioral changes — skip formatting-only noise. Prefer 1–5 concise rationales per ticket, each tied to a specific file and diff hunk.
90
+
91
+ ## Context & Artifacts
92
+
93
+ \`\`\`bash
94
+ ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
95
+ ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
96
+ ovld protocol artifact-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --file ./spec.pdf --content-type application/pdf
97
+ \`\`\`
98
+
99
+ ## Rules
100
+
101
+ - Always attach first; always deliver when done.
102
+ - Post at least one update before delivering.
103
+ - If blocked on human-only work, call \`ask\` and request a follow-up human ticket.
104
+ - The \`summary\` in deliver is what the PM reads first — write it as a narrative, not a command list.
105
+ - Use \`write-context\` for facts a future agent session should know.
106
+ - **If the user sends you a message during your session, immediately publish a \`user_follow_up\` activity event with the user's message recorded verbatim in the summary before doing anything else. This DOES NOT apply to the initial ticket.**
107
+ `;
108
+
109
+ const OPENCODE_AGENTS_SECTION = `# Overlord Local Workflow
110
+
111
+ If you receive a prompt with a specified ticket ID, adhere to the following. If the prompt does not have a ticket ID, the user may choose to add one later, but otherwise, proceed without it.
112
+
113
+ ## Lifecycle
114
+
115
+ 1. **Attach first** — Always call attach before doing any work:
116
+ \`\`\`bash
117
+ ovld protocol attach --ticket-id $TICKET_ID
118
+ \`\`\`
119
+ Store \`session.sessionKey\` from the response — it is required for all subsequent calls.
120
+
121
+ 2. **Update during work** — Post at least one progress update before delivering:
122
+ \`\`\`bash
123
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
124
+ \`\`\`
125
+ Phases: \`draft\`, \`execute\`, \`review\`, \`deliver\`, \`complete\`, \`blocked\`, \`cancelled\`.
126
+ Use \`execute\` while working.
127
+
128
+ Pass \`--event-type <type>\` for activity events: \`update\`, \`user_follow_up\`, \`alert\`.
129
+
130
+ 3. **Ask when blocked** — Stop working after calling:
131
+ \`\`\`bash
132
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
133
+ \`\`\`
134
+
135
+ 4. **Deliver last** — Always deliver when done:
136
+ \`\`\`bash
137
+ ovld protocol deliver --session-key <sessionKey> \\\\
138
+ --ticket-id $TICKET_ID \\\\
139
+ --summary "Narrative: what you did, next steps." \\\\
140
+ --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
141
+ --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
142
+ \`\`\`
143
+
144
+ ## Change Rationales
145
+
146
+ Always include \`changeRationales\` when delivering. Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact. Record only meaningful behavioral changes. Overlord stores these as structured rows in the \`file_changes\` table.
147
+
148
+ \`\`\`bash
149
+ ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
150
+ --summary "Recorded rationale details for the latest code changes." --phase execute \\\\
151
+ --change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
152
+ \`\`\`
153
+
154
+ ## Context & Artifacts
155
+
156
+ \`\`\`bash
157
+ ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
158
+ ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
159
+ ovld protocol artifact-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --file ./spec.pdf --content-type application/pdf
160
+ \`\`\`
161
+
162
+ ## Rules
163
+
164
+ - Always attach first; always deliver when done.
165
+ - Post at least one update before delivering.
166
+ - If blocked on human-only work, call \`ask\` and request a follow-up human ticket.
167
+ - The \`summary\` in deliver is what the PM reads first — write it as a narrative.
168
+ - Use \`write-context\` for facts a future agent session should know.
169
+ - **If the user sends you a message during your session, immediately publish a \`user_follow_up\` activity event with the user's message recorded verbatim in the summary before doing anything else. This DOES NOT apply to the initial ticket.**
170
+ `;
171
+
172
+ const PERMISSION_HOOK_SCRIPT = `#!/bin/bash
173
+ # Overlord PermissionRequest notification hook (managed by Overlord)
174
+ BODY=$(cat -)
175
+ if [ -n "$OVERLORD_URL" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$TICKET_ID" ]; then
176
+ curl -sf -m 5 \\
177
+ -X POST "$OVERLORD_URL/api/protocol/permission-request?ticketId=$TICKET_ID" \\
178
+ -H "Authorization: Bearer $AGENT_TOKEN" \\
179
+ -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \\
180
+ -H "Content-Type: application/json" \\
181
+ -d "$BODY" \\
182
+ >/dev/null 2>&1 &
183
+ disown
184
+ fi
185
+ exit 0
186
+ `;
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Helpers
190
+ // ---------------------------------------------------------------------------
191
+
192
+ function contentHash(content) {
193
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
194
+ }
195
+
196
+ function backupFile(filePath) {
197
+ if (!fs.existsSync(filePath)) return null;
198
+ const dir = path.dirname(filePath);
199
+ const ext = path.extname(filePath);
200
+ const base = path.basename(filePath, ext);
201
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
202
+ const backupPath = path.join(dir, `${base}.backup-${ts}${ext}`);
203
+ fs.copyFileSync(filePath, backupPath);
204
+ return backupPath;
205
+ }
206
+
207
+ function readJsonFile(filePath) {
208
+ try {
209
+ const raw = fs.readFileSync(filePath, 'utf-8');
210
+ const parsed = JSON.parse(raw);
211
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
212
+ return {};
213
+ } catch {
214
+ return {};
215
+ }
216
+ }
217
+
218
+ function writeJsonFile(filePath, data) {
219
+ const dir = path.dirname(filePath);
220
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
221
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
222
+ }
223
+
224
+ function readTextFile(filePath) {
225
+ try {
226
+ return fs.readFileSync(filePath, 'utf-8');
227
+ } catch {
228
+ return '';
229
+ }
230
+ }
231
+
232
+ function writeTextFile(filePath, content, mode) {
233
+ const dir = path.dirname(filePath);
234
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
235
+ const options = { encoding: 'utf-8' };
236
+ if (mode !== undefined) options.mode = mode;
237
+ fs.writeFileSync(filePath, content, options);
238
+ }
239
+
240
+ function deepClone(obj) {
241
+ return JSON.parse(JSON.stringify(obj));
242
+ }
243
+
244
+ function mergeMarkdownSection(existing, newContent) {
245
+ const wrappedContent = `${MD_MARKER_START}\n${newContent.trim()}\n${MD_MARKER_END}`;
246
+ const startIdx = existing.indexOf(MD_MARKER_START);
247
+ const endIdx = existing.indexOf(MD_MARKER_END);
248
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
249
+ const before = existing.slice(0, startIdx);
250
+ const after = existing.slice(endIdx + MD_MARKER_END.length);
251
+ return `${before}${wrappedContent}${after}`;
252
+ }
253
+ const trimmed = existing.trimEnd();
254
+ if (trimmed.length === 0) return wrappedContent + '\n';
255
+ return `${trimmed}\n\n${wrappedContent}\n`;
256
+ }
257
+
258
+ function readManifest() {
259
+ return readJsonFile(MANIFEST_FILE);
260
+ }
261
+ function writeManifest(manifest) {
262
+ writeJsonFile(MANIFEST_FILE, manifest);
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Install
267
+ // ---------------------------------------------------------------------------
268
+
269
+ function claudePaths() {
270
+ const base = path.join(os.homedir(), '.claude');
271
+ return {
272
+ skillDir: path.join(base, 'skills', 'overlord-local'),
273
+ skillFile: path.join(base, 'skills', 'overlord-local', 'SKILL.md'),
274
+ settingsFile: path.join(base, 'settings.json'),
275
+ hookScript: path.join(base, 'overlord-permission-hook.sh')
276
+ };
277
+ }
278
+
279
+ function openCodePaths() {
280
+ const base = path.join(os.homedir(), '.config', 'opencode');
281
+ return {
282
+ agentsFile: path.join(base, 'AGENTS.md'),
283
+ configFile: path.join(base, 'opencode.json'),
284
+ commandsDir: path.join(base, 'commands')
285
+ };
286
+ }
287
+
288
+ function installClaude() {
289
+ const paths = claudePaths();
290
+ const backups = [];
291
+
292
+ // 1. Install skill file
293
+ writeTextFile(paths.skillFile, CLAUDE_SKILL_CONTENT);
294
+ console.log(` ✓ Installed skill: ${paths.skillFile}`);
295
+
296
+ // 2. Install permission hook
297
+ writeTextFile(paths.hookScript, PERMISSION_HOOK_SCRIPT, 0o755);
298
+ console.log(` ✓ Installed hook: ${paths.hookScript}`);
299
+
300
+ // 3. Merge hook into settings.json
301
+ const backup = backupFile(paths.settingsFile);
302
+ if (backup) {
303
+ backups.push(backup);
304
+ console.log(` ✓ Backed up: ${paths.settingsFile} → ${path.basename(backup)}`);
305
+ }
306
+
307
+ const existingSettings = readJsonFile(paths.settingsFile);
308
+ const overlordHook = {
309
+ matcher: '.*',
310
+ hooks: [{ type: 'command', command: paths.hookScript }]
311
+ };
312
+
313
+ const existingHooks = existingSettings.hooks ?? {};
314
+ const existingPermHooks = Array.isArray(existingHooks.PermissionRequest)
315
+ ? existingHooks.PermissionRequest
316
+ : [];
317
+
318
+ // Remove existing Overlord hooks
319
+ const filteredPermHooks = existingPermHooks.filter(hook => {
320
+ if (hook && typeof hook === 'object' && hook.hooks) {
321
+ return !hook.hooks.some(
322
+ inner =>
323
+ typeof inner.command === 'string' && inner.command.includes('overlord-permission-hook')
324
+ );
325
+ }
326
+ return true;
327
+ });
328
+
329
+ const merged = deepClone(existingSettings);
330
+ merged.hooks = { ...existingHooks, PermissionRequest: [...filteredPermHooks, overlordHook] };
331
+ merged.__overlord_managed = {
332
+ version: BUNDLE_VERSION,
333
+ paths: ['hooks.PermissionRequest'],
334
+ updatedAt: new Date().toISOString()
335
+ };
336
+ writeJsonFile(paths.settingsFile, merged);
337
+ console.log(` ✓ Merged hook into: ${paths.settingsFile}`);
338
+
339
+ // 4. Update manifest
340
+ const manifest = readManifest();
341
+ manifest.claude = {
342
+ version: BUNDLE_VERSION,
343
+ contentHash: contentHash(CLAUDE_SKILL_CONTENT),
344
+ installedAt: new Date().toISOString(),
345
+ files: [paths.skillFile, paths.hookScript, paths.settingsFile]
346
+ };
347
+ writeManifest(manifest);
348
+
349
+ return { ok: true, backups };
350
+ }
351
+
352
+ function installOpenCode() {
353
+ const paths = openCodePaths();
354
+ const backups = [];
355
+
356
+ const agentsBackup = backupFile(paths.agentsFile);
357
+ if (agentsBackup) {
358
+ backups.push(agentsBackup);
359
+ console.log(` ✓ Backed up: ${paths.agentsFile} → ${path.basename(agentsBackup)}`);
360
+ }
361
+
362
+ const existingAgents = readTextFile(paths.agentsFile);
363
+ const mergedAgents = mergeMarkdownSection(existingAgents, OPENCODE_AGENTS_SECTION);
364
+ writeTextFile(paths.agentsFile, mergedAgents);
365
+ console.log(` ✓ Installed agents config: ${paths.agentsFile}`);
366
+
367
+ const configBackup = backupFile(paths.configFile);
368
+ if (configBackup) {
369
+ backups.push(configBackup);
370
+ console.log(` ✓ Backed up: ${paths.configFile} → ${path.basename(configBackup)}`);
371
+ }
372
+
373
+ const existingConfig = readJsonFile(paths.configFile);
374
+ const existingInstructions = Array.isArray(existingConfig.instructions)
375
+ ? existingConfig.instructions.filter(v => typeof v === 'string' && v.trim())
376
+ : [];
377
+ const existingPermission =
378
+ existingConfig.permission && typeof existingConfig.permission === 'object'
379
+ ? existingConfig.permission
380
+ : {};
381
+ const existingBash =
382
+ existingPermission.bash && typeof existingPermission.bash === 'object'
383
+ ? existingPermission.bash
384
+ : {};
385
+
386
+ writeJsonFile(paths.configFile, {
387
+ ...existingConfig,
388
+ $schema: 'https://opencode.ai/config.json',
389
+ instructions: Array.from(new Set([...existingInstructions, paths.agentsFile])),
390
+ permission: {
391
+ ...existingPermission,
392
+ bash: {
393
+ '*': 'ask',
394
+ ...existingBash,
395
+ 'ovld protocol *': 'allow',
396
+ 'curl -sS -X POST *': 'allow',
397
+ 'curl -s -X POST *': 'allow'
398
+ }
399
+ }
400
+ });
401
+ console.log(` ✓ Updated config: ${paths.configFile}`);
402
+
403
+ const commandFiles = [
404
+ {
405
+ file: path.join(paths.commandsDir, 'connect.md'),
406
+ content: `---
407
+ description: Connect this session to another Overlord ticket by ticket ID
408
+ agent: build
409
+ ---
410
+
411
+ Run \`ovld protocol connect --ticket-id <ticketId>\` using \`$ARGUMENTS\` as the ticket ID. If no ticket ID was provided, ask the user for one and stop.`
412
+ },
413
+ {
414
+ file: path.join(paths.commandsDir, 'load.md'),
415
+ content: `---
416
+ description: Load Overlord ticket context without creating a new session
417
+ agent: build
418
+ ---
419
+
420
+ Run \`ovld protocol load-context --ticket-id <ticketId>\` using \`$ARGUMENTS\` as the ticket ID. If no ticket ID was provided, ask the user for one and stop.`
421
+ },
422
+ {
423
+ file: path.join(paths.commandsDir, 'spawn.md'),
424
+ content: `---
425
+ description: Create a new Overlord ticket from the current conversation
426
+ agent: build
427
+ ---
428
+
429
+ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
430
+ }
431
+ ];
432
+
433
+ for (const commandFile of commandFiles) {
434
+ const commandBackup = backupFile(commandFile.file);
435
+ if (commandBackup) {
436
+ backups.push(commandBackup);
437
+ console.log(` ✓ Backed up: ${commandFile.file} → ${path.basename(commandBackup)}`);
438
+ }
439
+ writeTextFile(commandFile.file, `${commandFile.content.trim()}\n`);
440
+ console.log(` ✓ Installed slash command: ${commandFile.file}`);
441
+ }
442
+
443
+ const manifest = readManifest();
444
+ manifest.opencode = {
445
+ version: BUNDLE_VERSION,
446
+ contentHash: contentHash(OPENCODE_AGENTS_SECTION),
447
+ installedAt: new Date().toISOString(),
448
+ files: [paths.agentsFile, paths.configFile, ...commandFiles.map(entry => entry.file)]
449
+ };
450
+ writeManifest(manifest);
451
+
452
+ return { ok: true, backups };
453
+ }
454
+
455
+ // ---------------------------------------------------------------------------
456
+ // Doctor
457
+ // ---------------------------------------------------------------------------
458
+
459
+ function doctorAgent(agent) {
460
+ const manifest = readManifest();
461
+ const entry = manifest[agent];
462
+
463
+ if (!entry) {
464
+ console.log(` ✗ ${agent}: not installed`);
465
+ return false;
466
+ }
467
+
468
+ if (entry.version !== BUNDLE_VERSION) {
469
+ console.log(` ⚠ ${agent}: stale (installed v${entry.version}, current v${BUNDLE_VERSION})`);
470
+ return false;
471
+ }
472
+
473
+ const missingFiles = entry.files.filter(f => !fs.existsSync(f));
474
+ if (missingFiles.length > 0) {
475
+ console.log(` ⚠ ${agent}: partial — missing files:`);
476
+ for (const f of missingFiles) console.log(` ${f}`);
477
+ return false;
478
+ }
479
+
480
+ console.log(` ✓ ${agent}: installed (v${entry.version})`);
481
+ return true;
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // Public API
486
+ // ---------------------------------------------------------------------------
487
+
488
+ export async function runSetupCommand(args) {
489
+ const agent = args[0];
490
+
491
+ if (agent === '--help' || agent === '-h' || agent === 'help') {
492
+ console.log(`Usage:
493
+ ovld setup claude Install Overlord bundle for Claude Code
494
+ ovld setup opencode Install Overlord connector for OpenCode
495
+ ovld setup all Install for all supported agents
496
+ ovld doctor Validate installed connectors`);
497
+ return;
498
+ }
499
+
500
+ if (agent === 'codex') {
501
+ console.error(
502
+ 'Codex no longer uses `ovld setup codex`. Install the Overlord Codex chat plugin from the desktop app Settings -> CLI -> Codex -> Chat plugin, or configure Codex cloud/headless access through Settings -> Agents & MCP.'
503
+ );
504
+ process.exit(1);
505
+ }
506
+
507
+ if (agent === 'all') {
508
+ console.log('Installing Overlord agent bundle for all supported agents...\n');
509
+ for (const a of supportedAgents) {
510
+ console.log(`[${a}]`);
511
+ try {
512
+ if (a === 'claude') installClaude();
513
+ else installOpenCode();
514
+ } catch (err) {
515
+ console.error(` ✗ Failed: ${err.message}`);
516
+ }
517
+ console.log();
518
+ }
519
+ console.log('Done.');
520
+ return;
521
+ }
522
+
523
+ if (!supportedAgents.includes(agent)) {
524
+ console.error(
525
+ `Unknown agent: ${agent ?? '(none)'}. Supported: ${supportedAgents.join(', ')}, all`
526
+ );
527
+ process.exit(1);
528
+ }
529
+
530
+ console.log(`Installing Overlord agent bundle for ${agent}...\n`);
531
+ try {
532
+ if (agent === 'claude') installClaude();
533
+ else installOpenCode();
534
+ console.log('\nDone.');
535
+ } catch (err) {
536
+ console.error(`\nFailed: ${err.message}`);
537
+ process.exit(1);
538
+ }
539
+ }
540
+
541
+ export async function runDoctorCommand() {
542
+ console.log('Overlord agent bundle status:\n');
543
+ let allOk = true;
544
+ for (const agent of supportedAgents) {
545
+ if (!doctorAgent(agent)) allOk = false;
546
+ }
547
+ console.log();
548
+ if (allOk) {
549
+ console.log('All bundles are up to date.');
550
+ } else {
551
+ console.log('Run `ovld setup <agent>` or `ovld setup all` to install/repair.');
552
+ }
553
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
4
+
5
+ export async function ticketContext(ticketId) {
6
+ if (!ticketId) {
7
+ // Fall back to TICKET_ID env var
8
+ ticketId = process.env.TICKET_ID;
9
+ }
10
+
11
+ if (!ticketId) {
12
+ console.error('Error: ticket ID is required.\n');
13
+ console.error('Usage: ovld ticket context <ticketId>');
14
+ console.error(' TICKET_ID=<id> ovld ticket context');
15
+ process.exit(1);
16
+ }
17
+
18
+ const { platformUrl, agentToken, localSecret } = resolveAuth();
19
+
20
+ const url = `${platformUrl}/api/protocol/context/${ticketId}`;
21
+ const res = await fetch(url, {
22
+ headers: buildAuthHeaders(agentToken, localSecret)
23
+ });
24
+
25
+ if (!res.ok) {
26
+ throw new Error(`Failed to fetch ticket context (${res.status}): ${await res.text()}`);
27
+ }
28
+
29
+ const text = await res.text();
30
+ process.stdout.write(text);
31
+ }
32
+
33
+ export async function runTicketCommand(subcommand, args) {
34
+ if (!subcommand || subcommand === 'help' || subcommand === '--help') {
35
+ console.log(`ovld ticket <subcommand>
36
+
37
+ Subcommands:
38
+ context <ticketId> Print ticket context (objective, acceptance criteria, tools)
39
+
40
+ Examples:
41
+ ovld ticket context abc-123
42
+ TICKET_ID=abc-123 ovld ticket context
43
+ `);
44
+ return;
45
+ }
46
+
47
+ if (subcommand === 'context') {
48
+ await ticketContext(args[0]);
49
+ return;
50
+ }
51
+
52
+ console.error(`Unknown ticket subcommand: ${subcommand}\n`);
53
+ console.log('Run: ovld ticket help');
54
+ process.exit(1);
55
+ }