overlord-cli 0.2605101225.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.
Files changed (42) hide show
  1. package/README.md +128 -0
  2. package/bin/_cli/attach.mjs +357 -0
  3. package/bin/_cli/auth.mjs +696 -0
  4. package/bin/_cli/cli-update.mjs +119 -0
  5. package/bin/_cli/credentials.mjs +723 -0
  6. package/bin/_cli/index.mjs +170 -0
  7. package/bin/_cli/launcher.mjs +474 -0
  8. package/bin/_cli/new-ticket.mjs +306 -0
  9. package/bin/_cli/postinstall.mjs +43 -0
  10. package/bin/_cli/protocol.mjs +1799 -0
  11. package/bin/_cli/setup.mjs +1634 -0
  12. package/bin/_cli/ticket.mjs +55 -0
  13. package/bin/_cli/tickets.mjs +121 -0
  14. package/bin/_cli/version.mjs +7 -0
  15. package/bin/ovld.mjs +13 -0
  16. package/package.json +36 -0
  17. package/plugins/claude/.claude-plugin/plugin.json +34 -0
  18. package/plugins/claude/README.md +42 -0
  19. package/plugins/claude/commands/connect.md +19 -0
  20. package/plugins/claude/commands/create.md +17 -0
  21. package/plugins/claude/commands/load.md +19 -0
  22. package/plugins/claude/commands/spawn.md +17 -0
  23. package/plugins/claude/hooks/hooks.json +15 -0
  24. package/plugins/claude/scripts/permission-hook.sh +21 -0
  25. package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
  26. package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
  27. package/plugins/cursor/commands/connect.md +3 -0
  28. package/plugins/cursor/commands/create.md +11 -0
  29. package/plugins/cursor/commands/load.md +3 -0
  30. package/plugins/cursor/commands/spawn.md +3 -0
  31. package/plugins/cursor/mcp.json +8 -0
  32. package/plugins/cursor/rules/overlord-local.mdc +10 -0
  33. package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
  34. package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
  35. package/plugins/overlord/.codex-plugin/plugin.json +35 -0
  36. package/plugins/overlord/.mcp.json +11 -0
  37. package/plugins/overlord/README.md +42 -0
  38. package/plugins/overlord/assets/icon.png +0 -0
  39. package/plugins/overlord/assets/logo.png +0 -0
  40. package/plugins/overlord/assets/screenshot.svg +28 -0
  41. package/plugins/overlord/scripts/overlord-mcp.mjs +688 -0
  42. package/plugins/overlord/skills/overlord-ticket/SKILL.md +146 -0
