overlord-cli 3.5.2 → 3.5.4

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.
@@ -11,14 +11,29 @@ import crypto from 'node:crypto';
11
11
  import fs from 'node:fs';
12
12
  import os from 'node:os';
13
13
  import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
14
15
 
15
- const BUNDLE_VERSION = '1.5.0';
16
+ const BUNDLE_VERSION = '1.8.0';
16
17
  const MD_MARKER_START = '<!-- overlord:managed:start -->';
17
18
  const MD_MARKER_END = '<!-- overlord:managed:end -->';
18
19
  const MANIFEST_DIR = path.join(os.homedir(), '.ovld');
19
20
  const MANIFEST_FILE = path.join(MANIFEST_DIR, 'bundle-manifest.json');
20
-
21
- const supportedAgents = ['claude', 'opencode'];
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const PACKAGE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'overlord');
23
+ const REPO_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'overlord');
24
+ const CODEX_TARGET_PLUGIN_DIR = path.join(os.homedir(), '.codex', 'plugins', 'overlord');
25
+ const CODEX_TARGET_PLUGIN_MANIFEST = path.join(
26
+ CODEX_TARGET_PLUGIN_DIR,
27
+ '.codex-plugin',
28
+ 'plugin.json'
29
+ );
30
+ const CODEX_TARGET_MARKETPLACE = path.join(os.homedir(), '.agents', 'plugins', 'marketplace.json');
31
+ const CODEX_TARGET_RULES = path.join(os.homedir(), '.codex', 'rules', 'default.rules');
32
+ const CODEX_LEGACY_AGENTS = path.join(os.homedir(), '.codex', 'AGENTS.md');
33
+ const CODEX_RULES_START = '# overlord:permissions:start';
34
+ const CODEX_RULES_END = '# overlord:permissions:end';
35
+
36
+ const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
22
37
 
23
38
  // ---------------------------------------------------------------------------
24
39
  // Templates (same content as electron/services/agent-bundle/templates.ts)
@@ -185,6 +200,40 @@ fi
185
200
  exit 0
186
201
  `;
187
202
 
203
+ const CURSOR_RULES_CONTENT = `---
204
+ description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work.
205
+ globs:
206
+ alwaysApply: true
207
+ ---
208
+
209
+ # Overlord Local Workflow
210
+
211
+ If a prompt includes a TICKET_ID, attach first with:
212
+ \`\`\`bash
213
+ ovld protocol attach --ticket-id $TICKET_ID
214
+ \`\`\`
215
+
216
+ During work, post progress updates with:
217
+ \`\`\`bash
218
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
219
+ \`\`\`
220
+
221
+ If blocked on human input, ask with:
222
+ \`\`\`bash
223
+ ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
224
+ \`\`\`
225
+
226
+ When done, deliver with artifacts and change rationales:
227
+ \`\`\`bash
228
+ 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 @@"}]}]'
229
+ \`\`\`
230
+
231
+ Rules:
232
+ - Always attach first and deliver last.
233
+ - Use \`ovld protocol\` commands instead of ad hoc repo scripts for ticket lifecycle work.
234
+ - If the user sends a new message during an active ticket session, publish a \`user_follow_up\` event before doing anything else.
235
+ `;
236
+
188
237
  // ---------------------------------------------------------------------------
189
238
  // Helpers
190
239
  // ---------------------------------------------------------------------------
@@ -215,6 +264,23 @@ function readJsonFile(filePath) {
215
264
  }
216
265
  }
217
266
 
