overlord-cli 5.8.0 → 5.11.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 CHANGED
@@ -91,8 +91,8 @@ Top-level commands (see `ovld help`):
91
91
  - `auth` - `login`, `status`, `repair` (shared Desktop/CLI credentials), or `logout`
92
92
  - `tickets` - `create` or `list` (optional `--status`)
93
93
  - `ticket` - `context <ticketId>` to print context for one ticket
94
- - `connect`, `restart` - launch or resume an agent session
95
- - `run`, `resume` - legacy aliases for `connect` and `restart`
94
+ - `launch`, `restart` - launch or resume an agent session
95
+ - `connect`, `run`, `resume` - legacy aliases for `launch` and `restart`
96
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
97
  - `update` - install the latest CLI release from npm
98
98
  - `doctor` - validate installed connectors and check for CLI updates
@@ -35,7 +35,8 @@ Usage:
35
35
  ${primaryCommand} tickets <subcommand> Create or list tickets
36
36
  ${primaryCommand} ticket <subcommand> Work with a single ticket
37
37
  ${primaryCommand} protocol <subcommand> Agent workflow commands
38
- ${primaryCommand} connect <agent> Launch an agent on a ticket
38
+ ${primaryCommand} launch <agent> Launch an agent on a ticket
39
+ ${primaryCommand} connect <agent> Launch an agent on a ticket (legacy alias)
39
40
  ${primaryCommand} restart <agent> Resume an agent session
40
41
  ${primaryCommand} setup [agent|all] Install Overlord agent connector (interactive if no args)
41
42
  ${primaryCommand} update Install the latest CLI version from npm
@@ -151,8 +152,9 @@ export async function runCli({ primaryCommand }) {
151
152
  return;
152
153
  }
153
154
 
