@cookielab.io/klovi 0.10.5 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/server.js +158 -115
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -31,6 +31,13 @@ bun install -g @cookielab.io/klovi
31
31
  klovi
32
32
  ```
33
33
 
34
+ Or install via [Homebrew](https://brew.sh) (no runtime dependency):
35
+
36
+ ```bash
37
+ brew install cookielab/tap/klovi
38
+ klovi
39
+ ```
40
+
34
41
  Open http://localhost:3583
35
42
 
36
43
  ### Development
package/dist/server.js CHANGED
@@ -3,10 +3,13 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // index.ts
6
- import { existsSync as existsSync2, readSync } from "node:fs";
6
+ import { existsSync as existsSync2 } from "node:fs";
7
7
  import { dirname, join as join4 } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
+ // src/server/cli.ts
11
+ import { readSync } from "node:fs";
12
+
10
13
  // src/server/parser/claude-dir.ts
11
14
  import { readdir, readFile, stat } from "node:fs/promises";
12
15
  import { join as join2 } from "node:path";
@@ -499,8 +502,8 @@ async function handleSubAgent(sessionId, agentId, encodedPath) {
499
502
 
500
503
  // src/server/version.ts
501
504
  var appVersion = {
502
- version: "0.10.5",
503
- commitHash: "e60d690"
505
+ version: "0.11.0",
506
+ commitHash: "45bb1d6"
504
507
  };
505
508
 
506
509
  // src/server/api/version.ts
@@ -508,6 +511,109 @@ function handleVersion() {
508
511
  return Response.json(appVersion);
509
512
  }
510
513
 
514
+ // src/server/cli.ts
515
+ function parseCliArgs(argv) {
516
+ const portIdx = argv.indexOf("--port");
517
+ let port = 3583;
518
+ if (portIdx !== -1) {
519
+ const val = argv[portIdx + 1];
520
+ if (!val || val.startsWith("-")) {
521
+ console.error("Error: --port requires a number argument.");
522
+ process.exit(1);
523
+ }
524
+ port = Number.parseInt(val, 10);
525
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
526
+ console.error("Error: --port must be a valid port number (1-65535).");
527
+ process.exit(1);
528
+ }
529
+ }
530
+ const acceptRisks = argv.includes("--accept-risks");
531
+ const showHelp = argv.includes("--help") || argv.includes("-h");
532
+ const projectsDirIdx = argv.indexOf("--projects-dir");
533
+ if (projectsDirIdx !== -1) {
534
+ const dir = argv[projectsDirIdx + 1];
535
+ if (!dir || dir.startsWith("-")) {
536
+ console.error("Error: --projects-dir requires a path argument.");
537
+ process.exit(1);
538
+ }
539
+ setProjectsDir(dir);
540
+ }
541
+ return { port, acceptRisks, showHelp };
542
+ }
543
+ function showHelpText() {
544
+ console.log(`
545
+ Klovi — a web viewer for Claude Code sessions
546
+
547
+ Usage:
548
+ klovi [options]
549
+
550
+ Options:
551
+ --accept-risks Skip the startup security warning
552
+ --port <number> Server port (default: 3583)
553
+ --projects-dir <path> Override the Claude projects directory
554
+ -h, --help Show this help message
555
+
556
+ The server runs on http://localhost:3583 by default.
557
+ `);
558
+ }
559
+ function promptSecurityWarning(port) {
560
+ const resolvedDir = getProjectsDir();
561
+ const yellow = "\x1B[33m";
562
+ const bold = "\x1B[1m";
563
+ const reset = "\x1B[0m";
564
+ const dim = "\x1B[2m";
565
+ console.log("");
566
+ console.log(`${yellow}${bold} ⚠ WARNING${reset}`);
567
+ console.log("");
568
+ console.log(` Klovi reads Claude Code session history from ${resolvedDir}.`);
569
+ console.log(" Session data may contain sensitive information such as API keys,");
570
+ console.log(" credentials, or private code snippets.");
571
+ console.log("");
572
+ console.log(` The server will expose this data on ${bold}http://localhost:${port}${reset}.`);
573
+ console.log("");
574
+ console.log(` ${dim}To skip this prompt, pass --accept-risks${reset}`);
575
+ console.log("");
576
+ process.stdout.write(" Continue? (y/N) ");
577
+ const buf = Buffer.alloc(1024);
578
+ const bytesRead = readSync(0, buf, 0, 1024, null);
579
+ const answer = buf.toString("utf-8", 0, bytesRead).trim().toLowerCase();
580
+ if (answer !== "y" && answer !== "yes") {
581
+ console.log(" Aborted.");
582
+ process.exit(0);
583
+ }
584
+ console.log("");
585
+ }
586
+ function createRoutes() {
587
+ return [
588
+ { pattern: "/api/version", handler: () => handleVersion() },
589
+ { pattern: "/api/projects", handler: () => handleProjects() },
590
+ {
591
+ pattern: "/api/projects/:encodedPath/sessions",
592
+ handler: (_req, p) => handleSessions(p.encodedPath)
593
+ },
594
+ {
595
+ pattern: "/api/sessions/:sessionId",
596
+ handler: (req, p) => {
597
+ const project = new URL(req.url).searchParams.get("project");
598
+ if (!project) {
599
+ return Response.json({ error: "project query parameter required" }, { status: 400 });
600
+ }
601
+ return handleSession(p.sessionId, project);
602
+ }
603
+ },
604
+ {
605
+ pattern: "/api/sessions/:sessionId/subagents/:agentId",
606
+ handler: (req, p) => {
607
+ const project = new URL(req.url).searchParams.get("project");
608
+ if (!project) {
609
+ return Response.json({ error: "project query parameter required" }, { status: 400 });
610
+ }
611
+ return handleSubAgent(p.sessionId, p.agentId, project);
612
+ }
613
+ }
614
+ ];
615
+ }
616
+
511
617
  // src/server/http.ts