267
+ function readJsonFileOrNull(filePath) {
268
+ try {
269
+ if (!fs.existsSync(filePath)) return null;
270
+ const raw = fs.readFileSync(filePath, 'utf-8');
271
+ const parsed = JSON.parse(raw);
272
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
273
+ return null;
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ function localCliVersion() {
280
+ const cliPackage = readJsonFileOrNull(path.resolve(__dirname, '..', '..', 'package.json'));
281
+ return typeof cliPackage?.version === 'string' ? cliPackage.version : null;
282
+ }
283
+
218
284
  function writeJsonFile(filePath, data) {
219
285
  const dir = path.dirname(filePath);
220
286
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -241,6 +307,11 @@ function deepClone(obj) {
241
307
  return JSON.parse(JSON.stringify(obj));
242
308
  }
243
309
 
310
+ function asStringArray(value) {
311
+ if (!Array.isArray(value)) return [];
312
+ return value.filter(entry => typeof entry === 'string');
313
+ }
314
+
244
315
  function mergeMarkdownSection(existing, newContent) {
245
316
  const wrappedContent = `${MD_MARKER_START}\n${newContent.trim()}\n${MD_MARKER_END}`;
246
317
  const startIdx = existing.indexOf(MD_MARKER_START);
@@ -262,6 +333,316 @@ function writeManifest(manifest) {
262
333
  writeJsonFile(MANIFEST_FILE, manifest);
263
334
  }
264
335
 
336
+ function slashCommandFiles(agent) {
337
+ if (agent === 'claude') {
338
+ const base = path.join(os.homedir(), '.claude', 'commands');
339
+ return [
340
+ {
341
+ path: path.join(base, 'connect.md'),
342
+ content: `---
343
+ description: Connect this session to another Overlord ticket by ticket ID
344
+ argument-hint: <ticket-id>
345
+ disable-model-invocation: true
346
+ ---
347
+
348
+ Run \`ovld protocol connect --ticket-id <ticketId>\` using \`$ARGUMENTS\` as the ticket ID.`
349
+ },
350
+ {
351
+ path: path.join(base, 'load.md'),
352
+ content: `---
353
+ description: Load Overlord ticket context without creating a new session
354
+ argument-hint: <ticket-id>
355
+ disable-model-invocation: true
356
+ ---
357
+
358
+ Run \`ovld protocol load-context --ticket-id <ticketId>\` using \`$ARGUMENTS\` as the ticket ID.`
359
+ },
360
+ {
361
+ path: path.join(base, 'spawn.md'),
362
+ content: `---
363
+ description: Create a new Overlord ticket from the current conversation
364
+ argument-hint: <objective or raw flags>
365
+ disable-model-invocation: true
366
+ ---
367
+
368
+ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
369
+ }
370
+ ];
371
+ }
372
+
373
+ if (agent === 'cursor') {
374
+ const base = path.join(os.homedir(), '.cursor', 'commands');
375
+ return [
376
+ {
377
+ path: path.join(base, 'connect.md'),
378
+ content:
379
+ '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'
380
+ },
381
+ {
382
+ path: path.join(base, 'load.md'),
383
+ content:
384
+ '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'
385
+ },
386
+ {
387
+ path: path.join(base, 'spawn.md'),
388
+ content:
389
+ 'Create a new Overlord ticket.\n\nRun `ovld protocol spawn --objective "<objective>"` using the text after `/spawn` unless raw flags were provided.\n'
390
+ }
391
+ ];
392
+ }
393
+
394
+ if (agent === 'gemini') {
395
+ const base = path.join(os.homedir(), '.gemini', 'commands');
396
+ return [
397
+ {
398
+ path: path.join(base, 'connect.toml'),
399
+ content:
400
+ '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'
401
+ },
402
+ {
403
+ path: path.join(base, 'load.toml'),
404
+ content:
405
+ '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'
406
+ },
407
+ {
408
+ path: path.join(base, 'spawn.toml'),
409
+ content:
410
+ 'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol spawn --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided.\n"""\n'
411
+ }
412
+ ];
413
+ }
414
+
415
+ const base = path.join(os.homedir(), '.config', 'opencode', 'commands');
416
+ return [
417
+ {
418
+ path: path.join(base, 'connect.md'),
419
+ content: `---
420
+ description: Connect this session to another Overlord ticket by ticket ID
421
+ agent: build
422
+ ---
423
+
424
+ 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.`
425
+ },
426
+ {
427
+ path: path.join(base, 'load.md'),
428
+ content: `---
429
+ description: Load Overlord ticket context without creating a new session
430
+ agent: build
431
+ ---
432
+
433
+ 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.`
434
+ },
435
+ {
436
+ path: path.join(base, 'spawn.md'),
437
+ content: `---
438
+ description: Create a new Overlord ticket from the current conversation
439
+ agent: build
440
+ ---
441
+
442
+ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
443
+ }
444
+ ];
445
+ }
446
+
447
+ function installSlashCommands(agent) {
448
+ const backups = [];
449
+ const files = slashCommandFiles(agent);
450
+ for (const file of files) {
451
+ const backup = backupFile(file.path);
452
+ if (backup) backups.push(backup);
453
+ writeTextFile(file.path, `${file.content.trim()}\n`);
454
+ console.log(` ✓ Installed slash command: ${file.path}`);
455
+ }
456
+ return {
457
+ backups,
458
+ managedFiles: files.map(file => file.path)
459
+ };
460
+ }
461
+
462
+ function currentContentHashForAgent(agent) {
463
+ if (agent === 'claude') {
464
+ return contentHash(
465
+ [CLAUDE_SKILL_CONTENT, PERMISSION_HOOK_SCRIPT, ...slashCommandFiles('claude').map(file => file.content)].join('\n')
466
+ );
467
+ }
468
+ if (agent === 'cursor') {
469
+ return contentHash(
470
+ [CURSOR_RULES_CONTENT, ...slashCommandFiles('cursor').map(file => file.content)].join('\n')
471
+ );
472
+ }
473
+ if (agent === 'gemini') {
474
+ return contentHash(slashCommandFiles('gemini').map(file => file.content).join('\n'));
475
+ }
476
+ if (agent === 'codex') return codexContentHash();
477
+ return contentHash(
478
+ [OPENCODE_AGENTS_SECTION, ...slashCommandFiles('opencode').map(file => file.content)].join('\n')
479
+ );
480
+ }
481
+
482
+ async function checkForCliUpdate() {
483
+ const currentVersion = localCliVersion();
484
+ if (!currentVersion) return null;
485
+ const controller = new AbortController();
486
+ const timeout = setTimeout(() => controller.abort(), 2500);
487
+ try {
488
+ const response = await fetch('https://registry.npmjs.org/overlord-cli/latest', {
489
+ signal: controller.signal,
490
+ headers: { Accept: 'application/json' }
491
+ });
492
+ if (!response.ok) return null;
493
+ const payload = await response.json();
494
+ const latestVersion = typeof payload?.version === 'string' ? payload.version : null;
495
+ if (!latestVersion) return null;
496
+ return latestVersion === currentVersion ? null : latestVersion;
497
+ } catch {
498
+ return null;
499
+ } finally {
500
+ clearTimeout(timeout);
501
+ }
502
+ }
503
+
504
+ function codexSourcePluginDir() {
505
+ if (fs.existsSync(PACKAGE_PLUGIN_DIR)) return PACKAGE_PLUGIN_DIR;
506
+ if (fs.existsSync(REPO_PLUGIN_DIR)) return REPO_PLUGIN_DIR;
507
+ throw new Error(
508
+ `Codex plugin bundle not found. Checked ${PACKAGE_PLUGIN_DIR} and ${REPO_PLUGIN_DIR}.`
509
+ );
510
+ }
511
+
512
+ function listFilesRecursive(dir) {
513
+ if (!fs.existsSync(dir)) return [];
514
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
515
+ const resolved = path.join(dir, entry.name);
516
+ if (entry.isDirectory()) return listFilesRecursive(resolved);
517
+ return [resolved];
518
+ });
519
+ }
520
+
521
+ function codexContentHash() {
522
+ const sourceDir = codexSourcePluginDir();
523
+ const hash = crypto.createHash('sha256');
524
+
525
+ for (const filePath of listFilesRecursive(sourceDir).sort()) {
526
+ hash.update(path.relative(sourceDir, filePath));
527
+ hash.update('\0');
528
+ hash.update(fs.readFileSync(filePath));
529
+ hash.update('\0');
530
+ }
531
+
532
+ return hash.digest('hex').slice(0, 16);
533
+ }
534
+
535
+ function mergeCodexRules(existingContent) {
536
+ const managedBlock = [
537
+ CODEX_RULES_START,
538
+ 'prefix_rule(',
539
+ ' pattern = ["npx", "overlord", "protocol"],',
540
+ ' decision = "allow",',
541
+ ' justification = "Allow all Overlord protocol commands without prompts.",',
542
+ ')',
543
+ '',
544
+ 'prefix_rule(',
545
+ ' pattern = ["ovld", "protocol"],',
546
+ ' decision = "allow",',
547
+ ' justification = "Allow all Overlord protocol commands without prompts.",',
548
+ ')',
549
+ '',
550
+ 'prefix_rule(',
551
+ ' pattern = ["curl", "-sS", "-X", "POST"],',
552
+ ' decision = "allow",',
553
+ ' justification = "Allow curl protocol POST commands without prompts.",',
554
+ ')',
555
+ CODEX_RULES_END
556
+ ].join('\n');
557
+
558
+ const startIndex = existingContent.indexOf(CODEX_RULES_START);
559
+ const endIndex = existingContent.indexOf(CODEX_RULES_END);
560
+
561
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
562
+ const before = existingContent.slice(0, startIndex).trimEnd();
563
+ const after = existingContent.slice(endIndex + CODEX_RULES_END.length).trimStart();
564
+ if (!before && !after) return `${managedBlock}\n`;
565
+ if (!before) return `${managedBlock}\n\n${after}`;
566
+ if (!after) return `${before}\n\n${managedBlock}\n`;
567
+ return `${before}\n\n${managedBlock}\n\n${after}`;
568
+ }
569
+
570
+ const trimmed = existingContent.trimEnd();
571
+ if (!trimmed) return `${managedBlock}\n`;
572
+ return `${trimmed}\n\n${managedBlock}\n`;
573
+ }
574
+
575
+ function pluginVersion(filePath) {
576
+ const parsed = readJsonFileOrNull(filePath);
577
+ return typeof parsed?.version === 'string' ? parsed.version : null;
578
+ }
579
+
580
+ function upsertCodexMarketplaceEntry() {
581
+ const current = readJsonFileOrNull(CODEX_TARGET_MARKETPLACE) ?? {
582
+ name: 'overlord-local',
583
+ interface: { displayName: 'Overlord Local Plugins' },
584
+ plugins: []
585
+ };
586
+
587
+ const nextPlugins = Array.isArray(current.plugins) ? [...current.plugins] : [];
588
+ const entry = {
589
+ name: 'overlord',
590
+ source: {
591
+ source: 'local',
592
+ path: './.codex/plugins/overlord'
593
+ },
594
+ policy: {
595
+ installation: 'AVAILABLE',
596
+ authentication: 'ON_INSTALL'
597
+ },
598
+ category: 'Productivity'
599
+ };
600
+
601
+ const existingIndex = nextPlugins.findIndex(plugin => plugin?.name === 'overlord');
602
+ if (existingIndex === -1) nextPlugins.push(entry);
603
+ else nextPlugins[existingIndex] = entry;
604
+
605
+ writeJsonFile(CODEX_TARGET_MARKETPLACE, {
606
+ name: current.name ?? 'overlord-local',
607
+ interface: {
608
+ displayName: current.interface?.displayName ?? 'Overlord Local Plugins'
609
+ },
610
+ plugins: nextPlugins
611
+ });
612
+ }
613
+
614
+ function removeLegacyCodexBundle() {
615
+ if (fs.existsSync(CODEX_LEGACY_AGENTS)) {
616
+ const existing = readTextFile(CODEX_LEGACY_AGENTS);
617
+ const startIndex = existing.indexOf(MD_MARKER_START);
618
+ const endIndex = existing.indexOf(MD_MARKER_END);
619
+
620
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
621
+ const before = existing.slice(0, startIndex).trimEnd();
622
+ const after = existing.slice(endIndex + MD_MARKER_END.length).trimStart();
623
+ const cleaned =
624
+ !before && !after
625
+ ? ''
626
+ : !before
627
+ ? `${after}\n`
628
+ : !after
629
+ ? `${before}\n`
630
+ : `${before}\n\n${after}\n`;
631
+
632
+ if (cleaned.trim().length > 0) {
633
+ writeTextFile(CODEX_LEGACY_AGENTS, cleaned);
634
+ } else {
635
+ fs.rmSync(CODEX_LEGACY_AGENTS, { force: true });
636
+ }
637
+ }
638
+ }
639
+
640
+ const manifest = readManifest();
641
+ if (!manifest.codex) return;
642
+ delete manifest.codex;
643
+ writeManifest(manifest);
644
+ }
645
+
265
646
  // ---------------------------------------------------------------------------
266
647
  // Install
267
648
  // ---------------------------------------------------------------------------
@@ -285,6 +666,21 @@ function openCodePaths() {
285
666
  };
286
667
  }
