overlord-cli 0.2605101225.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.
Files changed (42) hide show
  1. package/README.md +128 -0
  2. package/bin/_cli/attach.mjs +357 -0
  3. package/bin/_cli/auth.mjs +696 -0
  4. package/bin/_cli/cli-update.mjs +119 -0
  5. package/bin/_cli/credentials.mjs +723 -0
  6. package/bin/_cli/index.mjs +170 -0
  7. package/bin/_cli/launcher.mjs +474 -0
  8. package/bin/_cli/new-ticket.mjs +306 -0
  9. package/bin/_cli/postinstall.mjs +43 -0
  10. package/bin/_cli/protocol.mjs +1799 -0
  11. package/bin/_cli/setup.mjs +1634 -0
  12. package/bin/_cli/ticket.mjs +55 -0
  13. package/bin/_cli/tickets.mjs +121 -0
  14. package/bin/_cli/version.mjs +7 -0
  15. package/bin/ovld.mjs +13 -0
  16. package/package.json +36 -0
  17. package/plugins/claude/.claude-plugin/plugin.json +34 -0
  18. package/plugins/claude/README.md +42 -0
  19. package/plugins/claude/commands/connect.md +19 -0
  20. package/plugins/claude/commands/create.md +17 -0
  21. package/plugins/claude/commands/load.md +19 -0
  22. package/plugins/claude/commands/spawn.md +17 -0
  23. package/plugins/claude/hooks/hooks.json +15 -0
  24. package/plugins/claude/scripts/permission-hook.sh +21 -0
  25. package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
  26. package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
  27. package/plugins/cursor/commands/connect.md +3 -0
  28. package/plugins/cursor/commands/create.md +11 -0
  29. package/plugins/cursor/commands/load.md +3 -0
  30. package/plugins/cursor/commands/spawn.md +3 -0
  31. package/plugins/cursor/mcp.json +8 -0
  32. package/plugins/cursor/rules/overlord-local.mdc +10 -0
  33. package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
  34. package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
  35. package/plugins/overlord/.codex-plugin/plugin.json +35 -0
  36. package/plugins/overlord/.mcp.json +11 -0
  37. package/plugins/overlord/README.md +42 -0
  38. package/plugins/overlord/assets/icon.png +0 -0
  39. package/plugins/overlord/assets/logo.png +0 -0
  40. package/plugins/overlord/assets/screenshot.svg +28 -0
  41. package/plugins/overlord/scripts/overlord-mcp.mjs +688 -0
  42. package/plugins/overlord/skills/overlord-ticket/SKILL.md +146 -0
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runAttachCommand } from './attach.mjs';
4
+ import { runAuthCommand } from './auth.mjs';
5
+ import { checkForCliUpdate, printCliUpdateNotice, runCliUpdateCommand } from './cli-update.mjs';
6
+ import { runLauncherCommand } from './launcher.mjs';
7
+ import { runProtocolCommand } from './protocol.mjs';
8
+ import { runDoctorCommand, runSetupCommand } from './setup.mjs';
9
+ import { runTicketCommand } from './ticket.mjs';
10
+ import { runTicketsCommand } from './tickets.mjs';
11
+ import { runVersionCommand } from './version.mjs';
12
+
13
+ const MIN_NODE_MAJOR = 20;
14
+
15
+ function assertSupportedNodeVersion() {
16
+ const major = Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
17
+ if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
18
+ throw new Error(
19
+ `Overlord CLI requires Node.js ${MIN_NODE_MAJOR} or newer. Found ${process.version}.\n` +
20
+ `Update Node before running \`ovld\`. If you installed the desktop wrapper, you can also point it at a newer runtime with \`OVLD_NODE_BIN=/path/to/node\`.`
21
+ );
22
+ }
23
+ }
24
+
25
+ function printHelp(primaryCommand) {
26
+ console.log(`Overlord CLI
27
+
28
+ Primary command: ${primaryCommand}
29
+
30
+ Usage:
31
+ ${primaryCommand} attach [ticketId] [agent] Search tickets and launch an agent (interactive)
32
+ ${primaryCommand} create "<objective>" Create a ticket with numbered project selection; supports --agent/--model/--delegate
33
+ ${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
34
+ ${primaryCommand} auth <subcommand> Login, logout, repair, or check auth status
35
+ ${primaryCommand} tickets <subcommand> Create or list tickets
36
+ ${primaryCommand} ticket <subcommand> Work with a single ticket
37
+ ${primaryCommand} protocol <subcommand> Agent workflow commands
38
+ ${primaryCommand} launch <agent> Launch an agent on a ticket
39
+ ${primaryCommand} connect <agent> Launch an agent on a ticket (legacy alias)
40
+ ${primaryCommand} restart <agent> Resume an agent session
41
+ ${primaryCommand} setup [agent|all] Install Overlord agent connector (interactive if no args)
42
+ ${primaryCommand} update Install the latest CLI version from npm
43
+ ${primaryCommand} doctor Validate installed agent connectors and check for CLI updates
44
+ ${primaryCommand} version Show the installed CLI version
45
+ ${primaryCommand} help Show this help message
46
+
47
+ Agents:
48
+ Use ${primaryCommand} protocol help for ticket lifecycle commands.
49
+ Key protocol commands: auth-status, discover-project, create, prompt, attach, connect, load-context.
50
+
51
+ Auth:
52
+ ${primaryCommand} auth login Authorize CLI via browser
53
+ ${primaryCommand} auth status Show login status
54
+ ${primaryCommand} auth repair Repair shared Desktop/CLI credentials
55
+ ${primaryCommand} auth logout Remove stored credentials
56
+
57
+ Tickets:
58
+ ${primaryCommand} create "..." [--agent <agent>] [--model <identifier>] [--delegate <agent>] [options]
59
+ ${primaryCommand} prompt "..." [options]
60
+ ${primaryCommand} tickets create "..." [options]
61
+ ${primaryCommand} tickets list [--status <status>]
62
+
63
+ Ticket:
64
+ ${primaryCommand} ticket context <ticketId>
65
+
66
+ Run a subcommand with --help for more detail.
67
+ `);
68
+ }
69
+
70
+ export async function runCli({ primaryCommand }) {
71
+ assertSupportedNodeVersion();
72
+ const [command, ...rest] = process.argv.slice(2);
73
+ const shouldCheckForUpdate =
74
+ Boolean(process.stdout.isTTY || process.stderr.isTTY) &&
75
+ command !== 'doctor' &&
76
+ command !== 'update';
77
+ const latestCliVersion = shouldCheckForUpdate ? await checkForCliUpdate() : null;
78
+
79
+ if (latestCliVersion && command !== 'doctor' && command !== 'update') {
80
+ printCliUpdateNotice(latestCliVersion);
81
+ }
82
+
83
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
84
+ printHelp(primaryCommand);
85
+ return;
86
+ }
87
+
88
+ // Attach command (interactive ticket search + agent launcher)
89
+ if (command === 'attach') {
90
+ await runAttachCommand(rest);
91
+ return;
92
+ }
93
+
94
+ if (command === 'create') {
95
+ const { runCreateCommand } = await import('./new-ticket.mjs');
96
+ await runCreateCommand(rest);
97
+ return;
98
+ }
99
+
100
+ if (command === 'prompt') {
101
+ const { runPromptCommand } = await import('./new-ticket.mjs');
102
+ await runPromptCommand(rest);
103
+ return;
104
+ }
105
+
106
+ // Auth group
107
+ if (command === 'auth') {
108
+ await runAuthCommand(rest[0], rest.slice(1));
109
+ return;
110
+ }
111
+
112
+ // Tickets (plural) group
113
+ if (command === 'tickets') {
114
+ await runTicketsCommand(rest[0], rest.slice(1));
115
+ return;
116
+ }
117
+
118
+ // Ticket (singular) group
119
+ if (command === 'ticket') {
120
+ await runTicketCommand(rest[0], rest.slice(1));
121
+ return;
122
+ }
123
+
124
+ // Protocol group
125
+ if (command === 'protocol') {
126
+ await runProtocolCommand(rest[0], rest.slice(1));
127
+ return;
128
+ }
129
+
130
+ if (command === 'setup') {
131
+ await runSetupCommand(rest);
132
+ return;
133
+ }
134
+
135
+ if (command === 'update') {
136
+ if (rest[0] === '--help' || rest[0] === '-h' || rest[0] === 'help') {
137
+ console.log(`Usage:
138
+ ${primaryCommand} update Install the latest CLI version from npm`);
139
+ return;
140
+ }
141
+ await runCliUpdateCommand();
142
+ return;
143
+ }
144
+
145
+ if (command === 'doctor') {
146
+ await runDoctorCommand({ latestCliVersion });
147
+ return;
148
+ }
149
+
150
+ if (command === 'version') {
151
+ runVersionCommand();
152
+ return;
153
+ }
154
+
155
+ // Launcher commands (`connect`, `run`, and `resume` kept as legacy aliases)
156
+ if (
157
+ command === 'launch' ||
158
+ command === 'connect' ||
159
+ command === 'restart' ||
160
+ command === 'run' ||
161
+ command === 'resume'
162
+ ) {
163
+ await runLauncherCommand(command, rest);
164
+ return;
165
+ }
166
+
167
+ console.error(`Unknown command: ${command}\n`);
168
+ printHelp(primaryCommand);
169
+ process.exit(1);
170
+ }
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Agent launcher commands (run / resume / context).
5
+ * Extracted from the original _agent-launcher-cli.mjs.
6
+ */
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
14
+ import { runAttachCommand } from './attach.mjs';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
18
+ const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
19
+
20
+ function claudeSourcePluginDir() {
21
+ if (fs.existsSync(PACKAGE_CLAUDE_PLUGIN_DIR)) return PACKAGE_CLAUDE_PLUGIN_DIR;
22
+ if (fs.existsSync(REPO_CLAUDE_PLUGIN_DIR)) return REPO_CLAUDE_PLUGIN_DIR;
23
+ return null;
24
+ }
25
+
26
+ function getInstructionMode(agent) {
27
+ if (agent === 'claude') {
28
+ return claudeSourcePluginDir() ? 'bundle' : 'legacy';
29
+ }
30
+
31
+ if (agent === 'codex') {
32
+ const pluginManifest = path.join(
33
+ os.homedir(),
34
+ '.codex',
35
+ 'plugins',
36
+ 'overlord',
37
+ '.codex-plugin',
38
+ 'plugin.json'
39
+ );
40
+ return fs.existsSync(pluginManifest) ? 'bundle' : 'legacy';
41
+ }
42
+
43
+ return 'legacy';
44
+ }
45
+
46
+ async function fetchContext(platformUrl, bearerToken, localSecret, organizationId, ticketId, agent) {
47
+ const params = new URLSearchParams({
48
+ context: 'cli',
49
+ agent,
50
+ instructionMode: getInstructionMode(agent)
51
+ });
52
+ const url = `${platformUrl}/api/protocol/context/${ticketId}?${params.toString()}`;
53
+ const response = await fetch(url, {
54
+ headers: buildAuthHeaders(bearerToken, localSecret, organizationId)
55
+ });
56
+
57
+ if (!response.ok) {
58
+ throw new Error(
59
+ `Failed to fetch ticket context (${response.status}): ${await response.text()}`
60
+ );
61
+ }
62
+
63
+ return response.text();
64
+ }
65
+
66
+ const agentIdentifierMap = {
67
+ claude: 'claude-code',
68
+ codex: 'codex',
69
+ cursor: 'cursor',
70
+ gemini: 'gemini',
71
+ opencode: 'opencode'
72
+ };
73
+
74
+ const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
75
+ const TICKET_ID_REGEX = /^(\d+):\d+$/;
76
+
77
+ function shellQuote(value) {
78
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
79
+ }
80
+
81
+ function toTomlString(value) {
82
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
83
+ }
84
+
85
+ function buildExtraArgs(agent, options = {}) {
86
+ const args = [];
87
+ const extraFlags = Array.isArray(options.flags)
88
+ ? options.flags.map(flag => String(flag).trim()).filter(Boolean)
89
+ : [];
90
+
91
+ if (options.model) {
92
+ args.push('--model', options.model);
93
+ }
94
+
95
+ if (options.thinking) {
96
+ if (agent === 'claude') {
97
+ args.push('--effort', options.thinking);
98
+ } else if (agent === 'codex') {
99
+ args.push('-c', `model_reasoning_effort=${toTomlString(options.thinking)}`);
100
+ } else if (agent === 'gemini') {
101
+ args.push('--thinking-level', options.thinking);
102
+ }
103
+ }
104
+
105
+ return [...args, ...extraFlags];
106
+ }
107
+
108
+ function parseOrganizationId(value) {
109
+ if (typeof value !== 'string' && typeof value !== 'number') return null;
110
+ const parsed = Number.parseInt(String(value), 10);
111
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
112
+ }
113
+
114
+ function organizationIdFromTicketId(ticketId) {
115
+ const match = String(ticketId ?? '').trim().match(TICKET_ID_REGEX);
116
+ return match ? parseOrganizationId(match[1]) : null;
117
+ }
118
+
119
+ function resolveLaunchOrganizationId(ticketId, optionsOrganizationId, authOrganizationId) {
120
+ return (
121
+ parseOrganizationId(optionsOrganizationId) ??
122
+ organizationIdFromTicketId(ticketId) ??
123
+ parseOrganizationId(authOrganizationId)
124
+ );
125
+ }
126
+
127
+ function buildRemoteLaunchCommand(agent, options) {
128
+ const organizationId = parseOrganizationId(options.organizationId);
129
+ const envParts = [];
130
+ for (const [key, value] of Object.entries({
131
+ OVERLORD_URL: process.env.OVERLORD_URL,
132
+ OVERLORD_ACCESS_TOKEN: process.env.OVERLORD_ACCESS_TOKEN,
133
+ OVERLORD_ORGANIZATION_ID: organizationId ?? process.env.OVERLORD_ORGANIZATION_ID,
134
+ OVERLORD_LOCAL_SECRET: process.env.OVERLORD_LOCAL_SECRET,
135
+ TICKET_ID: process.env.TICKET_ID,
136
+ AGENT_IDENTIFIER: agentIdentifierMap[agent],
137
+ OVERLORD_MODEL_IDENTIFIER: options.model ?? '',
138
+ MODEL_IDENTIFIER: options.model ?? ''
139
+ })) {
140
+ if (typeof value === 'string') {
141
+ envParts.push(`export ${key}=${shellQuote(value)}`);
142
+ }
143
+ }
144
+
145
+ const nestedParts = ['ovld', 'launch', agent, '--ticket-id', shellQuote(process.env.TICKET_ID ?? '')];
146
+ if (organizationId) {
147
+ nestedParts.push('--organization-id', String(organizationId));
148
+ }
149
+ if (options.launchMode === 'ask') {
150
+ nestedParts.push('--launch-mode', 'ask');
151
+ }
152
+ if (options.model) {
153
+ nestedParts.push('--model', shellQuote(options.model));
154
+ }
155
+ if (options.thinking) {
156
+ nestedParts.push('--thinking', shellQuote(options.thinking));
157
+ }
158
+ for (const flag of options.flags ?? []) {
159
+ nestedParts.push('--flag', shellQuote(flag));
160
+ }
161
+
162
+ const remoteCwd = typeof options.remoteWorkingDirectory === 'string'
163
+ ? options.remoteWorkingDirectory.trim()
164
+ : '';
165
+ const remotePrelude = [
166
+ '[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"',
167
+ '[ -f "$HOME/.zshrc" ] && . "$HOME/.zshrc"',
168
+ '[ -f "$HOME/.profile" ] && . "$HOME/.profile"',
169
+ 'export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"',
170
+ '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"',
171
+ 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$HOME/.cargo/bin:$PATH"',
172
+ ...envParts,
173
+ remoteCwd ? `cd ${shellQuote(remoteCwd)}` : null,
174
+ nestedParts.join(' ')
175
+ ].filter(Boolean);
176
+
177
+ const innerCommand = remotePrelude.join('; ');
178
+ const remoteCommand =
179
+ options.serverMultiplexer === 'tmux'
180
+ ? buildTmuxWrappedCommand(innerCommand, options.tmuxCommand)
181
+ : innerCommand;
182
+
183
+ return `${options.sshCommand.trim()} ${shellQuote(remoteCommand)}`;
184
+ }
185
+
186
+ function buildTmuxWrappedCommand(innerCommand, tmuxCommand) {
187
+ const template = typeof tmuxCommand === 'string' && tmuxCommand.trim().includes('{script}')
188
+ ? tmuxCommand.trim()
189
+ : 'tmux new-session bash {script}';
190
+ const scriptCommand = `bash -lc ${shellQuote(innerCommand)}`;
191
+ return template.replaceAll('{script}', shellQuote(scriptCommand));
192
+ }
193
+
194
+ function printLauncherHelp() {
195
+ console.log(`Usage:
196
+ ovld launch <agent> --ticket-id <ticket_id> [options]
197
+ ovld connect <agent> --ticket-id <ticket_id> [options] # alias for ovld launch
198
+ ovld restart <agent> --ticket-id <ticket_id> [options]
199
+
200
+ Options:
201
+ --ticket-id <ticket_id> Ticket to launch or resume (e.g. 1:899). Also accepts UUID.
202
+ --organization-id <id> Organization scope for UUID ticket ids; inferred from ticket_id when possible.
203
+ --working-directory <path> Change to a local working directory before launch
204
+ --launch-mode <run|ask> Ask mode adjusts the fetched prompt context
205
+ --model <identifier> Preferred model identifier
206
+ --thinking <level> Agent reasoning/effort level
207
+ --flag <value> Extra agent flag (repeatable)
208
+ --ssh-command <command> Launch remotely over SSH by running ovld on the target host
209
+ --remote-working-directory <path> Change to this path on the remote host before launch
210
+ --server-multiplexer <none|tmux> Wrap remote launches in tmux
211
+ --tmux-command <template> Remote tmux template; use {script} as the placeholder
212
+
213
+ Notes:
214
+ - ovld launch is the primary user-facing launcher.
215
+ - ovld connect remains available as a compatibility alias.
216
+ - ovld restart keeps using the native resume flow when the target agent supports it.`);
217
+ }
218
+
219
+ function parseLauncherArgs(args) {
220
+ const positionals = [];
221
+ const flags = {};
222
+
223
+ for (let i = 0; i < args.length; i++) {
224
+ const arg = args[i];
225
+ if (!arg.startsWith('--')) {
226
+ positionals.push(arg);
227
+ continue;
228
+ }
229
+
230
+ const eqIdx = arg.indexOf('=');
231
+ if (eqIdx !== -1) {
232
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
233
+ continue;
234
+ }
235
+
236
+ const key = arg.slice(2);
237
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
238
+ flags[key] = args[i + 1];
239
+ i++;
240
+ } else {
241
+ flags[key] = true;
242
+ }
243
+ }
244
+
245
+ return { positionals, flags };
246
+ }
247
+
248
+ function parseRepeatedFlags(args) {
249
+ const result = [];
250
+ for (let i = 0; i < args.length; i++) {
251
+ const arg = args[i];
252
+ if (arg === '--flag' && i + 1 < args.length) {
253
+ result.push(args[i + 1]);
254
+ i++;
255
+ continue;
256
+ }
257
+ if (arg.startsWith('--flag=')) {
258
+ result.push(arg.slice('--flag='.length));
259
+ }
260
+ }
261
+ return result;
262
+ }
263
+
264
+ async function runAgent(agent, mode = 'run', options = {}) {
265
+ if (!agent || !supportedAgents.includes(agent)) {
266
+ printLauncherHelp();
267
+ console.error(`\nAgent must be one of: ${supportedAgents.join(', ')}`);
268
+ process.exit(1);
269
+ }
270
+
271
+ const ticketId = process.env.TICKET_ID;
272
+ if (!ticketId) {
273
+ console.error('Missing required environment variable: TICKET_ID');
274
+ process.exit(1);
275
+ }
276
+
277
+ if (options.workingDirectory) {
278
+ process.chdir(options.workingDirectory);
279
+ }
280
+
281
+ const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
282
+ const launchOrganizationId = resolveLaunchOrganizationId(
283
+ ticketId,
284
+ options.organizationId,
285
+ organizationId
286
+ );
287
+ if (options.sshCommand?.trim()) {
288
+ const remoteCommand = buildRemoteLaunchCommand(agent, {
289
+ ...options,
290
+ organizationId: launchOrganizationId
291
+ });
292
+ try {
293
+ execFileSync('sh', ['-lc', remoteCommand], { stdio: 'inherit', env: process.env });
294
+ return;
295
+ } catch (error) {
296
+ const message = error instanceof Error ? error.message : String(error);
297
+ console.error(message);
298
+ process.exit(1);
299
+ }
300
+ }
301
+
302
+ const context = await fetchContext(
303
+ platformUrl,
304
+ bearerToken,
305
+ localSecret,
306
+ launchOrganizationId,
307
+ ticketId,
308
+ agent
309
+ );
310
+
311
+ const childEnv = {
312
+ ...process.env,
313
+ AGENT_IDENTIFIER: agentIdentifierMap[agent],
314
+ ...(launchOrganizationId ? { OVERLORD_ORGANIZATION_ID: String(launchOrganizationId) } : {})
315
+ };
316
+ const extraArgs = buildExtraArgs(agent, options);
317
+
318
+ try {
319
+ if (agent === 'claude') {
320
+ const pluginDir = claudeSourcePluginDir();
321
+ if (mode === 'resume') {
322
+ const claudeSessionId = process.env.CLAUDE_SESSION_ID?.trim();
323
+ const args = claudeSessionId
324
+ ? ['--resume', claudeSessionId, context]
325
+ : ['--continue', context];
326
+ args.unshift(...extraArgs);
327
+ if (pluginDir) args.unshift('--plugin-dir', pluginDir);
328
+ execFileSync('claude', args, { stdio: 'inherit', env: childEnv });
329
+ } else {
330
+ const args = [
331
+ '--append-system-prompt',
332
+ context,
333
+ ...extraArgs,
334
+ 'Begin working on this ticket. Start by calling the attach endpoint, then proceed with the objective described in your system prompt.'
335
+ ];
336
+ if (pluginDir) args.unshift('--plugin-dir', pluginDir);
337
+ execFileSync(
338
+ 'claude',
339
+ args,
340
+ { stdio: 'inherit', env: childEnv }
341
+ );
342
+ }
343
+ } else if (agent === 'codex') {
344
+ if (mode === 'resume') {
345
+ const codexSessionId = process.env.CODEX_SESSION_ID?.trim();
346
+ const args = codexSessionId
347
+ ? ['resume', codexSessionId, context]
348
+ : ['resume', '--last', context];
349
+ args.splice(1, 0, ...extraArgs);
350
+ execFileSync('codex', args, { stdio: 'inherit', env: childEnv });
351
+ } else {
352
+ execFileSync('codex', [...extraArgs, context], { stdio: 'inherit', env: childEnv });
353
+ }
354
+ } else if (agent === 'cursor') {
355
+ execFileSync('agent', [...extraArgs, context], { stdio: 'inherit', env: childEnv });
356
+ } else if (agent === 'opencode') {
357
+ if (mode === 'resume') {
358
+ const openCodeSessionId = process.env.OPENCODE_SESSION_ID?.trim();
359
+ const args = openCodeSessionId
360
+ ? ['--continue', '--session', openCodeSessionId, '--prompt', context]
361
+ : ['--continue', '--prompt', context];
362
+ args.unshift(...extraArgs);
363
+ execFileSync('opencode', args, { stdio: 'inherit', env: childEnv });
364
+ } else {
365
+ execFileSync('opencode', [...extraArgs, '--prompt', context], {
366
+ stdio: 'inherit',
367
+ env: childEnv
368
+ });
369
+ }
370
+ } else if (agent === 'gemini') {
371
+ // Write context to a temp file. Passing inline content as a positional arg
372
+ // causes Gemini's @-reference parser to lstat(cwd + content) when it encounters
373
+ // @ symbols in the markdown (e.g. "@@ -10,6 +10,14 @@" in JSON hunk examples),
374
+ // producing an ENAMETOOLONG crash. Using @file keeps the path short.
375
+ const tag = `overlord-${ticketId.slice(-8)}-${Date.now()}`;
376
+ const contextFile = path.join(os.tmpdir(), `${tag}-ctx.md`);
377
+ fs.writeFileSync(contextFile, context, 'utf-8');
378
+ setTimeout(() => { try { fs.unlinkSync(contextFile); } catch { /* already gone */ } }, 30 * 60_000).unref();
379
+
380
+ if (mode === 'resume') {
381
+ const geminiSessionId = process.env.GEMINI_SESSION_ID?.trim();
382
+ const resumeTarget = geminiSessionId ?? 'latest';
383
+ execFileSync(
384
+ 'gemini',
385
+ [...extraArgs, '--resume', resumeTarget, '--include-directories', os.tmpdir(), `@${contextFile}`],
386
+ { stdio: 'inherit', env: childEnv }
387
+ );
388
+ } else {
389
+ execFileSync(
390
+ 'gemini',
391
+ [...extraArgs, '--include-directories', os.tmpdir(), `@${contextFile}`],
392
+ { stdio: 'inherit', env: childEnv }
393
+ );
394
+ }
395
+ }
396
+ } catch (error) {
397
+ const isResume = mode === 'resume';
398
+ const noSessionHint =
399
+ agent === 'claude'
400
+ ? `No prior Claude session was found. Start one with \`ovld launch claude --ticket-id <ticket_id>\` first.`
401
+ : agent === 'codex'
402
+ ? `No prior Codex session was found. Start one with \`ovld launch codex --ticket-id <ticket_id>\` first.`
403
+ : agent === 'opencode'
404
+ ? `No prior OpenCode session was found. Start one with \`ovld launch opencode --ticket-id <ticket_id>\` first.`
405
+ : '';
406
+ const message = error instanceof Error ? error.message : String(error);
407
+
408
+ if (isResume && noSessionHint) {
409
+ console.error(`${message}\n${noSessionHint}`);
410
+ } else {
411
+ console.error(message);
412
+ }
413
+ process.exit(1);
414
+ }
415
+ }
416
+
417
+ export async function runLauncherCommand(command, args) {
418
+ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
419
+ printLauncherHelp();
420
+ return;
421
+ }
422
+
423
+ const normalizedCommand =
424
+ command === 'launch' || command === 'connect'
425
+ ? 'run'
426
+ : command === 'restart'
427
+ ? 'resume'
428
+ : command;
429
+ const { positionals, flags } = parseLauncherArgs(args);
430
+ const repeatedFlags = parseRepeatedFlags(args);
431
+ const ticketId = typeof flags['ticket-id'] === 'string' ? flags['ticket-id'].trim() : '';
432
+
433
+ if (ticketId) {
434
+ process.env.TICKET_ID = ticketId;
435
+ }
436
+
437
+ const launchOptions = {
438
+ workingDirectory:
439
+ typeof flags['working-directory'] === 'string' ? flags['working-directory'].trim() : '',
440
+ launchMode: flags['launch-mode'] === 'ask' ? 'ask' : 'run',
441
+ model: typeof flags.model === 'string' ? flags.model.trim() : '',
442
+ thinking: typeof flags.thinking === 'string' ? flags.thinking.trim() : '',
443
+ flags: repeatedFlags,
444
+ organizationId:
445
+ typeof flags['organization-id'] === 'string' ? flags['organization-id'].trim() : '',
446
+ sshCommand: typeof flags['ssh-command'] === 'string' ? flags['ssh-command'].trim() : '',
447
+ remoteWorkingDirectory:
448
+ typeof flags['remote-working-directory'] === 'string'
449
+ ? flags['remote-working-directory'].trim()
450
+ : '',
451
+ serverMultiplexer:
452
+ flags['server-multiplexer'] === 'tmux' ? 'tmux' : 'none',
453
+ tmuxCommand: typeof flags['tmux-command'] === 'string' ? flags['tmux-command'].trim() : ''
454
+ };
455
+
456
+ if (normalizedCommand === 'run') {
457
+ // If no ticket-id flag and no TICKET_ID env var, present interactive ticket search
458
+ if (!ticketId && !process.env.TICKET_ID) {
459
+ await runAttachCommand([undefined, positionals[0]]);
460
+ return;
461
+ }
462
+ await runAgent(positionals[0], 'run', launchOptions);
463
+ return;
464
+ }
465
+
466
+ if (normalizedCommand === 'resume') {
467
+ await runAgent(positionals[0], 'resume', launchOptions);
468
+ return;
469
+ }
470
+
471
+ // Should not happen if called correctly
472
+ console.error(`Unknown launcher command: ${command}`);
473
+ process.exit(1);
474
+ }