@cookielab.io/klovi 0.12.0 → 0.13.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.
|
@@ -61,11 +61,11 @@ code {
|
|
|
61
61
|
--bg-system: #fff8c5;
|
|
62
62
|
--bg-tool: #eaeef2;
|
|
63
63
|
--bg-thinking: #eef6ee;
|
|
64
|
-
--bg-code: #
|
|
64
|
+
--bg-code: #fafafa;
|
|
65
65
|
--text-primary: #1f2328;
|
|
66
66
|
--text-secondary: #59636e;
|
|
67
67
|
--text-muted: #57606a;
|
|
68
|
-
--text-code: #
|
|
68
|
+
--text-code: #383a42;
|
|
69
69
|
--text-inverse: #fff;
|
|
70
70
|
--accent: #1a7f37;
|
|
71
71
|
--accent-hover: #218c3a;
|
|
@@ -100,11 +100,11 @@ code {
|
|
|
100
100
|
--bg-system: #1a1610;
|
|
101
101
|
--bg-tool: #161b22;
|
|
102
102
|
--bg-thinking: #11151a;
|
|
103
|
-
--bg-code: #
|
|
103
|
+
--bg-code: #282c34;
|
|
104
104
|
--text-primary: #c9d1d9;
|
|
105
105
|
--text-secondary: #8b949e;
|
|
106
106
|
--text-muted: #848d97;
|
|
107
|
-
--text-code: #
|
|
107
|
+
--text-code: #abb2bf;
|
|
108
108
|
--text-inverse: #0d1117;
|
|
109
109
|
--accent: #3fb950;
|
|
110
110
|
--accent-hover: #56d364;
|
|
@@ -460,6 +460,142 @@ code {
|
|
|
460
460
|
font-size: .85rem;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
.message-parse-error {
|
|
464
|
+
background: var(--bg-primary);
|
|
465
|
+
border: 1px solid var(--error);
|
|
466
|
+
border-left: 4px solid var(--error);
|
|
467
|
+
font-size: .85rem;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.message-parse-error .message-role {
|
|
471
|
+
color: var(--error);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.parse-error-line {
|
|
475
|
+
color: var(--text-secondary);
|
|
476
|
+
margin-left: 8px;
|
|
477
|
+
font-size: .8em;
|
|
478
|
+
font-weight: normal;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.parse-error-type {
|
|
482
|
+
display: inline-block;
|
|
483
|
+
color: var(--error);
|
|
484
|
+
margin-bottom: 4px;
|
|
485
|
+
font-size: .8em;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.parse-error-details {
|
|
490
|
+
color: var(--text-secondary);
|
|
491
|
+
font-size: .85em;
|
|
492
|
+
font-family: var(--font-mono, monospace);
|
|
493
|
+
margin-bottom: 4px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.parse-error-raw {
|
|
497
|
+
margin-top: 8px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.parse-error-raw summary {
|
|
501
|
+
color: var(--text-muted);
|
|
502
|
+
cursor: pointer;
|
|
503
|
+
user-select: none;
|
|
504
|
+
font-size: .8em;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.parse-error-raw pre {
|
|
508
|
+
background: var(--bg-tertiary);
|
|
509
|
+
border-radius: var(--radius-sm);
|
|
510
|
+
overflow-x: auto;
|
|
511
|
+
white-space: pre-wrap;
|
|
512
|
+
word-break: break-all;
|
|
513
|
+
margin-top: 4px;
|
|
514
|
+
padding: 8px;
|
|
515
|
+
font-size: .8em;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.error-card {
|
|
519
|
+
background: var(--bg-primary);
|
|
520
|
+
border: 1px solid var(--error);
|
|
521
|
+
border-left: 4px solid var(--error);
|
|
522
|
+
border-radius: var(--radius-md);
|
|
523
|
+
margin: 8px 0;
|
|
524
|
+
padding: 12px 16px;
|
|
525
|
+
font-size: .85rem;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.error-card-header {
|
|
529
|
+
display: flex;
|
|
530
|
+
justify-content: space-between;
|
|
531
|
+
align-items: center;
|
|
532
|
+
gap: 8px;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.error-card-title {
|
|
536
|
+
color: var(--error);
|
|
537
|
+
font-weight: 600;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.error-card-details {
|
|
541
|
+
margin-top: 8px;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.error-card-details summary {
|
|
545
|
+
color: var(--text-muted);
|
|
546
|
+
cursor: pointer;
|
|
547
|
+
user-select: none;
|
|
548
|
+
font-size: .8em;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.error-card-details pre {
|
|
552
|
+
background: var(--bg-tertiary);
|
|
553
|
+
border-radius: var(--radius-sm);
|
|
554
|
+
overflow-x: auto;
|
|
555
|
+
white-space: pre-wrap;
|
|
556
|
+
word-break: break-all;
|
|
557
|
+
margin-top: 4px;
|
|
558
|
+
padding: 8px;
|
|
559
|
+
font-size: .8em;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.error-view {
|
|
563
|
+
display: flex;
|
|
564
|
+
text-align: center;
|
|
565
|
+
flex-direction: column;
|
|
566
|
+
justify-content: center;
|
|
567
|
+
align-items: center;
|
|
568
|
+
padding: 40px;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.error-view-title {
|
|
572
|
+
color: var(--error);
|
|
573
|
+
margin-bottom: 8px;
|
|
574
|
+
font-size: 1.1rem;
|
|
575
|
+
font-weight: 600;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.error-view-message {
|
|
579
|
+
color: var(--text-secondary);
|
|
580
|
+
margin-bottom: 16px;
|
|
581
|
+
font-size: .9rem;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.fetch-error {
|
|
585
|
+
display: flex;
|
|
586
|
+
color: var(--text-secondary);
|
|
587
|
+
flex-direction: column;
|
|
588
|
+
justify-content: center;
|
|
589
|
+
align-items: center;
|
|
590
|
+
gap: 12px;
|
|
591
|
+
padding: 40px;
|
|
592
|
+
font-size: .9rem;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.fetch-error-message {
|
|
596
|
+
color: var(--error);
|
|
597
|
+
}
|
|
598
|
+
|
|
463
599
|
.status-notice {
|
|
464
600
|
text-align: center;
|
|
465
601
|
color: var(--text-muted);
|
|
@@ -630,6 +766,32 @@ code {
|
|
|
630
766
|
padding: 12px 16px;
|
|
631
767
|
}
|
|
632
768
|
|
|
769
|
+
.diff-view-wrapper {
|
|
770
|
+
position: relative;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.diff-view-header {
|
|
774
|
+
display: flex;
|
|
775
|
+
background: var(--bg-code);
|
|
776
|
+
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
777
|
+
color: var(--text-muted);
|
|
778
|
+
align-items: center;
|
|
779
|
+
padding: 6px 12px;
|
|
780
|
+
font-family: SF Mono, Fira Code, Menlo, monospace;
|
|
781
|
+
font-size: .75rem;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.diff-view-content {
|
|
785
|
+
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
|
786
|
+
overflow-x: auto;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.diff-view-content pre {
|
|
790
|
+
background: var(--bg-code);
|
|
791
|
+
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
|
|
792
|
+
margin: 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
633
795
|
.collapsible {
|
|
634
796
|
border-radius: var(--radius-sm);
|
|
635
797
|
margin: 4px 0;
|
|
@@ -699,6 +861,13 @@ code {
|
|
|
699
861
|
color: var(--error);
|
|
700
862
|
}
|
|
701
863
|
|
|
864
|
+
.tool-call-truncated {
|
|
865
|
+
color: var(--text-muted);
|
|
866
|
+
padding: 4px 0;
|
|
867
|
+
font-size: .75rem;
|
|
868
|
+
font-style: italic;
|
|
869
|
+
}
|
|
870
|
+
|
|
702
871
|
.tool-call-summary {
|
|
703
872
|
color: var(--text-muted);
|
|
704
873
|
font-family: SF Mono, Fira Code, Menlo, monospace;
|
package/dist/public/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<link rel="apple-touch-icon" href="./apple-touch-icon-st4rb42e.png" />
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./index-zwpjgh70.css"><script type="module" crossorigin src="./index-42y46a3t.js"></script></head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
|
15
15
|
|
package/dist/server.js
CHANGED
|
@@ -4,7 +4,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
|
|
5
5
|
// index.ts
|
|
6
6
|
import { existsSync as existsSync2 } from "node:fs";
|
|
7
|
-
import { dirname, join as
|
|
7
|
+
import { dirname, join as join4 } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/server/cli.ts
|
|
@@ -27,6 +27,9 @@ function setClaudeCodeDir(dir) {
|
|
|
27
27
|
function getProjectsDir() {
|
|
28
28
|
return join(claudeCodeDir, "projects");
|
|
29
29
|
}
|
|
30
|
+
function getStatsCachePath() {
|
|
31
|
+
return join(claudeCodeDir, "stats-cache.json");
|
|
32
|
+
}
|
|
30
33
|
|
|
31
34
|
// src/server/parser/command-message.ts
|
|
32
35
|
function cleanCommandMessage(text) {
|
|
@@ -35,6 +38,9 @@ function cleanCommandMessage(text) {
|
|
|
35
38
|
const argsMatch = text.match(/<command-args>([\s\S]*?)<\/command-args>/);
|
|
36
39
|
if (argsMatch?.[1])
|
|
37
40
|
return argsMatch[1].trim();
|
|
41
|
+
const nameMatch = text.match(/<command-name>([\s\S]*?)<\/command-name>/);
|
|
42
|
+
if (nameMatch?.[1])
|
|
43
|
+
return nameMatch[1].trim();
|
|
38
44
|
return text.replace(/<command-message>[\s\S]*?<\/command-message>/g, "").replace(/<command-name>[\s\S]*?<\/command-name>/g, "").trim();
|
|
39
45
|
}
|
|
40
46
|
function parseCommandMessage(text) {
|
|
@@ -242,10 +248,10 @@ async function handleSearchSessions() {
|
|
|
242
248
|
// src/server/parser/session.ts
|
|
243
249
|
import { join as join3 } from "node:path";
|
|
244
250
|
async function parseSession(sessionId, encodedPath) {
|
|
245
|
-
const rawLines = await readJsonlLines(join3(getProjectsDir(), encodedPath, `${sessionId}.jsonl`));
|
|
251
|
+
const { rawLines, parseErrors } = await readJsonlLines(join3(getProjectsDir(), encodedPath, `${sessionId}.jsonl`));
|
|
246
252
|
const subAgentMap = extractSubAgentMap(rawLines);
|
|
247
253
|
const slug = extractSlug(rawLines);
|
|
248
|
-
const turns = buildTurns(rawLines);
|
|
254
|
+
const turns = buildTurns(rawLines, parseErrors);
|
|
249
255
|
for (const turn of turns) {
|
|
250
256
|
if (turn.kind !== "assistant")
|
|
251
257
|
continue;
|
|
@@ -269,14 +275,14 @@ async function parseSession(sessionId, encodedPath) {
|
|
|
269
275
|
}
|
|
270
276
|
async function parseSubAgentSession(sessionId, encodedPath, agentId) {
|
|
271
277
|
const filePath = join3(getProjectsDir(), encodedPath, sessionId, "subagents", `agent-${agentId}.jsonl`);
|
|
272
|
-
let
|
|
278
|
+
let parsed;
|
|
273
279
|
try {
|
|
274
|
-
|
|
280
|
+
parsed = await readJsonlLines(filePath);
|
|
275
281
|
} catch {
|
|
276
282
|
return { sessionId, project: encodedPath, turns: [] };
|
|
277
283
|
}
|
|
278
|
-
const subAgentMap = extractSubAgentMap(rawLines);
|
|
279
|
-
const turns = buildTurns(rawLines);
|
|
284
|
+
const subAgentMap = extractSubAgentMap(parsed.rawLines);
|
|
285
|
+
const turns = buildTurns(parsed.rawLines, parsed.parseErrors);
|
|
280
286
|
for (const turn of turns) {
|
|
281
287
|
if (turn.kind !== "assistant")
|
|
282
288
|
continue;
|
|
@@ -353,14 +359,29 @@ function findImplSessionId(slug, sessions, currentSessionId) {
|
|
|
353
359
|
async function readJsonlLines(filePath) {
|
|
354
360
|
const { readFile: readFile2 } = await import("node:fs/promises");
|
|
355
361
|
const text = await readFile2(filePath, "utf-8");
|
|
356
|
-
|
|
357
|
-
`)
|
|
362
|
+
const lines = text.split(`
|
|
363
|
+
`);
|
|
364
|
+
const rawLines = [];
|
|
365
|
+
const parseErrors = [];
|
|
366
|
+
for (let i = 0;i < lines.length; i++) {
|
|
367
|
+
const line = lines[i];
|
|
368
|
+
if (!line.trim())
|
|
369
|
+
continue;
|
|
358
370
|
try {
|
|
359
|
-
|
|
360
|
-
} catch {
|
|
361
|
-
|
|
371
|
+
rawLines.push(JSON.parse(line));
|
|
372
|
+
} catch (err) {
|
|
373
|
+
parseErrors.push({
|
|
374
|
+
kind: "parse_error",
|
|
375
|
+
uuid: `parse-error-line-${i + 1}`,
|
|
376
|
+
timestamp: rawLines[rawLines.length - 1]?.timestamp ?? "",
|
|
377
|
+
lineNumber: i + 1,
|
|
378
|
+
rawLine: line.length > 500 ? `${line.slice(0, 500)}… (truncated)` : line,
|
|
379
|
+
errorType: "json_parse",
|
|
380
|
+
errorDetails: err instanceof Error ? err.message : undefined
|
|
381
|
+
});
|
|
362
382
|
}
|
|
363
|
-
}
|
|
383
|
+
}
|
|
384
|
+
return { rawLines, parseErrors };
|
|
364
385
|
}
|
|
365
386
|
function isDisplayableLine(l) {
|
|
366
387
|
if (l.type === "progress")
|
|
@@ -500,9 +521,21 @@ function handleUserLine(line, currentAssistant, turns) {
|
|
|
500
521
|
turns.push(result);
|
|
501
522
|
return flushed;
|
|
502
523
|
}
|
|
503
|
-
function handleAssistantLine(line, currentAssistant, toolResults) {
|
|
504
|
-
if (!line.message
|
|
524
|
+
function handleAssistantLine(line, currentAssistant, toolResults, structureErrors) {
|
|
525
|
+
if (!line.message)
|
|
505
526
|
return currentAssistant;
|
|
527
|
+
if (!Array.isArray(line.message.content)) {
|
|
528
|
+
structureErrors.push({
|
|
529
|
+
kind: "parse_error",
|
|
530
|
+
uuid: `parse-error-${line.uuid || "unknown"}`,
|
|
531
|
+
timestamp: line.timestamp || "",
|
|
532
|
+
lineNumber: 0,
|
|
533
|
+
rawLine: JSON.stringify(line.message.content).slice(0, 500),
|
|
534
|
+
errorType: "invalid_structure",
|
|
535
|
+
errorDetails: `Assistant message content is ${typeof line.message.content}, expected array`
|
|
536
|
+
});
|
|
537
|
+
return currentAssistant;
|
|
538
|
+
}
|
|
506
539
|
const current = currentAssistant ?? createAssistantTurn(line);
|
|
507
540
|
processAssistantLine(line, current, toolResults);
|
|
508
541
|
return current;
|
|
@@ -520,21 +553,26 @@ function handleSystemLine(line, currentAssistant, turns) {
|
|
|
520
553
|
});
|
|
521
554
|
return null;
|
|
522
555
|
}
|
|
523
|
-
function buildTurns(lines) {
|
|
556
|
+
function buildTurns(lines, parseErrors = []) {
|
|
524
557
|
const displayable = lines.filter(isDisplayableLine);
|
|
525
558
|
const toolResults = collectToolResults(displayable);
|
|
526
559
|
const turns = [];
|
|
527
560
|
let currentAssistant = null;
|
|
561
|
+
const structureErrors = [];
|
|
528
562
|
for (const line of displayable) {
|
|
529
563
|
if (line.type === "user") {
|
|
530
564
|
currentAssistant = handleUserLine(line, currentAssistant, turns);
|
|
531
565
|
} else if (line.type === "assistant") {
|
|
532
|
-
currentAssistant = handleAssistantLine(line, currentAssistant, toolResults);
|
|
566
|
+
currentAssistant = handleAssistantLine(line, currentAssistant, toolResults, structureErrors);
|
|
533
567
|
} else if (line.type === "system") {
|
|
534
568
|
currentAssistant = handleSystemLine(line, currentAssistant, turns);
|
|
535
569
|
}
|
|
536
570
|
}
|
|
537
571
|
flushAssistant(currentAssistant, turns);
|
|
572
|
+
const allErrors = [...parseErrors, ...structureErrors];
|
|
573
|
+
if (allErrors.length > 0) {
|
|
574
|
+
turns.push(...allErrors);
|
|
575
|
+
}
|
|
538
576
|
return turns;
|
|
539
577
|
}
|
|
540
578
|
function extractToolResult(tr) {
|
|
@@ -578,11 +616,82 @@ async function handleSessions(encodedPath) {
|
|
|
578
616
|
|
|
579
617
|
// src/server/parser/stats.ts
|
|
580
618
|
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
581
|
-
|
|
619
|
+
async function loadStatsCache() {
|
|
620
|
+
try {
|
|
621
|
+
const text = await readFile2(getStatsCachePath(), "utf-8");
|
|
622
|
+
const data = JSON.parse(text);
|
|
623
|
+
if (data.version !== 2)
|
|
624
|
+
return null;
|
|
625
|
+
return data;
|
|
626
|
+
} catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function countProjects() {
|
|
631
|
+
try {
|
|
632
|
+
const entries = await readdir2(getProjectsDir(), { withFileTypes: true });
|
|
633
|
+
return entries.filter((e) => e.isDirectory()).length;
|
|
634
|
+
} catch {
|
|
635
|
+
return 0;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function todayDateString() {
|
|
639
|
+
const d = new Date;
|
|
640
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
641
|
+
}
|
|
642
|
+
function isWithinLastWeek(dateStr) {
|
|
643
|
+
const now = new Date;
|
|
644
|
+
const weekAgo = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
|
|
645
|
+
const weekAgoStr = `${weekAgo.getFullYear()}-${String(weekAgo.getMonth() + 1).padStart(2, "0")}-${String(weekAgo.getDate()).padStart(2, "0")}`;
|
|
646
|
+
return dateStr >= weekAgoStr;
|
|
647
|
+
}
|
|
648
|
+
function buildFromCache(cache, projects) {
|
|
649
|
+
const today = todayDateString();
|
|
650
|
+
const todayEntry = cache.dailyActivity.find((d) => d.date === today);
|
|
651
|
+
const thisWeekEntries = cache.dailyActivity.filter((d) => isWithinLastWeek(d.date));
|
|
652
|
+
let inputTokens = 0;
|
|
653
|
+
let outputTokens = 0;
|
|
654
|
+
let cacheReadTokens = 0;
|
|
655
|
+
let cacheCreationTokens = 0;
|
|
656
|
+
const models = {};
|
|
657
|
+
for (const [model, usage] of Object.entries(cache.modelUsage)) {
|
|
658
|
+
inputTokens += usage.inputTokens;
|
|
659
|
+
outputTokens += usage.outputTokens;
|
|
660
|
+
cacheReadTokens += usage.cacheReadInputTokens;
|
|
661
|
+
cacheCreationTokens += usage.cacheCreationInputTokens;
|
|
662
|
+
models[model] = {
|
|
663
|
+
inputTokens: usage.inputTokens,
|
|
664
|
+
outputTokens: usage.outputTokens,
|
|
665
|
+
cacheReadTokens: usage.cacheReadInputTokens,
|
|
666
|
+
cacheCreationTokens: usage.cacheCreationInputTokens
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const toolCalls = cache.dailyActivity.reduce((sum, d) => sum + d.toolCallCount, 0);
|
|
670
|
+
return {
|
|
671
|
+
projects,
|
|
672
|
+
sessions: cache.totalSessions,
|
|
673
|
+
messages: cache.totalMessages,
|
|
674
|
+
todaySessions: todayEntry?.sessionCount ?? 0,
|
|
675
|
+
thisWeekSessions: thisWeekEntries.reduce((sum, d) => sum + d.sessionCount, 0),
|
|
676
|
+
inputTokens,
|
|
677
|
+
outputTokens,
|
|
678
|
+
cacheReadTokens,
|
|
679
|
+
cacheCreationTokens,
|
|
680
|
+
toolCalls,
|
|
681
|
+
models
|
|
682
|
+
};
|
|
683
|
+
}
|
|
582
684
|
async function scanStats() {
|
|
583
|
-
const
|
|
584
|
-
|
|
685
|
+
const [cache, projects] = await Promise.all([loadStatsCache(), countProjects()]);
|
|
686
|
+
if (cache) {
|
|
687
|
+
return buildFromCache(cache, projects);
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
projects,
|
|
585
691
|
sessions: 0,
|
|
692
|
+
messages: 0,
|
|
693
|
+
todaySessions: 0,
|
|
694
|
+
thisWeekSessions: 0,
|
|
586
695
|
inputTokens: 0,
|
|
587
696
|
outputTokens: 0,
|
|
588
697
|
cacheReadTokens: 0,
|
|
@@ -590,84 +699,11 @@ async function scanStats() {
|
|
|
590
699
|
toolCalls: 0,
|
|
591
700
|
models: {}
|
|
592
701
|
};
|
|
593
|
-
const projectsDir = getProjectsDir();
|
|
594
|
-
let entries;
|
|
595
|
-
try {
|
|
596
|
-
entries = await readdir2(projectsDir, { withFileTypes: true });
|
|
597
|
-
} catch {
|
|
598
|
-
return stats;
|
|
599
|
-
}
|
|
600
|
-
for (const entry of entries) {
|
|
601
|
-
if (!entry.isDirectory())
|
|
602
|
-
continue;
|
|
603
|
-
const projectDir = join4(projectsDir, entry.name);
|
|
604
|
-
let files;
|
|
605
|
-
try {
|
|
606
|
-
files = (await readdir2(projectDir)).filter((f) => f.endsWith(".jsonl"));
|
|
607
|
-
} catch {
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
if (files.length === 0)
|
|
611
|
-
continue;
|
|
612
|
-
stats.projects++;
|
|
613
|
-
stats.sessions += files.length;
|
|
614
|
-
for (const file of files) {
|
|
615
|
-
await scanFile(join4(projectDir, file), stats);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return stats;
|
|
619
|
-
}
|
|
620
|
-
function processAssistantLine2(msg, stats) {
|
|
621
|
-
const usage = msg.usage;
|
|
622
|
-
if (usage) {
|
|
623
|
-
stats.inputTokens += usage.input_tokens ?? 0;
|
|
624
|
-
stats.outputTokens += usage.output_tokens ?? 0;
|
|
625
|
-
stats.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
626
|
-
stats.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
627
|
-
}
|
|
628
|
-
const model = msg.model;
|
|
629
|
-
if (model) {
|
|
630
|
-
stats.models[model] = (stats.models[model] ?? 0) + 1;
|
|
631
|
-
}
|
|
632
|
-
const content = msg.content;
|
|
633
|
-
if (Array.isArray(content)) {
|
|
634
|
-
for (const block of content) {
|
|
635
|
-
if (block.type === "tool_use") {
|
|
636
|
-
stats.toolCalls++;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
async function scanFile(filePath, stats) {
|
|
642
|
-
let text;
|
|
643
|
-
try {
|
|
644
|
-
text = await readFile2(filePath, "utf-8");
|
|
645
|
-
} catch {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
for (const line of text.split(`
|
|
649
|
-
`)) {
|
|
650
|
-
if (!line.trim())
|
|
651
|
-
continue;
|
|
652
|
-
try {
|
|
653
|
-
const obj = JSON.parse(line);
|
|
654
|
-
if (obj.type === "assistant" && obj.message) {
|
|
655
|
-
processAssistantLine2(obj.message, stats);
|
|
656
|
-
}
|
|
657
|
-
} catch {}
|
|
658
|
-
}
|
|
659
702
|
}
|
|
660
703
|
|
|
661
704
|
// src/server/api/stats.ts
|
|
662
|
-
var CACHE_TTL = 300000;
|
|
663
|
-
var cache = null;
|
|
664
705
|
async function handleStats() {
|
|
665
|
-
const now = Date.now();
|
|
666
|
-
if (cache && now - cache.timestamp < CACHE_TTL) {
|
|
667
|
-
return Response.json({ stats: cache.data });
|
|
668
|
-
}
|
|
669
706
|
const stats = await scanStats();
|
|
670
|
-
cache = { data: stats, timestamp: now };
|
|
671
707
|
return Response.json({ stats });
|
|
672
708
|
}
|
|
673
709
|
|
|
@@ -683,8 +719,8 @@ async function handleSubAgent(sessionId, agentId, encodedPath) {
|
|
|
683
719
|
|
|
684
720
|
// src/server/version.ts
|
|
685
721
|
var appVersion = {
|
|
686
|
-
version: "0.
|
|
687
|
-
commitHash: "
|
|
722
|
+
version: "0.13.0",
|
|
723
|
+
commitHash: "df6e758"
|
|
688
724
|
};
|
|
689
725
|
|
|
690
726
|
// src/server/api/version.ts
|
|
@@ -970,6 +1006,6 @@ if (!acceptRisks) {
|
|
|
970
1006
|
promptSecurityWarning(port);
|
|
971
1007
|
}
|
|
972
1008
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
973
|
-
var staticDir = existsSync2(
|
|
1009
|
+
var staticDir = existsSync2(join4(__dirname2, "public", "index.html")) ? join4(__dirname2, "public") : join4(__dirname2, "dist", "public");
|
|
974
1010
|
startServer(port, createRoutes(), staticDir);
|
|
975
1011
|
printStartupBanner(port);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cookielab.io/klovi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "A local web app for browsing and presenting Claude Code session history",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -50,8 +50,7 @@
|
|
|
50
50
|
"check:fix": "bunx biome check --write ."
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "2.
|
|
54
|
-
"@testing-library/jest-dom": "^6.9.1",
|
|
53
|
+
"@biomejs/biome": "2.4.2",
|
|
55
54
|
"@testing-library/react": "^16.3.2",
|
|
56
55
|
"@types/bun": "^1.3.8",
|
|
57
56
|
"@types/react": "^19.2.13",
|