287
668
 
669
+ function cursorPaths() {
670
+ const base = path.join(os.homedir(), '.cursor');
671
+ return {
672
+ rulesFile: path.join(base, 'rules', 'overlord-local.mdc'),
673
+ settingsFile: path.join(base, 'settings.json')
674
+ };
675
+ }
676
+
677
+ function geminiPaths() {
678
+ const base = path.join(os.homedir(), '.gemini');
679
+ return {
680
+ policyFile: path.join(base, 'policies', 'overlord-protocol.toml')
681
+ };
682
+ }
683
+
288
684
  function installClaude() {
289
685
  const paths = claudePaths();
290
686
  const backups = [];
@@ -314,6 +710,17 @@ function installClaude() {
314
710
  const existingPermHooks = Array.isArray(existingHooks.PermissionRequest)
315
711
  ? existingHooks.PermissionRequest
316
712
  : [];
713
+ const existingPermissions =
714
+ existingSettings.permissions && typeof existingSettings.permissions === 'object'
715
+ ? existingSettings.permissions
716
+ : {};
717
+ const mergedAllow = Array.from(
718
+ new Set([
719
+ ...asStringArray(existingPermissions.allow),
720
+ 'Bash(ovld protocol:*)',
721
+ 'Bash(curl -sS -X POST:*)'
722
+ ])
723
+ );
317
724
 
318
725
  // Remove existing Overlord hooks
319
726
  const filteredPermHooks = existingPermHooks.filter(hook => {
@@ -328,21 +735,25 @@ function installClaude() {
328
735
 
329
736
  const merged = deepClone(existingSettings);
330
737
  merged.hooks = { ...existingHooks, PermissionRequest: [...filteredPermHooks, overlordHook] };
738
+ merged.permissions = { ...existingPermissions, allow: mergedAllow };
331
739
  merged.__overlord_managed = {
332
740
  version: BUNDLE_VERSION,
333
- paths: ['hooks.PermissionRequest'],
741
+ paths: ['hooks.PermissionRequest', 'permissions.allow'],
334
742
  updatedAt: new Date().toISOString()
335
743
  };
336
744
  writeJsonFile(paths.settingsFile, merged);
337
745
  console.log(` ✓ Merged hook into: ${paths.settingsFile}`);
338
746
 
747
+ const slashResult = installSlashCommands('claude');
748
+ backups.push(...slashResult.backups);
749
+
339
750
  // 4. Update manifest
340
751
  const manifest = readManifest();
341
752
  manifest.claude = {
342
753
  version: BUNDLE_VERSION,
343
- contentHash: contentHash(CLAUDE_SKILL_CONTENT),
754
+ contentHash: currentContentHashForAgent('claude'),
344
755
  installedAt: new Date().toISOString(),
345
- files: [paths.skillFile, paths.hookScript, paths.settingsFile]
756
+ files: [paths.skillFile, paths.hookScript, paths.settingsFile, ...slashResult.managedFiles]
346
757
  };
347
758
  writeManifest(manifest);
348
759
 
@@ -400,56 +811,124 @@ function installOpenCode() {
400
811
  });
401
812
  console.log(` ✓ Updated config: ${paths.configFile}`);
402
813
 
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
- ---
814
+ const slashResult = installSlashCommands('opencode');
815
+ backups.push(...slashResult.backups);
410
816
 
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
- ---
817
+ const manifest = readManifest();
818
+ manifest.opencode = {
819
+ version: BUNDLE_VERSION,
820
+ contentHash: currentContentHashForAgent('opencode'),
821
+ installedAt: new Date().toISOString(),
822
+ files: [paths.agentsFile, paths.configFile, ...slashResult.managedFiles]
823
+ };
824
+ writeManifest(manifest);
419
825
 
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
- ---
826
+ return { ok: true, backups };
827
+ }
428
828
 
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
- }
829
+ function installCodex() {
830
+ const sourceDir = codexSourcePluginDir();
831
+ fs.mkdirSync(path.dirname(CODEX_TARGET_PLUGIN_DIR), { recursive: true });
832
+ fs.rmSync(CODEX_TARGET_PLUGIN_DIR, { recursive: true, force: true });
833
+ fs.cpSync(sourceDir, CODEX_TARGET_PLUGIN_DIR, { recursive: true });
834
+ console.log(` ✓ Installed plugin: ${CODEX_TARGET_PLUGIN_DIR}`);
835
+
836
+ writeTextFile(CODEX_TARGET_RULES, mergeCodexRules(readTextFile(CODEX_TARGET_RULES)));
837
+ console.log(` ✓ Updated rules: ${CODEX_TARGET_RULES}`);
838
+
839
+ upsertCodexMarketplaceEntry();
840
+ console.log(` ✓ Updated marketplace: ${CODEX_TARGET_MARKETPLACE}`);
841
+
842
+ removeLegacyCodexBundle();
843
+
844
+ const installedFiles = [
845
+ ...listFilesRecursive(CODEX_TARGET_PLUGIN_DIR),
846
+ CODEX_TARGET_MARKETPLACE,
847
+ CODEX_TARGET_RULES
431
848
  ];
849
+ const manifest = readManifest();
850
+ manifest.codex = {
851
+ version: pluginVersion(CODEX_TARGET_PLUGIN_MANIFEST) ?? '0.0.0',
852
+ contentHash: codexContentHash(),
853
+ installedAt: new Date().toISOString(),
854
+ files: installedFiles
855
+ };
856
+ writeManifest(manifest);
857
+
858
+ return { ok: true, installedFiles };
859
+ }
432
860
 
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)}`);
861
+ function installCursor() {
862
+ const paths = cursorPaths();
863
+ writeTextFile(paths.rulesFile, CURSOR_RULES_CONTENT);
864
+ console.log(` ✓ Installed rules: ${paths.rulesFile}`);
865
+
866
+ const slashResult = installSlashCommands('cursor');
867
+
868
+ const existingSettings = readJsonFile(paths.settingsFile);
869
+ const permissions =
870
+ existingSettings.permissions && typeof existingSettings.permissions === 'object'
871
+ ? existingSettings.permissions
872
+ : {};
873
+ const mergedAllow = Array.from(
874
+ new Set([
875
+ ...asStringArray(permissions.allow),
876
+ 'Shell(ovld protocol:*)',
877
+ 'Shell(curl -sS -X POST:*)'
878
+ ])
879
+ );
880
+ writeJsonFile(paths.settingsFile, {
881
+ ...existingSettings,
882
+ permissions: {
883
+ ...permissions,
884
+ allow: mergedAllow
438
885
  }
439
- writeTextFile(commandFile.file, `${commandFile.content.trim()}\n`);
440
- console.log(` ✓ Installed slash command: ${commandFile.file}`);
441
- }
886
+ });
887
+ console.log(` ✓ Updated permissions: ${paths.settingsFile}`);
442
888
 
443
889
  const manifest = readManifest();
444
- manifest.opencode = {
890
+ manifest.cursor = {
445
891
  version: BUNDLE_VERSION,
446
- contentHash: contentHash(OPENCODE_AGENTS_SECTION),
892
+ contentHash: currentContentHashForAgent('cursor'),
447
893
  installedAt: new Date().toISOString(),
448
- files: [paths.agentsFile, paths.configFile, ...commandFiles.map(entry => entry.file)]
894
+ files: [paths.rulesFile, paths.settingsFile, ...slashResult.managedFiles]
449
895
  };
450
896
  writeManifest(manifest);
451
897
 
452
- return { ok: true, backups };
898
+ return { ok: true };
899
+ }
900
+
901
+ function installGemini() {
902
+ const slashResult = installSlashCommands('gemini');
903
+ const paths = geminiPaths();
904
+ const policyContent = [
905
+ '# Managed by Overlord onboarding',
906
+ '[[rule]]',
907
+ 'toolName = "run_shell_command"',
908
+ 'commandPrefix = "ovld protocol"',
909
+ 'decision = "allow"',
910
+ 'priority = 900',
911
+ '',
912
+ '[[rule]]',
913
+ 'toolName = "run_shell_command"',
914
+ 'commandPrefix = "curl -sS -X POST"',
915
+ 'decision = "allow"',
916
+ 'priority = 900',
917
+ ''
918
+ ].join('\n');
919
+ writeTextFile(paths.policyFile, policyContent);
920
+ console.log(` ✓ Installed policy: ${paths.policyFile}`);
921
+
922
+ const manifest = readManifest();
923
+ manifest.gemini = {
924
+ version: BUNDLE_VERSION,
925
+ contentHash: currentContentHashForAgent('gemini'),
926
+ installedAt: new Date().toISOString(),
927
+ files: [paths.policyFile, ...slashResult.managedFiles]
928
+ };
929
+ writeManifest(manifest);
930
+
931
+ return { ok: true };
453
932
  }
454
933
 
455
934
  // ---------------------------------------------------------------------------
@@ -465,8 +944,16 @@ function doctorAgent(agent) {
465
944
  return false;
466
945
  }
467
946
 
468
- if (entry.version !== BUNDLE_VERSION) {
469
- console.log(` ⚠ ${agent}: stale (installed v${entry.version}, current v${BUNDLE_VERSION})`);
947
+ const currentVersion =
948
+ agent === 'codex'
949
+ ? pluginVersion(path.join(codexSourcePluginDir(), '.codex-plugin', 'plugin.json'))
950
+ : BUNDLE_VERSION;
951
+ const currentHash = currentContentHashForAgent(agent);
952
+
953
+ if (entry.version !== currentVersion || entry.contentHash !== currentHash) {
954
+ console.log(
955
+ ` ⚠ ${agent}: stale (installed v${entry.version}, current v${currentVersion ?? 'unknown'})`
956
+ );
470
957
  return false;
471
958
  }
472
959
 
@@ -491,25 +978,24 @@ export async function runSetupCommand(args) {
491
978
  if (agent === '--help' || agent === '-h' || agent === 'help') {
492
979
  console.log(`Usage:
