@tryarcanist/cli 0.1.12 → 0.1.14

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.
Files changed (2) hide show
  1. package/dist/index.js +787 -154
  2. 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/api.ts
13
- async function apiRequest(config, path, init) {
14
- const res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
15
- ...init,
16
- headers: {
17
- "Content-Type": "application/json",
18
- Authorization: `Bearer ${config.token}`,
19
- ...init?.headers
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
- if (res.status === 401) {
23
- throw new Error("Token is invalid or expired. Run `arcanist login` to re-authenticate.");
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 Error(`API error ${res.status}: ${body}`);
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
- function loadConfig() {
147
+ var DEFAULT_API_URL = "https://app.tryarcanist.com";
148
+ function loadFileConfig() {
47
149
  try {
48
- const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
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
- console.error("Error: Not logged in. Run `arcanist login` first.");
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, prompt, options) {
92
- const config = requireConfig();
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
- console.error(`Error: ${repoError}`);
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
- console.error(`Session created (${sessionId}) but prompt failed: ${err instanceof Error ? err.message : String(err)}`);
119
- console.error(`Retry with: arcanist message ${sessionId} "${prompt}"`);
120
- process.exit(1);
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: ${uiUrl}/sessions/${sessionId}`);
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
- const chunks = [];
133
- for await (const chunk of process.stdin) {
134
- chunks.push(chunk);
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
- console.error("Error: No token provided.");
142
- process.exit(1);
346
+ throw new CliError("user", "No token provided.");
143
347
  }
144
348
  if (!token.startsWith("arc_")) {
145
- console.error("Error: Invalid token format. Token must start with 'arc_'.");
146
- process.exit(1);
349
+ throw new CliError("user", "Invalid token format. Token must start with 'arc_'.");
147
350
  }
148
- if (options.apiUrl) {
149
- const urlError = validateApiUrl(options.apiUrl);
351
+ if (runtime.apiUrl) {
352
+ const urlError = validateApiUrl(runtime.apiUrl);
150
353
  if (urlError) {
151
- console.error(`Error: ${urlError}`);
152
- process.exit(1);
354
+ throw new CliError("user", urlError);
153
355
  }
154
356
  }
155
- const apiUrl = normalizeBaseUrl(options.apiUrl ?? loadConfig()?.apiUrl ?? "https://app.tryarcanist.com");
357
+ const apiUrl = normalizeBaseUrl(resolveLoginApiUrl(runtime.apiUrl));
156
358
  saveConfig({ apiUrl, token });
157
- console.log(`Logged in. API: ${apiUrl}`);
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
- return new Promise((resolve) => {
173
- const rl = createInterface({ input: process.stdin, output: process.stdout });
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
- process.exit(1);
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,36 +408,26 @@ function promptHidden(prompt) {
197
408
  }
198
409
 
199
410
  // src/commands/message.ts
200
- async function messageCommand(sessionId, prompt) {
201
- const config = requireConfig();
202
- try {
203
- await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
204
- method: "POST",
205
- body: JSON.stringify({ prompt })
206
- });
207
- console.log(`Message sent to session ${sessionId}.`);
208
- } catch (err) {
209
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
210
- process.exit(1);
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
229
429
  var DUPLICATE_TEXT_DELTA_MIN_CHARS = 24;
430
+ var SUBAGENT_EVENT_TYPES = /* @__PURE__ */ new Set(["subagent_start", "subagent_complete", "subagent_tool_call", "subagent_text"]);
230
431
  var RAW_OPENCODE_NOISE = /* @__PURE__ */ new Set([
231
432
  "session.updated",
232
433
  "session.diff",
@@ -240,6 +441,12 @@ function shouldAppendTextDelta(existingText, incomingText) {
240
441
  if (incomingText.length < DUPLICATE_TEXT_DELTA_MIN_CHARS) return true;
241
442
  return !existingText.endsWith(incomingText);
242
443
  }
444
+ function streamableKey(type, streamId) {
445
+ return `${type}:${streamId}`;
446
+ }
447
+ function resolveSegmentId(streamId, segmentOrdinal) {
448
+ return segmentOrdinal === 0 ? streamId : `${streamId}#${segmentOrdinal}`;
449
+ }
243
450
  function isRecord(value) {
244
451
  return !!value && typeof value === "object" && !Array.isArray(value);
245
452
  }
@@ -274,6 +481,8 @@ function normalizeToolStatus(value) {
274
481
  function flattenSessionEvents(raw) {
275
482
  const merged = [];
276
483
  const streamableIndexById = /* @__PURE__ */ new Map();
484
+ const segmentOrdinalByStreamId = /* @__PURE__ */ new Map();
485
+ const concatByStreamId = /* @__PURE__ */ new Map();
277
486
  const toolCallIndexById = /* @__PURE__ */ new Map();
278
487
  const questionIndexById = /* @__PURE__ */ new Map();
279
488
  for (const event of raw) {
@@ -326,7 +535,11 @@ function flattenSessionEvents(raw) {
326
535
  message: typeof data?.message === "string" ? data.message : "Retrying...",
327
536
  ...typeof data?.nextRetryAt === "string" ? { nextRetryAt: data.nextRetryAt } : {},
328
537
  ...typeof data?.provider === "string" ? { provider: data.provider } : {},
329
- ...typeof data?.errorCode === "string" ? { errorCode: data.errorCode } : {}
538
+ ...typeof data?.errorCode === "string" ? { errorCode: data.errorCode } : {},
539
+ ...typeof data?.scope === "string" ? { scope: data.scope } : {},
540
+ ...typeof data?.maxAttempts === "number" ? { maxAttempts: data.maxAttempts } : {},
541
+ ...typeof data?.reason === "string" ? { reason: data.reason } : {},
542
+ ...typeof data?.retryAfterMs === "number" ? { retryAfterMs: data.retryAfterMs } : {}
330
543
  });
331
544
  continue;
332
545
  }
@@ -365,21 +578,30 @@ function flattenSessionEvents(raw) {
365
578
  continue;
366
579
  }
367
580
  if (event.type === "reasoning") {
368
- const id = resolveEventId(data, "reasoning", merged.length);
581
+ const streamId = resolveEventId(data, "reasoning", merged.length);
582
+ const key = streamableKey("reasoning", streamId);
369
583
  const text = resolveTextValue(data);
370
584
  const promptId = resolvePromptId(data);
371
- const existingIdx = streamableIndexById.get(id);
372
- if (existingIdx !== void 0) {
585
+ const existingText = concatByStreamId.get(key) ?? "";
586
+ const existingIdx = streamableIndexById.get(key);
587
+ if (existingIdx !== void 0 && existingIdx === merged.length - 1) {
373
588
  const existing = merged[existingIdx];
374
- if (existing.type === "reasoning" && shouldAppendTextDelta(existing.text, text)) {
589
+ if (existing.type === "reasoning" && shouldAppendTextDelta(existingText, text)) {
375
590
  existing.text += text;
591
+ concatByStreamId.set(key, existingText + text);
376
592
  if (!existing.promptId && promptId) existing.promptId = promptId;
377
593
  }
378
594
  } else {
379
- streamableIndexById.set(id, merged.length);
595
+ if (existingText && !shouldAppendTextDelta(existingText, text)) continue;
596
+ const previousOrdinal = segmentOrdinalByStreamId.get(key);
597
+ const segmentOrdinal = previousOrdinal === void 0 ? 0 : previousOrdinal + 1;
598
+ segmentOrdinalByStreamId.set(key, segmentOrdinal);
599
+ streamableIndexById.set(key, merged.length);
600
+ concatByStreamId.set(key, existingText + text);
380
601
  merged.push({
381
602
  type: "reasoning",
382
- id,
603
+ id: resolveSegmentId(streamId, segmentOrdinal),
604
+ ...segmentOrdinal > 0 ? { streamId } : {},
383
605
  text,
384
606
  ...promptId ? { promptId } : {}
385
607
  });
@@ -429,21 +651,30 @@ function flattenSessionEvents(raw) {
429
651
  continue;
430
652
  }
431
653
  if (event.type === "text") {
432
- const id = resolveEventId(data, "text", merged.length);
654
+ const streamId = resolveEventId(data, "text", merged.length);
655
+ const key = streamableKey("text", streamId);
433
656
  const text = resolveTextValue(data);
434
657
  const promptId = resolvePromptId(data);
435
- const existingIdx = streamableIndexById.get(id);
436
- if (existingIdx !== void 0) {
658
+ const existingText = concatByStreamId.get(key) ?? "";
659
+ const existingIdx = streamableIndexById.get(key);
660
+ if (existingIdx !== void 0 && existingIdx === merged.length - 1) {
437
661
  const existing = merged[existingIdx];
438
- if (existing.type === "text" && shouldAppendTextDelta(existing.text, text)) {
662
+ if (existing.type === "text" && shouldAppendTextDelta(existingText, text)) {
439
663
  existing.text += text;
664
+ concatByStreamId.set(key, existingText + text);
440
665
  if (!existing.promptId && promptId) existing.promptId = promptId;
441
666
  }
442
667
  } else {
443
- streamableIndexById.set(id, merged.length);
668
+ if (existingText && !shouldAppendTextDelta(existingText, text)) continue;
669
+ const previousOrdinal = segmentOrdinalByStreamId.get(key);
670
+ const segmentOrdinal = previousOrdinal === void 0 ? 0 : previousOrdinal + 1;
671
+ segmentOrdinalByStreamId.set(key, segmentOrdinal);
672
+ streamableIndexById.set(key, merged.length);
673
+ concatByStreamId.set(key, existingText + text);
444
674
  merged.push({
445
675
  type: "text",
446
- id,
676
+ id: resolveSegmentId(streamId, segmentOrdinal),
677
+ ...segmentOrdinal > 0 ? { streamId } : {},
447
678
  text,
448
679
  ...promptId ? { promptId } : {}
449
680
  });
@@ -537,6 +768,65 @@ function getEmbeddedTerminalHistory(raw) {
537
768
  function resolveAuthoritativePromptEvents(raw) {
538
769
  return getEmbeddedTerminalHistory(raw) ?? raw;
539
770
  }
771
+ function extractSubagentInfo(raw) {
772
+ const empty = {
773
+ names: /* @__PURE__ */ new Map(),
774
+ activity: /* @__PURE__ */ new Map(),
775
+ toolToChildSessions: /* @__PURE__ */ new Map(),
776
+ subagentUsage: /* @__PURE__ */ new Map()
777
+ };
778
+ if (!raw.some((event) => SUBAGENT_EVENT_TYPES.has(event.type))) return empty;
779
+ const { names, activity, toolToChildSessions, subagentUsage } = empty;
780
+ for (const event of raw) {
781
+ const data = event.data;
782
+ if (!data) continue;
783
+ const childSessionId = typeof data.childSessionId === "string" ? data.childSessionId : void 0;
784
+ const parentToolId = typeof data.parentToolId === "string" ? data.parentToolId : void 0;
785
+ const key = childSessionId || parentToolId;
786
+ if (!key) continue;
787
+ if (event.type === "subagent_start" || event.type === "subagent_complete") {
788
+ const name = typeof data.name === "string" ? data.name : "";
789
+ if (name) names.set(key, name);
790
+ if (parentToolId && childSessionId) {
791
+ const existing = toolToChildSessions.get(parentToolId) ?? [];
792
+ if (!existing.includes(childSessionId)) existing.push(childSessionId);
793
+ toolToChildSessions.set(parentToolId, existing);
794
+ }
795
+ if (event.type === "subagent_complete" && isRecord(data.tokenUsage)) {
796
+ subagentUsage.set(key, {
797
+ input: Number(data.tokenUsage.input ?? 0),
798
+ output: Number(data.tokenUsage.output ?? 0),
799
+ cacheRead: Number(data.tokenUsage.cacheRead ?? 0),
800
+ cacheWrite: Number(data.tokenUsage.cacheWrite ?? 0)
801
+ });
802
+ }
803
+ continue;
804
+ }
805
+ if (event.type === "subagent_tool_call") {
806
+ const items = activity.get(key) ?? [];
807
+ items.push({
808
+ type: "tool",
809
+ tool: typeof data.tool === "string" ? data.tool : "unknown",
810
+ summary: typeof data.summary === "string" ? data.summary : ""
811
+ });
812
+ activity.set(key, items);
813
+ continue;
814
+ }
815
+ if (event.type === "subagent_text") {
816
+ const delta = typeof data.delta === "string" ? data.delta : typeof data.text === "string" ? data.text : "";
817
+ if (!delta) continue;
818
+ const items = activity.get(key) ?? [];
819
+ const last = items[items.length - 1];
820
+ if (last?.type === "text") {
821
+ items[items.length - 1] = { ...last, text: last.text + delta };
822
+ } else {
823
+ items.push({ type: "text", text: delta });
824
+ }
825
+ activity.set(key, items);
826
+ }
827
+ }
828
+ return { names, activity, toolToChildSessions, subagentUsage };
829
+ }
540
830
 
541
831
  // src/utils/session-output.ts
542
832
  function formatDate(value) {
@@ -544,13 +834,33 @@ function formatDate(value) {
544
834
  if (Number.isNaN(date.getTime())) return value;
545
835
  return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
546
836
  }
547
- function renderTranscriptEvent(event) {
837
+ function renderSubagentActivityItem(item) {
838
+ if (item.type === "text") return ` - ${item.text}
839
+ `;
840
+ return ` - **Tool:** ${item.tool}${item.summary ? ` - ${item.summary}` : ""}
841
+ `;
842
+ }
843
+ function renderSubagentActivityForTool(toolId, subagents) {
844
+ const childSessionIds = subagents.toolToChildSessions.get(toolId) ?? [];
845
+ if (childSessionIds.length === 0) return "";
846
+ const lines = [];
847
+ for (const childSessionId of childSessionIds) {
848
+ const name = subagents.names.get(childSessionId);
849
+ lines.push(` - **Subagent:** ${name ?? childSessionId}`);
850
+ for (const item of subagents.activity.get(childSessionId) ?? []) {
851
+ lines.push(renderSubagentActivityItem(item).trimEnd());
852
+ }
853
+ }
854
+ return `${lines.join("\n")}
855
+ `;
856
+ }
857
+ function renderTranscriptEvent(event, subagents) {
548
858
  switch (event.type) {
549
859
  case "text":
550
860
  return event.text;
551
861
  case "tool_call":
552
862
  return `**Tool call:** ${event.tool}${event.summary ? ` - ${event.summary}` : ""}${event.toolStatus ? ` (${event.toolStatus})` : ""}
553
- `;
863
+ ${subagents ? renderSubagentActivityForTool(event.id, subagents) : ""}`;
554
864
  case "reasoning":
555
865
  return `<details><summary>Reasoning</summary>
556
866
 
@@ -613,7 +923,9 @@ function renderSessionTranscript(exportData) {
613
923
  for (let i = 0; i < exportData.prompts.length; i++) {
614
924
  const prompt = exportData.prompts[i];
615
925
  const rawEvents = eventBuckets.get(prompt.id) ?? [];
616
- const events = flattenSessionEvents(resolveAuthoritativePromptEvents(rawEvents));
926
+ const authoritativeEvents = resolveAuthoritativePromptEvents(rawEvents);
927
+ const events = flattenSessionEvents(authoritativeEvents);
928
+ const subagents = extractSubagentInfo(authoritativeEvents);
617
929
  lines.push("---\n");
618
930
  lines.push(`## Turn ${i + 1}
619
931
  `);
@@ -627,7 +939,7 @@ function renderSessionTranscript(exportData) {
627
939
  lines.push("**Assistant:**\n");
628
940
  let pendingText = "";
629
941
  for (const event of events) {
630
- const rendered = renderTranscriptEvent(event);
942
+ const rendered = renderTranscriptEvent(event, subagents);
631
943
  if (!rendered) continue;
632
944
  if (event.type === "text") {
633
945
  pendingText += rendered;
@@ -794,22 +1106,6 @@ function renderWatchEvent(event, state) {
794
1106
  }
795
1107
  }
796
1108
 
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
1109
  // src/constants/watch.ts
814
1110
  var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
815
1111
  var WATCH_REPLAY_PAGE_SIZE = 200;
@@ -821,11 +1117,17 @@ function sleep(ms) {
821
1117
  }
822
1118
  function parsePollInterval(raw) {
823
1119
  if (!raw) return DEFAULT_WATCH_POLL_INTERVAL_MS;
824
- const parsed = Number.parseInt(raw, 10);
825
- if (!Number.isFinite(parsed) || parsed < 0) {
826
- throw new Error("Polling interval must be a non-negative integer.");
1120
+ if (!/^\d+$/.test(raw)) {
1121
+ throw new CliError("user", "Polling interval must be a non-negative integer.");
827
1122
  }
828
- return parsed;
1123
+ return Number(raw);
1124
+ }
1125
+ function parseNonNegativeInteger(raw, name, defaultValue) {
1126
+ if (!raw) return defaultValue;
1127
+ if (!/^\d+$/.test(raw)) {
1128
+ throw new CliError("user", `${name} must be a non-negative integer.`);
1129
+ }
1130
+ return Number(raw);
829
1131
  }
830
1132
  function formatStatusLine(status) {
831
1133
  const details = [];
@@ -837,33 +1139,42 @@ async function fetchPromptLabels(config, sessionId) {
837
1139
  const data = await apiFetch(config, `/api/sessions/${sessionId}/prompts`);
838
1140
  return buildPromptLabelMap(data.prompts);
839
1141
  }
840
- async function watchCommand(sessionId, options) {
841
- const config = requireConfig();
1142
+ async function watchCommand(sessionId, options, command) {
1143
+ const runtime = getRuntimeOptions(command, options);
1144
+ const config = requireConfig(runtime);
842
1145
  const pollIntervalMs = parsePollInterval(options.pollInterval);
1146
+ const initialAfterSequence = parseNonNegativeInteger(options.afterSequence, "--after-sequence", 0);
1147
+ const pageSize = parseNonNegativeInteger(options.limit, "--limit", WATCH_REPLAY_PAGE_SIZE);
1148
+ if (pageSize === 0) {
1149
+ throw new CliError("user", "--limit must be greater than 0.");
1150
+ }
1151
+ const json = isJson(command, options);
843
1152
  let promptLabels = /* @__PURE__ */ new Map();
844
- try {
845
- promptLabels = await fetchPromptLabels(config, sessionId);
846
- } catch (err) {
847
- console.error(`Warning: failed to fetch prompt labels for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
1153
+ if (!json) {
1154
+ try {
1155
+ promptLabels = await fetchPromptLabels(config, sessionId);
1156
+ } catch (err) {
1157
+ console.error(`Warning: failed to fetch prompt labels for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
1158
+ }
848
1159
  }
849
1160
  const renderState = {
850
1161
  promptLabels,
851
1162
  toolCalls: /* @__PURE__ */ new Map()
852
1163
  };
853
- let afterSequence = 0;
1164
+ let afterSequence = initialAfterSequence;
854
1165
  let lastStatusLine = null;
855
1166
  let textOpen = false;
856
- console.log(`Watching session ${sessionId}...`);
1167
+ if (!json) console.log(`Watching session ${sessionId}...`);
857
1168
  try {
858
1169
  while (true) {
859
1170
  const query = new URLSearchParams({
860
1171
  afterSequence: String(afterSequence),
861
- limit: String(WATCH_REPLAY_PAGE_SIZE)
1172
+ limit: String(pageSize)
862
1173
  });
863
1174
  const payload = await apiFetchText(config, `/api/sessions/${sessionId}/events?${query}`);
864
1175
  const parsed = parseSsePayload(payload);
865
- const receivedFullPage = parsed.events.length >= WATCH_REPLAY_PAGE_SIZE;
866
- if (parsed.status) {
1176
+ const receivedFullPage = parsed.events.length >= pageSize;
1177
+ if (!json && parsed.status) {
867
1178
  const nextStatusLine = formatStatusLine(parsed.status);
868
1179
  if (nextStatusLine !== lastStatusLine) {
869
1180
  if (textOpen) {
@@ -876,6 +1187,11 @@ async function watchCommand(sessionId, options) {
876
1187
  }
877
1188
  for (const event of parsed.events) {
878
1189
  if (typeof event.id === "number" && event.id > afterSequence) afterSequence = event.id;
1190
+ if (json) {
1191
+ process.stdout.write(`${JSON.stringify({ sequence: event.id, type: event.type, data: event.data })}
1192
+ `);
1193
+ continue;
1194
+ }
879
1195
  const rendered = renderWatchEvent(event, renderState);
880
1196
  if (!rendered) continue;
881
1197
  if (rendered.kind === "text") {
@@ -899,30 +1215,347 @@ async function watchCommand(sessionId, options) {
899
1215
  await sleep(pollIntervalMs);
900
1216
  }
901
1217
  }
902
- } catch (err) {
1218
+ } finally {
903
1219
  if (textOpen) process.stdout.write("\n");
904
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
905
- process.exit(1);
1220
+ }
1221
+ }
1222
+
1223
+ // src/commands/sessions.ts
1224
+ async function listSessionsCommand(options, command) {
1225
+ const runtime = getRuntimeOptions(command, options);
1226
+ const config = requireConfig(runtime);
1227
+ const query = new URLSearchParams();
1228
+ if (options.status) query.set("status", options.status);
1229
+ if (options.scope) query.set("scope", options.scope);
1230
+ if (options.limit) query.set("limit", options.limit);
1231
+ if (options.cursor) query.set("cursor", options.cursor);
1232
+ const payload = await apiFetch(
1233
+ config,
1234
+ `/api/sessions${query.size ? `?${query.toString()}` : ""}`
1235
+ );
1236
+ if (isJson(command, options)) {
1237
+ writeJson(payload);
1238
+ return;
1239
+ }
1240
+ if (payload.sessions.length === 0) {
1241
+ console.log("No sessions found.");
1242
+ return;
1243
+ }
1244
+ for (const session of payload.sessions) {
1245
+ const id = String(session.id ?? session.sessionId ?? "");
1246
+ const status = String(session.status ?? "");
1247
+ const title = typeof session.title === "string" ? ` ${session.title}` : "";
1248
+ console.log(`${id} ${status}${title}`);
1249
+ }
1250
+ if (payload.nextCursor) console.log(`Next cursor: ${payload.nextCursor}`);
1251
+ }
1252
+ async function getSessionCommand(sessionId, options, command) {
1253
+ const runtime = getRuntimeOptions(command, options);
1254
+ const config = requireConfig(runtime);
1255
+ const payload = await apiFetch(config, `/api/sessions/${sessionId}`);
1256
+ if (isJson(command, options)) {
1257
+ writeJson(payload);
1258
+ return;
1259
+ }
1260
+ const session = payload.session && typeof payload.session === "object" ? payload.session : payload;
1261
+ console.log(`Session: ${String(session.sessionId ?? session.id ?? sessionId)}`);
1262
+ console.log(`Status: ${String(session.status ?? "unknown")}`);
1263
+ if (session.repoUrl) console.log(`Repo: ${String(session.repoUrl)}`);
1264
+ if (session.title) console.log(`Title: ${String(session.title)}`);
1265
+ }
1266
+ async function sessionEventsCommand(sessionId, options, command) {
1267
+ if (options.follow) {
1268
+ if (options.beforeSequence) {
1269
+ throw new CliError("user", "--before-sequence cannot be used with --follow.");
1270
+ }
1271
+ if (options.promptId) {
1272
+ throw new CliError("user", "--prompt-id cannot be used with --follow because the follow endpoint does not support prompt filtering.");
1273
+ }
1274
+ await watchCommand(sessionId, { ...options, pollInterval: options.pollInterval, afterSequence: options.afterSequence, limit: options.limit }, command);
1275
+ return;
1276
+ }
1277
+ const runtime = getRuntimeOptions(command, options);
1278
+ const config = requireConfig(runtime);
1279
+ const query = new URLSearchParams();
1280
+ if (options.afterSequence) query.set("after_sequence", options.afterSequence);
1281
+ if (options.beforeSequence) query.set("before_sequence", options.beforeSequence);
1282
+ if (options.promptId) query.set("prompt_id", options.promptId);
1283
+ if (options.limit) query.set("limit", options.limit);
1284
+ const payload = await apiFetch(config, `/api/sessions/${sessionId}/events/history${query.size ? `?${query.toString()}` : ""}`);
1285
+ if (isJson(command, options)) {
1286
+ writeJson(payload);
1287
+ return;
1288
+ }
1289
+ const promptLabels = await fetchPromptLabels(config, sessionId).catch(() => /* @__PURE__ */ new Map());
1290
+ const state = { promptLabels, toolCalls: /* @__PURE__ */ new Map() };
1291
+ let textOpen = false;
1292
+ for (const event of payload.events) {
1293
+ const rendered = renderWatchEvent(event, state);
1294
+ if (rendered?.kind === "line") {
1295
+ if (textOpen) {
1296
+ process.stdout.write("\n");
1297
+ textOpen = false;
1298
+ }
1299
+ console.log(rendered.line);
1300
+ }
1301
+ if (rendered?.kind === "text" && rendered.text) {
1302
+ process.stdout.write(rendered.text);
1303
+ textOpen = true;
1304
+ }
906
1305
  }
907
1306
  if (textOpen) process.stdout.write("\n");
908
1307
  }
1308
+ async function usageCommand(sessionId, options, command) {
1309
+ const runtime = getRuntimeOptions(command, options);
1310
+ const config = requireConfig(runtime);
1311
+ const payload = await apiFetch(config, `/api/sessions/${sessionId}/usage`);
1312
+ if (isJson(command, options)) {
1313
+ writeJson(payload);
1314
+ return;
1315
+ }
1316
+ const usage = payload.usage && typeof payload.usage === "object" ? payload.usage : payload;
1317
+ console.log(`Input tokens: ${String(usage.inputTokens ?? 0)}`);
1318
+ console.log(`Output tokens: ${String(usage.outputTokens ?? 0)}`);
1319
+ console.log(`Total tokens: ${String(usage.totalTokens ?? 0)}`);
1320
+ if (usage.totalCostUsd !== void 0) console.log(`Cost: ${String(usage.totalCostUsd)}`);
1321
+ }
1322
+
1323
+ // src/commands/stop.ts
1324
+ async function stopCommand(sessionId, options = {}, command) {
1325
+ const runtime = getRuntimeOptions(command, options);
1326
+ const config = requireConfig(runtime);
1327
+ const response = await apiFetch(config, `/api/sessions/${sessionId}/stop`, {
1328
+ method: "POST"
1329
+ });
1330
+ const status = response.status ?? "stopping";
1331
+ if (isJson(command, options)) {
1332
+ writeJson({ sessionId, status });
1333
+ return;
1334
+ }
1335
+ console.log(status === "already_stopped" ? `Session ${sessionId} is already stopped.` : `Stop requested for session ${sessionId}.`);
1336
+ }
1337
+
1338
+ // src/commands/tokens.ts
1339
+ async function listTokensCommand(options, command) {
1340
+ const runtime = getRuntimeOptions(command, options);
1341
+ const config = requireConfig(runtime);
1342
+ const query = new URLSearchParams();
1343
+ if (options.limit) query.set("limit", options.limit);
1344
+ if (options.cursor) query.set("cursor", options.cursor);
1345
+ const payload = await apiFetch(config, `/api/cli-tokens${query.size ? `?${query.toString()}` : ""}`);
1346
+ if (isJson(command, options)) {
1347
+ writeJson(payload);
1348
+ return;
1349
+ }
1350
+ if (payload.data.length === 0) {
1351
+ console.log("No CLI tokens found.");
1352
+ return;
1353
+ }
1354
+ for (const token of payload.data) {
1355
+ console.log(`${String(token.id)} ${String(token.scope)} ${String(token.tokenPrefix)} ${String(token.revokedAt ? "revoked" : "active")}`);
1356
+ }
1357
+ if (payload.nextCursor) console.log(`Next cursor: ${payload.nextCursor}`);
1358
+ }
1359
+ async function createTokenCommand(options, command) {
1360
+ const runtime = getRuntimeOptions(command, options);
1361
+ const config = requireConfig(runtime);
1362
+ const expiresInDays = options.expiresInDays === void 0 ? void 0 : Number(options.expiresInDays);
1363
+ if (expiresInDays !== void 0 && (!Number.isInteger(expiresInDays) || expiresInDays <= 0)) {
1364
+ throw new CliError("user", "expiresInDays must be a positive integer.");
1365
+ }
1366
+ const payload = await apiFetch(config, "/api/cli-tokens", {
1367
+ method: "POST",
1368
+ body: JSON.stringify({
1369
+ scope: options.scope ?? "read",
1370
+ ...expiresInDays !== void 0 ? { expiresInDays } : {}
1371
+ })
1372
+ });
1373
+ if (isJson(command, options)) {
1374
+ writeJson(payload);
1375
+ return;
1376
+ }
1377
+ console.log(`Token: ${String(payload.token)}`);
1378
+ console.log(`ID: ${String(payload.id)}`);
1379
+ console.log(`Scope: ${String(payload.scope)}`);
1380
+ }
1381
+ async function revokeTokenCommand(tokenId, options, command) {
1382
+ if (isJson(command, options) && options.yes !== true) {
1383
+ throw new CliError("user", "`tokens revoke --json` requires --yes.");
1384
+ }
1385
+ if (options.yes !== true) {
1386
+ await confirmOrThrow(`Revoke CLI token ${tokenId}?`);
1387
+ }
1388
+ const runtime = getRuntimeOptions(command, options);
1389
+ const config = requireConfig(runtime);
1390
+ const payload = await apiFetch(config, `/api/cli-tokens/${tokenId}/revoke`, { method: "POST" });
1391
+ if (isJson(command, options)) {
1392
+ writeJson(payload);
1393
+ return;
1394
+ }
1395
+ console.log(`Revoked token ${tokenId}.`);
1396
+ }
1397
+
1398
+ // src/commands/transcript.ts
1399
+ async function transcriptCommand(sessionId, options, command) {
1400
+ const runtime = getRuntimeOptions(command, options);
1401
+ const config = requireConfig(runtime);
1402
+ const exportData = await apiFetch(config, `/api/sessions/${sessionId}/export`);
1403
+ if (isJson(command, options)) {
1404
+ writeJson(exportData);
1405
+ return;
1406
+ }
1407
+ console.log(renderSessionTranscript(exportData));
1408
+ }
909
1409
 
910
1410
  // src/index.ts
911
- var require2 = createRequire(import.meta.url);
912
- var { version } = require2("../package.json");
913
- var program = new Command().name("arcanist").description("Arcanist CLI").version(version);
914
- 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(loginCommand);
915
- program.command("create").description("Create a session and send a prompt").argument("<repo-url>", "Repository URL").argument("<prompt>", "Prompt to send").option("--model <model>", "Model to use").action(createCommand);
916
- program.command("message").description("Send a message to an existing session").argument("<session-id>", "Session ID").argument("<prompt>", "Message to send").action(messageCommand);
917
- program.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").action(stopCommand);
918
- program.command("transcript").description("Render a session transcript").argument("<session-id>", "Session ID").option("--json", "Output raw session export JSON").action(transcriptCommand);
919
- 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(watchCommand);
1411
+ var require3 = createRequire2(import.meta.url);
1412
+ var { version: version2 } = require3("../package.json");
1413
+ 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", `
1414
+ Examples:
1415
+ arcanist auth login --token-stdin
1416
+ arcanist sessions create https://github.com/org/repo "fix bug" --json | jq -r .sessionId
1417
+ printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
1418
+ arcanist sessions events <session-id> --follow --json
1419
+
1420
+ Exit codes:
1421
+ 0 ok, 1 user/input, 2 auth, 3 not found, 4 conflict, 10 server/network, 130 interrupted
1422
+ `);
1423
+ program.configureOutput({
1424
+ writeErr: (str) => process.stderr.write(str)
1425
+ });
1426
+ program.hook("preAction", (_thisCommand, actionCommand) => {
1427
+ applyColorEnvironment(getRuntimeOptions(actionCommand));
1428
+ });
1429
+ function addCreateOptions(cmd) {
1430
+ 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", `
1431
+ Examples:
1432
+ arcanist sessions create https://github.com/org/repo "fix bug"
1433
+ printf "fix bug" | arcanist sessions create https://github.com/org/repo --prompt-stdin --json
1434
+ arcanist sessions create https://github.com/org/repo - --json | jq -r .sessionId | xargs -I{} arcanist sessions events {} --follow --json
1435
+ `);
1436
+ }
1437
+ function addSendOptions(cmd) {
1438
+ 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", `
1439
+ Examples:
1440
+ arcanist sessions send <session-id> "also update tests"
1441
+ printf "also update tests" | arcanist sessions send <session-id> --prompt-stdin --json
1442
+ `);
1443
+ }
1444
+ var auth = program.command("auth").description("Authentication commands");
1445
+ 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", `
1446
+ Examples:
1447
+ arcanist auth login
1448
+ printf "arc_..." | arcanist auth login --token-stdin
1449
+ ARCANIST_TOKEN=arc_... arcanist auth whoami --json
1450
+ `).action((options, command) => loginCommand(options, command));
1451
+ auth.command("whoami").description("Print the authenticated user and token scope").addHelpText("after", `
1452
+ Examples:
1453
+ arcanist auth whoami
1454
+ ARCANIST_TOKEN=arc_... arcanist auth whoami --json
1455
+ `).action((options, command) => whoamiCommand(options, command));
1456
+ var sessions = program.command("sessions").description("Session commands");
1457
+ addCreateOptions(sessions.command("create").description("Create a session and send a prompt")).action((repoUrl, prompt, options, command) => createCommand(repoUrl, prompt, options, command));
1458
+ addSendOptions(sessions.command("send").description("Send a message to an existing session")).action((sessionId, prompt, options, command) => messageCommand(sessionId, prompt, options, command));
1459
+ sessions.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").addHelpText("after", `
1460
+ Examples:
1461
+ arcanist sessions stop <session-id>
1462
+ arcanist sessions stop <session-id> --json
1463
+ `).action((sessionId, options, command) => stopCommand(sessionId, options, command));
1464
+ sessions.command("get").description("Get session details").argument("<session-id>", "Session ID").addHelpText("after", `
1465
+ Examples:
1466
+ arcanist sessions get <session-id>
1467
+ arcanist sessions get <session-id> --json
1468
+ `).action((sessionId, options, command) => getSessionCommand(sessionId, options, command));
1469
+ 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", `
1470
+ Examples:
1471
+ arcanist sessions list
1472
+ arcanist sessions list --status idle --json
1473
+ `).action((options, command) => listSessionsCommand(options, command));
1474
+ 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", `
1475
+ Examples:
1476
+ arcanist sessions events <session-id> --json
1477
+ arcanist sessions events <session-id> --follow --json
1478
+ `).action((sessionId, options, command) => sessionEventsCommand(sessionId, options, command));
1479
+ sessions.command("transcript").description("Render a session transcript").argument("<session-id>", "Session ID").addHelpText("after", `
1480
+ Examples:
1481
+ arcanist sessions transcript <session-id>
1482
+ arcanist sessions transcript <session-id> --json
1483
+ `).action((sessionId, options, command) => transcriptCommand(sessionId, options, command));
1484
+ 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", `
1485
+ Examples:
1486
+ arcanist sessions watch <session-id>
1487
+ arcanist sessions watch <session-id> --json
1488
+ `).action((sessionId, options, command) => watchCommand(sessionId, options, command));
1489
+ sessions.command("usage").description("Get token usage for a session").argument("<session-id>", "Session ID").addHelpText("after", `
1490
+ Examples:
1491
+ arcanist sessions usage <session-id>
1492
+ arcanist sessions usage <session-id> --json
1493
+ `).action((sessionId, options, command) => usageCommand(sessionId, options, command));
1494
+ var tokens = program.command("tokens").description("CLI token commands");
1495
+ 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));
1496
+ 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", `
1497
+ Examples:
1498
+ arcanist tokens create --scope read
1499
+ arcanist tokens create --scope read --json
1500
+ `).action((options, command) => createTokenCommand(options, command));
1501
+ tokens.command("revoke").description("Revoke a CLI token").argument("<id>", "Token ID").option("--yes", "Confirm revocation without prompting").addHelpText("after", `
1502
+ Examples:
1503
+ arcanist tokens revoke 42
1504
+ arcanist tokens revoke 42 --yes --json
1505
+ `).action((id, options, command) => revokeTokenCommand(id, options, command));
1506
+ 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) => {
1507
+ printDeprecatedAlias("login", "auth login", command);
1508
+ return loginCommand(options, command);
1509
+ });
1510
+ addCreateOptions(program.command("create").description("Create a session and send a prompt")).action((repoUrl, prompt, options, command) => {
1511
+ printDeprecatedAlias("create", "sessions create", command);
1512
+ return createCommand(repoUrl, prompt, options, command);
1513
+ });
1514
+ addSendOptions(program.command("message").description("Send a message to an existing session")).action((sessionId, prompt, options, command) => {
1515
+ printDeprecatedAlias("message", "sessions send", command);
1516
+ return messageCommand(sessionId, prompt, options, command);
1517
+ });
1518
+ program.command("stop").description("Stop the active run for a session").argument("<session-id>", "Session ID").action((sessionId, options, command) => {
1519
+ printDeprecatedAlias("stop", "sessions stop", command);
1520
+ return stopCommand(sessionId, options, command);
1521
+ });
1522
+ program.command("transcript").description("Render a session transcript").argument("<session-id>", "Session ID").action((sessionId, options, command) => {
1523
+ printDeprecatedAlias("transcript", "sessions transcript", command);
1524
+ return transcriptCommand(sessionId, options, command);
1525
+ });
1526
+ 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) => {
1527
+ printDeprecatedAlias("watch", "sessions events --follow", command);
1528
+ return watchCommand(sessionId, options, command);
1529
+ });
920
1530
  async function main() {
921
1531
  try {
922
1532
  await program.parseAsync(process.argv);
923
1533
  } catch (err) {
924
- console.error(`Error: ${err instanceof Error ? err.message : "An unexpected error occurred."}`);
925
- process.exit(1);
1534
+ if (isCommanderHelpOrVersion(err)) {
1535
+ process.exit(0);
1536
+ }
1537
+ if (err instanceof CliError && err.exitCode === 130) restoreTerminal();
1538
+ const cliError = toCliError(err);
1539
+ const options = program.opts();
1540
+ if (options.json) {
1541
+ process.stderr.write(`${formatJsonError(cliError)}
1542
+ `);
1543
+ } else {
1544
+ process.stderr.write(`Error: ${cliError.message}
1545
+ `);
1546
+ if (cliError.hint) process.stderr.write(`Hint: ${cliError.hint}
1547
+ `);
1548
+ }
1549
+ process.exit(cliError.exitCode);
926
1550
  }
927
1551
  }
1552
+ function isCommanderHelpOrVersion(err) {
1553
+ if (!err || typeof err !== "object") return false;
1554
+ const code = err.code;
1555
+ return code === "commander.helpDisplayed" || code === "commander.version";
1556
+ }
1557
+ function restoreTerminal() {
1558
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1559
+ process.stdout.write("\x1B[?25h");
1560
+ }
928
1561
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {