overlord-cli 0.2605101225.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 +128 -0
- package/bin/_cli/attach.mjs +357 -0
- package/bin/_cli/auth.mjs +696 -0
- package/bin/_cli/cli-update.mjs +119 -0
- package/bin/_cli/credentials.mjs +723 -0
- package/bin/_cli/index.mjs +170 -0
- package/bin/_cli/launcher.mjs +474 -0
- package/bin/_cli/new-ticket.mjs +306 -0
- package/bin/_cli/postinstall.mjs +43 -0
- package/bin/_cli/protocol.mjs +1799 -0
- package/bin/_cli/setup.mjs +1634 -0
- package/bin/_cli/ticket.mjs +55 -0
- package/bin/_cli/tickets.mjs +121 -0
- package/bin/_cli/version.mjs +7 -0
- package/bin/ovld.mjs +13 -0
- package/package.json +36 -0
- package/plugins/claude/.claude-plugin/plugin.json +34 -0
- package/plugins/claude/README.md +42 -0
- package/plugins/claude/commands/connect.md +19 -0
- package/plugins/claude/commands/create.md +17 -0
- package/plugins/claude/commands/load.md +19 -0
- package/plugins/claude/commands/spawn.md +17 -0
- package/plugins/claude/hooks/hooks.json +15 -0
- package/plugins/claude/scripts/permission-hook.sh +21 -0
- package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
- package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
- package/plugins/cursor/commands/connect.md +3 -0
- package/plugins/cursor/commands/create.md +11 -0
- package/plugins/cursor/commands/load.md +3 -0
- package/plugins/cursor/commands/spawn.md +3 -0
- package/plugins/cursor/mcp.json +8 -0
- package/plugins/cursor/rules/overlord-local.mdc +10 -0
- package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
- package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
- package/plugins/overlord/.codex-plugin/plugin.json +35 -0
- package/plugins/overlord/.mcp.json +11 -0
- package/plugins/overlord/README.md +42 -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 +688 -0
- package/plugins/overlord/skills/overlord-ticket/SKILL.md +146 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
Note: Use Node.js 20 or newer for every CLI install or update.
|
|
10
|
+
|
|
11
|
+
Install it globally so the `ovld` and `overlord` commands are available on your `PATH`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g overlord-cli
|
|
15
|
+
|
|
16
|
+
#After installing, run
|
|
17
|
+
ovld setup all #to configure every supported connector (`ovld setup` alone is interactive)
|
|
18
|
+
|
|
19
|
+
#for individual connectors, run `ovld setup <connector>`.
|
|
20
|
+
ovld setup cursor
|
|
21
|
+
ovld setup claude
|
|
22
|
+
ovld setup codex
|
|
23
|
+
ovld setup all
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# To update the CLI to the latest release, run:
|
|
28
|
+
ovld update
|
|
29
|
+
```
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ovld help
|
|
34
|
+
overlord help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Auth
|
|
38
|
+
```bash
|
|
39
|
+
# Repair shared credentials first when a session already exists
|
|
40
|
+
ovld auth repair # mirror and chmod shared Desktop/CLI credentials when possible
|
|
41
|
+
|
|
42
|
+
# Login to Overlord if repair does not restore access
|
|
43
|
+
ovld auth login #opens a browser when possible and also prints a verification URL/code so login can be completed from another machine over SSH.
|
|
44
|
+
ovld auth status # show current login status (use --verbose for redacted diagnostics)
|
|
45
|
+
ovld auth logout # remove stored credentials
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Desktop-installed wrappers default `OVERLORD_URL` to `https://www.ovld.ai` unless you override it explicitly.
|
|
49
|
+
For local dev against the web app on port 3000, export the override before running auth or protocol commands:
|
|
50
|
+
```bash
|
|
51
|
+
export OVERLORD_URL=http://localhost:3000
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Common commands:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ovld auth login
|
|
58
|
+
ovld auth status
|
|
59
|
+
ovld attach
|
|
60
|
+
ovld create "Investigate the failing build" --agent codex
|
|
61
|
+
ovld prompt "Draft a fix for the onboarding flow"
|
|
62
|
+
ovld version
|
|
63
|
+
ovld update
|
|
64
|
+
ovld protocol discover-project
|
|
65
|
+
ovld protocol attach --ticket-id <ticket_id>
|
|
66
|
+
ovld protocol search-tickets --query "auth refactor" --status next-up,execute
|
|
67
|
+
ovld protocol update --session-key <session-key> --ticket-id <ticket_id> --summary "Working on it" --phase execute
|
|
68
|
+
ovld setup
|
|
69
|
+
ovld setup codex
|
|
70
|
+
ovld setup claude
|
|
71
|
+
ovld setup cursor
|
|
72
|
+
ovld setup gemini
|
|
73
|
+
ovld setup all
|
|
74
|
+
ovld doctor
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- Node.js 20 or newer
|
|
80
|
+
- Access to an Overlord instance when using authenticated commands
|
|
81
|
+
|
|
82
|
+
## Commands for Humans
|
|
83
|
+
|
|
84
|
+
Find full CLI docs here: https://www.ovld.ai/docs/surfaces/cli
|
|
85
|
+
|
|
86
|
+
Top-level commands (see `ovld help`):
|
|
87
|
+
|
|
88
|
+
- `attach` - search tickets and launch an agent interactively (`ovld attach [ticketId] [agent]`)
|
|
89
|
+
- `create` - create a ticket with numbered project selection; supports `--agent`, `--model`, `--delegate` (same delegate flags as `ovld protocol create`)
|
|
90
|
+
- `prompt` - create a ticket, then launch an agent on it
|
|
91
|
+
- `auth` - `login`, `status`, `repair` (shared Desktop/CLI credentials), or `logout`
|
|
92
|
+
- `tickets` - `create` or `list` (optional `--status`)
|
|
93
|
+
- `ticket` - `context <ticketId>` to print context for one ticket
|
|
94
|
+
- `launch`, `restart` - launch or resume an agent session
|
|
95
|
+
- `connect`, `run`, `resume` - legacy aliases for `launch` and `restart`
|
|
96
|
+
- `setup` - install the Overlord connector for an agent; `ovld setup [agent|all]` (interactive with no args). `ovld setup claude` also performs the one-time v3.25.0 to v4 Claude plugin migration
|
|
97
|
+
- `update` - install the latest CLI release from npm
|
|
98
|
+
- `doctor` - validate installed connectors and check for CLI updates
|
|
99
|
+
- `version` - print the installed CLI version
|
|
100
|
+
|
|
101
|
+
## Commands for agents
|
|
102
|
+
Agents can find docs here: https://www.ovld.ai/docs/for-agents
|
|
103
|
+
|
|
104
|
+
`ovld protocol <subcommand>` is the surface agents and hooks use for ticket lifecycle work. Run `ovld protocol help` for flags, env fallbacks, and examples.
|
|
105
|
+
|
|
106
|
+
- `auth-status` - return machine-readable auth status for agent runtimes
|
|
107
|
+
- `discover-project` - resolve a project from the current (or given) working directory
|
|
108
|
+
- `attach` - start a ticket session and return full working context
|
|
109
|
+
- `connect` - start a lightweight session without full context
|
|
110
|
+
- `load-context` - read ticket context without creating a session
|
|
111
|
+
- `search-tickets` - find tickets by keyword, status, project, creator, or update date
|
|
112
|
+
- `create` - create a draft ticket without attaching (standalone or follow-up)
|
|
113
|
+
- `prompt` - create a ticket and attach to it immediately (`spawn` is a backward-compatible alias)
|
|
114
|
+
- `update` - post progress, activity events, and optional change rationales
|
|
115
|
+
- `record-change-rationales` - persist structured change rationales without a normal progress update
|
|
116
|
+
- `ask` - post a blocking question and move the ticket to review
|
|
117
|
+
- `permission-request` - notify Overlord that the agent is requesting tool permission
|
|
118
|
+
- `read-context` - read shared persistent context for this ticket
|
|
119
|
+
- `write-context` - write shared persistent context for future sessions
|
|
120
|
+
- `deliver` - finish work, send artifacts, and move the ticket to review
|
|
121
|
+
- `attachment-prepare-upload` - get a signed upload URL for an objective attachment
|
|
122
|
+
- `attachment-finalize-upload` - finalize an uploaded objective attachment row after storage upload
|
|
123
|
+
- `attachment-download-url` - get a signed download URL for an existing objective attachment
|
|
124
|
+
- `attachment-upload-file` - prepare, upload, and finalize a local objective attachment in one command
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
Permission is granted to use this software for any purpose, free of charge. You may not modify, distribute, sublicense, or sell copies of the software without explicit permission from the author.
|
|
@@ -0,0 +1,357 @@
|
|
|
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, bearerToken, localSecret, organizationId, query) {
|
|
62
|
+
const res = await fetch(`${platformUrl}/api/protocol/search-tickets`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
...buildAuthHeaders(bearerToken, localSecret, organizationId),
|
|
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, bearerToken, localSecret, organizationId } = await 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 =>
|
|
305
|
+
searchTickets(platformUrl, bearerToken, localSecret, organizationId, nextQuery)
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!selectedTicket) {
|
|
309
|
+
process.stdout.write(dim('\n Cancelled.\n\n'));
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
ticketId = selectedTicket.id;
|
|
314
|
+
ticketTitle = selectedTicket.title || selectedTicket.objective || '';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Phase 2: Agent selection ───────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
const shortId = ticketId.slice(-8).toUpperCase();
|
|
320
|
+
|
|
321
|
+
let agent = agentArg;
|
|
322
|
+
|
|
323
|
+
if (!agent) {
|
|
324
|
+
process.stdout.write('\n');
|
|
325
|
+
process.stdout.write(bold(` ovld attach `) + cyan(shortId) + '\n');
|
|
326
|
+
if (ticketTitle) {
|
|
327
|
+
process.stdout.write(gray(` ${truncate(ticketTitle, 60)}\n`));
|
|
328
|
+
}
|
|
329
|
+
process.stdout.write('\n');
|
|
330
|
+
|
|
331
|
+
agent = await runInteractivePrompt({
|
|
332
|
+
label: 'Agent',
|
|
333
|
+
items: AGENTS,
|
|
334
|
+
prefix: `ovld attach ${shortId} `
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!agent) {
|
|
338
|
+
process.stdout.write(dim('\n Cancelled.\n\n'));
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!AGENTS.includes(agent)) {
|
|
344
|
+
console.error(`\nUnknown agent: "${agent}". Must be one of: ${AGENTS.join(', ')}`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Launch ─────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
process.stdout.write('\n');
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
` ${green('✓')} ${bold(agent)} ← ${truncate(ticketTitle || shortId, 55)}\n\n`
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
process.env.TICKET_ID = ticketId;
|
|
356
|
+
await runLauncherCommand('run', [agent]);
|
|
357
|
+
}
|