carmoji 0.3.8 → 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.
- package/carmoji.js +98 -15
- 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,12 +629,48 @@ function sessionTokens(payload) {
|
|
|
608
629
|
writeFileSync(statePath, JSON.stringify(state));
|
|
609
630
|
}
|
|
610
631
|
}
|
|
611
|
-
return state.total;
|
|
632
|
+
return { total: state.total, texts };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* The agent's final say for the turn. Hooks never carry assistant text,
|
|
637
|
+
* but Stop hands over transcript_path, and the JSONL's tail holds the
|
|
638
|
+
* reply — Claude records {type:"assistant", message:{content:[{type:
|
|
639
|
+
* "text",text}]}}, Codex rollouts {payload:{type:"agent_message",message}}.
|
|
640
|
+
*/
|
|
641
|
+
function lastAssistantText(payload) {
|
|
642
|
+
try {
|
|
643
|
+
const transcript = payload.transcript_path;
|
|
644
|
+
if (!transcript) return undefined;
|
|
645
|
+
const size = statSync(transcript).size;
|
|
646
|
+
const window = Math.min(size, 256 * 1024);
|
|
647
|
+
if (window === 0) return undefined;
|
|
648
|
+
const fd = openSync(transcript, 'r');
|
|
649
|
+
const buffer = Buffer.alloc(window);
|
|
650
|
+
const bytesRead = readSync(fd, buffer, 0, window, size - window);
|
|
651
|
+
closeSync(fd);
|
|
652
|
+
const lines = buffer.subarray(0, bytesRead).toString('utf8').split('\n');
|
|
653
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
654
|
+
const line = lines[i];
|
|
655
|
+
if (!line.includes('"assistant"') && !line.includes('agent_message')) continue;
|
|
656
|
+
try {
|
|
657
|
+
const text = assistantTextOf(JSON.parse(line));
|
|
658
|
+
if (text) return text;
|
|
659
|
+
} catch { /* the window's first line may be cut mid-record */ }
|
|
660
|
+
}
|
|
661
|
+
} catch { /* no transcript, no say */ }
|
|
662
|
+
return undefined;
|
|
612
663
|
}
|
|
613
664
|
|
|
614
665
|
const TEST_EVENTS = {
|
|
615
666
|
start: { event: 'session_start' },
|
|
616
|
-
prompt: { event: 'prompt' },
|
|
667
|
+
prompt: { event: 'prompt', detail: 'Fix the corner release bug' },
|
|
668
|
+
say: { event: 'turn_done',
|
|
669
|
+
detail: 'Done — corners now release on yaw alone with an 8s cap, '
|
|
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.' },
|
|
617
674
|
edit: { event: 'tool_start', tool: 'Edit' },
|
|
618
675
|
read: { event: 'tool_start', tool: 'Read' },
|
|
619
676
|
run: { event: 'tool_start', tool: 'Bash' },
|
|
@@ -1377,23 +1434,49 @@ async function main() {
|
|
|
1377
1434
|
// sum across them instead of showing whichever reported last.
|
|
1378
1435
|
const message = { source, session: payload.session_id,
|
|
1379
1436
|
host: hostname(), ...event };
|
|
1380
|
-
// Claude and Codex transcripts power the tok/s readout
|
|
1381
|
-
// quietly produce
|
|
1382
|
-
const
|
|
1383
|
-
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;
|
|
1384
1441
|
// Gemini and Trae hooks carry no cwd — the hook itself runs in the
|
|
1385
1442
|
// project directory, so fall back to that.
|
|
1386
1443
|
const cwd = payload.cwd || process.cwd();
|
|
1387
1444
|
if (cwd) message.project = basename(cwd);
|
|
1388
|
-
|
|
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
|
+
|
|
1464
|
+
// A snippet of human-readable context for the phone to show. The
|
|
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.
|
|
1389
1468
|
const detail = event.event === 'prompt'
|
|
1390
1469
|
? firstString(payload.prompt, payload.user_prompt, payload.userPrompt,
|
|
1391
1470
|
payload.message, payload.input, payload.text)
|
|
1392
1471
|
: event.event === 'permission'
|
|
1393
1472
|
? firstString(payload.message, payload.detail,
|
|
1394
1473
|
payload.tool_input ? summarizeToolInput(payload) : undefined)
|
|
1395
|
-
:
|
|
1396
|
-
|
|
1474
|
+
: isTurnDone
|
|
1475
|
+
? (newTexts[newTexts.length - 1] ?? lastAssistantText(payload))
|
|
1476
|
+
: undefined;
|
|
1477
|
+
const detailCap = isTurnDone ? 2000
|
|
1478
|
+
: event.event === 'prompt' ? 1000 : 140;
|
|
1479
|
+
if (detail) message.detail = clip(detail, detailCap);
|
|
1397
1480
|
// Plan-window usage comes from Claude Code's local transcripts;
|
|
1398
1481
|
// recompute only at turn boundaries.
|
|
1399
1482
|
if (source === 'claude') {
|
package/package.json
CHANGED