overlord-cli 0.2605290912.0 → 0.2605291450.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) {
@@ -212,7 +216,7 @@ Overlord flags:
212
216
  --model <identifier> Preferred model (sets the ticket delegate and is forwarded to the agent)
213
217
  --thinking <level> Agent reasoning/effort level
214
218
  --flag <value> Extra agent flag (repeatable)
215
- --pre-command <command> Command to run before the agent binary (e.g. ollama)
219
+ --pre-command <command> Shell command to run before the agent binary (e.g. ollama, agent-pod)
216
220
  --project-id <uuid> Target project (skips working-directory resolution)
217
221
  --personal Create the ticket in your personal project
218
222
  --for-human Create a human-review ticket
@@ -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 = {
@@ -124,24 +137,53 @@ const supportedAgents = ['claude', 'codex', 'cursor', 'antigravity', 'opencode',
124
137
  // Re-exported under clearer names for the direct-launch dispatcher in index.mjs.
125
138
  export { supportedAgents as BUILTIN_LAUNCH_AGENTS, agentIdentifierMap };
126
139
 
140
+ /** @internal Test-only overrides for launcher subprocess calls. */
141
+ export const launcherTestHooks = {
142
+ execFileSync: null,
143
+ shell: null,
144
+ platform: null
145
+ };
146
+
127
147
  function shellQuote(value) {
128
148
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
129
149
  }
130
150
 
151
+ function resolveLauncherShell() {
152
+ const platform = launcherTestHooks.platform ?? process.platform;
153
+ if (platform === 'win32') {
154
+ return null;
155
+ }
156
+ return launcherTestHooks.shell ?? process.env.SHELL ?? 'sh';
157
+ }
158
+
131
159
  /**
132
160
  * 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...>
161
+ * (e.g. `ollama` to run claude-code through Ollama). On POSIX shells, execute
162
+ * the wrapper through the user's interactive login shell so aliases, functions,
163
+ * and PATH customizations are available:
164
+ * preCommand="agent-pod" → $SHELL -ilc 'agent-pod codex <args...>'
165
+ *
166
+ * The shell must be interactive (`-i`), not just a login shell (`-l`): zsh only
167
+ * sources ~/.zshrc — and bash only sources ~/.bashrc — for interactive shells.
168
+ * Wrappers like agent-pod are commonly installed as a shell alias in ~/.zshrc
169
+ * (its documented default), so a plain `-lc` login shell never sees them and
170
+ * fails with `command not found`.
136
171
  */
137
172
  function execAgentBinary(binary, args, opts, preCommand) {
173
+ const exec = launcherTestHooks.execFileSync ?? execFileSync;
138
174
  const pre = typeof preCommand === 'string' ? preCommand.trim() : '';
139
175
  if (pre) {
176
+ const shell = resolveLauncherShell();
177
+ if (shell) {
178
+ const command = [pre, shellQuote(binary), ...args.map(arg => shellQuote(arg))].join(' ');
179
+ exec(shell, ['-ilc', command], opts);
180
+ return;
181
+ }
140
182
  const preTokens = pre.split(/\s+/).filter(Boolean);
141
- execFileSync(preTokens[0], [...preTokens.slice(1), binary, ...args], opts);
183
+ exec(preTokens[0], [...preTokens.slice(1), binary, ...args], opts);
142
184
  return;
143
185
  }
144
- execFileSync(binary, args, opts);
186
+ exec(binary, args, opts);
145
187
  }
146
188
 
147
189
  function toTomlString(value) {
@@ -279,11 +321,13 @@ Options:
279
321
  --model <identifier> Preferred model identifier
280
322
  --thinking <level> Agent reasoning/effort level
281
323
  --flag <value> Extra agent flag (repeatable)
282
- --pre-command <command> Command to run before the agent binary (e.g. ollama)
324
+ --pre-command <command> Shell command to run before the agent binary (e.g. ollama, agent-pod)
283
325
  --ssh-command <command> Launch remotely over SSH by running ovld on the target host
284
326
  --remote-working-directory <path> Change to this path on the remote host before launch
285
327
  --server-multiplexer <none|tmux> Wrap remote launches in tmux
286
328
  --tmux-command <template> Remote tmux template; use {script} as the placeholder
329
+ --feed-post-id <uuid> Include a feed post in the context (desktop feed discuss)
330
+ --initial-question <text> First user question for feed discuss (paired with --feed-post-id)
287
331
 
288
332
  Notes:
289
333
  - ovld launch is the primary user-facing launcher.
@@ -370,7 +414,10 @@ async function runAgent(agent, mode = 'run', options = {}) {
370
414
  options.organizationId,
371
415
  organizationId
372
416
  );
373
- if (options.sshCommand?.trim()) {
417
+ const launchSessionId = crypto.randomUUID();
418
+ const isRemote = Boolean(options.sshCommand?.trim());
419
+
420
+ if (isRemote) {
374
421
  const remoteCommand = buildRemoteLaunchCommand(agent, {
375
422
  ...options,
376
423
  organizationId: resolvedLaunchOrganizationId
@@ -385,18 +432,40 @@ async function runAgent(agent, mode = 'run', options = {}) {
385
432
  }
386
433
  }
387
434
 
388
- const context = await fetchContext(
435
+ const { context, workingDirectory: apiWorkingDirectory, humanTicketId } = await fetchContext(
389
436
  platformUrl,
390
437
  bearerToken,
391
438
  localSecret,
392
439
  resolvedLaunchOrganizationId,
393
440
  ticketId,
394
- agent
441
+ agent,
442
+ {
443
+ launchMode: options.launchMode,
444
+ sessionId: launchSessionId,
445
+ workspace: isRemote ? 'ssh' : undefined,
446
+ feedPostId: options.feedPostId,
447
+ initialQuestion: options.initialQuestion
448
+ }
395
449
  );
396
450
 
451
+ if (humanTicketId) {
452
+ process.env.TICKET_ID = humanTicketId;
453
+ }
454
+
455
+ if (!options.workingDirectory && apiWorkingDirectory) {
456
+ try {
457
+ process.chdir(apiWorkingDirectory);
458
+ } catch {
459
+ // Best-effort; the agent will run in the current directory.
460
+ }
461
+ }
462
+
397
463
  const childEnv = withLaunchTempEnv({
398
464
  ...process.env,
399
465
  AGENT_IDENTIFIER: agentIdentifierMap[agent],
466
+ OVERLORD_LAUNCH_SESSION_ID: launchSessionId,
467
+ OVERLORD_MODEL_IDENTIFIER: options.model ?? '',
468
+ MODEL_IDENTIFIER: options.model ?? '',
400
469
  ...(resolvedLaunchOrganizationId
401
470
  ? { OVERLORD_ORGANIZATION_ID: String(resolvedLaunchOrganizationId) }
402
471
  : {})
@@ -568,19 +637,27 @@ async function runCustomAgent(args) {
568
637
  flags['organization-id'],
569
638
  organizationId
570
639
  );
640
+ const launchSessionId = crypto.randomUUID();
641
+ const feedPostId = typeof flags['feed-post-id'] === 'string' ? flags['feed-post-id'].trim() : '';
642
+ const initialQuestion = typeof flags['initial-question'] === 'string' ? flags['initial-question'].trim() : '';
571
643
  // Custom agents have no Overlord plugin/bundle; use the generic "claude" context.
572
- const context = await fetchContext(
644
+ const { context } = await fetchContext(
573
645
  platformUrl,
574
646
  bearerToken,
575
647
  localSecret,
576
648
  resolvedLaunchOrganizationId,
577
649
  resolvedTicketId,
578
- 'claude'
650
+ 'claude',
651
+ {
652
+ sessionId: launchSessionId,
653
+ ...(feedPostId ? { feedPostId, initialQuestion } : {})
654
+ }
579
655
  );
580
656
 
581
657
  const childEnv = withLaunchTempEnv({
582
658
  ...process.env,
583
659
  AGENT_IDENTIFIER: 'custom',
660
+ OVERLORD_LAUNCH_SESSION_ID: launchSessionId,
584
661
  ...(resolvedLaunchOrganizationId
585
662
  ? { OVERLORD_ORGANIZATION_ID: String(resolvedLaunchOrganizationId) }
586
663
  : {})
@@ -639,7 +716,9 @@ export async function runLauncherCommand(command, args) {
639
716
  : '',
640
717
  serverMultiplexer:
641
718
  flags['server-multiplexer'] === 'tmux' ? 'tmux' : 'none',
642
- tmuxCommand: typeof flags['tmux-command'] === 'string' ? flags['tmux-command'].trim() : ''
719
+ tmuxCommand: typeof flags['tmux-command'] === 'string' ? flags['tmux-command'].trim() : '',
720
+ feedPostId: typeof flags['feed-post-id'] === 'string' ? flags['feed-post-id'].trim() : '',
721
+ initialQuestion: typeof flags['initial-question'] === 'string' ? flags['initial-question'].trim() : ''
643
722
  };
644
723
 
645
724
  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.2605291450.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",