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 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
+ }