overlord-cli 3.5.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.
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runAttachCommand } from './attach.mjs';
4
+ import { runAuthCommand } from './auth.mjs';
5
+ import { runLauncherCommand } from './launcher.mjs';
6
+ import { runCreateCommand, runPromptCommand } from './new-ticket.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
+
12
+ function printHelp(primaryCommand) {
13
+ console.log(`Overlord CLI
14
+
15
+ Primary command: ${primaryCommand}
16
+
17
+ Usage:
18
+ ${primaryCommand} attach [ticketId] [agent] Search tickets and launch an agent (interactive)
19
+ ${primaryCommand} create "<objective>" Create a ticket with numbered project selection
20
+ ${primaryCommand} prompt "<objective>" Create a ticket, then launch an agent on it
21
+ ${primaryCommand} auth <subcommand> Login, logout, or check auth status
22
+ ${primaryCommand} tickets <subcommand> Create or list tickets
23
+ ${primaryCommand} ticket <subcommand> Work with a single ticket
24
+ ${primaryCommand} protocol <subcommand> Agent workflow commands
25
+ ${primaryCommand} connect <agent> Launch an agent on a ticket
26
+ ${primaryCommand} restart <agent> Resume an agent session
27
+ ${primaryCommand} context Print ticket context (requires TICKET_ID)
28
+ ${primaryCommand} setup <agent|all> Install Overlord agent connector
29
+ ${primaryCommand} doctor Validate installed agent connectors
30
+ ${primaryCommand} help Show this help message
31
+
32
+ Agents:
33
+ Use ${primaryCommand} protocol help for ticket lifecycle commands.
34
+
35
+ Auth:
36
+ ${primaryCommand} auth login Authorize CLI via browser
37
+ ${primaryCommand} auth status Show login status
38
+ ${primaryCommand} auth logout Remove stored credentials
39
+
40
+ Tickets:
41
+ ${primaryCommand} create "..." [options]
42
+ ${primaryCommand} prompt "..." [options]
43
+ ${primaryCommand} tickets create "..." [options]
44
+ ${primaryCommand} tickets list [--status <status>]
45
+
46
+ Ticket:
47
+ ${primaryCommand} ticket context <ticketId>
48
+
49
+ Run a subcommand with --help for more detail.
50
+ `);
51
+ }
52
+
53
+ export async function runCli({ primaryCommand }) {
54
+ const [command, ...rest] = process.argv.slice(2);
55
+
56
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
57
+ printHelp(primaryCommand);
58
+ return;
59
+ }
60
+
61
+ // Attach command (interactive ticket search + agent launcher)
62
+ if (command === 'attach') {
63
+ await runAttachCommand(rest);
64
+ return;
65
+ }
66
+
67
+ if (command === 'create') {
68
+ await runCreateCommand(rest);
69
+ return;
70
+ }
71
+
72
+ if (command === 'prompt') {
73
+ await runPromptCommand(rest);
74
+ return;
75
+ }
76
+
77
+ // Auth group
78
+ if (command === 'auth') {
79
+ await runAuthCommand(rest[0], rest.slice(1));
80
+ return;
81
+ }
82
+
83
+ // Tickets (plural) group
84
+ if (command === 'tickets') {
85
+ await runTicketsCommand(rest[0], rest.slice(1));
86
+ return;
87
+ }
88
+
89
+ // Ticket (singular) group
90
+ if (command === 'ticket') {
91
+ await runTicketCommand(rest[0], rest.slice(1));
92
+ return;
93
+ }
94
+
95
+ // Protocol group
96
+ if (command === 'protocol') {
97
+ await runProtocolCommand(rest[0], rest.slice(1));
98
+ return;
99
+ }
100
+
101
+ if (command === 'setup') {
102
+ await runSetupCommand(rest);
103
+ return;
104
+ }
105
+
106
+ if (command === 'doctor') {
107
+ await runDoctorCommand();
108
+ return;
109
+ }
110
+
111
+ // Launcher commands (`run` / `resume` kept as legacy aliases)
112
+ if (
113
+ command === 'connect' ||
114
+ command === 'restart' ||
115
+ command === 'run' ||
116
+ command === 'resume' ||
117
+ command === 'context'
118
+ ) {
119
+ await runLauncherCommand(command, rest);
120
+ return;
121
+ }
122
+
123
+ console.error(`Unknown command: ${command}\n`);
124
+ printHelp(primaryCommand);
125
+ process.exit(1);
126
+ }
@@ -0,0 +1,196 @@
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 { buildAuthHeaders, resolveAuth } from './credentials.mjs';
10
+ import { runAttachCommand } from './attach.mjs';
11
+
12
+ async function fetchContext(platformUrl, agentToken, localSecret, ticketId) {
13
+ const url = `${platformUrl}/api/protocol/context/${ticketId}?context=cli`;
14
+ const response = await fetch(url, {
15
+ headers: buildAuthHeaders(agentToken, localSecret)
16
+ });
17
+
18
+ if (!response.ok) {
19
+ throw new Error(
20
+ `Failed to fetch ticket context (${response.status}): ${await response.text()}`
21
+ );
22
+ }
23
+
24
+ return response.text();
25
+ }
26
+
27
+ const agentIdentifierMap = {
28
+ claude: 'claude-code',
29
+ codex: 'codex',
30
+ cursor: 'cursor',
31
+ gemini: 'gemini',
32
+ opencode: 'opencode'
33
+ };
34
+
35
+ const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
36
+
37
+ function parseLauncherArgs(args) {
38
+ const positionals = [];
39
+ const flags = {};
40
+
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i];
43
+ if (!arg.startsWith('--')) {
44
+ positionals.push(arg);
45
+ continue;
46
+ }
47
+
48
+ const eqIdx = arg.indexOf('=');
49
+ if (eqIdx !== -1) {
50
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
51
+ continue;
52
+ }
53
+
54
+ const key = arg.slice(2);
55
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
56
+ flags[key] = args[i + 1];
57
+ i++;
58
+ } else {
59
+ flags[key] = true;
60
+ }
61
+ }
62
+
63
+ return { positionals, flags };
64
+ }
65
+
66
+ async function runAgent(agent, mode = 'run') {
67
+ if (!agent || !supportedAgents.includes(agent)) {
68
+ console.error(
69
+ `Usage: ovld connect <agent> [--ticket-id <id>] | ovld restart <agent> [--ticket-id <id>] (agent must be one of: ${supportedAgents.join(', ')})`
70
+ );
71
+ process.exit(1);
72
+ }
73
+
74
+ const ticketId = process.env.TICKET_ID;
75
+ if (!ticketId) {
76
+ console.error('Missing required environment variable: TICKET_ID');
77
+ process.exit(1);
78
+ }
79
+
80
+ const { platformUrl, agentToken, localSecret } = resolveAuth();
81
+ const context = await fetchContext(platformUrl, agentToken, localSecret, ticketId);
82
+
83
+ const childEnv = { ...process.env, AGENT_IDENTIFIER: agentIdentifierMap[agent] };
84
+
85
+ try {
86
+ if (agent === 'claude') {
87
+ if (mode === 'resume') {
88
+ const claudeSessionId = process.env.CLAUDE_SESSION_ID?.trim();
89
+ const args = claudeSessionId
90
+ ? ['--resume', claudeSessionId, context]
91
+ : ['--continue', context];
92
+ execFileSync('claude', args, { stdio: 'inherit', env: childEnv });
93
+ } else {
94
+ execFileSync(
95
+ 'claude',
96
+ [
97
+ '--append-system-prompt',
98
+ context,
99
+ 'Begin working on this ticket. Start by calling the attach endpoint, then proceed with the objective described in your system prompt.'
100
+ ],
101
+ { stdio: 'inherit', env: childEnv }
102
+ );
103
+ }
104
+ } else if (agent === 'codex') {
105
+ if (mode === 'resume') {
106
+ const codexSessionId = process.env.CODEX_SESSION_ID?.trim();
107
+ const args = codexSessionId
108
+ ? ['resume', codexSessionId, context]
109
+ : ['resume', '--last', context];
110
+ execFileSync('codex', args, { stdio: 'inherit', env: childEnv });
111
+ } else {
112
+ execFileSync('codex', [context], { stdio: 'inherit', env: childEnv });
113
+ }
114
+ } else if (agent === 'cursor') {
115
+ execFileSync('agent', [context], { stdio: 'inherit', env: childEnv });
116
+ } else if (agent === 'opencode') {
117
+ if (mode === 'resume') {
118
+ const openCodeSessionId = process.env.OPENCODE_SESSION_ID?.trim();
119
+ const args = openCodeSessionId
120
+ ? ['--continue', '--session', openCodeSessionId, '--prompt', context]
121
+ : ['--continue', '--prompt', context];
122
+ execFileSync('opencode', args, { stdio: 'inherit', env: childEnv });
123
+ } else {
124
+ execFileSync('opencode', ['--prompt', context], { stdio: 'inherit', env: childEnv });
125
+ }
126
+ } else {
127
+ execFileSync('gemini', [context], { stdio: 'inherit', env: childEnv });
128
+ }
129
+ } catch (error) {
130
+ const isResume = mode === 'resume';
131
+ const noSessionHint =
132
+ agent === 'claude'
133
+ ? `No prior Claude session was found. Start one with \`ovld connect claude --ticket-id <ticket-id>\` first.`
134
+ : agent === 'codex'
135
+ ? `No prior Codex session was found. Start one with \`ovld connect codex --ticket-id <ticket-id>\` first.`
136
+ : agent === 'opencode'
137
+ ? `No prior OpenCode session was found. Start one with \`ovld connect opencode --ticket-id <ticket-id>\` first.`
138
+ : '';
139
+ const message = error instanceof Error ? error.message : String(error);
140
+
141
+ if (isResume && noSessionHint) {
142
+ console.error(`${message}\n${noSessionHint}`);
143
+ } else {
144
+ console.error(message);
145
+ }
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ async function printContext() {
151
+ const ticketId = process.env.TICKET_ID;
152
+ if (!ticketId) {
153
+ console.error('Missing required environment variable: TICKET_ID\n');
154
+ console.error('Usage: TICKET_ID=<id> ovld context');
155
+ console.error(' ovld ticket context <id> (recommended)');
156
+ process.exit(1);
157
+ }
158
+
159
+ const { platformUrl, agentToken, localSecret } = resolveAuth();
160
+ const context = await fetchContext(platformUrl, agentToken, localSecret, ticketId);
161
+ process.stdout.write(context);
162
+ }
163
+
164
+ export async function runLauncherCommand(command, args) {
165
+ const normalizedCommand = command === 'connect' ? 'run' : command === 'restart' ? 'resume' : command;
166
+ const { positionals, flags } = parseLauncherArgs(args);
167
+ const ticketId = typeof flags['ticket-id'] === 'string' ? flags['ticket-id'].trim() : '';
168
+
169
+ if (ticketId) {
170
+ process.env.TICKET_ID = ticketId;
171
+ }
172
+
173
+ if (normalizedCommand === 'run') {
174
+ // If no ticket-id flag and no TICKET_ID env var, present interactive ticket search
175
+ if (!ticketId && !process.env.TICKET_ID) {
176
+ await runAttachCommand([undefined, positionals[0]]);
177
+ return;
178
+ }
179
+ await runAgent(positionals[0]);
180
+ return;
181
+ }
182
+
183
+ if (normalizedCommand === 'resume') {
184
+ await runAgent(positionals[0], 'resume');
185
+ return;
186
+ }
187
+
188
+ if (normalizedCommand === 'context') {
189
+ await printContext();
190
+ return;
191
+ }
192
+
193
+ // Should not happen if called correctly
194
+ console.error(`Unknown launcher command: ${command}`);
195
+ process.exit(1);
196
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ import readline from 'node:readline/promises';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+
6
+ import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
7
+ import { runLauncherCommand } from './launcher.mjs';
8
+
9
+ const PROMPT_AGENTS = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
10
+
11
+ function parseFlags(args) {
12
+ const flags = {};
13
+ const positionals = [];
14
+
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+
18
+ if (!arg.startsWith('--')) {
19
+ positionals.push(arg);
20
+ continue;
21
+ }
22
+
23
+ const eqIdx = arg.indexOf('=');
24
+ if (eqIdx !== -1) {
25
+ flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
26
+ continue;
27
+ }
28
+
29
+ const key = arg.slice(2);
30
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
31
+ flags[key] = args[i + 1];
32
+ i++;
33
+ } else {
34
+ flags[key] = true;
35
+ }
36
+ }
37
+
38
+ return { flags, positionals };
39
+ }
40
+
41
+ function buildUsage(commandName) {
42
+ if (commandName === 'prompt') {
43
+ return 'Usage: ovld prompt "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>]';
44
+ }
45
+
46
+ return 'Usage: ovld create "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>]';
47
+ }
48
+
49
+ function ensureObjective(commandName, objective) {
50
+ if (objective) return;
51
+
52
+ console.error(`Error: objective is required.\n`);
53
+ console.error(buildUsage(commandName));
54
+ process.exit(1);
55
+ }
56
+
57
+ export function parseNumberedSelection(rawValue, count) {
58
+ const trimmed = rawValue.trim();
59
+ if (!trimmed) return null;
60
+ if (!/^\d+$/.test(trimmed)) return null;
61
+
62
+ const selected = Number.parseInt(trimmed, 10);
63
+ if (selected < 1 || selected > count) return null;
64
+
65
+ return selected - 1;
66
+ }
67
+
68
+ export function sortProjects(projects) {
69
+ return [...projects].sort((left, right) => {
70
+ const byOrganization = String(left.organizationName ?? '').localeCompare(
71
+ String(right.organizationName ?? '')
72
+ );
73
+ if (byOrganization !== 0) return byOrganization;
74
+
75
+ const byName = String(left.name ?? '').localeCompare(String(right.name ?? ''));
76
+ if (byName !== 0) return byName;
77
+
78
+ return String(left.id ?? '').localeCompare(String(right.id ?? ''));
79
+ });
80
+ }
81
+
82
+ function projectLabel(project) {
83
+ const organizationName = String(project.organizationName ?? '').trim();
84
+ return organizationName ? `${project.name} - ${organizationName}` : project.name;
85
+ }
86
+
87
+ async function promptForSelection({ items, label, prompt, renderItem }) {
88
+ if (!items.length) {
89
+ throw new Error(`No ${label.toLowerCase()} available.`);
90
+ }
91
+
92
+ const rl = readline.createInterface({ input, output });
93
+
94
+ try {
95
+ while (true) {
96
+ output.write(`\n${label}\n`);
97
+ items.forEach((item, index) => {
98
+ output.write(` ${index + 1}. ${renderItem(item, index)}\n`);
99
+ });
100
+
101
+ const answer = await rl.question(`\n${prompt} `);
102
+ const selectedIndex = parseNumberedSelection(answer, items.length);
103
+ if (selectedIndex !== null) {
104
+ return items[selectedIndex];
105
+ }
106
+
107
+ output.write(`Enter a number between 1 and ${items.length}.\n`);
108
+ }
109
+ } finally {
110
+ rl.close();
111
+ }
112
+ }
113
+
114
+ async function fetchProjects(platformUrl, agentToken, localSecret) {
115
+ const res = await fetch(`${platformUrl}/api/protocol/projects`, {
116
+ headers: buildAuthHeaders(agentToken, localSecret)
117
+ });
118
+
119
+ const data = await res.json().catch(() => ({}));
120
+ if (!res.ok) {
121
+ throw new Error(
122
+ `Failed to list projects (${res.status}): ${data.error ?? JSON.stringify(data)}`
123
+ );
124
+ }
125
+
126
+ return Array.isArray(data.projects) ? sortProjects(data.projects) : [];
127
+ }
128
+
129
+ async function createTicket(platformUrl, agentToken, localSecret, body) {
130
+ const res = await fetch(`${platformUrl}/api/protocol/tickets`, {
131
+ method: 'POST',
132
+ headers: {
133
+ ...buildAuthHeaders(agentToken, localSecret),
134
+ 'Content-Type': 'application/json'
135
+ },
136
+ body: JSON.stringify(body)
137
+ });
138
+
139
+ const data = await res.json().catch(() => ({}));
140
+ if (!res.ok) {
141
+ throw new Error(`Failed to create ticket (${res.status}): ${data.error ?? JSON.stringify(data)}`);
142
+ }
143
+
144
+ return data.ticket;
145
+ }
146
+
147
+ function resolveProject(projects, projectId) {
148
+ if (!projectId) return null;
149
+
150
+ const project = projects.find(candidate => candidate.id === projectId);
151
+ if (!project) {
152
+ throw new Error(`Unknown project ID: ${projectId}`);
153
+ }
154
+
155
+ return project;
156
+ }
157
+
158
+ function resolveAgent(agent) {
159
+ if (!agent) return null;
160
+
161
+ const normalizedAgent = agent.trim().toLowerCase();
162
+ if (!PROMPT_AGENTS.includes(normalizedAgent)) {
163
+ throw new Error(`Unknown agent: "${agent}". Must be one of: ${PROMPT_AGENTS.join(', ')}`);
164
+ }
165
+
166
+ return normalizedAgent;
167
+ }
168
+
169
+ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
170
+ const { flags, positionals } = parseFlags(args);
171
+ const objective = String(flags.objective ?? positionals.join(' ')).trim();
172
+ ensureObjective(commandName, objective);
173
+
174
+ const { platformUrl, agentToken, localSecret } = resolveAuth();
175
+ const projects = await fetchProjects(platformUrl, agentToken, localSecret);
176
+
177
+ if (!projects.length) {
178
+ throw new Error('No projects available. Create a project first.');
179
+ }
180
+
181
+ const selectedProject =
182
+ resolveProject(projects, typeof flags['project-id'] === 'string' ? flags['project-id'] : '') ??
183
+ (await promptForSelection({
184
+ items: projects,
185
+ label: 'Projects',
186
+ prompt: 'Select a project by number:',
187
+ renderItem: project => projectLabel(project)
188
+ }));
189
+
190
+ const ticket = await createTicket(platformUrl, agentToken, localSecret, {
191
+ objective,
192
+ title: String(flags.title ?? ''),
193
+ acceptanceCriteria: String(flags['acceptance-criteria'] ?? ''),
194
+ availableTools: String(flags['available-tools'] ?? ''),
195
+ executionTarget: String(flags['execution-target'] ?? 'agent'),
196
+ priority: String(flags.priority ?? 'medium'),
197
+ projectId: selectedProject.id
198
+ });
199
+
200
+ if (!launchAgent) {
201
+ console.log(`ticket created with id ${ticket.id}`);
202
+ return;
203
+ }
204
+
205
+ const selectedAgent =
206
+ resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
207
+ (await promptForSelection({
208
+ items: PROMPT_AGENTS,
209
+ label: 'Agents',
210
+ prompt: 'Select an agent by number:',
211
+ renderItem: agent => agent
212
+ }));
213
+
214
+ process.env.TICKET_ID = ticket.id;
215
+ await runLauncherCommand('run', [selectedAgent, '--ticket-id', ticket.id]);
216
+ }
217
+
218
+ export async function runCreateCommand(args) {
219
+ if (args[0] === '--help' || args[0] === 'help') {
220
+ console.log(`${buildUsage('create')}
221
+
222
+ Creates a ticket after interactive numbered project selection.
223
+
224
+ Examples:
225
+ ovld create "Implement login page"
226
+ ovld create "Fix sync bug" --project-id <project-id>
227
+ `);
228
+ return;
229
+ }
230
+
231
+ await runTicketCreationFlow(args, { commandName: 'create', launchAgent: false });
232
+ }
233
+
234
+ export async function runPromptCommand(args) {
235
+ if (args[0] === '--help' || args[0] === 'help') {
236
+ console.log(`${buildUsage('prompt')}
237
+
238
+ Creates a ticket after interactive numbered project selection, then lets you pick an agent by number and launches it on the new ticket.
239
+
240
+ Examples:
241
+ ovld prompt "Implement login page"
242
+ ovld prompt "Investigate flaky tests" --agent codex
243
+ `);
244
+ return;
245
+ }
246
+
247
+ await runTicketCreationFlow(args, { commandName: 'prompt', launchAgent: true });
248
+ }