ccviz 0.2.0 → 0.3.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dhruv Yadav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # ccviz
2
+
3
+ CLI tool to visualize [Claude Code](https://docs.anthropic.com/en/docs/claude-code) conversations. Opens a browser UI to explore conversation transcripts stored in `~/.claude/`, with focus on debugging MCP tool calls, identifying context bloat, and understanding token/timing patterns.
4
+
5
+ ![npm](https://img.shields.io/npm/v/ccviz)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i -g ccviz
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ # Start the browser UI
17
+ ccviz
18
+
19
+ # Custom port
20
+ ccviz --port 8080
21
+
22
+ # Don't auto-open browser
23
+ ccviz --no-open
24
+
25
+ # Dump a conversation as JSON (no server)
26
+ ccviz --json ~/.claude/projects/-Users-you-code-myproject/session-id.jsonl
27
+
28
+ # Open directly to a specific conversation
29
+ ccviz --conversation -Users-you-code-myproject/session-id
30
+ ```
31
+
32
+ ## Features
33
+
34
+ ### Project Browser
35
+ - Hierarchical folder tree of all Claude Code projects in `~/.claude/projects/`
36
+ - Most recent conversations shown on the landing page
37
+ - Search/filter conversations by title, model, or session ID
38
+
39
+ ### Conversation View
40
+
41
+ Five tabs for analyzing a conversation:
42
+
43
+ **Timeline** — Turn-by-turn view with expandable user/assistant messages. Each turn shows a context contribution bar (green→red heat scale) and expandable tool call cards with full input parameters and result previews.
44
+
45
+ **Tokens** — Stacked area chart of per-turn token usage (input, output, cache creation, cache read) and cumulative context growth line chart. Interactive tooltips and per-turn breakdown table.
46
+
47
+ **Tools** — Sortable table of all tool calls with duration, result size, and MCP server info. Aggregation bar charts for call count and result size by tool. Filter by tool name or MCP/native. Click any row to see full input/output.
48
+
49
+ **Context** — Per-tool stacked bar chart showing exactly which tools cause context spikes at each turn. Click any bar segment to inspect the tool calls. Pie chart of context consumption by tool category — click a slice to drill down and see every call with input params and results. Optimization suggestions flag large results, repeated calls, and oversized reads.
50
+
51
+ **Subagents** — List of spawned subagents with token usage comparison bars. Click to expand and see the subagent's own timeline.
52
+
53
+ ## Data Source
54
+
55
+ Reads conversation transcripts from `~/.claude/projects/`. Each project directory contains `.jsonl` files (one per conversation) with the full message history, tool calls, token usage, and subagent transcripts.
56
+
57
+ ## Tech Stack
58
+
59
+ - **CLI**: Node.js, Commander, `open`
60
+ - **Server**: Express 5
61
+ - **Parser**: Streaming JSONL parser (handles 25MB+ conversations)
62
+ - **UI**: React 19, Vite, Tailwind CSS v4, [visx](https://airbnb.io/visx/) for visualizations
63
+ - **Build**: tsup (server), Vite (UI)
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ git clone https://github.com/dhruvyad/ccviz.git
69
+ cd ccviz
70
+ npm install
71
+ npm run dev # Starts Express + Vite dev server with HMR
72
+ npm run build # Production build
73
+ npm start # Run production build
74
+ ```
75
+
76
+ ## License
77
+
78
+ [MIT](LICENSE)
@@ -88,6 +88,63 @@ import fs2 from "fs/promises";
88
88
  import path2 from "path";
89
89
  import { createReadStream } from "fs";
90
90
  import readline from "readline";
91
+ function createRecentRouter(claudeDir) {
92
+ const router = Router2();
93
+ router.get("/", async (req, res) => {
94
+ try {
95
+ const limit = Math.min(parseInt(req.query.limit) || 20, 50);
96
+ const projectsDir = path2.join(claudeDir, "projects");
97
+ const projectFolders = await fs2.readdir(projectsDir, {
98
+ withFileTypes: true
99
+ });
100
+ const all = [];
101
+ for (const folder of projectFolders) {
102
+ if (!folder.isDirectory()) continue;
103
+ const projectDir = path2.join(projectsDir, folder.name);
104
+ let files;
105
+ try {
106
+ files = await fs2.readdir(projectDir);
107
+ } catch {
108
+ continue;
109
+ }
110
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
111
+ for (const file of jsonlFiles) {
112
+ const filePath = path2.join(projectDir, file);
113
+ try {
114
+ const stat = await fs2.stat(filePath);
115
+ all.push({
116
+ sessionId: file.replace(".jsonl", ""),
117
+ projectEncoded: folder.name,
118
+ filePath,
119
+ sizeBytes: stat.size,
120
+ lastModified: stat.mtime.toISOString(),
121
+ mtimeMs: stat.mtimeMs
122
+ });
123
+ } catch {
124
+ }
125
+ }
126
+ }
127
+ all.sort((a, b) => b.mtimeMs - a.mtimeMs);
128
+ const top = all.slice(0, limit);
129
+ const results = await Promise.all(
130
+ top.map(async (entry) => {
131
+ const summary = await quickScan(entry.filePath);
132
+ return {
133
+ sessionId: entry.sessionId,
134
+ projectEncoded: entry.projectEncoded,
135
+ sizeBytes: entry.sizeBytes,
136
+ lastModified: entry.lastModified,
137
+ ...summary
138
+ };
139
+ })
140
+ );
141
+ res.json(results);
142
+ } catch {
143
+ res.status(500).json({ error: "Failed to list recent conversations" });
144
+ }
145
+ });
146
+ return router;
147
+ }
91
148
  function createConversationsRouter(claudeDir) {
92
149
  const router = Router2();
93
150
  router.get("/:encodedPath/conversations", async (req, res) => {
@@ -528,10 +585,12 @@ function createServer({ claudeDir, isDev }) {
528
585
  app.use("/api/projects", createProjectsRouter(claudeDir));
529
586
  app.use("/api/projects", createConversationsRouter(claudeDir));
530
587
  app.use("/api/conversations", createConversationRouter(claudeDir));
588
+ app.use("/api/recent", createRecentRouter(claudeDir));
531
589
  if (!isDev) {
532
590
  const uiDir = path6.join(__dirname, "ui");
533
591
  app.use(express.static(uiDir));
534
- app.get("/{*splat}", (_req, res) => {
592
+ app.get("/{*splat}", (req, res, next) => {
593
+ if (req.path.startsWith("/api/")) return next();
535
594
  res.sendFile(path6.join(uiDir, "index.html"));
536
595
  });
537
596
  }
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createServer,
4
4
  parseConversation
5
- } from "./chunk-AQM3FQFJ.js";
5
+ } from "./chunk-37WFE64V.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createServer
3
- } from "../chunk-AQM3FQFJ.js";
3
+ } from "../chunk-37WFE64V.js";
4
4
  export {
5
5
  createServer
6
6
  };