graphtrack 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,70 +1,105 @@
1
+ <div align="center">
2
+
1
3
  # Graphtrack
2
4
 
3
- Visualize your **Claude Code** token usage as an interactive graph.
5
+ [![npm version](https://img.shields.io/npm/v/graphtrack.svg?style=flat-square)](https://www.npmjs.com/package/graphtrack)
6
+ [![license](https://img.shields.io/npm/l/graphtrack?style=flat-square)](https://github.com/nicholasgriffintn/graphtrack/blob/main/LICENSE)
4
7
 
5
- Instead of tables and bars, Graphtrack turns a session into a navigable network —
6
- prompts, tool calls, and their token weight rendered as nodes and edges. Node size
7
- is token share, color is tool type, and edges trace how your tokens actually flowed.
8
- Find out at a glance which tool is eating your budget, or trace a single request
9
- from prompt to result.
8
+ **Interactive token-usage dashboard for Claude Code.**
10
9
 
11
- ```
12
- ~/.claude/projects/<project>/<session>.jsonl → parse → graph → serve on localhost
13
- ```
10
+ Run `npx graphtrack`, open `localhost:9090`, and see exactly where your tokens go —
11
+ as a graph, a table, or a multi-session overview.
12
+
13
+ Your logs never leave your machine.
14
+
15
+ [Installation](#installation) · [Quick Start](#quick-start) · [Features](#features) · [CLI Options](#cli-options) · [Development](#development) · [License](#license)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # No install needed — runs directly from npm
25
+ npx graphtrack
14
26
 
15
- ## Why local?
27
+ # Or install globally
28
+ npm install -g graphtrack
29
+ graphtrack
30
+ ```
16
31
 
17
- This is a **local-first, open-source tool**. Your logs are parsed on your machine and
18
- served to your own browser at `localhost`. Nothing is uploaded, stored, or shared.
19
- Your usage data never leaves your computer.
32
+ Requires [Node.js](https://nodejs.org/) 18+.
20
33
 
21
- ## Getting started
34
+ ## Quick Start
22
35
 
23
36
  ```bash
24
- # from the repo root
25
- npm install
26
- npm run build
27
- npm start # starts a localhost server
37
+ npx graphtrack
38
+ # → Server starts at http://localhost:9090
39
+ # Picks up sessions from ~/.claude/projects automatically
40
+ # Pick a session from the dropdown and explore
28
41
  ```
29
42
 
30
- Then open the printed URL, pick a session (or point Graphtrack at
31
- `~/.claude/projects`), and explore the graph.
43
+ Point at a custom log directory:
32
44
 
33
- ### Preview (no server needed)
45
+ ```bash
46
+ npx graphtrack --dir /path/to/.claude/projects
47
+ ```
34
48
 
35
- A standalone page is generated from a synthetic sample session so you can judge
36
- the visualization without any setup:
49
+ Use a different port:
37
50
 
38
51
  ```bash
39
- npm run build
40
- node scripts/build-spike.mjs
41
- # open public/index.html in a browser
52
+ npx graphtrack --port 3000
42
53
  ```
43
54
 
44
- ## Roadmap
55
+ ## Features
56
+
57
+ ### Four views, one dashboard
58
+
59
+ | View | What it shows |
60
+ | ----------------------- | ------------------------------------------------------------------------------------------- |
61
+ | **Overview** | Aggregate stats across all sessions — total tokens, per-project breakdown, tool usage share |
62
+ | **Aggregate Graph** | Hub-and-spoke network: each tool is a node, size = token share, color = tool type |
63
+ | **Chronological Graph** | Turn-by-turn chain: each prompt links to the tools it triggered, ordered in time |
64
+ | **Table** | Sortable per-tool and per-turn tables with color-coded token bars |
45
65
 
46
- - **Phase 0** — validate the graph metaphor on realistic data ✓ (spike shipped)
47
- - **Phase 1** — JSONL parser + typed model + per-tool aggregation
48
- - **Phase 2** — localhost web app: vis.js graph + table view
49
- - **Phase 3** — multi-session overview, empty state, error handling
50
- - **Phase 4** — install polish, docs, CI
66
+ ### What gets tracked
51
67
 
52
- ## Project layout
68
+ - **Input / output / cache tokens** per session, per turn, and per tool call
69
+ - **Real timestamps** — session start time and wall-clock duration from the actual logs
70
+ - **Tool breakdown** — which tools consume the most tokens, how many times each was called
71
+ - **Project grouping** — sessions organized by project in the Overview view
72
+
73
+ ### Tool color legend
74
+
75
+ | Tool | Color |
76
+ | ----- | ------------------------------------------- |
77
+ | Edit | <span style="color:#ff6b6b">●</span> Red |
78
+ | Bash | <span style="color:#faff69">●</span> Yellow |
79
+ | Read | <span style="color:#3b82f6">●</span> Blue |
80
+ | Write | <span style="color:#4cff4c">●</span> Green |
81
+ | Glob | <span style="color:#b06bff">●</span> Purple |
82
+ | Grep | <span style="color:#ff8cff">●</span> Pink |
83
+ | Agent | <span style="color:#ffb347">●</span> Orange |
84
+
85
+ ## Project Structure
53
86
 
54
87
  ```
55
88
  src/
56
- types.ts shared data model (Session/Turn/ToolCall/Usage)
57
- parser.ts JSONL → typed Session
58
- graph.ts Session → vis.js nodes/edges (aggregate + chronological)
59
- scripts/
60
- build-spike.mjs regenerates public/graph-data.js from a sample session
89
+ types.ts Data model (Session, Turn, ToolCall, Usage, Overview)
90
+ parser.ts JSONL → Session + aggregateSessions()
91
+ graph.ts Session → vis.js nodes/edges (aggregate + chronological)
92
+ server.ts Express API + static file serving
93
+ cli.ts CLI entry point (--dir, --port, --help)
94
+
61
95
  public/
62
- index.html the app page (hand-authored)
63
- graph-data.js generated graph data
96
+ index.html Dashboard UI (4 views, vanilla JS)
97
+
64
98
  test/
65
- fixtures/ sample-session generator
99
+ parser.test.ts Unit tests for parser, aggregation, and graph builders
100
+ fixtures/ Sample session data for tests
66
101
  ```
67
102
 
68
103
  ## License
69
104
 
70
- MIT
105
+ MIT
package/dist/graph.d.ts CHANGED
@@ -37,6 +37,15 @@ export interface VisEdge {
37
37
  arrows?: string;
38
38
  }
39
39
  export declare function toolColor(name: string): string;
40
+ /** Hub-and-tool aggregate graph: one root connected to tool nodes sized by token share. */
41
+ export declare function buildToolHubGraph(tools: {
42
+ name: string;
43
+ calls: number;
44
+ totalTokens: number;
45
+ }[], rootLabel: string, rootTitle?: string): {
46
+ nodes: VisNode[];
47
+ edges: VisEdge[];
48
+ };
40
49
  /** Hub-and-tool aggregate graph for one session. */
41
50
  export declare function buildAggregateGraph(session: Session): {
42
51
  nodes: VisNode[];
package/dist/graph.js CHANGED
@@ -33,43 +33,47 @@ export function toolColor(name) {
33
33
  function scale(value) {
34
34
  return Math.sqrt(value) / 10;
35
35
  }
36
- /** Hub-and-tool aggregate graph for one session. */
37
- export function buildAggregateGraph(session) {
36
+ /** Hub-and-tool aggregate graph: one root connected to tool nodes sized by token share. */
37
+ export function buildToolHubGraph(tools, rootLabel, rootTitle = "This session") {
38
38
  const nodes = [];
39
39
  const edges = [];
40
- const rootId = "session";
41
40
  nodes.push({
42
- id: rootId,
43
- label: `${session.project}\n${session.turns.length} turns · ${fmt(session.totalTokens)} tok`,
41
+ id: "session",
42
+ label: rootLabel,
44
43
  value: 6,
45
44
  color: { background: "#faff69", border: "#e6eb52" },
46
45
  group: "session",
47
- title: "This session",
46
+ title: rootTitle,
48
47
  });
48
+ const sorted = [...tools].sort((a, b) => b.totalTokens - a.totalTokens);
49
+ const maxTokens = Math.max(1, ...[...tools].map((v) => v.totalTokens));
50
+ for (const t of sorted) {
51
+ const id = `tool:${t.name}`;
52
+ nodes.push({
53
+ id,
54
+ label: `${t.name}\n${t.calls}× · ${fmt(t.totalTokens)}`,
55
+ value: 1 + 5 * (t.totalTokens / maxTokens),
56
+ color: { background: toolColor(t.name), border: "#ffffff" },
57
+ group: "tool",
58
+ title: `${t.name}: ${t.calls} calls, ${fmt(t.totalTokens)} tokens`,
59
+ });
60
+ edges.push({ from: "session", to: id, value: 2 + 4 * (t.totalTokens / maxTokens), title: `${t.name}: ${fmt(t.totalTokens)}` });
61
+ }
62
+ return { nodes, edges };
63
+ }
64
+ /** Hub-and-tool aggregate graph for one session. */
65
+ export function buildAggregateGraph(session) {
49
66
  // Group tool calls across the whole session (reuse aggregation).
50
67
  const byTool = new Map();
51
68
  for (const turn of session.turns) {
52
69
  for (const call of turn.toolCalls) {
53
- const entry = byTool.get(call.name) ?? { calls: 0, tokens: 0 };
70
+ const entry = byTool.get(call.name) ?? { name: call.name, calls: 0, totalTokens: 0 };
54
71
  entry.calls += 1;
55
- entry.tokens += call.totalTokens || 1;
72
+ entry.totalTokens += call.totalTokens || 1;
56
73
  byTool.set(call.name, entry);
57
74
  }
58
75
  }
59
- const maxTokens = Math.max(1, ...[...byTool.values()].map((v) => v.tokens));
60
- for (const [name, agg] of byTool) {
61
- const id = `tool:${name}`;
62
- nodes.push({
63
- id,
64
- label: `${name}\n${agg.calls}× · ${fmt(agg.tokens)}`,
65
- value: 1 + 5 * (agg.tokens / maxTokens),
66
- color: { background: toolColor(name), border: "#ffffff" },
67
- group: "tool",
68
- title: `${name}: ${agg.calls} calls, ${fmt(agg.tokens)} tokens`,
69
- });
70
- edges.push({ from: rootId, to: id, value: 2 + 4 * (agg.tokens / maxTokens), title: `${name}: ${fmt(agg.tokens)}` });
71
- }
72
- return { nodes, edges };
76
+ return buildToolHubGraph([...byTool.values()], `${session.project}\n${session.turns.length} turns · ${fmt(session.totalTokens)} tok`, "This session");
73
77
  }
74
78
  /** Turn-by-turn chronological graph: session → prompts → their tool calls. */
75
79
  export function buildChronologicalGraph(session) {
package/dist/graph.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}
package/dist/parser.d.ts CHANGED
@@ -1,5 +1,11 @@
1
- import type { Session, ToolAggregate } from "./types.js";
1
+ import type { Overview, Session, ToolAggregate } from "./types.js";
2
2
  /** Extracts tool calls from a session's JSONL and groups them into turns. */
3
3
  export declare function parseSession(filePath: string, project: string): Session;
4
4
  /** Aggregate tool calls across a session (the collapse-by-tool model). */
5
5
  export declare function aggregateTools(session: Session): ToolAggregate[];
6
+ /**
7
+ * Collapse many parsed sessions into a single overview: per-project rows,
8
+ * global totals, and tool aggregates across everything. Callers must run
9
+ * spreadTokens() on each session first so per-tool attribution is seeded.
10
+ */
11
+ export declare function aggregateSessions(sessions: Session[]): Overview;
package/dist/parser.js CHANGED
@@ -5,7 +5,15 @@
5
5
  * unreadable file (callers decide how to surface partial failures).
6
6
  */
7
7
  import { readFileSync } from "node:fs";
8
- /** Parse one line of JSONL into a drop of structure we care about. */
8
+ /** Normalize a message's content field to an array for the rest of the parser. */
9
+ function normalizeContent(content) {
10
+ if (!content)
11
+ return [];
12
+ if (typeof content === "string")
13
+ return [{ type: "text", text: content }];
14
+ return Array.isArray(content) ? content : [content];
15
+ }
16
+ /** Parse one line of JSONL into the message we care about + its timestamp. */
9
17
  function parseLine(raw) {
10
18
  const line = raw.trim();
11
19
  if (!line)
@@ -17,19 +25,13 @@ function parseLine(raw) {
17
25
  catch {
18
26
  return null; // malformed line — skip, don't crash
19
27
  }
20
- return obj?.message ?? null;
28
+ if (!obj?.message)
29
+ return null;
30
+ return { msg: obj.message, timestamp: obj.timestamp };
21
31
  }
22
- function contentSummary(content) {
23
- if (!content)
24
- return "";
25
- return content
26
- .map((b) => {
27
- if (b.type === "text")
28
- return b.text ?? "";
29
- if (b.type === "tool_use")
30
- return "";
31
- return "";
32
- })
32
+ function contentSummary(blocks) {
33
+ return blocks
34
+ .map((b) => (b.type === "text" && typeof b.text === "string" ? b.text : ""))
33
35
  .filter(Boolean)
34
36
  .join(" ");
35
37
  }
@@ -50,6 +52,7 @@ export function parseSession(filePath, project) {
50
52
  project,
51
53
  model: "unknown",
52
54
  startedAt: "",
55
+ durationMs: null,
53
56
  turns: [],
54
57
  totalTokens: 0,
55
58
  totalInput: 0,
@@ -57,17 +60,33 @@ export function parseSession(filePath, project) {
57
60
  totalCacheRead: 0,
58
61
  totalCacheWrite: 0,
59
62
  };
63
+ // Track the earliest/latest timestamp across all lines to get real timings.
64
+ let startTs = null;
65
+ let lastTs = null;
66
+ const noteTime = (ts) => {
67
+ if (!ts)
68
+ return;
69
+ const n = Date.parse(ts);
70
+ if (Number.isNaN(n))
71
+ return;
72
+ if (startTs === null || n < startTs)
73
+ startTs = n;
74
+ if (lastTs === null || n > lastTs)
75
+ lastTs = n;
76
+ };
60
77
  // We walk the stream and slice it into turns. A turn starts at a user message
61
78
  // that contains a text prompt (i.e. a real request, not a tool_result).
62
79
  let current = null;
63
80
  for (const line of lines) {
64
- const msg = parseLine(line);
65
- if (!msg)
81
+ const parsed = parseLine(line);
82
+ if (!parsed)
66
83
  continue;
67
- const text = contentSummary(msg.content);
84
+ const { msg, timestamp } = parsed;
85
+ noteTime(timestamp);
86
+ const content = normalizeContent(msg.content);
87
+ const text = contentSummary(content);
68
88
  // Determine message kind
69
89
  const hasText = Boolean(text.trim());
70
- const content = msg.content ?? [];
71
90
  // A new turn begins when the user speaks with text (not a tool_result).
72
91
  if (msg.role === "user" && hasText && content.every((b) => b.type !== "tool_result")) {
73
92
  if (current)
@@ -108,9 +127,11 @@ export function parseSession(filePath, project) {
108
127
  }
109
128
  if (current)
110
129
  session.turns.push(current);
111
- session.startedAt = session.turns[0] ? String(session.turns[0]?.prompt.length) : "";
112
- // Derive a pseudo start time from the file's first activity is omitted here;
113
- // the fixture/deployer can supply timestamps via a richer parser later.
130
+ if (startTs !== null) {
131
+ session.startedAt = new Date(startTs).toISOString();
132
+ if (lastTs !== null)
133
+ session.durationMs = lastTs - startTs;
134
+ }
114
135
  session.totalTokens = session.turns.reduce((a, t) => a + t.totalTokens, 0);
115
136
  return session;
116
137
  }
@@ -142,4 +163,67 @@ export function aggregateTools(session) {
142
163
  }
143
164
  return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens);
144
165
  }
166
+ /**
167
+ * Collapse many parsed sessions into a single overview: per-project rows,
168
+ * global totals, and tool aggregates across everything. Callers must run
169
+ * spreadTokens() on each session first so per-tool attribution is seeded.
170
+ */
171
+ export function aggregateSessions(sessions) {
172
+ const projectMap = new Map();
173
+ const toolMap = new Map();
174
+ const totals = {
175
+ sessions: sessions.length,
176
+ totalTokens: 0,
177
+ totalInput: 0,
178
+ totalOutput: 0,
179
+ totalCacheRead: 0,
180
+ totalCacheWrite: 0,
181
+ };
182
+ for (const s of sessions) {
183
+ totals.totalTokens += s.totalTokens;
184
+ totals.totalInput += s.totalInput;
185
+ totals.totalOutput += s.totalOutput;
186
+ totals.totalCacheRead += s.totalCacheRead;
187
+ totals.totalCacheWrite += s.totalCacheWrite;
188
+ const p = projectMap.get(s.project) ?? {
189
+ project: s.project,
190
+ sessions: 0,
191
+ totalTokens: 0,
192
+ totalInput: 0,
193
+ totalOutput: 0,
194
+ totalCacheRead: 0,
195
+ totalCacheWrite: 0,
196
+ };
197
+ p.sessions += 1;
198
+ p.totalTokens += s.totalTokens;
199
+ p.totalInput += s.totalInput;
200
+ p.totalOutput += s.totalOutput;
201
+ p.totalCacheRead += s.totalCacheRead;
202
+ p.totalCacheWrite += s.totalCacheWrite;
203
+ projectMap.set(s.project, p);
204
+ for (const agg of aggregateTools(s)) {
205
+ const t = toolMap.get(agg.name) ?? {
206
+ name: agg.name,
207
+ calls: 0,
208
+ totalTokens: 0,
209
+ inputTokens: 0,
210
+ outputTokens: 0,
211
+ cacheRead: 0,
212
+ cacheWrite: 0,
213
+ };
214
+ t.calls += agg.calls;
215
+ t.totalTokens += agg.totalTokens;
216
+ t.inputTokens += agg.inputTokens;
217
+ t.outputTokens += agg.outputTokens;
218
+ t.cacheRead += agg.cacheRead;
219
+ t.cacheWrite += agg.cacheWrite;
220
+ toolMap.set(agg.name, t);
221
+ }
222
+ }
223
+ return {
224
+ projects: [...projectMap.values()].sort((a, b) => b.totalTokens - a.totalTokens),
225
+ totals,
226
+ toolAggregates: [...toolMap.values()].sort((a, b) => b.totalTokens - a.totalTokens),
227
+ };
228
+ }
145
229
  //# sourceMappingURL=parser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -14,8 +14,8 @@ import { readdirSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join, relative, dirname, basename } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
- import { parseSession, aggregateTools } from "./parser.js";
18
- import { buildAggregateGraph, buildChronologicalGraph, toolColor, spreadTokens } from "./graph.js";
17
+ import { parseSession, aggregateTools, aggregateSessions } from "./parser.js";
18
+ import { buildAggregateGraph, buildChronologicalGraph, buildToolHubGraph, toolColor, spreadTokens } from "./graph.js";
19
19
  export const DEFAULT_LOGS_DIR = join(homedir(), ".claude", "projects");
20
20
  const DEFAULT_PORT = 9090;
21
21
  /**
@@ -87,6 +87,8 @@ export function startServer(opts = {}) {
87
87
  id: session.id,
88
88
  project: session.project,
89
89
  model: session.model,
90
+ startedAt: session.startedAt,
91
+ durationMs: session.durationMs,
90
92
  turns: session.turns.length,
91
93
  toolCalls: session.turns.reduce((a, t) => a + t.toolCalls.length, 0),
92
94
  totalTokens: session.totalTokens,
@@ -95,11 +97,46 @@ export function startServer(opts = {}) {
95
97
  totalCacheRead: session.totalCacheRead,
96
98
  totalCacheWrite: session.totalCacheWrite,
97
99
  },
100
+ turns: session.turns.map((t) => ({
101
+ index: t.index,
102
+ prompt: t.prompt,
103
+ toolCalls: t.toolCalls.length,
104
+ totalTokens: t.totalTokens,
105
+ })),
98
106
  aggregate: buildAggregateGraph(session),
99
107
  chronological: buildChronologicalGraph(session),
100
108
  toolAggregates: aggregates,
101
109
  });
102
110
  });
111
+ // Multi-session overview: per-project totals + tool share across everything.
112
+ // Parses every session file; fine for a local tool, cached later if ever slow.
113
+ app.get("/api/overview", (_req, res) => {
114
+ const files = findSessionFiles(logsDir);
115
+ const sessions = [];
116
+ for (const file of files) {
117
+ try {
118
+ const s = parseSession(file, projectOf(logsDir, file));
119
+ spreadTokens(s);
120
+ sessions.push(s);
121
+ }
122
+ catch {
123
+ // Skip unparseable files — a broken session shouldn't sink the overview.
124
+ }
125
+ }
126
+ const overview = aggregateSessions(sessions);
127
+ res.json({
128
+ totals: overview.totals,
129
+ projects: overview.projects,
130
+ toolAggregates: overview.toolAggregates.map((a) => ({
131
+ name: a.name,
132
+ calls: a.calls,
133
+ totalTokens: a.totalTokens,
134
+ color: toolColor(a.name),
135
+ })),
136
+ graph: buildToolHubGraph(overview.toolAggregates.map((a) => ({ name: a.name, calls: a.calls, totalTokens: a.totalTokens })), `All sessions\n${overview.totals.sessions} sessions · ${overview.totals.totalTokens} tok`, "Every Claude Code session Graphtrack can see"),
137
+ logsDir,
138
+ });
139
+ });
103
140
  // Static app (resolved relative to the installed package, not the cwd).
104
141
  app.use(express.static(PUBLIC_DIR));
105
142
  return app.listen(port, () => {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -38,8 +38,10 @@ export interface Session {
38
38
  /** Project slug directory the session lives under. */
39
39
  project: string;
40
40
  model: string;
41
- /** ISO timestamp of first activity. */
41
+ /** ISO timestamp of first activity ("" if the log carries no timestamps). */
42
42
  startedAt: string;
43
+ /** Wall-clock duration from first to last timestamped line (null if unknown). */
44
+ durationMs: number | null;
43
45
  turns: Turn[];
44
46
  totalTokens: number;
45
47
  totalInput: number;
@@ -60,3 +62,26 @@ export interface ToolAggregate {
60
62
  cacheRead: number;
61
63
  cacheWrite: number;
62
64
  }
65
+ /** Totals for one project across all of its sessions. */
66
+ export interface ProjectOverview {
67
+ project: string;
68
+ sessions: number;
69
+ totalTokens: number;
70
+ totalInput: number;
71
+ totalOutput: number;
72
+ totalCacheRead: number;
73
+ totalCacheWrite: number;
74
+ }
75
+ /** The multi-session overview: per-project rows + global totals + tool share. */
76
+ export interface Overview {
77
+ projects: ProjectOverview[];
78
+ totals: {
79
+ sessions: number;
80
+ totalTokens: number;
81
+ totalInput: number;
82
+ totalOutput: number;
83
+ totalCacheRead: number;
84
+ totalCacheWrite: number;
85
+ };
86
+ toolAggregates: ToolAggregate[];
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphtrack",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Visualize Claude Code token usage as an interactive graph. Local-first, open source, your logs never leave your machine.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,10 @@
15
15
  "prepublishOnly": "npm run build",
16
16
  "pack:check": "npm pack --dry-run"
17
17
  },
18
- "files": ["dist", "public/index.html"],
18
+ "files": [
19
+ "dist",
20
+ "public/index.html"
21
+ ],
19
22
  "keywords": [
20
23
  "claude-code",
21
24
  "token",
@@ -32,4 +35,4 @@
32
35
  "@types/node": "^22.5.0",
33
36
  "typescript": "^5.5.0"
34
37
  }
35
- }
38
+ }
package/public/index.html CHANGED
@@ -55,10 +55,34 @@
55
55
  #graph .empty h2 { color:#fff; margin:0; font-size:20px; }
56
56
  #graph .empty p { color:var(--muted); margin:0; font-size:13px; max-width:460px; line-height:1.5; }
57
57
  #graph .empty code { background:var(--card); padding:2px 6px; border-radius:4px; color:var(--yellow); }
58
- #spinner { display:none; position:absolute; inset:0; z-index:3; align-items:center; justify-content:center; }
58
+ #spinner { display:none; position:fixed; inset:0; z-index:1000; align-items:center; justify-content:center; }
59
59
  #spinner.on { display:flex; }
60
60
  #spinner span { color:var(--yellow); border:2px solid #3a3a3a; border-top-color:var(--yellow); border-radius:50%; width:34px; height:34px; animation:spin 0.8s linear infinite; }
61
61
  @keyframes spin { to { transform: rotate(360deg); } }
62
+ #btnOverview {
63
+ background:var(--card); color:var(--body); border:1px solid #3a3a3a;
64
+ padding:6px 12px; border-radius:6px; cursor:pointer; font-size:13px;
65
+ }
66
+ #btnOverview:hover { border-color:#5a5a5a; }
67
+ #btnOverview.active { background:var(--yellow); color:#0a0a0a; border-color:var(--yellow); font-weight:600; }
68
+ .tables { height:100%; overflow:auto; padding:20px 28px 40px; }
69
+ .tbl-sec { margin-bottom:28px; max-width:900px; }
70
+ .tables h3 { color:#fff; margin:0 0 10px; font-size:14px; letter-spacing:1px; text-transform:uppercase; }
71
+ .tables table { width:100%; border-collapse:collapse; font-size:13px; }
72
+ .tables th, .ov-list th { text-align:left; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.5px; font-weight:600; padding:6px 12px; border-bottom:1px solid #2a2a2a; }
73
+ .tables td, .ov-list td { padding:7px 12px; border-bottom:1px solid #242424; color:var(--body); }
74
+ .tables th.num, .tables td.num, .ov-list th.num, .ov-list td.num { text-align:right; font-variant-numeric:tabular-nums; }
75
+ .swatch { display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:8px; vertical-align:middle; }
76
+ .bar { display:inline-block; height:8px; border-radius:4px; min-width:2px; vertical-align:middle; }
77
+ .tables .muted, .ov-list .muted { color:var(--muted); }
78
+ .ov-wrap { height:100%; display:flex; }
79
+ .ov-graph { flex:1 1 auto; min-width:0; position:relative; }
80
+ .ov-list { flex:0 0 420px; overflow:auto; padding:20px 24px; border-left:1px solid #2a2a2a; }
81
+ .ov-list h3 { color:#fff; margin:0 0 12px; font-size:14px; letter-spacing:1px; text-transform:uppercase; }
82
+ .ov-list .hint { color:var(--muted); font-size:12px; margin:12px 0 0; }
83
+ .project-row { cursor:pointer; }
84
+ .project-row:hover td { background:#121212; }
85
+ .proj-name { color:#fff; font-family:ui-monospace, monospace; font-size:12px; }
62
86
  #glow {
63
87
  position: fixed; inset: 0; pointer-events: none; z-index: 9999;
64
88
  background: radial-gradient(circle 200px at var(--gx, -9999px) var(--gy, -9999px),
@@ -72,13 +96,17 @@
72
96
  <header>
73
97
  <h1><span class="dot">▪</span>Graphtrack</h1>
74
98
  <span class="sub">Claude Code token usage, as a graph</span>
99
+ <button id="btnOverview" title="All sessions at a glance">Overview</button>
75
100
  <select id="sessionSelect" disabled title="Select a session"><option>Loading sessions…</option></select>
76
101
  <div class="toggle">
77
102
  <button id="btnAgg" class="active">Aggregate</button>
78
103
  <button id="btnChain">Chronological</button>
104
+ <button id="btnTable">Table</button>
79
105
  </div>
80
106
  </header>
81
107
  <div id="stats">
108
+ <div class="stat"><span class="label">Started</span><span class="value" id="sStarted">–</span></div>
109
+ <div class="stat"><span class="label">Duration</span><span class="value" id="sDur">–</span></div>
82
110
  <div class="stat"><span class="label">Model</span><span class="value" id="sModel">–</span></div>
83
111
  <div class="stat"><span class="label">Turns</span><span class="value" id="sTurns">–</span></div>
84
112
  <div class="stat"><span class="label">Tool calls</span><span class="value" id="sCalls">–</span></div>
@@ -88,17 +116,31 @@
88
116
  <div class="stat"><span class="label">Cache read</span><span class="value" id="sRead">–</span></div>
89
117
  <div class="stat"><span class="label">Cache write</span><span class="value" id="sWrite">–</span></div>
90
118
  </div>
91
- <div id="graph"><div id="spinner"><span></span></div></div>
119
+ <div id="graph"></div>
120
+ <div id="spinner"><span></span></div>
92
121
  <div id="glow"></div>
93
122
  <script>
94
123
  const $ = (id) => document.getElementById(id);
95
124
  const fmt = (n) => n == null ? "–" : n >= 1e6 ? (n/1e6).toFixed(1)+"M" : n >= 1e3 ? (n/1e3).toFixed(1)+"k" : n;
125
+ const fmtTime = (iso) => iso ? new Date(iso).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "–";
126
+ const fmtDur = (ms) => {
127
+ if (ms == null) return "–";
128
+ const s = Math.round(ms / 1000);
129
+ if (s < 60) return s + "s";
130
+ const m = Math.floor(s / 60), r = s % 60;
131
+ return m < 60 ? m + "m " + r + "s" : Math.floor(m / 60) + "h " + (m % 60) + "m";
132
+ };
133
+ function escapeHtml(s) {
134
+ return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
135
+ }
136
+ function truncate(s, n) { s = String(s); return s.length > n ? s.slice(0, n - 1) + "…" : s; }
96
137
 
97
138
  const sessionSelect = $("sessionSelect");
98
139
  const spinner = $("spinner");
99
140
  const container = $("graph");
100
- let current = null; // { session, aggregate, chronological, toolAggregates }
101
- let view = "aggregate";
141
+ let current = null; // { session, turns, aggregate, chronological, toolAggregates }
142
+ let overview = null; // { totals, projects, toolAggregates, graph }
143
+ let view = "overview";
102
144
  let network = null;
103
145
 
104
146
  const glow = document.getElementById("glow");
@@ -117,23 +159,19 @@
117
159
  edges: { color: { color: "#3a3a3a", highlight: "#faff69" }, smooth: { enabled: true, type: "dynamic" } },
118
160
  };
119
161
 
120
- function render() {
121
- if (!current) return;
122
- const data = view === "aggregate" ? current.aggregate : current.chronological;
123
- if (network) network.destroy();
124
- network = new vis.Network(container, { nodes: new vis.DataSet(data.nodes), edges: new vis.DataSet(data.edges) }, options);
162
+ function showSpinner(on) { spinner.classList.toggle("on", on); }
163
+ function setViewActive(active) {
164
+ ["btnAgg", "btnChain", "btnTable", "btnOverview"].forEach((id) => $(id).classList.toggle("active", id === active));
125
165
  }
126
-
127
- function showEmpty(title, body) {
128
- if (network) network.destroy();
129
- network = null;
130
- container.innerHTML = '<div class="empty">' + (title ? "<h2></h2><p></p>" : "") + '</div><div id="spinner"><span></span></div>';
131
- if (title) { container.querySelector("h2").textContent = title; container.querySelector("p").textContent = body; }
132
- // reattach glow/spinner refs are fixed; spinner is a child now
133
- spinner.classList.remove("on");
166
+ function unmountGraph() { if (network) { network.destroy(); network = null; } }
167
+ function mountGraph(data, el) {
168
+ unmountGraph();
169
+ network = new vis.Network(el || container, { nodes: new vis.DataSet(data.nodes), edges: new vis.DataSet(data.edges) }, options);
134
170
  }
135
171
 
136
172
  function fillStats(s) {
173
+ $("sStarted").textContent = fmtTime(s.startedAt);
174
+ $("sDur").textContent = fmtDur(s.durationMs);
137
175
  $("sModel").textContent = s.model;
138
176
  $("sTurns").textContent = s.turns;
139
177
  $("sCalls").textContent = s.toolCalls;
@@ -144,27 +182,152 @@
144
182
  $("sWrite").textContent = fmt(s.totalCacheWrite);
145
183
  }
146
184
 
185
+ function showEmpty(title, body) {
186
+ unmountGraph();
187
+ container.innerHTML = '<div class="empty">' + (title ? "<h2></h2><p></p>" : "") + '</div>';
188
+ if (title) { container.querySelector("h2").textContent = title; container.querySelector("p").textContent = body; }
189
+ }
190
+
191
+ // ---- session views (aggregate / chronological / table) ----
192
+
193
+ function render() {
194
+ if (!current) { showEmpty("No session selected", "Pick a session from the dropdown, or open Overview for all sessions."); return; }
195
+ if (view === "table") { renderTable(); return; }
196
+ mountGraph(view === "chronological" ? current.chronological : current.aggregate);
197
+ }
198
+
199
+ function renderTable() {
200
+ unmountGraph();
201
+ const tools = current.toolAggregates || [];
202
+ const turns = current.turns || [];
203
+ const toolMax = Math.max(1, ...tools.map((t) => t.totalTokens));
204
+ const toolRows = tools.map((t) => `
205
+ <tr>
206
+ <td><span class="swatch" style="background:${t.color}"></span>${escapeHtml(t.name)}</td>
207
+ <td class="num">${t.calls}</td>
208
+ <td class="num">${fmt(t.totalTokens)}</td>
209
+ <td><div class="bar" style="width:${Math.max(2, Math.round(t.totalTokens / toolMax * 100))}%;background:${t.color}"></div></td>
210
+ </tr>`).join("");
211
+ const turnRows = turns.map((t) => `
212
+ <tr>
213
+ <td class="num">${t.index + 1}</td>
214
+ <td title="${escapeHtml(t.prompt)}">${escapeHtml(truncate(t.prompt, 64))}</td>
215
+ <td class="num">${t.toolCalls}</td>
216
+ <td class="num">${fmt(t.totalTokens)}</td>
217
+ </tr>`).join("");
218
+ container.innerHTML = `
219
+ <div class="tables">
220
+ <section class="tbl-sec">
221
+ <h3>Tool share</h3>
222
+ <table>
223
+ <thead><tr><th>Tool</th><th class="num">Calls</th><th class="num">Tokens</th><th style="width:34%">Share</th></tr></thead>
224
+ <tbody>${toolRows || '<tr><td colspan="4" class="muted">No tool calls in this session.</td></tr>'}</tbody>
225
+ </table>
226
+ </section>
227
+ <section class="tbl-sec">
228
+ <h3>Turns</h3>
229
+ <table>
230
+ <thead><tr><th class="num">#</th><th>Prompt</th><th class="num">Calls</th><th class="num">Tokens</th></tr></thead>
231
+ <tbody>${turnRows || '<tr><td colspan="4" class="muted">No turns in this session.</td></tr>'}</tbody>
232
+ </table>
233
+ </section>
234
+ </div>`;
235
+ }
236
+
147
237
  async function loadSession(slug, id) {
148
- spinner.classList.add("on");
238
+ showSpinner(true);
149
239
  try {
150
240
  const r = await fetch("/api/session/" + encodeURIComponent(slug) + "/" + encodeURIComponent(id));
151
241
  if (!r.ok) throw new Error("no data");
152
242
  current = await r.json();
153
243
  fillStats(current.session);
154
- spinner.classList.remove("on");
244
+ view = "aggregate";
245
+ setViewActive("btnAgg");
155
246
  render();
156
247
  } catch (e) {
157
- spinner.classList.remove("on");
158
248
  showEmpty("Could not load session", "The server couldn't parse this session file. It may be malformed or the API returned no data.");
249
+ } finally {
250
+ showSpinner(false);
251
+ }
252
+ }
253
+
254
+ // ---- overview ----
255
+
256
+ function overviewStats(o) {
257
+ return {
258
+ model: "All sessions",
259
+ startedAt: "",
260
+ durationMs: null,
261
+ turns: o.totals.sessions,
262
+ toolCalls: o.toolAggregates.reduce((a, t) => a + t.calls, 0),
263
+ totalTokens: o.totals.totalTokens,
264
+ totalInput: o.totals.totalInput,
265
+ totalOutput: o.totals.totalOutput,
266
+ totalCacheRead: o.totals.totalCacheRead,
267
+ totalCacheWrite: o.totals.totalCacheWrite,
268
+ };
269
+ }
270
+
271
+ function filterSessions(project) {
272
+ for (const opt of sessionSelect.options) opt.hidden = opt.dataset.slug !== project;
273
+ const firstVisible = [...sessionSelect.options].find((o) => !o.hidden);
274
+ if (firstVisible) { sessionSelect.value = firstVisible.value; loadSession(firstVisible.dataset.slug, firstVisible.value); }
275
+ }
276
+
277
+ function renderOverview() {
278
+ unmountGraph();
279
+ const o = overview;
280
+ if (!o) { showEmpty("No overview data", "The server didn't return an overview."); return; }
281
+ fillStats(overviewStats(o));
282
+ const projMax = Math.max(1, ...o.projects.map((p) => p.totalTokens));
283
+ const total = o.totals.totalTokens || 1;
284
+ const projRows = o.projects.map((p) => `
285
+ <tr class="project-row" data-project="${escapeHtml(p.project)}">
286
+ <td class="proj-name">${escapeHtml(p.project)}</td>
287
+ <td class="num">${p.sessions}</td>
288
+ <td class="num">${fmt(p.totalTokens)}</td>
289
+ <td class="num">${Math.round(p.totalTokens / total * 100)}%</td>
290
+ <td><div class="bar" style="width:${Math.max(2, Math.round(p.totalTokens / projMax * 100))}%;background:var(--yellow)"></div></td>
291
+ </tr>`).join("");
292
+ container.innerHTML = `
293
+ <div class="ov-wrap">
294
+ <div class="ov-graph" id="ovGraph"></div>
295
+ <div class="ov-list">
296
+ <h3>Projects · ${o.totals.sessions} sessions</h3>
297
+ <table>
298
+ <thead><tr><th>Project</th><th class="num">Sess</th><th class="num">Tokens</th><th class="num">%</th><th style="width:30%"></th></tr></thead>
299
+ <tbody>${projRows || '<tr><td colspan="5" class="muted">No sessions found.</td></tr>'}</tbody>
300
+ </table>
301
+ <p class="hint">Click a project to load its sessions above.</p>
302
+ </div>
303
+ </div>`;
304
+ container.querySelectorAll(".project-row").forEach((tr) =>
305
+ tr.addEventListener("click", () => filterSessions(tr.dataset.project)));
306
+ const hub = o.graph || { nodes: [{ id: "session", label: "All sessions", value: 6, color: { background: "#faff69" } }], edges: [] };
307
+ if ($("ovGraph")) mountGraph(hub, $("ovGraph"));
308
+ }
309
+
310
+ async function loadOverview() {
311
+ showSpinner(true);
312
+ try {
313
+ const r = await fetch("/api/overview");
314
+ overview = await r.json();
315
+ view = "overview";
316
+ setViewActive("btnOverview");
317
+ renderOverview();
318
+ } catch (e) {
319
+ showEmpty("Can't reach the Graphtrack server", "Start it with: graphtrack — this page must be served from http://localhost:9090.");
320
+ } finally {
321
+ showSpinner(false);
159
322
  }
160
323
  }
161
324
 
325
+ // ---- boot + controls ----
326
+
162
327
  async function loadSessions() {
163
- spinner.classList.add("on");
164
328
  try {
165
329
  const r = await fetch("/api/sessions");
166
330
  const data = await r.json();
167
- spinner.classList.remove("on");
168
331
  if (!data.sessions || data.sessions.length === 0) {
169
332
  showEmpty("No Claude Code sessions found",
170
333
  "Graphtrack reads session logs from " + data.logsDir + ". Run Claude Code a few times, then restart with: graphtrack --dir <path> if your logs live elsewhere.");
@@ -179,10 +342,8 @@
179
342
  opt.textContent = s.project + " · " + s.id;
180
343
  sessionSelect.appendChild(opt);
181
344
  });
182
- const first = data.sessions[0];
183
- loadSession(first.project, first.id);
345
+ loadOverview();
184
346
  } catch (e) {
185
- spinner.classList.remove("on");
186
347
  showEmpty("Can't reach the Graphtrack server",
187
348
  "Start it with: graphtrack — this page must be served from http://localhost:9090.");
188
349
  }
@@ -192,9 +353,14 @@
192
353
  const opt = sessionSelect.selectedOptions[0];
193
354
  if (opt) loadSession(opt.dataset.slug, opt.value);
194
355
  });
195
- $("btnAgg").addEventListener("click", () => { view = "aggregate"; setActive("btnAgg"); render(); });
196
- $("btnChain").addEventListener("click", () => { view = "chronological"; setActive("btnChain"); render(); });
197
- function setActive(id) { $("btnAgg").classList.toggle("active", id==="btnAgg"); $("btnChain").classList.toggle("active", id==="btnChain"); }
356
+ $("btnOverview").addEventListener("click", () => {
357
+ view = "overview";
358
+ setViewActive("btnOverview");
359
+ if (!overview) loadOverview(); else renderOverview();
360
+ });
361
+ $("btnAgg").addEventListener("click", () => { view = "aggregate"; setViewActive("btnAgg"); render(); });
362
+ $("btnChain").addEventListener("click", () => { view = "chronological"; setViewActive("btnChain"); render(); });
363
+ $("btnTable").addEventListener("click", () => { view = "table"; setViewActive("btnTable"); if (!current) render(); else renderTable(); });
198
364
 
199
365
  loadSessions();
200
366
  </script>