493
980
  ovld setup claude Install Overlord bundle for Claude Code
981
+ ovld setup codex Install Overlord Codex plugin bundle
982
+ ovld setup cursor Install Overlord rules, slash commands, and permissions for Cursor
983
+ ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
494
984
  ovld setup opencode Install Overlord connector for OpenCode
495
985
  ovld setup all Install for all supported agents
496
986
  ovld doctor Validate installed connectors`);
497
987
  return;
498
988
  }
499
989
 
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
990
  if (agent === 'all') {
508
991
  console.log('Installing Overlord agent bundle for all supported agents...\n');
509
992
  for (const a of supportedAgents) {
510
993
  console.log(`[${a}]`);
511
994
  try {
512
995
  if (a === 'claude') installClaude();
996
+ else if (a === 'codex') installCodex();
997
+ else if (a === 'cursor') installCursor();
998
+ else if (a === 'gemini') installGemini();
513
999
  else installOpenCode();
514
1000
  } catch (err) {
515
1001
  console.error(` ✗ Failed: ${err.message}`);
@@ -530,6 +1016,9 @@ export async function runSetupCommand(args) {
530
1016
  console.log(`Installing Overlord agent bundle for ${agent}...\n`);
531
1017
  try {
532
1018
  if (agent === 'claude') installClaude();
1019
+ else if (agent === 'codex') installCodex();
1020
+ else if (agent === 'cursor') installCursor();
1021
+ else if (agent === 'gemini') installGemini();
533
1022
  else installOpenCode();
534
1023
  console.log('\nDone.');
535
1024
  } catch (err) {
@@ -544,10 +1033,16 @@ export async function runDoctorCommand() {
544
1033
  for (const agent of supportedAgents) {
545
1034
  if (!doctorAgent(agent)) allOk = false;
546
1035
  }
1036
+ const latestCliVersion = await checkForCliUpdate();
547
1037
  console.log();
548
1038
  if (allOk) {
549
1039
  console.log('All bundles are up to date.');
550
1040
  } else {
551
1041
  console.log('Run `ovld setup <agent>` or `ovld setup all` to install/repair.');
552
1042
  }
1043
+ if (latestCliVersion) {
1044
+ console.log();
1045
+ console.log(`CLI update available: ${latestCliVersion}`);
1046
+ console.log('Run `npm install -g overlord-cli@latest` to update.');
1047
+ }
553
1048
  }