overlord-cli 0.2605290912.0 → 0.2605291046.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/bin/_cli/direct-launch.mjs +6 -2
- package/bin/_cli/launcher.mjs +58 -8
- package/bin/_cli/runner.mjs +176 -6
- package/package.json +1 -1
|
@@ -39,7 +39,9 @@ const VALUE_FLAGS = new Set([
|
|
|
39
39
|
'ssh-command',
|
|
40
40
|
'remote-working-directory',
|
|
41
41
|
'server-multiplexer',
|
|
42
|
-
'tmux-command'
|
|
42
|
+
'tmux-command',
|
|
43
|
+
'feed-post-id',
|
|
44
|
+
'initial-question'
|
|
43
45
|
]);
|
|
44
46
|
|
|
45
47
|
const BOOLEAN_FLAGS = new Set(['personal', 'for-human', 'allow-uninstalled']);
|
|
@@ -52,7 +54,9 @@ const FORWARDED_VALUE_FLAGS = [
|
|
|
52
54
|
'ssh-command',
|
|
53
55
|
'remote-working-directory',
|
|
54
56
|
'server-multiplexer',
|
|
55
|
-
'tmux-command'
|
|
57
|
+
'tmux-command',
|
|
58
|
+
'feed-post-id',
|
|
59
|
+
'initial-question'
|
|
56
60
|
];
|
|
57
61
|
|
|
58
62
|
export function isBuiltinLaunchAgent(name) {
|
package/bin/_cli/launcher.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
9
10
|
import fs from 'node:fs';
|
|
10
11
|
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
@@ -90,12 +91,21 @@ function buildAgyLaunchArgv({ contextFile, scratchDir, mode, sessionId, extraArg
|
|
|
90
91
|
return argv;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
async function fetchContext(platformUrl, bearerToken, localSecret, organizationId, ticketId, agent) {
|
|
94
|
+
async function fetchContext(platformUrl, bearerToken, localSecret, organizationId, ticketId, agent, options = {}) {
|
|
94
95
|
const params = new URLSearchParams({
|
|
95
96
|
context: 'cli',
|
|
96
97
|
agent,
|
|
97
98
|
instructionMode: getInstructionMode(agent)
|
|
98
99
|
});
|
|
100
|
+
if (options.launchMode === 'ask') params.set('mode', 'ask');
|
|
101
|
+
if (options.sessionId) params.set('sessionId', options.sessionId);
|
|
102
|
+
if (options.workspace) params.set('workspace', options.workspace);
|
|
103
|
+
if (options.feedPostId) {
|
|
104
|
+
params.set('feedPostId', options.feedPostId);
|
|
105
|
+
if (typeof options.initialQuestion === 'string') {
|
|
106
|
+
params.set('initialQuestion', options.initialQuestion);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
99
109
|
const url = `${platformUrl}/api/protocol/context/${ticketId}?${params.toString()}`;
|
|
100
110
|
const response = await fetch(url, {
|
|
101
111
|
headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
|
|
@@ -107,7 +117,10 @@ async function fetchContext(platformUrl, bearerToken, localSecret, organizationI
|
|
|
107
117
|
);
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
const context = await response.text();
|
|
121
|
+
const workingDirectory = response.headers.get('X-Working-Directory') ?? '';
|
|
122
|
+
const humanTicketId = response.headers.get('X-Ticket-Id') ?? '';
|
|
123
|
+
return { context, workingDirectory, humanTicketId };
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
const agentIdentifierMap = {
|
|
@@ -284,6 +297,8 @@ Options:
|
|
|
284
297
|
--remote-working-directory <path> Change to this path on the remote host before launch
|
|
285
298
|
--server-multiplexer <none|tmux> Wrap remote launches in tmux
|
|
286
299
|
--tmux-command <template> Remote tmux template; use {script} as the placeholder
|
|
300
|
+
--feed-post-id <uuid> Include a feed post in the context (desktop feed discuss)
|
|
301
|
+
--initial-question <text> First user question for feed discuss (paired with --feed-post-id)
|
|
287
302
|
|
|
288
303
|
Notes:
|
|
289
304
|
- ovld launch is the primary user-facing launcher.
|
|
@@ -370,7 +385,10 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
370
385
|
options.organizationId,
|
|
371
386
|
organizationId
|
|
372
387
|
);
|
|
373
|
-
|
|
388
|
+
const launchSessionId = crypto.randomUUID();
|
|
389
|
+
const isRemote = Boolean(options.sshCommand?.trim());
|
|
390
|
+
|
|
391
|
+
if (isRemote) {
|
|
374
392
|
const remoteCommand = buildRemoteLaunchCommand(agent, {
|
|
375
393
|
...options,
|
|
376
394
|
organizationId: resolvedLaunchOrganizationId
|
|
@@ -385,18 +403,40 @@ async function runAgent(agent, mode = 'run', options = {}) {
|
|
|
385
403
|
}
|
|
386
404
|
}
|
|
387
405
|
|
|
388
|
-
const context = await fetchContext(
|
|
406
|
+
const { context, workingDirectory: apiWorkingDirectory, humanTicketId } = await fetchContext(
|
|
389
407
|
platformUrl,
|
|
390
408
|
bearerToken,
|
|
391
409
|
localSecret,
|
|
392
410
|
resolvedLaunchOrganizationId,
|
|
393
411
|
ticketId,
|
|
394
|
-
agent
|
|
412
|
+
agent,
|
|
413
|
+
{
|
|
414
|
+
launchMode: options.launchMode,
|
|
415
|
+
sessionId: launchSessionId,
|
|
416
|
+
workspace: isRemote ? 'ssh' : undefined,
|
|
417
|
+
feedPostId: options.feedPostId,
|
|
418
|
+
initialQuestion: options.initialQuestion
|
|
419
|
+
}
|
|
395
420
|
);
|
|
396
421
|
|
|
422
|
+
if (humanTicketId) {
|
|
423
|
+
process.env.TICKET_ID = humanTicketId;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!options.workingDirectory && apiWorkingDirectory) {
|
|
427
|
+
try {
|
|
428
|
+
process.chdir(apiWorkingDirectory);
|
|
429
|
+
} catch {
|
|
430
|
+
// Best-effort; the agent will run in the current directory.
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
397
434
|
const childEnv = withLaunchTempEnv({
|
|
398
435
|
...process.env,
|
|
399
436
|
AGENT_IDENTIFIER: agentIdentifierMap[agent],
|
|
437
|
+
OVERLORD_LAUNCH_SESSION_ID: launchSessionId,
|
|
438
|
+
OVERLORD_MODEL_IDENTIFIER: options.model ?? '',
|
|
439
|
+
MODEL_IDENTIFIER: options.model ?? '',
|
|
400
440
|
...(resolvedLaunchOrganizationId
|
|
401
441
|
? { OVERLORD_ORGANIZATION_ID: String(resolvedLaunchOrganizationId) }
|
|
402
442
|
: {})
|
|
@@ -568,19 +608,27 @@ async function runCustomAgent(args) {
|
|
|
568
608
|
flags['organization-id'],
|
|
569
609
|
organizationId
|
|
570
610
|
);
|
|
611
|
+
const launchSessionId = crypto.randomUUID();
|
|
612
|
+
const feedPostId = typeof flags['feed-post-id'] === 'string' ? flags['feed-post-id'].trim() : '';
|
|
613
|
+
const initialQuestion = typeof flags['initial-question'] === 'string' ? flags['initial-question'].trim() : '';
|
|
571
614
|
// Custom agents have no Overlord plugin/bundle; use the generic "claude" context.
|
|
572
|
-
const context = await fetchContext(
|
|
615
|
+
const { context } = await fetchContext(
|
|
573
616
|
platformUrl,
|
|
574
617
|
bearerToken,
|
|
575
618
|
localSecret,
|
|
576
619
|
resolvedLaunchOrganizationId,
|
|
577
620
|
resolvedTicketId,
|
|
578
|
-
'claude'
|
|
621
|
+
'claude',
|
|
622
|
+
{
|
|
623
|
+
sessionId: launchSessionId,
|
|
624
|
+
...(feedPostId ? { feedPostId, initialQuestion } : {})
|
|
625
|
+
}
|
|
579
626
|
);
|
|
580
627
|
|
|
581
628
|
const childEnv = withLaunchTempEnv({
|
|
582
629
|
...process.env,
|
|
583
630
|
AGENT_IDENTIFIER: 'custom',
|
|
631
|
+
OVERLORD_LAUNCH_SESSION_ID: launchSessionId,
|
|
584
632
|
...(resolvedLaunchOrganizationId
|
|
585
633
|
? { OVERLORD_ORGANIZATION_ID: String(resolvedLaunchOrganizationId) }
|
|
586
634
|
: {})
|
|
@@ -639,7 +687,9 @@ export async function runLauncherCommand(command, args) {
|
|
|
639
687
|
: '',
|
|
640
688
|
serverMultiplexer:
|
|
641
689
|
flags['server-multiplexer'] === 'tmux' ? 'tmux' : 'none',
|
|
642
|
-
tmuxCommand: typeof flags['tmux-command'] === 'string' ? flags['tmux-command'].trim() : ''
|
|
690
|
+
tmuxCommand: typeof flags['tmux-command'] === 'string' ? flags['tmux-command'].trim() : '',
|
|
691
|
+
feedPostId: typeof flags['feed-post-id'] === 'string' ? flags['feed-post-id'].trim() : '',
|
|
692
|
+
initialQuestion: typeof flags['initial-question'] === 'string' ? flags['initial-question'].trim() : ''
|
|
643
693
|
};
|
|
644
694
|
|
|
645
695
|
if (normalizedCommand === 'run') {
|
package/bin/_cli/runner.mjs
CHANGED
|
@@ -8,11 +8,13 @@ import path from 'node:path';
|
|
|
8
8
|
|
|
9
9
|
const OVLD_ENTRY = process.argv[1];
|
|
10
10
|
const DEVICE_FILE = path.join(os.homedir(), '.ovld', 'device.json');
|
|
11
|
+
const DEFAULT_TMUX_COMMAND = 'tmux new-session bash {script}';
|
|
11
12
|
|
|
12
13
|
/** @internal Test-only overrides for protocol/launch subprocess calls. */
|
|
13
14
|
export const runnerTestHooks = {
|
|
14
15
|
execFileSync: null,
|
|
15
|
-
spawn: null
|
|
16
|
+
spawn: null,
|
|
17
|
+
platform: null
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
function printRunnerHelp(primaryCommand = 'ovld') {
|
|
@@ -131,9 +133,167 @@ export function buildLaunchArgs(launch) {
|
|
|
131
133
|
}
|
|
132
134
|
if (launch.serverMultiplexer === 'tmux') args.push('--server-multiplexer', 'tmux');
|
|
133
135
|
if (launch.tmuxCommand) args.push('--tmux-command', launch.tmuxCommand);
|
|
136
|
+
if (launch.feedPostId) args.push('--feed-post-id', launch.feedPostId);
|
|
137
|
+
if (launch.initialQuestion) args.push('--initial-question', launch.initialQuestion);
|
|
134
138
|
return args;
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
function shellQuote(value) {
|
|
142
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appleScriptString(value) {
|
|
146
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeRunnerTerminalProfile(value) {
|
|
150
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
151
|
+
return {
|
|
152
|
+
terminalApp: typeof value.terminalApp === 'string' ? value.terminalApp : 'default',
|
|
153
|
+
terminalLaunchMode:
|
|
154
|
+
typeof value.terminalLaunchMode === 'string' ? value.terminalLaunchMode : 'tab',
|
|
155
|
+
terminalCustomHotkey:
|
|
156
|
+
typeof value.terminalCustomHotkey === 'string' ? value.terminalCustomHotkey : '',
|
|
157
|
+
customTerminalApp:
|
|
158
|
+
typeof value.customTerminalApp === 'string' ? value.customTerminalApp : '',
|
|
159
|
+
terminalTmuxHostApp:
|
|
160
|
+
typeof value.terminalTmuxHostApp === 'string' ? value.terminalTmuxHostApp : 'terminal',
|
|
161
|
+
customTerminalTmuxHostApp:
|
|
162
|
+
typeof value.customTerminalTmuxHostApp === 'string'
|
|
163
|
+
? value.customTerminalTmuxHostApp
|
|
164
|
+
: '',
|
|
165
|
+
terminalTmuxCommand:
|
|
166
|
+
typeof value.terminalTmuxCommand === 'string' && value.terminalTmuxCommand.trim()
|
|
167
|
+
? value.terminalTmuxCommand
|
|
168
|
+
: DEFAULT_TMUX_COMMAND
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildRunnerLaunchShellCommand(args, deviceFingerprint) {
|
|
173
|
+
return [
|
|
174
|
+
`export OVERLORD_DEVICE_FINGERPRINT=${shellQuote(deviceFingerprint)}`,
|
|
175
|
+
`exec ${shellQuote(process.execPath)} ${[OVLD_ENTRY, ...args].map(shellQuote).join(' ')}`
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function writeRunnerLaunchScript(launchCommand) {
|
|
180
|
+
const dir = path.join(os.tmpdir(), 'ovld-runner');
|
|
181
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
182
|
+
const scriptPath = path.join(
|
|
183
|
+
dir,
|
|
184
|
+
`launch-${process.pid}-${Date.now()}-${crypto.randomUUID()}.sh`
|
|
185
|
+
);
|
|
186
|
+
fs.writeFileSync(scriptPath, `#!/usr/bin/env bash\nset -e\n${launchCommand}\n`, {
|
|
187
|
+
mode: 0o700
|
|
188
|
+
});
|
|
189
|
+
return scriptPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildTmuxCommand(template, scriptPath) {
|
|
193
|
+
const resolvedTemplate =
|
|
194
|
+
typeof template === 'string' && template.trim().includes('{script}')
|
|
195
|
+
? template.trim()
|
|
196
|
+
: DEFAULT_TMUX_COMMAND;
|
|
197
|
+
return resolvedTemplate.replaceAll('{script}', shellQuote(scriptPath));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function terminalAppName(profile) {
|
|
201
|
+
if (profile.terminalApp === 'custom') return profile.customTerminalApp.trim();
|
|
202
|
+
const appMap = {
|
|
203
|
+
default: 'Terminal',
|
|
204
|
+
terminal: 'Terminal',
|
|
205
|
+
iterm: 'iTerm',
|
|
206
|
+
warp: 'Warp',
|
|
207
|
+
ghostty: 'Ghostty',
|
|
208
|
+
alacritty: 'Alacritty',
|
|
209
|
+
kitty: 'Kitty',
|
|
210
|
+
hyper: 'Hyper'
|
|
211
|
+
};
|
|
212
|
+
return appMap[profile.terminalApp] ?? '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function buildRunnerTerminalOpenCommand(
|
|
216
|
+
profileValue,
|
|
217
|
+
launchCommand,
|
|
218
|
+
platform = process.platform
|
|
219
|
+
) {
|
|
220
|
+
const profile = normalizeRunnerTerminalProfile(profileValue);
|
|
221
|
+
if (!profile) return null;
|
|
222
|
+
|
|
223
|
+
const command =
|
|
224
|
+
profile.terminalApp === 'tmux'
|
|
225
|
+
? buildTmuxCommand(profile.terminalTmuxCommand, writeRunnerLaunchScript(launchCommand))
|
|
226
|
+
: launchCommand;
|
|
227
|
+
|
|
228
|
+
if (profile.terminalApp === 'tmux' && platform !== 'darwin') {
|
|
229
|
+
return command;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (platform !== 'darwin') return null;
|
|
233
|
+
|
|
234
|
+
const appName =
|
|
235
|
+
profile.terminalApp === 'tmux'
|
|
236
|
+
? terminalAppName({
|
|
237
|
+
...profile,
|
|
238
|
+
terminalApp: profile.terminalTmuxHostApp,
|
|
239
|
+
customTerminalApp: profile.customTerminalTmuxHostApp
|
|
240
|
+
})
|
|
241
|
+
: terminalAppName(profile);
|
|
242
|
+
if (!appName) return null;
|
|
243
|
+
|
|
244
|
+
if (appName === 'iTerm') {
|
|
245
|
+
return [
|
|
246
|
+
'osascript',
|
|
247
|
+
'-e',
|
|
248
|
+
shellQuote('tell application "iTerm"'),
|
|
249
|
+
'-e',
|
|
250
|
+
shellQuote(`create window with default profile command ${appleScriptString(command)}`),
|
|
251
|
+
'-e',
|
|
252
|
+
shellQuote('activate'),
|
|
253
|
+
'-e',
|
|
254
|
+
shellQuote('end tell')
|
|
255
|
+
].join(' ');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [
|
|
259
|
+
'osascript',
|
|
260
|
+
'-e',
|
|
261
|
+
shellQuote(
|
|
262
|
+
`tell application ${appleScriptString(appName)} to do script ${appleScriptString(command)}`
|
|
263
|
+
),
|
|
264
|
+
'-e',
|
|
265
|
+
shellQuote(`tell application ${appleScriptString(appName)} to activate`)
|
|
266
|
+
].join(' ');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function spawnLaunchProcess(args, claim, deviceFingerprint) {
|
|
270
|
+
const spawnImpl = runnerTestHooks.spawn ?? spawn;
|
|
271
|
+
const launchCommand = buildRunnerLaunchShellCommand(args, deviceFingerprint);
|
|
272
|
+
const terminalCommand = buildRunnerTerminalOpenCommand(
|
|
273
|
+
claim.launch?.runnerTerminalProfile,
|
|
274
|
+
launchCommand,
|
|
275
|
+
runnerTestHooks.platform ?? process.platform
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!terminalCommand) {
|
|
279
|
+
return {
|
|
280
|
+
child: spawnImpl(process.execPath, [OVLD_ENTRY, ...args], {
|
|
281
|
+
stdio: 'inherit',
|
|
282
|
+
env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
|
|
283
|
+
}),
|
|
284
|
+
completeOnClose: false
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
child: spawnImpl('sh', ['-lc', terminalCommand], {
|
|
290
|
+
stdio: 'inherit',
|
|
291
|
+
env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
|
|
292
|
+
}),
|
|
293
|
+
completeOnClose: true
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
137
297
|
export async function launchClaimedRequest(claim, deviceFingerprint) {
|
|
138
298
|
const requestId = claim.request?.id;
|
|
139
299
|
if (!requestId || !claim.launch?.agent || !claim.launch?.ticketId) return false;
|
|
@@ -144,13 +304,10 @@ export async function launchClaimedRequest(claim, deviceFingerprint) {
|
|
|
144
304
|
);
|
|
145
305
|
|
|
146
306
|
await new Promise((resolve, reject) => {
|
|
147
|
-
const
|
|
148
|
-
const child = spawnImpl(process.execPath, [OVLD_ENTRY, ...args], {
|
|
149
|
-
stdio: 'inherit',
|
|
150
|
-
env: { ...process.env, OVERLORD_DEVICE_FINGERPRINT: deviceFingerprint }
|
|
151
|
-
});
|
|
307
|
+
const { child, completeOnClose } = spawnLaunchProcess(args, claim, deviceFingerprint);
|
|
152
308
|
|
|
153
309
|
child.once('spawn', () => {
|
|
310
|
+
if (completeOnClose) return;
|
|
154
311
|
try {
|
|
155
312
|
completeLaunch(requestId, deviceFingerprint);
|
|
156
313
|
} catch (error) {
|
|
@@ -168,6 +325,19 @@ export async function launchClaimedRequest(claim, deviceFingerprint) {
|
|
|
168
325
|
});
|
|
169
326
|
|
|
170
327
|
child.once('close', code => {
|
|
328
|
+
if (completeOnClose && (!code || code === 0)) {
|
|
329
|
+
try {
|
|
330
|
+
completeLaunch(requestId, deviceFingerprint);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
process.stderr.write(`[runner] failed to mark request launched: ${error}\n`);
|
|
333
|
+
}
|
|
334
|
+
} else if (completeOnClose && code) {
|
|
335
|
+
try {
|
|
336
|
+
failLaunch(requestId, deviceFingerprint, `terminal launcher exited with code ${code}`);
|
|
337
|
+
} catch {
|
|
338
|
+
// Preserve the launcher exit below.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
171
341
|
if (code && code !== 0) {
|
|
172
342
|
process.stderr.write(`[runner] launch process exited with code ${code}\n`);
|
|
173
343
|
}
|