@tryarcanist/cli 0.1.6 → 0.1.8

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