carmoji 0.3.9 → 0.3.10

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/carmoji.js +64 -27
  2. package/package.json +1 -1
package/carmoji.js CHANGED
@@ -570,12 +570,27 @@ function applyTranscriptTokenRecord(state, record) {
570
570
  return state;
571
571
  }
572
572
 
573
+ /** One transcript record's assistant prose, if it carries any. */
574
+ function assistantTextOf(record) {
575
+ if (record.type === 'assistant') {
576
+ const texts = (record.message?.content ?? [])
577
+ .filter((block) => block.type === 'text' && block.text)
578
+ .map((block) => block.text);
579
+ if (texts.length) return texts.join('\n');
580
+ }
581
+ if (record.payload?.type === 'agent_message' && record.payload.message) {
582
+ return String(record.payload.message);
583
+ }
584
+ return undefined;
585
+ }
586
+
573
587
  /**
574
- * Cumulative output tokens for this session, counted incrementally from the
575
- * Claude or Codex transcript JSONL. A per-session byte offset is cached so
576
- * each hook only parses lines appended since the previous one.
588
+ * What the transcript gained since the last hook: the session's cumulative
589
+ * output tokens, plus any assistant prose that appeared the play-by-play
590
+ * an agent narrates between tools, and eventually the turn's reply. A
591
+ * per-session byte offset is cached so each hook only parses new lines.
577
592
  */
578
- function sessionTokens(payload) {
593
+ function sessionTranscriptNews(payload) {
579
594
  const transcript = payload.transcript_path;
580
595
  const session = payload.session_id;
581
596
  if (!transcript || !session) return undefined;
@@ -584,6 +599,7 @@ function sessionTokens(payload) {
584
599
  try { state = JSON.parse(readFileSync(statePath, 'utf8')); } catch {}
585
600
  if (!Number.isFinite(state.offset) || state.offset < 0) state.offset = 0;
586
601
  if (!Number.isFinite(state.total) || state.total < 0) state.total = 0;
602
+ const texts = [];
587
603
  let size;
588
604
  try { size = statSync(transcript).size; } catch { return undefined; }
589
605
  if (size < state.offset) state = { offset: 0, total: 0 }; // truncated/rotated
@@ -598,9 +614,14 @@ function sessionTokens(payload) {
598
614
  const lastNewline = chunk.lastIndexOf(0x0a);
599
615
  if (lastNewline >= 0) {
600
616
  for (const line of chunk.subarray(0, lastNewline + 1).toString('utf8').split('\n')) {
601
- if (!line.includes('"usage"') && !line.includes('"token_count"')) continue;
617
+ const wantsTokens = line.includes('"usage"') || line.includes('"token_count"');
618
+ const wantsText = line.includes('"assistant"') || line.includes('agent_message');
619
+ if (!wantsTokens && !wantsText) continue;
602
620
  try {
603
- applyTranscriptTokenRecord(state, JSON.parse(line));
621
+ const record = JSON.parse(line);
622
+ if (wantsTokens) applyTranscriptTokenRecord(state, record);
623
+ const text = assistantTextOf(record);
624
+ if (text && text.trim().length >= 20) texts.push(text);
604
625
  } catch { /* ignore malformed lines */ }
605
626
  }
606
627
  state.offset += lastNewline + 1;
@@ -608,7 +629,7 @@ function sessionTokens(payload) {
608
629
  writeFileSync(statePath, JSON.stringify(state));
609
630
  }
610
631
  }
611
- return state.total;
632
+ return { total: state.total, texts };
612
633
  }
613
634
 
614
635
  /**
@@ -633,16 +654,8 @@ function lastAssistantText(payload) {
633
654
  const line = lines[i];
634
655
  if (!line.includes('"assistant"') && !line.includes('agent_message')) continue;
635
656
  try {
636
- const record = JSON.parse(line);
637
- if (record.type === 'assistant') {
638
- const texts = (record.message?.content ?? [])
639
- .filter((block) => block.type === 'text' && block.text)
640
- .map((block) => block.text);
641
- if (texts.length) return texts.join(' ');
642
- }
643
- if (record.payload?.type === 'agent_message' && record.payload.message) {
644
- return String(record.payload.message);
645
- }
657
+ const text = assistantTextOf(JSON.parse(line));
658
+ if (text) return text;
646
659
  } catch { /* the window's first line may be cut mid-record */ }
647
660
  }
648
661
  } catch { /* no transcript, no say */ }
