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.
@@ -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) {
@@ -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
- return response.text();
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
- if (options.sshCommand?.trim()) {
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') {
@@ -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 spawnImpl = runnerTestHooks.spawn ?? spawn;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605290912.0",
3
+ "version": "0.2605291046.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",