portable-agent-layer 0.62.0 → 0.62.2
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 +3 -3
- package/assets/skills/consulting-report/tools/generate-pdf.mjs +226 -0
- package/assets/skills/consulting-report/tools/generate-pdf.ts +4 -1
- package/assets/skills/consulting-report/tools/scaffold.ts +1 -1
- package/assets/skills/create-pdf/SKILL.md +4 -4
- package/assets/skills/create-pdf/tools/md-to-html-pdf.mjs +103 -0
- package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +4 -1
- package/assets/skills/playwright/SKILL.md +1 -1
- package/assets/skills/playwright/tools/shot-lib.mjs +44 -0
- package/assets/skills/playwright/tools/shot.mjs +89 -0
- package/assets/skills/playwright/tools/shot.ts +4 -2
- package/assets/templates/settings.claude.json +14 -0
- package/package.json +3 -1
- package/src/hooks/lib/models.ts +65 -10
- package/src/tools/session-summary.ts +8 -11
- package/src/tools/token-cost.ts +4 -33
|
@@ -78,7 +78,7 @@ Wraps `bun run dev` in the report directory. Open the URL printed by Next, edit
|
|
|
78
78
|
### 4. Render the PDF
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
node
|
|
81
|
+
node ~/.pal/skills/consulting-report/tools/generate-pdf.mjs <report-dir>
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
Runs `next build` (which produces a static export at `out/`), then Playwright loads it via a tiny in-process HTTP server and prints the PDF with page-numbered header/footer. Output:
|
|
@@ -140,14 +140,14 @@ Run with **Node**, not Bun — Playwright's `chromium.launch()` hangs under Bun
|
|
|
140
140
|
## Demo
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
|
-
node
|
|
143
|
+
node ~/.pal/skills/consulting-report/tools/generate-pdf.mjs ~/.pal/skills/consulting-report/demo
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
Renders the bundled Acme Industries example end-to-end. Inspect the resulting PDF to see the full layout before authoring your own.
|
|
147
147
|
|
|
148
148
|
## Important
|
|
149
149
|
|
|
150
|
-
- Run on Node
|
|
150
|
+
- Run on Node (Playwright); the tool ships as a compiled `.mjs` so no `--experimental-strip-types` is needed
|
|
151
151
|
- Bundled fonts come from Google Fonts via `next/font/google` — no licensing surface, no CDN at runtime, glyphs embedded at build time
|
|
152
152
|
- Reports are disposable artifacts of `lib/report-data.ts` + `app/page.tsx`; commit the source, not the PDF
|
|
153
153
|
- The scaffolder runs `bun install` inside the target by default — pass `--no-install` to skip
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createReadStream, constants as fsConstants, realpathSync } from "node:fs";
|
|
3
|
+
import { access, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { extname, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { PDFDocument } from "pdf-lib";
|
|
8
|
+
import { chromium } from "playwright";
|
|
9
|
+
const COLOR = {
|
|
10
|
+
navy: "#0f172a",
|
|
11
|
+
blue: "#1d4ed8",
|
|
12
|
+
red: "#dc2626"
|
|
13
|
+
};
|
|
14
|
+
async function exists(p) {
|
|
15
|
+
try {
|
|
16
|
+
await access(p, fsConstants.F_OK);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function escapeHtml(s) {
|
|
23
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
24
|
+
}
|
|
25
|
+
function slugify(s) {
|
|
26
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
27
|
+
}
|
|
28
|
+
async function logoDataUri(outDir, publicPath) {
|
|
29
|
+
if (!publicPath)
|
|
30
|
+
return null;
|
|
31
|
+
const filePath = join(outDir, publicPath);
|
|
32
|
+
if (!await exists(filePath))
|
|
33
|
+
return null;
|
|
34
|
+
const buf = await readFile(filePath);
|
|
35
|
+
const ext = extname(filePath).toLowerCase();
|
|
36
|
+
let mime = "image/jpeg";
|
|
37
|
+
if (ext === ".svg") {
|
|
38
|
+
mime = "image/svg+xml";
|
|
39
|
+
} else if (ext === ".png") {
|
|
40
|
+
mime = "image/png";
|
|
41
|
+
}
|
|
42
|
+
return `data:${mime};base64,${buf.toString("base64")}`;
|
|
43
|
+
}
|
|
44
|
+
async function loadMeta(reportDir) {
|
|
45
|
+
const dataPath = join(reportDir, "lib", "report-data.ts");
|
|
46
|
+
if (!await exists(dataPath)) {
|
|
47
|
+
throw new Error(`lib/report-data.ts not found at ${dataPath}`);
|
|
48
|
+
}
|
|
49
|
+
const mod = await import(pathToFileURL(dataPath).href);
|
|
50
|
+
if (!mod.reportData) {
|
|
51
|
+
throw new Error(`lib/report-data.ts must export a named 'reportData' constant`);
|
|
52
|
+
}
|
|
53
|
+
return mod.reportData;
|
|
54
|
+
}
|
|
55
|
+
function buildNext(reportDir) {
|
|
56
|
+
const result = spawnSync("bun", ["run", "build"], {
|
|
57
|
+
cwd: reportDir,
|
|
58
|
+
stdio: "inherit",
|
|
59
|
+
shell: true
|
|
60
|
+
});
|
|
61
|
+
if (result.status !== 0) {
|
|
62
|
+
throw new Error(`next build failed (exit ${result.status})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function serveStatic(rootDir) {
|
|
66
|
+
const MIME = {
|
|
67
|
+
".html": "text/html; charset=utf-8",
|
|
68
|
+
".css": "text/css; charset=utf-8",
|
|
69
|
+
".js": "application/javascript; charset=utf-8",
|
|
70
|
+
".json": "application/json; charset=utf-8",
|
|
71
|
+
".woff": "font/woff",
|
|
72
|
+
".woff2": "font/woff2",
|
|
73
|
+
".svg": "image/svg+xml",
|
|
74
|
+
".png": "image/png",
|
|
75
|
+
".jpg": "image/jpeg",
|
|
76
|
+
".jpeg": "image/jpeg",
|
|
77
|
+
".webp": "image/webp",
|
|
78
|
+
".ico": "image/x-icon"
|
|
79
|
+
};
|
|
80
|
+
return new Promise((res) => {
|
|
81
|
+
const server = createServer((req, response) => {
|
|
82
|
+
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
83
|
+
let filePath = join(rootDir, urlPath === "/" ? "/index.html" : urlPath);
|
|
84
|
+
if (filePath.endsWith("/"))
|
|
85
|
+
filePath = join(filePath, "index.html");
|
|
86
|
+
const ext = extname(filePath).toLowerCase();
|
|
87
|
+
response.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
88
|
+
const stream = createReadStream(filePath);
|
|
89
|
+
stream.on("error", () => {
|
|
90
|
+
response.statusCode = 404;
|
|
91
|
+
response.end();
|
|
92
|
+
});
|
|
93
|
+
stream.pipe(response);
|
|
94
|
+
});
|
|
95
|
+
server.listen(0, "127.0.0.1", () => {
|
|
96
|
+
const addr = server.address();
|
|
97
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
98
|
+
res({ server, url: `http://127.0.0.1:${port}/` });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function renderPdf(htmlPath, pdfPath, meta) {
|
|
103
|
+
const outDir = resolve(htmlPath, "..");
|
|
104
|
+
const { server, url } = await serveStatic(outDir);
|
|
105
|
+
const browser = await chromium.launch();
|
|
106
|
+
try {
|
|
107
|
+
const page = await browser.newPage();
|
|
108
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
109
|
+
await page.evaluate(async () => {
|
|
110
|
+
await document.fonts?.ready;
|
|
111
|
+
const imgs = Array.from(document.querySelectorAll("img"));
|
|
112
|
+
await Promise.all(imgs.map((img) => img.complete ? Promise.resolve() : new Promise((res) => {
|
|
113
|
+
img.onload = () => res(null);
|
|
114
|
+
img.onerror = () => res(null);
|
|
115
|
+
setTimeout(() => res(null), 5000);
|
|
116
|
+
})));
|
|
117
|
+
});
|
|
118
|
+
const [clientUri, consultancyUri] = await Promise.all([
|
|
119
|
+
logoDataUri(outDir, meta.clientLogoSrc),
|
|
120
|
+
logoDataUri(outDir, meta.consultancyLogoSrc)
|
|
121
|
+
]);
|
|
122
|
+
const clientSlot = clientUri ? `<img src="${clientUri}" style="height:18px; width:auto; object-fit:contain; display:block;">` : `<span style="font-weight:600; color:${COLOR.navy}; letter-spacing:0.05em;">${escapeHtml(meta.clientName.toUpperCase())}</span>`;
|
|
123
|
+
const consultancySlot = consultancyUri ? `<img src="${consultancyUri}" style="height:14px; width:auto; object-fit:contain; display:block;">` : `<span style="color:${COLOR.navy};">${escapeHtml(meta.consultancyName)}</span>`;
|
|
124
|
+
const header = `
|
|
125
|
+
<div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
|
|
126
|
+
${clientSlot}
|
|
127
|
+
<span style="color:#94a3b8;">${escapeHtml(meta.reportTitle)}</span>
|
|
128
|
+
</div>`;
|
|
129
|
+
const footer = `
|
|
130
|
+
<div style="width:100%; font-family:Inter,'Helvetica Neue',Arial,sans-serif; font-size:7.5pt; padding:0 0.7in; display:flex; justify-content:space-between; align-items:center;">
|
|
131
|
+
${consultancySlot}
|
|
132
|
+
<span style="color:${COLOR.navy};"><span class="pageNumber"></span></span>
|
|
133
|
+
</div>`;
|
|
134
|
+
const margin = { top: "0.7in", right: "0.7in", bottom: "0.7in", left: "0.7in" };
|
|
135
|
+
const tmpCover = `${pdfPath}.cover.tmp.pdf`;
|
|
136
|
+
const tmpBody = `${pdfPath}.body.tmp.pdf`;
|
|
137
|
+
await Promise.all([
|
|
138
|
+
page.pdf({
|
|
139
|
+
path: tmpCover,
|
|
140
|
+
format: "A4",
|
|
141
|
+
pageRanges: "1",
|
|
142
|
+
printBackground: true,
|
|
143
|
+
displayHeaderFooter: false,
|
|
144
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" },
|
|
145
|
+
preferCSSPageSize: false
|
|
146
|
+
}),
|
|
147
|
+
page.pdf({
|
|
148
|
+
path: tmpBody,
|
|
149
|
+
format: "A4",
|
|
150
|
+
pageRanges: "2-",
|
|
151
|
+
printBackground: true,
|
|
152
|
+
displayHeaderFooter: true,
|
|
153
|
+
headerTemplate: header,
|
|
154
|
+
footerTemplate: footer,
|
|
155
|
+
margin,
|
|
156
|
+
preferCSSPageSize: false
|
|
157
|
+
})
|
|
158
|
+
]);
|
|
159
|
+
const [coverBytes, bodyBytes] = await Promise.all([
|
|
160
|
+
readFile(tmpCover),
|
|
161
|
+
readFile(tmpBody)
|
|
162
|
+
]);
|
|
163
|
+
const [coverDoc, bodyDoc] = await Promise.all([
|
|
164
|
+
PDFDocument.load(coverBytes),
|
|
165
|
+
PDFDocument.load(bodyBytes)
|
|
166
|
+
]);
|
|
167
|
+
const [coverPage] = await bodyDoc.copyPages(coverDoc, [0]);
|
|
168
|
+
bodyDoc.insertPage(0, coverPage);
|
|
169
|
+
await writeFile(pdfPath, await bodyDoc.save());
|
|
170
|
+
await Promise.all([unlink(tmpCover), unlink(tmpBody)]);
|
|
171
|
+
} finally {
|
|
172
|
+
await browser.close();
|
|
173
|
+
server.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function generate(opts) {
|
|
177
|
+
const dir = resolve(opts.reportDir);
|
|
178
|
+
if (!await exists(join(dir, "package.json"))) {
|
|
179
|
+
throw new Error(`not a scaffolded report (missing package.json): ${dir}`);
|
|
180
|
+
}
|
|
181
|
+
if (!opts.skipBuild) {
|
|
182
|
+
buildNext(dir);
|
|
183
|
+
}
|
|
184
|
+
const htmlPath = join(dir, "out", "index.html");
|
|
185
|
+
if (!await exists(htmlPath)) {
|
|
186
|
+
throw new Error(`static export missing: ${htmlPath} — run without --skip-build`);
|
|
187
|
+
}
|
|
188
|
+
const meta = await loadMeta(dir);
|
|
189
|
+
const slug = `${slugify(meta.clientName)}-${slugify(meta.reportTitle)}-${slugify(new Date().toISOString().slice(0, 10))}`;
|
|
190
|
+
const pdfPath = opts.pdfOut ? resolve(opts.pdfOut) : join(dir, `${slug}.pdf`);
|
|
191
|
+
await renderPdf(htmlPath, pdfPath, meta);
|
|
192
|
+
return { htmlPath, pdfPath };
|
|
193
|
+
}
|
|
194
|
+
function parseArgs(argv) {
|
|
195
|
+
if (argv.length === 0) {
|
|
196
|
+
throw new Error("usage: generate-pdf.ts <report-dir> [--pdf <out>] [--skip-build]");
|
|
197
|
+
}
|
|
198
|
+
const opts = { reportDir: argv[0] };
|
|
199
|
+
for (let i = 1;i < argv.length; i++) {
|
|
200
|
+
if (argv[i] === "--pdf")
|
|
201
|
+
opts.pdfOut = argv[++i];
|
|
202
|
+
else if (argv[i] === "--html")
|
|
203
|
+
opts.htmlOut = argv[++i];
|
|
204
|
+
else if (argv[i] === "--skip-build")
|
|
205
|
+
opts.skipBuild = true;
|
|
206
|
+
}
|
|
207
|
+
return opts;
|
|
208
|
+
}
|
|
209
|
+
async function run(argv = process.argv.slice(2)) {
|
|
210
|
+
const opts = parseArgs(argv);
|
|
211
|
+
const { htmlPath, pdfPath } = await generate(opts);
|
|
212
|
+
const [htmlStat, pdfStat] = await Promise.all([stat(htmlPath), stat(pdfPath)]);
|
|
213
|
+
console.log(`HTML: ${htmlPath} (${(htmlStat.size / 1024).toFixed(1)} KB)`);
|
|
214
|
+
console.log(`PDF: ${pdfPath} (${(pdfStat.size / 1024).toFixed(1)} KB)`);
|
|
215
|
+
}
|
|
216
|
+
function realResolve(p) {
|
|
217
|
+
try {
|
|
218
|
+
return realpathSync(resolve(p));
|
|
219
|
+
} catch {
|
|
220
|
+
return resolve(p);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const isMain = process.argv[1] && realResolve(process.argv[1]) === realResolve(new URL(import.meta.url).pathname);
|
|
224
|
+
if (isMain) {
|
|
225
|
+
await run();
|
|
226
|
+
}
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
// Windows because it uses --remote-debugging-pipe over stdio and Bun's Windows
|
|
9
9
|
// child-process pipe handling doesn't complete the CDP handshake.
|
|
10
10
|
//
|
|
11
|
+
// pal-build:mjs — ships as a compiled generate-pdf.mjs sibling (scripts/build-skill-tools.ts)
|
|
12
|
+
// and is invoked as that .mjs: a .ts under node_modules can't be type-stripped by Node.
|
|
13
|
+
//
|
|
11
14
|
// Usage:
|
|
12
|
-
// node
|
|
15
|
+
// node ~/.pal/skills/consulting-report/tools/generate-pdf.mjs <report-dir> [--pdf <out>] [--html <out>] [--skip-build]
|
|
13
16
|
|
|
14
17
|
import { spawnSync } from "node:child_process";
|
|
15
18
|
import { createReadStream, constants as fsConstants, realpathSync } from "node:fs";
|
|
@@ -100,7 +100,7 @@ async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
|
100
100
|
console.log(` 2. Edit lib/report-data.ts (metadata) and app/page.tsx (layout)`);
|
|
101
101
|
console.log(` 3. Live preview: bun run dev`);
|
|
102
102
|
console.log(
|
|
103
|
-
` 4. Generate PDF: node
|
|
103
|
+
` 4. Generate PDF: node ~/.pal/skills/consulting-report/tools/generate-pdf.mjs ${opts.targetDir}`
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -55,13 +55,13 @@ Invoke the skill tool. Flags:
|
|
|
55
55
|
Single-file example:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
node
|
|
58
|
+
node ~/.pal/skills/create-pdf/tools/md-to-html-pdf.mjs /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
|
-
node
|
|
64
|
+
node ~/.pal/skills/create-pdf/tools/md-to-html-pdf.mjs /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.
|
|
@@ -82,13 +82,13 @@ Default styling (A4, 25mm margins, GitHub-ish look, table-friendly, page-break-a
|
|
|
82
82
|
- `--header <html|file>` / `--footer <html|file>` — running header/footer on every page. The value is either an inline HTML string or a path to an HTML file. Templates may use Playwright's injected classes: `pageNumber`, `totalPages`, `date`, `title`, `url`.
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
node
|
|
85
|
+
node ~/.pal/skills/create-pdf/tools/md-to-html-pdf.mjs report.md --pdf report.pdf \
|
|
86
86
|
--margin 18mm \
|
|
87
87
|
--header '<div style="font-size:9px;width:100%;text-align:center;color:#888">CONFIDENTIAL</div>' \
|
|
88
88
|
--footer '<div style="font-size:9px;width:100%;text-align:right;padding-right:12mm;color:#888"><span class="pageNumber"></span>/<span class="totalPages"></span></div>'
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Header/footer templates **need an explicit `font-size`** (Playwright defaults them to 0) and render *inside* the page margin — widen `--margin` so they have room. Do NOT also add CSS `@page` margin-box rules; they duplicate. For deeper changes (fonts, base CSS), edit the `css` string in `tools/md-to-html-pdf.ts`.
|
|
91
|
+
Header/footer templates **need an explicit `font-size`** (Playwright defaults them to 0) and render *inside* the page margin — widen `--margin` so they have room. Do NOT also add CSS `@page` margin-box rules; they duplicate. For deeper changes (fonts, base CSS), edit the `css` string in `tools/md-to-html-pdf.ts` (the `.mjs` is generated from it — never edit the `.mjs`).
|
|
92
92
|
|
|
93
93
|
## Translation Variant
|
|
94
94
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { marked } from "marked";
|
|
5
|
+
import { chromium } from "playwright";
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args.length === 0) {
|
|
8
|
+
console.error("usage: md-to-html-pdf.ts <input.md> [--html <out>] [--pdf <out>] [--margin <css>] [--header <html|file>] [--footer <html|file>]");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const input = resolve(args[0]);
|
|
12
|
+
let htmlOut = "";
|
|
13
|
+
let pdfOut = "";
|
|
14
|
+
let margin = "25mm";
|
|
15
|
+
let headerArg = "";
|
|
16
|
+
let footerArg = "";
|
|
17
|
+
for (let i = 1;i < args.length; i++) {
|
|
18
|
+
if (args[i] === "--html")
|
|
19
|
+
htmlOut = resolve(args[++i]);
|
|
20
|
+
else if (args[i] === "--pdf")
|
|
21
|
+
pdfOut = resolve(args[++i]);
|
|
22
|
+
else if (args[i] === "--margin")
|
|
23
|
+
margin = args[++i];
|
|
24
|
+
else if (args[i] === "--header")
|
|
25
|
+
headerArg = args[++i];
|
|
26
|
+
else if (args[i] === "--footer")
|
|
27
|
+
footerArg = args[++i];
|
|
28
|
+
}
|
|
29
|
+
const stem = basename(input, extname(input));
|
|
30
|
+
const dir = dirname(input);
|
|
31
|
+
htmlOut ||= resolve(dir, `${stem}.html`);
|
|
32
|
+
pdfOut ||= resolve(dir, `${stem}.pdf`);
|
|
33
|
+
async function resolveTemplate(value) {
|
|
34
|
+
if (!value)
|
|
35
|
+
return "";
|
|
36
|
+
try {
|
|
37
|
+
return await readFile(resolve(value), "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const headerTemplate = await resolveTemplate(headerArg);
|
|
43
|
+
const footerTemplate = await resolveTemplate(footerArg);
|
|
44
|
+
const displayHeaderFooter = Boolean(headerTemplate || footerTemplate);
|
|
45
|
+
const md = await readFile(input, "utf8");
|
|
46
|
+
marked.setOptions({ gfm: true, breaks: false });
|
|
47
|
+
const body = await marked.parse(md);
|
|
48
|
+
const css = `
|
|
49
|
+
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
50
|
+
body {
|
|
51
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
52
|
+
font-size: 11px; line-height: 1.6; color: #1a1a1a; margin: 0;
|
|
53
|
+
}
|
|
54
|
+
h1 { font-size: 22px; border-bottom: 2px solid #333; padding-bottom: 8px; margin-top: 0; }
|
|
55
|
+
h2 { font-size: 17px; margin-top: 1.6em; }
|
|
56
|
+
h3 { font-size: 14px; }
|
|
57
|
+
h1, h2, h3, h4 { page-break-after: avoid; }
|
|
58
|
+
p, li { orphans: 3; widows: 3; }
|
|
59
|
+
hr { border: none; border-top: 1px solid #ccc; margin: 20px 0; }
|
|
60
|
+
a { color: #0366d6; text-decoration: none; }
|
|
61
|
+
blockquote { border-left: 3px solid #666; padding-left: 12px; color: #444; margin: 12px 0; }
|
|
62
|
+
code { background: #f4f4f4; padding: 1px 4px; border-radius: 3px; font-size: 10px; }
|
|
63
|
+
pre { background: #f4f4f4; padding: 10px; border-radius: 4px; overflow-x: auto; }
|
|
64
|
+
pre code { background: transparent; padding: 0; }
|
|
65
|
+
table { border-collapse: collapse; width: 100%; margin: 12px 0; page-break-inside: avoid; }
|
|
66
|
+
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; font-size: 10px; vertical-align: top; }
|
|
67
|
+
th { background: #f0f0f0; }
|
|
68
|
+
tr { page-break-inside: avoid; }
|
|
69
|
+
ul, ol { padding-left: 1.4em; }
|
|
70
|
+
`;
|
|
71
|
+
const html = `<!doctype html>
|
|
72
|
+
<html>
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="utf-8">
|
|
75
|
+
<title>${stem}</title>
|
|
76
|
+
<style>${css}</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
${body}
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
82
|
+
`;
|
|
83
|
+
await writeFile(htmlOut, html, "utf8");
|
|
84
|
+
const browser = await chromium.launch();
|
|
85
|
+
try {
|
|
86
|
+
const page = await browser.newPage();
|
|
87
|
+
await page.goto(pathToFileURL(htmlOut).href, { waitUntil: "networkidle" });
|
|
88
|
+
await page.pdf({
|
|
89
|
+
path: pdfOut,
|
|
90
|
+
format: "A4",
|
|
91
|
+
margin: { top: margin, right: margin, bottom: margin, left: margin },
|
|
92
|
+
printBackground: true,
|
|
93
|
+
preferCSSPageSize: false,
|
|
94
|
+
displayHeaderFooter,
|
|
95
|
+
headerTemplate: headerTemplate || "<span></span>",
|
|
96
|
+
footerTemplate: footerTemplate || "<span></span>"
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
await browser.close();
|
|
100
|
+
}
|
|
101
|
+
const [htmlStat, pdfStat] = await Promise.all([stat(htmlOut), stat(pdfOut)]);
|
|
102
|
+
console.log(`HTML: ${htmlOut} (${(htmlStat.size / 1024).toFixed(1)} KB)`);
|
|
103
|
+
console.log(`PDF: ${pdfOut} (${(pdfStat.size / 1024).toFixed(1)} KB)`);
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
// because it uses --remote-debugging-pipe over stdio and Bun's Windows child-process
|
|
7
7
|
// pipe handling doesn't complete the CDP handshake.
|
|
8
8
|
//
|
|
9
|
+
// pal-build:mjs — ships as a compiled md-to-html-pdf.mjs sibling (scripts/build-skill-tools.ts)
|
|
10
|
+
// and is invoked as that .mjs: a .ts under node_modules can't be type-stripped by Node.
|
|
11
|
+
//
|
|
9
12
|
// Usage:
|
|
10
|
-
// node
|
|
13
|
+
// node ~/.pal/skills/create-pdf/tools/md-to-html-pdf.mjs <input.md> \
|
|
11
14
|
// [--html <out.html>] [--pdf <out.pdf>] [--margin <css>] [--header <html|file>] [--footer <html|file>]
|
|
12
15
|
// --margin defaults to 25mm (all sides). --header/--footer accept inline HTML or a file path.
|
|
13
16
|
|
|
@@ -23,7 +23,7 @@ If neither engine is usable, the tool prints `NO_PLAYWRIGHT_CLI` and exits non-z
|
|
|
23
23
|
2. Run the tool (it prints the absolute PNG path as its last stdout line):
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
node
|
|
26
|
+
node ~/.pal/skills/playwright/tools/shot.mjs <url> \
|
|
27
27
|
[--viewport 1440x900] [--full-page] [--selector "<css>"] [-o <out.png>]
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const USAGE = "usage: shot.ts <url> [-o <file>] [--viewport WxH] [--full-page] [--selector <css>] [--wait <ms>]";
|
|
2
|
+
export function parseArgs(argv) {
|
|
3
|
+
let url = "";
|
|
4
|
+
let out = "";
|
|
5
|
+
let viewport;
|
|
6
|
+
let fullPage = false;
|
|
7
|
+
let selector;
|
|
8
|
+
let waitMs;
|
|
9
|
+
for (let i = 0;i < argv.length; i++) {
|
|
10
|
+
const a = argv[i];
|
|
11
|
+
if (a === "-o" || a === "--out")
|
|
12
|
+
out = argv[++i] ?? "";
|
|
13
|
+
else if (a === "--viewport") {
|
|
14
|
+
const m = /^(\d+)[x,](\d+)$/.exec(argv[++i] ?? "");
|
|
15
|
+
if (!m)
|
|
16
|
+
throw new Error("--viewport expects WxH, e.g. 1440x900");
|
|
17
|
+
viewport = { width: Number(m[1]), height: Number(m[2]) };
|
|
18
|
+
} else if (a === "--full-page")
|
|
19
|
+
fullPage = true;
|
|
20
|
+
else if (a === "--selector")
|
|
21
|
+
selector = argv[++i];
|
|
22
|
+
else if (a === "--wait") {
|
|
23
|
+
const n = Number(argv[++i]);
|
|
24
|
+
if (!Number.isFinite(n))
|
|
25
|
+
throw new Error("--wait expects a number of milliseconds");
|
|
26
|
+
waitMs = n;
|
|
27
|
+
} else if (!a.startsWith("-") && !url)
|
|
28
|
+
url = a;
|
|
29
|
+
else
|
|
30
|
+
throw new Error(`unknown argument: ${a}
|
|
31
|
+
${USAGE}`);
|
|
32
|
+
}
|
|
33
|
+
if (!url)
|
|
34
|
+
throw new Error(`a URL is required
|
|
35
|
+
${USAGE}`);
|
|
36
|
+
return { url, out, viewport, fullPage, selector, waitMs };
|
|
37
|
+
}
|
|
38
|
+
export function chooseTier(opts) {
|
|
39
|
+
if (!opts.cliAvailable)
|
|
40
|
+
return "node";
|
|
41
|
+
if (opts.viewport || opts.fullPage)
|
|
42
|
+
return "node";
|
|
43
|
+
return "cli";
|
|
44
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { chooseTier, parseArgs } from "./shot-lib.mjs";
|
|
6
|
+
function playwrightCliAvailable() {
|
|
7
|
+
try {
|
|
8
|
+
return spawnSync("playwright-cli", ["--version"], { stdio: "ignore" }).status === 0;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function runViaCli(opts, out) {
|
|
14
|
+
const work = mkdtempSync(join(tmpdir(), "pal-pwcli-"));
|
|
15
|
+
const run = (args, quiet = false) => spawnSync("playwright-cli", args, { stdio: quiet ? "ignore" : "inherit", cwd: work });
|
|
16
|
+
try {
|
|
17
|
+
if (run(["open", opts.url]).status !== 0)
|
|
18
|
+
return false;
|
|
19
|
+
const args = ["screenshot", `--filename=${out}`];
|
|
20
|
+
if (opts.selector)
|
|
21
|
+
args.push(opts.selector);
|
|
22
|
+
const shot = run(args);
|
|
23
|
+
run(["close"], true);
|
|
24
|
+
return shot.status === 0 && existsSync(out);
|
|
25
|
+
} finally {
|
|
26
|
+
rmSync(work, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function runViaNode(opts, out) {
|
|
30
|
+
let chromium;
|
|
31
|
+
try {
|
|
32
|
+
({ chromium } = await import("playwright"));
|
|
33
|
+
} catch {
|
|
34
|
+
return "unavailable";
|
|
35
|
+
}
|
|
36
|
+
let browser;
|
|
37
|
+
try {
|
|
38
|
+
browser = await chromium.launch();
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error(`chromium launch failed: ${e.message}`);
|
|
41
|
+
return "unavailable";
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const page = await browser.newPage(opts.viewport ? { viewport: opts.viewport } : {});
|
|
45
|
+
await page.goto(opts.url, { waitUntil: "networkidle" });
|
|
46
|
+
if (opts.waitMs)
|
|
47
|
+
await page.waitForTimeout(opts.waitMs);
|
|
48
|
+
if (opts.selector)
|
|
49
|
+
await page.locator(opts.selector).screenshot({ path: out });
|
|
50
|
+
else
|
|
51
|
+
await page.screenshot({ path: out, fullPage: opts.fullPage });
|
|
52
|
+
return existsSync(out) ? "ok" : "error";
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(`screenshot failed: ${e.message}`);
|
|
55
|
+
return "error";
|
|
56
|
+
} finally {
|
|
57
|
+
await browser.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function main() {
|
|
61
|
+
let opts;
|
|
62
|
+
try {
|
|
63
|
+
opts = parseArgs(process.argv.slice(2));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(e.message);
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
const out = opts.out ? resolve(opts.out) : join(tmpdir(), `pal-shot-${Date.now()}.png`);
|
|
69
|
+
const tier = chooseTier({
|
|
70
|
+
cliAvailable: playwrightCliAvailable(),
|
|
71
|
+
viewport: opts.viewport,
|
|
72
|
+
fullPage: opts.fullPage
|
|
73
|
+
});
|
|
74
|
+
if (tier === "cli" && runViaCli(opts, out)) {
|
|
75
|
+
console.log(out);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const result = await runViaNode(opts, out);
|
|
79
|
+
if (result === "ok") {
|
|
80
|
+
console.log(out);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (result === "unavailable") {
|
|
84
|
+
console.error("NO_PLAYWRIGHT_CLI");
|
|
85
|
+
process.exit(3);
|
|
86
|
+
}
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
await main();
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
// If neither engine works, prints NO_PLAYWRIGHT_CLI on stderr and exits non-zero so the
|
|
10
10
|
// caller (SKILL.md) can fall back to the Playwright MCP.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
12
|
+
// pal-build:mjs — ships as a compiled shot.mjs sibling (scripts/build-skill-tools.ts).
|
|
13
|
+
// Run under Node via the compiled file (a .ts under node_modules can't be type-stripped;
|
|
14
|
+
// plain .mjs needs no stripping and runs on every OS, keeping the Windows fallback):
|
|
15
|
+
// node ~/.pal/skills/playwright/tools/shot.mjs <url> [opts]
|
|
14
16
|
|
|
15
17
|
import { spawnSync } from "node:child_process";
|
|
16
18
|
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
"Bash(stat //*)",
|
|
19
19
|
"Bash(readlink //*)",
|
|
20
20
|
"Bash(lsof *)",
|
|
21
|
+
"Bash(rtk read *)",
|
|
22
|
+
"Bash(rtk ls *)",
|
|
23
|
+
"Bash(rtk tree *)",
|
|
24
|
+
"Bash(rtk find *)",
|
|
25
|
+
"Bash(rtk grep *)",
|
|
26
|
+
"Bash(rtk rg *)",
|
|
27
|
+
"Bash(rtk wc *)",
|
|
28
|
+
"Bash(rtk diff *)",
|
|
29
|
+
"Bash(rtk json *)",
|
|
30
|
+
"Bash(rtk log *)",
|
|
31
|
+
"Bash(rtk deps *)",
|
|
32
|
+
"Bash(rtk env *)",
|
|
33
|
+
"Bash(rtk smart *)",
|
|
34
|
+
"Bash(rtk gain *)",
|
|
21
35
|
"Bash(bun ~/.pal/skills/*/tools/*.ts *)",
|
|
22
36
|
"Bash(bun ~/.pal/tools/*.ts *)",
|
|
23
37
|
"Bash(node --experimental-strip-types ~/.pal/skills/consulting-report/tools/generate-pdf.ts *)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.62.
|
|
3
|
+
"version": "0.62.2",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"jscpd": "jscpd --noTips",
|
|
47
47
|
"lint-staged": "lint-staged",
|
|
48
48
|
"prepare": "bun .husky/install.mjs",
|
|
49
|
+
"build:skill-tools": "bun run scripts/build-skill-tools.ts",
|
|
50
|
+
"prepack": "bun run build:skill-tools",
|
|
49
51
|
"install:all": "bun run src/cli/index.ts cli install",
|
|
50
52
|
"uninstall": "bun run src/cli/index.ts cli uninstall",
|
|
51
53
|
"tool:synthesize": "bun run src/tools/agent/synthesize.ts",
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -26,17 +26,24 @@ export function flagshipAuthorModel(agent: AgentType): string | undefined {
|
|
|
26
26
|
return FLAGSHIP_AUTHOR_MODEL[agent];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface ModelPricing {
|
|
30
|
+
input: number;
|
|
31
|
+
output: number;
|
|
32
|
+
cacheWrite5m: number;
|
|
33
|
+
cacheWrite1h: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TokenUsage {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
cacheWrite5m: number;
|
|
41
|
+
cacheWrite1h: number;
|
|
42
|
+
cacheRead: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
/** Pricing per million tokens (USD) — from https://platform.claude.com/docs/en/about-claude/pricing */
|
|
30
|
-
export const MODEL_PRICING: Record<
|
|
31
|
-
string,
|
|
32
|
-
{
|
|
33
|
-
input: number;
|
|
34
|
-
output: number;
|
|
35
|
-
cacheWrite5m: number;
|
|
36
|
-
cacheWrite1h: number;
|
|
37
|
-
cacheRead: number;
|
|
38
|
-
}
|
|
39
|
-
> = {
|
|
46
|
+
export const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
40
47
|
[HAIKU_MODEL]: {
|
|
41
48
|
input: 1,
|
|
42
49
|
output: 5,
|
|
@@ -51,6 +58,13 @@ export const MODEL_PRICING: Record<
|
|
|
51
58
|
cacheWrite1h: 20,
|
|
52
59
|
cacheRead: 1,
|
|
53
60
|
},
|
|
61
|
+
"claude-opus-5": {
|
|
62
|
+
input: 5,
|
|
63
|
+
output: 25,
|
|
64
|
+
cacheWrite5m: 6.25,
|
|
65
|
+
cacheWrite1h: 10,
|
|
66
|
+
cacheRead: 0.5,
|
|
67
|
+
},
|
|
54
68
|
"claude-opus-4-8": {
|
|
55
69
|
input: 5,
|
|
56
70
|
output: 25,
|
|
@@ -72,6 +86,13 @@ export const MODEL_PRICING: Record<
|
|
|
72
86
|
cacheWrite1h: 10,
|
|
73
87
|
cacheRead: 0.5,
|
|
74
88
|
},
|
|
89
|
+
"claude-opus-4-5": {
|
|
90
|
+
input: 5,
|
|
91
|
+
output: 25,
|
|
92
|
+
cacheWrite5m: 6.25,
|
|
93
|
+
cacheWrite1h: 10,
|
|
94
|
+
cacheRead: 0.5,
|
|
95
|
+
},
|
|
75
96
|
// Claude Sonnet 5 — introductory pricing through 2026-08-31; standard (3/15/3.75/6/0.30) applies from 2026-09-01.
|
|
76
97
|
"claude-sonnet-5": {
|
|
77
98
|
input: 2,
|
|
@@ -95,3 +116,37 @@ export const MODEL_PRICING: Record<
|
|
|
95
116
|
cacheRead: 0.3,
|
|
96
117
|
},
|
|
97
118
|
};
|
|
119
|
+
|
|
120
|
+
function longestPrefixKey(model: string): string | null {
|
|
121
|
+
let best: string | null = null;
|
|
122
|
+
for (const key of Object.keys(MODEL_PRICING)) {
|
|
123
|
+
if (model.startsWith(key) && (best === null || key.length > best.length)) best = key;
|
|
124
|
+
}
|
|
125
|
+
return best;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Rates for a model ID. Transcripts carry variants of the same model — dated
|
|
130
|
+
* (`claude-opus-5-20260115`) and context-tagged (`claude-opus-5[1m]`) — so an
|
|
131
|
+
* exact miss falls back to the longest table key the ID starts with.
|
|
132
|
+
*/
|
|
133
|
+
export function pricingFor(model: string): ModelPricing | null {
|
|
134
|
+
const exact = MODEL_PRICING[model];
|
|
135
|
+
if (exact) return exact;
|
|
136
|
+
const prefix = longestPrefixKey(model);
|
|
137
|
+
return prefix ? MODEL_PRICING[prefix] : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** USD cost of a token usage record. Unpriced models bill as 0. */
|
|
141
|
+
export function costOfUsage(model: string, usage: TokenUsage): number {
|
|
142
|
+
const p = pricingFor(model);
|
|
143
|
+
if (!p) return 0;
|
|
144
|
+
return (
|
|
145
|
+
(usage.input * p.input +
|
|
146
|
+
usage.output * p.output +
|
|
147
|
+
usage.cacheWrite5m * p.cacheWrite5m +
|
|
148
|
+
usage.cacheWrite1h * p.cacheWrite1h +
|
|
149
|
+
usage.cacheRead * p.cacheRead) /
|
|
150
|
+
1_000_000
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -9,7 +9,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
|
-
import {
|
|
12
|
+
import { costOfUsage } from "../hooks/lib/models";
|
|
13
13
|
|
|
14
14
|
// ── Types ──
|
|
15
15
|
|
|
@@ -142,16 +142,13 @@ function parseSession(filepath: string, sessionId: string): Usage {
|
|
|
142
142
|
: (u.cache_creation_input_tokens ?? 0);
|
|
143
143
|
const cacheWrite1h = cw1h ?? 0;
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
cr * p.cacheRead) /
|
|
153
|
-
1_000_000;
|
|
154
|
-
}
|
|
145
|
+
usage.cost += costOfUsage(model, {
|
|
146
|
+
input,
|
|
147
|
+
output,
|
|
148
|
+
cacheWrite5m,
|
|
149
|
+
cacheWrite1h,
|
|
150
|
+
cacheRead: cr,
|
|
151
|
+
});
|
|
155
152
|
|
|
156
153
|
usage.input += input;
|
|
157
154
|
usage.output += output;
|
package/src/tools/token-cost.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { resolve } from "node:path";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
|
-
import {
|
|
16
|
+
import { costOfUsage } from "../hooks/lib/models";
|
|
17
17
|
import { palHome } from "../hooks/lib/paths";
|
|
18
18
|
import { findBinaryOnPath } from "../hooks/lib/which";
|
|
19
19
|
|
|
@@ -59,34 +59,6 @@ function emptyTimeBuckets(): TimeBuckets {
|
|
|
59
59
|
|
|
60
60
|
// ── Helpers ──
|
|
61
61
|
|
|
62
|
-
function findPricing(model: string): (typeof MODEL_PRICING)[string] | null {
|
|
63
|
-
if (MODEL_PRICING[model]) return MODEL_PRICING[model];
|
|
64
|
-
for (const key of Object.keys(MODEL_PRICING)) {
|
|
65
|
-
if (model.startsWith(key)) return MODEL_PRICING[key];
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function costForUsage(
|
|
71
|
-
model: string,
|
|
72
|
-
input: number,
|
|
73
|
-
output: number,
|
|
74
|
-
cacheWrite5m: number,
|
|
75
|
-
cacheWrite1h: number,
|
|
76
|
-
cacheRead: number
|
|
77
|
-
): number {
|
|
78
|
-
const p = findPricing(model);
|
|
79
|
-
if (!p) return 0;
|
|
80
|
-
return (
|
|
81
|
-
(input * p.input +
|
|
82
|
-
output * p.output +
|
|
83
|
-
cacheWrite5m * p.cacheWrite5m +
|
|
84
|
-
cacheWrite1h * p.cacheWrite1h +
|
|
85
|
-
cacheRead * p.cacheRead) /
|
|
86
|
-
1_000_000
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
62
|
function addToBucket(
|
|
91
63
|
bucket: Bucket,
|
|
92
64
|
model: string,
|
|
@@ -101,14 +73,13 @@ function addToBucket(
|
|
|
101
73
|
bucket.cacheWrite5m += cacheWrite5m;
|
|
102
74
|
bucket.cacheWrite1h += cacheWrite1h;
|
|
103
75
|
bucket.cacheRead += cacheRead;
|
|
104
|
-
bucket.cost +=
|
|
105
|
-
model,
|
|
76
|
+
bucket.cost += costOfUsage(model, {
|
|
106
77
|
input,
|
|
107
78
|
output,
|
|
108
79
|
cacheWrite5m,
|
|
109
80
|
cacheWrite1h,
|
|
110
|
-
cacheRead
|
|
111
|
-
);
|
|
81
|
+
cacheRead,
|
|
82
|
+
});
|
|
112
83
|
bucket.calls++;
|
|
113
84
|
}
|
|
114
85
|
|