154
- // Launcher commands (`run` / `resume` kept as legacy aliases)
155
+ // Launcher commands (`connect`, `run`, and `resume` kept as legacy aliases)
155
156
  if (
157
+ command === 'launch' ||
156
158
  command === 'connect' ||
157
159
  command === 'restart' ||
158
160
  command === 'run' ||
@@ -73,6 +73,124 @@ const agentIdentifierMap = {
73
73
 
74
74
  const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
75
75
 
76
+ function shellQuote(value) {
77
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
78
+ }
79
+
80
+ function toTomlString(value) {
81
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
82
+ }
83
+
84
+ function buildExtraArgs(agent, options = {}) {
85
+ const args = [];
86
+ const extraFlags = Array.isArray(options.flags)
87
+ ? options.flags.map(flag => String(flag).trim()).filter(Boolean)
88
+ : [];
89
+
90
+ if (options.model) {
91
+ args.push('--model', options.model);
92
+ }
93
+
94
+ if (options.thinking) {
95
+ if (agent === 'claude') {
96
+ args.push('--effort', options.thinking);
97
+ } else if (agent === 'codex') {
98
+ args.push('-c', `model_reasoning_effort=${toTomlString(options.thinking)}`);
99
+ } else if (agent === 'gemini') {
100
+ args.push('--thinking-level', options.thinking);
101
+ }
102
+ }
103
+
104
+ return [...args, ...extraFlags];
105
+ }
106
+
107
+ function buildRemoteLaunchCommand(agent, options) {
108
+ const envParts = [];
109
+ for (const [key, value] of Object.entries({
110
+ OVERLORD_URL: process.env.OVERLORD_URL,
111
+ OVERLORD_ACCESS_TOKEN: process.env.OVERLORD_ACCESS_TOKEN,
112
+ OVERLORD_ORGANIZATION_ID: process.env.OVERLORD_ORGANIZATION_ID,
113
+ OVERLORD_LOCAL_SECRET: process.env.OVERLORD_LOCAL_SECRET,
114
+ TICKET_ID: process.env.TICKET_ID,
115
+ AGENT_IDENTIFIER: agentIdentifierMap[agent],
116
+ OVERLORD_MODEL_IDENTIFIER: options.model ?? '',
117
+ MODEL_IDENTIFIER: options.model ?? ''
118
+ })) {
119
+ if (typeof value === 'string') {
120
+ envParts.push(`export ${key}=${shellQuote(value)}`);
121
+ }
122
+ }
123
+
124
+ const nestedParts = ['ovld', 'launch', agent, '--ticket-id', shellQuote(process.env.TICKET_ID ?? '')];
125
+ if (options.launchMode === 'ask') {
126
+ nestedParts.push('--launch-mode', 'ask');
127
+ }
128
+ if (options.model) {
129
+ nestedParts.push('--model', shellQuote(options.model));
130
+ }
131
+ if (options.thinking) {
132
+ nestedParts.push('--thinking', shellQuote(options.thinking));
133
+ }
134
+ for (const flag of options.flags ?? []) {
135
+ nestedParts.push('--flag', shellQuote(flag));
136
+ }
137
+
138
+ const remoteCwd = typeof options.remoteWorkingDirectory === 'string'
139
+ ? options.remoteWorkingDirectory.trim()
140
+ : '';
141
+ const remotePrelude = [
142
+ '[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"',
143
+ '[ -f "$HOME/.zshrc" ] && . "$HOME/.zshrc"',
144
+ '[ -f "$HOME/.profile" ] && . "$HOME/.profile"',
145
+ 'export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"',
146
+ '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"',
147
+ 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/.cargo/bin:$PATH"',
148
+ ...envParts,
149
+ remoteCwd ? `cd ${shellQuote(remoteCwd)}` : null,
150
+ nestedParts.join(' ')
151
+ ].filter(Boolean);
152
+
153
+ const innerCommand = remotePrelude.join('; ');
154
+ const remoteCommand =
155
+ options.serverMultiplexer === 'tmux'
156
+ ? buildTmuxWrappedCommand(innerCommand, options.tmuxCommand)
157
+ : innerCommand;
158
+
159
+ return `${options.sshCommand.trim()} ${shellQuote(remoteCommand)}`;
160
+ }
161
+
162
+ function buildTmuxWrappedCommand(innerCommand, tmuxCommand) {
163
+ const template = typeof tmuxCommand === 'string' && tmuxCommand.trim().includes('{script}')
164
+ ? tmuxCommand.trim()
165
+ : 'tmux new-session bash {script}';
166
+ const scriptCommand = `bash -lc ${shellQuote(innerCommand)}`;
167
+ return template.replaceAll('{script}', shellQuote(scriptCommand));
168
+ }
169
+
170
+ function printLauncherHelp() {
171
+ console.log(`Usage:
172
+ ovld launch <agent> --ticket-id <id> [options]
173
+ ovld connect <agent> --ticket-id <id> [options] # alias for ovld launch
174
+ ovld restart <agent> --ticket-id <id> [options]
175
+
176
+ Options:
177
+ --ticket-id <id> Ticket to launch or resume
178
+ --working-directory <path> Change to a local working directory before launch
179
+ --launch-mode <run|ask> Ask mode adjusts the fetched prompt context
180
+ --model <identifier> Preferred model identifier
181
+ --thinking <level> Agent reasoning/effort level
182
+ --flag <value> Extra agent flag (repeatable)
183
+ --ssh-command <command> Launch remotely over SSH by running ovld on the target host
184
+ --remote-working-directory <path> Change to this path on the remote host before launch
185
+ --server-multiplexer <none|tmux> Wrap remote launches in tmux
186
+ --tmux-command <template> Remote tmux template; use {script} as the placeholder
187
+
188
+ Notes:
189
+ - ovld launch is the primary user-facing launcher.
190
+ - ovld connect remains available as a compatibility alias.
191
+ - ovld restart keeps using the native resume flow when the target agent supports it.`);
192
+ }
193
+
76
194
  function parseLauncherArgs(args) {
77
195
  const positionals = [];
78
196
  const flags = {};
@@ -102,11 +220,26 @@ function parseLauncherArgs(args) {
102
220
  return { positionals, flags };
103
221
  }
104
222
 
105
- async function runAgent(agent, mode = 'run') {
223
+ function parseRepeatedFlags(args) {
224
+ const result = [];
225
+ for (let i = 0; i < args.length; i++) {
226
+ const arg = args[i];
227
+ if (arg === '--flag' && i + 1 < args.length) {
228
+ result.push(args[i + 1]);
229
+ i++;
230
+ continue;
231
+ }
232
+ if (arg.startsWith('--flag=')) {
233
+ result.push(arg.slice('--flag='.length));
234
+ }
235
+ }
236
+ return result;
237
+ }
238
+
239
+ async function runAgent(agent, mode = 'run', options = {}) {
106
240
  if (!agent || !supportedAgents.includes(agent)) {
107
- console.error(
108
- `Usage: ovld connect <agent> [--ticket-id <id>] | ovld restart <agent> [--ticket-id <id>] (agent must be one of: ${supportedAgents.join(', ')})`
109
- );
241
+ printLauncherHelp();
242
+ console.error(`\nAgent must be one of: ${supportedAgents.join(', ')}`);
110
243
  process.exit(1);
111
244
  }
112
245
 
@@ -116,7 +249,23 @@ async function runAgent(agent, mode = 'run') {
116
249
  process.exit(1);
117
250
  }
118
251
 
252
+ if (options.workingDirectory) {
253
+ process.chdir(options.workingDirectory);
254
+ }
255
+
119
256
  const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
257
+ if (options.sshCommand?.trim()) {
258
+ const remoteCommand = buildRemoteLaunchCommand(agent, options);
259
+ try {
260
+ execFileSync('sh', ['-lc', remoteCommand], { stdio: 'inherit', env: process.env });
261
+ return;
262
+ } catch (error) {
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ console.error(message);
265
+ process.exit(1);
266
+ }
267
+ }
268
+
120
269
  const context = await fetchContext(
121
270
  platformUrl,
122
271
  bearerToken,
@@ -127,6 +276,7 @@ async function runAgent(agent, mode = 'run') {
127
276
  );
128
277
 
129
278
  const childEnv = { ...process.env, AGENT_IDENTIFIER: agentIdentifierMap[agent] };
279
+ const extraArgs = buildExtraArgs(agent, options);
130
280
 
131
281
  try {
132
282
  if (agent === 'claude') {
@@ -136,12 +286,14 @@ async function runAgent(agent, mode = 'run') {
136
286
  const args = claudeSessionId
137
287
  ? ['--resume', claudeSessionId, context]
138
288
  : ['--continue', context];
289
+ args.unshift(...extraArgs);
139
290
  if (pluginDir) args.unshift('--plugin-dir', pluginDir);
140
291
  execFileSync('claude', args, { stdio: 'inherit', env: childEnv });
141
292
  } else {
142
293
  const args = [
143
294
  '--append-system-prompt',
144
295
  context,
296
+ ...extraArgs,
145
297
  'Begin working on this ticket. Start by calling the attach endpoint, then proceed with the objective described in your system prompt.'
146
298
  ];
147
299
  if (pluginDir) args.unshift('--plugin-dir', pluginDir);
@@ -157,21 +309,26 @@ async function runAgent(agent, mode = 'run') {
157
309
  const args = codexSessionId
158
310
  ? ['resume', codexSessionId, context]
159
311
  : ['resume', '--last', context];
312
+ args.splice(1, 0, ...extraArgs);
160
313
  execFileSync('codex', args, { stdio: 'inherit', env: childEnv });
161
314
  } else {
162
- execFileSync('codex', [context], { stdio: 'inherit', env: childEnv });
315
+ execFileSync('codex', [...extraArgs, context], { stdio: 'inherit', env: childEnv });
163
316
  }
164
317
  } else if (agent === 'cursor') {
165
- execFileSync('agent', [context], { stdio: 'inherit', env: childEnv });
318
+ execFileSync('agent', [...extraArgs, context], { stdio: 'inherit', env: childEnv });
166
319
  } else if (agent === 'opencode') {
167
320
  if (mode === 'resume') {
168
321
  const openCodeSessionId = process.env.OPENCODE_SESSION_ID?.trim();
169
322
  const args = openCodeSessionId
170
323
  ? ['--continue', '--session', openCodeSessionId, '--prompt', context]
171
324
  : ['--continue', '--prompt', context];
325
+ args.unshift(...extraArgs);
172
326
  execFileSync('opencode', args, { stdio: 'inherit', env: childEnv });
173
327
  } else {
174
- execFileSync('opencode', ['--prompt', context], { stdio: 'inherit', env: childEnv });
328
+ execFileSync('opencode', [...extraArgs, '--prompt', context], {
329
+ stdio: 'inherit',
330
+ env: childEnv
331
+ });
175
332
  }
176
333
  } else if (agent === 'gemini') {
177
334
  // Write context to a temp file. Passing inline content as a positional arg
@@ -188,13 +345,13 @@ async function runAgent(agent, mode = 'run') {
188
345
  const resumeTarget = geminiSessionId ?? 'latest';
189
346
  execFileSync(
190
347
  'gemini',
191
- ['--resume', resumeTarget, '--include-directories', os.tmpdir(), `@${contextFile}`],
348
+ [...extraArgs, '--resume', resumeTarget, '--include-directories', os.tmpdir(), `@${contextFile}`],
192
349
  { stdio: 'inherit', env: childEnv }
193
350
  );
194
351
  } else {
195
352
  execFileSync(
196
353
  'gemini',
197
- ['--include-directories', os.tmpdir(), `@${contextFile}`],
354
+ [...extraArgs, '--include-directories', os.tmpdir(), `@${contextFile}`],
198
355
  { stdio: 'inherit', env: childEnv }
199
356
  );
200
357
  }
@@ -203,12 +360,12 @@ async function runAgent(agent, mode = 'run') {
203
360
  const isResume = mode === 'resume';
204
361
  const noSessionHint =
205
362
  agent === 'claude'
206
- ? `No prior Claude session was found. Start one with \`ovld connect claude --ticket-id <ticket-id>\` first.`
363
+ ? `No prior Claude session was found. Start one with \`ovld launch claude --ticket-id <ticket-id>\` first.`
207
364
  : agent === 'codex'
208
- ? `No prior Codex session was found. Start one with \`ovld connect codex --ticket-id <ticket-id>\` first.`
365
+ ? `No prior Codex session was found. Start one with \`ovld launch codex --ticket-id <ticket-id>\` first.`
209
366
  : agent === 'opencode'
210
- ? `No prior OpenCode session was found. Start one with \`ovld connect opencode --ticket-id <ticket-id>\` first.`
211
- : '';
367
+ ? `No prior OpenCode session was found. Start one with \`ovld launch opencode --ticket-id <ticket-id>\` first.`
368
+ : '';
212
369
  const message = error instanceof Error ? error.message : String(error);
213
370
 
214
371
  if (isResume && noSessionHint) {
@@ -221,26 +378,54 @@ async function runAgent(agent, mode = 'run') {
221
378
  }
222
379
 
223
380
  export async function runLauncherCommand(command, args) {
224
- const normalizedCommand = command === 'connect' ? 'run' : command === 'restart' ? 'resume' : command;
381
+ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
382
+ printLauncherHelp();
383
+ return;
384
+ }
385
+
386
+ const normalizedCommand =
387
+ command === 'launch' || command === 'connect'
388
+ ? 'run'
389
+ : command === 'restart'
390
+ ? 'resume'
391
+ : command;
225
392
  const { positionals, flags } = parseLauncherArgs(args);
393
+ const repeatedFlags = parseRepeatedFlags(args);
226
394
  const ticketId = typeof flags['ticket-id'] === 'string' ? flags['ticket-id'].trim() : '';
227
395
 
228
396
  if (ticketId) {
229
397
  process.env.TICKET_ID = ticketId;
230
398
  }
231
399
 
400
+ const launchOptions = {
401
+ workingDirectory:
402
+ typeof flags['working-directory'] === 'string' ? flags['working-directory'].trim() : '',
403
+ launchMode: flags['launch-mode'] === 'ask' ? 'ask' : 'run',
404
+ model: typeof flags.model === 'string' ? flags.model.trim() : '',
405
+ thinking: typeof flags.thinking === 'string' ? flags.thinking.trim() : '',
406
+ flags: repeatedFlags,
407
+ sshCommand: typeof flags['ssh-command'] === 'string' ? flags['ssh-command'].trim() : '',
408
+ remoteWorkingDirectory:
409
+ typeof flags['remote-working-directory'] === 'string'
410
+ ? flags['remote-working-directory'].trim()
411
+ : '',
412
+ serverMultiplexer:
413
+ flags['server-multiplexer'] === 'tmux' ? 'tmux' : 'none',
414
+ tmuxCommand: typeof flags['tmux-command'] === 'string' ? flags['tmux-command'].trim() : ''
415
+ };
416
+
232
417
  if (normalizedCommand === 'run') {
233
418
  // If no ticket-id flag and no TICKET_ID env var, present interactive ticket search
234
419
  if (!ticketId && !process.env.TICKET_ID) {
235
420
  await runAttachCommand([undefined, positionals[0]]);
236
421
  return;
237
422
  }
238
- await runAgent(positionals[0]);
423
+ await runAgent(positionals[0], 'run', launchOptions);
239
424
  return;
240
425
  }
241
426
 
242
427
  if (normalizedCommand === 'resume') {
243
- await runAgent(positionals[0], 'resume');
428
+ await runAgent(positionals[0], 'resume', launchOptions);
244
429
  return;
245
430
  }
246
431
 
@@ -822,7 +822,7 @@ function installClaude() {
822
822
  } else {
823
823
  console.log(' ✓ No v3.25 Claude connector files needed migration.');
824
824
  }
825
- console.log(' ✓ `ovld connect claude` now loads this plugin with `claude --plugin-dir`.');
825
+ console.log(' ✓ `ovld launch claude` now loads this plugin with `claude --plugin-dir`.');
826
826
 
827
827
  const manifest = readManifest();
828
828
  manifest.claude = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "5.8.0",
3
+ "version": "5.11.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {