portable-agent-layer 0.27.0 → 0.28.0

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.
@@ -48,7 +48,7 @@ Edit:
48
48
  ### Step 3: Render
49
49
 
50
50
  ```bash
51
- bun ~/.pal/skills/consulting-report/tools/generate-pdf.ts <report-dir>
51
+ node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts <report-dir>
52
52
  ```
53
53
 
54
54
  Output: `<dir>/<client-slug>-<title-slug>-<date>.pdf` and matching `.html`. Override with `--pdf <path>` / `--html <path>`.
@@ -101,7 +101,7 @@ Do NOT combine CSS `@page` margin-box rules with the Playwright `displayHeaderFo
101
101
  A runnable demo lives at `~/.pal/skills/consulting-report/demo/`:
102
102
 
103
103
  ```bash
104
- bun ~/.pal/skills/consulting-report/tools/generate-pdf.ts ~/.pal/skills/consulting-report/demo
104
+ node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts ~/.pal/skills/consulting-report/demo
105
105
  ```
106
106
 
107
107
  Inspect the produced PDF to see the full layout (cover, TOC, sections, findings, recommendations, conclusion, appendix) before writing your own report.
@@ -10,7 +10,7 @@
10
10
  ## Render
11
11
 
12
12
  ```
13
- bun ~/.pal/skills/consulting-report/tools/generate-pdf.ts .
13
+ node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts .
14
14
  ```
15
15
 
16
16
  Output goes into this directory as `<client>-<title>-<date>.{pdf,html}` unless you pass `--pdf <path>` or `--html <path>`.
@@ -1,10 +1,14 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
 
3
3
  // consulting-report skill tool: render a structured report directory to a branded PDF.
4
4
  // Pipeline: report-data.ts + section markdown + diagrams -> HTML -> PDF (Playwright).
5
5
  //
6
+ // Run with Node (not Bun) — Playwright's chromium.launch hangs under Bun on Windows
7
+ // because it uses --remote-debugging-pipe over stdio and Bun's Windows child-process
8
+ // pipe handling doesn't complete the CDP handshake.
9
+ //
6
10
  // Usage:
7
- // bun ~/.pal/skills/consulting-report/tools/generate-pdf.ts <report-dir> [--pdf <out>] [--html <out>]
11
+ // node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts <report-dir> [--pdf <out>] [--html <out>]
8
12
  //
9
13
  // <report-dir> must contain content/report-data.ts (default export or named `report`).
10
14
 
@@ -12,6 +16,7 @@ import { spawnSync } from "node:child_process";
12
16
  import { constants as fsConstants } from "node:fs";
13
17
  import { access, readdir, readFile, stat, writeFile } from "node:fs/promises";
14
18
  import { isAbsolute, join, resolve } from "node:path";
19
+ import { pathToFileURL } from "node:url";
15
20
  import { marked } from "marked";
16
21
  import { chromium } from "playwright";
17
22
 
@@ -403,7 +408,7 @@ async function renderPdf(
403
408
  const browser = await chromium.launch();
404
409
  try {
405
410
  const page = await browser.newPage();
406
- await page.goto(`file://${htmlPath}`, { waitUntil: "networkidle" });
411
+ await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "networkidle" });
407
412
 
408
413
  // Wait for all images