@@ -655,6 +668,9 @@ const TEST_EVENTS = {
655
668
  say: { event: 'turn_done',
656
669
  detail: 'Done — corners now release on yaw alone with an 8s cap, '
657
670
  + 'and cornerEnded bypasses every gate so the lean never sticks.' },
671
+ note: { event: 'agent_note',
672
+ detail: 'Found it — the release threshold on lateral was 0.078g, '
673
+ + 'which a hand never settles under. Trying a yaw-only exit next.' },
658
674
  edit: { event: 'tool_start', tool: 'Edit' },
659
675
  read: { event: 'tool_start', tool: 'Read' },
660
676
  run: { event: 'tool_start', tool: 'Bash' },
@@ -1418,28 +1434,49 @@ async function main() {
1418
1434
  // sum across them instead of showing whichever reported last.
1419
1435
  const message = { source, session: payload.session_id,
1420
1436
  host: hostname(), ...event };
1421
- // Claude and Codex transcripts power the tok/s readout; other formats
1422
- // quietly produce no token changes.
1423
- const tokens = sessionTokens(payload);
1424
- if (tokens !== undefined) message.tokens = tokens;
1437
+ // Claude and Codex transcripts power the tok/s readout and the
1438
+ // play-by-play; other formats quietly produce neither.
1439
+ const news = sessionTranscriptNews(payload);
1440
+ if (news?.total !== undefined) message.tokens = news.total;
1425
1441
  // Gemini and Trae hooks carry no cwd — the hook itself runs in the
1426
1442
  // project directory, so fall back to that.
1427
1443
  const cwd = payload.cwd || process.cwd();
1428
1444
  if (cwd) message.project = basename(cwd);
1445
+
1446
+ // Prose keeps its newlines and is clipped only when abnormally
1447
+ // long; the one-line permission caption stays terse.
1448
+ const clip = (text, cap) =>
1449
+ String(text).replace(/[ \t]+/g, ' ').trim().slice(0, cap);
1450
+
1451
+ // On Stop the newest text block is the turn's reply; anything
1452
+ // before it — and everything on other hooks — is mid-procedure
1453
+ // narration, shipped as agent_note for the live play-by-play.
1454
+ const newTexts = news?.texts ?? [];
1455
+ const isTurnDone = event.event === 'turn_done';
1456
+ const noteTexts = isTurnDone ? newTexts.slice(0, -1) : newTexts;
1457
+ for (const text of noteTexts.slice(-4)) {
1458
+ await sendAll(devices, { source, session: payload.session_id,
1459
+ host: hostname(), project: message.project,
1460
+ event: 'agent_note',
1461
+ detail: clip(text, 2000) });
1462
+ }
1463
+
1429
1464
  // A snippet of human-readable context for the phone to show. The
1430
- // turn's reply gets more room than a toast line — it feeds the
1431
- // reviewable chat history, not just a caption.
1465
+ // turn's reply gets room — it feeds the reviewable chat history,
1466
+ // not just a caption — with the tail-window read as fallback when
1467
+ // the incremental scan saw nothing new.
1432
1468
  const detail = event.event === 'prompt'
1433
1469
  ? firstString(payload.prompt, payload.user_prompt, payload.userPrompt,
1434
1470
  payload.message, payload.input, payload.text)
1435
1471
  : event.event === 'permission'
1436
1472
  ? firstString(payload.message, payload.detail,
1437
1473
  payload.tool_input ? summarizeToolInput(payload) : undefined)
1438
- : event.event === 'turn_done'
1439
- ? lastAssistantText(payload)
1474
+ : isTurnDone
1475
+ ? (newTexts[newTexts.length - 1] ?? lastAssistantText(payload))
1440
1476
  : undefined;
1441
- const detailCap = event.event === 'turn_done' ? 400 : 140;
1442
- if (detail) message.detail = String(detail).replace(/\s+/g, ' ').slice(0, detailCap);
1477
+ const detailCap = isTurnDone ? 2000
1478
+ : event.event === 'prompt' ? 1000 : 140;
1479
+ if (detail) message.detail = clip(detail, detailCap);
1443
1480
  // Plan-window usage comes from Claude Code's local transcripts;
1444
1481
  // recompute only at turn boundaries.
1445
1482
  if (source === 'claude') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carmoji",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Bring your CarMoji pet to life from Claude Code / Codex — a LAN bridge that streams coding events to the CarMoji iOS app",
5
5
  "type": "module",
6
6
  "bin": {