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