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.
- package/README.md +61 -0
- package/bin/_cli/attach.mjs +356 -0
- package/bin/_cli/auth.mjs +382 -0
- package/bin/_cli/credentials.mjs +267 -0
- package/bin/_cli/index.mjs +126 -0
- package/bin/_cli/launcher.mjs +196 -0
- package/bin/_cli/new-ticket.mjs +248 -0
- package/bin/_cli/protocol.mjs +1271 -0
- package/bin/_cli/setup.mjs +553 -0
- package/bin/_cli/ticket.mjs +55 -0
- package/bin/_cli/tickets.mjs +120 -0
- package/bin/ovld.mjs +8 -0
- package/package.json +30 -0
|
@@ -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
|
+
}
|