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