409
414
  await page.evaluate(async () => {
@@ -459,7 +464,7 @@ async function loadReport(
459
464
  throw new Error(`report-data.ts not found at ${dataPath}`);
460
465
  }
461
466
  const mod: { default?: ConsultingReport; report?: ConsultingReport } = await import(
462
- dataPath
467
+ pathToFileURL(dataPath).href
463
468
  );
464
469
  const report = mod.default || mod.report;
465
470
  if (!report) {
@@ -70,5 +70,5 @@ console.log(` 2. Fill ${join(targetDir, "content")} with your section markdown
70
70
  console.log(` 3. Drop images into ${join(targetDir, "diagrams")}`);
71
71
  console.log(` 4. Render:`);
72
72
  console.log(
73
- ` bun ~/.pal/skills/consulting-report/tools/generate-pdf.ts ${targetDir}`
73
+ ` node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts ${targetDir}`
74
74
  );
@@ -55,13 +55,13 @@ Invoke the skill tool. Flags:
55
55
  Single-file example:
56
56
 
57
57
  ```bash
58
- bun ~/.pal/skills/create-pdf/tools/md-to-html-pdf.ts /path/to/report.md --pdf /path/to/report.pdf
58
+ node --experimental-strip-types ~/.pal/skills/create-pdf/tools/md-to-html-pdf.ts /path/to/report.md --pdf /path/to/report.pdf
59
59
  ```
60
60
 
61
61
  Multi-file example (after Step 2):
62
62
 
63
63
  ```bash
64
- bun ~/.pal/skills/create-pdf/tools/md-to-html-pdf.ts /tmp/combined.md --pdf /path/to/report.pdf --html /path/to/report.html
64
+ node --experimental-strip-types ~/.pal/skills/create-pdf/tools/md-to-html-pdf.ts /tmp/combined.md --pdf /path/to/report.pdf --html /path/to/report.html
65
65
  ```
66
66
 
67
67
  The tool writes the self-contained HTML (inline CSS, UTF-8) and the PDF, and prints both paths + sizes on stdout.
@@ -1,12 +1,17 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  // create-pdf skill tool: Markdown -> HTML (marked, GFM) -> PDF (Playwright).
3
3
  // Self-contained HTML: all CSS inlined, no CDN at render time.
4
4
  //
5
+ // Run with Node (not Bun) — Playwright's chromium.launch hangs under Bun on Windows
6
+ // because it uses --remote-debugging-pipe over stdio and Bun's Windows child-process
7
+ // pipe handling doesn't complete the CDP handshake.
8
+ //
5
9
  // Usage:
6
- // bun ~/.pal/skills/create-pdf/tools/md-to-html-pdf.ts <input.md> [--html <out.html>] [--pdf <out.pdf>]
10
+ // node --experimental-strip-types ~/.pal/skills/create-pdf/tools/md-to-html-pdf.ts <input.md> [--html <out.html>] [--pdf <out.pdf>]
7
11
 
8
12
  import { readFile, stat, writeFile } from "node:fs/promises";
9
13
  import { basename, dirname, extname, resolve } from "node:path";
14
+ import { pathToFileURL } from "node:url";
10
15
  import { marked } from "marked";
11
16
  import { chromium } from "playwright";
12
17
 
@@ -74,7 +79,7 @@ await writeFile(htmlOut, html, "utf8");
74
79
  const browser = await chromium.launch();
75
80
  try {
76
81
  const page = await browser.newPage();
77
- await page.goto(`file://${htmlOut}`, { waitUntil: "networkidle" });
82
+ await page.goto(pathToFileURL(htmlOut).href, { waitUntil: "networkidle" });
78
83
  await page.pdf({
79
84
  path: pdfOut,
80
85
  format: "A4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -356,6 +356,39 @@ function checkPlaywrightChromium(): boolean {
356
356
  }
357
357
  }
358
358
 
359
+ interface NodeCheck {
360
+ available: boolean;
361
+ version?: string;
362
+ meetsMinimum?: boolean;
363
+ }
364
+
365
+ // Minimum Node version with `--experimental-strip-types` is 22.6.0 — required
366
+ // by the consulting-report skill, which runs under Node on Windows because
367
+ // Playwright's chromium.launch() hangs under Bun.
368
+ function checkNode(): NodeCheck {
369
+ const minMajor = 22;
370
+ const minMinor = 6;
371
+ const result = checkTool("node");
372
+ if (!result.available) return { available: false };
373
+ const raw = (result.version || "").replace(/^v/, "");
374
+ const [majorStr = "", minorStr = ""] = raw.split(".");
375
+ const major = Number(majorStr);
376
+ const minor = Number(minorStr);
377
+ const meetsMinimum =
378
+ Number.isFinite(major) &&
379
+ Number.isFinite(minor) &&
380
+ (major > minMajor || (major === minMajor && minor >= minMinor));
381
+ return { available: true, version: raw, meetsMinimum };
382
+ }
383
+
384
+ function nodeInstallHint(): string {
385
+ if (process.platform === "win32")
386
+ return "install Node ≥ 22.6 (`winget install OpenJS.NodeJS.LTS` or https://nodejs.org)";
387
+ if (process.platform === "darwin")
388
+ return "install Node ≥ 22.6 (`brew install node` or https://nodejs.org)";
389
+ return "install Node ≥ 22.6 (see https://nodejs.org or your package manager)";
390
+ }
391
+
359
392
  function checkHookHealth(home: string): HookHealth {
360
393
  const logPath = resolve(home, "memory", "state", "debug.log");
361
394
 
@@ -435,6 +468,18 @@ function doctor(silent = false): DoctorResult {
435
468
  console.log("");
436
469
  log.info("Doctor");
437
470
  ok(`Bun ${bun.version}`);
471
+ const node = checkNode();
472
+ if (!node.available) {
473
+ warn(
474
+ `Node — not found; consulting-report PDF skill will not work. ${nodeInstallHint()}`
475
+ );
476
+ } else if (!node.meetsMinimum) {
477
+ warn(
478
+ `Node ${node.version} — too old for consulting-report PDF skill (needs ≥ 22.6 for --experimental-strip-types). ${nodeInstallHint()}`
479
+ );
480
+ } else {
481
+ ok(`Node ${node.version}`);
482
+ }
438
483
  claude.available
439
484
  ? ok(`Claude Code ${claude.version || ""}`.trim())
440
485
  : fail("Claude Code — not found");
@@ -635,18 +680,35 @@ async function install(targets: Targets) {
635
680
  // Fetch the Chromium build Playwright uses for PDF rendering (create-pdf skill).
636
681
  // Idempotent — skipped if already cached. Skipped entirely under PAL_SKIP_BROWSER_INSTALL=1
637
682
  // (used by tests to avoid a ~150MB download on every run).
683
+ // Uses `bun x` (not `bunx`) for Windows compatibility — bunx resolves unreliably under cmd.exe.
638
684
  if (process.env.PAL_SKIP_BROWSER_INSTALL !== "1") {
639
685
  log.info("Installing Playwright Chromium...");
640
- const pw = spawnSync("bunx", ["playwright", "install", "chromium"], {
686
+ const pw = spawnSync("bun", ["x", "playwright", "install", "chromium"], {
641
687
  cwd: pkg,
642
688
  stdio: "inherit",
643
689
  shell: true,
644
690
  });
645
691
  if (pw.status !== 0) {
646
- log.warn("playwright install chromium failed — create-pdf skill will not work");
692
+ log.warn(
693
+ `playwright install chromium failed (exit ${pw.status}) — create-pdf and consulting-report skills won't work. Retry manually: bun x playwright install chromium`
694
+ );
647
695
  }
648
696
  }
649
697
 
698
+ // Node check — the consulting-report skill runs under `node --experimental-strip-types`
699
+ // because Playwright's chromium.launch() hangs under Bun on Windows (CDP handshake over
700
+ // stdio pipes). Node ≥ 22.6 is required for --experimental-strip-types.
701
+ const node = checkNode();
702
+ if (!node.available) {
703
+ log.warn(
704
+ `Node not found — consulting-report PDF skill will not work. ${nodeInstallHint()}`
705
+ );
706
+ } else if (!node.meetsMinimum) {
707
+ log.warn(
708
+ `Node ${node.version} is older than 22.6 — consulting-report PDF skill will not work (needs --experimental-strip-types). ${nodeInstallHint()}`
709
+ );
710
+ }
711
+
650
712
  // Scaffold TELOS + PAL settings, then prompt for missing identity
651
713
  const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
652
714
  const { promptIdentity } = await import("./setup-identity");
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stop handler: desktop notification when Claude finishes responding.
3
+ * Platform dispatch lives in lib/notify — this just chooses the message.
4
+ */
5
+
6
+ import { basename } from "node:path";
7
+ import { notify } from "../lib/notify";
8
+ import { readSessionNames } from "../lib/session-names";
9
+ import { identity } from "../lib/settings";
10
+
11
+ export async function notifyDesktop(sessionId?: string): Promise<void> {
12
+ await notify(identity().ai.name, resolveBody(sessionId));
13
+ }
14
+
15
+ function resolveBody(sessionId?: string): string {
16
+ if (sessionId) {
17
+ const name = readSessionNames()[sessionId];
18
+ if (name && name !== "untitled session") return `New message in task "${name}".`;
19
+ }
20
+ const cwd = basename(process.cwd());
21
+ return cwd && cwd !== "/" ? `New message in task "${cwd}".` : "You have a new message.";
22
+ }
@@ -8,10 +8,47 @@ export const SONNET_MODEL = "claude-sonnet-4-6";
8
8
  /** Pricing per million tokens (USD) — from https://platform.claude.com/docs/en/about-claude/pricing */
9
9
  export const MODEL_PRICING: Record<
10
10
  string,
11
- { input: number; output: number; cacheWrite: number; cacheRead: number }
11
+ {
12
+ input: number;
13
+ output: number;
14
+ cacheWrite5m: number;
15
+ cacheWrite1h: number;
16
+ cacheRead: number;
17
+ }
12
18
  > = {
13
- [HAIKU_MODEL]: { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
14
- "claude-opus-4-6": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
15
- "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
16
- "claude-sonnet-4-5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
19
+ [HAIKU_MODEL]: {
20
+ input: 1,
21
+ output: 5,
22
+ cacheWrite5m: 1.25,
23
+ cacheWrite1h: 2,
24
+ cacheRead: 0.1,
25
+ },
26
+ "claude-opus-4-7": {
27
+ input: 5,
28
+ output: 25,
29
+ cacheWrite5m: 6.25,
30
+ cacheWrite1h: 10,
31
+ cacheRead: 0.5,
32
+ },
33
+ "claude-opus-4-6": {
34
+ input: 5,
35
+ output: 25,
36
+ cacheWrite5m: 6.25,
37
+ cacheWrite1h: 10,
38
+ cacheRead: 0.5,
39
+ },
40
+ "claude-sonnet-4-6": {
41
+ input: 3,
42
+ output: 15,
43
+ cacheWrite5m: 3.75,
44
+ cacheWrite1h: 6,
45
+ cacheRead: 0.3,
46
+ },
47
+ "claude-sonnet-4-5": {
48
+ input: 3,
49
+ output: 15,
50
+ cacheWrite5m: 3.75,
51
+ cacheWrite1h: 6,
52
+ cacheRead: 0.3,
53
+ },
17
54
  };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Cross-platform desktop notification primitive.
3
+ *
4
+ * macOS : osascript "display notification"
5
+ * Linux : notify-send (libnotify)
6
+ * Windows: PowerShell NotifyIcon (Win10+ surfaces this as a toast)
7
+ *
8
+ * All implementations fail silently if the underlying command is missing —
9
+ * notifications are non-essential, never surface as errors.
10
+ */
11
+
12
+ import { spawn } from "node:child_process";
13
+
14
+ function spawnSilent(cmd: string, args: string[]): Promise<void> {
15
+ return new Promise((res) => {
16
+ const p = spawn(cmd, args, { stdio: "ignore", windowsHide: true });
17
+ p.on("close", () => res());
18
+ p.on("error", () => res());
19
+ });
20
+ }
21
+
22
+ function escapeAppleScript(s: string): string {
23
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
24
+ }
25
+
26
+ function escapePowerShellSingle(s: string): string {
27
+ return s.replace(/'/g, "''");
28
+ }
29
+
30
+ export async function notify(title: string, body: string): Promise<void> {
31
+ if (process.platform === "darwin") {
32
+ const script = `display notification "${escapeAppleScript(body)}" with title "${escapeAppleScript(title)}"`;
33
+ await spawnSilent("osascript", ["-e", script]);
34
+ return;
35
+ }
36
+ if (process.platform === "linux") {
37
+ await spawnSilent("notify-send", [title, body]);
38
+ return;
39
+ }
40
+ if (process.platform === "win32") {
41
+ const t = escapePowerShellSingle(title);
42
+ const b = escapePowerShellSingle(body);
43
+ const ps = [
44
+ "Add-Type -AssemblyName System.Windows.Forms;",
45
+ "$n = New-Object System.Windows.Forms.NotifyIcon;",
46
+ "$n.Icon = [System.Drawing.SystemIcons]::Information;",
47
+ "$n.Visible = $true;",
48
+ `$n.ShowBalloonTip(3000, '${t}', '${b}', 'Info');`,
49
+ "Start-Sleep -Seconds 3;",
50
+ "$n.Dispose()",
51
+ ].join(" ");
52
+ await spawnSilent("powershell.exe", ["-NoProfile", "-Command", ps]);
53
+ return;
54
+ }
55
+ }
@@ -6,6 +6,7 @@
6
6
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { autoBackup } from "../handlers/backup";
9
+ import { notifyDesktop } from "../handlers/desktop-notify";
9
10
  import { captureFailure } from "../handlers/failure";
10
11
  import { checkReflectTrigger } from "../handlers/reflect-trigger";
11
12
  import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
@@ -48,6 +49,7 @@ export async function runStopHandlers(
48
49
  checkReflectTrigger(),
49
50
  checkSelfModelTrigger(),
50
51
  runSynthesis(),
52
+ notifyDesktop(options.sessionId),
51
53
  ]);
52
54
 
53
55
  const handlerNames = [
@@ -58,7 +60,9 @@ export async function runStopHandlers(
58
60
  "update-counts",
59
61
  "backup",
60
62
  "reflect-trigger",
63
+ "self-model-trigger",
61
64
  "synthesis",
65
+ "desktop-notify",
62
66
  ];
63
67
  for (let i = 0; i < results.length; i++) {
64
68
  const r = results[i];
@@ -16,7 +16,8 @@ import { MODEL_PRICING } from "../hooks/lib/models";
16
16
  interface Usage {
17
17
  input: number;
18
18
  output: number;
19
- cacheWrite: number;
19
+ cacheWrite5m: number;
20
+ cacheWrite1h: number;
20
21
  cacheRead: number;
21
22
  cost: number;
22
23
  calls: number;
@@ -82,7 +83,8 @@ export function parseSession(filepath: string, sessionId: string): Usage {
82
83
  const usage: Usage = {
83
84
  input: 0,
84
85
  output: 0,
85
- cacheWrite: 0,
86
+ cacheWrite5m: 0,
87
+ cacheWrite1h: 0,
86
88
  cacheRead: 0,
87
89
  cost: 0,
88
90
  calls: 0,
@@ -109,6 +111,10 @@ export function parseSession(filepath: string, sessionId: string): Usage {
109
111
  output_tokens?: number;
110
112
  cache_creation_input_tokens?: number;
111
113
  cache_read_input_tokens?: number;
114
+ cache_creation?: {
115
+ ephemeral_5m_input_tokens?: number;
116
+ ephemeral_1h_input_tokens?: number;
117
+ };
112
118
  };
113
119
  };
114
120
  };
@@ -127,19 +133,30 @@ export function parseSession(filepath: string, sessionId: string): Usage {
127
133
 
128
134
  const input = u.input_tokens ?? 0;
129
135
  const output = u.output_tokens ?? 0;
130
- const cw = u.cache_creation_input_tokens ?? 0;
131
136
  const cr = u.cache_read_input_tokens ?? 0;
137
+ const cw5m = u.cache_creation?.ephemeral_5m_input_tokens;
138
+ const cw1h = u.cache_creation?.ephemeral_1h_input_tokens;
139
+ const hasBreakdown = cw5m !== undefined || cw1h !== undefined;
140
+ const cacheWrite5m = hasBreakdown
141
+ ? (cw5m ?? 0)
142
+ : (u.cache_creation_input_tokens ?? 0);
143
+ const cacheWrite1h = cw1h ?? 0;
132
144
 
133
145
  const p = MODEL_PRICING[model];
134
146
  if (p) {
135
147
  usage.cost +=
136
- (input * p.input + output * p.output + cw * p.cacheWrite + cr * p.cacheRead) /
148
+ (input * p.input +
149
+ output * p.output +
150
+ cacheWrite5m * p.cacheWrite5m +
151
+ cacheWrite1h * p.cacheWrite1h +
152
+ cr * p.cacheRead) /
137
153
  1_000_000;
138
154
  }
139
155
 
140
156
  usage.input += input;
141
157
  usage.output += output;
142
- usage.cacheWrite += cw;
158
+ usage.cacheWrite5m += cacheWrite5m;
159
+ usage.cacheWrite1h += cacheWrite1h;
143
160
  usage.cacheRead += cr;
144
161
  usage.calls++;
145
162
  usage.models.add(model);
@@ -195,7 +212,12 @@ function run() {
195
212
  const usage = parseSession(file.filepath, sessionId);
196
213
  if (usage.calls === 0) process.exit(0);
197
214
 
198
- const totalTokens = usage.input + usage.output + usage.cacheWrite + usage.cacheRead;
215
+ const totalTokens =
216
+ usage.input +
217
+ usage.output +
218
+ usage.cacheWrite5m +
219
+ usage.cacheWrite1h +
220
+ usage.cacheRead;
199
221
  const model = [...usage.models].map((m) => m.replace("claude-", "")).join(", ");
200
222
 
201
223
  const dim = "\x1b[2m";
@@ -20,14 +20,23 @@ import { palHome } from "../hooks/lib/paths";
20
20
  export interface Bucket {
21
21
  input: number;
22
22
  output: number;
23
- cacheWrite: number;
23
+ cacheWrite5m: number;
24
+ cacheWrite1h: number;
24
25
  cacheRead: number;
25
26
  cost: number;
26
27
  calls: number;
27
28
  }
28
29
 
29
30
  export function emptyBucket(): Bucket {
30
- return { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0, calls: 0 };
31
+ return {
32
+ input: 0,
33
+ output: 0,
34
+ cacheWrite5m: 0,
35
+ cacheWrite1h: 0,
36
+ cacheRead: 0,
37
+ cost: 0,
38
+ calls: 0,
39
+ };
31
40
  }
32
41
 
33
42
  export interface TimeBuckets {
@@ -60,7 +69,8 @@ function costForUsage(
60
69
  model: string,
61
70
  input: number,
62
71
  output: number,
63
- cacheWrite: number,
72
+ cacheWrite5m: number,
73
+ cacheWrite1h: number,
64
74
  cacheRead: number
65
75
  ): number {
66
76
  const p = findPricing(model);
@@ -68,7 +78,8 @@ function costForUsage(
68
78
  return (
69
79
  (input * p.input +
70
80
  output * p.output +
71
- cacheWrite * p.cacheWrite +
81
+ cacheWrite5m * p.cacheWrite5m +
82
+ cacheWrite1h * p.cacheWrite1h +
72
83
  cacheRead * p.cacheRead) /
73
84
  1_000_000
74
85
  );
@@ -79,14 +90,23 @@ export function addToBucket(
79
90
  model: string,
80
91
  input: number,
81
92
  output: number,
82
- cacheWrite: number,
93
+ cacheWrite5m: number,
94
+ cacheWrite1h: number,
83
95
  cacheRead: number
84
96
  ): void {
85
97
  bucket.input += input;
86
98
  bucket.output += output;
87
- bucket.cacheWrite += cacheWrite;
99
+ bucket.cacheWrite5m += cacheWrite5m;
100
+ bucket.cacheWrite1h += cacheWrite1h;
88
101
  bucket.cacheRead += cacheRead;
89
- bucket.cost += costForUsage(model, input, output, cacheWrite, cacheRead);
102
+ bucket.cost += costForUsage(
103
+ model,
104
+ input,
105
+ output,
106
+ cacheWrite5m,
107
+ cacheWrite1h,
108
+ cacheRead
109
+ );
90
110
  bucket.calls++;
91
111
  }
92
112
 
@@ -104,7 +124,7 @@ function fmtCost(n: number): string {
104
124
  }
105
125
 
106
126
  function printRow(label: string, b: Bucket, labelWidth = 14): void {
107
- const tokens = b.input + b.output + b.cacheWrite + b.cacheRead;
127
+ const tokens = b.input + b.output + b.cacheWrite5m + b.cacheWrite1h + b.cacheRead;
108
128
  console.log(
109
129
  ` ${label.padEnd(labelWidth)} ${fmt(tokens).padStart(8)} tok ${fmt(b.calls).padStart(5)} calls ${fmtCost(b.cost).padStart(8)}`
110
130
  );
@@ -112,7 +132,7 @@ function printRow(label: string, b: Bucket, labelWidth = 14): void {
112
132
 
113
133
  function printDetailed(label: string, b: Bucket, labelWidth = 14): void {
114
134
  console.log(
115
- ` ${label.padEnd(labelWidth)} ${fmt(b.input).padStart(8)} in ${fmt(b.output).padStart(8)} out ${fmt(b.cacheWrite).padStart(8)} cw ${fmt(b.cacheRead).padStart(8)} cr ${fmtCost(b.cost).padStart(8)}`
135
+ ` ${label.padEnd(labelWidth)} ${fmt(b.input).padStart(8)} in ${fmt(b.output).padStart(8)} out ${fmt(b.cacheWrite5m).padStart(7)} cw5m ${fmt(b.cacheWrite1h).padStart(7)} cw1h ${fmt(b.cacheRead).padStart(8)} cr ${fmtCost(b.cost).padStart(8)}`
116
136
  );
117
137
  }
118
138
 
@@ -124,17 +144,20 @@ function addToTimeBuckets(
124
144
  model: string,
125
145
  input: number,
126
146
  output: number,
127
- cacheWrite: number,
147
+ cacheWrite5m: number,
148
+ cacheWrite1h: number,
128
149
  cacheRead: number,
129
150
  todayPrefix: string,
130
151
  weekAgo: string,
131
152
  monthAgo: string
132
153
  ): void {
133
- addToBucket(tb.total, model, input, output, cacheWrite, cacheRead);
134
- if (ts >= monthAgo) addToBucket(tb.month, model, input, output, cacheWrite, cacheRead);
135
- if (ts >= weekAgo) addToBucket(tb.week, model, input, output, cacheWrite, cacheRead);
154
+ addToBucket(tb.total, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
155
+ if (ts >= monthAgo)
156
+ addToBucket(tb.month, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
157
+ if (ts >= weekAgo)
158
+ addToBucket(tb.week, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
136
159
  if (ts.startsWith(todayPrefix))
137
- addToBucket(tb.today, model, input, output, cacheWrite, cacheRead);
160
+ addToBucket(tb.today, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
138
161
  }
139
162
 
140
163
  export function readClaudeCode(projectFilter?: string): {
@@ -205,6 +228,10 @@ export function readClaudeCode(projectFilter?: string): {
205
228
  output_tokens?: number;
206
229
  cache_creation_input_tokens?: number;
207
230
  cache_read_input_tokens?: number;
231
+ cache_creation?: {
232
+ ephemeral_5m_input_tokens?: number;
233
+ ephemeral_1h_input_tokens?: number;
234
+ };
208
235
  };
209
236
  };
210
237
  };
@@ -216,8 +243,15 @@ export function readClaudeCode(projectFilter?: string): {
216
243
 
217
244
  const input = usage.input_tokens ?? 0;
218
245
  const output = usage.output_tokens ?? 0;
219
- const cw = usage.cache_creation_input_tokens ?? 0;
220
246
  const cr = usage.cache_read_input_tokens ?? 0;
247
+ const cw5m = usage.cache_creation?.ephemeral_5m_input_tokens;
248
+ const cw1h = usage.cache_creation?.ephemeral_1h_input_tokens;
249
+ // Older transcripts only have the summed cache_creation_input_tokens — bill as 5m.
250
+ const hasBreakdown = cw5m !== undefined || cw1h !== undefined;
251
+ const cacheWrite5m = hasBreakdown
252
+ ? (cw5m ?? 0)
253
+ : (usage.cache_creation_input_tokens ?? 0);
254
+ const cacheWrite1h = cw1h ?? 0;
221
255
 
222
256
  addToTimeBuckets(
223
257
  buckets,
@@ -225,7 +259,8 @@ export function readClaudeCode(projectFilter?: string): {
225
259
  model,
226
260
  input,
227
261
  output,
228
- cw,
262
+ cacheWrite5m,
263
+ cacheWrite1h,
229
264
  cr,
230
265
  todayPrefix,
231
266
  weekAgo,
@@ -233,7 +268,15 @@ export function readClaudeCode(projectFilter?: string): {
233
268
  );
234
269
 
235
270
  if (!byModel[model]) byModel[model] = emptyBucket();
236
- addToBucket(byModel[model], model, input, output, cw, cr);
271
+ addToBucket(
272
+ byModel[model],
273
+ model,
274
+ input,
275
+ output,
276
+ cacheWrite5m,
277
+ cacheWrite1h,
278
+ cr
279
+ );
237
280
 
238
281
  if (!byProject[projName]) byProject[projName] = emptyTimeBuckets();
239
282
  addToTimeBuckets(
@@ -242,7 +285,8 @@ export function readClaudeCode(projectFilter?: string): {
242
285
  model,
243
286
  input,
244
287
  output,
245
- cw,
288
+ cacheWrite5m,
289
+ cacheWrite1h,
246
290
  cr,
247
291
  todayPrefix,
248
292
  weekAgo,
@@ -297,6 +341,7 @@ export function readPalInference(): {
297
341
  e.outputTokens,
298
342
  0,
299
343
  0,
344
+ 0,
300
345
  todayPrefix,
301
346
  weekAgo,
302
347
  monthAgo
@@ -310,12 +355,13 @@ export function readPalInference(): {
310
355
  e.outputTokens,
311
356
  0,
312
357
  0,
358
+ 0,
313
359
  todayPrefix,
314
360
  weekAgo,
315
361
  monthAgo
316
362
  );
317
363
  if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
318
- addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0);
364
+ addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0, 0);
319
365
  } catch {
320
366
  /* skip */
321
367
  }
@@ -385,7 +431,8 @@ function run() {
385
431
  for (const b of [cc.buckets.total, pal.buckets.total]) {
386
432
  grand.input += b.input;
387
433
  grand.output += b.output;
388
- grand.cacheWrite += b.cacheWrite;
434
+ grand.cacheWrite5m += b.cacheWrite5m;
435
+ grand.cacheWrite1h += b.cacheWrite1h;
389
436
  grand.cacheRead += b.cacheRead;
390
437
  grand.cost += b.cost;
391
438
  grand.calls += b.calls;