@@ -0,0 +1,1634 @@
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
+ import { fileURLToPath } from 'node:url';
15
+ import { checkForCliUpdate, getCurrentCliVersion, printCliUpdateNotice } from './cli-update.mjs';
16
+
17
+ const BUNDLE_VERSION = '1.11.0';
18
+ const MD_MARKER_START = '<!-- overlord:managed:start -->';
19
+ const MD_MARKER_END = '<!-- overlord:managed:end -->';
20
+ const MANIFEST_DIR = path.join(os.homedir(), '.ovld');
21
+ const MANIFEST_FILE = path.join(MANIFEST_DIR, 'bundle-manifest.json');
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const PACKAGE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'overlord');
24
+ const REPO_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'overlord');
25
+ const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
26
+ const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
27
+ const PACKAGE_CURSOR_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'cursor');
28
+ const REPO_CURSOR_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'cursor');
29
+ const CODEX_TARGET_PLUGIN_DIR = path.join(os.homedir(), '.codex', 'plugins', 'overlord');
30
+ const CODEX_TARGET_PLUGIN_MANIFEST = path.join(
31
+ CODEX_TARGET_PLUGIN_DIR,
32
+ '.codex-plugin',
33
+ 'plugin.json'
34
+ );
35
+ const CODEX_TARGET_MARKETPLACE = path.join(os.homedir(), '.agents', 'plugins', 'marketplace.json');
36
+ const CODEX_TARGET_RULES = path.join(os.homedir(), '.codex', 'rules', 'default.rules');
37
+ const CODEX_LEGACY_AGENTS = path.join(os.homedir(), '.codex', 'AGENTS.md');
38
+ const CODEX_RULES_START = '# overlord:permissions:start';
39
+ const CODEX_RULES_END = '# overlord:permissions:end';
40
+ const REQUIRED_NODE_MAJOR = 20;
41
+
42
+ const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Templates (same content as electron/services/agent-bundle/templates.ts)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ const CLAUDE_SKILL_CONTENT = `---
49
+ name: overlord-local
50
+ description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work.
51
+ ---
52
+
53
+ # Overlord Local Workflow
54
+
55
+ 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.
56
+
57
+ ## Lifecycle
58
+
59
+ 1. **Attach first** — Always call attach before doing any work:
60
+ \`\`\`bash
61
+ ovld protocol attach --ticket-id $TICKET_ID
62
+ \`\`\`
63
+ Store \`session.sessionKey\` from the response — it is required for all subsequent calls.
64
+
65
+ 2. **Update during work** — Post at least one progress update before delivering:
66
+ \`\`\`bash
67
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
68
+ \`\`\`
69
+ Phases: \`draft\`, \`execute\`, \`review\`, \`deliver\`, \`complete\`, \`blocked\`, \`cancelled\`.
70
+ Use \`execute\` while working.
71
+
72
+ Pass \`--event-type <type>\` to publish a specific activity event (default: \`update\`):
73
+ - \`update\` — standard progress update (default)
74
+ - \`user_follow_up\` — a message or question from the human user (EXCLUDING THE INITIAL TICKET)
75
+ - \`alert\` — surface a warning or non-blocking alert
76
+
77
+ 3. **Ask when blocked** — Stop working after calling:
78
+ \`\`\`bash
79
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
80
+ \`\`\`
81
+
82
+ 4. **Deliver last** — Always deliver when done:
83
+ \`\`\`bash
84
+ ovld protocol deliver --session-key <sessionKey> \\\\
85
+ --ticket-id $TICKET_ID \\\\
86
+ --summary "Narrative: what you did, next steps." \\\\
87
+ --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
88
+ --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 @@"}]}]'
89
+ \`\`\`
90
+
91
+ For larger or quote-sensitive deliveries, prefer a single JSON payload on stdin:
92
+ \`\`\`bash
93
+ ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --payload-file -
94
+ \`\`\`
95
+ This avoids creating a scratch delivery file that needs cleanup. If your runtime cannot provide stdin directly, \`--payload-file ./deliver.json\` remains supported; treat that file as ephemeral scratch data, never commit it, and remove it after delivery.
96
+
97
+ ## Change Rationales
98
+
99
+ Always include \`changeRationales\` when delivering. Optionally include them on updates during long-running work.
100
+
101
+ Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact.
102
+
103
+ 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 larger full delivery payloads, prefer \`--payload-file -\` so summary, artifacts, and change rationales stay in one JSON document without creating a temporary file. Ordinary deliver artifacts should use \`next_steps\`, \`test_results\`, \`migration\`, \`note\`, \`url\`, or \`decision\`.
104
+
105
+ \`\`\`bash
106
+ ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
107
+ --summary "Recorded rationale details for the latest code changes." --phase execute \\\\
108
+ --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 @@"}]}]'
109
+ \`\`\`
110
+
111
+ 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.
112
+
113
+ ## Project Discovery & Ticket Creation
114
+
115
+ When creating tickets from within a repository:
116
+ - Prefer \`ovld protocol create --agent claude-code\` by default for draft ticket creation.
117
+ - Use \`ovld protocol prompt --agent claude-code\` only when the user explicitly asks to create and execute immediately.
118
+ - Both commands can resolve the project from the current working directory; use \`--working-directory\` to override.
119
+
120
+ \`\`\`bash
121
+ ovld protocol create --agent claude-code --objective "Capture follow-up work from this repository"
122
+ \`\`\`
123
+
124
+ \`\`\`bash
125
+ ovld protocol prompt --agent claude-code --objective "Implement feature X" --priority medium
126
+ \`\`\`
127
+
128
+ ## Context & Artifacts
129
+
130
+ \`\`\`bash
131
+ ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
132
+ ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
133
+ ovld protocol attachment-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --objective-id <objective-id> --file ./spec.pdf --content-type application/pdf
134
+ \`\`\`
135
+
136
+ ## Rules
137
+
138
+ - Always attach first; always deliver when done.
139
+ - Post at least one update before delivering.
140
+ - If blocked on human-only work, call \`ask\` and request a follow-up human ticket.
141
+ - The \`summary\` in deliver is what the PM reads first — write it as a narrative, not a command list.
142
+ - Use \`write-context\` for facts a future agent session should know.
143
+ - **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.**
144
+ `;
145
+
146
+ const OPENCODE_AGENTS_SECTION = `# Overlord Local Workflow
147
+
148
+ 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.
149
+
150
+ ## Lifecycle
151
+
152
+ 1. **Attach first** — Always call attach before doing any work:
153
+ \`\`\`bash
154
+ ovld protocol attach --ticket-id $TICKET_ID
155
+ \`\`\`
156
+ Store \`session.sessionKey\` from the response — it is required for all subsequent calls.
157
+
158
+ 2. **Update during work** — Post at least one progress update before delivering:
159
+ \`\`\`bash
160
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
161
+ \`\`\`
162
+ Phases: \`draft\`, \`execute\`, \`review\`, \`deliver\`, \`complete\`, \`blocked\`, \`cancelled\`.
163
+ Use \`execute\` while working.
164
+
165
+ Pass \`--event-type <type>\` for activity events: \`update\`, \`user_follow_up\`, \`alert\`.
166
+
167
+ 3. **Ask when blocked** — Stop working after calling:
168
+ \`\`\`bash
169
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
170
+ \`\`\`
171
+
172
+ 4. **Deliver last** — Always deliver when done:
173
+ \`\`\`bash
174
+ ovld protocol deliver --session-key <sessionKey> \\\\
175
+ --ticket-id $TICKET_ID \\\\
176
+ --summary "Narrative: what you did, next steps." \\\\
177
+ --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
178
+ --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 @@"}]}]'
179
+ \`\`\`
180
+ For larger delivery JSON, prefer \`--payload-file -\` and stream the full payload on stdin so no scratch file needs to be created or removed. If you use \`--payload-file\`, \`--artifacts-file\`, or \`--change-rationales-file\` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery. Do not leave delivery JSON checked into the worktree.
181
+
182
+ ## Change Rationales
183
+
184
+ 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. For larger delivery payloads, prefer \`--payload-file -\` with stdin. If you need a JSON file for transport, keep it ephemeral and out of the repository.
185
+
186
+ \`\`\`bash
187
+ ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
188
+ --summary "Recorded rationale details for the latest code changes." --phase execute \\\\
189
+ --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 @@"}]}]'
190
+ \`\`\`
191
+
192
+ ## Project Discovery & Ticket Creation
193
+
194
+ When creating tickets from within a repository:
195
+ - Prefer \`ovld protocol create --agent opencode\` by default for draft ticket creation.
196
+ - Use \`ovld protocol prompt --agent opencode\` only when the user explicitly asks to create and execute immediately.
197
+ - Both commands can resolve the project from the current working directory; use \`--working-directory\` to override.
198
+
199
+ \`\`\`bash
200
+ ovld protocol create --agent opencode --objective "Capture follow-up work from this repository"
201
+ \`\`\`
202
+
203
+ \`\`\`bash
204
+ ovld protocol prompt --agent opencode --objective "Implement feature X" --priority medium
205
+ \`\`\`
206
+
207
+ ## Context & Artifacts
208
+
209
+ \`\`\`bash
210
+ ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
211
+ ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
212
+ ovld protocol attachment-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --objective-id <objective-id> --file ./spec.pdf --content-type application/pdf
213
+ \`\`\`
214
+
215
+ ## Rules
216
+
217
+ - Always attach first; always deliver when done.
218
+ - Post at least one update before delivering.
219
+ - If blocked on human-only work, call \`ask\` and request a follow-up human ticket.
220
+ - The \`summary\` in deliver is what the PM reads first — write it as a narrative.
221
+ - Use \`write-context\` for facts a future agent session should know.
222
+ - **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.**
223
+ `;
224
+
225
+ const PERMISSION_HOOK_SCRIPT = `#!/bin/bash
226
+ # Overlord PermissionRequest notification hook (managed by Overlord)
227
+ BODY=$(cat -)
228
+ if [ -n "$TICKET_ID" ] && command -v ovld >/dev/null 2>&1; then
229
+ { if [ -n "$BODY" ]; then printf '%s' "$BODY"; else printf '{}'; fi; } \\
230
+ | ovld protocol permission-request --ticket-id "$TICKET_ID" --payload-file - \\
231
+ >/dev/null 2>&1 &
232
+ disown
233
+ fi
234
+ exit 0
235
+ `;
236
+
237
+ const CURSOR_RULES_CONTENT = `---
238
+ description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work.
239
+ globs:
240
+ alwaysApply: true
241
+ ---
242
+
243
+ # Overlord Local Workflow
244
+
245
+ If a prompt includes a TICKET_ID, attach first with:
246
+ \`\`\`bash
247
+ ovld protocol attach --ticket-id $TICKET_ID
248
+ \`\`\`
249
+
250
+ During work, post progress updates with:
251
+ \`\`\`bash
252
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
253
+ \`\`\`
254
+
255
+ If blocked on human input, ask with:
256
+ \`\`\`bash
257
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
258
+ \`\`\`
259
+
260
+ When done, deliver with artifacts and change rationales:
261
+ \`\`\`bash
262
+ ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --summary "Narrative: what you did, next steps." --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' --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 @@"}]}]'
263
+ \`\`\`
264
+ For larger delivery JSON, prefer \`--payload-file -\` with stdin so no scratch file needs to be created or removed. If you use a JSON file for delivery transport, keep it ephemeral scratch data outside the repository and remove it after the protocol call.
265
+
266
+ Rules:
267
+ - Always attach first and deliver last.
268
+ - Use \`ovld protocol\` commands instead of ad hoc repo scripts for ticket lifecycle work.
269
+ - Prefer \`ovld protocol create --agent cursor\` for draft ticket creation; use \`prompt --agent cursor\` only for create-and-execute requests.
270
+ - If the user sends a new message during an active ticket session, publish a \`user_follow_up\` event before doing anything else.
271
+ `;
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Helpers
275
+ // ---------------------------------------------------------------------------
276
+
277
+ function contentHash(content) {
278
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
279
+ }
280
+
281
+ function backupFile(filePath) {
282
+ if (!fs.existsSync(filePath)) return null;
283
+ const dir = path.dirname(filePath);
284
+ const ext = path.extname(filePath);
285
+ const base = path.basename(filePath, ext);
286
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
287
+ const backupPath = path.join(dir, `${base}.backup-${ts}${ext}`);
288
+ fs.copyFileSync(filePath, backupPath);
289
+ return backupPath;
290
+ }
291
+
292
+ function readJsonFile(filePath) {
293
+ try {
294
+ const raw = fs.readFileSync(filePath, 'utf-8');
295
+ const parsed = JSON.parse(raw);
296
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
297
+ return {};
298
+ } catch {
299
+ return {};
300
+ }
301
+ }
302
+
303
+ function readJsonFileOrNull(filePath) {
304
+ try {
305
+ if (!fs.existsSync(filePath)) return null;
306
+ const raw = fs.readFileSync(filePath, 'utf-8');
307
+ const parsed = JSON.parse(raw);
308
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
309
+ return null;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+
315
+ function writeJsonFile(filePath, data) {
316
+ const dir = path.dirname(filePath);
317
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
318
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
319
+ }
320
+
321
+ function readTextFile(filePath) {
322
+ try {
323
+ return fs.readFileSync(filePath, 'utf-8');
324
+ } catch {
325
+ return '';
326
+ }
327
+ }
328
+
329
+ function writeTextFile(filePath, content, mode) {
330
+ const dir = path.dirname(filePath);
331
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
332
+ const options = { encoding: 'utf-8' };
333
+ if (mode !== undefined) options.mode = mode;
334
+ fs.writeFileSync(filePath, content, options);
335
+ }
336
+
337
+ function deepClone(obj) {
338
+ return JSON.parse(JSON.stringify(obj));
339
+ }
340
+
341
+ function asStringArray(value) {
342
+ if (!Array.isArray(value)) return [];
343
+ return value.filter(entry => typeof entry === 'string');
344
+ }
345
+
346
+ function mergeMarkdownSection(existing, newContent) {
347
+ const wrappedContent = `${MD_MARKER_START}\n${newContent.trim()}\n${MD_MARKER_END}`;
348
+ const startIdx = existing.indexOf(MD_MARKER_START);
349
+ const endIdx = existing.indexOf(MD_MARKER_END);
350
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
351
+ const before = existing.slice(0, startIdx);
352
+ const after = existing.slice(endIdx + MD_MARKER_END.length);
353
+ return `${before}${wrappedContent}${after}`;
354
+ }
355
+ const trimmed = existing.trimEnd();
356
+ if (trimmed.length === 0) return wrappedContent + '\n';
357
+ return `${trimmed}\n\n${wrappedContent}\n`;
358
+ }
359
+
360
+ function readManifest() {
361
+ return readJsonFile(MANIFEST_FILE);
362
+ }
363
+ function writeManifest(manifest) {
364
+ writeJsonFile(MANIFEST_FILE, manifest);
365
+ }
366
+
367
+ function slashCommandFiles(agent) {
368
+ if (agent === 'claude') {
369
+ const base = path.join(os.homedir(), '.claude', 'commands');
370
+ return [
371
+ {
372
+ path: path.join(base, 'connect.md'),
373
+ content: `---
374
+ description: Connect this session to another Overlord ticket by ticket identifier
375
+ argument-hint: <ticket_id>
376
+ disable-model-invocation: true
377
+ ---
378
+
379
+ Run \`ovld protocol connect --ticket-id <ticket_id>\` using \`$ARGUMENTS\` as the ticket ID.`
380
+ },
381
+ {
382
+ path: path.join(base, 'load.md'),
383
+ content: `---
384
+ description: Load Overlord ticket context without creating a new session
385
+ argument-hint: <ticket_id>
386
+ disable-model-invocation: true
387
+ ---
388
+
389
+ Run \`ovld protocol load-context --ticket-id <ticket_id>\` using \`$ARGUMENTS\` as the ticket ID.`
390
+ },
391
+ {
392
+ path: path.join(base, 'create.md'),
393
+ content: `---
394
+ description: Create a draft Overlord ticket from the current conversation
395
+ argument-hint: <objective or raw flags>
396
+ disable-model-invocation: true
397
+ ---
398
+
399
+ Run \`ovld protocol create --agent claude-code\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol create --agent claude-code --objective "<objective>"\`.`
400
+ },
401
+ {
402
+ path: path.join(base, 'prompt.md'),
403
+ content: `---
404
+ description: Create a new Overlord ticket from the current conversation
405
+ argument-hint: <objective or raw flags>
406
+ disable-model-invocation: true
407
+ ---
408
+
409
+ Run \`ovld protocol prompt --agent claude-code\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol prompt --agent claude-code --objective "<objective>"\`.`
410
+ }
411
+ ];
412
+ }
413
+
414
+ if (agent === 'cursor') {
415
+ const base = path.join(os.homedir(), '.cursor', 'commands');
416
+ return [
417
+ {
418
+ path: path.join(base, 'connect.md'),
419
+ content:
420
+ 'Connect this session to another Overlord ticket.\n\nRun `ovld protocol connect --ticket-id <ticketId>` using the text after `/connect` as the ticket ID.\n'
421
+ },
422
+ {
423
+ path: path.join(base, 'load.md'),
424
+ content:
425
+ 'Load Overlord ticket context without attaching.\n\nRun `ovld protocol load-context --ticket-id <ticketId>` using the text after `/load` as the ticket ID.\n'
426
+ },
427
+ {
428
+ path: path.join(base, 'create.md'),
429
+ content:
430
+ 'Create a draft Overlord ticket.\n\nRun `ovld protocol create --agent cursor --objective "<objective>"` using the text after `/create` unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol create --agent cursor`.\n'
431
+ },
432
+ {
433
+ path: path.join(base, 'prompt.md'),
434
+ content:
435
+ 'Create a new Overlord ticket.\n\nRun `ovld protocol prompt --agent cursor --objective "<objective>"` using the text after `/prompt` unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol prompt --agent cursor`.\n'
436
+ }
437
+ ];
438
+ }
439
+
440
+ if (agent === 'gemini') {
441
+ const base = path.join(os.homedir(), '.gemini', 'commands');
442
+ return [
443
+ {
444
+ path: path.join(base, 'connect.toml'),
445
+ content:
446
+ 'description = "Connect this session to another Overlord ticket by ticket ID."\nprompt = """\nRun `ovld protocol connect --ticket-id <ticketId>` using `{{args}}` as the ticket ID.\n"""\n'
447
+ },
448
+ {
449
+ path: path.join(base, 'load.toml'),
450
+ content:
451
+ 'description = "Load Overlord ticket context without creating a new session."\nprompt = """\nRun `ovld protocol load-context --ticket-id <ticketId>` using `{{args}}` as the ticket ID.\n"""\n'
452
+ },
453
+ {
454
+ path: path.join(base, 'create.toml'),
455
+ content:
456
+ 'description = "Create a draft Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol create --agent gemini --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol create --agent gemini`.\n"""\n'
457
+ },
458
+ {
459
+ path: path.join(base, 'prompt.toml'),
460
+ content:
461
+ 'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol prompt --agent gemini --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol prompt --agent gemini`.\n"""\n'
462
+ }
463
+ ];
464
+ }
465
+
466
+ const base = path.join(os.homedir(), '.config', 'opencode', 'commands');
467
+ return [
468
+ {
469
+ path: path.join(base, 'connect.md'),
470
+ content: `---
471
+ description: Connect this session to another Overlord ticket by ticket ID
472
+ agent: build
473
+ ---
474
+
475
+ 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.`
476
+ },
477
+ {
478
+ path: path.join(base, 'load.md'),
479
+ content: `---
480
+ description: Load Overlord ticket context without creating a new session
481
+ agent: build
482
+ ---
483
+
484
+ 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.`
485
+ },
486
+ {
487
+ path: path.join(base, 'create.md'),
488
+ content: `---
489
+ description: Create a draft Overlord ticket from the current conversation
490
+ agent: build
491
+ ---
492
+
493
+ Run \`ovld protocol create --agent opencode\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol create --agent opencode --objective "<objective>"\`.`
494
+ },
495
+ {
496
+ path: path.join(base, 'prompt.md'),
497
+ content: `---
498
+ description: Create a new Overlord ticket from the current conversation
499
+ agent: build
500
+ ---
501
+
502
+ Run \`ovld protocol prompt --agent opencode\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol prompt --agent opencode --objective "<objective>"\`.`
503
+ }
504
+ ];
505
+ }
506
+
507
+ function installSlashCommands(agent) {
508
+ const backups = [];
509
+ const files = slashCommandFiles(agent);
510
+ for (const file of files) {
511
+ const backup = backupFile(file.path);
512
+ if (backup) backups.push(backup);
513
+ writeTextFile(file.path, `${file.content.trim()}\n`);
514
+ console.log(` ✓ Installed slash command: ${file.path}`);
515
+ }
516
+ return {
517
+ backups,
518
+ managedFiles: files.map(file => file.path)
519
+ };
520
+ }
521
+
522
+ function uninstallSlashCommands(agent) {
523
+ const removedFiles = [];
524
+ const files = slashCommandFiles(agent);
525
+ for (const file of files) {
526
+ if (!fs.existsSync(file.path)) continue;
527
+ const existing = readTextFile(file.path);
528
+ if (existing.trim() !== file.content.trim()) continue;
529
+ fs.rmSync(file.path, { force: true });
530
+ removedFiles.push(file.path);
531
+ }
532
+ return { removedFiles };
533
+ }
534
+
535
+ function currentContentHashForAgent(agent) {
536
+ if (agent === 'claude') {
537
+ return claudeContentHash();
538
+ }
539
+ if (agent === 'cursor') {
540
+ return contentHashForDirectory(cursorSourcePluginDir());
541
+ }
542
+ if (agent === 'gemini') {
543
+ return contentHash(
544
+ slashCommandFiles('gemini')
545
+ .map(file => file.content)
546
+ .join('\n')
547
+ );
548
+ }
549
+ if (agent === 'codex') return codexContentHash();
550
+ return contentHash(
551
+ [OPENCODE_AGENTS_SECTION, ...slashCommandFiles('opencode').map(file => file.content)].join('\n')
552
+ );
553
+ }
554
+
555
+ function codexSourcePluginDir() {
556
+ if (fs.existsSync(PACKAGE_PLUGIN_DIR)) return PACKAGE_PLUGIN_DIR;
557
+ if (fs.existsSync(REPO_PLUGIN_DIR)) return REPO_PLUGIN_DIR;
558
+ throw new Error(
559
+ `Codex plugin bundle not found. Checked ${PACKAGE_PLUGIN_DIR} and ${REPO_PLUGIN_DIR}.`
560
+ );
561
+ }
562
+
563
+ function claudeSourcePluginDir() {
564
+ if (fs.existsSync(PACKAGE_CLAUDE_PLUGIN_DIR)) return PACKAGE_CLAUDE_PLUGIN_DIR;
565
+ if (fs.existsSync(REPO_CLAUDE_PLUGIN_DIR)) return REPO_CLAUDE_PLUGIN_DIR;
566
+ throw new Error(
567
+ `Claude plugin bundle not found. Checked ${PACKAGE_CLAUDE_PLUGIN_DIR} and ${REPO_CLAUDE_PLUGIN_DIR}.`
568
+ );
569
+ }
570
+
571
+ function cursorSourcePluginDir() {
572
+ if (fs.existsSync(PACKAGE_CURSOR_PLUGIN_DIR)) return PACKAGE_CURSOR_PLUGIN_DIR;
573
+ if (fs.existsSync(REPO_CURSOR_PLUGIN_DIR)) return REPO_CURSOR_PLUGIN_DIR;
574
+ throw new Error(
575
+ `Cursor plugin bundle not found. Checked ${PACKAGE_CURSOR_PLUGIN_DIR} and ${REPO_CURSOR_PLUGIN_DIR}.`
576
+ );
577
+ }
578
+
579
+ function listFilesRecursive(dir) {
580
+ if (!fs.existsSync(dir)) return [];
581
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
582
+ const resolved = path.join(dir, entry.name);
583
+ if (entry.isDirectory()) return listFilesRecursive(resolved);
584
+ return [resolved];
585
+ });
586
+ }
587
+
588
+ function contentHashForDirectory(sourceDir) {
589
+ const hash = crypto.createHash('sha256');
590
+
591
+ for (const filePath of listFilesRecursive(sourceDir).sort()) {
592
+ hash.update(path.relative(sourceDir, filePath));
593
+ hash.update('\0');
594
+ hash.update(fs.readFileSync(filePath));
595
+ hash.update('\0');
596
+ }
597
+
598
+ return hash.digest('hex').slice(0, 16);
599
+ }
600
+
601
+ function codexContentHash() {
602
+ return contentHashForDirectory(codexSourcePluginDir());
603
+ }
604
+
605
+ function claudeContentHash() {
606
+ return contentHashForDirectory(claudeSourcePluginDir());
607
+ }
608
+
609
+ function mergeCodexRules(existingContent) {
610
+ const managedBlock = [
611
+ CODEX_RULES_START,
612
+ 'prefix_rule(',
613
+ ' pattern = ["npx", "overlord", "protocol"],',
614
+ ' decision = "allow",',
615
+ ' justification = "Allow all Overlord protocol commands without prompts.",',
616
+ ')',
617
+ '',
618
+ 'prefix_rule(',
619
+ ' pattern = ["ovld", "protocol"],',
620
+ ' decision = "allow",',
621
+ ' justification = "Allow all Overlord protocol commands without prompts.",',
622
+ ')',
623
+ '',
624
+ 'prefix_rule(',
625
+ ' pattern = ["curl", "-sS", "-X", "POST"],',
626
+ ' decision = "allow",',
627
+ ' justification = "Allow curl protocol POST commands without prompts.",',
628
+ ')',
629
+ CODEX_RULES_END
630
+ ].join('\n');
631
+
632
+ const startIndex = existingContent.indexOf(CODEX_RULES_START);
633
+ const endIndex = existingContent.indexOf(CODEX_RULES_END);
634
+
635
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
636
+ const before = existingContent.slice(0, startIndex).trimEnd();
637
+ const after = existingContent.slice(endIndex + CODEX_RULES_END.length).trimStart();
638
+ if (!before && !after) return `${managedBlock}\n`;
639
+ if (!before) return `${managedBlock}\n\n${after}`;
640
+ if (!after) return `${before}\n\n${managedBlock}\n`;
641
+ return `${before}\n\n${managedBlock}\n\n${after}`;
642
+ }
643
+
644
+ const trimmed = existingContent.trimEnd();
645
+ if (!trimmed) return `${managedBlock}\n`;
646
+ return `${trimmed}\n\n${managedBlock}\n`;
647
+ }
648
+
649
+ function pluginVersion(filePath) {
650
+ const parsed = readJsonFileOrNull(filePath);
651
+ return typeof parsed?.version === 'string' ? parsed.version : null;
652
+ }
653
+
654
+ function upsertCodexMarketplaceEntry() {
655
+ const current = readJsonFileOrNull(CODEX_TARGET_MARKETPLACE) ?? {
656
+ name: 'overlord-local',
657
+ interface: { displayName: 'Overlord Local Plugins' },
658
+ plugins: []
659
+ };
660
+
661
+ const nextPlugins = Array.isArray(current.plugins) ? [...current.plugins] : [];
662
+ const entry = {
663
+ name: 'overlord',
664
+ source: {
665
+ source: 'local',
666
+ path: './.codex/plugins/overlord'
667
+ },
668
+ policy: {
669
+ installation: 'AVAILABLE',
670
+ authentication: 'ON_INSTALL'
671
+ },
672
+ category: 'Productivity'
673
+ };
674
+
675
+ const existingIndex = nextPlugins.findIndex(plugin => plugin?.name === 'overlord');
676
+ if (existingIndex === -1) nextPlugins.push(entry);
677
+ else nextPlugins[existingIndex] = entry;
678
+
679
+ writeJsonFile(CODEX_TARGET_MARKETPLACE, {
680
+ name: current.name ?? 'overlord-local',
681
+ interface: {
682
+ displayName: current.interface?.displayName ?? 'Overlord Local Plugins'
683
+ },
684
+ plugins: nextPlugins
685
+ });
686
+ }
687
+
688
+ function removeLegacyCodexBundle() {
689
+ if (fs.existsSync(CODEX_LEGACY_AGENTS)) {
690
+ const existing = readTextFile(CODEX_LEGACY_AGENTS);
691
+ const startIndex = existing.indexOf(MD_MARKER_START);
692
+ const endIndex = existing.indexOf(MD_MARKER_END);
693
+
694
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
695
+ const before = existing.slice(0, startIndex).trimEnd();
696
+ const after = existing.slice(endIndex + MD_MARKER_END.length).trimStart();
697
+ const cleaned =
698
+ !before && !after
699
+ ? ''
700
+ : !before
701
+ ? `${after}\n`
702
+ : !after
703
+ ? `${before}\n`
704
+ : `${before}\n\n${after}\n`;
705
+
706
+ if (cleaned.trim().length > 0) {
707
+ writeTextFile(CODEX_LEGACY_AGENTS, cleaned);
708
+ } else {
709
+ fs.rmSync(CODEX_LEGACY_AGENTS, { force: true });
710
+ }
711
+ }
712
+ }
713
+
714
+ const manifest = readManifest();
715
+ if (!manifest.codex) return;
716
+ delete manifest.codex;
717
+ writeManifest(manifest);
718
+ }
719
+
720
+ function removeLegacyClaudeBundle() {
721
+ const paths = claudePaths();
722
+ const removed = [];
723
+
724
+ if (fs.existsSync(paths.skillDir)) {
725
+ fs.rmSync(paths.skillDir, { recursive: true, force: true });
726
+ removed.push(paths.skillDir);
727
+ }
728
+
729
+ if (fs.existsSync(paths.hookScript)) {
730
+ fs.rmSync(paths.hookScript, { force: true });
731
+ removed.push(paths.hookScript);
732
+ }
733
+
734
+ const slashResult = uninstallSlashCommands('claude');
735
+ removed.push(...slashResult.removedFiles);
736
+
737
+ if (fs.existsSync(paths.settingsFile)) {
738
+ const settings = readJsonFile(paths.settingsFile);
739
+ let changed = false;
740
+
741
+ if (settings.__overlord_managed) {
742
+ delete settings.__overlord_managed;
743
+ changed = true;
744
+ }
745
+
746
+ const hooks = settings.hooks && typeof settings.hooks === 'object' ? settings.hooks : {};
747
+ if (Array.isArray(hooks.PermissionRequest)) {
748
+ const nextPermissionHooks = hooks.PermissionRequest.filter(hook => {
749
+ if (hook && typeof hook === 'object' && Array.isArray(hook.hooks)) {
750
+ return !hook.hooks.some(
751
+ inner =>
752
+ typeof inner?.command === 'string' &&
753
+ inner.command.includes('overlord-permission-hook')
754
+ );
755
+ }
756
+ return true;
757
+ });
758
+ if (nextPermissionHooks.length !== hooks.PermissionRequest.length) {
759
+ changed = true;
760
+ if (nextPermissionHooks.length > 0) hooks.PermissionRequest = nextPermissionHooks;
761
+ else delete hooks.PermissionRequest;
762
+ }
763
+ }
764
+
765
+ if (changed) {
766
+ if (Object.keys(hooks).length > 0) settings.hooks = hooks;
767
+ else delete settings.hooks;
768
+ writeJsonFile(paths.settingsFile, settings);
769
+ removed.push(paths.settingsFile);
770
+ }
771
+ }
772
+
773
+ return removed;
774
+ }
775
+
776
+ // ---------------------------------------------------------------------------
777
+ // Install
778
+ // ---------------------------------------------------------------------------
779
+
780
+ function claudePaths() {
781
+ const base = path.join(os.homedir(), '.claude');
782
+ return {
783
+ skillDir: path.join(base, 'skills', 'overlord-local'),
784
+ skillFile: path.join(base, 'skills', 'overlord-local', 'SKILL.md'),
785
+ settingsFile: path.join(base, 'settings.json'),
786
+ hookScript: path.join(base, 'overlord-permission-hook.sh')
787
+ };
788
+ }
789
+
790
+ function openCodePaths() {
791
+ const base = path.join(os.homedir(), '.config', 'opencode');
792
+ return {
793
+ agentsFile: path.join(base, 'AGENTS.md'),
794
+ configFile: path.join(base, 'opencode.json'),
795
+ commandsDir: path.join(base, 'commands')
796
+ };
797
+ }
798
+
799
+ function cursorPaths() {
800
+ const base = path.join(os.homedir(), '.cursor');
801
+ return {
802
+ pluginDir: path.join(base, 'plugins', 'local', 'overlord'),
803
+ pluginManifest: path.join(
804
+ base,
805
+ 'plugins',
806
+ 'local',
807
+ 'overlord',
808
+ '.cursor-plugin',
809
+ 'plugin.json'
810
+ ),
811
+ rulesFile: path.join(base, 'rules', 'overlord-local.mdc'),
812
+ settingsFile: path.join(base, 'settings.json')
813
+ };
814
+ }
815
+
816
+ function geminiPaths() {
817
+ const base = path.join(os.homedir(), '.gemini');
818
+ return {
819
+ policyFile: path.join(base, 'policies', 'overlord-protocol.toml')
820
+ };
821
+ }
822
+
823
+ function installClaude() {
824
+ const sourceDir = claudeSourcePluginDir();
825
+ const sourceManifest = path.join(sourceDir, '.claude-plugin', 'plugin.json');
826
+ const sourceVersion = pluginVersion(sourceManifest) ?? '0.0.0';
827
+ const removed = removeLegacyClaudeBundle();
828
+
829
+ console.log(` ✓ Found Claude plugin source: ${sourceDir}`);
830
+ if (removed.length > 0) {
831
+ console.log(' ✓ Migrated v3.25 Claude connector files:');
832
+ for (const filePath of removed) console.log(` ${filePath}`);
833
+ } else {
834
+ console.log(' ✓ No v3.25 Claude connector files needed migration.');
835
+ }
836
+ console.log(' ✓ `ovld launch claude` now loads this plugin with `claude --plugin-dir`.');
837
+
838
+ const manifest = readManifest();
839
+ manifest.claude = {
840
+ version: sourceVersion,
841
+ contentHash: currentContentHashForAgent('claude'),
842
+ installedAt: new Date().toISOString(),
843
+ files: listFilesRecursive(sourceDir)
844
+ };
845
+ writeManifest(manifest);
846
+
847
+ return { ok: true, backups: [] };
848
+ }
849
+
850
+ function installOpenCode() {
851
+ const paths = openCodePaths();
852
+ const backups = [];
853
+
854
+ const agentsBackup = backupFile(paths.agentsFile);
855
+ if (agentsBackup) {
856
+ backups.push(agentsBackup);
857
+ console.log(` ✓ Backed up: ${paths.agentsFile} → ${path.basename(agentsBackup)}`);
858
+ }
859
+
860
+ const existingAgents = readTextFile(paths.agentsFile);
861
+ const mergedAgents = mergeMarkdownSection(existingAgents, OPENCODE_AGENTS_SECTION);
862
+ writeTextFile(paths.agentsFile, mergedAgents);
863
+ console.log(` ✓ Installed agents config: ${paths.agentsFile}`);
864
+
865
+ const configBackup = backupFile(paths.configFile);
866
+ if (configBackup) {
867
+ backups.push(configBackup);
868
+ console.log(` ✓ Backed up: ${paths.configFile} → ${path.basename(configBackup)}`);
869
+ }
870
+
871
+ const existingConfig = readJsonFile(paths.configFile);
872
+ const existingInstructions = Array.isArray(existingConfig.instructions)
873
+ ? existingConfig.instructions.filter(v => typeof v === 'string' && v.trim())
874
+ : [];
875
+ const existingPermission =
876
+ existingConfig.permission && typeof existingConfig.permission === 'object'
877
+ ? existingConfig.permission
878
+ : {};
879
+ const existingBash =
880
+ existingPermission.bash && typeof existingPermission.bash === 'object'
881
+ ? existingPermission.bash
882
+ : {};
883
+
884
+ writeJsonFile(paths.configFile, {
885
+ ...existingConfig,
886
+ $schema: 'https://opencode.ai/config.json',
887
+ instructions: Array.from(new Set([...existingInstructions, paths.agentsFile])),
888
+ permission: {
889
+ ...existingPermission,
890
+ bash: {
891
+ '*': 'ask',
892
+ ...existingBash,
893
+ 'ovld protocol *': 'allow'
894
+ }
895
+ }
896
+ });
897
+ console.log(` ✓ Updated config: ${paths.configFile}`);
898
+
899
+ const slashResult = installSlashCommands('opencode');
900
+ backups.push(...slashResult.backups);
901
+
902
+ const manifest = readManifest();
903
+ manifest.opencode = {
904
+ version: BUNDLE_VERSION,
905
+ contentHash: currentContentHashForAgent('opencode'),
906
+ installedAt: new Date().toISOString(),
907
+ files: [paths.agentsFile, paths.configFile, ...slashResult.managedFiles]
908
+ };
909
+ writeManifest(manifest);
910
+
911
+ return { ok: true, backups };
912
+ }
913
+
914
+ function installCodex() {
915
+ const sourceDir = codexSourcePluginDir();
916
+ fs.mkdirSync(path.dirname(CODEX_TARGET_PLUGIN_DIR), { recursive: true });
917
+ fs.rmSync(CODEX_TARGET_PLUGIN_DIR, { recursive: true, force: true });
918
+ fs.cpSync(sourceDir, CODEX_TARGET_PLUGIN_DIR, { recursive: true });
919
+ console.log(` ✓ Installed plugin: ${CODEX_TARGET_PLUGIN_DIR}`);
920
+
921
+ writeTextFile(CODEX_TARGET_RULES, mergeCodexRules(readTextFile(CODEX_TARGET_RULES)));
922
+ console.log(` ✓ Updated rules: ${CODEX_TARGET_RULES}`);
923
+
924
+ upsertCodexMarketplaceEntry();
925
+ console.log(` ✓ Updated marketplace: ${CODEX_TARGET_MARKETPLACE}`);
926
+
927
+ removeLegacyCodexBundle();
928
+
929
+ const installedFiles = [
930
+ ...listFilesRecursive(CODEX_TARGET_PLUGIN_DIR),
931
+ CODEX_TARGET_MARKETPLACE,
932
+ CODEX_TARGET_RULES
933
+ ];
934
+ const manifest = readManifest();
935
+ manifest.codex = {
936
+ version: pluginVersion(CODEX_TARGET_PLUGIN_MANIFEST) ?? '0.0.0',
937
+ contentHash: codexContentHash(),
938
+ installedAt: new Date().toISOString(),
939
+ files: installedFiles
940
+ };
941
+ writeManifest(manifest);
942
+
943
+ return { ok: true, installedFiles };
944
+ }
945
+
946
+ function installCursor() {
947
+ const paths = cursorPaths();
948
+ const sourceDir = cursorSourcePluginDir();
949
+ fs.mkdirSync(path.dirname(paths.pluginDir), { recursive: true });
950
+ fs.rmSync(paths.pluginDir, { recursive: true, force: true });
951
+ fs.cpSync(sourceDir, paths.pluginDir, { recursive: true });
952
+ console.log(` ✓ Installed plugin: ${paths.pluginDir}`);
953
+
954
+ if (fs.existsSync(paths.rulesFile)) {
955
+ fs.rmSync(paths.rulesFile, { force: true });
956
+ console.log(` ✓ Removed legacy rules file: ${paths.rulesFile}`);
957
+ }
958
+ const removedLegacySlash = uninstallSlashCommands('cursor');
959
+ if (removedLegacySlash.removedFiles.length > 0) {
960
+ console.log(' ✓ Removed legacy slash commands:');
961
+ for (const filePath of removedLegacySlash.removedFiles) console.log(` ${filePath}`);
962
+ }
963
+
964
+ const existingSettings = readJsonFile(paths.settingsFile);
965
+ const permissions =
966
+ existingSettings.permissions && typeof existingSettings.permissions === 'object'
967
+ ? existingSettings.permissions
968
+ : {};
969
+ const mergedAllow = Array.from(
970
+ new Set([...asStringArray(permissions.allow), 'Shell(ovld protocol:*)'])
971
+ );
972
+ writeJsonFile(paths.settingsFile, {
973
+ ...existingSettings,
974
+ permissions: {
975
+ ...permissions,
976
+ allow: mergedAllow
977
+ }
978
+ });
979
+ console.log(` ✓ Updated permissions: ${paths.settingsFile}`);
980
+
981
+ const manifest = readManifest();
982
+ manifest.cursor = {
983
+ version: pluginVersion(paths.pluginManifest) ?? '0.0.0',
984
+ contentHash: currentContentHashForAgent('cursor'),
985
+ installedAt: new Date().toISOString(),
986
+ files: [...listFilesRecursive(paths.pluginDir), paths.settingsFile]
987
+ };
988
+ writeManifest(manifest);
989
+
990
+ return { ok: true };
991
+ }
992
+
993
+ function installGemini() {
994
+ const slashResult = installSlashCommands('gemini');
995
+ const paths = geminiPaths();
996
+ const policyContent = [
997
+ '# Managed by Overlord onboarding',
998
+ '[[rule]]',
999
+ 'toolName = "run_shell_command"',
1000
+ 'commandPrefix = "ovld protocol"',
1001
+ 'decision = "allow"',
1002
+ 'priority = 900',
1003
+ ''
1004
+ ].join('\n');
1005
+ writeTextFile(paths.policyFile, policyContent);
1006
+ console.log(` ✓ Installed policy: ${paths.policyFile}`);
1007
+
1008
+ const manifest = readManifest();
1009
+ manifest.gemini = {
1010
+ version: BUNDLE_VERSION,
1011
+ contentHash: currentContentHashForAgent('gemini'),
1012
+ installedAt: new Date().toISOString(),
1013
+ files: [paths.policyFile, ...slashResult.managedFiles]
1014
+ };
1015
+ writeManifest(manifest);
1016
+
1017
+ return { ok: true };
1018
+ }
1019
+
1020
+ // ---------------------------------------------------------------------------
1021
+ // Doctor
1022
+ // ---------------------------------------------------------------------------
1023
+
1024
+ function doctorAgent(agent) {
1025
+ const manifest = readManifest();
1026
+ const entry = manifest[agent];
1027
+
1028
+ if (!entry) {
1029
+ console.log(` ✗ ${agent}: not installed`);
1030
+ return false;
1031
+ }
1032
+
1033
+ const currentVersion =
1034
+ agent === 'claude'
1035
+ ? pluginVersion(path.join(claudeSourcePluginDir(), '.claude-plugin', 'plugin.json'))
1036
+ : agent === 'codex'
1037
+ ? pluginVersion(path.join(codexSourcePluginDir(), '.codex-plugin', 'plugin.json'))
1038
+ : agent === 'cursor'
1039
+ ? pluginVersion(path.join(cursorSourcePluginDir(), '.cursor-plugin', 'plugin.json'))
1040
+ : BUNDLE_VERSION;
1041
+ const currentHash = currentContentHashForAgent(agent);
1042
+
1043
+ if (entry.version !== currentVersion || entry.contentHash !== currentHash) {
1044
+ console.log(
1045
+ ` ⚠ ${agent}: stale (installed v${entry.version}, current v${currentVersion ?? 'unknown'})`
1046
+ );
1047
+ return false;
1048
+ }
1049
+
1050
+ const missingFiles = entry.files.filter(f => !fs.existsSync(f));
1051
+ if (missingFiles.length > 0) {
1052
+ console.log(` ⚠ ${agent}: partial — missing files:`);
1053
+ for (const f of missingFiles) console.log(` ${f}`);
1054
+ return false;
1055
+ }
1056
+
1057
+ console.log(` ✓ ${agent}: installed (v${entry.version})`);
1058
+ return true;
1059
+ }
1060
+
1061
+ function currentNodeMajor() {
1062
+ return Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
1063
+ }
1064
+
1065
+ // ---------------------------------------------------------------------------
1066
+ // Interactive checkbox prompt
1067
+ // ---------------------------------------------------------------------------
1068
+
1069
+ /**
1070
+ * Run an interactive checkbox list (multiselect with spacebar).
1071
+ *
1072
+ * @param {object} opts
1073
+ * @param {string} opts.message - Prompt message shown above the list
1074
+ * @param {string[]} opts.choices - List of choice labels
1075
+ * @param {string[]} [opts.defaults] - Initially selected choices
1076
+ * @returns {Promise<string[]>} - Array of selected choice labels
1077
+ */
1078
+ function runCheckboxPrompt({ message, choices, defaults = [] }) {
1079
+ return new Promise(resolve => {
1080
+ const hide = '\x1b[?25l';
1081
+ const show = '\x1b[?25h';
1082
+ const saveCursor = '\x1b7';
1083
+ const restoreCursor = '\x1b8';
1084
+ const eraseBelow = '\x1b[J';
1085
+ const cyan = s => `\x1b[36m${s}\x1b[0m`;
1086
+ const bold = s => `\x1b[1m${s}\x1b[0m`;
1087
+ const dim = s => `\x1b[2m${s}\x1b[0m`;
1088
+
1089
+ let cursorIdx = 0;
1090
+ let selected = new Set(defaults);
1091
+ let hasRendered = false;
1092
+
1093
+ function render() {
1094
+ const lines = [];
1095
+ lines.push(bold(message));
1096
+ lines.push(dim(' ↑↓ navigate · Space toggle · Enter confirm · Esc cancel'));
1097
+ lines.push('');
1098
+
1099
+ for (let i = 0; i < choices.length; i++) {
1100
+ const choice = choices[i];
1101
+ const isSelected = selected.has(choice);
1102
+ const isCursor = i === cursorIdx;
1103
+ const checkbox = isSelected ? '[✓]' : '[ ]';
1104
+ const marker = isCursor ? cyan('▶') : ' ';
1105
+ const label = isCursor ? bold(choice) : choice;
1106
+ lines.push(` ${marker} ${checkbox} ${label}`);
1107
+ }
1108
+
1109
+ if (hasRendered) {
1110
+ process.stdout.write(restoreCursor + eraseBelow);
1111
+ }
1112
+ process.stdout.write(saveCursor + lines.join('\n'));
1113
+ hasRendered = true;
1114
+ }
1115
+
1116
+ function cleanup() {
1117
+ if (hasRendered) {
1118
+ process.stdout.write(restoreCursor + eraseBelow);
1119
+ }
1120
+ process.stdin.setRawMode(false);
1121
+ process.stdin.removeAllListeners('data');
1122
+ process.stdout.write(show);
1123
+ }
1124
+
1125
+ process.stdin.setRawMode(true);
1126
+ process.stdin.resume();
1127
+ process.stdin.setEncoding('utf8');
1128
+ process.stdout.write(hide);
1129
+ render();
1130
+
1131
+ process.stdin.on('data', key => {
1132
+ // Ctrl-C / Ctrl-D → exit
1133
+ if (key === '\x03' || key === '\x04') {
1134
+ cleanup();
1135
+ process.exit(0);
1136
+ }
1137
+
1138
+ // Escape → cancel
1139
+ if (key === '\x1b') {
1140
+ cleanup();
1141
+ resolve([]);
1142
+ return;
1143
+ }
1144
+
1145
+ // Enter → confirm selection
1146
+ if (key === '\r' || key === '\n') {
1147
+ cleanup();
1148
+ resolve(Array.from(selected));
1149
+ return;
1150
+ }
1151
+
1152
+ // Arrow up
1153
+ if (key === '\x1b[A') {
1154
+ cursorIdx = (cursorIdx - 1 + choices.length) % choices.length;
1155
+ render();
1156
+ return;
1157
+ }
1158
+
1159
+ // Arrow down
1160
+ if (key === '\x1b[B') {
1161
+ cursorIdx = (cursorIdx + 1) % choices.length;
1162
+ render();
1163
+ return;
1164
+ }
1165
+
1166
+ // Spacebar → toggle selection
1167
+ if (key === ' ') {
1168
+ const choice = choices[cursorIdx];
1169
+ if (selected.has(choice)) {
1170
+ selected.delete(choice);
1171
+ } else {
1172
+ selected.add(choice);
1173
+ }
1174
+ render();
1175
+ return;
1176
+ }
1177
+ });
1178
+ });
1179
+ }
1180
+
1181
+ /**
1182
+ * Ask a yes/no question interactively.
1183
+ *
1184
+ * @param {string} question - The question to ask
1185
+ * @param {boolean} defaultYes - Default answer if user just presses Enter
1186
+ * @returns {Promise<boolean>} - true if yes, false if no
1187
+ */
1188
+ function askYesNo(question, defaultYes = true) {
1189
+ return new Promise(resolve => {
1190
+ const hide = '\x1b[?25l';
1191
+ const show = '\x1b[?25h';
1192
+ const saveCursor = '\x1b7';
1193
+ const restoreCursor = '\x1b8';
1194
+ const eraseBelow = '\x1b[J';
1195
+ const cyan = s => `\x1b[36m${s}\x1b[0m`;
1196
+ const bold = s => `\x1b[1m${s}\x1b[0m`;
1197
+ const dim = s => `\x1b[2m${s}\x1b[0m`;
1198
+
1199
+ const choices = ['Yes', 'No'];
1200
+ let cursorIdx = defaultYes ? 0 : 1;
1201
+ let hasRendered = false;
1202
+
1203
+ function render() {
1204
+ const lines = [];
1205
+ lines.push(bold(question));
1206
+ lines.push('');
1207
+
1208
+ for (let i = 0; i < choices.length; i++) {
1209
+ const choice = choices[i];
1210
+ const isCursor = i === cursorIdx;
1211
+ const marker = isCursor ? cyan('▶') : ' ';
1212
+ const label = isCursor ? bold(choice) : choice;
1213
+ lines.push(` ${marker} ${label}`);
1214
+ }
1215
+
1216
+ lines.push('');
1217
+ lines.push(dim(' ↑↓ navigate · Enter confirm · Esc cancel'));
1218
+
1219
+ if (hasRendered) {
1220
+ process.stdout.write(restoreCursor + eraseBelow);
1221
+ }
1222
+ process.stdout.write(saveCursor + lines.join('\n'));
1223
+ hasRendered = true;
1224
+ }
1225
+
1226
+ function cleanup() {
1227
+ if (hasRendered) {
1228
+ process.stdout.write(restoreCursor + eraseBelow);
1229
+ }
1230
+ process.stdin.setRawMode(false);
1231
+ process.stdin.removeAllListeners('data');
1232
+ process.stdout.write(show);
1233
+ }
1234
+
1235
+ process.stdin.setRawMode(true);
1236
+ process.stdin.resume();
1237
+ process.stdin.setEncoding('utf8');
1238
+ process.stdout.write(hide);
1239
+ render();
1240
+
1241
+ process.stdin.on('data', key => {
1242
+ // Ctrl-C / Ctrl-D → exit
1243
+ if (key === '\x03' || key === '\x04') {
1244
+ cleanup();
1245
+ process.exit(0);
1246
+ }
1247
+
1248
+ // Escape → cancel (default to No)
1249
+ if (key === '\x1b') {
1250
+ cleanup();
1251
+ resolve(false);
1252
+ return;
1253
+ }
1254
+
1255
+ // Enter → confirm selection
1256
+ if (key === '\r' || key === '\n') {
1257
+ cleanup();
1258
+ resolve(cursorIdx === 0);
1259
+ return;
1260
+ }
1261
+
1262
+ // Arrow up
1263
+ if (key === '\x1b[A') {
1264
+ cursorIdx = (cursorIdx - 1 + choices.length) % choices.length;
1265
+ render();
1266
+ return;
1267
+ }
1268
+
1269
+ // Arrow down
1270
+ if (key === '\x1b[B') {
1271
+ cursorIdx = (cursorIdx + 1) % choices.length;
1272
+ render();
1273
+ return;
1274
+ }
1275
+
1276
+ // y/Y → yes
1277
+ if (key === 'y' || key === 'Y') {
1278
+ cleanup();
1279
+ resolve(true);
1280
+ return;
1281
+ }
1282
+
1283
+ // n/N → no
1284
+ if (key === 'n' || key === 'N') {
1285
+ cleanup();
1286
+ resolve(false);
1287
+ return;
1288
+ }
1289
+ });
1290
+ });
1291
+ }
1292
+
1293
+ // ---------------------------------------------------------------------------
1294
+ // Agent permissions installation
1295
+ // ---------------------------------------------------------------------------
1296
+
1297
+ function getPlatformUrl() {
1298
+ // Check for OVERLORD_URL env var first, otherwise default to localhost
1299
+ return process.env.OVERLORD_URL || 'http://localhost:3000';
1300
+ }
1301
+
1302
+ function installAgentPermissions(agents, platformUrl) {
1303
+ console.log(`\nInstalling agent permissions for: ${agents.join(', ')}`);
1304
+ console.log(`Platform URL: ${platformUrl}\n`);
1305
+
1306
+ for (const agent of agents) {
1307
+ if (agent === 'claude') {
1308
+ installClaudePermissions(platformUrl);
1309
+ } else if (agent === 'opencode') {
1310
+ installOpenCodePermissions(platformUrl);
1311
+ } else if (agent === 'codex') {
1312
+ installCodexPermissions(platformUrl);
1313
+ }
1314
+ // cursor and gemini don't have permission configuration
1315
+ }
1316
+ }
1317
+
1318
+ function installClaudePermissions(platformUrl) {
1319
+ const settingsPath = path.join(process.cwd(), '.claude', 'settings.local.json');
1320
+ console.log(`--- Claude Code ---`);
1321
+ console.log(`Settings file: ${settingsPath}`);
1322
+
1323
+ let settings = { permissions: { allow: [] } };
1324
+ if (fs.existsSync(settingsPath)) {
1325
+ try {
1326
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
1327
+ } catch (e) {
1328
+ console.error(` ERROR: Could not parse ${settingsPath}: ${e.message}`);
1329
+ return false;
1330
+ }
1331
+ if (!settings.permissions) settings.permissions = {};
1332
+ if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
1333
+ }
1334
+
1335
+ const entries = ['Bash(ovld protocol:*)'];
1336
+
1337
+ const existing = new Set(settings.permissions.allow);
1338
+ const toAdd = entries.filter(e => !existing.has(e));
1339
+
1340
+ if (toAdd.length === 0) {
1341
+ console.log(' All required permissions already present. Nothing to do.\n');
1342
+ return true;
1343
+ }
1344
+
1345
+ console.log(` Adding ${toAdd.length} permission entries:`);
1346
+ for (const entry of toAdd) {
1347
+ console.log(` + ${entry}`);
1348
+ }
1349
+
1350
+ // Backup
1351
+ if (fs.existsSync(settingsPath)) {
1352
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
1353
+ const backupPath = `${settingsPath}.backup-${ts}`;
1354
+ fs.copyFileSync(settingsPath, backupPath);
1355
+ console.log(` Backup: ${backupPath}`);
1356
+ }
1357
+
1358
+ settings.permissions.allow = [...settings.permissions.allow, ...toAdd];
1359
+
1360
+ const dir = path.dirname(settingsPath);
1361
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1362
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
1363
+ console.log(' Settings updated.\n');
1364
+ return true;
1365
+ }
1366
+
1367
+ function installOpenCodePermissions(_platformUrl) {
1368
+ console.log(`--- OpenCode ---`);
1369
+ const configPath = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
1370
+ console.log(`Config file: ${configPath}`);
1371
+
1372
+ let config = {};
1373
+ if (fs.existsSync(configPath)) {
1374
+ try {
1375
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1376
+ } catch (e) {
1377
+ console.error(` ERROR: Could not parse ${configPath}: ${e.message}`);
1378
+ return false;
1379
+ }
1380
+ }
1381
+
1382
+ const existingPermission =
1383
+ config.permission && typeof config.permission === 'object' ? config.permission : {};
1384
+ const existingBash =
1385
+ existingPermission.bash && typeof existingPermission.bash === 'object'
1386
+ ? existingPermission.bash
1387
+ : {};
1388
+
1389
+ const next = {
1390
+ ...config,
1391
+ $schema: 'https://opencode.ai/config.json',
1392
+ permission: {
1393
+ ...existingPermission,
1394
+ bash: {
1395
+ '*': 'ask',
1396
+ ...existingBash,
1397
+ 'ovld protocol *': 'allow'
1398
+ }
1399
+ }
1400
+ };
1401
+
1402
+ if (fs.existsSync(configPath)) {
1403
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
1404
+ const backupPath = `${configPath}.backup-${ts}`;
1405
+ fs.copyFileSync(configPath, backupPath);
1406
+ console.log(` Backup: ${backupPath}`);
1407
+ }
1408
+
1409
+ const dir = path.dirname(configPath);
1410
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1411
+ fs.writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');
1412
+ console.log(' Config updated.\n');
1413
+ return true;
1414
+ }
1415
+
1416
+ function installCodexPermissions() {
1417
+ console.log(`--- Codex ---`);
1418
+ console.log(' Codex does not support file-based permission configuration.');
1419
+ console.log(' To warm up permissions, run the following command once inside a Codex session:');
1420
+ console.log(' (Codex will prompt for approval; approve it to persist the prefix.)\n');
1421
+ console.log(' ovld protocol help');
1422
+ console.log();
1423
+ return true;
1424
+ }
1425
+
1426
+ // ---------------------------------------------------------------------------
1427
+ // Public API
1428
+ // ---------------------------------------------------------------------------
1429
+
1430
+ export async function runSetupCommand(args) {
1431
+ const agent = args[0];
1432
+
1433
+ if (agent === '--help' || agent === '-h' || agent === 'help') {
1434
+ console.log(`Usage:
1435
+ ovld setup Interactive setup (select agents and configure permissions)
1436
+ ovld setup claude Prepare the Overlord Claude plugin and migrate v3.25 connector files
1437
+ ovld setup codex Install Overlord Codex plugin bundle
1438
+ ovld setup cursor Install Overlord Cursor local plugin and permissions
1439
+ ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
1440
+ ovld setup opencode Install Overlord connector for OpenCode
1441
+ ovld setup all Prepare all supported agents
1442
+ ovld doctor Validate installed connectors and check for CLI updates`);
1443
+ return;
1444
+ }
1445
+
1446
+ // Interactive mode when called without arguments
1447
+ if (!agent) {
1448
+ console.log('Welcome to Overlord agent setup!\n');
1449
+
1450
+ // Step 1: Select agents to install
1451
+ const agentLabels = supportedAgents.map(a => {
1452
+ const descriptions = {
1453
+ claude: 'Claude Code',
1454
+ codex: 'Codex',
1455
+ cursor: 'Cursor',
1456
+ gemini: 'Gemini CLI',
1457
+ opencode: 'OpenCode'
1458
+ };
1459
+ return `${a.padEnd(10)} - ${descriptions[a] || a}`;
1460
+ });
1461
+
1462
+ const selectedLabels = await runCheckboxPrompt({
1463
+ message: 'Select agent plugins/connectors to prepare (Space to toggle, Enter to confirm):',
1464
+ choices: agentLabels,
1465
+ defaults: []
1466
+ });
1467
+
1468
+ if (selectedLabels.length === 0) {
1469
+ console.log('\nNo agents selected. Setup cancelled.');
1470
+ return;
1471
+ }
1472
+
1473
+ // Extract agent names from selected labels
1474
+ const selectedAgents = selectedLabels.map(label => label.split('-')[0].trim());
1475
+
1476
+ // Step 2: Install selected agents
1477
+ console.log(
1478
+ `\nPreparing Overlord agent plugins/connectors for: ${selectedAgents.join(', ')}...\n`
1479
+ );
1480
+
1481
+ const installedAgents = [];
1482
+ for (const a of selectedAgents) {
1483
+ console.log(`[${a}]`);
1484
+ try {
1485
+ if (a === 'claude') installClaude();
1486
+ else if (a === 'codex') installCodex();
1487
+ else if (a === 'cursor') installCursor();
1488
+ else if (a === 'gemini') installGemini();
1489
+ else installOpenCode();
1490
+ installedAgents.push(a);
1491
+ } catch (err) {
1492
+ console.error(` ✗ Failed: ${err.message}`);
1493
+ }
1494
+ console.log();
1495
+ }
1496
+
1497
+ if (installedAgents.length === 0) {
1498
+ console.log('No agents were successfully installed.');
1499
+ return;
1500
+ }
1501
+
1502
+ // Step 3: Offer to configure agent permissions
1503
+ const agentsThatNeedPermissions = installedAgents.filter(a =>
1504
+ ['claude', 'codex', 'opencode'].includes(a)
1505
+ );
1506
+
1507
+ if (agentsThatNeedPermissions.length > 0) {
1508
+ console.log('Agent plugins/connectors prepared successfully!\n');
1509
+
1510
+ const shouldInstallPermissions = await askYesNo(
1511
+ 'Would you like to configure agent permissions for Overlord protocol access?',
1512
+ true
1513
+ );
1514
+
1515
+ if (shouldInstallPermissions) {
1516
+ const platformUrl = getPlatformUrl();
1517
+ installAgentPermissions(agentsThatNeedPermissions, platformUrl);
1518
+ console.log('✓ Agent permissions configured.\n');
1519
+ } else {
1520
+ console.log('\nSkipped agent permissions configuration.');
1521
+ console.log('You can run the permission installer later with:');
1522
+ console.log(' node scripts/install-agent-permissions.mjs\n');
1523
+ }
1524
+ }
1525
+
1526
+ console.log('Setup complete! Run `ovld doctor` to verify your installation.');
1527
+ return;
1528
+ }
1529
+
1530
+ if (agent === 'all') {
1531
+ console.log('Preparing Overlord agent plugins/connectors for all supported agents...\n');
1532
+ const installedAgents = [];
1533
+
1534
+ for (const a of supportedAgents) {
1535
+ console.log(`[${a}]`);
1536
+ try {
1537
+ if (a === 'claude') installClaude();
1538
+ else if (a === 'codex') installCodex();
1539
+ else if (a === 'cursor') installCursor();
1540
+ else if (a === 'gemini') installGemini();
1541
+ else installOpenCode();
1542
+ installedAgents.push(a);
1543
+ } catch (err) {
1544
+ console.error(` ✗ Failed: ${err.message}`);
1545
+ }
1546
+ console.log();
1547
+ }
1548
+
1549
+ // Offer permissions setup for 'all' command too
1550
+ const agentsThatNeedPermissions = installedAgents.filter(a =>
1551
+ ['claude', 'codex', 'opencode'].includes(a)
1552
+ );
1553
+
1554
+ if (agentsThatNeedPermissions.length > 0) {
1555
+ const shouldInstallPermissions = await askYesNo(
1556
+ '\nWould you like to configure agent permissions for Overlord protocol access?',
1557
+ true
1558
+ );
1559
+
1560
+ if (shouldInstallPermissions) {
1561
+ const platformUrl = getPlatformUrl();
1562
+ installAgentPermissions(agentsThatNeedPermissions, platformUrl);
1563
+ }
1564
+ }
1565
+
1566
+ console.log('Done.');
1567
+ return;
1568
+ }
1569
+
1570
+ if (!supportedAgents.includes(agent)) {
1571
+ console.error(
1572
+ `Unknown agent: ${agent ?? '(none)'}. Supported: ${supportedAgents.join(', ')}, all`
1573
+ );
1574
+ process.exit(1);
1575
+ }
1576
+
1577
+ console.log(`Preparing Overlord agent plugin/connector for ${agent}...\n`);
1578
+ try {
1579
+ if (agent === 'claude') installClaude();
1580
+ else if (agent === 'codex') installCodex();
1581
+ else if (agent === 'cursor') installCursor();
1582
+ else if (agent === 'gemini') installGemini();
1583
+ else installOpenCode();
1584
+ console.log('\nDone.');
1585
+
1586
+ // Offer permissions setup for single agent install too
1587
+ if (['claude', 'codex', 'opencode'].includes(agent)) {
1588
+ const shouldInstallPermissions = await askYesNo(
1589
+ '\nWould you like to configure agent permissions for Overlord protocol access?',
1590
+ true
1591
+ );
1592
+
1593
+ if (shouldInstallPermissions) {
1594
+ const platformUrl = getPlatformUrl();
1595
+ installAgentPermissions([agent], platformUrl);
1596
+ }
1597
+ }
1598
+ } catch (err) {
1599
+ console.error(`\nFailed: ${err.message}`);
1600
+ process.exit(1);
1601
+ }
1602
+ }
1603
+
1604
+ export async function runDoctorCommand({ latestCliVersion = null } = {}) {
1605
+ console.log('Overlord agent bundle status:\n');
1606
+ let allOk = true;
1607
+ const nodeMajor = currentNodeMajor();
1608
+ if (Number.isNaN(nodeMajor) || nodeMajor < REQUIRED_NODE_MAJOR) {
1609
+ console.log(
1610
+ ` ✗ node: unsupported runtime (${process.version}; requires Node.js ${REQUIRED_NODE_MAJOR}+)`
1611
+ );
1612
+ allOk = false;
1613
+ } else {
1614
+ console.log(` ✓ node: ${process.version}`);
1615
+ }
1616
+ console.log();
1617
+ for (const agent of supportedAgents) {
1618
+ if (!doctorAgent(agent)) allOk = false;
1619
+ }
1620
+ const updateVersion = latestCliVersion ?? (await checkForCliUpdate());
1621
+ console.log();
1622
+ if (allOk) {
1623
+ console.log('All bundles are up to date.');
1624
+ } else {
1625
+ console.log('Run `ovld setup <agent>` or `ovld setup all` to install/repair.');
1626
+ }
1627
+ if (updateVersion) {
1628
+ console.log();
1629
+ printCliUpdateNotice(updateVersion, {
1630
+ currentVersion: getCurrentCliVersion(),
1631
+ stream: process.stdout
1632
+ });
1633
+ }
1634
+ }