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.
- package/README.md +128 -0
- package/bin/_cli/attach.mjs +357 -0
- package/bin/_cli/auth.mjs +696 -0
- package/bin/_cli/cli-update.mjs +119 -0
- package/bin/_cli/credentials.mjs +723 -0
- package/bin/_cli/index.mjs +170 -0
- package/bin/_cli/launcher.mjs +474 -0
- package/bin/_cli/new-ticket.mjs +306 -0
- package/bin/_cli/postinstall.mjs +43 -0
- package/bin/_cli/protocol.mjs +1799 -0
- package/bin/_cli/setup.mjs +1634 -0
- package/bin/_cli/ticket.mjs +55 -0
- package/bin/_cli/tickets.mjs +121 -0
- package/bin/_cli/version.mjs +7 -0
- package/bin/ovld.mjs +13 -0
- package/package.json +36 -0
- package/plugins/claude/.claude-plugin/plugin.json +34 -0
- package/plugins/claude/README.md +42 -0
- package/plugins/claude/commands/connect.md +19 -0
- package/plugins/claude/commands/create.md +17 -0
- package/plugins/claude/commands/load.md +19 -0
- package/plugins/claude/commands/spawn.md +17 -0
- package/plugins/claude/hooks/hooks.json +15 -0
- package/plugins/claude/scripts/permission-hook.sh +21 -0
- package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
- package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
- package/plugins/cursor/commands/connect.md +3 -0
- package/plugins/cursor/commands/create.md +11 -0
- package/plugins/cursor/commands/load.md +3 -0
- package/plugins/cursor/commands/spawn.md +3 -0
- package/plugins/cursor/mcp.json +8 -0
- package/plugins/cursor/rules/overlord-local.mdc +10 -0
- package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
- package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
- package/plugins/overlord/.codex-plugin/plugin.json +35 -0
- package/plugins/overlord/.mcp.json +11 -0
- package/plugins/overlord/README.md +42 -0
- package/plugins/overlord/assets/icon.png +0 -0
- package/plugins/overlord/assets/logo.png +0 -0
- package/plugins/overlord/assets/screenshot.svg +28 -0
- package/plugins/overlord/scripts/overlord-mcp.mjs +688 -0
- 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
|
+
}
|