512
618
  import { existsSync } from "node:fs";
513
619
  import { readFile as readFile2 } from "node:fs/promises";
@@ -568,30 +674,53 @@ async function serveStatic(pathname, staticDir) {
568
674
  return null;
569
675
  }
570
676
  }
571
- function startServer(port, routes, staticDir) {
572
- const hasStaticDir = existsSync(staticDir);
677
+ function serveEmbedded(pathname, assets) {
678
+ const key = pathname === "/" ? "index.html" : pathname.slice(1);
679
+ const asset = assets.get(key);
680
+ if (asset) {
681
+ return new Response(asset.data.buffer, {
682
+ headers: { "content-type": asset.contentType }
683
+ });
684
+ }
685
+ if (!extname(pathname)) {
686
+ const index = assets.get("index.html");
687
+ if (index) {
688
+ return new Response(index.data.buffer, {
689
+ headers: { "content-type": "text/html; charset=utf-8" }
690
+ });
691
+ }
692
+ }
693
+ return null;
694
+ }
695
+ async function handleRequest(pathname, url, method, routes, staticDir, hasStaticDir, embeddedAssets) {
696
+ for (const route of routes) {
697
+ const params = matchRoute(route.pattern, pathname);
698
+ if (params !== null) {
699
+ const webReq = new Request(url.toString(), { method });
700
+ return route.handler(webReq, params);
701
+ }
702
+ }
703
+ if (embeddedAssets) {
704
+ const response = serveEmbedded(pathname, embeddedAssets);
705
+ if (response)
706
+ return response;
707
+ } else if (hasStaticDir) {
708
+ const response = await serveStatic(pathname, staticDir);
709
+ if (response)
710
+ return response;
711
+ }
712
+ return new Response("Not Found", {
713
+ status: 404,
714
+ headers: { "content-type": "text/plain" }
715
+ });
716
+ }
717
+ function startServer(port, routes, staticDir, embeddedAssets) {
718
+ const hasStaticDir = !embeddedAssets && existsSync(staticDir);
573
719
  const server = createServer(async (req, res) => {
574
720
  try {
575
721
  const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
576
- const pathname = url.pathname;
577
- for (const route of routes) {
578
- const params = matchRoute(route.pattern, pathname);
579
- if (params !== null) {
580
- const webReq = new Request(url.toString(), { method: req.method });
581
- const response = await route.handler(webReq, params);
582
- await writeResponse(res, response);
583
- return;
584
- }
585
- }
586
- if (hasStaticDir) {
587
- const response = await serveStatic(pathname, staticDir);
588
- if (response) {
589
- await writeResponse(res, response);
590
- return;
591
- }
592
- }
593
- res.writeHead(404, { "content-type": "text/plain" });
594
- res.end("Not Found");
722
+ const response = await handleRequest(url.pathname, url, req.method, routes, staticDir, hasStaticDir, embeddedAssets);
723
+ await writeResponse(res, response);
595
724
  } catch (err) {
596
725
  console.error("Request error:", err);
597
726
  res.writeHead(500, { "content-type": "text/plain" });
@@ -611,47 +740,11 @@ async function writeResponse(res, webResponse) {
611
740
  }
612
741
 
613
742
  // index.ts
614
- var portIdx = process.argv.indexOf("--port");
615
- var port = 3583;
616
- if (portIdx !== -1) {
617
- const val = process.argv[portIdx + 1];
618
- if (!val || val.startsWith("-")) {
619
- console.error("Error: --port requires a number argument.");
620
- process.exit(1);
621
- }
622
- port = Number.parseInt(val, 10);
623
- if (Number.isNaN(port) || port < 1 || port > 65535) {
624
- console.error("Error: --port must be a valid port number (1-65535).");
625
- process.exit(1);
626
- }
627
- }
628
- var acceptRisks = process.argv.includes("--accept-risks");
629
- if (process.argv.includes("--help") || process.argv.includes("-h")) {
630
- console.log(`
631
- Klovi — a web viewer for Claude Code sessions
632
-
633
- Usage:
634
- klovi [options]
635
-
636
- Options:
637
- --accept-risks Skip the startup security warning
638
- --port <number> Server port (default: 3583)
639
- --projects-dir <path> Override the Claude projects directory
640
- -h, --help Show this help message
641
-
642
- The server runs on http://localhost:3583 by default.
643
- `);
743
+ var { port, acceptRisks, showHelp } = parseCliArgs(process.argv);
744
+ if (showHelp) {
745
+ showHelpText();
644
746
  process.exit(0);
645
747
  }
646
- var projectsDirIdx = process.argv.indexOf("--projects-dir");
647
- if (projectsDirIdx !== -1) {
648
- const dir = process.argv[projectsDirIdx + 1];
649
- if (!dir || dir.startsWith("-")) {
650
- console.error("Error: --projects-dir requires a path argument.");
651
- process.exit(1);
652
- }
653
- setProjectsDir(dir);
654
- }
655
748
  var resolvedDir = getProjectsDir();
656
749
  if (!existsSync2(resolvedDir)) {
657
750
  console.error(`Error: projects directory not found: ${resolvedDir}`);
@@ -659,59 +752,9 @@ if (!existsSync2(resolvedDir)) {
659
752
  process.exit(1);
660
753
  }
661
754
  if (!acceptRisks) {
662
- const yellow = "\x1B[33m";
663
- const bold = "\x1B[1m";
664
- const reset = "\x1B[0m";
665
- const dim = "\x1B[2m";
666
- console.log("");
667
- console.log(`${yellow}${bold} ⚠ WARNING${reset}`);
668
- console.log("");
669
- console.log(` Klovi reads Claude Code session history from ${resolvedDir}.`);
670
- console.log(" Session data may contain sensitive information such as API keys,");
671
- console.log(" credentials, or private code snippets.");
672
- console.log("");
673
- console.log(` The server will expose this data on ${bold}http://localhost:${port}${reset}.`);
674
- console.log("");
675
- console.log(` ${dim}To skip this prompt, pass --accept-risks${reset}`);
676
- console.log("");
677
- process.stdout.write(" Continue? (y/N) ");
678
- const buf = Buffer.alloc(1024);
679
- const bytesRead = readSync(0, buf, 0, 1024, null);
680
- const answer = buf.toString("utf-8", 0, bytesRead).trim().toLowerCase();
681
- if (answer !== "y" && answer !== "yes") {
682
- console.log(" Aborted.");
683
- process.exit(0);
684
- }
685
- console.log("");
755
+ promptSecurityWarning(port);
686
756
  }
687
757
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
688
758
  var staticDir = existsSync2(join4(__dirname2, "public", "index.html")) ? join4(__dirname2, "public") : join4(__dirname2, "dist", "public");
689
- startServer(port, [
690
- { pattern: "/api/version", handler: () => handleVersion() },
691
- { pattern: "/api/projects", handler: () => handleProjects() },
692
- {
693
- pattern: "/api/projects/:encodedPath/sessions",
694
- handler: (_req, p) => handleSessions(p.encodedPath)
695
- },
696
- {
697
- pattern: "/api/sessions/:sessionId",
698
- handler: (req, p) => {
699
- const project = new URL(req.url).searchParams.get("project");
700
- if (!project) {
701
- return Response.json({ error: "project query parameter required" }, { status: 400 });
702
- }
703
- return handleSession(p.sessionId, project);
704
- }
705
- },
706
- {
707
- pattern: "/api/sessions/:sessionId/subagents/:agentId",
708
- handler: (req, p) => {
709
- const project = new URL(req.url).searchParams.get("project");
710
- if (!project) {
711
- return Response.json({ error: "project query parameter required" }, { status: 400 });
712
- }
713
- return handleSubAgent(p.sessionId, p.agentId, project);
714
- }
715
- }
716
- ], staticDir);
759
+ startServer(port, createRoutes(), staticDir);
717
760
  console.log(`Klovi running at http://localhost:${port}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cookielab.io/klovi",
3
- "version": "0.10.5",
3
+ "version": "0.11.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",
@@ -41,6 +41,7 @@
41
41
  "build": "bun run build:frontend && bun run build:server",
42
42
  "build:frontend": "rm -rf dist/public && bun build ./index.html --outdir dist/public",
43
43
  "build:server": "bun scripts/build-server.ts",
44
+ "build:compile": "bun scripts/build-compile.ts",
44
45
  "test": "bun test",
45
46
  "typecheck": "tsc --noEmit",
46
47
  "lint": "bunx biome lint .",