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.
- package/assets/skills/consulting-report/SKILL.md +2 -2
- package/assets/skills/consulting-report/template/README.md +1 -1
- package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -4
- package/assets/skills/consulting-report/tools/scaffold.ts +1 -1
- package/assets/skills/create-pdf/SKILL.md +2 -2
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +8 -3
- package/package.json +1 -1
- package/src/cli/index.ts +64 -2
- package/src/hooks/handlers/desktop-notify.ts +22 -0
- package/src/hooks/lib/models.ts +42 -5
- package/src/hooks/lib/notify.ts +55 -0
- package/src/hooks/lib/stop.ts +4 -0
- package/src/tools/session-summary.ts +28 -6
- package/src/tools/token-cost.ts +67 -20
|
@@ -48,7 +48,7 @@ Edit:
|
|
|
48
48
|
### Step 3: Render
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
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
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("
|
|
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(
|
|
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
|
+
}
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -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
|
-
{
|
|
11
|
+
{
|
|
12
|
+
input: number;
|
|
13
|
+
output: number;
|
|
14
|
+
cacheWrite5m: number;
|
|
15
|
+
cacheWrite1h: number;
|
|
16
|
+
cacheRead: number;
|
|
17
|
+
}
|
|
12
18
|
> = {
|
|
13
|
-
[HAIKU_MODEL]: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 +
|
|
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.
|
|
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 =
|
|
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";
|
package/src/tools/token-cost.ts
CHANGED
|
@@ -20,14 +20,23 @@ import { palHome } from "../hooks/lib/paths";
|
|
|
20
20
|
export interface Bucket {
|
|
21
21
|
input: number;
|
|
22
22
|
output: number;
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
99
|
+
bucket.cacheWrite5m += cacheWrite5m;
|
|
100
|
+
bucket.cacheWrite1h += cacheWrite1h;
|
|
88
101
|
bucket.cacheRead += cacheRead;
|
|
89
|
-
bucket.cost += costForUsage(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
134
|
-
if (ts >= monthAgo)
|
|
135
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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;
|