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,1799 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { buildAuthHeaders, getAuthStatus, resolveAuth } from './credentials.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse simple CLI flags: --key value or --key=value
|
|
12
|
+
* @param {string[]} args
|
|
13
|
+
* @returns {Record<string, string | boolean>}
|
|
14
|
+
*/
|
|
15
|
+
function parseFlags(args) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
const arg = args[i];
|
|
19
|
+
if (arg.startsWith('--')) {
|
|
20
|
+
const eqIdx = arg.indexOf('=');
|
|
21
|
+
if (eqIdx !== -1) {
|
|
22
|
+
const key = arg.slice(2, eqIdx);
|
|
23
|
+
result[key] = arg.slice(eqIdx + 1);
|
|
24
|
+
} else {
|
|
25
|
+
const key = arg.slice(2);
|
|
26
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
27
|
+
result[key] = args[i + 1];
|
|
28
|
+
i++;
|
|
29
|
+
} else {
|
|
30
|
+
result[key] = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveProtocolAgentIdentifier(flags = {}) {
|
|
39
|
+
const explicitAgent = typeof flags.agent === 'string' ? flags.agent.trim() : '';
|
|
40
|
+
if (explicitAgent) return explicitAgent;
|
|
41
|
+
|
|
42
|
+
const envAgent = process.env.AGENT_IDENTIFIER?.trim();
|
|
43
|
+
return envAgent || 'claude-code';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveProtocolTicketDelegate(
|
|
47
|
+
flags = {},
|
|
48
|
+
modelIdentifier = '',
|
|
49
|
+
agentIdentifier = ''
|
|
50
|
+
) {
|
|
51
|
+
const explicitDelegate = typeof flags.delegate === 'string' ? flags.delegate.trim() : '';
|
|
52
|
+
if (explicitDelegate) return explicitDelegate;
|
|
53
|
+
|
|
54
|
+
const resolvedModel = typeof modelIdentifier === 'string' ? modelIdentifier.trim() : '';
|
|
55
|
+
if (resolvedModel) return resolvedModel;
|
|
56
|
+
|
|
57
|
+
const resolvedAgent = String(agentIdentifier).trim();
|
|
58
|
+
return resolvedAgent || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveProtocolModelIdentifier(flags = {}) {
|
|
62
|
+
const explicitModel = typeof flags.model === 'string' ? flags.model.trim() : '';
|
|
63
|
+
if (explicitModel) return explicitModel;
|
|
64
|
+
|
|
65
|
+
const envModel =
|
|
66
|
+
process.env.OVERLORD_MODEL_IDENTIFIER?.trim() ||
|
|
67
|
+
process.env.MODEL_IDENTIFIER?.trim() ||
|
|
68
|
+
process.env.AGENT_MODEL?.trim();
|
|
69
|
+
return envModel || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveProtocolMetadata(flags = {}, base = {}) {
|
|
73
|
+
const metadata = { ...base };
|
|
74
|
+
|
|
75
|
+
if (flags['metadata-json']) {
|
|
76
|
+
const parsed = parseJsonFlag('--metadata-json', flags['metadata-json']);
|
|
77
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
78
|
+
throw new Error('--metadata-json must be a JSON object');
|
|
79
|
+
}
|
|
80
|
+
Object.assign(metadata, parsed);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const modelIdentifier = resolveProtocolModelIdentifier(flags);
|
|
84
|
+
if (modelIdentifier) {
|
|
85
|
+
metadata.model = modelIdentifier;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return metadata;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Default request timeout in milliseconds. Overridable via --timeout flag or
|
|
93
|
+
* OVERLORD_TIMEOUT env var. A bounded timeout prevents indefinite spinner hangs
|
|
94
|
+
* in sandboxed runtimes where deliver requests can stall without a connection error.
|
|
95
|
+
*/
|
|
96
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
97
|
+
|
|
98
|
+
async function apiPost(
|
|
99
|
+
platformUrl,
|
|
100
|
+
token,
|
|
101
|
+
localSecret,
|
|
102
|
+
organizationId,
|
|
103
|
+
path,
|
|
104
|
+
body,
|
|
105
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
106
|
+
) {
|
|
107
|
+
const requestUrl = `${platformUrl}${path}`;
|
|
108
|
+
const requestStart = Date.now();
|
|
109
|
+
let res;
|
|
110
|
+
try {
|
|
111
|
+
res = await fetch(requestUrl, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
...buildAuthHeaders(token, localSecret, organizationId),
|
|
115
|
+
'Content-Type': 'application/json'
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
|
|
123
|
+
// AbortSignal.timeout() throws a DOMException with name 'TimeoutError'
|
|
124
|
+
if (error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Request timed out after ${timeoutMs}ms calling ${requestUrl}.\n` +
|
|
127
|
+
`Tip: Ensure Overlord is running and reachable from this environment. ` +
|
|
128
|
+
`Increase the limit with --timeout <ms> or OVERLORD_TIMEOUT=<ms>.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const causeCode =
|
|
133
|
+
typeof error === 'object' &&
|
|
134
|
+
error !== null &&
|
|
135
|
+
'cause' in error &&
|
|
136
|
+
typeof error.cause === 'object' &&
|
|
137
|
+
error.cause !== null &&
|
|
138
|
+
'code' in error.cause
|
|
139
|
+
? String(error.cause.code)
|
|
140
|
+
: '';
|
|
141
|
+
|
|
142
|
+
let hint = 'Check your network and Overlord server settings.';
|
|
143
|
+
if (causeCode === 'ECONNREFUSED') {
|
|
144
|
+
hint =
|
|
145
|
+
'Connection refused. Verify Overlord is running and OVERLORD_URL points to the correct port. If this environment is sandboxed or network-restricted, request permission escalation before retrying.';
|
|
146
|
+
} else if (causeCode === 'ENOTFOUND') {
|
|
147
|
+
hint =
|
|
148
|
+
'Host not found. Verify OVERLORD_URL uses a valid hostname. If this environment is sandboxed or network-restricted, request permission escalation before retrying.';
|
|
149
|
+
} else if (causeCode === 'ETIMEDOUT') {
|
|
150
|
+
hint =
|
|
151
|
+
'Connection timed out. Verify server availability and local firewall/VPN settings. If this environment is sandboxed or network-restricted, request permission escalation before retrying.';
|
|
152
|
+
} else if (requestUrl.includes('localhost') || requestUrl.includes('127.0.0.1')) {
|
|
153
|
+
hint =
|
|
154
|
+
'Local server unreachable. Start Overlord (usually http://localhost:3000) or update OVERLORD_URL. If this environment is sandboxed or network-restricted, request permission escalation before retrying.';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Network error calling ${requestUrl}: ${message}${causeCode ? ` (${causeCode})` : ''}\n${hint}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const durationMs = Date.now() - requestStart;
|
|
163
|
+
process.stderr.write(`[protocol] ${path} → ${res.status} (${durationMs}ms)\n`);
|
|
164
|
+
|
|
165
|
+
const data = await res.json().catch(() => ({}));
|
|
166
|
+
|
|
167
|
+
if (res.status === 401) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Authentication failed (401): ${data.error ?? 'Invalid or missing token.'}\n` +
|
|
170
|
+
`IMPORTANT: Stop all work immediately. Your Overlord auth session is invalid, expired, or missing required scope.\n` +
|
|
171
|
+
`First run \`ovld auth repair\` yourself.\n` +
|
|
172
|
+
`If repair does not fix it, ask the user to sign in again with Overlord Desktop or \`ovld auth login\` if needed.\n` +
|
|
173
|
+
`Then ask whether they would like to proceed without submitting updates to Overlord.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
throw new Error(`API error (${res.status}): ${data.error ?? JSON.stringify(data)}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return data;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function uploadToSignedUrl(uploadUrl, bytes, contentType, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
185
|
+
let res;
|
|
186
|
+
try {
|
|
187
|
+
res = await fetch(uploadUrl, {
|
|
188
|
+
method: 'PUT',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': contentType,
|
|
191
|
+
'x-upsert': 'false'
|
|
192
|
+
},
|
|
193
|
+
body: bytes,
|
|
194
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
198
|
+
if (error && (error.name === 'TimeoutError' || error.name === 'AbortError')) {
|
|
199
|
+
throw new Error(`Upload timed out after ${timeoutMs}ms.`);
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Upload failed: ${message}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
const text = await res.text().catch(() => '');
|
|
206
|
+
throw new Error(`Upload failed (${res.status}): ${text || 'Unknown storage error.'}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Read SESSION_KEY and TICKET_ID from env if flags not provided */
|
|
211
|
+
function resolveSessionFlags(flags) {
|
|
212
|
+
return {
|
|
213
|
+
sessionKey: String(flags['session-key'] ?? process.env.SESSION_KEY ?? ''),
|
|
214
|
+
ticketId: String(flags['ticket-id'] ?? process.env.TICKET_ID ?? '')
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Resolve request timeout from --timeout flag or OVERLORD_TIMEOUT env var. */
|
|
219
|
+
function resolveTimeout(flags) {
|
|
220
|
+
const raw = flags['timeout'] ?? process.env.OVERLORD_TIMEOUT;
|
|
221
|
+
if (raw) {
|
|
222
|
+
const ms = parseInt(String(raw), 10);
|
|
223
|
+
if (!isNaN(ms) && ms > 0) return ms;
|
|
224
|
+
}
|
|
225
|
+
return DEFAULT_TIMEOUT_MS;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function requireFlag(flags, name, envAlias) {
|
|
229
|
+
const value = flags[name] ?? (envAlias ? process.env[envAlias] : undefined);
|
|
230
|
+
if (!value) {
|
|
231
|
+
throw new Error(`--${name} is required (or set ${envAlias ?? name.toUpperCase()})`);
|
|
232
|
+
}
|
|
233
|
+
return String(value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseJsonFlag(flagName, rawValue) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(String(rawValue));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
241
|
+
throw new Error(`${flagName} must be valid JSON: ${detail}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function readTextFile(filePath, label) {
|
|
246
|
+
try {
|
|
247
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
248
|
+
} catch (err) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`${label}: could not read "${filePath}": ${err instanceof Error ? err.message : String(err)}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readJsonFile(filePath, label) {
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(readTextFile(filePath, label));
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (err instanceof Error && err.message.startsWith(`${label}: could not read`)) {
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
throw new Error(
|
|
263
|
+
`${label}: could not parse "${filePath}": ${err instanceof Error ? err.message : String(err)}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function readTextFromStdin(label) {
|
|
269
|
+
const chunks = [];
|
|
270
|
+
try {
|
|
271
|
+
for await (const chunk of process.stdin) {
|
|
272
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`${label}: could not read stdin: ${err instanceof Error ? err.message : String(err)}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function readJsonFileOrStdin(filePath, label) {
|
|
283
|
+
if (filePath !== '-') {
|
|
284
|
+
return readJsonFile(filePath, label);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
return JSON.parse(await readTextFromStdin(label));
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err instanceof Error && err.message.startsWith(`${label}: could not read stdin`)) {
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
throw new Error(
|
|
294
|
+
`${label}: could not parse stdin: ${err instanceof Error ? err.message : String(err)}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// changeRationales helper
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolve changeRationales from --change-rationales-json or --change-rationales-file flags.
|
|
305
|
+
* @param {Record<string, string | boolean>} flags
|
|
306
|
+
* @returns {Promise<Array<object>>}
|
|
307
|
+
*/
|
|
308
|
+
async function resolveChangeRationales(flags) {
|
|
309
|
+
if (flags['change-rationales-file']) {
|
|
310
|
+
return await readJsonFileOrStdin(
|
|
311
|
+
String(flags['change-rationales-file']),
|
|
312
|
+
'--change-rationales-file'
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (flags['change-rationales-json']) {
|
|
316
|
+
return parseJsonFlag('--change-rationales-json', flags['change-rationales-json']);
|
|
317
|
+
}
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function normalizeRepoRelativeFilePath(filePath, repoRoot) {
|
|
322
|
+
if (typeof filePath !== 'string') return null;
|
|
323
|
+
|
|
324
|
+
const trimmed = filePath.trim();
|
|
325
|
+
if (!trimmed) return null;
|
|
326
|
+
|
|
327
|
+
if (path.isAbsolute(trimmed)) {
|
|
328
|
+
const relative = path.relative(repoRoot, trimmed);
|
|
329
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return relative.replaceAll(path.sep, '/');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return trimmed.replace(/^[.][/\\]+/, '').replaceAll('\\', '/');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getGitChangedFiles() {
|
|
339
|
+
try {
|
|
340
|
+
const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
341
|
+
cwd: process.cwd(),
|
|
342
|
+
encoding: 'utf8',
|
|
343
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
344
|
+
}).trim();
|
|
345
|
+
|
|
346
|
+
if (!repoRoot) return null;
|
|
347
|
+
|
|
348
|
+
const output = execFileSync('git', ['status', '--porcelain=v1', '-z'], {
|
|
349
|
+
cwd: repoRoot,
|
|
350
|
+
encoding: 'utf8',
|
|
351
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const changedFiles = new Set();
|
|
355
|
+
const entries = output.split('\0');
|
|
356
|
+
|
|
357
|
+
for (let i = 0; i < entries.length; i++) {
|
|
358
|
+
const entry = entries[i];
|
|
359
|
+
if (!entry) continue;
|
|
360
|
+
|
|
361
|
+
const status = entry.slice(0, 2);
|
|
362
|
+
const normalizedPath = normalizeRepoRelativeFilePath(entry.slice(3), repoRoot);
|
|
363
|
+
if (normalizedPath) {
|
|
364
|
+
changedFiles.add(normalizedPath);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (status.includes('R') || status.includes('C')) {
|
|
368
|
+
i += 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { repoRoot, changedFiles };
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function createFileChangeCheckError(message, changedFiles, rationalePaths = []) {
|
|
379
|
+
const changedPreview = [...changedFiles].slice(0, 10).join(', ');
|
|
380
|
+
const rationalePreview = rationalePaths.slice(0, 10).join(', ');
|
|
381
|
+
|
|
382
|
+
return new Error(
|
|
383
|
+
`${message}\n` +
|
|
384
|
+
`Overlord persists file changes through \`changeRationales\`, not \`file_changes\` artifacts.\n` +
|
|
385
|
+
`Re-run with --change-rationales-json or --change-rationales-file, or pass --skip-file-change-check if this was intentional.` +
|
|
386
|
+
`${changedPreview ? `\nChanged files: ${changedPreview}${changedFiles.size > 10 ? ', ...' : ''}` : ''}` +
|
|
387
|
+
`${rationalePreview ? `\nProvided rationale paths: ${rationalePreview}${rationalePaths.length > 10 ? ', ...' : ''}` : ''}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function validateDeliverFileChanges(flags, changeRationales) {
|
|
392
|
+
if (flags['skip-file-change-check']) return;
|
|
393
|
+
|
|
394
|
+
const gitState = getGitChangedFiles();
|
|
395
|
+
if (!gitState || gitState.changedFiles.size === 0) return;
|
|
396
|
+
|
|
397
|
+
const rationalePaths = changeRationales
|
|
398
|
+
.map(rationale => normalizeRepoRelativeFilePath(rationale?.file_path, gitState.repoRoot))
|
|
399
|
+
.filter(Boolean);
|
|
400
|
+
|
|
401
|
+
if (rationalePaths.length === 0) {
|
|
402
|
+
throw createFileChangeCheckError(
|
|
403
|
+
'Git shows changed files in this workspace, but this delivery did not include matching `changeRationales`.',
|
|
404
|
+
gitState.changedFiles
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const hasMatch = rationalePaths.some(filePath => gitState.changedFiles.has(filePath));
|
|
409
|
+
if (!hasMatch) {
|
|
410
|
+
throw createFileChangeCheckError(
|
|
411
|
+
'Git shows changed files in this workspace, but none of the supplied `changeRationales.file_path` entries match them.',
|
|
412
|
+
gitState.changedFiles,
|
|
413
|
+
rationalePaths
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// Auto-detect native agent session IDs
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Attempt to detect the current Claude Code session ID by finding the most
|
|
424
|
+
* recently modified .jsonl conversation file in ~/.claude/projects/<project>/.
|
|
425
|
+
* Returns the UUID or null if detection fails.
|
|
426
|
+
*/
|
|
427
|
+
function detectClaudeSessionId() {
|
|
428
|
+
try {
|
|
429
|
+
const cwd = process.cwd();
|
|
430
|
+
const projectDir = cwd.replace(/\//g, '-');
|
|
431
|
+
const sessionsDir = path.join(os.homedir(), '.claude', 'projects', projectDir);
|
|
432
|
+
|
|
433
|
+
if (!fs.existsSync(sessionsDir)) return null;
|
|
434
|
+
|
|
435
|
+
const files = fs
|
|
436
|
+
.readdirSync(sessionsDir)
|
|
437
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
438
|
+
.map(f => ({
|
|
439
|
+
name: f,
|
|
440
|
+
mtime: fs.statSync(path.join(sessionsDir, f)).mtimeMs
|
|
441
|
+
}))
|
|
442
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
443
|
+
|
|
444
|
+
if (files.length === 0) return null;
|
|
445
|
+
|
|
446
|
+
const uuid = files[0].name.replace('.jsonl', '');
|
|
447
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uuid)) {
|
|
448
|
+
return uuid;
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Codex exposes the active resumable thread id directly in the runtime
|
|
458
|
+
* environment, so prefer that over filesystem heuristics.
|
|
459
|
+
*/
|
|
460
|
+
function detectCodexSessionId() {
|
|
461
|
+
const sessionId = process.env.CODEX_THREAD_ID?.trim() || process.env.CODEX_SESSION_ID?.trim();
|
|
462
|
+
return sessionId || null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Resolve the external session ID from flags, env, or auto-detection.
|
|
467
|
+
* Priority: explicit flag > env var > auto-detect.
|
|
468
|
+
*/
|
|
469
|
+
function resolveExternalSessionId(flags) {
|
|
470
|
+
if (flags['external-session-id']) {
|
|
471
|
+
const val = String(flags['external-session-id']).trim();
|
|
472
|
+
return val.toLowerCase() === 'null' ? null : val;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const agentId = String(flags.agent ?? process.env.AGENT_IDENTIFIER ?? '').toLowerCase();
|
|
476
|
+
if (agentId.includes('codex')) {
|
|
477
|
+
const detected = detectCodexSessionId();
|
|
478
|
+
if (detected) return detected;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (agentId.includes('claude') || agentId === '' || agentId === 'claude-code') {
|
|
482
|
+
const detected = detectClaudeSessionId();
|
|
483
|
+
if (detected) return detected;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return undefined; // undefined = omit from payload
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// attach
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
async function protocolAttach(args) {
|
|
494
|
+
const flags = parseFlags(args);
|
|
495
|
+
const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
|
|
496
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
497
|
+
const timeoutMs = resolveTimeout(flags);
|
|
498
|
+
|
|
499
|
+
const externalSessionId = resolveExternalSessionId(flags);
|
|
500
|
+
|
|
501
|
+
const body = {
|
|
502
|
+
ticketId,
|
|
503
|
+
agentIdentifier: resolveProtocolAgentIdentifier(flags),
|
|
504
|
+
connectionMethod: String(flags.method ?? 'cli'),
|
|
505
|
+
...(externalSessionId !== undefined ? { externalSessionId } : {}),
|
|
506
|
+
metadata: resolveProtocolMetadata(flags, { cwd: process.cwd() })
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const data = await apiPost(
|
|
510
|
+
platformUrl,
|
|
511
|
+
bearerToken,
|
|
512
|
+
localSecret,
|
|
513
|
+
organizationId,
|
|
514
|
+
'/api/protocol/attach',
|
|
515
|
+
body,
|
|
516
|
+
timeoutMs
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const sessionKey = data.session?.sessionKey;
|
|
520
|
+
console.log(JSON.stringify(data, null, 2));
|
|
521
|
+
|
|
522
|
+
if (sessionKey) {
|
|
523
|
+
// Emit a machine-readable line for easy shell capture:
|
|
524
|
+
// SESSION_KEY=$(ovld protocol attach --ticket-id ... | grep ^SESSION_KEY= | cut -d= -f2)
|
|
525
|
+
process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// update
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
async function protocolUpdate(args) {
|
|
534
|
+
const flags = parseFlags(args);
|
|
535
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
536
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
537
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
538
|
+
const summary = flags['summary-file']
|
|
539
|
+
? readTextFile(String(flags['summary-file']), '--summary-file')
|
|
540
|
+
: requireFlag(flags, 'summary', undefined);
|
|
541
|
+
|
|
542
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
543
|
+
const timeoutMs = resolveTimeout(flags);
|
|
544
|
+
const changeRationales = await resolveChangeRationales(flags);
|
|
545
|
+
const externalSessionId = resolveExternalSessionId(flags);
|
|
546
|
+
|
|
547
|
+
const body = {
|
|
548
|
+
sessionKey,
|
|
549
|
+
ticketId,
|
|
550
|
+
summary,
|
|
551
|
+
...(externalSessionId !== undefined ? { externalSessionId } : {}),
|
|
552
|
+
...(flags['external-url']
|
|
553
|
+
? {
|
|
554
|
+
externalUrl:
|
|
555
|
+
String(flags['external-url']).trim().toLowerCase() === 'null'
|
|
556
|
+
? null
|
|
557
|
+
: String(flags['external-url'])
|
|
558
|
+
}
|
|
559
|
+
: {}),
|
|
560
|
+
...(flags.phase ? { phase: String(flags.phase) } : {}),
|
|
561
|
+
...(flags['event-type'] ? { eventType: String(flags['event-type']) } : {}),
|
|
562
|
+
...(flags['payload-json']
|
|
563
|
+
? { payload: parseJsonFlag('--payload-json', flags['payload-json']) }
|
|
564
|
+
: {}),
|
|
565
|
+
...(changeRationales.length > 0 ? { changeRationales } : {})
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const data = await apiPost(
|
|
569
|
+
platformUrl,
|
|
570
|
+
bearerToken,
|
|
571
|
+
localSecret,
|
|
572
|
+
organizationId,
|
|
573
|
+
'/api/protocol/update',
|
|
574
|
+
body,
|
|
575
|
+
timeoutMs
|
|
576
|
+
);
|
|
577
|
+
console.log(JSON.stringify(data, null, 2));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// record-change-rationales
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
async function protocolRecordChangeRationales(args) {
|
|
585
|
+
const flags = parseFlags(args);
|
|
586
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
587
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
588
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
589
|
+
|
|
590
|
+
const changeRationales = await resolveChangeRationales(flags);
|
|
591
|
+
if (changeRationales.length === 0) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
'Provide at least one rationale with --change-rationales-json or --change-rationales-file'
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
598
|
+
const timeoutMs = resolveTimeout(flags);
|
|
599
|
+
|
|
600
|
+
const body = {
|
|
601
|
+
sessionKey,
|
|
602
|
+
ticketId,
|
|
603
|
+
changeRationales,
|
|
604
|
+
...(flags['summary-file']
|
|
605
|
+
? { summary: readTextFile(String(flags['summary-file']), '--summary-file') }
|
|
606
|
+
: flags.summary
|
|
607
|
+
? { summary: String(flags.summary) }
|
|
608
|
+
: {}),
|
|
609
|
+
...(flags.phase ? { phase: String(flags.phase) } : {})
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const data = await apiPost(
|
|
613
|
+
platformUrl,
|
|
614
|
+
bearerToken,
|
|
615
|
+
localSecret,
|
|
616
|
+
organizationId,
|
|
617
|
+
'/api/protocol/record-change-rationales',
|
|
618
|
+
body,
|
|
619
|
+
timeoutMs
|
|
620
|
+
);
|
|
621
|
+
console.log(JSON.stringify(data, null, 2));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// ask
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
async function protocolAsk(args) {
|
|
629
|
+
const flags = parseFlags(args);
|
|
630
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
631
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
632
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
633
|
+
const question = flags['question-file']
|
|
634
|
+
? readTextFile(String(flags['question-file']), '--question-file')
|
|
635
|
+
: requireFlag(flags, 'question', undefined);
|
|
636
|
+
|
|
637
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
638
|
+
const timeoutMs = resolveTimeout(flags);
|
|
639
|
+
|
|
640
|
+
const body = {
|
|
641
|
+
sessionKey,
|
|
642
|
+
ticketId,
|
|
643
|
+
question,
|
|
644
|
+
...(flags.phase ? { phase: String(flags.phase) } : {}),
|
|
645
|
+
...(flags['payload-json']
|
|
646
|
+
? { payload: parseJsonFlag('--payload-json', flags['payload-json']) }
|
|
647
|
+
: {})
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const data = await apiPost(
|
|
651
|
+
platformUrl,
|
|
652
|
+
bearerToken,
|
|
653
|
+
localSecret,
|
|
654
|
+
organizationId,
|
|
655
|
+
'/api/protocol/ask',
|
|
656
|
+
body,
|
|
657
|
+
timeoutMs
|
|
658
|
+
);
|
|
659
|
+
console.log(JSON.stringify(data, null, 2));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
// permission-request
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
|
|
666
|
+
async function protocolPermissionRequest(args) {
|
|
667
|
+
const flags = parseFlags(args);
|
|
668
|
+
const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
|
|
669
|
+
const payload = flags['payload-file']
|
|
670
|
+
? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
|
|
671
|
+
: {};
|
|
672
|
+
|
|
673
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
674
|
+
const timeoutMs = resolveTimeout(flags);
|
|
675
|
+
|
|
676
|
+
const data = await apiPost(
|
|
677
|
+
platformUrl,
|
|
678
|
+
bearerToken,
|
|
679
|
+
localSecret,
|
|
680
|
+
organizationId,
|
|
681
|
+
`/api/protocol/permission-request?ticketId=${encodeURIComponent(ticketId)}`,
|
|
682
|
+
payload,
|
|
683
|
+
timeoutMs
|
|
684
|
+
);
|
|
685
|
+
console.log(JSON.stringify(data, null, 2));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// read-context
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
async function protocolReadContext(args) {
|
|
693
|
+
const flags = parseFlags(args);
|
|
694
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
695
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
696
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
697
|
+
|
|
698
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
699
|
+
const timeoutMs = resolveTimeout(flags);
|
|
700
|
+
|
|
701
|
+
const body = {
|
|
702
|
+
sessionKey,
|
|
703
|
+
ticketId,
|
|
704
|
+
...(flags.query ? { query: String(flags.query) } : {}),
|
|
705
|
+
...(flags.limit ? { limit: parseInt(String(flags.limit), 10) } : {})
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const data = await apiPost(
|
|
709
|
+
platformUrl,
|
|
710
|
+
bearerToken,
|
|
711
|
+
localSecret,
|
|
712
|
+
organizationId,
|
|
713
|
+
'/api/protocol/read-context',
|
|
714
|
+
body,
|
|
715
|
+
timeoutMs
|
|
716
|
+
);
|
|
717
|
+
console.log(JSON.stringify(data, null, 2));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
// write-context
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
|
|
724
|
+
async function protocolWriteContext(args) {
|
|
725
|
+
const flags = parseFlags(args);
|
|
726
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
727
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
728
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
729
|
+
const key = requireFlag(flags, 'key', undefined);
|
|
730
|
+
|
|
731
|
+
if (flags.value === undefined) {
|
|
732
|
+
throw new Error('--value is required');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
let value;
|
|
736
|
+
try {
|
|
737
|
+
value = JSON.parse(String(flags.value));
|
|
738
|
+
} catch {
|
|
739
|
+
value = String(flags.value);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
743
|
+
const timeoutMs = resolveTimeout(flags);
|
|
744
|
+
|
|
745
|
+
const body = {
|
|
746
|
+
sessionKey,
|
|
747
|
+
ticketId,
|
|
748
|
+
key,
|
|
749
|
+
value,
|
|
750
|
+
...(flags.tags
|
|
751
|
+
? {
|
|
752
|
+
tags: String(flags.tags)
|
|
753
|
+
.split(',')
|
|
754
|
+
.map(t => t.trim())
|
|
755
|
+
}
|
|
756
|
+
: {})
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const data = await apiPost(
|
|
760
|
+
platformUrl,
|
|
761
|
+
bearerToken,
|
|
762
|
+
localSecret,
|
|
763
|
+
organizationId,
|
|
764
|
+
'/api/protocol/write-context',
|
|
765
|
+
body,
|
|
766
|
+
timeoutMs
|
|
767
|
+
);
|
|
768
|
+
console.log(JSON.stringify(data, null, 2));
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
// deliver
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
|
|
775
|
+
async function protocolDeliver(args) {
|
|
776
|
+
const flags = parseFlags(args);
|
|
777
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
778
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
779
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
780
|
+
const deliverPayload = flags['payload-file']
|
|
781
|
+
? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
|
|
782
|
+
: null;
|
|
783
|
+
const summary =
|
|
784
|
+
deliverPayload?.summary ??
|
|
785
|
+
(flags['summary-file']
|
|
786
|
+
? readTextFile(String(flags['summary-file']), '--summary-file')
|
|
787
|
+
: requireFlag(flags, 'summary', undefined));
|
|
788
|
+
|
|
789
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
790
|
+
const timeoutMs = resolveTimeout(flags);
|
|
791
|
+
|
|
792
|
+
let artifacts = deliverPayload?.artifacts ?? [];
|
|
793
|
+
if (deliverPayload && flags['artifacts-file']) {
|
|
794
|
+
throw new Error('Use either --payload-file or --artifacts-file, not both');
|
|
795
|
+
}
|
|
796
|
+
if (deliverPayload && flags['artifacts-json']) {
|
|
797
|
+
throw new Error('Use either --payload-file or --artifacts-json, not both');
|
|
798
|
+
}
|
|
799
|
+
if (flags['artifacts-file']) {
|
|
800
|
+
artifacts = await readJsonFileOrStdin(String(flags['artifacts-file']), '--artifacts-file');
|
|
801
|
+
} else if (flags['artifacts-json']) {
|
|
802
|
+
artifacts = parseJsonFlag('--artifacts-json', flags['artifacts-json']);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (deliverPayload && (flags['change-rationales-file'] || flags['change-rationales-json'])) {
|
|
806
|
+
throw new Error('Use either --payload-file or change-rationale flags, not both');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const changeRationales =
|
|
810
|
+
deliverPayload?.changeRationales ?? (await resolveChangeRationales(flags));
|
|
811
|
+
validateDeliverFileChanges(flags, changeRationales);
|
|
812
|
+
|
|
813
|
+
const body = {
|
|
814
|
+
sessionKey,
|
|
815
|
+
ticketId,
|
|
816
|
+
summary,
|
|
817
|
+
artifacts,
|
|
818
|
+
...(changeRationales.length > 0 ? { changeRationales } : {})
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const data = await apiPost(
|
|
822
|
+
platformUrl,
|
|
823
|
+
bearerToken,
|
|
824
|
+
localSecret,
|
|
825
|
+
organizationId,
|
|
826
|
+
'/api/protocol/deliver',
|
|
827
|
+
body,
|
|
828
|
+
timeoutMs
|
|
829
|
+
);
|
|
830
|
+
console.log(JSON.stringify(data, null, 2));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// objective attachments
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
async function protocolAttachmentList(args) {
|
|
838
|
+
const flags = parseFlags(args);
|
|
839
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
840
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
841
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
842
|
+
|
|
843
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
844
|
+
const timeoutMs = resolveTimeout(flags);
|
|
845
|
+
|
|
846
|
+
const body = {
|
|
847
|
+
sessionKey,
|
|
848
|
+
ticketId,
|
|
849
|
+
...(flags['objective-id'] ? { objectiveId: String(flags['objective-id']) } : {})
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const data = await apiPost(
|
|
853
|
+
platformUrl,
|
|
854
|
+
bearerToken,
|
|
855
|
+
localSecret,
|
|
856
|
+
organizationId,
|
|
857
|
+
'/api/protocol/attachments/list',
|
|
858
|
+
body,
|
|
859
|
+
timeoutMs
|
|
860
|
+
);
|
|
861
|
+
console.log(JSON.stringify(data, null, 2));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function protocolAttachmentPrepareUpload(args) {
|
|
865
|
+
const flags = parseFlags(args);
|
|
866
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
867
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
868
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
869
|
+
const objectiveId = requireFlag(flags, 'objective-id', undefined);
|
|
870
|
+
const fileName = requireFlag(flags, 'file-name', undefined);
|
|
871
|
+
|
|
872
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
873
|
+
const timeoutMs = resolveTimeout(flags);
|
|
874
|
+
|
|
875
|
+
const body = {
|
|
876
|
+
sessionKey,
|
|
877
|
+
ticketId,
|
|
878
|
+
objectiveId,
|
|
879
|
+
fileName,
|
|
880
|
+
...(flags.label ? { label: String(flags.label) } : {}),
|
|
881
|
+
...(flags['content-type'] ? { contentType: String(flags['content-type']) } : {}),
|
|
882
|
+
...(flags['file-size'] ? { fileSize: parseInt(String(flags['file-size']), 10) } : {}),
|
|
883
|
+
...(flags['metadata-json']
|
|
884
|
+
? { metadata: parseJsonFlag('--metadata-json', flags['metadata-json']) }
|
|
885
|
+
: {})
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const data = await apiPost(
|
|
889
|
+
platformUrl,
|
|
890
|
+
bearerToken,
|
|
891
|
+
localSecret,
|
|
892
|
+
organizationId,
|
|
893
|
+
'/api/protocol/attachments/prepare-upload',
|
|
894
|
+
body,
|
|
895
|
+
timeoutMs
|
|
896
|
+
);
|
|
897
|
+
console.log(JSON.stringify(data, null, 2));
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function protocolAttachmentFinalizeUpload(args) {
|
|
901
|
+
const flags = parseFlags(args);
|
|
902
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
903
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
904
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
905
|
+
const objectiveId = requireFlag(flags, 'objective-id', undefined);
|
|
906
|
+
const storagePath = requireFlag(flags, 'storage-path', undefined);
|
|
907
|
+
const label = requireFlag(flags, 'label', undefined);
|
|
908
|
+
|
|
909
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
910
|
+
const timeoutMs = resolveTimeout(flags);
|
|
911
|
+
|
|
912
|
+
const body = {
|
|
913
|
+
sessionKey,
|
|
914
|
+
ticketId,
|
|
915
|
+
objectiveId,
|
|
916
|
+
storagePath,
|
|
917
|
+
label,
|
|
918
|
+
...(flags['content-type'] ? { contentType: String(flags['content-type']) } : {}),
|
|
919
|
+
...(flags['file-size'] ? { fileSize: parseInt(String(flags['file-size']), 10) } : {}),
|
|
920
|
+
...(flags['metadata-json']
|
|
921
|
+
? { metadata: parseJsonFlag('--metadata-json', flags['metadata-json']) }
|
|
922
|
+
: {})
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const data = await apiPost(
|
|
926
|
+
platformUrl,
|
|
927
|
+
bearerToken,
|
|
928
|
+
localSecret,
|
|
929
|
+
organizationId,
|
|
930
|
+
'/api/protocol/attachments/finalize-upload',
|
|
931
|
+
body,
|
|
932
|
+
timeoutMs
|
|
933
|
+
);
|
|
934
|
+
console.log(JSON.stringify(data, null, 2));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async function protocolAttachmentGetDownloadUrl(args) {
|
|
938
|
+
const flags = parseFlags(args);
|
|
939
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
940
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
941
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
942
|
+
if (!flags['attachment-id'] && !flags['storage-path']) {
|
|
943
|
+
throw new Error('--attachment-id or --storage-path is required');
|
|
944
|
+
}
|
|
945
|
+
if (flags['storage-path'] && !flags['objective-id']) {
|
|
946
|
+
throw new Error('--objective-id is required when using --storage-path');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
950
|
+
const timeoutMs = resolveTimeout(flags);
|
|
951
|
+
|
|
952
|
+
const body = {
|
|
953
|
+
sessionKey,
|
|
954
|
+
ticketId,
|
|
955
|
+
...(flags['objective-id'] ? { objectiveId: String(flags['objective-id']) } : {}),
|
|
956
|
+
...(flags['attachment-id'] ? { attachmentId: String(flags['attachment-id']) } : {}),
|
|
957
|
+
...(flags['storage-path'] ? { storagePath: String(flags['storage-path']) } : {}),
|
|
958
|
+
...(flags['expires-in'] ? { expiresIn: parseInt(String(flags['expires-in']), 10) } : {})
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
const data = await apiPost(
|
|
962
|
+
platformUrl,
|
|
963
|
+
bearerToken,
|
|
964
|
+
localSecret,
|
|
965
|
+
organizationId,
|
|
966
|
+
'/api/protocol/attachments/get-download-url',
|
|
967
|
+
body,
|
|
968
|
+
timeoutMs
|
|
969
|
+
);
|
|
970
|
+
console.log(JSON.stringify(data, null, 2));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function protocolAttachmentUploadFile(args) {
|
|
974
|
+
const flags = parseFlags(args);
|
|
975
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
976
|
+
if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
|
|
977
|
+
if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
|
|
978
|
+
const objectiveId = requireFlag(flags, 'objective-id', undefined);
|
|
979
|
+
const filePath = requireFlag(flags, 'file', undefined);
|
|
980
|
+
|
|
981
|
+
const { readFile, stat } = await import('node:fs/promises');
|
|
982
|
+
const path = await import('node:path');
|
|
983
|
+
const fileName = String(flags['file-name'] ?? path.basename(filePath));
|
|
984
|
+
const contentType = String(flags['content-type'] ?? 'application/octet-stream');
|
|
985
|
+
const label = String(flags.label ?? fileName);
|
|
986
|
+
|
|
987
|
+
const fileStats = await stat(filePath);
|
|
988
|
+
const fileBytes = await readFile(filePath);
|
|
989
|
+
|
|
990
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
991
|
+
const timeoutMs = resolveTimeout(flags);
|
|
992
|
+
|
|
993
|
+
const metadata = flags['metadata-json']
|
|
994
|
+
? parseJsonFlag('--metadata-json', flags['metadata-json'])
|
|
995
|
+
: undefined;
|
|
996
|
+
|
|
997
|
+
const prepared = await apiPost(
|
|
998
|
+
platformUrl,
|
|
999
|
+
bearerToken,
|
|
1000
|
+
localSecret,
|
|
1001
|
+
organizationId,
|
|
1002
|
+
'/api/protocol/attachments/prepare-upload',
|
|
1003
|
+
{
|
|
1004
|
+
sessionKey,
|
|
1005
|
+
ticketId,
|
|
1006
|
+
objectiveId,
|
|
1007
|
+
fileName,
|
|
1008
|
+
label,
|
|
1009
|
+
contentType,
|
|
1010
|
+
fileSize: fileStats.size,
|
|
1011
|
+
...(metadata ? { metadata } : {})
|
|
1012
|
+
},
|
|
1013
|
+
timeoutMs
|
|
1014
|
+
);
|
|
1015
|
+
|
|
1016
|
+
const uploadUrl = prepared?.upload?.url;
|
|
1017
|
+
const storagePath = prepared?.draft?.storagePath;
|
|
1018
|
+
if (!uploadUrl || !storagePath) {
|
|
1019
|
+
throw new Error('Prepare upload response missing upload URL or storagePath.');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
await uploadToSignedUrl(uploadUrl, fileBytes, contentType, timeoutMs);
|
|
1023
|
+
|
|
1024
|
+
const finalized = await apiPost(
|
|
1025
|
+
platformUrl,
|
|
1026
|
+
bearerToken,
|
|
1027
|
+
localSecret,
|
|
1028
|
+
organizationId,
|
|
1029
|
+
'/api/protocol/attachments/finalize-upload',
|
|
1030
|
+
{
|
|
1031
|
+
sessionKey,
|
|
1032
|
+
ticketId,
|
|
1033
|
+
objectiveId,
|
|
1034
|
+
storagePath,
|
|
1035
|
+
label,
|
|
1036
|
+
contentType,
|
|
1037
|
+
fileSize: fileStats.size,
|
|
1038
|
+
...(metadata ? { metadata } : {})
|
|
1039
|
+
},
|
|
1040
|
+
timeoutMs
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
console.log(JSON.stringify(finalized, null, 2));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ---------------------------------------------------------------------------
|
|
1047
|
+
// discover-project (resolve project from working directory)
|
|
1048
|
+
// ---------------------------------------------------------------------------
|
|
1049
|
+
|
|
1050
|
+
async function protocolDiscoverProject(args) {
|
|
1051
|
+
const flags = parseFlags(args);
|
|
1052
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
1053
|
+
const timeoutMs = resolveTimeout(flags);
|
|
1054
|
+
|
|
1055
|
+
const workingDirectory = String(flags['working-directory'] ?? process.cwd());
|
|
1056
|
+
|
|
1057
|
+
const data = await apiPost(
|
|
1058
|
+
platformUrl,
|
|
1059
|
+
bearerToken,
|
|
1060
|
+
localSecret,
|
|
1061
|
+
organizationId,
|
|
1062
|
+
'/api/protocol/discover-project',
|
|
1063
|
+
{ workingDirectory },
|
|
1064
|
+
timeoutMs
|
|
1065
|
+
);
|
|
1066
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1067
|
+
|
|
1068
|
+
if (data.project?.id) {
|
|
1069
|
+
process.stderr.write(`\nPROJECT_ID=${data.project.id}\n`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// ---------------------------------------------------------------------------
|
|
1074
|
+
// connect (lightweight session, no context returned)
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
|
|
1077
|
+
async function protocolConnect(args) {
|
|
1078
|
+
const flags = parseFlags(args);
|
|
1079
|
+
const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
|
|
1080
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
1081
|
+
const timeoutMs = resolveTimeout(flags);
|
|
1082
|
+
|
|
1083
|
+
const body = {
|
|
1084
|
+
ticketId,
|
|
1085
|
+
agentIdentifier: resolveProtocolAgentIdentifier(flags),
|
|
1086
|
+
connectionMethod: String(flags.method ?? 'cli'),
|
|
1087
|
+
metadata: resolveProtocolMetadata(flags, { cwd: process.cwd() })
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const data = await apiPost(
|
|
1091
|
+
platformUrl,
|
|
1092
|
+
bearerToken,
|
|
1093
|
+
localSecret,
|
|
1094
|
+
organizationId,
|
|
1095
|
+
'/api/protocol/connect',
|
|
1096
|
+
body,
|
|
1097
|
+
timeoutMs
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
const sessionKey = data.session?.sessionKey;
|
|
1101
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1102
|
+
|
|
1103
|
+
if (sessionKey) {
|
|
1104
|
+
process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// ---------------------------------------------------------------------------
|
|
1109
|
+
// load-context (read-only ticket fetch, no session)
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
|
|
1112
|
+
async function protocolLoadContext(args) {
|
|
1113
|
+
const flags = parseFlags(args);
|
|
1114
|
+
const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
|
|
1115
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
1116
|
+
const timeoutMs = resolveTimeout(flags);
|
|
1117
|
+
|
|
1118
|
+
const body = { ticketId };
|
|
1119
|
+
|
|
1120
|
+
const data = await apiPost(
|
|
1121
|
+
platformUrl,
|
|
1122
|
+
bearerToken,
|
|
1123
|
+
localSecret,
|
|
1124
|
+
organizationId,
|
|
1125
|
+
'/api/protocol/load-context',
|
|
1126
|
+
body,
|
|
1127
|
+
timeoutMs
|
|
1128
|
+
);
|
|
1129
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ---------------------------------------------------------------------------
|
|
1133
|
+
// prompt (create ticket + connect in one call)
|
|
1134
|
+
// ---------------------------------------------------------------------------
|
|
1135
|
+
|
|
1136
|
+
async function protocolPrompt(args) {
|
|
1137
|
+
const flags = parseFlags(args);
|
|
1138
|
+
const objective = requireFlag(flags, 'objective', undefined);
|
|
1139
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
1140
|
+
const timeoutMs = resolveTimeout(flags);
|
|
1141
|
+
const agentIdentifier = resolveProtocolAgentIdentifier(flags);
|
|
1142
|
+
const modelIdentifier = resolveProtocolModelIdentifier(flags);
|
|
1143
|
+
|
|
1144
|
+
// When --project-id is not provided, auto-send cwd as workingDirectory so
|
|
1145
|
+
// the server can resolve the project from the caller's project_user
|
|
1146
|
+
// local_working_directory setting.
|
|
1147
|
+
const personal = Boolean(flags.personal);
|
|
1148
|
+
const workingDirectory =
|
|
1149
|
+
flags['working-directory'] ?? (!flags['project-id'] && !personal ? process.cwd() : undefined);
|
|
1150
|
+
|
|
1151
|
+
const body = {
|
|
1152
|
+
objective,
|
|
1153
|
+
agentIdentifier,
|
|
1154
|
+
connectionMethod: String(flags.method ?? 'cli'),
|
|
1155
|
+
metadata: resolveProtocolMetadata(flags, { cwd: process.cwd() }),
|
|
1156
|
+
...(flags.title ? { title: String(flags.title) } : {}),
|
|
1157
|
+
...(flags.priority ? { priority: String(flags.priority) } : {}),
|
|
1158
|
+
...(flags['project-id'] ? { projectId: String(flags['project-id']) } : {}),
|
|
1159
|
+
...(personal ? { personal: true } : {}),
|
|
1160
|
+
...(workingDirectory ? { workingDirectory: String(workingDirectory) } : {}),
|
|
1161
|
+
...(flags['acceptance-criteria']
|
|
1162
|
+
? { acceptanceCriteria: String(flags['acceptance-criteria']) }
|
|
1163
|
+
: {}),
|
|
1164
|
+
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
1165
|
+
...(flags['execution-target'] ? { executionTarget: String(flags['execution-target']) } : {}),
|
|
1166
|
+
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier),
|
|
1167
|
+
...(flags['parent-session-key']
|
|
1168
|
+
? { parentSessionKey: String(flags['parent-session-key']) }
|
|
1169
|
+
: {}),
|
|
1170
|
+
...(flags['parent-ticket-id']
|
|
1171
|
+
? { parentTicketId: String(flags['parent-ticket-id'] ?? process.env.TICKET_ID ?? '') }
|
|
1172
|
+
: {})
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const data = await apiPost(
|
|
1176
|
+
platformUrl,
|
|
1177
|
+
bearerToken,
|
|
1178
|
+
localSecret,
|
|
1179
|
+
organizationId,
|
|
1180
|
+
'/api/protocol/prompt',
|
|
1181
|
+
body,
|
|
1182
|
+
timeoutMs
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
const sessionKey = data.session?.sessionKey;
|
|
1186
|
+
const ticketId = data.ticket?.ticket_id ?? data.ticket?.id;
|
|
1187
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1188
|
+
|
|
1189
|
+
if (sessionKey) {
|
|
1190
|
+
process.stderr.write(`\nSESSION_KEY=${sessionKey}\n`);
|
|
1191
|
+
}
|
|
1192
|
+
if (ticketId) {
|
|
1193
|
+
process.stderr.write(`TICKET_ID=${ticketId}\n`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// ---------------------------------------------------------------------------
|
|
1198
|
+
// create (create follow-up ticket draft only)
|
|
1199
|
+
// ---------------------------------------------------------------------------
|
|
1200
|
+
|
|
1201
|
+
async function protocolCreateTicket(args) {
|
|
1202
|
+
const flags = parseFlags(args);
|
|
1203
|
+
const { sessionKey, ticketId } = resolveSessionFlags(flags);
|
|
1204
|
+
const objective = requireFlag(flags, 'objective', undefined);
|
|
1205
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
1206
|
+
const timeoutMs = resolveTimeout(flags);
|
|
1207
|
+
const agentIdentifier = resolveProtocolAgentIdentifier(flags);
|
|
1208
|
+
const modelIdentifier = resolveProtocolModelIdentifier(flags);
|
|
1209
|
+
|
|
1210
|
+
const hasSessionContext = Boolean(sessionKey && ticketId);
|
|
1211
|
+
|
|
1212
|
+
// Follow-up mode: create a draft ticket linked to the current session ticket.
|
|
1213
|
+
if (hasSessionContext) {
|
|
1214
|
+
const body = {
|
|
1215
|
+
sessionKey,
|
|
1216
|
+
ticketId,
|
|
1217
|
+
objective,
|
|
1218
|
+
...(flags.title ? { title: String(flags.title) } : {}),
|
|
1219
|
+
...(flags.priority ? { priority: String(flags.priority) } : {}),
|
|
1220
|
+
...(flags['acceptance-criteria']
|
|
1221
|
+
? { acceptanceCriteria: String(flags['acceptance-criteria']) }
|
|
1222
|
+
: {}),
|
|
1223
|
+
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
1224
|
+
...(flags['execution-target'] ? { executionTarget: String(flags['execution-target']) } : {}),
|
|
1225
|
+
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier)
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
const data = await apiPost(
|
|
1229
|
+
platformUrl,
|
|
1230
|
+
bearerToken,
|
|
1231
|
+
localSecret,
|
|
1232
|
+
organizationId,
|
|
1233
|
+
'/api/protocol/create-ticket',
|
|
1234
|
+
body,
|
|
1235
|
+
timeoutMs
|
|
1236
|
+
);
|
|
1237
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Standalone mode: resolve project from cwd/--working-directory first, then create draft ticket.
|
|
1242
|
+
if (sessionKey || ticketId) {
|
|
1243
|
+
throw new Error(
|
|
1244
|
+
'Provide both --session-key and --ticket-id for follow-up create, or provide neither for standalone create.'
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const standaloneWorkingDirectory =
|
|
1249
|
+
!flags.personal && !flags['project-id']
|
|
1250
|
+
? String(flags['working-directory'] ?? process.cwd())
|
|
1251
|
+
: undefined;
|
|
1252
|
+
|
|
1253
|
+
const standaloneBody = {
|
|
1254
|
+
objective,
|
|
1255
|
+
...(flags.personal ? { personal: true } : {}),
|
|
1256
|
+
...(!flags.personal && flags['project-id'] ? { projectId: String(flags['project-id']) } : {}),
|
|
1257
|
+
...(standaloneWorkingDirectory ? { workingDirectory: standaloneWorkingDirectory } : {}),
|
|
1258
|
+
...(flags.title ? { title: String(flags.title) } : {}),
|
|
1259
|
+
...(flags.priority ? { priority: String(flags.priority) } : {}),
|
|
1260
|
+
...(flags['acceptance-criteria']
|
|
1261
|
+
? { acceptanceCriteria: String(flags['acceptance-criteria']) }
|
|
1262
|
+
: {}),
|
|
1263
|
+
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
1264
|
+
...(flags['execution-target'] ? { executionTarget: String(flags['execution-target']) } : {}),
|
|
1265
|
+
delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier)
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
const data = await apiPost(
|
|
1269
|
+
platformUrl,
|
|
1270
|
+
bearerToken,
|
|
1271
|
+
localSecret,
|
|
1272
|
+
organizationId,
|
|
1273
|
+
'/api/protocol/tickets',
|
|
1274
|
+
standaloneBody,
|
|
1275
|
+
timeoutMs
|
|
1276
|
+
);
|
|
1277
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ---------------------------------------------------------------------------
|
|
1281
|
+
// search-tickets (find tickets by query/status/project/created_by/dates)
|
|
1282
|
+
// ---------------------------------------------------------------------------
|
|
1283
|
+
|
|
1284
|
+
async function protocolSearchTickets(args) {
|
|
1285
|
+
const flags = parseFlags(args);
|
|
1286
|
+
const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
|
|
1287
|
+
const timeoutMs = resolveTimeout(flags);
|
|
1288
|
+
|
|
1289
|
+
const statuses = flags.status
|
|
1290
|
+
? String(flags.status)
|
|
1291
|
+
.split(',')
|
|
1292
|
+
.map(s => s.trim())
|
|
1293
|
+
.filter(Boolean)
|
|
1294
|
+
: undefined;
|
|
1295
|
+
|
|
1296
|
+
const body = {
|
|
1297
|
+
...(flags.query ? { query: String(flags.query) } : {}),
|
|
1298
|
+
...(statuses?.length ? { statuses } : {}),
|
|
1299
|
+
...(flags['include-completed'] !== undefined
|
|
1300
|
+
? {
|
|
1301
|
+
includeCompleted:
|
|
1302
|
+
flags['include-completed'] !== false && flags['include-completed'] !== 'false'
|
|
1303
|
+
}
|
|
1304
|
+
: {}),
|
|
1305
|
+
...(flags.limit ? { limit: parseInt(String(flags.limit), 10) } : {}),
|
|
1306
|
+
...(flags['project-id'] ? { projectId: String(flags['project-id']) } : {}),
|
|
1307
|
+
...(flags['created-by'] ? { createdBy: String(flags['created-by']) } : {}),
|
|
1308
|
+
...(flags['updated-after'] ? { updatedAfter: String(flags['updated-after']) } : {}),
|
|
1309
|
+
...(flags['updated-before'] ? { updatedBefore: String(flags['updated-before']) } : {})
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
const data = await apiPost(
|
|
1313
|
+
platformUrl,
|
|
1314
|
+
bearerToken,
|
|
1315
|
+
localSecret,
|
|
1316
|
+
organizationId,
|
|
1317
|
+
'/api/protocol/search-tickets',
|
|
1318
|
+
body,
|
|
1319
|
+
timeoutMs
|
|
1320
|
+
);
|
|
1321
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ---------------------------------------------------------------------------
|
|
1325
|
+
// auth-status (agent-friendly auth diagnostics)
|
|
1326
|
+
// ---------------------------------------------------------------------------
|
|
1327
|
+
|
|
1328
|
+
async function protocolAuthStatus() {
|
|
1329
|
+
const status = await getAuthStatus();
|
|
1330
|
+
|
|
1331
|
+
console.log(
|
|
1332
|
+
JSON.stringify(
|
|
1333
|
+
{
|
|
1334
|
+
ok: status.isLoggedIn,
|
|
1335
|
+
authStatus: {
|
|
1336
|
+
isLoggedIn: status.isLoggedIn,
|
|
1337
|
+
platformUrl: status.platformUrl,
|
|
1338
|
+
platformUrlSource: status.platformUrlSource,
|
|
1339
|
+
tokenSource: status.tokenSource,
|
|
1340
|
+
tokenPresent: status.tokenPresent,
|
|
1341
|
+
organizationId: status.organizationId,
|
|
1342
|
+
authMode: status.authMode,
|
|
1343
|
+
error: status.error,
|
|
1344
|
+
hasLocalSecret: status.hasLocalSecret,
|
|
1345
|
+
credentialsFileExists: status.credentialsFileExists,
|
|
1346
|
+
electronCredentialsFileExists: status.electronCredentialsFileExists
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1349
|
+
null,
|
|
1350
|
+
2
|
|
1351
|
+
)
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// ---------------------------------------------------------------------------
|
|
1356
|
+
// Router
|
|
1357
|
+
// ---------------------------------------------------------------------------
|
|
1358
|
+
|
|
1359
|
+
export async function runProtocolCommand(subcommand, args) {
|
|
1360
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help') {
|
|
1361
|
+
console.log(`ovld protocol <subcommand> [flags]
|
|
1362
|
+
|
|
1363
|
+
Use this for ticket lifecycle work from an agent runtime: create a standalone
|
|
1364
|
+
draft with \`ovld protocol create\`, create-and-attach with \`ovld protocol prompt\`,
|
|
1365
|
+
or attach to an existing ticket with \`ovld protocol attach --ticket-id <ticket_id>\`.
|
|
1366
|
+
|
|
1367
|
+
Project discovery:
|
|
1368
|
+
When prompting or creating tickets, the CLI automatically resolves the correct
|
|
1369
|
+
project by matching your current working directory against your configured
|
|
1370
|
+
"Local working directory" for that project (stored per user in Overlord).
|
|
1371
|
+
You can also discover the project explicitly:
|
|
1372
|
+
|
|
1373
|
+
ovld protocol discover-project
|
|
1374
|
+
ovld protocol discover-project --working-directory /path/to/repo
|
|
1375
|
+
|
|
1376
|
+
Use --project-id to override automatic resolution on prompt or ticket creation.
|
|
1377
|
+
Use --personal to create a private ticket without assigning any project.
|
|
1378
|
+
|
|
1379
|
+
Subcommands:
|
|
1380
|
+
auth-status Return machine-readable auth status for agent runtimes
|
|
1381
|
+
discover-project Resolve a project from the current working directory
|
|
1382
|
+
attach Start a ticket session and return full working context
|
|
1383
|
+
connect Start a lightweight session without full context
|
|
1384
|
+
load-context Read ticket context without creating a session
|
|
1385
|
+
search-tickets Find tickets by keyword, status, project, creator, or update date
|
|
1386
|
+
create Create a draft ticket without attaching (follow-up or standalone)
|
|
1387
|
+
prompt Create a ticket and attach to it immediately
|
|
1388
|
+
update Post progress, activity events, and optional change rationales
|
|
1389
|
+
record-change-rationales Persist structured change rationales without a progress update
|
|
1390
|
+
ask Post a blocking question and move the ticket to review
|
|
1391
|
+
permission-request Notify Overlord that the agent is requesting tool permission
|
|
1392
|
+
read-context Read shared persistent context for this ticket
|
|
1393
|
+
write-context Write shared persistent context for future sessions
|
|
1394
|
+
deliver Finish work, send artifacts, and move the ticket to review
|
|
1395
|
+
attachment-list List objective attachments visible to the current session
|
|
1396
|
+
attachment-prepare-upload Get a signed upload URL for an objective attachment
|
|
1397
|
+
attachment-finalize-upload Finalize an uploaded attachment row after storage upload
|
|
1398
|
+
attachment-download-url Get a signed download URL for an existing attachment
|
|
1399
|
+
attachment-upload-file Prepare, upload, and finalize a local file in one command
|
|
1400
|
+
|
|
1401
|
+
Environment fallback:
|
|
1402
|
+
--session-key <- SESSION_KEY
|
|
1403
|
+
--ticket-id <- TICKET_ID (human-readable ticket_id, e.g. 1:899)
|
|
1404
|
+
auth/host <- OVERLORD_URL, optional OVERLORD_ACCESS_TOKEN + OVERLORD_ORGANIZATION_ID, or shared OAuth credentials from ovld auth/Desktop login
|
|
1405
|
+
--timeout <- OVERLORD_TIMEOUT
|
|
1406
|
+
|
|
1407
|
+
Common flags:
|
|
1408
|
+
--timeout <ms> Request timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS})
|
|
1409
|
+
--ticket-id <ticket_id> Ticket identifier (e.g. 1:899) when the subcommand operates on an existing ticket
|
|
1410
|
+
--session-key <key> Session key returned by attach/connect/prompt
|
|
1411
|
+
--agent <identifier> Agent identifier sent to Overlord (default: AGENT_IDENTIFIER or claude-code)
|
|
1412
|
+
--model <identifier> Model identifier to snapshot on executing objectives
|
|
1413
|
+
--method <connectionMethod> Connection method sent to Overlord (default: cli)
|
|
1414
|
+
|
|
1415
|
+
auth-status:
|
|
1416
|
+
Purpose:
|
|
1417
|
+
Check whether the local runtime has usable Overlord credentials.
|
|
1418
|
+
Returns:
|
|
1419
|
+
JSON with ok=true|false plus authStatus fields describing token and host sources.
|
|
1420
|
+
|
|
1421
|
+
discover-project:
|
|
1422
|
+
Purpose:
|
|
1423
|
+
Resolve the Overlord project that corresponds to the current (or given) working directory.
|
|
1424
|
+
Uses the caller's configured "Local working directory" for matching.
|
|
1425
|
+
Optional:
|
|
1426
|
+
--working-directory <path> Directory to match (default: current working directory)
|
|
1427
|
+
Returns:
|
|
1428
|
+
Project JSON with id, name, organizationId. Prints PROJECT_ID=<id> on stderr.
|
|
1429
|
+
Notes:
|
|
1430
|
+
Set your local working directory for a project in the Overlord UI under Project Settings.
|
|
1431
|
+
When no match is found, returns a 404 with a hint.
|
|
1432
|
+
|
|
1433
|
+
attach:
|
|
1434
|
+
Purpose:
|
|
1435
|
+
Create the working session for an agent on an existing ticket. This is the normal first call.
|
|
1436
|
+
Required:
|
|
1437
|
+
--ticket-id <ticket_id> Human-readable identifier (e.g. 1:899). Also accepts UUID.
|
|
1438
|
+
Optional:
|
|
1439
|
+
--agent <identifier>
|
|
1440
|
+
--model <identifier>
|
|
1441
|
+
--method <connectionMethod>
|
|
1442
|
+
--external-session-id <id|null> Store the native agent thread/session id, or clear it with null
|
|
1443
|
+
--metadata-json <json> Extra session metadata object
|
|
1444
|
+
Returns:
|
|
1445
|
+
Full JSON including session.sessionKey, ticket, history, artifacts, sharedState, and promptContext
|
|
1446
|
+
Notes:
|
|
1447
|
+
If --external-session-id is omitted, the CLI may auto-detect Codex or Claude session ids
|
|
1448
|
+
|
|
1449
|
+
connect:
|
|
1450
|
+
Purpose:
|
|
1451
|
+
Create a lightweight session when you only need a session key and not the full ticket payload
|
|
1452
|
+
Required:
|
|
1453
|
+
--ticket-id <ticket_id> Human-readable identifier (e.g. 1:899). Also accepts UUID.
|
|
1454
|
+
Optional:
|
|
1455
|
+
--agent <identifier>
|
|
1456
|
+
--model <identifier>
|
|
1457
|
+
--method <connectionMethod>
|
|
1458
|
+
--metadata-json <json> Extra session metadata object
|
|
1459
|
+
Returns:
|
|
1460
|
+
Session JSON and SESSION_KEY on stderr when available
|
|
1461
|
+
|
|
1462
|
+
load-context:
|
|
1463
|
+
Purpose:
|
|
1464
|
+
Read ticket details without creating a session
|
|
1465
|
+
Required:
|
|
1466
|
+
--ticket-id <id>
|
|
1467
|
+
|
|
1468
|
+
search-tickets:
|
|
1469
|
+
Purpose:
|
|
1470
|
+
Find tickets in your organization by keyword, status, project, creator, or update window.
|
|
1471
|
+
Omit --query to list mode (most recently updated first).
|
|
1472
|
+
Optional:
|
|
1473
|
+
--query <text> Free-text search across the ticket search vector + title fallback
|
|
1474
|
+
--status <csv> Comma-separated statuses, e.g. "draft,next-up,execute"
|
|
1475
|
+
--include-completed <bool> Include completed tickets (default: false)
|
|
1476
|
+
--limit <n> Max results 1..50 (default: 8)
|
|
1477
|
+
--project-id <uuid> Restrict to a single project
|
|
1478
|
+
--created-by <uuid> Restrict to tickets created by this user
|
|
1479
|
+
--updated-after <iso> Updated_at >= ISO timestamp
|
|
1480
|
+
--updated-before <iso> Updated_at <= ISO timestamp
|
|
1481
|
+
Returns:
|
|
1482
|
+
JSON with { tickets, count }.
|
|
1483
|
+
|
|
1484
|
+
update:
|
|
1485
|
+
Purpose:
|
|
1486
|
+
Post progress or activity events during execution
|
|
1487
|
+
Required:
|
|
1488
|
+
--session-key <key>
|
|
1489
|
+
--ticket-id <id>
|
|
1490
|
+
--summary <text> or --summary-file <path>
|
|
1491
|
+
Optional:
|
|
1492
|
+
--phase <status> draft | execute | review | deliver | complete | blocked | cancelled
|
|
1493
|
+
--event-type <type> update | user_follow_up | alert
|
|
1494
|
+
--payload-json <json> Additional structured payload, for example notifications
|
|
1495
|
+
--external-url <url|null> Store or clear a deep link to the live agent session
|
|
1496
|
+
--external-session-id <id|null>
|
|
1497
|
+
--change-rationales-json <json>
|
|
1498
|
+
--change-rationales-file <path>
|
|
1499
|
+
Notes:
|
|
1500
|
+
Use phase=execute while actively working. user_follow_up is for verbatim human follow-up messages.
|
|
1501
|
+
|
|
1502
|
+
record-change-rationales:
|
|
1503
|
+
Purpose:
|
|
1504
|
+
Persist structured file-change rationale records without also posting a normal update
|
|
1505
|
+
Required:
|
|
1506
|
+
--session-key <key>
|
|
1507
|
+
--ticket-id <id>
|
|
1508
|
+
--change-rationales-json <json> or --change-rationales-file <path>
|
|
1509
|
+
Optional:
|
|
1510
|
+
--summary <text> or --summary-file <path>
|
|
1511
|
+
--phase <status>
|
|
1512
|
+
|
|
1513
|
+
ask:
|
|
1514
|
+
Purpose:
|
|
1515
|
+
Raise a blocking question for a human reviewer/PM
|
|
1516
|
+
Required:
|
|
1517
|
+
--session-key <key>
|
|
1518
|
+
--ticket-id <id>
|
|
1519
|
+
--question <text> or --question-file <path>
|
|
1520
|
+
Optional:
|
|
1521
|
+
--phase <status>
|
|
1522
|
+
--payload-json <json>
|
|
1523
|
+
Notes:
|
|
1524
|
+
After ask succeeds, stop working until the human responds
|
|
1525
|
+
|
|
1526
|
+
permission-request:
|
|
1527
|
+
Purpose:
|
|
1528
|
+
Notify Overlord that the local agent runtime is requesting tool permission.
|
|
1529
|
+
This is primarily used by installed permission hooks.
|
|
1530
|
+
Required:
|
|
1531
|
+
--ticket-id <id>
|
|
1532
|
+
Optional:
|
|
1533
|
+
--payload-file <path|-> Hook JSON payload, or stdin when "-"
|
|
1534
|
+
|
|
1535
|
+
read-context:
|
|
1536
|
+
Purpose:
|
|
1537
|
+
Read persistent shared context written by earlier sessions
|
|
1538
|
+
Required:
|
|
1539
|
+
--session-key <key>
|
|
1540
|
+
--ticket-id <id>
|
|
1541
|
+
Optional:
|
|
1542
|
+
--query <text> Filter by key substring
|
|
1543
|
+
--limit <n> Max entries to return
|
|
1544
|
+
|
|
1545
|
+
write-context:
|
|
1546
|
+
Purpose:
|
|
1547
|
+
Save shared facts for future sessions
|
|
1548
|
+
Required:
|
|
1549
|
+
--session-key <key>
|
|
1550
|
+
--ticket-id <id>
|
|
1551
|
+
--key <name>
|
|
1552
|
+
--value <json-or-string> Parsed as JSON first; stored as a string if JSON parsing fails
|
|
1553
|
+
Optional:
|
|
1554
|
+
--tags <csv>
|
|
1555
|
+
|
|
1556
|
+
deliver:
|
|
1557
|
+
Purpose:
|
|
1558
|
+
Conclude the session and submit the final narrative plus artifacts/change rationales
|
|
1559
|
+
Required:
|
|
1560
|
+
--session-key <key>
|
|
1561
|
+
--ticket-id <id>
|
|
1562
|
+
--summary <text> or --summary-file <path>
|
|
1563
|
+
or: --payload-file <path|-> containing { summary, artifacts, changeRationales }
|
|
1564
|
+
Optional:
|
|
1565
|
+
--artifacts-json <json>
|
|
1566
|
+
--artifacts-file <path|->
|
|
1567
|
+
--change-rationales-json <json>
|
|
1568
|
+
--change-rationales-file <path|->
|
|
1569
|
+
--skip-file-change-check Bypass local git vs changeRationales validation
|
|
1570
|
+
Notes:
|
|
1571
|
+
Use --payload-file - to read the full delivery JSON from stdin without creating a scratch file.
|
|
1572
|
+
Do not combine --payload-file with --artifacts-json/--artifacts-file or change-rationale flags.
|
|
1573
|
+
In a git workspace, deliver validates that changed files are represented by changeRationales unless skipped.
|
|
1574
|
+
|
|
1575
|
+
prompt:
|
|
1576
|
+
Purpose:
|
|
1577
|
+
Create a ticket and attach to it in one call.
|
|
1578
|
+
When --project-id is omitted, automatically resolves the project from the
|
|
1579
|
+
current working directory (matching against the caller's project_user.local_working_directory).
|
|
1580
|
+
Required:
|
|
1581
|
+
--objective <text>
|
|
1582
|
+
Optional:
|
|
1583
|
+
--title <text>
|
|
1584
|
+
--priority <level> low | medium | high | urgent
|
|
1585
|
+
--project-id <id> Explicit project; skips working-directory resolution
|
|
1586
|
+
--personal Create the ticket without assigning a project
|
|
1587
|
+
--working-directory <path> Override cwd for project resolution (default: cwd)
|
|
1588
|
+
--acceptance-criteria <text>
|
|
1589
|
+
--available-tools <text>
|
|
1590
|
+
--execution-target <t> agent | human
|
|
1591
|
+
--delegate <model> Model or delegate identifier that created the ticket
|
|
1592
|
+
--parent-session-key <key>
|
|
1593
|
+
--parent-ticket-id <id>
|
|
1594
|
+
--agent <identifier>
|
|
1595
|
+
--model <identifier>
|
|
1596
|
+
--method <connectionMethod>
|
|
1597
|
+
--metadata-json <json> Extra session metadata object
|
|
1598
|
+
Returns:
|
|
1599
|
+
New ticket/session JSON plus SESSION_KEY and TICKET_ID on stderr when available
|
|
1600
|
+
|
|
1601
|
+
create:
|
|
1602
|
+
Purpose:
|
|
1603
|
+
Create a draft ticket without attaching to it.
|
|
1604
|
+
If session flags are provided, creates a follow-up draft linked to the current ticket.
|
|
1605
|
+
If session flags are omitted, resolves project by working directory and creates a standalone draft.
|
|
1606
|
+
Required:
|
|
1607
|
+
--objective <text>
|
|
1608
|
+
Optional:
|
|
1609
|
+
--session-key <key>
|
|
1610
|
+
--ticket-id <id>
|
|
1611
|
+
--working-directory <path> Resolve project by your configured local working directory (default: cwd)
|
|
1612
|
+
--project-id <id> Explicit project for standalone draft creation
|
|
1613
|
+
--personal Create a private standalone draft without a project
|
|
1614
|
+
--title <text>
|
|
1615
|
+
--priority <level> low | medium | high | urgent
|
|
1616
|
+
--acceptance-criteria <text>
|
|
1617
|
+
--available-tools <text>
|
|
1618
|
+
--execution-target <t> agent | human
|
|
1619
|
+
--delegate <model> Model or delegate identifier that created the ticket
|
|
1620
|
+
--agent <identifier>
|
|
1621
|
+
--model <identifier>
|
|
1622
|
+
Returns:
|
|
1623
|
+
New draft ticket JSON (follow-up draft when session flags are provided)
|
|
1624
|
+
Notes:
|
|
1625
|
+
Standalone create auto-discovers the project from the current working directory unless --personal is set.
|
|
1626
|
+
Follow-up create requires both --session-key and --ticket-id.
|
|
1627
|
+
|
|
1628
|
+
attachment-list:
|
|
1629
|
+
Required:
|
|
1630
|
+
--session-key <key>
|
|
1631
|
+
--ticket-id <id>
|
|
1632
|
+
Optional:
|
|
1633
|
+
--objective-id <id> Filter to a single objective
|
|
1634
|
+
Returns:
|
|
1635
|
+
JSON array of { id, label, content_type, file_size, objective_id, storage_path, created_at }
|
|
1636
|
+
|
|
1637
|
+
attachment-prepare-upload:
|
|
1638
|
+
Required:
|
|
1639
|
+
--session-key <key>
|
|
1640
|
+
--ticket-id <id>
|
|
1641
|
+
--objective-id <id>
|
|
1642
|
+
--file-name <name>
|
|
1643
|
+
Optional:
|
|
1644
|
+
--label <text>
|
|
1645
|
+
--content-type <mime>
|
|
1646
|
+
--file-size <bytes>
|
|
1647
|
+
--metadata-json <json>
|
|
1648
|
+
|
|
1649
|
+
attachment-finalize-upload:
|
|
1650
|
+
Required:
|
|
1651
|
+
--session-key <key>
|
|
1652
|
+
--ticket-id <id>
|
|
1653
|
+
--objective-id <id>
|
|
1654
|
+
--storage-path <path>
|
|
1655
|
+
--label <text>
|
|
1656
|
+
Optional:
|
|
1657
|
+
--content-type <mime>
|
|
1658
|
+
--file-size <bytes>
|
|
1659
|
+
--metadata-json <json>
|
|
1660
|
+
|
|
1661
|
+
attachment-download-url:
|
|
1662
|
+
Required:
|
|
1663
|
+
--session-key <key>
|
|
1664
|
+
--ticket-id <id>
|
|
1665
|
+
one of: --attachment-id <id> | --storage-path <path>
|
|
1666
|
+
Optional:
|
|
1667
|
+
--objective-id <id> Required when using --storage-path
|
|
1668
|
+
--expires-in <seconds>
|
|
1669
|
+
|
|
1670
|
+
attachment-upload-file:
|
|
1671
|
+
Required:
|
|
1672
|
+
--session-key <key>
|
|
1673
|
+
--ticket-id <id>
|
|
1674
|
+
--objective-id <id>
|
|
1675
|
+
--file <path>
|
|
1676
|
+
Optional:
|
|
1677
|
+
--file-name <name> Defaults to basename of --file
|
|
1678
|
+
--label <text> Defaults to file name
|
|
1679
|
+
--content-type <mime> Defaults to application/octet-stream
|
|
1680
|
+
--metadata-json <json>
|
|
1681
|
+
|
|
1682
|
+
Examples:
|
|
1683
|
+
ovld protocol auth-status
|
|
1684
|
+
ovld protocol discover-project
|
|
1685
|
+
ovld protocol discover-project --working-directory /path/to/repo
|
|
1686
|
+
ovld protocol prompt --agent codex --objective "Implement feature X" # auto-resolves project from cwd
|
|
1687
|
+
ovld protocol attach --ticket-id abc-123
|
|
1688
|
+
ovld protocol attach --ticket-id abc-123 --external-session-id null
|
|
1689
|
+
ovld protocol connect --ticket-id abc-123
|
|
1690
|
+
ovld protocol load-context --ticket-id abc-123
|
|
1691
|
+
ovld protocol search-tickets --query "auth refactor" --status next-up,execute --limit 10
|
|
1692
|
+
ovld protocol create --agent codex --objective "Capture follow-up work from this repo"
|
|
1693
|
+
ovld protocol create --agent codex --session-key <key> --ticket-id <id> --objective "Capture follow-up work"
|
|
1694
|
+
ovld protocol prompt --agent codex --objective "Implement user auth" --priority high
|
|
1695
|
+
ovld protocol update --session-key <key> --ticket-id <id> --summary "Did X" --phase execute
|
|
1696
|
+
ovld protocol update --session-key <key> --ticket-id <id> --summary-file ./update.txt --event-type user_follow_up
|
|
1697
|
+
ovld protocol record-change-rationales --session-key <key> --ticket-id <id> --change-rationales-json '[{"label":"...","file_path":"...","summary":"...","why":"...","impact":"...","hunks":[{"header":"@@ ... @@"}]}]'
|
|
1698
|
+
ovld protocol ask --session-key <key> --ticket-id <id> --question-file ./question.txt
|
|
1699
|
+
ovld protocol read-context --session-key <key> --ticket-id <id> --query arch --limit 5
|
|
1700
|
+
ovld protocol write-context --session-key <key> --ticket-id <id> --key "arch" --value '"monorepo"' --tags repo,agent
|
|
1701
|
+
ovld protocol attachment-list --session-key <key> --ticket-id <id>
|
|
1702
|
+
ovld protocol attachment-prepare-upload --session-key <key> --ticket-id <id> --objective-id <objective-id> --file-name spec.pdf --content-type application/pdf
|
|
1703
|
+
ovld protocol attachment-upload-file --session-key <key> --ticket-id <id> --objective-id <objective-id> --file ./spec.pdf
|
|
1704
|
+
ovld protocol attachment-download-url --session-key <key> --ticket-id <id> --attachment-id <attachment-id>
|
|
1705
|
+
ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done"
|
|
1706
|
+
ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done" --artifacts-file ./artifacts.json
|
|
1707
|
+
ovld protocol deliver --session-key <key> --ticket-id <id> --payload-file ./deliver.json
|
|
1708
|
+
ovld protocol deliver --session-key <key> --ticket-id <id> --payload-file -
|
|
1709
|
+
ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done" --skip-file-change-check
|
|
1710
|
+
ovld protocol deliver --session-key <key> --ticket-id <id> --summary "Done" --timeout 60000
|
|
1711
|
+
`);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (subcommand === 'discover-project') {
|
|
1716
|
+
await protocolDiscoverProject(args);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (subcommand === 'auth-status') {
|
|
1720
|
+
await protocolAuthStatus();
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (subcommand === 'attach') {
|
|
1724
|
+
await protocolAttach(args);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (subcommand === 'connect') {
|
|
1728
|
+
await protocolConnect(args);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (subcommand === 'load-context') {
|
|
1732
|
+
await protocolLoadContext(args);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (subcommand === 'search-tickets') {
|
|
1736
|
+
await protocolSearchTickets(args);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (subcommand === 'create' || subcommand === 'create-ticket') {
|
|
1740
|
+
await protocolCreateTicket(args);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
if (subcommand === 'prompt' || subcommand === 'spawn') {
|
|
1744
|
+
await protocolPrompt(args);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
if (subcommand === 'attachment-list') {
|
|
1748
|
+
await protocolAttachmentList(args);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (subcommand === 'attachment-prepare-upload') {
|
|
1752
|
+
await protocolAttachmentPrepareUpload(args);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (subcommand === 'attachment-finalize-upload') {
|
|
1756
|
+
await protocolAttachmentFinalizeUpload(args);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (subcommand === 'attachment-download-url') {
|
|
1760
|
+
await protocolAttachmentGetDownloadUrl(args);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (subcommand === 'attachment-upload-file') {
|
|
1764
|
+
await protocolAttachmentUploadFile(args);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (subcommand === 'update') {
|
|
1768
|
+
await protocolUpdate(args);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (subcommand === 'record-change-rationales') {
|
|
1772
|
+
await protocolRecordChangeRationales(args);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (subcommand === 'ask') {
|
|
1776
|
+
await protocolAsk(args);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (subcommand === 'permission-request') {
|
|
1780
|
+
await protocolPermissionRequest(args);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (subcommand === 'read-context') {
|
|
1784
|
+
await protocolReadContext(args);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (subcommand === 'write-context') {
|
|
1788
|
+
await protocolWriteContext(args);
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (subcommand === 'deliver') {
|
|
1792
|
+
await protocolDeliver(args);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
console.error(`Unknown protocol subcommand: ${subcommand}\n`);
|
|
1797
|
+
console.log('Run: ovld protocol help');
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
}
|