@tryarcanist/cli 0.1.7 → 0.1.9

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 +235 -70
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,9 +4,14 @@
4
4
  import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
+ // ../../shared/utils/url.ts
8
+ function normalizeBaseUrl(url) {
9
+ return url.replace(/\/+$/, "");
10
+ }
11
+
7
12
  // src/api.ts
8
13
  async function apiRequest(config, path, init) {
9
- const res = await fetch(`${config.apiUrl}${path}`, {
14
+ const res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
10
15
  ...init,
11
16
  headers: {
12
17
  "Content-Type": "application/json",
@@ -40,14 +45,15 @@ var CONFIG_DIR = join(homedir(), ".arcanist");
40
45
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
41
46
  function loadConfig() {
42
47
  try {
43
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
48
+ const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
49
+ return { ...config, apiUrl: normalizeBaseUrl(config.apiUrl) };
44
50
  } catch {
45
51
  return null;
46
52
  }
47
53
  }
48
54
  function saveConfig(config) {
49
55
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
50
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
56
+ writeFileSync(CONFIG_FILE, JSON.stringify({ ...config, apiUrl: normalizeBaseUrl(config.apiUrl) }, null, 2) + "\n", { mode: 384 });
51
57
  }
52
58
  function requireConfig() {
53
59
  const config = loadConfig();
@@ -146,7 +152,7 @@ async function loginCommand(options) {
146
152
  process.exit(1);
147
153
  }
148
154
  }
149
- const apiUrl = options.apiUrl ?? loadConfig()?.apiUrl ?? "https://app.tryarcanist.com";
155
+ const apiUrl = normalizeBaseUrl(options.apiUrl ?? loadConfig()?.apiUrl ?? "https://app.tryarcanist.com");
150
156
  saveConfig({ apiUrl, token });
151
157
  console.log(`Logged in. API: ${apiUrl}`);
152
158
  try {
@@ -219,151 +225,285 @@ async function stopCommand(sessionId) {
219
225
  }
220
226
  }
221
227
 
222
- // src/utils/session-output.ts
228
+ // ../../shared/transcript/projector.ts
229
+ var DUPLICATE_TEXT_DELTA_MIN_CHARS = 24;
230
+ var RAW_OPENCODE_NOISE = /* @__PURE__ */ new Set([
231
+ "session.updated",
232
+ "session.diff",
233
+ "server.heartbeat",
234
+ "session.idle",
235
+ "lsp.updated",
236
+ "lsp.client.diagnostics"
237
+ ]);
238
+ function shouldAppendTextDelta(existingText, incomingText) {
239
+ if (!incomingText) return false;
240
+ if (incomingText.length < DUPLICATE_TEXT_DELTA_MIN_CHARS) return true;
241
+ return !existingText.endsWith(incomingText);
242
+ }
243
+ function isRecord(value) {
244
+ return !!value && typeof value === "object" && !Array.isArray(value);
245
+ }
246
+ function resolveEventId(data, prefix, index) {
247
+ return typeof data?.id === "string" ? data.id : `${prefix}-${index}`;
248
+ }
249
+ function resolveTextValue(data) {
250
+ const text = data?.text;
251
+ if (typeof text === "string") return text;
252
+ const content = data?.content;
253
+ return typeof content === "string" ? content : "";
254
+ }
255
+ function resolvePromptId(data) {
256
+ return typeof data?.promptId === "string" ? data.promptId : void 0;
257
+ }
258
+ function mergeToolCall(previous, incoming) {
259
+ return {
260
+ ...incoming,
261
+ ...incoming.promptId === void 0 && previous.promptId !== void 0 ? { promptId: previous.promptId } : {},
262
+ ...incoming.input === void 0 && previous.input !== void 0 ? { input: previous.input } : {},
263
+ ...incoming.toolStatus === void 0 && previous.toolStatus !== void 0 ? { toolStatus: previous.toolStatus } : {},
264
+ ...incoming.truncated === void 0 && previous.truncated !== void 0 ? { truncated: previous.truncated } : {},
265
+ ...incoming.inputEstimatedTokens === void 0 && previous.inputEstimatedTokens !== void 0 ? { inputEstimatedTokens: previous.inputEstimatedTokens } : {},
266
+ ...incoming.outputEstimatedTokens === void 0 && previous.outputEstimatedTokens !== void 0 ? { outputEstimatedTokens: previous.outputEstimatedTokens } : {},
267
+ ...incoming.outputChars === void 0 && previous.outputChars !== void 0 ? { outputChars: previous.outputChars } : {},
268
+ ...incoming.duplicateCount === void 0 && previous.duplicateCount !== void 0 ? { duplicateCount: previous.duplicateCount } : {}
269
+ };
270
+ }
271
+ function normalizeToolStatus(value) {
272
+ return value === "running" || value === "completed" || value === "error" ? value : void 0;
273
+ }
223
274
  function flattenSessionEvents(raw) {
224
275
  const merged = [];
225
276
  const streamableIndexById = /* @__PURE__ */ new Map();
226
277
  const toolCallIndexById = /* @__PURE__ */ new Map();
278
+ const questionIndexById = /* @__PURE__ */ new Map();
227
279
  for (const event of raw) {
228
- const data = event.data ?? {};
280
+ const data = isRecord(event.data) ? event.data : void 0;
229
281
  if (event.type === "sandbox_compaction_start") {
230
- const entry2 = { type: "compaction_start", id: `cs-${data.timestamp ?? merged.length}` };
231
- if (data.contextTokens != null) entry2.contextTokens = data.contextTokens;
232
- merged.push(entry2);
282
+ merged.push({
283
+ type: "compaction_start",
284
+ id: `cs-${data?.timestamp ?? merged.length}`,
285
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
286
+ ...typeof data?.contextTokens === "number" ? { contextTokens: data.contextTokens } : {}
287
+ });
233
288
  continue;
234
289
  }
235
290
  if (event.type === "sandbox_compaction_complete") {
236
- const entry2 = { type: "compaction_complete", id: `cc-${data.timestamp ?? merged.length}` };
237
- if (data.contextTokensBefore != null) entry2.contextTokensBefore = data.contextTokensBefore;
238
- if (data.contextTokensAfter != null) entry2.contextTokensAfter = data.contextTokensAfter;
239
- merged.push(entry2);
291
+ merged.push({
292
+ type: "compaction_complete",
293
+ id: `cc-${data?.timestamp ?? merged.length}`,
294
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
295
+ ...typeof data?.contextTokensBefore === "number" ? { contextTokensBefore: data.contextTokensBefore } : {},
296
+ ...typeof data?.contextTokensAfter === "number" ? { contextTokensAfter: data.contextTokensAfter } : {}
297
+ });
240
298
  continue;
241
299
  }
242
300
  if (event.type === "sandbox_context_fill_warning") {
243
- const entry2 = {
301
+ merged.push({
244
302
  type: "context_fill_warning",
245
- id: `cfw-${data.timestamp ?? merged.length}`,
246
- fillPercent: Number(data.fillPercent ?? 0)
247
- };
248
- if (data.contextTokens != null) entry2.contextTokens = data.contextTokens;
249
- if (data.contextWindow != null) entry2.contextWindow = data.contextWindow;
250
- merged.push(entry2);
303
+ id: `cfw-${data?.timestamp ?? merged.length}`,
304
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
305
+ fillPercent: typeof data?.fillPercent === "number" ? data.fillPercent : Number(data?.fillPercent ?? 0),
306
+ ...typeof data?.contextTokens === "number" ? { contextTokens: data.contextTokens } : {},
307
+ ...typeof data?.contextWindow === "number" ? { contextWindow: data.contextWindow } : {}
308
+ });
251
309
  continue;
252
310
  }
253
311
  if (event.type === "sandbox_tool_truncated") {
254
312
  merged.push({
255
313
  type: "tool_truncated",
256
- id: String(data.callId ?? merged.length),
257
- tool: String(data.tool ?? "")
314
+ id: typeof data?.callId === "string" ? data.callId : String(merged.length),
315
+ tool: typeof data?.tool === "string" ? data.tool : "",
316
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {}
317
+ });
318
+ continue;
319
+ }
320
+ if (event.type === "retry_status") {
321
+ merged.push({
322
+ type: "retry_status",
323
+ id: `rs-${data?.timestamp ?? merged.length}`,
324
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
325
+ attempt: typeof data?.attempt === "number" ? data.attempt : Number(data?.attempt ?? 0),
326
+ message: typeof data?.message === "string" ? data.message : "Retrying...",
327
+ ...typeof data?.nextRetryAt === "string" ? { nextRetryAt: data.nextRetryAt } : {},
328
+ ...typeof data?.provider === "string" ? { provider: data.provider } : {},
329
+ ...typeof data?.errorCode === "string" ? { errorCode: data.errorCode } : {}
258
330
  });
259
331
  continue;
260
332
  }
261
333
  if (event.type === "branch_changed") {
262
- merged.push({ type: "branch_changed", id: `bc-${data.timestamp ?? merged.length}`, branch: String(data.branch ?? "") });
334
+ merged.push({
335
+ type: "branch_changed",
336
+ id: `bc-${data?.timestamp ?? merged.length}`,
337
+ branch: typeof data?.branch === "string" ? data.branch : "",
338
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {}
339
+ });
263
340
  continue;
264
341
  }
265
342
  if (event.type === "session_error") {
266
- const entry2 = {
343
+ merged.push({
267
344
  type: "session_error",
268
- id: String(data.id ?? `err-${merged.length}`),
269
- error: String(data.error ?? "Unknown error")
270
- };
271
- if (data.code) entry2.code = String(data.code);
272
- merged.push(entry2);
345
+ id: resolveEventId(data, "err", merged.length),
346
+ error: typeof data?.error === "string" ? data.error : "Unknown error",
347
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
348
+ ...typeof data?.code === "string" ? { code: data.code } : {}
349
+ });
273
350
  continue;
274
351
  }
275
352
  if (event.type === "raw_opencode") {
276
- merged.push({ type: "raw_opencode", id: String(data.id ?? `raw-${merged.length}`), data });
353
+ const partType = typeof data?.partType === "string" ? data.partType : void 0;
354
+ const eventType = typeof data?.eventType === "string" ? data.eventType : void 0;
355
+ if (partType === "text") continue;
356
+ if (eventType && RAW_OPENCODE_NOISE.has(eventType)) continue;
357
+ merged.push({
358
+ type: "raw_opencode",
359
+ id: resolveEventId(data, "raw", merged.length),
360
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
361
+ ...partType ? { partType } : {},
362
+ ...eventType ? { eventType } : {},
363
+ data: data ?? {}
364
+ });
277
365
  continue;
278
366
  }
279
367
  if (event.type === "reasoning") {
280
- const id = String(data.id ?? `reasoning-${merged.length}`);
281
- const text = String(data.text ?? "");
368
+ const id = resolveEventId(data, "reasoning", merged.length);
369
+ const text = resolveTextValue(data);
370
+ const promptId = resolvePromptId(data);
282
371
  const existingIdx = streamableIndexById.get(id);
283
372
  if (existingIdx !== void 0) {
284
373
  const existing = merged[existingIdx];
285
- if (existing.type === "reasoning") existing.text += text;
374
+ if (existing.type === "reasoning" && shouldAppendTextDelta(existing.text, text)) {
375
+ existing.text += text;
376
+ if (!existing.promptId && promptId) existing.promptId = promptId;
377
+ }
286
378
  } else {
287
379
  streamableIndexById.set(id, merged.length);
288
- merged.push({ type: "reasoning", id, text });
380
+ merged.push({
381
+ type: "reasoning",
382
+ id,
383
+ text,
384
+ ...promptId ? { promptId } : {}
385
+ });
289
386
  }
290
387
  continue;
291
388
  }
292
389
  if (event.type === "patch") {
293
- const files = Array.isArray(data.files) ? data.files.filter((item) => typeof item === "string") : [];
294
- if (files.length > 0) merged.push({ type: "patch", id: `patch-${merged.length}`, files });
390
+ const files = Array.isArray(data?.files) ? data.files.filter((item) => typeof item === "string") : [];
391
+ if (files.length > 0) {
392
+ merged.push({ type: "patch", id: `patch-${merged.length}`, files });
393
+ if (resolvePromptId(data)) {
394
+ const last = merged[merged.length - 1];
395
+ if (last?.type === "patch") last.promptId = resolvePromptId(data);
396
+ }
397
+ }
295
398
  continue;
296
399
  }
297
400
  if (event.type === "todo_update") {
298
- const todos = Array.isArray(data.todos) ? data.todos : [];
401
+ const todos = Array.isArray(data?.todos) ? data.todos : [];
299
402
  if (todos.length > 0) {
300
- for (let i = merged.length - 1; i >= 0; i--) {
301
- const existing = merged[i];
403
+ for (let index = merged.length - 1; index >= 0; index--) {
404
+ const existing = merged[index];
302
405
  if (existing.type === "tool_call" && existing.tool.toLowerCase() === "todowrite") {
303
- existing.input = { ...existing.input, todos };
406
+ merged[index] = { ...existing, input: { ...existing.input, todos } };
304
407
  break;
305
408
  }
306
409
  }
307
410
  }
308
411
  continue;
309
412
  }
413
+ if (event.type === "answer") {
414
+ const questionId = typeof data?.id === "string" ? data.id : void 0;
415
+ if (!questionId) continue;
416
+ const existingIdx = questionIndexById.get(questionId);
417
+ if (existingIdx !== void 0) {
418
+ const existing = merged[existingIdx];
419
+ if (existing?.type === "question") {
420
+ merged[existingIdx] = {
421
+ ...existing,
422
+ answer: data?.answer == null ? null : String(data.answer)
423
+ };
424
+ }
425
+ }
426
+ continue;
427
+ }
310
428
  if (event.type !== "text" && event.type !== "tool_call" && event.type !== "tool_update" && event.type !== "question") {
311
429
  continue;
312
430
  }
313
431
  if (event.type === "text") {
314
- const id = String(data.id ?? `text-${merged.length}`);
315
- const text = String(data.text ?? "");
432
+ const id = resolveEventId(data, "text", merged.length);
433
+ const text = resolveTextValue(data);
434
+ const promptId = resolvePromptId(data);
316
435
  const existingIdx = streamableIndexById.get(id);
317
436
  if (existingIdx !== void 0) {
318
437
  const existing = merged[existingIdx];
319
- if (existing.type === "text") existing.text += text;
438
+ if (existing.type === "text" && shouldAppendTextDelta(existing.text, text)) {
439
+ existing.text += text;
440
+ if (!existing.promptId && promptId) existing.promptId = promptId;
441
+ }
320
442
  } else {
321
443
  streamableIndexById.set(id, merged.length);
322
- merged.push({ type: "text", id, text });
444
+ merged.push({
445
+ type: "text",
446
+ id,
447
+ text,
448
+ ...promptId ? { promptId } : {}
449
+ });
323
450
  }
324
451
  continue;
325
452
  }
326
453
  if (event.type === "tool_call") {
327
- const id = String(data.id ?? `tool-${merged.length}`);
328
- const entry2 = {
454
+ const id = resolveEventId(data, "tool", merged.length);
455
+ const nextEntry = {
329
456
  type: "tool_call",
330
457
  id,
331
- tool: String(data.tool ?? "unknown"),
332
- summary: String(data.summary ?? "")
458
+ tool: typeof data?.tool === "string" ? data.tool : typeof data?.toolName === "string" ? data.toolName : "unknown",
459
+ summary: typeof data?.summary === "string" ? data.summary : "",
460
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
461
+ ...isRecord(data?.input) ? { input: data.input } : {},
462
+ ...normalizeToolStatus(data?.toolStatus) ? { toolStatus: normalizeToolStatus(data?.toolStatus) } : {},
463
+ ...typeof data?.truncated === "boolean" ? { truncated: data.truncated } : {},
464
+ ...typeof data?.inputEstimatedTokens === "number" ? { inputEstimatedTokens: data.inputEstimatedTokens } : {},
465
+ ...typeof data?.outputEstimatedTokens === "number" ? { outputEstimatedTokens: data.outputEstimatedTokens } : {},
466
+ ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {}
333
467
  };
334
- if (data.input && typeof data.input === "object") entry2.input = data.input;
335
468
  const existingIdx = toolCallIndexById.get(id);
336
469
  if (existingIdx !== void 0) {
337
- const prev = merged[existingIdx];
338
- if (prev.type === "tool_call") {
339
- if (prev.toolStatus) entry2.toolStatus = prev.toolStatus;
340
- merged[existingIdx] = entry2;
470
+ const previous = merged[existingIdx];
471
+ if (previous.type === "tool_call") {
472
+ merged[existingIdx] = mergeToolCall(previous, nextEntry);
341
473
  }
342
474
  } else {
343
475
  toolCallIndexById.set(id, merged.length);
344
- merged.push(entry2);
476
+ merged.push(nextEntry);
345
477
  }
346
478
  continue;
347
479
  }
348
480
  if (event.type === "tool_update") {
349
- const id = String(data.id ?? "");
481
+ const id = typeof data?.id === "string" ? data.id : "";
350
482
  const existingIdx = toolCallIndexById.get(id);
351
483
  if (existingIdx !== void 0) {
352
- const prev = merged[existingIdx];
353
- if (prev.type === "tool_call" && data.status) {
354
- prev.toolStatus = String(data.status);
484
+ const previous = merged[existingIdx];
485
+ if (previous.type === "tool_call") {
486
+ merged[existingIdx] = {
487
+ ...previous,
488
+ ...normalizeToolStatus(data?.status) ? { toolStatus: normalizeToolStatus(data?.status) } : {},
489
+ ...typeof data?.outputEstimatedTokens === "number" ? { outputEstimatedTokens: data.outputEstimatedTokens } : {},
490
+ ...typeof data?.outputChars === "number" ? { outputChars: data.outputChars } : {},
491
+ ...typeof data?.truncated === "boolean" ? { truncated: data.truncated } : {}
492
+ };
355
493
  }
356
494
  }
357
495
  continue;
358
496
  }
359
- const entry = {
497
+ const question = {
360
498
  type: "question",
361
- id: String(data.id ?? `question-${merged.length}`),
362
- question: String(data.question ?? ""),
363
- answer: data.answer == null ? null : String(data.answer)
499
+ id: resolveEventId(data, "question", merged.length),
500
+ question: typeof data?.question === "string" ? data.question : "",
501
+ answer: data?.answer == null ? null : String(data.answer),
502
+ ...resolvePromptId(data) ? { promptId: resolvePromptId(data) } : {},
503
+ ...Array.isArray(data?.options) ? { options: data.options } : {}
364
504
  };
365
- if (Array.isArray(data.options)) entry.options = data.options;
366
- merged.push(entry);
505
+ questionIndexById.set(question.id, merged.length);
506
+ merged.push(question);
367
507
  }
368
508
  return merged;
369
509
  }
@@ -372,17 +512,39 @@ function partitionEventsByPrompt(allEvents, promptIds) {
372
512
  const buckets = new Map(promptIds.map((id) => [id, []]));
373
513
  let currentPromptId = null;
374
514
  for (const event of allEvents) {
515
+ const explicitPromptId = typeof event.data?.promptId === "string" && promptSet.has(event.data.promptId) ? event.data.promptId : null;
375
516
  if (event.type === "prompt_processing") {
376
- const promptId = typeof event.data?.promptId === "string" ? event.data.promptId : null;
377
- currentPromptId = promptId && promptSet.has(promptId) ? promptId : null;
517
+ currentPromptId = explicitPromptId;
378
518
  }
379
- if (currentPromptId) {
380
- const bucket = buckets.get(currentPromptId);
519
+ const targetPromptId = explicitPromptId ?? currentPromptId;
520
+ if (targetPromptId) {
521
+ const bucket = buckets.get(targetPromptId);
381
522
  if (bucket) bucket.push(event);
382
523
  }
383
524
  }
384
525
  return buckets;
385
526
  }
527
+ function getEmbeddedTerminalHistory(raw) {
528
+ for (let index = raw.length - 1; index >= 0; index--) {
529
+ const event = raw[index];
530
+ if (event.type !== "prompt_completed" && event.type !== "prompt_failed") continue;
531
+ const history = event.data?.history;
532
+ if (!Array.isArray(history) || history.length === 0) return null;
533
+ return history;
534
+ }
535
+ return null;
536
+ }
537
+ function resolveAuthoritativePromptEvents(raw) {
538
+ return getEmbeddedTerminalHistory(raw) ?? raw;
539
+ }
540
+
541
+ // src/utils/session-output.ts
542
+ function flattenSessionEvents2(raw) {
543
+ return flattenSessionEvents(raw);
544
+ }
545
+ function partitionEventsByPrompt2(allEvents, promptIds) {
546
+ return partitionEventsByPrompt(allEvents, promptIds);
547
+ }
386
548
  function formatDate(value) {
387
549
  const date = new Date(value);
388
550
  if (Number.isNaN(date.getTime())) return value;
@@ -408,6 +570,9 @@ ${event.text}
408
570
  return `**Question:** ${event.question}
409
571
  ${event.answer ? `**Answer:** ${event.answer}
410
572
  ` : ""}`;
573
+ case "retry_status":
574
+ return `*[retry ${event.attempt}: ${event.message}]*
575
+ `;
411
576
  case "compaction_start":
412
577
  return event.contextTokens ? `*[compacting context at ${Math.round(event.contextTokens / 1e3)}k tokens]*
413
578
  ` : "*[compacting context]*\n";
@@ -440,7 +605,7 @@ function formatNumber(value) {
440
605
  function renderSessionTranscript(exportData) {
441
606
  const lines = [];
442
607
  const promptIds = exportData.prompts.map((prompt) => prompt.id);
443
- const eventBuckets = partitionEventsByPrompt(exportData.events, promptIds);
608
+ const eventBuckets = partitionEventsByPrompt2(exportData.events, promptIds);
444
609
  lines.push("# Session transcript\n");
445
610
  if (exportData.session.repoUrl) lines.push(`**Repo:** ${exportData.session.repoUrl} `);
446
611
  lines.push(`**Session:** ${exportData.session.id.slice(0, 8)} `);
@@ -454,7 +619,7 @@ function renderSessionTranscript(exportData) {
454
619
  for (let i = 0; i < exportData.prompts.length; i++) {
455
620
  const prompt = exportData.prompts[i];
456
621
  const rawEvents = eventBuckets.get(prompt.id) ?? [];
457
- const events = flattenSessionEvents(rawEvents);
622
+ const events = flattenSessionEvents2(resolveAuthoritativePromptEvents(rawEvents));
458
623
  lines.push("---\n");
459
624
  lines.push(`## Turn ${i + 1}
460
625
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {