carmoji 0.3.9 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/carmoji.js +76 -28
- 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
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
637
|
-
if (
|
|
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,60 @@ 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
|
|
1422
|
-
// quietly produce
|
|
1423
|
-
const
|
|
1424
|
-
if (
|
|
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 ships whole and verbatim — markdown indentation, code
|
|
1447
|
+
// blocks, and tables all matter to the phone's renderer, and the
|
|
1448
|
+
// history is a review surface, so no length cap either. Only
|
|
1449
|
+
// trailing per-line whitespace goes. Captions are one-liners, so
|
|
1450
|
+
// they still collapse and clip.
|
|
1451
|
+
const clipProse = (text) =>
|
|
1452
|
+
String(text).replace(/[ \t]+$/gm, '').trim();
|
|
1453
|
+
const clipCaption = (text, cap) =>
|
|
1454
|
+
String(text).replace(/\s+/g, ' ').trim().slice(0, cap);
|
|
1455
|
+
|
|
1456
|
+
// On Stop the newest text block is the turn's reply; anything
|
|
1457
|
+
// before it — and everything on other hooks — is mid-procedure
|
|
1458
|
+
// narration, shipped as agent_note for the live play-by-play.
|
|
1459
|
+
const newTexts = news?.texts ?? [];
|
|
1460
|
+
const isTurnDone = event.event === 'turn_done';
|
|
1461
|
+
const noteTexts = isTurnDone ? newTexts.slice(0, -1) : newTexts;
|
|
1462
|
+
for (const text of noteTexts.slice(-4)) {
|
|
1463
|
+
await sendAll(devices, { source, session: payload.session_id,
|
|
1464
|
+
host: hostname(), project: message.project,
|
|
1465
|
+
event: 'agent_note',
|
|
1466
|
+
detail: clipProse(text) });
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1429
1469
|
// A snippet of human-readable context for the phone to show. The
|
|
1430
|
-
// turn's reply gets
|
|
1431
|
-
//
|
|
1470
|
+
// turn's reply gets room — it feeds the reviewable chat history,
|
|
1471
|
+
// not just a caption — with the tail-window read as fallback when
|
|
1472
|
+
// the incremental scan saw nothing new.
|
|
1473
|
+
// tool_start also names what the tool is chewing on (the command,
|
|
1474
|
+
// the file) so the phone's history can say more than "Bash".
|
|
1475
|
+
const toolInfo = event.event === 'tool_start' && payload.tool_input
|
|
1476
|
+
? summarizeToolInput(payload) : undefined;
|
|
1432
1477
|
const detail = event.event === 'prompt'
|
|
1433
1478
|
? firstString(payload.prompt, payload.user_prompt, payload.userPrompt,
|
|
1434
1479
|
payload.message, payload.input, payload.text)
|
|
1435
1480
|
: event.event === 'permission'
|
|
1436
1481
|
? firstString(payload.message, payload.detail,
|
|
1437
1482
|
payload.tool_input ? summarizeToolInput(payload) : undefined)
|
|
1438
|
-
:
|
|
1439
|
-
? lastAssistantText(payload)
|
|
1440
|
-
: undefined;
|
|
1441
|
-
|
|
1442
|
-
|
|
1483
|
+
: isTurnDone
|
|
1484
|
+
? (newTexts[newTexts.length - 1] ?? lastAssistantText(payload))
|
|
1485
|
+
: (toolInfo !== '{}' ? toolInfo : undefined);
|
|
1486
|
+
if (detail) {
|
|
1487
|
+
message.detail = isTurnDone || event.event === 'prompt'
|
|
1488
|
+
? clipProse(detail)
|
|
1489
|
+
: clipCaption(detail, 140);
|
|
1490
|
+
}
|
|
1443
1491
|
// Plan-window usage comes from Claude Code's local transcripts;
|
|
1444
1492
|
// recompute only at turn boundaries.
|
|
1445
1493
|
if (source === 'claude') {
|
package/package.json
CHANGED