overlord-cli 0.2605281741.0 → 0.2605290912.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 +5 -0
- package/bin/_cli/direct-launch.mjs +366 -0
- package/bin/_cli/index.mjs +17 -1
- package/bin/_cli/launcher.mjs +166 -23
- package/bin/_cli/setup.mjs +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ Common commands:
|
|
|
60
60
|
ovld auth login
|
|
61
61
|
ovld auth status
|
|
62
62
|
ovld attach
|
|
63
|
+
ovld claude "Refactor the auth middleware" --model opus --thinking high
|
|
64
|
+
ovld codex "Investigate the memory leak" -- --search --full-auto
|
|
63
65
|
ovld create "Investigate the failing build" --agent codex
|
|
64
66
|
ovld prompt "Draft a fix for the onboarding flow"
|
|
65
67
|
ovld version
|
|
@@ -85,6 +87,8 @@ ovld doctor
|
|
|
85
87
|
|
|
86
88
|
For ticket-scoped protocol and launch commands, `ticket_id` values such as `1:899` carry the organization id. The CLI uses that first, then `--organization-id` for UUID compatibility, then stored auth. `ovld auth login` stores the first available organization as a default when an account belongs to multiple organizations; pass `--organization-id <id>` only when you want a different default.
|
|
87
89
|
|
|
90
|
+
When `ovld launch` has an explicit `--working-directory`, or when you run it from a registered project directory containing `.overlord/project.json`, it exports `TMPDIR`, `TMP`, `TEMP`, and `OVERLORD_TMPDIR` to that project's `.overlord/tmp/` directory before spawning the agent. Otherwise it falls back to the system temp directory.
|
|
91
|
+
|
|
88
92
|
## Requirements
|
|
89
93
|
|
|
90
94
|
- Node.js 20 or newer
|
|
@@ -96,6 +100,7 @@ Find full CLI docs here: https://www.ovld.ai/docs/surfaces/cli
|
|
|
96
100
|
|
|
97
101
|
Top-level commands (see `ovld help`):
|
|
98
102
|
|
|
103
|
+
- `<agent> "<prompt>"` - create a ticket from the prompt (project inferred from the current directory) and launch the agent on it in one step, e.g. `ovld claude "fix the flaky login test" --model opus`. Built-in agents need an installed connector (`ovld setup <agent>`, override with `--allow-uninstalled`); your custom agents launch by id. Flags after a standalone `--` pass through to the agent binary.
|
|
99
104
|
- `attach` - search tickets and launch an agent interactively (`ovld attach [ticketId] [agent]`)
|
|
100
105
|
- `create` - create a ticket with numbered project selection; supports `--agent`, `--model`, `--delegate` (same delegate flags as `ovld protocol create`)
|
|
101
106
|
- `prompt` - create a ticket, then launch an agent on it
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Direct agent launch: `ovld <agent> "<prompt>" [overlord flags] [-- passthrough]`.
|
|
5
|
+
*
|
|
6
|
+
* Creates a ticket from the prompt (project inferred from the working directory),
|
|
7
|
+
* then launches the chosen agent on it — giving power users all of Overlord's
|
|
8
|
+
* tracking without ever opening the desktop/web UI. Launch is limited to agents
|
|
9
|
+
* with an installed connector (built-ins) or one of the user's custom agents.
|
|
10
|
+
*
|
|
11
|
+
* This is a thin composition over existing pieces:
|
|
12
|
+
* - `/api/protocol/prompt` creates + auto-titles the ticket and resolves the
|
|
13
|
+
* project from the working directory.
|
|
14
|
+
* - `runLauncherCommand('run' | 'launch-custom', …)` launches the agent locally.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
|
|
18
|
+
import {
|
|
19
|
+
BUILTIN_LAUNCH_AGENTS,
|
|
20
|
+
agentIdentifierMap,
|
|
21
|
+
runLauncherCommand
|
|
22
|
+
} from './launcher.mjs';
|
|
23
|
+
import { isAgentConnectorInstalled } from './setup.mjs';
|
|
24
|
+
|
|
25
|
+
/** Overlord flags that take a value (everything else after `--key` is a boolean). */
|
|
26
|
+
const VALUE_FLAGS = new Set([
|
|
27
|
+
'project-id',
|
|
28
|
+
'priority',
|
|
29
|
+
'title',
|
|
30
|
+
'acceptance-criteria',
|
|
31
|
+
'available-tools',
|
|
32
|
+
'working-directory',
|
|
33
|
+
'model',
|
|
34
|
+
'thinking',
|
|
35
|
+
'delegate',
|
|
36
|
+
'pre-command',
|
|
37
|
+
'launch-mode',
|
|
38
|
+
'organization-id',
|
|
39
|
+
'ssh-command',
|
|
40
|
+
'remote-working-directory',
|
|
41
|
+
'server-multiplexer',
|
|
42
|
+
'tmux-command'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const BOOLEAN_FLAGS = new Set(['personal', 'for-human', 'allow-uninstalled']);
|
|
46
|
+
|
|
47
|
+
/** Launch flags forwarded verbatim to `ovld launch`/`launch-custom`. */
|
|
48
|
+
const FORWARDED_VALUE_FLAGS = [
|
|
49
|
+
'pre-command',
|
|
50
|
+
'launch-mode',
|
|
51
|
+
'organization-id',
|
|
52
|
+
'ssh-command',
|
|
53
|
+
'remote-working-directory',
|
|
54
|
+
'server-multiplexer',
|
|
55
|
+
'tmux-command'
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export function isBuiltinLaunchAgent(name) {
|
|
59
|
+
return BUILTIN_LAUNCH_AGENTS.includes(String(name ?? '').trim().toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Substitute `{{token}}` placeholders in a custom-agent command template.
|
|
64
|
+
* Mirrors `resolveCustomAgentCommand` in lib/helpers/custom-agent.ts (kept in
|
|
65
|
+
* sync by hand because the published CLI cannot import the app's TypeScript).
|
|
66
|
+
*/
|
|
67
|
+
export function resolveTemplate(template, values) {
|
|
68
|
+
const substituted = String(template).replace(
|
|
69
|
+
/\{\{\s*([\w.-]+)\s*\}\}/g,
|
|
70
|
+
(_match, token) => {
|
|
71
|
+
const value = values[token];
|
|
72
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
return substituted.replace(/\s+/g, ' ').trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default placeholder values for a custom agent given a model/thinking choice. */
|
|
79
|
+
export function buildCustomAgentValues(agent, model, thinking) {
|
|
80
|
+
const values = {};
|
|
81
|
+
for (const placeholder of agent.placeholders ?? []) {
|
|
82
|
+
if (placeholder.role === 'model' && model) {
|
|
83
|
+
values[placeholder.token] = model;
|
|
84
|
+
} else if (placeholder.role === 'thinking' && thinking) {
|
|
85
|
+
values[placeholder.token] = thinking;
|
|
86
|
+
} else if (Array.isArray(placeholder.options) && placeholder.options.length > 0) {
|
|
87
|
+
values[placeholder.token] = placeholder.options[0].value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return values;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse direct-launch args into the objective, recognized Overlord flags, the
|
|
95
|
+
* repeatable `--flag` values, and everything after a standalone `--`.
|
|
96
|
+
*/
|
|
97
|
+
export function parseDirectLaunchArgs(rawArgs) {
|
|
98
|
+
const args = Array.isArray(rawArgs) ? rawArgs : [];
|
|
99
|
+
const sepIdx = args.indexOf('--');
|
|
100
|
+
const head = sepIdx === -1 ? args : args.slice(0, sepIdx);
|
|
101
|
+
const passthrough = sepIdx === -1 ? [] : args.slice(sepIdx + 1);
|
|
102
|
+
|
|
103
|
+
const flags = {};
|
|
104
|
+
const repeatedFlags = [];
|
|
105
|
+
const positionals = [];
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < head.length; i++) {
|
|
108
|
+
const arg = head[i];
|
|
109
|
+
if (!arg.startsWith('--')) {
|
|
110
|
+
positionals.push(arg);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const eqIdx = arg.indexOf('=');
|
|
115
|
+
const key = eqIdx === -1 ? arg.slice(2) : arg.slice(2, eqIdx);
|
|
116
|
+
const inlineValue = eqIdx === -1 ? undefined : arg.slice(eqIdx + 1);
|
|
117
|
+
|
|
118
|
+
if (key === 'flag') {
|
|
119
|
+
const value = inlineValue !== undefined ? inlineValue : head[++i];
|
|
120
|
+
if (value !== undefined) repeatedFlags.push(value);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
125
|
+
flags[key] =
|
|
126
|
+
inlineValue === undefined ? true : /^(1|true|yes|)$/i.test(inlineValue);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (inlineValue !== undefined) {
|
|
131
|
+
flags[key] = inlineValue;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Value-taking flag (known or unknown): consume the next token unless it
|
|
136
|
+
// looks like another flag.
|
|
137
|
+
if (i + 1 < head.length && !head[i + 1].startsWith('--')) {
|
|
138
|
+
flags[key] = head[++i];
|
|
139
|
+
} else {
|
|
140
|
+
flags[key] = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
objective: positionals.join(' ').trim(),
|
|
146
|
+
flags,
|
|
147
|
+
repeatedFlags,
|
|
148
|
+
passthrough
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveForHuman(flags) {
|
|
153
|
+
const raw = flags['for-human'];
|
|
154
|
+
if (raw === undefined) return false;
|
|
155
|
+
if (raw === true) return true;
|
|
156
|
+
const normalized = String(raw).trim().toLowerCase();
|
|
157
|
+
return normalized === '' || normalized === 'true' || normalized === '1' || normalized === 'yes';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function readStdin() {
|
|
161
|
+
const chunks = [];
|
|
162
|
+
for await (const chunk of process.stdin) {
|
|
163
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
164
|
+
}
|
|
165
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function fetchUserAgents({ platformUrl, bearerToken, localSecret, organizationId }) {
|
|
169
|
+
const res = await fetch(`${platformUrl}/api/protocol/agents`, {
|
|
170
|
+
headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
|
|
171
|
+
});
|
|
172
|
+
const data = await res.json().catch(() => ({}));
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Failed to list agents (${res.status}): ${data.error ?? JSON.stringify(data)}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
builtins: Array.isArray(data.builtins) ? data.builtins : [],
|
|
180
|
+
customAgents: Array.isArray(data.customAgents) ? data.customAgents : []
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function createPromptTicket(
|
|
185
|
+
{ platformUrl, bearerToken, localSecret, organizationId },
|
|
186
|
+
body
|
|
187
|
+
) {
|
|
188
|
+
const res = await fetch(`${platformUrl}/api/protocol/prompt`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
...buildAuthHeaders(bearerToken, localSecret, organizationId),
|
|
192
|
+
'Content-Type': 'application/json'
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(body)
|
|
195
|
+
});
|
|
196
|
+
const data = await res.json().catch(() => ({}));
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const detail = data.error ?? JSON.stringify(data);
|
|
199
|
+
throw new Error(`Failed to create ticket (${res.status}): ${detail}`);
|
|
200
|
+
}
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function printDirectLaunchHelp(agent = '<agent>') {
|
|
205
|
+
console.log(`Usage:
|
|
206
|
+
ovld ${agent} "<prompt>" [overlord flags] [-- <agent passthrough flags>]
|
|
207
|
+
|
|
208
|
+
Creates a ticket from the prompt (project inferred from the current directory),
|
|
209
|
+
then launches the agent on it and runs the normal ticket lifecycle.
|
|
210
|
+
|
|
211
|
+
Overlord flags:
|
|
212
|
+
--model <identifier> Preferred model (sets the ticket delegate and is forwarded to the agent)
|
|
213
|
+
--thinking <level> Agent reasoning/effort level
|
|
214
|
+
--flag <value> Extra agent flag (repeatable)
|
|
215
|
+
--pre-command <command> Command to run before the agent binary (e.g. ollama)
|
|
216
|
+
--project-id <uuid> Target project (skips working-directory resolution)
|
|
217
|
+
--personal Create the ticket in your personal project
|
|
218
|
+
--for-human Create a human-review ticket
|
|
219
|
+
--priority <low|medium|high|urgent>
|
|
220
|
+
--title "<title>" Override the auto-generated title
|
|
221
|
+
--working-directory <path> Directory used for project resolution and launch
|
|
222
|
+
--allow-uninstalled Launch a built-in agent even if its connector isn't installed
|
|
223
|
+
--launch-mode <run|ask> Ask mode adjusts the fetched prompt context
|
|
224
|
+
--ssh-command / --remote-working-directory / --server-multiplexer / --tmux-command
|
|
225
|
+
|
|
226
|
+
Agent passthrough:
|
|
227
|
+
Anything after a standalone \`--\` is forwarded verbatim to the agent binary.
|
|
228
|
+
|
|
229
|
+
Examples:
|
|
230
|
+
ovld claude "refactor the auth middleware" --model opus --thinking high
|
|
231
|
+
ovld codex "investigate the memory leak" -- --search --full-auto
|
|
232
|
+
ovld cursor "tidy the dashboard styles" --project-id <uuid> --for-human
|
|
233
|
+
echo "summarize today's diffs" | ovld claude`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Entry point for `ovld <agent> …`. `agent` is argv[0]; `rawArgs` is the rest.
|
|
238
|
+
*/
|
|
239
|
+
export async function runDirectLaunch(agent, rawArgs = []) {
|
|
240
|
+
if (rawArgs[0] === '--help' || rawArgs[0] === '-h' || rawArgs[0] === 'help') {
|
|
241
|
+
printDirectLaunchHelp(agent);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const normalizedAgent = String(agent ?? '').trim().toLowerCase();
|
|
246
|
+
const { objective: parsedObjective, flags, repeatedFlags, passthrough } =
|
|
247
|
+
parseDirectLaunchArgs(rawArgs);
|
|
248
|
+
|
|
249
|
+
let objective = parsedObjective;
|
|
250
|
+
if (!objective && !process.stdin.isTTY) {
|
|
251
|
+
objective = (await readStdin()).trim();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const builtin = isBuiltinLaunchAgent(normalizedAgent);
|
|
255
|
+
const auth = await resolveAuth();
|
|
256
|
+
|
|
257
|
+
// Classify + gate the agent before we create a ticket.
|
|
258
|
+
let customAgent = null;
|
|
259
|
+
if (builtin) {
|
|
260
|
+
if (!flags['allow-uninstalled'] && !isAgentConnectorInstalled(normalizedAgent)) {
|
|
261
|
+
console.error(
|
|
262
|
+
`The ${normalizedAgent} connector is not installed. Run \`ovld setup ${normalizedAgent}\` first, ` +
|
|
263
|
+
`or pass --allow-uninstalled to launch anyway.`
|
|
264
|
+
);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const { customAgents } = await fetchUserAgents(auth);
|
|
269
|
+
customAgent =
|
|
270
|
+
customAgents.find(candidate => candidate.id === agent) ??
|
|
271
|
+
customAgents.find(candidate => candidate.id === normalizedAgent) ??
|
|
272
|
+
customAgents.find(candidate => candidate.name === agent) ??
|
|
273
|
+
null;
|
|
274
|
+
if (!customAgent) {
|
|
275
|
+
console.error(`Unknown agent: "${agent}".`);
|
|
276
|
+
console.error(`Built-in agents: ${BUILTIN_LAUNCH_AGENTS.join(', ')}`);
|
|
277
|
+
console.error(
|
|
278
|
+
customAgents.length
|
|
279
|
+
? `Your custom agents: ${customAgents.map(a => a.id).join(', ')}`
|
|
280
|
+
: 'You have no custom agents. Create one in Overlord settings to launch it by id.'
|
|
281
|
+
);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!objective) {
|
|
287
|
+
console.error('An objective is required. Provide it as a quoted argument or pipe it via stdin.');
|
|
288
|
+
printDirectLaunchHelp(agent);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const personal = Boolean(flags.personal);
|
|
293
|
+
const explicitProject =
|
|
294
|
+
typeof flags['project-id'] === 'string' && flags['project-id'].trim().length > 0;
|
|
295
|
+
const explicitWorkingDirectory =
|
|
296
|
+
typeof flags['working-directory'] === 'string' && flags['working-directory'].trim().length > 0
|
|
297
|
+
? flags['working-directory'].trim()
|
|
298
|
+
: '';
|
|
299
|
+
// Send cwd so the server can resolve the project, unless the caller pinned a
|
|
300
|
+
// project or asked for a personal ticket.
|
|
301
|
+
const workingDirectory =
|
|
302
|
+
explicitWorkingDirectory || (!explicitProject && !personal ? process.cwd() : '');
|
|
303
|
+
|
|
304
|
+
const model = typeof flags.model === 'string' ? flags.model.trim() : '';
|
|
305
|
+
const thinking = typeof flags.thinking === 'string' ? flags.thinking.trim() : '';
|
|
306
|
+
const agentIdentifier = builtin ? agentIdentifierMap[normalizedAgent] : 'custom';
|
|
307
|
+
const delegate =
|
|
308
|
+
(typeof flags.delegate === 'string' && flags.delegate.trim()) || model || agentIdentifier;
|
|
309
|
+
|
|
310
|
+
const body = {
|
|
311
|
+
objectives: [{ objective }],
|
|
312
|
+
agentIdentifier,
|
|
313
|
+
connectionMethod: 'cli',
|
|
314
|
+
metadata: { launchedVia: 'ovld-direct', ...(model ? { model } : {}) },
|
|
315
|
+
delegate,
|
|
316
|
+
...(flags.title ? { title: String(flags.title) } : {}),
|
|
317
|
+
...(flags.priority ? { priority: String(flags.priority) } : {}),
|
|
318
|
+
...(explicitProject ? { projectId: String(flags['project-id']).trim() } : {}),
|
|
319
|
+
...(personal ? { personal: true } : {}),
|
|
320
|
+
...(workingDirectory ? { workingDirectory } : {}),
|
|
321
|
+
...(flags['acceptance-criteria']
|
|
322
|
+
? { acceptanceCriteria: String(flags['acceptance-criteria']) }
|
|
323
|
+
: {}),
|
|
324
|
+
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
325
|
+
...(resolveForHuman(flags) ? { forHuman: true } : {})
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = await createPromptTicket(auth, body);
|
|
329
|
+
const ticketId = result.ticket?.ticket_id ?? result.ticket?.id;
|
|
330
|
+
if (!ticketId) {
|
|
331
|
+
console.error('Ticket creation did not return a ticket id.');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log(`Created ticket ${ticketId} — launching ${agent}…`);
|
|
336
|
+
process.env.TICKET_ID = String(ticketId);
|
|
337
|
+
|
|
338
|
+
const baseLaunchArgs = ['--ticket-id', String(ticketId)];
|
|
339
|
+
if (workingDirectory) baseLaunchArgs.push('--working-directory', workingDirectory);
|
|
340
|
+
for (const name of FORWARDED_VALUE_FLAGS) {
|
|
341
|
+
const value = flags[name];
|
|
342
|
+
if (typeof value === 'string' && value.trim()) {
|
|
343
|
+
baseLaunchArgs.push(`--${name}`, value.trim());
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (builtin) {
|
|
348
|
+
const launchArgs = [normalizedAgent, ...baseLaunchArgs];
|
|
349
|
+
if (model) launchArgs.push('--model', model);
|
|
350
|
+
if (thinking) launchArgs.push('--thinking', thinking);
|
|
351
|
+
for (const flag of repeatedFlags) launchArgs.push('--flag', flag);
|
|
352
|
+
// Forward `--` passthrough as individual agent flags (same channel as --flag).
|
|
353
|
+
for (const token of passthrough) launchArgs.push('--flag', token);
|
|
354
|
+
await runLauncherCommand('run', launchArgs);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Custom agent: resolve the template locally, then append any --flag/passthrough
|
|
359
|
+
// tokens to the resolved command (launch-custom runs `<command> <context>`).
|
|
360
|
+
const values = buildCustomAgentValues(customAgent, model, thinking);
|
|
361
|
+
const extraTokens = [...repeatedFlags, ...passthrough].filter(Boolean);
|
|
362
|
+
const resolvedCommand = [resolveTemplate(customAgent.commandTemplate, values), ...extraTokens]
|
|
363
|
+
.join(' ')
|
|
364
|
+
.trim();
|
|
365
|
+
await runLauncherCommand('launch-custom', ['--command', resolvedCommand, ...baseLaunchArgs]);
|
|
366
|
+
}
|
package/bin/_cli/index.mjs
CHANGED
|
@@ -29,6 +29,7 @@ function printHelp(primaryCommand) {
|
|
|
29
29
|
Primary command: ${primaryCommand}
|
|
30
30
|
|
|
31
31
|
Usage:
|
|
32
|
+
${primaryCommand} <agent> "<prompt>" [flags] Create a ticket from the prompt and launch the agent on it
|
|
32
33
|
${primaryCommand} attach [ticketId] [agent] Search tickets and launch an agent (interactive)
|
|
33
34
|
${primaryCommand} create "<objective>" Create a ticket with numbered project selection; supports --agent/--model/--delegate
|
|
34
35
|
${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
|
|
@@ -48,6 +49,10 @@ Usage:
|
|
|
48
49
|
${primaryCommand} help Show this help message
|
|
49
50
|
|
|
50
51
|
Agents:
|
|
52
|
+
${primaryCommand} <agent> "<prompt>" [--model <id>] [--thinking <level>] [-- <agent flags>]
|
|
53
|
+
Built-in agents (claude, codex, cursor, antigravity, opencode) must have an
|
|
54
|
+
installed connector (${primaryCommand} setup <agent>); your custom agents launch by id.
|
|
55
|
+
Anything after a standalone -- is forwarded verbatim to the agent binary.
|
|
51
56
|
Use ${primaryCommand} protocol help for ticket lifecycle commands.
|
|
52
57
|
Key protocol commands: auth-status, discover-project, create, prompt, attach, connect, load-context.
|
|
53
58
|
|
|
@@ -172,12 +177,23 @@ export async function runCli({ primaryCommand }) {
|
|
|
172
177
|
command === 'connect' ||
|
|
173
178
|
command === 'restart' ||
|
|
174
179
|
command === 'run' ||
|
|
175
|
-
command === 'resume'
|
|
180
|
+
command === 'resume' ||
|
|
181
|
+
command === 'launch-custom'
|
|
176
182
|
) {
|
|
177
183
|
await runLauncherCommand(command, rest);
|
|
178
184
|
return;
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
// Direct agent launch: `ovld <agent> "<prompt>" [flags]`. Built-in agents
|
|
188
|
+
// dispatch immediately; an unknown command with a candidate objective may be
|
|
189
|
+
// a user's custom agent, so we let direct-launch resolve and report it.
|
|
190
|
+
const { isBuiltinLaunchAgent, runDirectLaunch } = await import('./direct-launch.mjs');
|
|
191
|
+
const hasCandidateObjective = rest.some(arg => !arg.startsWith('--'));
|
|
192
|
+
if (isBuiltinLaunchAgent(command) || hasCandidateObjective) {
|
|
193
|
+
await runDirectLaunch(command, rest);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
console.error(`Unknown command: ${command}\n`);
|
|
182
198
|
printHelp(primaryCommand);
|
|
183
199
|
process.exit(1);
|
package/bin/_cli/launcher.mjs
CHANGED
|
@@ -17,6 +17,29 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
17
17
|
const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
|
|
18
18
|
const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
|
|
19
19
|
|
|
20
|
+
function resolveLaunchScratchDir(workingDirectory, { explicit = false } = {}) {
|
|
21
|
+
const trimmed = typeof workingDirectory === 'string' ? workingDirectory.trim() : '';
|
|
22
|
+
const candidate = trimmed || process.cwd();
|
|
23
|
+
const useProjectScratch =
|
|
24
|
+
explicit || fs.existsSync(path.join(candidate, '.overlord', 'project.json'));
|
|
25
|
+
if (!useProjectScratch) {
|
|
26
|
+
return os.tmpdir();
|
|
27
|
+
}
|
|
28
|
+
const scratchDir = path.join(candidate, '.overlord', 'tmp');
|
|
29
|
+
fs.mkdirSync(scratchDir, { recursive: true });
|
|
30
|
+
return scratchDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withLaunchTempEnv(baseEnv, scratchDir) {
|
|
34
|
+
return {
|
|
35
|
+
...baseEnv,
|
|
36
|
+
TMPDIR: scratchDir,
|
|
37
|
+
TMP: scratchDir,
|
|
38
|
+
TEMP: scratchDir,
|
|
39
|
+
OVERLORD_TMPDIR: scratchDir
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
function claudeSourcePluginDir() {
|
|
21
44
|
if (fs.existsSync(PACKAGE_CLAUDE_PLUGIN_DIR)) return PACKAGE_CLAUDE_PLUGIN_DIR;
|
|
22
45
|
if (fs.existsSync(REPO_CLAUDE_PLUGIN_DIR)) return REPO_CLAUDE_PLUGIN_DIR;
|
|
@@ -53,8 +76,8 @@ function getInstructionMode(agent) {
|
|
|
53
76
|
return 'legacy';
|
|
54
77
|
}
|
|
55
78
|
|
|
56
|
-
function buildAgyLaunchArgv({ contextFile, mode, sessionId, extraArgs }) {
|
|
57
|
-
const argv = [...extraArgs, '--add-dir',
|
|
79
|
+
function buildAgyLaunchArgv({ contextFile, scratchDir, mode, sessionId, extraArgs }) {
|
|
80
|
+
const argv = [...extraArgs, '--add-dir', scratchDir];
|
|
58
81
|
if (mode === 'resume') {
|
|
59
82
|
const agySessionId = sessionId?.trim();
|
|
60
83
|
if (agySessionId) {
|
|
@@ -98,10 +121,29 @@ const agentIdentifierMap = {
|
|
|
98
121
|
|
|
99
122
|
const supportedAgents = ['claude', 'codex', 'cursor', 'antigravity', 'opencode', 'pi'];
|
|
100
123
|
|
|
124
|
+
// Re-exported under clearer names for the direct-launch dispatcher in index.mjs.
|
|
125
|
+
export { supportedAgents as BUILTIN_LAUNCH_AGENTS, agentIdentifierMap };
|
|
126
|
+
|
|
101
127
|
function shellQuote(value) {
|
|
102
128
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
103
129
|
}
|
|
104
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Run an agent binary, optionally routed through a user-defined pre-command
|
|
133
|
+
* (e.g. `ollama` to run claude-code through Ollama). When a pre-command is set,
|
|
134
|
+
* its first token becomes the executable and the agent binary + args follow it:
|
|
135
|
+
* preCommand="ollama" → ollama claude <args...>
|
|
136
|
+
*/
|
|
137
|
+
function execAgentBinary(binary, args, opts, preCommand) {
|
|
138
|
+
const pre = typeof preCommand === 'string' ? preCommand.trim() : '';
|
|
139
|
+
if (pre) {
|
|
140
|
+
const preTokens = pre.split(/\s+/).filter(Boolean);
|
|
141
|
+
execFileSync(preTokens[0], [...preTokens.slice(1), binary, ...args], opts);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
execFileSync(binary, args, opts);
|
|
145
|
+
}
|
|
146
|
+
|
|
105
147
|
function toTomlString(value) {
|
|
106
148
|
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
107
149
|
}
|
|
@@ -178,6 +220,9 @@ function buildRemoteLaunchCommand(agent, options) {
|
|
|
178
220
|
if (options.launchMode === 'ask') {
|
|
179
221
|
nestedParts.push('--launch-mode', 'ask');
|
|
180
222
|
}
|
|
223
|
+
if (options.preCommand) {
|
|
224
|
+
nestedParts.push('--pre-command', shellQuote(options.preCommand));
|
|
225
|
+
}
|
|
181
226
|
if (options.model) {
|
|
182
227
|
nestedParts.push('--model', shellQuote(options.model));
|
|
183
228
|
}
|
|
@@ -234,6 +279,7 @@ Options:
|
|
|
234
279
|
--model <identifier> Preferred model identifier
|
|
235
280
|
--thinking <level> Agent reasoning/effort level
|
|
236
281
|
--flag <value> Extra agent flag (repeatable)
|
|
282
|
+
--pre-command <command> Command to run before the agent binary (e.g. ollama)
|
|
237
283
|
--ssh-command <command> Launch remotely over SSH by running ovld on the target host
|
|
238
284
|
--remote-working-directory <path> Change to this path on the remote host before launch
|
|
239
285
|
--server-multiplexer <none|tmux> Wrap remote launches in tmux
|
|
@@ -307,6 +353,10 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
307
353
|
process.chdir(options.workingDirectory);
|
|
308
354
|
}
|
|
309
355
|
|
|
356
|
+
const scratchDir = resolveLaunchScratchDir(options.workingDirectory, {
|
|
357
|
+
explicit: Boolean(options.workingDirectory)
|
|
358
|
+
});
|
|
359
|
+
|
|
310
360
|
const launchOrganizationId = resolveLaunchOrganizationId(
|
|
311
361
|
ticketId,
|
|
312
362
|
options.organizationId,
|
|
@@ -344,13 +394,13 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
344
394
|
agent
|
|
345
395
|
);
|
|
346
396
|
|
|
347
|
-
const childEnv = {
|
|
397
|
+
const childEnv = withLaunchTempEnv({
|
|
348
398
|
...process.env,
|
|
349
399
|
AGENT_IDENTIFIER: agentIdentifierMap[agent],
|
|
350
400
|
...(resolvedLaunchOrganizationId
|
|
351
401
|
? { OVERLORD_ORGANIZATION_ID: String(resolvedLaunchOrganizationId) }
|
|
352
402
|
: {})
|
|
353
|
-
};
|
|
403
|
+
}, scratchDir);
|
|
354
404
|
const extraArgs = buildExtraArgs(agent, options);
|
|
355
405
|
|
|
356
406
|
try {
|
|
@@ -363,7 +413,7 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
363
413
|
: ['--continue', context];
|
|
364
414
|
args.unshift(...extraArgs);
|
|
365
415
|
if (pluginDir) args.unshift('--plugin-dir', pluginDir);
|
|
366
|
-
|
|
416
|
+
execAgentBinary('claude', args, { stdio: 'inherit', env: childEnv }, options.preCommand);
|
|
367
417
|
} else {
|
|
368
418
|
const args = [
|
|
369
419
|
'--append-system-prompt',
|
|
@@ -372,11 +422,7 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
372
422
|
'Begin working on this ticket. Start by calling the attach endpoint, then proceed with the objective described in your system prompt.'
|
|
373
423
|
];
|
|
374
424
|
if (pluginDir) args.unshift('--plugin-dir', pluginDir);
|
|
375
|
-
|
|
376
|
-
'claude',
|
|
377
|
-
args,
|
|
378
|
-
{ stdio: 'inherit', env: childEnv }
|
|
379
|
-
);
|
|
425
|
+
execAgentBinary('claude', args, { stdio: 'inherit', env: childEnv }, options.preCommand);
|
|
380
426
|
}
|
|
381
427
|
} else if (agent === 'codex') {
|
|
382
428
|
if (mode === 'resume') {
|
|
@@ -385,12 +431,22 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
385
431
|
? ['resume', codexSessionId, context]
|
|
386
432
|
: ['resume', '--last', context];
|
|
387
433
|
args.splice(1, 0, ...extraArgs);
|
|
388
|
-
|
|
434
|
+
execAgentBinary('codex', args, { stdio: 'inherit', env: childEnv }, options.preCommand);
|
|
389
435
|
} else {
|
|
390
|
-
|
|
436
|
+
execAgentBinary(
|
|
437
|
+
'codex',
|
|
438
|
+
[...extraArgs, context],
|
|
439
|
+
{ stdio: 'inherit', env: childEnv },
|
|
440
|
+
options.preCommand
|
|
441
|
+
);
|
|
391
442
|
}
|
|
392
443
|
} else if (agent === 'cursor') {
|
|
393
|
-
|
|
444
|
+
execAgentBinary(
|
|
445
|
+
'agent',
|
|
446
|
+
[...extraArgs, context],
|
|
447
|
+
{ stdio: 'inherit', env: childEnv },
|
|
448
|
+
options.preCommand
|
|
449
|
+
);
|
|
394
450
|
} else if (agent === 'opencode') {
|
|
395
451
|
if (mode === 'resume') {
|
|
396
452
|
const openCodeSessionId = process.env.OPENCODE_SESSION_ID?.trim();
|
|
@@ -398,12 +454,14 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
398
454
|
? ['--continue', '--session', openCodeSessionId, '--prompt', context]
|
|
399
455
|
: ['--continue', '--prompt', context];
|
|
400
456
|
args.unshift(...extraArgs);
|
|
401
|
-
|
|
457
|
+
execAgentBinary('opencode', args, { stdio: 'inherit', env: childEnv }, options.preCommand);
|
|
402
458
|
} else {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
459
|
+
execAgentBinary(
|
|
460
|
+
'opencode',
|
|
461
|
+
[...extraArgs, '--prompt', context],
|
|
462
|
+
{ stdio: 'inherit', env: childEnv },
|
|
463
|
+
options.preCommand
|
|
464
|
+
);
|
|
407
465
|
}
|
|
408
466
|
} else if (agent === 'pi') {
|
|
409
467
|
if (mode === 'resume') {
|
|
@@ -412,27 +470,34 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
412
470
|
? ['--session', piSessionId, context]
|
|
413
471
|
: ['--continue', context];
|
|
414
472
|
args.unshift(...extraArgs);
|
|
415
|
-
|
|
473
|
+
execAgentBinary('pi', args, { stdio: 'inherit', env: childEnv }, options.preCommand);
|
|
416
474
|
} else {
|
|
417
|
-
|
|
475
|
+
execAgentBinary(
|
|
476
|
+
'pi',
|
|
477
|
+
[...extraArgs, context],
|
|
478
|
+
{ stdio: 'inherit', env: childEnv },
|
|
479
|
+
options.preCommand
|
|
480
|
+
);
|
|
418
481
|
}
|
|
419
482
|
} else if (agent === 'antigravity') {
|
|
420
483
|
const tag = `overlord-${ticketId.slice(-8)}-${Date.now()}`;
|
|
421
|
-
const contextFile = path.join(
|
|
484
|
+
const contextFile = path.join(scratchDir, `${tag}-ctx.md`);
|
|
422
485
|
fs.writeFileSync(contextFile, context, 'utf-8');
|
|
423
486
|
setTimeout(() => { try { fs.unlinkSync(contextFile); } catch { /* already gone */ } }, 30 * 60_000).unref();
|
|
424
487
|
|
|
425
488
|
const agySessionId =
|
|
426
489
|
process.env.AGY_SESSION_ID?.trim() ?? process.env.GEMINI_SESSION_ID?.trim();
|
|
427
|
-
|
|
490
|
+
execAgentBinary(
|
|
428
491
|
'agy',
|
|
429
492
|
buildAgyLaunchArgv({
|
|
430
493
|
contextFile,
|
|
494
|
+
scratchDir,
|
|
431
495
|
mode,
|
|
432
496
|
sessionId: agySessionId,
|
|
433
497
|
extraArgs
|
|
434
498
|
}),
|
|
435
|
-
{ stdio: 'inherit', env: childEnv }
|
|
499
|
+
{ stdio: 'inherit', env: childEnv },
|
|
500
|
+
options.preCommand
|
|
436
501
|
);
|
|
437
502
|
}
|
|
438
503
|
} catch (error) {
|
|
@@ -460,12 +525,89 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
460
525
|
}
|
|
461
526
|
}
|
|
462
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Launch a user-defined custom agent. The resolved launch command (with all
|
|
530
|
+
* `{{token}}` placeholders already substituted) is passed via --command; we
|
|
531
|
+
* fetch the ticket context and run `<command> <context>` in the shell.
|
|
532
|
+
*/
|
|
533
|
+
async function runCustomAgent(args) {
|
|
534
|
+
const { flags } = parseLauncherArgs(args);
|
|
535
|
+
const command = typeof flags.command === 'string' ? flags.command.trim() : '';
|
|
536
|
+
const ticketId = typeof flags['ticket-id'] === 'string' ? flags['ticket-id'].trim() : '';
|
|
537
|
+
if (!command) {
|
|
538
|
+
console.error('Missing required option: --command "<resolved launch command>"');
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
if (ticketId) {
|
|
542
|
+
process.env.TICKET_ID = ticketId;
|
|
543
|
+
}
|
|
544
|
+
const resolvedTicketId = process.env.TICKET_ID;
|
|
545
|
+
if (!resolvedTicketId) {
|
|
546
|
+
console.error('Missing required environment variable: TICKET_ID');
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (typeof flags['working-directory'] === 'string' && flags['working-directory'].trim()) {
|
|
551
|
+
process.chdir(flags['working-directory'].trim());
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const scratchDir = resolveLaunchScratchDir(flags['working-directory'], {
|
|
555
|
+
explicit: typeof flags['working-directory'] === 'string' && flags['working-directory'].trim().length > 0
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const launchOrganizationId = resolveLaunchOrganizationId(
|
|
559
|
+
resolvedTicketId,
|
|
560
|
+
flags['organization-id'],
|
|
561
|
+
null
|
|
562
|
+
);
|
|
563
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth({
|
|
564
|
+
organizationIdHint: launchOrganizationId
|
|
565
|
+
});
|
|
566
|
+
const resolvedLaunchOrganizationId = resolveLaunchOrganizationId(
|
|
567
|
+
resolvedTicketId,
|
|
568
|
+
flags['organization-id'],
|
|
569
|
+
organizationId
|
|
570
|
+
);
|
|
571
|
+
// Custom agents have no Overlord plugin/bundle; use the generic "claude" context.
|
|
572
|
+
const context = await fetchContext(
|
|
573
|
+
platformUrl,
|
|
574
|
+
bearerToken,
|
|
575
|
+
localSecret,
|
|
576
|
+
resolvedLaunchOrganizationId,
|
|
577
|
+
resolvedTicketId,
|
|
578
|
+
'claude'
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const childEnv = withLaunchTempEnv({
|
|
582
|
+
...process.env,
|
|
583
|
+
AGENT_IDENTIFIER: 'custom',
|
|
584
|
+
...(resolvedLaunchOrganizationId
|
|
585
|
+
? { OVERLORD_ORGANIZATION_ID: String(resolvedLaunchOrganizationId) }
|
|
586
|
+
: {})
|
|
587
|
+
}, scratchDir);
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
execFileSync('sh', ['-lc', `${command} ${shellQuote(context)}`], {
|
|
591
|
+
stdio: 'inherit',
|
|
592
|
+
env: childEnv
|
|
593
|
+
});
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
463
600
|
export async function runLauncherCommand(command, args) {
|
|
464
601
|
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
465
602
|
printLauncherHelp();
|
|
466
603
|
return;
|
|
467
604
|
}
|
|
468
605
|
|
|
606
|
+
if (command === 'launch-custom') {
|
|
607
|
+
await runCustomAgent(args);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
469
611
|
const normalizedCommand =
|
|
470
612
|
command === 'launch' || command === 'connect'
|
|
471
613
|
? 'run'
|
|
@@ -486,6 +628,7 @@ export async function runLauncherCommand(command, args) {
|
|
|
486
628
|
launchMode: flags['launch-mode'] === 'ask' ? 'ask' : 'run',
|
|
487
629
|
model: typeof flags.model === 'string' ? flags.model.trim() : '',
|
|
488
630
|
thinking: typeof flags.thinking === 'string' ? flags.thinking.trim() : '',
|
|
631
|
+
preCommand: typeof flags['pre-command'] === 'string' ? flags['pre-command'].trim() : '',
|
|
489
632
|
flags: repeatedFlags,
|
|
490
633
|
organizationId:
|
|
491
634
|
typeof flags['organization-id'] === 'string' ? flags['organization-id'].trim() : '',
|
package/bin/_cli/setup.mjs
CHANGED
|
@@ -485,6 +485,24 @@ function writeManifest(manifest) {
|
|
|
485
485
|
writeJsonFile(MANIFEST_FILE, manifest);
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Quietly report whether an agent connector is installed: present in the bundle
|
|
490
|
+
* manifest with all of its recorded files still on disk. Unlike `doctorAgent`,
|
|
491
|
+
* this logs nothing and ignores version/hash staleness — a stale-but-present
|
|
492
|
+
* connector can still launch. Used to gate `ovld <agent>` direct launches.
|
|
493
|
+
*/
|
|
494
|
+
export function isAgentConnectorInstalled(agent) {
|
|
495
|
+
const manifest = readManifest();
|
|
496
|
+
const entry = manifest[agent];
|
|
497
|
+
if (!entry || !Array.isArray(entry.files)) return false;
|
|
498
|
+
return entry.files.every(file => fs.existsSync(file));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** The built-in agents whose connectors are currently installed. */
|
|
502
|
+
export function listInstalledConnectors() {
|
|
503
|
+
return supportedAgents.filter(agent => isAgentConnectorInstalled(agent));
|
|
504
|
+
}
|
|
505
|
+
|
|
488
506
|
function slashCommandFiles(agent) {
|
|
489
507
|
if (agent === 'claude') {
|
|
490
508
|
const base = path.join(os.homedir(), '.claude', 'commands');
|