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
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Overlord CLI
|
|
2
|
+
|
|
3
|
+
`overlord-cli` is the packaged command-line interface for Overlord. It lets you launch agents on tickets, create new tickets, and manage the ticket lifecycle from the terminal.
|
|
4
|
+
|
|
5
|
+
Website: [ovld.ai](https://ovld.ai)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install it globally so the `ovld` and `overlord` commands are available on your `PATH`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g overlord-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ovld help
|
|
19
|
+
overlord help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The CLI exposes the same command set under both names.
|
|
23
|
+
|
|
24
|
+
Common commands:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
ovld auth login
|
|
28
|
+
ovld attach
|
|
29
|
+
ovld create "Investigate the failing build"
|
|
30
|
+
ovld prompt "Draft a fix for the onboarding flow"
|
|
31
|
+
ovld setup all
|
|
32
|
+
ovld doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Node.js 18 or newer
|
|
38
|
+
- Access to an Overlord instance when using authenticated commands
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
- `attach` - search tickets and launch an agent interactively
|
|
43
|
+
- `create` - create a ticket from a short objective
|
|
44
|
+
- `prompt` - create a ticket and launch an agent on it
|
|
45
|
+
- `auth` - log in, log out, or check auth status
|
|
46
|
+
- `tickets` - list or create tickets
|
|
47
|
+
- `ticket` - work with a single ticket
|
|
48
|
+
- `protocol` - run ticket lifecycle commands
|
|
49
|
+
- `connect`, `restart`, `run`, `resume`, `context` - launch or resume an agent session
|
|
50
|
+
- `setup` - install the Overlord connector for a supported agent
|
|
51
|
+
- `doctor` - verify installed agent connectors
|
|
52
|
+
|
|
53
|
+
## Publishing
|
|
54
|
+
|
|
55
|
+
This package is published from the repository root with:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
yarn cli:publish
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That script syncs the package payload and then runs `npm publish --access public` from `packages/overlord-cli/`.
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `ovld attach` — interactive ticket search + agent launcher.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ovld attach # interactive: search tickets, pick agent
|
|
8
|
+
* ovld attach <ticketId> # skip ticket search, pick agent interactively
|
|
9
|
+
* ovld attach <ticketId> <agent> # non-interactive: launch immediately
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
|
|
13
|
+
import { runLauncherCommand } from './launcher.mjs';
|
|
14
|
+
|
|
15
|
+
const AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'opencode'];
|
|
16
|
+
const MAX_VISIBLE = 8;
|
|
17
|
+
const SEARCH_DEBOUNCE_MS = 120;
|
|
18
|
+
|
|
19
|
+
// ─── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const hide = '\x1b[?25l';
|
|
22
|
+
const show = '\x1b[?25h';
|
|
23
|
+
|
|
24
|
+
const saveCursor = '\x1b7';
|
|
25
|
+
const restoreCursor = '\x1b8';
|
|
26
|
+
const eraseBelow = '\x1b[J';
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
30
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
31
|
+
const cyan = s => `\x1b[36m${s}\x1b[0m`;
|
|
32
|
+
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
33
|
+
const gray = s => `\x1b[90m${s}\x1b[0m`;
|
|
34
|
+
const yellow = s => `\x1b[33m${s}\x1b[0m`;
|
|
35
|
+
const red = s => `\x1b[31m${s}\x1b[0m`;
|
|
36
|
+
|
|
37
|
+
function truncate(str, max) {
|
|
38
|
+
if (!str) return '';
|
|
39
|
+
return str.length > max ? str.slice(0, max - 1) + '…' : str;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function statusColor(status) {
|
|
43
|
+
switch (status) {
|
|
44
|
+
case 'draft':
|
|
45
|
+
return dim(status);
|
|
46
|
+
case 'execute':
|
|
47
|
+
return cyan(status);
|
|
48
|
+
case 'review':
|
|
49
|
+
return yellow(status);
|
|
50
|
+
case 'complete':
|
|
51
|
+
return green(status);
|
|
52
|
+
case 'blocked':
|
|
53
|
+
return red(status);
|
|
54
|
+
default:
|
|
55
|
+
return gray(status ?? '?');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── API ──────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async function searchTickets(platformUrl, agentToken, localSecret, query) {
|
|
62
|
+
const res = await fetch(`${platformUrl}/api/protocol/search-tickets`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
...buildAuthHeaders(agentToken, localSecret),
|
|
66
|
+
'Content-Type': 'application/json'
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
includeCompleted: false,
|
|
70
|
+
query,
|
|
71
|
+
limit: MAX_VISIBLE
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
throw new Error(`Failed to search tickets (${res.status}): ${await res.text()}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
return data.tickets ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Interactive prompt ───────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run an interactive list selector.
|
|
87
|
+
*
|
|
88
|
+
* In ticket mode (search callback provided), shows a search-as-you-type menu.
|
|
89
|
+
* In items mode (items array provided), shows a fixed list selector.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} opts
|
|
92
|
+
* @param {string} opts.label - Label shown before the search input
|
|
93
|
+
* @param {string[]} [opts.items] - Fixed list of choices (agent picker)
|
|
94
|
+
* @param {(query: string) => Promise<object[]>} [opts.search] - Ticket search callback
|
|
95
|
+
* @param {string} [opts.prefix] - Text prepended to the input line (for UX context)
|
|
96
|
+
* @returns {Promise<string|object|null>} - Selected item/value, or null if cancelled
|
|
97
|
+
*/
|
|
98
|
+
function runInteractivePrompt({ label, items = [], search, prefix = '' }) {
|
|
99
|
+
return new Promise(resolve => {
|
|
100
|
+
const isTicketMode = typeof search === 'function';
|
|
101
|
+
let query = '';
|
|
102
|
+
let selectedIdx = 0;
|
|
103
|
+
let hasRendered = false;
|
|
104
|
+
let loading = isTicketMode;
|
|
105
|
+
let errorMessage = '';
|
|
106
|
+
let filteredItems = isTicketMode ? [] : items;
|
|
107
|
+
let activeRequestId = 0;
|
|
108
|
+
let debounceTimer = null;
|
|
109
|
+
|
|
110
|
+
function getFiltered() {
|
|
111
|
+
return filteredItems;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderTicketRow(t, active) {
|
|
115
|
+
const seq = String(t.ticket_sequence ?? '?').padStart(3, ' ');
|
|
116
|
+
const status = t.status ?? '?';
|
|
117
|
+
const title = truncate(t.title || t.objective || '(no title)', 55);
|
|
118
|
+
const marker = active ? cyan('▶') : ' ';
|
|
119
|
+
return ` ${marker} ${gray('#' + seq)} ${gray('[')}${statusColor(status)}${gray(']')} ${active ? bold(title) : title}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderAgentRow(agent, active) {
|
|
123
|
+
const marker = active ? cyan('▶') : ' ';
|
|
124
|
+
return ` ${marker} ${active ? bold(agent) : agent}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function render() {
|
|
128
|
+
const filtered = getFiltered();
|
|
129
|
+
const count = Math.min(filtered.length, MAX_VISIBLE);
|
|
130
|
+
// Clamp selected within visible range
|
|
131
|
+
if (selectedIdx >= count) selectedIdx = Math.max(0, count - 1);
|
|
132
|
+
|
|
133
|
+
const lines = [];
|
|
134
|
+
|
|
135
|
+
// Input line
|
|
136
|
+
if (prefix) {
|
|
137
|
+
lines.push(` ${dim(prefix)}${query}${cyan('│')}`);
|
|
138
|
+
} else {
|
|
139
|
+
lines.push(` ${gray(label + ':')} ${query}${cyan('│')}`);
|
|
140
|
+
}
|
|
141
|
+
lines.push('');
|
|
142
|
+
|
|
143
|
+
if (loading) {
|
|
144
|
+
lines.push(gray(' Searching tickets…'));
|
|
145
|
+
} else if (errorMessage) {
|
|
146
|
+
lines.push(red(` ${errorMessage}`));
|
|
147
|
+
} else if (filtered.length === 0) {
|
|
148
|
+
lines.push(gray(' No matches'));
|
|
149
|
+
} else {
|
|
150
|
+
for (let i = 0; i < count; i++) {
|
|
151
|
+
lines.push(
|
|
152
|
+
isTicketMode
|
|
153
|
+
? renderTicketRow(filtered[i], i === selectedIdx)
|
|
154
|
+
: renderAgentRow(filtered[i], i === selectedIdx)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (filtered.length > MAX_VISIBLE) {
|
|
158
|
+
lines.push(gray(` … ${filtered.length - MAX_VISIBLE} more — keep typing to narrow`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push(dim(' ↑↓ navigate · type to filter · Enter select · Esc cancel'));
|
|
164
|
+
|
|
165
|
+
if (hasRendered) {
|
|
166
|
+
process.stdout.write(restoreCursor + eraseBelow);
|
|
167
|
+
}
|
|
168
|
+
process.stdout.write(saveCursor + lines.join('\n'));
|
|
169
|
+
hasRendered = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadMatches(nextQuery) {
|
|
173
|
+
if (!isTicketMode) return;
|
|
174
|
+
const requestId = ++activeRequestId;
|
|
175
|
+
loading = true;
|
|
176
|
+
errorMessage = '';
|
|
177
|
+
render();
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const nextItems = await search(nextQuery);
|
|
181
|
+
if (requestId !== activeRequestId) return;
|
|
182
|
+
filteredItems = nextItems;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (requestId !== activeRequestId) return;
|
|
185
|
+
filteredItems = [];
|
|
186
|
+
errorMessage = error instanceof Error ? error.message : 'Ticket search failed.';
|
|
187
|
+
} finally {
|
|
188
|
+
if (requestId !== activeRequestId) return;
|
|
189
|
+
loading = false;
|
|
190
|
+
render();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function scheduleLoad() {
|
|
195
|
+
if (!isTicketMode) return;
|
|
196
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
197
|
+
debounceTimer = setTimeout(() => {
|
|
198
|
+
debounceTimer = null;
|
|
199
|
+
void loadMatches(query);
|
|
200
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cleanup() {
|
|
204
|
+
if (debounceTimer) {
|
|
205
|
+
clearTimeout(debounceTimer);
|
|
206
|
+
debounceTimer = null;
|
|
207
|
+
}
|
|
208
|
+
if (hasRendered) {
|
|
209
|
+
process.stdout.write(restoreCursor + eraseBelow);
|
|
210
|
+
}
|
|
211
|
+
process.stdin.setRawMode(false);
|
|
212
|
+
process.stdin.removeAllListeners('data');
|
|
213
|
+
process.stdout.write(show);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
process.stdin.setRawMode(true);
|
|
217
|
+
process.stdin.resume();
|
|
218
|
+
process.stdin.setEncoding('utf8');
|
|
219
|
+
process.stdout.write(hide);
|
|
220
|
+
render();
|
|
221
|
+
scheduleLoad();
|
|
222
|
+
|
|
223
|
+
process.stdin.on('data', key => {
|
|
224
|
+
const filtered = getFiltered();
|
|
225
|
+
const count = Math.min(filtered.length, MAX_VISIBLE);
|
|
226
|
+
|
|
227
|
+
// Ctrl-C / Ctrl-D → exit
|
|
228
|
+
if (key === '\x03' || key === '\x04') {
|
|
229
|
+
cleanup();
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Escape (lone \x1b, not a sequence like \x1b[A)
|
|
234
|
+
if (key === '\x1b') {
|
|
235
|
+
cleanup();
|
|
236
|
+
resolve(null);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Enter → confirm selection
|
|
241
|
+
if (key === '\r' || key === '\n') {
|
|
242
|
+
if (loading || filtered.length === 0) return;
|
|
243
|
+
const item = filtered[selectedIdx] ?? filtered[0];
|
|
244
|
+
cleanup();
|
|
245
|
+
resolve(item);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Arrow up
|
|
250
|
+
if (key === '\x1b[A') {
|
|
251
|
+
selectedIdx = (selectedIdx - 1 + count) % Math.max(1, count);
|
|
252
|
+
render();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Arrow down
|
|
257
|
+
if (key === '\x1b[B') {
|
|
258
|
+
selectedIdx = (selectedIdx + 1) % Math.max(1, count);
|
|
259
|
+
render();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Backspace
|
|
264
|
+
if (key === '\x7f' || key === '\b') {
|
|
265
|
+
if (query.length > 0) {
|
|
266
|
+
query = query.slice(0, -1);
|
|
267
|
+
selectedIdx = 0;
|
|
268
|
+
if (isTicketMode) scheduleLoad();
|
|
269
|
+
else render();
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Printable character → append to query
|
|
275
|
+
if (key.length === 1 && key >= ' ') {
|
|
276
|
+
query += key;
|
|
277
|
+
selectedIdx = 0;
|
|
278
|
+
if (isTicketMode) scheduleLoad();
|
|
279
|
+
else render();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
export async function runAttachCommand(args) {
|
|
288
|
+
const [ticketIdArg, agentArg] = args;
|
|
289
|
+
|
|
290
|
+
const { platformUrl, agentToken, localSecret } = resolveAuth();
|
|
291
|
+
|
|
292
|
+
// ── Phase 1: Ticket selection ──────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
let ticketId = ticketIdArg;
|
|
295
|
+
let ticketTitle = '';
|
|
296
|
+
|
|
297
|
+
if (!ticketId) {
|
|
298
|
+
process.stdout.write('\n');
|
|
299
|
+
process.stdout.write(bold(' ovld attach\n'));
|
|
300
|
+
process.stdout.write('\n');
|
|
301
|
+
|
|
302
|
+
const selectedTicket = await runInteractivePrompt({
|
|
303
|
+
label: 'Search tickets',
|
|
304
|
+
search: nextQuery => searchTickets(platformUrl, agentToken, localSecret, nextQuery)
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (!selectedTicket) {
|
|
308
|
+
process.stdout.write(dim('\n Cancelled.\n\n'));
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ticketId = selectedTicket.id;
|
|
313
|
+
ticketTitle = selectedTicket.title || selectedTicket.objective || '';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Phase 2: Agent selection ───────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
const shortId = ticketId.slice(-8).toUpperCase();
|
|
319
|
+
|
|
320
|
+
let agent = agentArg;
|
|
321
|
+
|
|
322
|
+
if (!agent) {
|
|
323
|
+
process.stdout.write('\n');
|
|
324
|
+
process.stdout.write(bold(` ovld attach `) + cyan(shortId) + '\n');
|
|
325
|
+
if (ticketTitle) {
|
|
326
|
+
process.stdout.write(gray(` ${truncate(ticketTitle, 60)}\n`));
|
|
327
|
+
}
|
|
328
|
+
process.stdout.write('\n');
|
|
329
|
+
|
|
330
|
+
agent = await runInteractivePrompt({
|
|
331
|
+
label: 'Agent',
|
|
332
|
+
items: AGENTS,
|
|
333
|
+
prefix: `ovld attach ${shortId} `
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!agent) {
|
|
337
|
+
process.stdout.write(dim('\n Cancelled.\n\n'));
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!AGENTS.includes(agent)) {
|
|
343
|
+
console.error(`\nUnknown agent: "${agent}". Must be one of: ${AGENTS.join(', ')}`);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Launch ─────────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
process.stdout.write('\n');
|
|
350
|
+
process.stdout.write(
|
|
351
|
+
` ${green('✓')} ${bold(agent)} ← ${truncate(ticketTitle || shortId, 55)}\n\n`
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
process.env.TICKET_ID = ticketId;
|
|
355
|
+
await runLauncherCommand('run', [agent]);
|
|
356
|
+
}
|