@tryarcanist/cli 0.1.11 → 0.1.13
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/dist/index.js +658 -137
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,30 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
|
+
// src/api.ts
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
|
|
7
10
|
// ../../shared/utils/url.ts
|
|
8
11
|
function normalizeBaseUrl(url) {
|
|
9
12
|
return url.replace(/\/+$/, "");
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// src/errors.ts
|
|
16
|
+
var CliError = class extends Error {
|
|
17
|
+
code;
|
|
18
|
+
exitCode;
|
|
19
|
+
hint;
|
|
20
|
+
requestId;
|
|
21
|
+
constructor(code, message, options = {}) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "CliError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.exitCode = options.exitCode ?? exitCodeForErrorCode(code);
|
|
26
|
+
this.hint = options.hint;
|
|
27
|
+
this.requestId = options.requestId;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var ApiError = class extends CliError {
|
|
31
|
+
status;
|
|
32
|
+
body;
|
|
33
|
+
constructor(status, body, requestId) {
|
|
34
|
+
const code = codeForHttpStatus(status);
|
|
35
|
+
super(code, messageForApiError(status, body), {
|
|
36
|
+
exitCode: exitCodeForHttpStatus(status),
|
|
37
|
+
hint: hintForHttpStatus(status),
|
|
38
|
+
requestId
|
|
39
|
+
});
|
|
40
|
+
this.name = "ApiError";
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.body = body;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function exitCodeForErrorCode(code) {
|
|
46
|
+
switch (code) {
|
|
47
|
+
case "auth":
|
|
48
|
+
return 2;
|
|
49
|
+
case "not_found":
|
|
50
|
+
return 3;
|
|
51
|
+
case "conflict":
|
|
52
|
+
return 4;
|
|
53
|
+
case "server":
|
|
54
|
+
return 10;
|
|
55
|
+
case "user":
|
|
56
|
+
default:
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function codeForHttpStatus(status) {
|
|
61
|
+
if (status === 401 || status === 403) return "auth";
|
|
62
|
+
if (status === 404) return "not_found";
|
|
63
|
+
if (status === 409) return "conflict";
|
|
64
|
+
if (status >= 500) return "server";
|
|
65
|
+
return "user";
|
|
66
|
+
}
|
|
67
|
+
function exitCodeForHttpStatus(status) {
|
|
68
|
+
return exitCodeForErrorCode(codeForHttpStatus(status));
|
|
69
|
+
}
|
|
70
|
+
function messageForApiError(status, body) {
|
|
71
|
+
const parsed = parseApiErrorBody(body);
|
|
72
|
+
if (parsed) return parsed;
|
|
73
|
+
return body ? `API error ${status}: ${body}` : `API error ${status}`;
|
|
74
|
+
}
|
|
75
|
+
function parseApiErrorBody(body) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(body);
|
|
78
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
79
|
+
const error = parsed.error;
|
|
80
|
+
return typeof error === "string" && error.length > 0 ? error : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function hintForHttpStatus(status) {
|
|
86
|
+
if (status === 401 || status === 403) return "Run `arcanist login` or set `ARCANIST_TOKEN`.";
|
|
87
|
+
if (status === 404) return "List sessions with `arcanist sessions list`.";
|
|
88
|
+
if (status === 409) return "Check the current resource state and retry the command when it is ready.";
|
|
89
|
+
if (status >= 500) return "Retry later or check the control-plane logs.";
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
function toCliError(err) {
|
|
93
|
+
if (err instanceof CliError) return err;
|
|
94
|
+
if (err instanceof Error) return new CliError("user", err.message);
|
|
95
|
+
return new CliError("user", String(err));
|
|
96
|
+
}
|
|
97
|
+
function formatJsonError(err) {
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
error: {
|
|
100
|
+
code: err.code,
|
|
101
|
+
message: err.message,
|
|
102
|
+
...err.hint ? { hint: err.hint } : {},
|
|
103
|
+
...err.requestId ? { requestId: err.requestId } : {}
|
|
20
104
|
}
|
|
21
105
|
});
|
|
22
|
-
|
|
23
|
-
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/api.ts
|
|
109
|
+
var require2 = createRequire(import.meta.url);
|
|
110
|
+
var { version } = require2("../package.json");
|
|
111
|
+
var CLI_USER_AGENT = `arcanist-cli/${version}`;
|
|
112
|
+
async function apiRequest(config, path, init) {
|
|
113
|
+
let res;
|
|
114
|
+
try {
|
|
115
|
+
const headers = new Headers(init?.headers);
|
|
116
|
+
headers.set("Content-Type", "application/json");
|
|
117
|
+
headers.set("User-Agent", CLI_USER_AGENT);
|
|
118
|
+
headers.set("Authorization", `Bearer ${config.token}`);
|
|
119
|
+
res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
|
|
120
|
+
...init,
|
|
121
|
+
headers
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new CliError("server", `Network error: ${err instanceof Error ? err.message : String(err)}`);
|
|
24
125
|
}
|
|
25
126
|
if (!res.ok) {
|
|
26
127
|
const body = await res.text().catch(() => "");
|
|
27
|
-
throw new
|
|
128
|
+
throw new ApiError(res.status, body, res.headers.get("x-request-id") ?? void 0);
|
|
28
129
|
}
|
|
29
130
|
return res;
|
|
30
131
|
}
|
|
@@ -43,26 +144,42 @@ import { homedir } from "os";
|
|
|
43
144
|
import { join } from "path";
|
|
44
145
|
var CONFIG_DIR = join(homedir(), ".arcanist");
|
|
45
146
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
46
|
-
|
|
147
|
+
var DEFAULT_API_URL = "https://app.tryarcanist.com";
|
|
148
|
+
function loadFileConfig() {
|
|
47
149
|
try {
|
|
48
|
-
|
|
49
|
-
return { ...config, apiUrl: normalizeBaseUrl(config.apiUrl) };
|
|
150
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
50
151
|
} catch {
|
|
51
152
|
return null;
|
|
52
153
|
}
|
|
53
154
|
}
|
|
155
|
+
function loadConfig(overrides = {}) {
|
|
156
|
+
const fileConfig = loadFileConfig();
|
|
157
|
+
const apiUrl = overrides.apiUrl ?? process.env.ARCANIST_API_URL ?? fileConfig?.apiUrl;
|
|
158
|
+
const token = overrides.token ?? process.env.ARCANIST_TOKEN ?? fileConfig?.token;
|
|
159
|
+
if (!apiUrl || !token) return null;
|
|
160
|
+
const urlError = validateApiUrl(apiUrl);
|
|
161
|
+
if (urlError) throw new CliError("user", urlError);
|
|
162
|
+
return { apiUrl: normalizeBaseUrl(apiUrl), token };
|
|
163
|
+
}
|
|
54
164
|
function saveConfig(config) {
|
|
165
|
+
const urlError = validateApiUrl(config.apiUrl);
|
|
166
|
+
if (urlError) throw new CliError("user", urlError);
|
|
55
167
|
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
56
168
|
writeFileSync(CONFIG_FILE, JSON.stringify({ ...config, apiUrl: normalizeBaseUrl(config.apiUrl) }, null, 2) + "\n", { mode: 384 });
|
|
57
169
|
}
|
|
58
|
-
function requireConfig() {
|
|
59
|
-
const config = loadConfig();
|
|
170
|
+
function requireConfig(overrides = {}) {
|
|
171
|
+
const config = loadConfig(overrides);
|
|
60
172
|
if (!config) {
|
|
61
|
-
|
|
62
|
-
process.exit(1);
|
|
173
|
+
throw new CliError("auth", "Not logged in.", { hint: "Run `arcanist login` or set `ARCANIST_TOKEN`." });
|
|
63
174
|
}
|
|
64
175
|
return config;
|
|
65
176
|
}
|
|
177
|
+
function resolveLoginApiUrl(apiUrl) {
|
|
178
|
+
const raw = apiUrl ?? process.env.ARCANIST_API_URL ?? loadFileConfig()?.apiUrl ?? DEFAULT_API_URL;
|
|
179
|
+
const urlError = validateApiUrl(raw);
|
|
180
|
+
if (urlError) throw new CliError("user", urlError);
|
|
181
|
+
return normalizeBaseUrl(raw);
|
|
182
|
+
}
|
|
66
183
|
function validateApiUrl(url) {
|
|
67
184
|
let parsed;
|
|
68
185
|
try {
|
|
@@ -78,6 +195,83 @@ function validateApiUrl(url) {
|
|
|
78
195
|
return null;
|
|
79
196
|
}
|
|
80
197
|
|
|
198
|
+
// src/runtime.ts
|
|
199
|
+
import { randomUUID } from "crypto";
|
|
200
|
+
import { createInterface } from "readline/promises";
|
|
201
|
+
function getRuntimeOptions(command, options = {}) {
|
|
202
|
+
const globals = command?.optsWithGlobals?.();
|
|
203
|
+
const merged = { ...globals, ...options };
|
|
204
|
+
return { ...merged, noColor: merged.noColor === true || merged.color === false };
|
|
205
|
+
}
|
|
206
|
+
function isJson(command, options = {}) {
|
|
207
|
+
return getRuntimeOptions(command, options).json === true;
|
|
208
|
+
}
|
|
209
|
+
function isQuiet(command, options = {}) {
|
|
210
|
+
return getRuntimeOptions(command, options).quiet === true;
|
|
211
|
+
}
|
|
212
|
+
function writeJson(value) {
|
|
213
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
async function readStdin() {
|
|
217
|
+
const chunks = [];
|
|
218
|
+
for await (const chunk of process.stdin) {
|
|
219
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
220
|
+
}
|
|
221
|
+
return Buffer.concat(chunks).toString();
|
|
222
|
+
}
|
|
223
|
+
async function readStdinTrimmed() {
|
|
224
|
+
return (await readStdin()).trim();
|
|
225
|
+
}
|
|
226
|
+
async function resolvePromptInput(promptArg, options) {
|
|
227
|
+
const shouldReadStdin = options.promptStdin === true || promptArg === "-";
|
|
228
|
+
const prompt = shouldReadStdin ? await readStdin() : promptArg;
|
|
229
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
230
|
+
throw new CliError("user", "Missing prompt. Pass a prompt argument, use '-' to read stdin, or pass --prompt-stdin.");
|
|
231
|
+
}
|
|
232
|
+
return prompt;
|
|
233
|
+
}
|
|
234
|
+
async function confirmOrThrow(message) {
|
|
235
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
236
|
+
throw new CliError("user", "Confirmation required. Re-run with --yes in non-interactive environments.");
|
|
237
|
+
}
|
|
238
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
239
|
+
try {
|
|
240
|
+
const answer = (await rl.question(`${message} [y/N] `)).trim().toLowerCase();
|
|
241
|
+
if (answer !== "y" && answer !== "yes") {
|
|
242
|
+
throw new CliError("user", "Aborted.");
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
rl.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function printDeprecatedAlias(alias, replacement, command, options = {}) {
|
|
249
|
+
if (isQuiet(command, options)) return;
|
|
250
|
+
process.stderr.write(`Warning: \`arcanist ${alias}\` is deprecated; use \`arcanist ${replacement}\`.
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
function applyColorEnvironment(options) {
|
|
254
|
+
if (options.noColor === true || !process.stdout.isTTY) {
|
|
255
|
+
process.env.NO_COLOR = "1";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function randomIdempotencyKey() {
|
|
259
|
+
return randomUUID();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/commands/auth.ts
|
|
263
|
+
async function whoamiCommand(options, command) {
|
|
264
|
+
const runtime = getRuntimeOptions(command, options);
|
|
265
|
+
const config = requireConfig(runtime);
|
|
266
|
+
const payload = await apiFetch(config, "/api/auth/whoami");
|
|
267
|
+
if (isJson(command, options)) {
|
|
268
|
+
writeJson(payload);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(`User: ${String(payload.email ?? payload.userId ?? "unknown")}`);
|
|
272
|
+
if (payload.tokenScope) console.log(`Token scope: ${String(payload.tokenScope)}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
81
275
|
// src/commands/create.ts
|
|
82
276
|
var REPO_URL_PATTERNS = [
|
|
83
277
|
/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/,
|
|
@@ -88,89 +282,104 @@ function validateRepoUrl(url) {
|
|
|
88
282
|
if (REPO_URL_PATTERNS.some((pattern) => pattern.test(url))) return null;
|
|
89
283
|
return `Invalid repo URL: "${url}". Expected a GitHub URL (https://github.com/owner/repo) or owner/repo shorthand.`;
|
|
90
284
|
}
|
|
91
|
-
async function createCommand(repoUrl,
|
|
92
|
-
const
|
|
285
|
+
async function createCommand(repoUrl, promptArg, options, command) {
|
|
286
|
+
const runtime = getRuntimeOptions(command, options);
|
|
287
|
+
const config = requireConfig(runtime);
|
|
288
|
+
const prompt = await resolvePromptInput(promptArg, options);
|
|
93
289
|
const repoError = validateRepoUrl(repoUrl);
|
|
94
290
|
if (repoError) {
|
|
95
|
-
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
let sessionId;
|
|
99
|
-
try {
|
|
100
|
-
const sessionData = await apiFetch(config, "/api/sessions", {
|
|
101
|
-
method: "POST",
|
|
102
|
-
body: JSON.stringify({
|
|
103
|
-
context: { repoUrl },
|
|
104
|
-
...options.model ? { model: options.model } : {}
|
|
105
|
-
})
|
|
106
|
-
});
|
|
107
|
-
sessionId = sessionData.sessionId;
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.error(`Error creating session: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
-
process.exit(1);
|
|
291
|
+
throw new CliError("user", repoError);
|
|
111
292
|
}
|
|
293
|
+
const idempotencyKey = options.idempotencyKey ?? randomIdempotencyKey();
|
|
294
|
+
const sessionIdempotencyKey = `${idempotencyKey}:session`;
|
|
295
|
+
const promptIdempotencyKey = `${idempotencyKey}:prompt`;
|
|
296
|
+
const sessionData = await apiFetch(config, "/api/sessions", {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: { "Idempotency-Key": sessionIdempotencyKey },
|
|
299
|
+
body: JSON.stringify({
|
|
300
|
+
context: { repoUrl },
|
|
301
|
+
...options.model ? { model: options.model } : {}
|
|
302
|
+
})
|
|
303
|
+
});
|
|
304
|
+
const sessionId = sessionData.sessionId;
|
|
112
305
|
try {
|
|
113
|
-
await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
306
|
+
const promptData = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
114
307
|
method: "POST",
|
|
308
|
+
headers: { "Idempotency-Key": promptIdempotencyKey },
|
|
115
309
|
body: JSON.stringify({ prompt })
|
|
116
310
|
});
|
|
311
|
+
const promptId = promptData.prompt?.promptId ?? promptData.prompt?.id;
|
|
312
|
+
if (isJson(command, options)) {
|
|
313
|
+
writeJson({
|
|
314
|
+
sessionId,
|
|
315
|
+
...sessionData.sessionUrl ? { sessionUrl: sessionData.sessionUrl } : {},
|
|
316
|
+
repoUrl,
|
|
317
|
+
...options.model ? { model: options.model } : {},
|
|
318
|
+
...promptId ? { promptId } : {}
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
117
322
|
} catch (err) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
323
|
+
throw new CliError(err instanceof CliError ? err.code : "server", `Session created (${sessionId}) but prompt failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
324
|
+
exitCode: err instanceof CliError ? err.exitCode : void 0,
|
|
325
|
+
hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
|
|
326
|
+
requestId: err instanceof CliError ? err.requestId : void 0
|
|
327
|
+
});
|
|
121
328
|
}
|
|
122
|
-
const uiUrl = config.apiUrl.replace(/:\d+$/, ":5173");
|
|
123
329
|
console.log(`Session: ${sessionId}`);
|
|
124
|
-
console.log(`URL: ${
|
|
330
|
+
if (sessionData.sessionUrl) console.log(`URL: ${sessionData.sessionUrl}`);
|
|
125
331
|
}
|
|
126
332
|
|
|
127
333
|
// src/commands/login.ts
|
|
128
|
-
import { createInterface } from "readline";
|
|
129
|
-
async function loginCommand(options) {
|
|
334
|
+
import { createInterface as createInterface2 } from "readline";
|
|
335
|
+
async function loginCommand(options, command) {
|
|
336
|
+
const runtime = getRuntimeOptions(command, options);
|
|
130
337
|
let token;
|
|
131
338
|
if (options.tokenStdin) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
token = Buffer.concat(chunks).toString().trim();
|
|
339
|
+
token = await readStdinTrimmed();
|
|
340
|
+
} else if (runtime.token) {
|
|
341
|
+
token = runtime.token;
|
|
137
342
|
} else {
|
|
138
343
|
token = await promptHidden("Enter your CLI token: ");
|
|
139
344
|
}
|
|
140
345
|
if (!token) {
|
|
141
|
-
|
|
142
|
-
process.exit(1);
|
|
346
|
+
throw new CliError("user", "No token provided.");
|
|
143
347
|
}
|
|
144
348
|
if (!token.startsWith("arc_")) {
|
|
145
|
-
|
|
146
|
-
process.exit(1);
|
|
349
|
+
throw new CliError("user", "Invalid token format. Token must start with 'arc_'.");
|
|
147
350
|
}
|
|
148
|
-
if (
|
|
149
|
-
const urlError = validateApiUrl(
|
|
351
|
+
if (runtime.apiUrl) {
|
|
352
|
+
const urlError = validateApiUrl(runtime.apiUrl);
|
|
150
353
|
if (urlError) {
|
|
151
|
-
|
|
152
|
-
process.exit(1);
|
|
354
|
+
throw new CliError("user", urlError);
|
|
153
355
|
}
|
|
154
356
|
}
|
|
155
|
-
const apiUrl = normalizeBaseUrl(
|
|
357
|
+
const apiUrl = normalizeBaseUrl(resolveLoginApiUrl(runtime.apiUrl));
|
|
156
358
|
saveConfig({ apiUrl, token });
|
|
157
|
-
|
|
359
|
+
if (runtime.json) {
|
|
360
|
+
writeJson({ ok: true, apiUrl });
|
|
361
|
+
} else if (!runtime.quiet) {
|
|
362
|
+
console.log(`Logged in. API: ${apiUrl}`);
|
|
363
|
+
}
|
|
158
364
|
try {
|
|
159
365
|
const res = await fetch(`${apiUrl}/api/cli-tokens`, {
|
|
160
|
-
headers: { Authorization: `Bearer ${token}
|
|
366
|
+
headers: { Authorization: `Bearer ${token}`, "User-Agent": CLI_USER_AGENT }
|
|
161
367
|
});
|
|
162
|
-
if (res.ok) {
|
|
368
|
+
if (res.ok && !runtime.json && !runtime.quiet) {
|
|
163
369
|
console.log("Token verified.");
|
|
164
|
-
} else if (res.status === 401) {
|
|
370
|
+
} else if (res.status === 401 && !runtime.json && !runtime.quiet) {
|
|
165
371
|
console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
|
|
166
372
|
}
|
|
167
373
|
} catch {
|
|
168
|
-
console.warn("Warning: Could not reach API to verify token.");
|
|
374
|
+
if (!runtime.json && !runtime.quiet) console.warn("Warning: Could not reach API to verify token.");
|
|
169
375
|
}
|
|
170
376
|
}
|
|
171
377
|
function promptHidden(prompt) {
|
|
172
|
-
|
|
173
|
-
|
|
378
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
379
|
+
return Promise.reject(new CliError("user", "No interactive terminal available. Re-run with --token-stdin or set ARCANIST_TOKEN."));
|
|
380
|
+
}
|
|
381
|
+
return new Promise((resolve, reject) => {
|
|
382
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
174
383
|
process.stdout.write(prompt);
|
|
175
384
|
const stdin = process.stdin;
|
|
176
385
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
@@ -185,7 +394,9 @@ function promptHidden(prompt) {
|
|
|
185
394
|
resolve(input);
|
|
186
395
|
} else if (c === "") {
|
|
187
396
|
if (stdin.isTTY) stdin.setRawMode(false);
|
|
188
|
-
|
|
397
|
+
stdin.removeListener("data", onData);
|
|
398
|
+
rl.close();
|
|
399
|
+
reject(new CliError("user", "Interrupted.", { exitCode: 130 }));
|
|
189
400
|
} else if (c === "\x7F" || c === "\b") {
|
|
190
401
|
input = input.slice(0, -1);
|
|
191
402
|
} else {
|
|
@@ -197,32 +408,21 @@ function promptHidden(prompt) {
|
|
|
197
408
|
}
|
|
198
409
|
|
|
199
410
|
// src/commands/message.ts
|
|
200
|
-
async function messageCommand(sessionId,
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// src/commands/stop.ts
|
|
215
|
-
async function stopCommand(sessionId) {
|
|
216
|
-
const config = requireConfig();
|
|
217
|
-
try {
|
|
218
|
-
await apiFetch(config, `/api/sessions/${sessionId}/stop`, {
|
|
219
|
-
method: "POST"
|
|
220
|
-
});
|
|
221
|
-
console.log(`Stop requested for session ${sessionId}.`);
|
|
222
|
-
} catch (err) {
|
|
223
|
-
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
-
process.exit(1);
|
|
411
|
+
async function messageCommand(sessionId, promptArg, options = {}, command) {
|
|
412
|
+
const runtime = getRuntimeOptions(command, options);
|
|
413
|
+
const config = requireConfig(runtime);
|
|
414
|
+
const prompt = await resolvePromptInput(promptArg, options);
|
|
415
|
+
const response = await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: { "Idempotency-Key": options.idempotencyKey ?? randomIdempotencyKey() },
|
|
418
|
+
body: JSON.stringify({ prompt })
|
|
419
|
+
});
|
|
420
|
+
const promptId = response.prompt?.promptId ?? response.prompt?.id;
|
|
421
|
+
if (isJson(command, options)) {
|
|
422
|
+
writeJson({ sessionId, ...promptId ? { promptId } : {} });
|
|
423
|
+
return;
|
|
225
424
|
}
|
|
425
|
+
console.log(`Message sent to session ${sessionId}.`);
|
|
226
426
|
}
|
|
227
427
|
|
|
228
428
|
// ../../shared/transcript/projector.ts
|
|
@@ -794,22 +994,6 @@ function renderWatchEvent(event, state) {
|
|
|
794
994
|
}
|
|
795
995
|
}
|
|
796
996
|
|
|
797
|
-
// src/commands/transcript.ts
|
|
798
|
-
async function transcriptCommand(sessionId, options) {
|
|
799
|
-
const config = requireConfig();
|
|
800
|
-
try {
|
|
801
|
-
const exportData = await apiFetch(config, `/api/sessions/${sessionId}/export`);
|
|
802
|
-
if (options.json) {
|
|
803
|
-
console.log(JSON.stringify(exportData, null, 2));
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
console.log(renderSessionTranscript(exportData));
|
|
807
|
-
} catch (err) {
|
|
808
|
-
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
809
|
-
process.exit(1);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
997
|
// src/constants/watch.ts
|
|
814
998
|
var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
|
|
815
999
|
var WATCH_REPLAY_PAGE_SIZE = 200;
|
|
@@ -821,11 +1005,17 @@ function sleep(ms) {
|
|
|
821
1005
|
}
|
|
822
1006
|
function parsePollInterval(raw) {
|
|
823
1007
|
if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
throw new Error("Polling interval must be a non-negative integer.");
|
|
1008
|
+
if (!/^\d+$/.test(raw)) {
|
|
1009
|
+
throw new CliError("user", "Polling interval must be a non-negative integer.");
|
|
827
1010
|
}
|
|
828
|
-
return
|
|
1011
|
+
return Number(raw);
|
|
1012
|
+
}
|
|
1013
|
+
function parseNonNegativeInteger(raw, name, defaultValue) {
|
|
1014
|
+
if (!raw) return defaultValue;
|
|
1015
|
+
if (!/^\d+$/.test(raw)) {
|
|
1016
|
+
throw new CliError("user", `${name} must be a non-negative integer.`);
|
|
1017
|
+
}
|
|
1018
|
+
return Number(raw);
|
|
829
1019
|
}
|
|
830
1020
|
function formatStatusLine(status) {
|
|
831
1021
|
const details = [];
|
|
@@ -837,33 +1027,42 @@ async function fetchPromptLabels(config, sessionId) {
|
|
|
837
1027
|
const data = await apiFetch(config, `/api/sessions/${sessionId}/prompts`);
|
|
838
1028
|
return buildPromptLabelMap(data.prompts);
|
|
839
1029
|
}
|
|
840
|
-
async function watchCommand(sessionId, options) {
|
|
841
|
-
const
|
|
1030
|
+
async function watchCommand(sessionId, options, command) {
|
|
1031
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1032
|
+
const config = requireConfig(runtime);
|
|
842
1033
|
const pollIntervalMs = parsePollInterval(options.pollInterval);
|
|
1034
|
+
const initialAfterSequence = parseNonNegativeInteger(options.afterSequence, "--after-sequence", 0);
|
|
1035
|
+
const pageSize = parseNonNegativeInteger(options.limit, "--limit", WATCH_REPLAY_PAGE_SIZE);
|
|
1036
|
+
if (pageSize === 0) {
|
|
1037
|
+
throw new CliError("user", "--limit must be greater than 0.");
|
|
1038
|
+
}
|
|
1039
|
+
const json = isJson(command, options);
|
|
843
1040
|
let promptLabels = /* @__PURE__ */ new Map();
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1041
|
+
if (!json) {
|
|
1042
|
+
try {
|
|
1043
|
+
promptLabels = await fetchPromptLabels(config, sessionId);
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
console.error(`Warning: failed to fetch prompt labels for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1046
|
+
}
|
|
848
1047
|
}
|
|
849
1048
|
const renderState = {
|
|
850
1049
|
promptLabels,
|
|
851
1050
|
toolCalls: /* @__PURE__ */ new Map()
|
|
852
1051
|
};
|
|
853
|
-
let afterSequence =
|
|
1052
|
+
let afterSequence = initialAfterSequence;
|
|
854
1053
|
let lastStatusLine = null;
|
|
855
1054
|
let textOpen = false;
|
|
856
|
-
console.log(`Watching session ${sessionId}...`);
|
|
1055
|
+
if (!json) console.log(`Watching session ${sessionId}...`);
|
|
857
1056
|
try {
|
|
858
1057
|
while (true) {
|
|
859
1058
|
const query = new URLSearchParams({
|
|
860
1059
|
afterSequence: String(afterSequence),
|
|
861
|
-
limit: String(
|
|
1060
|
+
limit: String(pageSize)
|
|
862
1061
|
});
|
|
863
1062
|
const payload = await apiFetchText(config, `/api/sessions/${sessionId}/events?${query}`);
|
|
864
1063
|
const parsed = parseSsePayload(payload);
|
|
865
|
-
const receivedFullPage = parsed.events.length >=
|
|
866
|
-
if (parsed.status) {
|
|
1064
|
+
const receivedFullPage = parsed.events.length >= pageSize;
|
|
1065
|
+
if (!json && parsed.status) {
|
|
867
1066
|
const nextStatusLine = formatStatusLine(parsed.status);
|
|
868
1067
|
if (nextStatusLine !== lastStatusLine) {
|
|
869
1068
|
if (textOpen) {
|
|
@@ -876,6 +1075,11 @@ async function watchCommand(sessionId, options) {
|
|
|
876
1075
|
}
|
|
877
1076
|
for (const event of parsed.events) {
|
|
878
1077
|
if (typeof event.id === "number" && event.id > afterSequence) afterSequence = event.id;
|
|
1078
|
+
if (json) {
|
|
1079
|
+
process.stdout.write(`${JSON.stringify({ sequence: event.id, type: event.type, data: event.data })}
|
|
1080
|
+
`);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
879
1083
|
const rendered = renderWatchEvent(event, renderState);
|
|
880
1084
|
if (!rendered) continue;
|
|
881
1085
|
if (rendered.kind === "text") {
|
|
@@ -899,30 +1103,347 @@ async function watchCommand(sessionId, options) {
|
|
|
899
1103
|
await sleep(pollIntervalMs);
|
|
900
1104
|
}
|
|
901
1105
|
}
|
|
902
|
-
}
|
|
1106
|
+
} finally {
|
|
903
1107
|
if (textOpen) process.stdout.write("\n");
|
|
904
|
-
|
|
905
|
-
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/commands/sessions.ts
|
|
1112
|
+
async function listSessionsCommand(options, command) {
|
|
1113
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1114
|
+
const config = requireConfig(runtime);
|
|
1115
|
+
const query = new URLSearchParams();
|
|
1116
|
+
if (options.status) query.set("status", options.status);
|
|
1117
|
+
if (options.scope) query.set("scope", options.scope);
|
|
1118
|
+
if (options.limit) query.set("limit", options.limit);
|
|
1119
|
+
if (options.cursor) query.set("cursor", options.cursor);
|
|
1120
|
+
const payload = await apiFetch(
|
|
1121
|
+
config,
|
|
1122
|
+
`/api/sessions${query.size ? `?${query.toString()}` : ""}`
|
|
1123
|
+
);
|
|
1124
|
+
if (isJson(command, options)) {
|
|
1125
|
+
writeJson(payload);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (payload.sessions.length === 0) {
|
|
1129
|
+
console.log("No sessions found.");
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
for (const session of payload.sessions) {
|
|
1133
|
+
const id = String(session.id ?? session.sessionId ?? "");
|
|
1134
|
+
const status = String(session.status ?? "");
|
|
1135
|
+
const title = typeof session.title === "string" ? ` ${session.title}` : "";
|
|
1136
|
+
console.log(`${id} ${status}${title}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (payload.nextCursor) console.log(`Next cursor: ${payload.nextCursor}`);
|
|
1139
|
+
}
|
|
1140
|
+
async function getSessionCommand(sessionId, options, command) {
|
|
1141
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1142
|
+
const config = requireConfig(runtime);
|
|
1143
|
+
const payload = await apiFetch(config, `/api/sessions/${sessionId}`);
|
|
1144
|
+
if (isJson(command, options)) {
|
|
1145
|
+
writeJson(payload);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const session = payload.session && typeof payload.session === "object" ? payload.session : payload;
|
|
1149
|
+
console.log(`Session: ${String(session.sessionId ?? session.id ?? sessionId)}`);
|
|
1150
|
+
console.log(`Status: ${String(session.status ?? "unknown")}`);
|
|
1151
|
+
if (session.repoUrl) console.log(`Repo: ${String(session.repoUrl)}`);
|
|
1152
|
+
if (session.title) console.log(`Title: ${String(session.title)}`);
|
|
1153
|
+
}
|
|
1154
|
+
async function sessionEventsCommand(sessionId, options, command) {
|
|
1155
|
+
if (options.follow) {
|
|
1156
|
+
if (options.beforeSequence) {
|
|
1157
|
+
throw new CliError("user", "--before-sequence cannot be used with --follow.");
|
|
1158
|
+
}
|
|
1159
|
+
if (options.promptId) {
|
|
1160
|
+
throw new CliError("user", "--prompt-id cannot be used with --follow because the follow endpoint does not support prompt filtering.");
|
|
1161
|
+
}
|
|
1162
|
+
await watchCommand(sessionId, { ...options, pollInterval: options.pollInterval, afterSequence: options.afterSequence, limit: options.limit }, command);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1166
|
+
const config = requireConfig(runtime);
|
|
1167
|
+
const query = new URLSearchParams();
|
|
1168
|
+
if (options.afterSequence) query.set("after_sequence", options.afterSequence);
|
|
1169
|
+
if (options.beforeSequence) query.set("before_sequence", options.beforeSequence);
|
|
1170
|
+
if (options.promptId) query.set("prompt_id", options.promptId);
|
|
1171
|
+
if (options.limit) query.set("limit", options.limit);
|
|
1172
|
+
const payload = await apiFetch(config, `/api/sessions/${sessionId}/events/history${query.size ? `?${query.toString()}` : ""}`);
|
|
1173
|
+
if (isJson(command, options)) {
|
|
1174
|
+
writeJson(payload);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const promptLabels = await fetchPromptLabels(config, sessionId).catch(() => /* @__PURE__ */ new Map());
|
|
1178
|
+
const state = { promptLabels, toolCalls: /* @__PURE__ */ new Map() };
|
|
1179
|
+
let textOpen = false;
|
|
1180
|
+
for (const event of payload.events) {
|
|
1181
|
+
const rendered = renderWatchEvent(event, state);
|
|
1182
|
+
if (rendered?.kind === "line") {
|
|
1183
|
+
if (textOpen) {
|
|
1184
|
+
process.stdout.write("\n");
|
|
1185
|
+
textOpen = false;
|
|
1186
|
+
}
|
|
1187
|
+
console.log(rendered.line);
|
|
1188
|
+
}
|
|
1189
|
+
if (rendered?.kind === "text" && rendered.text) {
|
|
1190
|
+
process.stdout.write(rendered.text);
|
|
1191
|
+
textOpen = true;
|
|
1192
|
+
}
|
|
906
1193
|
}
|
|
907
1194
|
if (textOpen) process.stdout.write("\n");
|
|
908
1195
|
}
|
|
1196
|
+
async function usageCommand(sessionId, options, command) {
|
|
1197
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1198
|
+
const config = requireConfig(runtime);
|
|
1199
|
+
const payload = await apiFetch(config, `/api/sessions/${sessionId}/usage`);
|
|
1200
|
+
if (isJson(command, options)) {
|
|
1201
|
+
writeJson(payload);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const usage = payload.usage && typeof payload.usage === "object" ? payload.usage : payload;
|
|
1205
|
+
console.log(`Input tokens: ${String(usage.inputTokens ?? 0)}`);
|
|
1206
|
+
console.log(`Output tokens: ${String(usage.outputTokens ?? 0)}`);
|
|
1207
|
+
console.log(`Total tokens: ${String(usage.totalTokens ?? 0)}`);
|
|
1208
|
+
if (usage.totalCostUsd !== void 0) console.log(`Cost: ${String(usage.totalCostUsd)}`);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/commands/stop.ts
|
|
1212
|
+
async function stopCommand(sessionId, options = {}, command) {
|
|
1213
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1214
|
+
const config = requireConfig(runtime);
|
|
1215
|
+
const response = await apiFetch(config, `/api/sessions/${sessionId}/stop`, {
|
|
1216
|
+
method: "POST"
|
|
1217
|
+
});
|
|
1218
|
+
const status = response.status ?? "stopping";
|
|
1219
|
+
if (isJson(command, options)) {
|
|
1220
|
+
writeJson({ sessionId, status });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
console.log(status === "already_stopped" ? `Session ${sessionId} is already stopped.` : `Stop requested for session ${sessionId}.`);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/commands/tokens.ts
|
|
1227
|
+
async function listTokensCommand(options, command) {
|
|
1228
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1229
|
+
const config = requireConfig(runtime);
|
|
1230
|
+
const query = new URLSearchParams();
|
|
1231
|
+
if (options.limit) query.set("limit", options.limit);
|
|
1232
|
+
if (options.cursor) query.set("cursor", options.cursor);
|
|
1233
|
+
const payload = await apiFetch(config, `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`);
|
|
1234
|
+
if (isJson(command, options)) {
|
|
1235
|
+
writeJson(payload);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (payload.data.length === 0) {
|
|
1239
|
+
console.log("No CLI tokens found.");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
for (const token of payload.data) {
|
|
1243
|
+
console.log(`${String(token.id)} ${String(token.scope)} ${String(token.tokenPrefix)} ${String(token.revokedAt ? "revoked" : "active")}`);
|
|
1244
|
+
}
|
|
1245
|
+
if (payload.nextCursor) console.log(`Next cursor: ${payload.nextCursor}`);
|
|
1246
|
+
}
|
|
1247
|
+
async function createTokenCommand(options, command) {
|
|
1248
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1249
|
+
const config = requireConfig(runtime);
|
|
1250
|
+
const expiresInDays = options.expiresInDays === void 0 ? void 0 : Number(options.expiresInDays);
|
|
1251
|
+
if (expiresInDays !== void 0 && (!Number.isInteger(expiresInDays) || expiresInDays <= 0)) {
|
|
1252
|
+
throw new CliError("user", "expiresInDays must be a positive integer.");
|
|
1253
|
+
}
|
|
1254
|
+
const payload = await apiFetch(config, "/api/cli-tokens", {
|
|
1255
|
+
method: "POST",
|
|
1256
|
+
body: JSON.stringify({
|
|
1257
|
+
scope: options.scope ?? "read",
|
|
1258
|
+
...expiresInDays !== void 0 ? { expiresInDays } : {}
|
|
1259
|
+
})
|
|
1260
|
+
});
|
|
1261
|
+
if (isJson(command, options)) {
|
|
1262
|
+
writeJson(payload);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
console.log(`Token: ${String(payload.token)}`);
|
|
1266
|
+
console.log(`ID: ${String(payload.id)}`);
|
|
1267
|
+
console.log(`Scope: ${String(payload.scope)}`);
|
|
1268
|
+
}
|
|
1269
|
+
async function revokeTokenCommand(tokenId, options, command) {
|
|
1270
|
+
if (isJson(command, options) && options.yes !== true) {
|
|
1271
|
+
throw new CliError("user", "`tokens revoke --json` requires --yes.");
|
|
1272
|
+
}
|
|
1273
|
+
if (options.yes !== true) {
|
|
1274
|
+
await confirmOrThrow(`Revoke CLI token ${tokenId}?`);
|
|
1275
|
+
}
|
|
1276
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1277
|
+
const config = requireConfig(runtime);
|
|
1278
|
+
const payload = await apiFetch(config, `/api/cli-tokens/${tokenId}/revoke`, { method: "POST" });
|
|
1279
|
+
if (isJson(command, options)) {
|
|
1280
|
+
writeJson(payload);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
console.log(`Revoked token ${tokenId}.`);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/commands/transcript.ts
|
|
1287
|
+
async function transcriptCommand(sessionId, options, command) {
|
|
1288
|
+
const runtime = getRuntimeOptions(command, options);
|
|
1289
|
+
const config = requireConfig(runtime);
|
|
1290
|
+
const exportData = await apiFetch(config, `/api/sessions/${sessionId}/export`);
|
|
1291
|
+
if (isJson(command, options)) {
|
|
1292
|
+
writeJson(exportData);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
console.log(renderSessionTranscript(exportData));
|
|
1296
|
+
}
|
|
909
1297
|
|
|
910
1298
|
// src/index.ts
|
|
911
|
-
var
|
|
912
|
-
var { version } =
|
|
913
|
-
var program = new Command().name("arcanist").description("Arcanist CLI").version(
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1299
|
+
var require3 = createRequire2(import.meta.url);
|
|
1300
|
+
var { version: version2 } = require3("../package.json");
|
|
1301
|
+
var program = new Command().name("arcanist").description("Arcanist CLI").version(version2).option("--json", "Output machine-readable JSON").option("--quiet", "Suppress non-essential stderr output").option("--api-url <url>", "Override API URL. Prefer ARCANIST_API_URL for persistent use").option("--token <token>", "Override API token. Prefer ARCANIST_TOKEN because flags can be visible in shell history and process lists").option("--no-color", "Disable color output").exitOverride().addHelpText("after", `
|
|
1302
|
+
Examples:
|
|
1303
|
+
arcanist auth login --token-stdin
|
|
1304
|
+
arcanist sessions create https://github.com/org/repo "fix bug" --json | jq -r .sessionId
|
|
1305
|
+
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
|
|
1306
|
+
arcanist sessions events <session-id> --follow --json
|
|
1307
|
+
|
|
1308
|
+
Exit codes:
|
|
1309
|
+
0 ok, 1 user/input, 2 auth, 3 not found, 4 conflict, 10 server/network, 130 interrupted
|
|
1310
|
+
`);
|
|
1311
|
+
program.configureOutput({
|
|
1312
|
+
writeErr: (str) => process.stderr.write(str)
|
|
1313
|
+
});
|
|
1314
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
1315
|
+
applyColorEnvironment(getRuntimeOptions(actionCommand));
|
|
1316
|
+
});
|
|
1317
|
+
function addCreateOptions(cmd) {
|
|
1318
|
+
return cmd.argument("<repo-url>", "Repository URL").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--model <model>", "Model to use").option("--prompt-stdin", "Read prompt from stdin").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText("after", `
|
|
1319
|
+
Examples:
|
|
1320
|
+
arcanist sessions create https://github.com/org/repo "fix bug"
|
|
1321
|
+
printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
|
|
1322
|
+
arcanist sessions create https://github.com/org/repo - --json | jq -r .sessionId | xargs -I{} arcanist sessions events {} --follow --json
|
|
1323
|
+
`);
|
|
1324
|
+
}
|
|
1325
|
+
function addSendOptions(cmd) {
|
|
1326
|
+
return cmd.argument("<session-id>", "Session ID").argument("[prompt]", "Prompt to send, or '-' to read stdin").option("--prompt-stdin", "Read prompt from stdin").option("--idempotency-key <uuid>", "Request idempotency key for safe manual retries").addHelpText("after", `
|
|
1327
|
+
Examples:
|
|
1328
|
+
arcanist sessions send <session-id> "also update tests"
|
|
1329
|
+
printf "also update tests" | arcanist sessions send <session-id> --prompt-stdin --json
|
|
1330
|
+
`);
|
|
1331
|
+
}
|
|
1332
|
+
var auth = program.command("auth").description("Authentication commands");
|
|
1333
|
+
auth.command("login").description("Authenticate with a personal access token").option("--token-stdin", "Read token from stdin instead of interactive prompt").option("--api-url <url>", "Set custom API URL").addHelpText("after", `
|
|
1334
|
+
Examples:
|
|
1335
|
+
arcanist auth login
|
|
1336
|
+
printf "arc_..." | arcanist auth login --token-stdin
|
|
1337
|
+
ARCANIST_TOKEN=arc_... arcanist auth whoami --json
|
|
1338
|
+
`).action((options, command) => loginCommand(options, command));
|
|
1339
|
+
auth.command("whoami").description("Print the authenticated user and token scope").addHelpText("after", `
|
|
1340
|
+
Examples:
|
|
1341
|
+
arcanist auth whoami
|
|
1342
|
+
ARCANIST_TOKEN=arc_... arcanist auth whoami --json
|
|
1343
|
+
`).action((options, command) => whoamiCommand(options, command));
|
|
1344
|
+
var sessions = program.command("sessions").description("Session commands");
|
|
1345
|
+
addCreateOptions(sessions.command("create").description("Create a session and send a prompt")).action((repoUrl, prompt, options, command) => createCommand(repoUrl, prompt, options, command));
|
|
1346
|
+
addSendOptions(sessions.command("send").description("Send a message to an existing session")).action((sessionId, prompt, options, command) => messageCommand(sessionId, prompt, options, command));
|
|
1347
|
+
sessions.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").addHelpText("after", `
|
|
1348
|
+
Examples:
|
|
1349
|
+
arcanist sessions stop <session-id>
|
|
1350
|
+
arcanist sessions stop <session-id> --json
|
|
1351
|
+
`).action((sessionId, options, command) => stopCommand(sessionId, options, command));
|
|
1352
|
+
sessions.command("get").description("Get session details").argument("<session-id>", "Session ID").addHelpText("after", `
|
|
1353
|
+
Examples:
|
|
1354
|
+
arcanist sessions get <session-id>
|
|
1355
|
+
arcanist sessions get <session-id> --json
|
|
1356
|
+
`).action((sessionId, options, command) => getSessionCommand(sessionId, options, command));
|
|
1357
|
+
sessions.command("list").description("List sessions").option("--status <status>", "Filter by session status").option("--scope <scope>", "Session scope: mine or business").option("--limit <n>", "Maximum sessions to return").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
|
|
1358
|
+
Examples:
|
|
1359
|
+
arcanist sessions list
|
|
1360
|
+
arcanist sessions list --status idle --json
|
|
1361
|
+
`).action((options, command) => listSessionsCommand(options, command));
|
|
1362
|
+
sessions.command("events").description("Read or follow session replay events").argument("<session-id>", "Session ID").option("--after-sequence <n>", "Return events after this sequence").option("--before-sequence <n>", "Return events before this sequence").option("--prompt-id <id>", "Filter events by prompt ID").option("--limit <n>", "Maximum events to return").option("--follow", "Follow events until the session is idle").option("--poll-interval <ms>", "Polling interval in milliseconds", String(DEFAULT_WATCH_POLL_INTERVAL_MS)).addHelpText("after", `
|
|
1363
|
+
Examples:
|
|
1364
|
+
arcanist sessions events <session-id> --json
|
|
1365
|
+
arcanist sessions events <session-id> --follow --json
|
|
1366
|
+
`).action((sessionId, options, command) => sessionEventsCommand(sessionId, options, command));
|
|
1367
|
+
sessions.command("transcript").description("Render a session transcript").argument("<session-id>", "Session ID").addHelpText("after", `
|
|
1368
|
+
Examples:
|
|
1369
|
+
arcanist sessions transcript <session-id>
|
|
1370
|
+
arcanist sessions transcript <session-id> --json
|
|
1371
|
+
`).action((sessionId, options, command) => transcriptCommand(sessionId, options, command));
|
|
1372
|
+
sessions.command("watch").description("Watch session activity until it becomes idle").argument("<session-id>", "Session ID").option("--poll-interval <ms>", "Polling interval in milliseconds", String(DEFAULT_WATCH_POLL_INTERVAL_MS)).addHelpText("after", `
|
|
1373
|
+
Examples:
|
|
1374
|
+
arcanist sessions watch <session-id>
|
|
1375
|
+
arcanist sessions watch <session-id> --json
|
|
1376
|
+
`).action((sessionId, options, command) => watchCommand(sessionId, options, command));
|
|
1377
|
+
sessions.command("usage").description("Get token usage for a session").argument("<session-id>", "Session ID").addHelpText("after", `
|
|
1378
|
+
Examples:
|
|
1379
|
+
arcanist sessions usage <session-id>
|
|
1380
|
+
arcanist sessions usage <session-id> --json
|
|
1381
|
+
`).action((sessionId, options, command) => usageCommand(sessionId, options, command));
|
|
1382
|
+
var tokens = program.command("tokens").description("CLI token commands");
|
|
1383
|
+
tokens.command("list").description("List CLI tokens").option("--limit <n>", "Maximum tokens to return").option("--cursor <cursor>", "Pagination cursor").action((options, command) => listTokensCommand(options, command));
|
|
1384
|
+
tokens.command("create").description("Create a CLI token and print it once").option("--scope <scope>", "Token scope: read or write", "read").option("--expires-in-days <days>", "Token expiry in days").addHelpText("after", `
|
|
1385
|
+
Examples:
|
|
1386
|
+
arcanist tokens create --scope read
|
|
1387
|
+
arcanist tokens create --scope read --json
|
|
1388
|
+
`).action((options, command) => createTokenCommand(options, command));
|
|
1389
|
+
tokens.command("revoke").description("Revoke a CLI token").argument("<id>", "Token ID").option("--yes", "Confirm revocation without prompting").addHelpText("after", `
|
|
1390
|
+
Examples:
|
|
1391
|
+
arcanist tokens revoke 42
|
|
1392
|
+
arcanist tokens revoke 42 --yes --json
|
|
1393
|
+
`).action((id, options, command) => revokeTokenCommand(id, options, command));
|
|
1394
|
+
program.command("login").description("Authenticate with a personal access token").option("--token-stdin", "Read token from stdin instead of interactive prompt").option("--api-url <url>", "Set custom API URL").action((options, command) => {
|
|
1395
|
+
printDeprecatedAlias("login", "auth login", command);
|
|
1396
|
+
return loginCommand(options, command);
|
|
1397
|
+
});
|
|
1398
|
+
addCreateOptions(program.command("create").description("Create a session and send a prompt")).action((repoUrl, prompt, options, command) => {
|
|
1399
|
+
printDeprecatedAlias("create", "sessions create", command);
|
|
1400
|
+
return createCommand(repoUrl, prompt, options, command);
|
|
1401
|
+
});
|
|
1402
|
+
addSendOptions(program.command("message").description("Send a message to an existing session")).action((sessionId, prompt, options, command) => {
|
|
1403
|
+
printDeprecatedAlias("message", "sessions send", command);
|
|
1404
|
+
return messageCommand(sessionId, prompt, options, command);
|
|
1405
|
+
});
|
|
1406
|
+
program.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").action((sessionId, options, command) => {
|
|
1407
|
+
printDeprecatedAlias("stop", "sessions stop", command);
|
|
1408
|
+
return stopCommand(sessionId, options, command);
|
|
1409
|
+
});
|
|
1410
|
+
program.command("transcript").description("Render a session transcript").argument("<session-id>", "Session ID").action((sessionId, options, command) => {
|
|
1411
|
+
printDeprecatedAlias("transcript", "sessions transcript", command);
|
|
1412
|
+
return transcriptCommand(sessionId, options, command);
|
|
1413
|
+
});
|
|
1414
|
+
program.command("watch").description("Watch session activity until it becomes idle").argument("<session-id>", "Session ID").option("--poll-interval <ms>", "Polling interval in milliseconds", String(DEFAULT_WATCH_POLL_INTERVAL_MS)).action((sessionId, options, command) => {
|
|
1415
|
+
printDeprecatedAlias("watch", "sessions events --follow", command);
|
|
1416
|
+
return watchCommand(sessionId, options, command);
|
|
1417
|
+
});
|
|
920
1418
|
async function main() {
|
|
921
1419
|
try {
|
|
922
1420
|
await program.parseAsync(process.argv);
|
|
923
1421
|
} catch (err) {
|
|
924
|
-
|
|
925
|
-
|
|
1422
|
+
if (isCommanderHelpOrVersion(err)) {
|
|
1423
|
+
process.exit(0);
|
|
1424
|
+
}
|
|
1425
|
+
if (err instanceof CliError && err.exitCode === 130) restoreTerminal();
|
|
1426
|
+
const cliError = toCliError(err);
|
|
1427
|
+
const options = program.opts();
|
|
1428
|
+
if (options.json) {
|
|
1429
|
+
process.stderr.write(`${formatJsonError(cliError)}
|
|
1430
|
+
`);
|
|
1431
|
+
} else {
|
|
1432
|
+
process.stderr.write(`Error: ${cliError.message}
|
|
1433
|
+
`);
|
|
1434
|
+
if (cliError.hint) process.stderr.write(`Hint: ${cliError.hint}
|
|
1435
|
+
`);
|
|
1436
|
+
}
|
|
1437
|
+
process.exit(cliError.exitCode);
|
|
926
1438
|
}
|
|
927
1439
|
}
|
|
1440
|
+
function isCommanderHelpOrVersion(err) {
|
|
1441
|
+
if (!err || typeof err !== "object") return false;
|
|
1442
|
+
const code = err.code;
|
|
1443
|
+
return code === "commander.helpDisplayed" || code === "commander.version";
|
|
1444
|
+
}
|
|
1445
|
+
function restoreTerminal() {
|
|
1446
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1447
|
+
process.stdout.write("\x1B[?25h");
|
|
1448
|
+
}
|
|
928
1449
|
main();
|