ok-claude 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,1065 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import open from "open";
5
+
6
+ // src/pipeline.ts
7
+ import { writeFile } from "fs/promises";
8
+ import { statSync } from "fs";
9
+ import { homedir as homedir2, userInfo } from "os";
10
+ import { join as join2, resolve } from "path";
11
+ import { execSync } from "child_process";
12
+
13
+ // src/discover.ts
14
+ import { readdir, stat } from "fs/promises";
15
+ import { homedir } from "os";
16
+ import { join, sep } from "path";
17
+ function logsRoot() {
18
+ return join(homedir(), ".claude", "projects");
19
+ }
20
+ var SUBAGENT_SEG = `${sep}subagents${sep}`;
21
+ async function discoverLogs() {
22
+ const root = logsRoot();
23
+ const paths = [];
24
+ try {
25
+ const entries = await readdir(root, {
26
+ recursive: true,
27
+ withFileTypes: true
28
+ });
29
+ for (const e of entries) {
30
+ if (!e.isFile()) continue;
31
+ if (!e.name.endsWith(".jsonl")) continue;
32
+ const full = join(e.parentPath, e.name);
33
+ if (full.includes(SUBAGENT_SEG)) continue;
34
+ paths.push(full);
35
+ }
36
+ } catch (err) {
37
+ if (err.code === "ENOENT") return [];
38
+ throw err;
39
+ }
40
+ paths.sort();
41
+ const out = [];
42
+ for (const p of paths) {
43
+ try {
44
+ const s = await stat(p);
45
+ out.push({ path: p, size: s.size });
46
+ } catch {
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ // src/stream.ts
53
+ import { createReadStream } from "fs";
54
+ import { createInterface } from "readline";
55
+
56
+ // src/parse.ts
57
+ var PROSE_TYPES = /* @__PURE__ */ new Set(["user", "assistant"]);
58
+ function dropReason(raw) {
59
+ if (raw.type !== void 0 && !PROSE_TYPES.has(raw.type)) return "non-prose-type";
60
+ if (raw.isMeta === true) return "isMeta";
61
+ if (raw.isSidechain === true) return "isSidechain";
62
+ if (raw.isApiErrorMessage === true) return "isApiErrorMessage";
63
+ if (raw.isCompactSummary === true) return "isCompactSummary";
64
+ if (raw.isVisibleInTranscriptOnly === true) return "isVisibleInTranscriptOnly";
65
+ const role = raw.message?.role;
66
+ if (role !== "user" && role !== "assistant") return "non-prose-role";
67
+ return null;
68
+ }
69
+ function extractText(content) {
70
+ if (typeof content === "string") return content;
71
+ if (!Array.isArray(content)) return "";
72
+ let out = "";
73
+ for (const block of content) {
74
+ if (block && block.type === "text" && typeof block.text === "string") {
75
+ out += block.text;
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ var HARNESS_TAGS = [
81
+ "system-reminder",
82
+ "command-name",
83
+ "command-message",
84
+ "command-args",
85
+ "local-command-stdout",
86
+ "local-command-stderr",
87
+ "task-notification",
88
+ "bash-input",
89
+ "bash-stdout",
90
+ "bash-stderr"
91
+ ];
92
+ var HARNESS_TAG_RE = new RegExp(
93
+ `<(${HARNESS_TAGS.join("|")})\\b[^>]*>[\\s\\S]*?<\\/\\1>`,
94
+ "g"
95
+ );
96
+ function stripHarnessTags(text) {
97
+ return text.replace(HARNESS_TAG_RE, "");
98
+ }
99
+ function parseLine(line) {
100
+ const trimmed = line.trim();
101
+ if (!trimmed) return null;
102
+ let raw;
103
+ try {
104
+ raw = JSON.parse(trimmed);
105
+ } catch {
106
+ return null;
107
+ }
108
+ if (dropReason(raw) !== null) return null;
109
+ const role = raw.message.role;
110
+ const rawText = extractText(raw.message?.content);
111
+ const text = stripHarnessTags(rawText);
112
+ if (!text.trim()) return null;
113
+ const event = { role, text };
114
+ if (typeof raw.timestamp === "string") event.timestamp = raw.timestamp;
115
+ const usage = raw.message?.usage;
116
+ if (usage && typeof usage.input_tokens === "number") {
117
+ event.tokensIn = usage.input_tokens;
118
+ }
119
+ if (usage && typeof usage.output_tokens === "number") {
120
+ event.tokensOut = usage.output_tokens;
121
+ }
122
+ return event;
123
+ }
124
+
125
+ // src/stream.ts
126
+ async function* streamEvents(files, onProgress) {
127
+ let bytesDone = 0;
128
+ for (let i = 0; i < files.length; i++) {
129
+ const entry = files[i];
130
+ try {
131
+ const rl = createInterface({
132
+ input: createReadStream(entry.path, { encoding: "utf8" }),
133
+ crlfDelay: Infinity
134
+ });
135
+ for await (const line of rl) {
136
+ const e = parseLine(line);
137
+ if (e) yield e;
138
+ }
139
+ } catch (err) {
140
+ process.stderr.write(
141
+ `ok-claude: skipped ${entry.path}: ${err.message}
142
+ `
143
+ );
144
+ }
145
+ bytesDone += entry.size;
146
+ onProgress(bytesDone, i + 1);
147
+ }
148
+ }
149
+
150
+ // src/denoise.ts
151
+ var FENCED_BLOCK = /```[\s\S]*?```/g;
152
+ var UNTERMINATED_FENCE = /```[\s\S]*$/;
153
+ var INLINE_BACKTICK = /`[^`\n]*`/g;
154
+ var STACK_FRAME_SINGLE = /\bat\s+[\w.<>$]+\s*\([^)]*[/\\][^)]*:\d+(?::\d+)?\)/g;
155
+ var WIN_PATH = /[A-Za-z]:\\[\w\\.\-]+/g;
156
+ var URL_PATTERN = /\bhttps?:\/\/[^\s)\]"'<>]+/g;
157
+ var PATH_WITH_EXT = /\b[\w.\-]+(?:[\\/][\w.\-]+)+\.[a-zA-Z]\w*\b/g;
158
+ var DEEP_PATH = /\b[\w.\-]{2,}[\\/][\w.\-]{2,}(?:[\\/][\w.\-]+){1,}/g;
159
+ var N_CLITIC = new RegExp("(\\p{L})n['\u2019]t\\b", "giu");
160
+ var CLITIC = new RegExp("(\\p{L})['\u2019](?:s|d|m|re|ve|ll)\\b", "giu");
161
+ function denoiseMarkdown(text) {
162
+ if (!text) return text;
163
+ let out = text.replace(FENCED_BLOCK, " ");
164
+ out = out.replace(UNTERMINATED_FENCE, " ");
165
+ out = stripIndentedBlocks(out);
166
+ out = stripLongStructuredLines(out);
167
+ out = stripNonFencedPasteBlocks(out);
168
+ out = out.replace(INLINE_BACKTICK, " ");
169
+ out = out.replace(STACK_FRAME_SINGLE, " ");
170
+ out = out.replace(URL_PATTERN, " ");
171
+ out = out.replace(WIN_PATH, " ");
172
+ out = out.replace(PATH_WITH_EXT, " ");
173
+ out = out.replace(DEEP_PATH, " ");
174
+ out = out.replace(N_CLITIC, "$1");
175
+ out = out.replace(CLITIC, "$1");
176
+ return out;
177
+ }
178
+ var STACK_FRAME_LINE = /^\s*(at\s+\S+\.|File\s+["'].+["'],\s*line\s+\d+)/;
179
+ var TS_TYPE_ERROR = /Type\s+'[^']+'\s+is\s+not\s+assignable|error\s+TS\d{4}:|NullReferenceException/;
180
+ var IDENT_DOT = /[a-z][a-zA-Z0-9]+\.[a-z][a-zA-Z0-9]|[A-Z][a-zA-Z0-9]+\.[A-Z][a-zA-Z0-9]/;
181
+ var ERROR_CLASS = /[A-Z][a-zA-Z]+(Error|Exception)\b/;
182
+ var NATIVE_JIT_FRAME = /^0x[0-9a-fA-F]+\s+\((?:Mono|Unity|UnityEditor|UnityEngine|UnityPlayer)\b/;
183
+ var BUILD_WARNING = /^(?:WARNING:|ERROR:|FAILURE:|> (?:Task|Configure project)|BUILD FAILED)\b/;
184
+ var STRUCT_CHARS = new Set(`{}[]():,;"='|<>`);
185
+ var STRUCT_DENSITY_THRESHOLD = 0.22;
186
+ var STRUCT_MIN_LINE_LEN = 20;
187
+ var STRUCT_INLINE_MIN_LEN = 200;
188
+ var JSON_KEY_PATTERN = /"\w+":/g;
189
+ var JSON_KEY_ANCHOR_MIN = 3;
190
+ function stripLongStructuredLines(text) {
191
+ const lines = text.split("\n");
192
+ return lines.map((l) => {
193
+ const nws = l.replace(/\s/g, "");
194
+ if (nws.length < STRUCT_INLINE_MIN_LEN) return l;
195
+ const keyHits = (l.match(JSON_KEY_PATTERN) ?? []).length;
196
+ if (keyHits >= JSON_KEY_ANCHOR_MIN) return "";
197
+ let hits = 0;
198
+ for (const c of nws) if (STRUCT_CHARS.has(c)) hits++;
199
+ return hits / nws.length >= STRUCT_DENSITY_THRESHOLD ? "" : l;
200
+ }).join("\n");
201
+ }
202
+ function looksLikeStructuredLine(line) {
203
+ const nws = line.replace(/\s/g, "");
204
+ if (nws.length < STRUCT_MIN_LINE_LEN) return false;
205
+ let hits = 0;
206
+ for (const c of nws) if (STRUCT_CHARS.has(c)) hits++;
207
+ return hits / nws.length >= STRUCT_DENSITY_THRESHOLD;
208
+ }
209
+ function looksLikeStackOrError(line) {
210
+ if (STACK_FRAME_LINE.test(line)) return true;
211
+ if (TS_TYPE_ERROR.test(line)) return true;
212
+ if (NATIVE_JIT_FRAME.test(line)) return true;
213
+ if (BUILD_WARNING.test(line)) return true;
214
+ if (looksLikeStructuredLine(line)) return true;
215
+ const tokens = line.split(/\s+/).filter((t) => t.length > 1);
216
+ if (tokens.length < 3) return false;
217
+ let hits = 0;
218
+ for (const t of tokens) {
219
+ if (IDENT_DOT.test(t) || ERROR_CLASS.test(t)) hits++;
220
+ }
221
+ return hits * 2 >= tokens.length;
222
+ }
223
+ function stripNonFencedPasteBlocks(text) {
224
+ const lines = text.split("\n");
225
+ const out = [];
226
+ let i = 0;
227
+ while (i < lines.length) {
228
+ let j = i;
229
+ while (j < lines.length && looksLikeStackOrError(lines[j])) j++;
230
+ if (j - i >= 3) {
231
+ i = j;
232
+ continue;
233
+ }
234
+ out.push(lines[i]);
235
+ i++;
236
+ }
237
+ return out.join("\n");
238
+ }
239
+ function stripIndentedBlocks(text) {
240
+ const lines = text.split("\n");
241
+ const kept = [];
242
+ let prevBlank = true;
243
+ let inBlock = false;
244
+ for (const line of lines) {
245
+ const isIndented = /^(?: {4}|\t)/.test(line);
246
+ const isBlank = line.trim() === "";
247
+ if (inBlock) {
248
+ if (isIndented || isBlank) {
249
+ continue;
250
+ }
251
+ inBlock = false;
252
+ }
253
+ if (prevBlank && isIndented) {
254
+ inBlock = true;
255
+ continue;
256
+ }
257
+ kept.push(line);
258
+ prevBlank = isBlank;
259
+ }
260
+ return kept.join("\n");
261
+ }
262
+
263
+ // src/tokenize.ts
264
+ var STOPWORDS = /* @__PURE__ */ new Set([
265
+ "the",
266
+ "a",
267
+ "an",
268
+ "is",
269
+ "are",
270
+ "was",
271
+ "were",
272
+ "of",
273
+ "to",
274
+ "in",
275
+ "on",
276
+ "at",
277
+ "for",
278
+ "and",
279
+ "or",
280
+ "but",
281
+ "i",
282
+ "you",
283
+ "it",
284
+ "this",
285
+ "that",
286
+ "with",
287
+ "as",
288
+ "be",
289
+ "by",
290
+ "from",
291
+ "if",
292
+ "so",
293
+ "not",
294
+ "do",
295
+ "does",
296
+ "did",
297
+ "have",
298
+ "has",
299
+ "had",
300
+ "will",
301
+ "would",
302
+ "can",
303
+ "could",
304
+ "should",
305
+ "just",
306
+ "like",
307
+ "get",
308
+ "got",
309
+ // n't-clitic survivors (BUG-004): won't → "wo", can't → "ca"
310
+ "wo",
311
+ "ca"
312
+ ]);
313
+ var CJK_SCRIPT = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+$/u;
314
+ var SHORT_LATIN_KEEP = /* @__PURE__ */ new Set(["y", "n", "k"]);
315
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
316
+ function tokenize(text) {
317
+ if (!text) return [];
318
+ const out = [];
319
+ for (const seg of segmenter.segment(text)) {
320
+ if (!seg.isWordLike) continue;
321
+ const lower = seg.segment.toLocaleLowerCase();
322
+ const isCjk = CJK_SCRIPT.test(lower);
323
+ if (!isCjk && [...lower].length < 2 && !SHORT_LATIN_KEEP.has(lower))
324
+ continue;
325
+ if (/^\d+$/.test(lower)) continue;
326
+ if (STOPWORDS.has(lower)) continue;
327
+ out.push(lower);
328
+ }
329
+ return out;
330
+ }
331
+
332
+ // src/aggregate.ts
333
+ function foldOpener(map, op) {
334
+ let inner = map.get(op.key);
335
+ if (!inner) {
336
+ inner = /* @__PURE__ */ new Map();
337
+ map.set(op.key, inner);
338
+ }
339
+ inner.set(op.surface, (inner.get(op.surface) ?? 0) + 1);
340
+ }
341
+ function topNOpeners(map, n) {
342
+ const entries = [];
343
+ for (const [, surfaceMap] of map) {
344
+ let total = 0;
345
+ let bestSurface = "";
346
+ let bestCount = -1;
347
+ for (const [surface, count] of surfaceMap) {
348
+ total += count;
349
+ if (count > bestCount || count === bestCount && surface < bestSurface) {
350
+ bestCount = count;
351
+ bestSurface = surface;
352
+ }
353
+ }
354
+ entries.push({ display: bestSurface, count: total });
355
+ }
356
+ entries.sort((a, b) => {
357
+ if (b.count !== a.count) return b.count - a.count;
358
+ return a.display < b.display ? -1 : a.display > b.display ? 1 : 0;
359
+ });
360
+ return entries.slice(0, n);
361
+ }
362
+
363
+ // src/openers.ts
364
+ var TRAILING_PUNCT = /[.,!?;:。、!?]+$/u;
365
+ var segmenter2 = new Intl.Segmenter(void 0, { granularity: "word" });
366
+ function firstOpener(text) {
367
+ if (!text) return null;
368
+ for (const seg of segmenter2.segment(text)) {
369
+ if (!seg.isWordLike) continue;
370
+ const surface = seg.segment.replace(TRAILING_PUNCT, "");
371
+ if (!surface) continue;
372
+ const key = surface.toLocaleLowerCase().replace(TRAILING_PUNCT, "");
373
+ if (!key) continue;
374
+ return { key, surface };
375
+ }
376
+ return null;
377
+ }
378
+
379
+ // src/render.ts
380
+ import { readFileSync } from "fs";
381
+ var VENDOR_JS = readFileSync(
382
+ new URL("./vendor/wordcloud2.js", import.meta.url),
383
+ "utf8"
384
+ );
385
+ var HTML_TO_IMAGE_JS = readFileSync(
386
+ new URL("./vendor/html-to-image.js", import.meta.url),
387
+ "utf8"
388
+ );
389
+ function fontB64(name) {
390
+ return readFileSync(
391
+ new URL(`./vendor/fonts/${name}`, import.meta.url)
392
+ ).toString("base64");
393
+ }
394
+ var FONT_CSS = `
395
+ @font-face {
396
+ font-family: 'Anton';
397
+ font-style: normal;
398
+ font-weight: 400;
399
+ font-display: swap;
400
+ src: url(data:font/woff2;base64,${fontB64("anton-400.woff2")}) format('woff2');
401
+ }
402
+ @font-face {
403
+ font-family: 'Archivo Narrow';
404
+ font-style: normal;
405
+ font-weight: 100 900;
406
+ font-display: swap;
407
+ src: url(data:font/woff2;base64,${fontB64("archivo-narrow.woff2")}) format('woff2');
408
+ }
409
+ @font-face {
410
+ font-family: 'Inter';
411
+ font-style: normal;
412
+ font-weight: 100 900;
413
+ font-display: swap;
414
+ src: url(data:font/woff2;base64,${fontB64("inter.woff2")}) format('woff2');
415
+ }
416
+ @font-face {
417
+ font-family: 'JetBrains Mono';
418
+ font-style: normal;
419
+ font-weight: 100 900;
420
+ font-display: swap;
421
+ src: url(data:font/woff2;base64,${fontB64("jetbrains-mono.woff2")}) format('woff2');
422
+ }
423
+ `;
424
+ function safeJson(value) {
425
+ return JSON.stringify(value).replace(/<\/(script)/gi, "<\\/$1");
426
+ }
427
+ function escapeHtml(s) {
428
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
429
+ }
430
+ function fmtTokens(n) {
431
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
432
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
433
+ return String(n);
434
+ }
435
+ function computeDays(range) {
436
+ if (!range) return 30;
437
+ const start = new Date(range[0]).getTime();
438
+ const end = new Date(range[1]).getTime();
439
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 30;
440
+ return Math.max(1, Math.round((end - start) / 864e5));
441
+ }
442
+ function renderHtml(input) {
443
+ const dataJson = safeJson({
444
+ topUser: input.topUser,
445
+ topClaude: input.topClaude,
446
+ meta: input.meta
447
+ });
448
+ const burned = input.meta.tokensOut;
449
+ const days = computeDays(input.meta.dateRange);
450
+ const perDay = Math.round(burned / days);
451
+ const burnedTxt = escapeHtml(`${fmtTokens(burned)} tokens`);
452
+ const daysTxt = escapeHtml(`${days} days`);
453
+ const perDayTxt = escapeHtml(`${fmtTokens(perDay)} tokens`);
454
+ const msgCountTxt = escapeHtml(input.meta.messages.toLocaleString("en-US"));
455
+ const usernameTxt = escapeHtml(input.meta.username || "you");
456
+ return `<!doctype html>
457
+ <html lang="en">
458
+ <head>
459
+ <meta charset="utf-8" />
460
+ <title>OK Claude</title>
461
+ <style>
462
+ ${FONT_CSS}
463
+ :root {
464
+ --paper: #0d0d0a;
465
+ --ink-1: #f4f1ea;
466
+ --ink-2: #8a857c;
467
+ --ink-3: #3a3a35;
468
+ --amber: #d97757;
469
+ --rule: #f4f1ea;
470
+ --s: 1;
471
+ }
472
+ * { box-sizing: border-box; margin: 0; padding: 0; }
473
+ html, body { width: 100%; height: 100%; overflow: hidden; }
474
+ body {
475
+ background: #050505;
476
+ font-family: 'Archivo Narrow', sans-serif;
477
+ color: var(--ink-1);
478
+ }
479
+ .page {
480
+ width: 100vw; height: 100vh;
481
+ display: flex; flex-direction: row;
482
+ align-items: center; justify-content: center;
483
+ gap: 24px; padding: 24px;
484
+ }
485
+ .stage {
486
+ width: calc(1080px * var(--s));
487
+ height: calc(1080px * var(--s));
488
+ flex: 0 0 auto;
489
+ }
490
+ .artifact {
491
+ width: 1080px; height: 1080px;
492
+ transform: scale(var(--s));
493
+ transform-origin: top left;
494
+ background:
495
+ radial-gradient(ellipse at 20% 10%, rgba(255,255,255,0.025), transparent 50%),
496
+ radial-gradient(ellipse at 80% 90%, rgba(255,255,255,0.03), transparent 50%),
497
+ var(--paper);
498
+ color: var(--ink-1);
499
+ display: flex; flex-direction: column;
500
+ padding: 52px 56px 44px;
501
+ position: relative;
502
+ }
503
+
504
+ .hdr-top {
505
+ text-align: left;
506
+ font-family: 'Anton', sans-serif;
507
+ font-size: 64px;
508
+ line-height: 1.0;
509
+ letter-spacing: -0.005em;
510
+ text-transform: uppercase;
511
+ color: var(--ink-1);
512
+ white-space: nowrap;
513
+ }
514
+ .hdr-top .num { color: var(--amber); }
515
+ .hdr-top .ok { text-transform: lowercase; color: var(--ink-2); }
516
+ .hdr-bot {
517
+ margin-top: 14px;
518
+ text-align: center;
519
+ font-family: 'Archivo Narrow', sans-serif;
520
+ font-size: 22px;
521
+ font-weight: 500;
522
+ text-transform: lowercase;
523
+ color: var(--ink-2);
524
+ letter-spacing: 0.01em;
525
+ }
526
+ .hdr-bot .num {
527
+ color: var(--ink-1);
528
+ font-weight: 700;
529
+ border-bottom: 2px solid var(--ink-1);
530
+ padding-bottom: 1px;
531
+ }
532
+ .hdr-bot .handle {
533
+ font-family: 'JetBrains Mono', monospace;
534
+ color: var(--ink-1);
535
+ font-weight: 700;
536
+ text-transform: none;
537
+ letter-spacing: 0;
538
+ }
539
+ .hdr-rule {
540
+ margin-top: 24px;
541
+ height: 4px; background: var(--ink-1);
542
+ position: relative;
543
+ }
544
+ .hdr-rule::after {
545
+ content: ''; position: absolute; top: 7px; left: 0; right: 0;
546
+ height: 1px; background: var(--ink-1);
547
+ }
548
+
549
+ .labels {
550
+ display: grid; grid-template-columns: 1fr 1fr;
551
+ margin-top: 28px;
552
+ font-family: 'Archivo Narrow', sans-serif;
553
+ font-size: 17px;
554
+ font-weight: 500;
555
+ color: var(--ink-2);
556
+ text-transform: lowercase;
557
+ letter-spacing: 0.01em;
558
+ }
559
+ .labels .l { text-align: left; }
560
+ .labels .r { text-align: left; padding-left: 14px; color: var(--ink-1); }
561
+ .labels .n { color: var(--amber); font-weight: 700; }
562
+
563
+ .halves {
564
+ flex: 1 1 auto;
565
+ margin-top: 14px;
566
+ display: grid; grid-template-columns: 1fr 1fr;
567
+ position: relative;
568
+ overflow: hidden;
569
+ }
570
+ .divider {
571
+ position: absolute; left: 50%; top: 0; bottom: 0;
572
+ width: 2px; background: var(--ink-1);
573
+ transform: translateX(-50%);
574
+ }
575
+ .half { position: relative; overflow: hidden; }
576
+ .half.claude { padding-left: 14px; }
577
+ .cv { width: 100%; height: 100%; display: block; }
578
+
579
+ .footer {
580
+ margin-top: 14px;
581
+ display: grid;
582
+ grid-template-columns: 1fr auto 1fr;
583
+ align-items: baseline;
584
+ border-top: 1px solid var(--ink-1);
585
+ padding-top: 12px;
586
+ }
587
+ .footer .cta {
588
+ grid-column: 2;
589
+ justify-self: center;
590
+ font-family: 'JetBrains Mono', monospace;
591
+ font-size: 16px;
592
+ color: var(--ink-1);
593
+ font-weight: 700;
594
+ }
595
+ .footer .cta .chev { color: var(--amber); margin-right: 4px; }
596
+ .footer .cta .cmt { color: var(--ink-2); font-weight: 400; }
597
+ .footer .byline {
598
+ grid-column: 3;
599
+ justify-self: end;
600
+ font-family: 'JetBrains Mono', monospace;
601
+ font-size: 16px;
602
+ color: var(--ink-2);
603
+ font-weight: 400;
604
+ white-space: nowrap;
605
+ }
606
+
607
+ .chrome {
608
+ flex: 0 0 auto;
609
+ display: flex; flex-direction: column;
610
+ align-items: stretch; gap: 14px;
611
+ min-width: 160px;
612
+ }
613
+ .actions { display: flex; flex-direction: column; gap: 12px; }
614
+ .btn {
615
+ font-family: 'JetBrains Mono', monospace;
616
+ font-weight: 700;
617
+ font-size: 16px;
618
+ text-transform: uppercase;
619
+ letter-spacing: 0.04em;
620
+ color: var(--ink-1);
621
+ background: transparent;
622
+ border: 1px solid var(--ink-1);
623
+ padding: 10px 18px;
624
+ cursor: pointer;
625
+ transition: background 120ms ease;
626
+ white-space: nowrap;
627
+ }
628
+ .btn:hover { background: rgba(244, 241, 234, 0.06); }
629
+ .btn .chev { margin-right: 6px; color: var(--ink-1); }
630
+ .btn-primary .chev { color: var(--amber); }
631
+ .toast {
632
+ font-family: 'Archivo Narrow', sans-serif;
633
+ font-size: 14px;
634
+ color: var(--ink-2);
635
+ text-transform: lowercase;
636
+ letter-spacing: 0.04em;
637
+ opacity: 0;
638
+ transition: opacity 200ms ease;
639
+ min-height: 1em;
640
+ text-align: center;
641
+ }
642
+ .toast.visible { opacity: 1; }
643
+
644
+ @media (max-aspect-ratio: 1/1) {
645
+ .page { flex-direction: column; }
646
+ .chrome { min-width: 0; align-items: center; }
647
+ .actions { flex-direction: row; gap: 16px; }
648
+ }
649
+ </style>
650
+ </head>
651
+ <body>
652
+ <div class="page">
653
+ <div class="stage">
654
+ <div id="artifact" class="artifact">
655
+ <div class="hdr-top">
656
+ <span class="ok">ok.</span> CLAUDE:
657
+ <span class="num">${burnedTxt}</span> burned in <span class="num">${daysTxt}</span>.
658
+ </div>
659
+ <div class="hdr-bot"><span class="handle">@${usernameTxt}</span> &middot; avg <span class="num">${perDayTxt}</span>/day.</div>
660
+ <div class="hdr-rule"></div>
661
+
662
+ <div class="labels">
663
+ <div class="l">this is what you dump across <span class="n">${msgCountTxt}</span> messages:</div>
664
+ <div class="r">and this is what claude response:</div>
665
+ </div>
666
+
667
+ <div class="halves">
668
+ <div class="divider"></div>
669
+ <div class="half user"><canvas id="canvas-user" class="cv"></canvas></div>
670
+ <div class="half claude"><canvas id="canvas-claude" class="cv"></canvas></div>
671
+ </div>
672
+
673
+ <div class="footer">
674
+ <div class="cta"><span class="chev">&#9656;</span>npx ok-claude<span class="cmt"> # confess yours</span></div>
675
+ <div class="byline">by rocky hong</div>
676
+ </div>
677
+ </div>
678
+ </div>
679
+
680
+ <div class="chrome">
681
+ <div class="actions">
682
+ <button type="button" id="btn-download" class="btn btn-primary">
683
+ <span class="chev">&#9656;</span>DOWNLOAD
684
+ </button>
685
+ <button type="button" id="btn-copy" class="btn" title="copy image to clipboard">
686
+ <span class="chev">&#9656;</span>COPY
687
+ </button>
688
+ </div>
689
+ <div class="toast" id="toast"></div>
690
+ </div>
691
+ </div>
692
+
693
+ <script>window.__DATA__ = ${dataJson};</script>
694
+ <script>
695
+ ${VENDOR_JS}
696
+ </script>
697
+ <script>
698
+ ${HTML_TO_IMAGE_JS}
699
+ </script>
700
+ <script>
701
+ (function boot() {
702
+ var DATA = window.__DATA__ || { topUser: [], topClaude: [], meta: {} };
703
+ var toastTimer = null;
704
+
705
+ function setupCanvas(canvas) {
706
+ var wrap = canvas.parentElement;
707
+ var dpr = window.devicePixelRatio || 1;
708
+ canvas.width = Math.max(1, Math.round(wrap.clientWidth * dpr));
709
+ canvas.height = Math.max(1, Math.round(wrap.clientHeight * dpr));
710
+ canvas.style.width = wrap.clientWidth + 'px';
711
+ canvas.style.height = wrap.clientHeight + 'px';
712
+ }
713
+
714
+ function logScale(entries, fontMin, fontMax) {
715
+ var max = entries[0][1];
716
+ var min = entries[entries.length - 1][1];
717
+ return function (count) {
718
+ if (max === min) return (fontMin + fontMax) / 2;
719
+ return fontMin + (fontMax - fontMin) * (Math.log(count) - Math.log(min)) / (Math.log(max) - Math.log(min));
720
+ };
721
+ }
722
+
723
+ function drawHalf(canvasId, rawEntries, opts) {
724
+ var canvas = document.getElementById(canvasId);
725
+ if (!canvas) return;
726
+ setupCanvas(canvas);
727
+ if (!rawEntries || rawEntries.length === 0) return;
728
+ var dpr = window.devicePixelRatio || 1;
729
+ var entries = rawEntries.map(function (pair) {
730
+ return [opts.caseFn ? opts.caseFn(pair[0]) : pair[0], pair[1]];
731
+ });
732
+ var cw = canvas.width, ch = canvas.height;
733
+ var innerEdgePx = 24 * dpr;
734
+ var origin = opts.side === 'user'
735
+ ? [cw - innerEdgePx, ch / 2]
736
+ : [innerEdgePx, ch / 2];
737
+ WordCloud(canvas, {
738
+ list: entries,
739
+ fontFamily: opts.fontFamily,
740
+ fontWeight: opts.fontWeight || 'normal',
741
+ color: opts.color,
742
+ backgroundColor: 'rgba(0,0,0,0)',
743
+ gridSize: 6,
744
+ weightFactor: logScale(entries, opts.fontMin * dpr, opts.fontMax * dpr),
745
+ rotateRatio: opts.rotateRatio,
746
+ minRotation: -Math.PI / 9,
747
+ maxRotation: Math.PI / 9,
748
+ rotationSteps: 0,
749
+ shuffle: false,
750
+ shrinkToFit: true,
751
+ drawOutOfBound: false,
752
+ origin: origin,
753
+ });
754
+ }
755
+
756
+ function fitHeadline() {
757
+ var el = document.querySelector('.hdr-top');
758
+ if (!el) return;
759
+ var size = 88;
760
+ el.style.fontSize = size + 'px';
761
+ var guard = 120;
762
+ while (el.scrollWidth > el.clientWidth && size > 24 && guard-- > 0) {
763
+ size -= 1;
764
+ el.style.fontSize = size + 'px';
765
+ }
766
+ }
767
+
768
+ var INTER_STACK = '"Inter", system-ui, -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif';
769
+ function upper(s) { return s.toUpperCase(); }
770
+
771
+ function renderAll() {
772
+ fitHeadline();
773
+ drawHalf('canvas-user', DATA.topUser || [], {
774
+ side: 'user',
775
+ fontFamily: INTER_STACK,
776
+ fontWeight: '800',
777
+ color: '#f4f1ea',
778
+ fontMin: 16, fontMax: 240,
779
+ rotateRatio: 0.35,
780
+ caseFn: upper,
781
+ });
782
+ drawHalf('canvas-claude', DATA.topClaude || [], {
783
+ side: 'claude',
784
+ fontFamily: INTER_STACK,
785
+ fontWeight: '800',
786
+ color: '#d97757',
787
+ fontMin: 16, fontMax: 200,
788
+ rotateRatio: 0,
789
+ caseFn: upper,
790
+ });
791
+ }
792
+
793
+ function fitStage() {
794
+ var chrome = document.querySelector('.chrome');
795
+ if (!chrome) return;
796
+ var landscape = window.innerWidth > window.innerHeight;
797
+ var padding = 24, gap = 24;
798
+ var availW, availH;
799
+ if (landscape) {
800
+ availW = window.innerWidth - padding * 2 - gap - chrome.offsetWidth;
801
+ availH = window.innerHeight - padding * 2;
802
+ } else {
803
+ availW = window.innerWidth - padding * 2;
804
+ availH = window.innerHeight - padding * 2 - gap - chrome.offsetHeight;
805
+ }
806
+ var s = Math.max(0.05, Math.min(availW, availH) / 1080);
807
+ document.documentElement.style.setProperty('--s', String(s));
808
+ }
809
+
810
+ function showToast(msg) {
811
+ var el = document.getElementById('toast');
812
+ if (!el) return;
813
+ el.textContent = msg;
814
+ el.classList.add('visible');
815
+ if (toastTimer) clearTimeout(toastTimer);
816
+ toastTimer = setTimeout(function () {
817
+ el.classList.remove('visible');
818
+ }, 2500);
819
+ }
820
+
821
+ function captureBlob() {
822
+ var node = document.getElementById('artifact');
823
+ return window.htmlToImage.toBlob(node, {
824
+ pixelRatio: 2,
825
+ backgroundColor: '#0d0d0a',
826
+ cacheBust: true,
827
+ style: { transform: 'none' },
828
+ });
829
+ }
830
+
831
+ function downloadPng() {
832
+ captureBlob().then(function (blob) {
833
+ if (!blob) { showToast('download failed'); return; }
834
+ var stamp = (DATA.meta && DATA.meta.timestamp) || 'unstamped';
835
+ var name = 'ok-claude-result-' + stamp + '.png';
836
+ var url = URL.createObjectURL(blob);
837
+ var a = document.createElement('a');
838
+ a.href = url; a.download = name;
839
+ document.body.appendChild(a);
840
+ a.click();
841
+ a.remove();
842
+ setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
843
+ showToast('saved ' + name);
844
+ }).catch(function () { showToast('download failed'); });
845
+ }
846
+
847
+ function copyPng() {
848
+ if (typeof ClipboardItem === 'undefined' || !navigator.clipboard || !navigator.clipboard.write) {
849
+ showToast('copy not supported \u2014 try download instead');
850
+ return;
851
+ }
852
+ captureBlob().then(function (blob) {
853
+ if (!blob) {
854
+ showToast('copy failed');
855
+ return Promise.reject('capture_failed');
856
+ }
857
+ return navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
858
+ }).then(function () {
859
+ showToast('copied to clipboard');
860
+ }).catch(function (reason) {
861
+ if (reason !== 'capture_failed') {
862
+ showToast('copy not supported \u2014 try download instead');
863
+ }
864
+ });
865
+ }
866
+
867
+ function whenFontsReady(cb) {
868
+ if (document.fonts && document.fonts.ready) {
869
+ document.fonts.ready.then(cb);
870
+ } else {
871
+ cb();
872
+ }
873
+ }
874
+
875
+ window.addEventListener('load', function () {
876
+ fitStage();
877
+ whenFontsReady(function () {
878
+ requestAnimationFrame(function () {
879
+ requestAnimationFrame(function () {
880
+ renderAll();
881
+ });
882
+ });
883
+ });
884
+ var dl = document.getElementById('btn-download');
885
+ var cp = document.getElementById('btn-copy');
886
+ if (dl) dl.addEventListener('click', downloadPng);
887
+ if (cp) cp.addEventListener('click', copyPng);
888
+ });
889
+ window.addEventListener('resize', function () {
890
+ clearTimeout(window.__rz);
891
+ window.__rz = setTimeout(fitStage, 60);
892
+ });
893
+ })();
894
+ </script>
895
+ </body>
896
+ </html>
897
+ `;
898
+ }
899
+
900
+ // src/progress.ts
901
+ var BAR_WIDTH = 20;
902
+ var THROTTLE_MS = 50;
903
+ function formatMB(bytes) {
904
+ return (bytes / (1024 * 1024)).toFixed(1);
905
+ }
906
+ function renderBar(ratio) {
907
+ const clamped = Math.max(0, Math.min(1, ratio));
908
+ const filled = Math.round(clamped * BAR_WIDTH);
909
+ return "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
910
+ }
911
+ function createProgress(totalBytes, fileCount) {
912
+ if (!process.stderr.isTTY) {
913
+ return { tick: () => {
914
+ }, done: () => {
915
+ } };
916
+ }
917
+ let lastDraw = 0;
918
+ function draw(bytesDone, fileIdx) {
919
+ const ratio = totalBytes === 0 ? 1 : bytesDone / totalBytes;
920
+ const pct = Math.round(ratio * 100);
921
+ const line = `\r[${renderBar(ratio)}] ${pct}% ${fileIdx} / ${fileCount} files \xB7 ${formatMB(bytesDone)} / ${formatMB(totalBytes)} MB`;
922
+ process.stderr.write(line);
923
+ lastDraw = Date.now();
924
+ }
925
+ return {
926
+ tick(bytesDone, fileIdx) {
927
+ const now = Date.now();
928
+ if (lastDraw !== 0 && now - lastDraw < THROTTLE_MS) return;
929
+ draw(bytesDone, fileIdx);
930
+ },
931
+ done() {
932
+ process.stderr.write("\r\x1B[K\n");
933
+ }
934
+ };
935
+ }
936
+
937
+ // src/pipeline.ts
938
+ var TOP_N = 100;
939
+ function buildStamp(now = /* @__PURE__ */ new Date()) {
940
+ const pad = (n) => String(n).padStart(2, "0");
941
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(
942
+ now.getDate()
943
+ )}-${pad(now.getHours())}${pad(now.getMinutes())}`;
944
+ }
945
+ function outputFilename(stamp) {
946
+ return `ok-claude-result-${stamp}.html`;
947
+ }
948
+ var GENERIC_OS_USERNAMES = /^(user|administrator|admin|default(\s|-)?user|root|nobody)$/i;
949
+ function slugName(raw) {
950
+ return raw.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]/g, "");
951
+ }
952
+ function tryGitName() {
953
+ try {
954
+ const out = execSync("git config --get user.name", {
955
+ stdio: ["ignore", "pipe", "ignore"],
956
+ timeout: 1e3
957
+ }).toString().trim();
958
+ if (!out) return null;
959
+ const slug = slugName(out);
960
+ return slug.length > 0 ? slug : null;
961
+ } catch {
962
+ return null;
963
+ }
964
+ }
965
+ function tryOsName() {
966
+ try {
967
+ const name = userInfo().username;
968
+ if (!name || GENERIC_OS_USERNAMES.test(name)) return null;
969
+ const slug = slugName(name);
970
+ return slug.length > 0 ? slug : null;
971
+ } catch {
972
+ return null;
973
+ }
974
+ }
975
+ function getUsername() {
976
+ return tryGitName() ?? tryOsName() ?? "you";
977
+ }
978
+ function outputDir() {
979
+ const downloads = join2(homedir2(), "Downloads");
980
+ try {
981
+ if (statSync(downloads).isDirectory()) return downloads;
982
+ } catch {
983
+ }
984
+ return process.cwd();
985
+ }
986
+ async function run() {
987
+ const files = await discoverLogs();
988
+ if (files.length === 0) {
989
+ return {
990
+ outPath: null,
991
+ reason: `No Claude Code logs found at ${logsRoot()}`
992
+ };
993
+ }
994
+ const totalBytes = files.reduce((s, f) => s + f.size, 0);
995
+ const progress = createProgress(totalBytes, files.length);
996
+ const userMap = /* @__PURE__ */ new Map();
997
+ const claudeMap = /* @__PURE__ */ new Map();
998
+ const userOpeners = /* @__PURE__ */ new Map();
999
+ const claudeOpeners = /* @__PURE__ */ new Map();
1000
+ let messages = 0;
1001
+ let tokensIn = 0;
1002
+ let tokensOut = 0;
1003
+ let minTs;
1004
+ let maxTs;
1005
+ for await (const e of streamEvents(files, progress.tick)) {
1006
+ const denoised = denoiseMarkdown(e.text);
1007
+ const op = firstOpener(denoised);
1008
+ if (op) {
1009
+ foldOpener(e.role === "user" ? userOpeners : claudeOpeners, op);
1010
+ }
1011
+ const map = e.role === "user" ? userMap : claudeMap;
1012
+ for (const tok of tokenize(denoised)) {
1013
+ map.set(tok, (map.get(tok) ?? 0) + 1);
1014
+ }
1015
+ messages++;
1016
+ if (typeof e.tokensIn === "number") tokensIn += e.tokensIn;
1017
+ if (typeof e.tokensOut === "number") tokensOut += e.tokensOut;
1018
+ if (e.timestamp) {
1019
+ if (minTs === void 0 || e.timestamp < minTs) minTs = e.timestamp;
1020
+ if (maxTs === void 0 || e.timestamp > maxTs) maxTs = e.timestamp;
1021
+ }
1022
+ }
1023
+ progress.done();
1024
+ const topUser = topNOpeners(userOpeners, TOP_N).map(
1025
+ (e) => [e.display, e.count]
1026
+ );
1027
+ const topClaude = topNOpeners(
1028
+ claudeOpeners,
1029
+ TOP_N
1030
+ ).map((e) => [e.display, e.count]);
1031
+ const stamp = buildStamp();
1032
+ const html = renderHtml({
1033
+ topUser,
1034
+ topClaude,
1035
+ meta: {
1036
+ sessions: files.length,
1037
+ messages,
1038
+ tokensIn,
1039
+ tokensOut,
1040
+ dateRange: minTs !== void 0 && maxTs !== void 0 ? [minTs, maxTs] : null,
1041
+ timestamp: stamp,
1042
+ username: getUsername()
1043
+ }
1044
+ });
1045
+ const outPath = resolve(outputDir(), outputFilename(stamp));
1046
+ await writeFile(outPath, html, "utf8");
1047
+ return { outPath };
1048
+ }
1049
+
1050
+ // src/cli.ts
1051
+ async function main() {
1052
+ const result = await run();
1053
+ if (result.outPath) {
1054
+ process.stdout.write(`Wrote ${result.outPath}
1055
+ `);
1056
+ await open(result.outPath);
1057
+ return;
1058
+ }
1059
+ process.stderr.write(result.reason + "\n");
1060
+ }
1061
+ main().catch((err) => {
1062
+ process.stderr.write(`ok-claude: ${err.message}
1063
+ `);
1064
+ process.exit(1);
1065
+ });