overlord-cli 3.5.3 → 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.
- package/README.md +5 -2
- package/bin/_cli/index.mjs +1 -1
- package/bin/_cli/setup.mjs +547 -52
- package/package.json +3 -2
- package/plugins/overlord/.codex-plugin/plugin.json +35 -0
- package/plugins/overlord/.mcp.json +11 -0
- package/plugins/overlord/README.md +41 -0
- package/plugins/overlord/assets/icon.png +0 -0
- package/plugins/overlord/assets/logo.png +0 -0
- package/plugins/overlord/assets/screenshot.svg +28 -0
- package/plugins/overlord/scripts/overlord-mcp.mjs +626 -0
- package/plugins/overlord/skills/overlord-ticket-workflow/SKILL.md +26 -0
package/bin/_cli/setup.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
886
|
+
});
|
|
887
|
+
console.log(` ✓ Updated permissions: ${paths.settingsFile}`);
|
|
442
888
|
|
|
443
889
|
const manifest = readManifest();
|
|
444
|
-
manifest.
|
|
890
|
+
manifest.cursor = {
|
|
445
891
|
version: BUNDLE_VERSION,
|
|
446
|
-
contentHash:
|
|
892
|
+
contentHash: currentContentHashForAgent('cursor'),
|
|
447
893
|
installedAt: new Date().toISOString(),
|
|
448
|
-
files: [paths.
|
|
894
|
+
files: [paths.rulesFile, paths.settingsFile, ...slashResult.managedFiles]
|
|
449
895
|
};
|
|
450
896
|
writeManifest(manifest);
|
|
451
897
|
|
|
452
|
-
return { ok: true
|
|
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
|
-
|
|
469
|
-
|
|
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
|
}
|