@abnersajr/claude-timeline 1.0.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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/capture.js +140 -0
- package/dist/classifier.d.ts +37 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.test.d.ts +2 -0
- package/dist/classifier.test.d.ts.map +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1328 -0
- package/dist/context-tracker.d.ts +44 -0
- package/dist/context-tracker.d.ts.map +1 -0
- package/dist/context-tracker.test.d.ts +2 -0
- package/dist/context-tracker.test.d.ts.map +1 -0
- package/dist/conversation-groups.d.ts +11 -0
- package/dist/conversation-groups.d.ts.map +1 -0
- package/dist/conversation-groups.test.d.ts +2 -0
- package/dist/conversation-groups.test.d.ts.map +1 -0
- package/dist/cost-stream-capture.d.ts +47 -0
- package/dist/cost-stream-capture.d.ts.map +1 -0
- package/dist/cost-stream-db.d.ts +87 -0
- package/dist/cost-stream-db.d.ts.map +1 -0
- package/dist/cost-stream-merger.d.ts +38 -0
- package/dist/cost-stream-merger.d.ts.map +1 -0
- package/dist/db-reader-BrPRGqww.mjs +1028 -0
- package/dist/db-reader-BrPRGqww.mjs.map +1 -0
- package/dist/db-reader-CPXmkt55.mjs +2 -0
- package/dist/db-reader.d.ts +58 -0
- package/dist/db-reader.d.ts.map +1 -0
- package/dist/db.js +100 -0
- package/dist/dedup.d.ts +16 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/dedup.test.d.ts +2 -0
- package/dist/dedup.test.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/jsonl-parser.d.ts +14 -0
- package/dist/jsonl-parser.d.ts.map +1 -0
- package/dist/jsonl-parser.test.d.ts +2 -0
- package/dist/jsonl-parser.test.d.ts.map +1 -0
- package/dist/merger.d.ts +31 -0
- package/dist/merger.d.ts.map +1 -0
- package/dist/model-parser.d.ts +25 -0
- package/dist/model-parser.d.ts.map +1 -0
- package/dist/model-parser.test.d.ts +2 -0
- package/dist/model-parser.test.d.ts.map +1 -0
- package/dist/noise-filter.d.ts +6 -0
- package/dist/noise-filter.d.ts.map +1 -0
- package/dist/pricing-B-rwfwDB.mjs +2 -0
- package/dist/pricing-DTmya3JY.mjs +273 -0
- package/dist/pricing-DTmya3JY.mjs.map +1 -0
- package/dist/pricing.d.ts +26 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/server.cjs +31237 -0
- package/dist/session-state.d.ts +19 -0
- package/dist/session-state.d.ts.map +1 -0
- package/dist/session-state.test.d.ts +2 -0
- package/dist/session-state.test.d.ts.map +1 -0
- package/dist/subagent-locator.d.ts +30 -0
- package/dist/subagent-locator.d.ts.map +1 -0
- package/dist/subagent-locator.test.d.ts +2 -0
- package/dist/subagent-locator.test.d.ts.map +1 -0
- package/dist/subagent-resolver.d.ts +35 -0
- package/dist/subagent-resolver.d.ts.map +1 -0
- package/dist/subagent-resolver.test.d.ts +2 -0
- package/dist/subagent-resolver.test.d.ts.map +1 -0
- package/dist/tool-extraction.d.ts +34 -0
- package/dist/tool-extraction.d.ts.map +1 -0
- package/dist/tool-extraction.test.d.ts +2 -0
- package/dist/tool-extraction.test.d.ts.map +1 -0
- package/dist/tool-matcher.d.ts +35 -0
- package/dist/tool-matcher.d.ts.map +1 -0
- package/dist/types.d.ts +272 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/web/assets/index-Dr0FGYfS.js +158 -0
- package/dist/web/assets/index-nXTIEelb.css +1 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon-light.svg +14 -0
- package/dist/web/favicon.svg +14 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.svg +20 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 claude-timeline contributors
|
|
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,227 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="packages/web/public/logo.svg" alt="claude-timeline" width="400" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Extract and visualize Claude Code session timelines</strong><br/>
|
|
7
|
+
Conversations · Tool calls · Pricing · Tokens · Subagents
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://github.com/abnersajr/claude-timeline/actions"><img src="https://github.com/abnersajr/claude-timeline/workflows/CI/badge.svg" alt="CI" /></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@abnersajr/claude-timeline"><img src="https://img.shields.io/npm/v/@abnersajr/claude-timeline" alt="npm version" /></a>
|
|
13
|
+
<a href="https://github.com/abnersajr/claude-timeline/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@abnersajr/claude-timeline" alt="license" /></a>
|
|
14
|
+
<a href="https://ko-fi.com/abnersajr"><img src="https://img.shields.io/badge/Ko--fi-Support%20the%20project-ff5e5b?logo=ko-fi&logoColor=white" alt="Ko-fi" /></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
> **👋 About me:** I'm Abner — a dev from Brazil 🇧🇷 now living in Montreal 🍁. This project started because I was frustrated with existing tools and decided to vibe-code my own. It grew into something real, and now I'm sharing it with the community.
|
|
18
|
+
>
|
|
19
|
+
> **☕ Donations** will support continued development of this project, and a portion will be donated to an NGO that takes care of animals 🐾. If this tool saved you time, consider fueling the next feature!
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @abnersajr/claude-timeline
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or run directly with npx (no install needed):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @abnersajr/claude-timeline
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Requirements:** Node.js >= 22
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Web UI (recommended)
|
|
40
|
+
|
|
41
|
+
Start the server and open the interactive timeline in your browser:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
claude-timeline serve
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Opens `http://localhost:5199` automatically. Use `--port` to change the port:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
claude-timeline serve --port 3000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### CLI
|
|
54
|
+
|
|
55
|
+
List recent sessions:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
claude-timeline list
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Extract a specific session to JSON:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
claude-timeline extract --session-id <id>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Cost Capture Setup
|
|
68
|
+
|
|
69
|
+
Get real-time per-turn cost tracking by installing the statusline wrapper:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
claude-timeline setup
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This hooks into Claude Code's statusline and captures token usage, pricing, and model data to a local SQLite database. The web UI reads this data for live cost streaming.
|
|
76
|
+
|
|
77
|
+
To uninstall, remove the `statusLine` key from `~/.claude/settings.json` and run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
rm -rf ~/.claude-timeline/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## What is this?
|
|
84
|
+
|
|
85
|
+
Claude Code stores rich session data in local SQLite and JSONL files, but there's no built-in way to see the full picture. **claude-timeline** extracts, merges, and visualizes everything — conversations, tool calls, token usage, costs, and subagent activity.
|
|
86
|
+
|
|
87
|
+
### Features
|
|
88
|
+
|
|
89
|
+
- 📊 **Full session timelines** — conversations, tool calls, file edits, all in order
|
|
90
|
+
- 💰 **Cost tracking** — per-turn and per-model pricing with token breakdowns
|
|
91
|
+
- 🤖 **Subagent detection** — automatically identifies and resolves subagent sessions
|
|
92
|
+
- 🧠 **Context analysis** — tracks context window usage, phases, and injections
|
|
93
|
+
- 🎨 **Web UI** — interactive timeline visualization with dark/light themes
|
|
94
|
+
- ⚡ **CLI tool** — extract session data from the command line
|
|
95
|
+
|
|
96
|
+
## CLI Reference
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
claude-timeline <command> [options]
|
|
100
|
+
|
|
101
|
+
Commands:
|
|
102
|
+
serve [--port <port>] Start web UI + API server (default: 5199)
|
|
103
|
+
extract --session-id <id> Extract a specific session to JSON
|
|
104
|
+
list List all available sessions
|
|
105
|
+
setup Install cost-capture statusline wrapper
|
|
106
|
+
update-pricing Fetch latest model pricing from Anthropic
|
|
107
|
+
--help Show help
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
--port <port> Server port (serve mode, default: 5199)
|
|
111
|
+
--db-path <path> SQLite DB path (default: ~/.claude/usage.db)
|
|
112
|
+
--projects-dir <path> Projects directory (default: ~/.claude/projects)
|
|
113
|
+
--output <path> Write JSON to file instead of stdout
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Understanding Claude Code Sessions
|
|
117
|
+
|
|
118
|
+
To get the most out of claude-timeline, it helps to understand how Claude Code works under the hood.
|
|
119
|
+
|
|
120
|
+
### Core Concepts
|
|
121
|
+
|
|
122
|
+
| Concept | What it is |
|
|
123
|
+
|---------|------------|
|
|
124
|
+
| **Session** | A single Claude Code interaction lifecycle. Started when you launch `claude` or run a slash command. All work shares a persistent conversation history. |
|
|
125
|
+
| **Turn** | A single API call to the Claude model. Each turn includes cached context, new input, and model output. |
|
|
126
|
+
| **Token** | A unit of text (~4 English characters). All billing is token-based. "Hello world" is roughly 3 tokens. |
|
|
127
|
+
| **Prompt Caching** | Claude caches repeated context (system prompt, conversation history) to avoid re-processing. Cached context is billed at 10% of standard input rates. |
|
|
128
|
+
|
|
129
|
+
### How Tokens Are Billed
|
|
130
|
+
|
|
131
|
+
Every Claude Code interaction uses four billable token types:
|
|
132
|
+
|
|
133
|
+
| Token Type | What it means | Cost (Sonnet 4) |
|
|
134
|
+
|------------|---------------|-----------------|
|
|
135
|
+
| **Input** | New text sent to the model that is NOT cached. Usually tiny (1-2 tokens). | $3.00 / MTok |
|
|
136
|
+
| **Output** | Model-generated text, tool requests, thinking. The most expensive type. | $15.00 / MTok |
|
|
137
|
+
| **Cache Creation** | One-time cost to write context to the cache. | $3.75 / MTok |
|
|
138
|
+
| **Cache Read** | Per-turn cost to retrieve cached context. Compounds over turns. | $0.30 / MTok |
|
|
139
|
+
|
|
140
|
+
> **Key insight**: `input_tokens` is NOT the total input. It's the non-cached delta (typically 1-2 tokens). The bulk of your context is in `cache_read_input_tokens`.
|
|
141
|
+
|
|
142
|
+
### How Cache Read Compounds
|
|
143
|
+
|
|
144
|
+
Each turn reads the entire cached context (system prompt + all previous conversation). As the conversation grows, so does cache read:
|
|
145
|
+
|
|
146
|
+
| Session Length | Avg Cache Read/Turn | Total Cache Read | Cache Read Cost |
|
|
147
|
+
|----------------|---------------------|------------------|-----------------|
|
|
148
|
+
| 1 turn | 12k | 12k | ~$0.004 |
|
|
149
|
+
| 10 turns | 34k | 340k | ~$0.10 |
|
|
150
|
+
| 28 turns | 33k | 929k | ~$0.28 |
|
|
151
|
+
| 100 turns | 35k | 3.5M | ~$1.05 |
|
|
152
|
+
|
|
153
|
+
The 929k number sounds large, but at $0.30/MTok it costs less than a quarter. **Output tokens are 5x more expensive** than cache read.
|
|
154
|
+
|
|
155
|
+
### How Tool Calls Affect Cost
|
|
156
|
+
|
|
157
|
+
Tool results (success or failure) are added to conversation history. Large tool outputs compound:
|
|
158
|
+
|
|
159
|
+
- Reading a 100k-token file in Turn 5 adds 100k tokens to history
|
|
160
|
+
- Turns 6-28 each read that extra 100k in cache
|
|
161
|
+
- Cost: 23 turns × 100k × $0.30/MTok = ~$0.69 extra
|
|
162
|
+
|
|
163
|
+
This is why targeted searches (`grep "ERROR" app.log`) are cheaper than reading entire files (`cat app.log`).
|
|
164
|
+
|
|
165
|
+
## Tips for Efficient Sessions
|
|
166
|
+
|
|
167
|
+
| Strategy | Impact |
|
|
168
|
+
|----------|--------|
|
|
169
|
+
| **Start in the project directory** | `cd ~/projects/my-app && claude` instead of `cd ~ && claude "fix X in my-app"`. Broader scope = more file discovery = more cache bloat. |
|
|
170
|
+
| **Use exact file paths** | "Edit `src/components/Button.tsx`" instead of "edit the button component". Reduces discovery turns. |
|
|
171
|
+
| **Batch related tasks** | "Fix X, Y, Z" in one session vs three separate sessions. Fewer turns = less cumulative cache read. |
|
|
172
|
+
| **Keep sessions under 15 turns** | After 10-15 turns, restart to reset cache accumulation. A 10-turn session costs ~$0.18 vs ~$0.63 for 28 turns. |
|
|
173
|
+
| **Watch tool output sizes** | Use `head`, `tail`, `grep` to limit output. A 100k-token file read adds ~$0.69 in cache bloat over 28 turns. |
|
|
174
|
+
| **Use `.claudeignore`** | Exclude `node_modules/`, `dist/`, `*.log` from discovery to prevent unrelated files from entering context. |
|
|
175
|
+
|
|
176
|
+
## Data Sources
|
|
177
|
+
|
|
178
|
+
| Source | Location | Contents |
|
|
179
|
+
|--------|----------|----------|
|
|
180
|
+
| SQLite | `~/.claude/usage.db` | Sessions, turns, token counts |
|
|
181
|
+
| JSONL | `~/.claude/projects/<project>/<session>.jsonl` | Full message content, tool calls, file paths |
|
|
182
|
+
|
|
183
|
+
## Troubleshooting
|
|
184
|
+
|
|
185
|
+
### Find your recent sessions
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
sqlite3 ~/.claude/usage.db "SELECT session_id, project_name, total_cache_read, turn_count, last_timestamp FROM sessions WHERE last_timestamp > datetime('now', '-1 day') ORDER BY total_cache_read DESC LIMIT 5;"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Get full token breakdown for a session
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
sqlite3 ~/.claude/usage.db "SELECT timestamp, cache_read_tokens, cache_creation_tokens, input_tokens, output_tokens, tool_name FROM turns WHERE session_id='YOUR_SESSION_ID' ORDER BY timestamp;"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Calculate session cost (Sonnet 4 rates)
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
sqlite3 ~/.claude/usage.db "SELECT
|
|
201
|
+
session_id,
|
|
202
|
+
ROUND(total_cache_read * 0.30 / 1000000, 4) AS cache_read_USD,
|
|
203
|
+
ROUND(total_output_tokens * 15.00 / 1000000, 4) AS output_USD,
|
|
204
|
+
ROUND(total_cache_creation * 3.75 / 1000000, 4) AS creation_USD,
|
|
205
|
+
ROUND(total_input_tokens * 3.00 / 1000000, 4) AS input_USD,
|
|
206
|
+
ROUND((total_cache_read * 0.30 + total_output_tokens * 15.00 + total_cache_creation * 3.75 + total_input_tokens * 3.00) / 1000000, 4) AS total_USD
|
|
207
|
+
FROM sessions WHERE session_id='YOUR_SESSION_ID';"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Check which files were read in a session
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
cat ~/.claude/projects/YOUR_PROJECT_DIR/YOUR_SESSION_ID.jsonl | jq -r 'select(.type == "assistant" and .message.content) | .message.content[]? | select(.type == "tool_use" and .name == "Read") | .input.filePath' 2>/dev/null
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
git clone https://github.com/abnersajr/claude-timeline.git
|
|
220
|
+
cd claude-timeline
|
|
221
|
+
pnpm install
|
|
222
|
+
pnpm dev
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
[MIT](LICENSE)
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { getDb, upsertSnapshot, upsertSessionSummary } from "./db.js";
|
|
7
|
+
|
|
8
|
+
const CONFIG_PATH = join(process.env.HOME, ".claude-timeline", "config.json");
|
|
9
|
+
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseStdin() {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
if (process.stdin.isTTY) return resolve(null);
|
|
21
|
+
|
|
22
|
+
let raw = "";
|
|
23
|
+
let settled = false;
|
|
24
|
+
let firstByteTimer;
|
|
25
|
+
let idleTimer;
|
|
26
|
+
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
clearTimeout(firstByteTimer);
|
|
29
|
+
clearTimeout(idleTimer);
|
|
30
|
+
process.stdin.off("data", onData);
|
|
31
|
+
process.stdin.off("end", onEnd);
|
|
32
|
+
process.stdin.pause();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const finish = (value) => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
settled = true;
|
|
38
|
+
cleanup();
|
|
39
|
+
resolve(value);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const tryParse = () => {
|
|
43
|
+
const trimmed = raw.trim();
|
|
44
|
+
if (!trimmed) return null;
|
|
45
|
+
try { return JSON.parse(trimmed); } catch { return undefined; }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const onData = (chunk) => {
|
|
49
|
+
clearTimeout(firstByteTimer);
|
|
50
|
+
raw += String(chunk);
|
|
51
|
+
const parsed = tryParse();
|
|
52
|
+
if (parsed !== undefined) return finish(parsed);
|
|
53
|
+
clearTimeout(idleTimer);
|
|
54
|
+
idleTimer = setTimeout(() => finish(tryParse() ?? null), 30);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onEnd = () => finish(tryParse() ?? null);
|
|
58
|
+
|
|
59
|
+
process.stdin.setEncoding("utf8");
|
|
60
|
+
process.stdin.on("data", onData);
|
|
61
|
+
process.stdin.on("end", onEnd);
|
|
62
|
+
firstByteTimer = setTimeout(() => finish(null), 250);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractCostData(json) {
|
|
67
|
+
const cost = json.cost ?? {};
|
|
68
|
+
const ctx = json.context_window ?? {};
|
|
69
|
+
const current = ctx.current_usage ?? {};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
session_id: json.session_id ?? null,
|
|
73
|
+
total_cost_usd: typeof cost.total_cost_usd === "number" ? cost.total_cost_usd : null,
|
|
74
|
+
duration_ms: cost.total_duration_ms ?? null,
|
|
75
|
+
api_duration_ms: cost.total_api_duration_ms ?? null,
|
|
76
|
+
input_tokens: ctx.total_input_tokens ?? current.input_tokens ?? 0,
|
|
77
|
+
output_tokens: ctx.total_output_tokens ?? current.output_tokens ?? 0,
|
|
78
|
+
cache_read_tokens: current.cache_read_input_tokens ?? 0,
|
|
79
|
+
cache_creation_tokens: current.cache_creation_input_tokens ?? 0,
|
|
80
|
+
model: json.model?.display_name ?? json.model?.id ?? null,
|
|
81
|
+
lines_added: cost.total_lines_added ?? 0,
|
|
82
|
+
lines_removed: cost.total_lines_removed ?? 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function killOtherInstances() {
|
|
87
|
+
try {
|
|
88
|
+
const myPid = process.pid;
|
|
89
|
+
const pids = execSync("pgrep -f 'node.*capture\\.js'", { encoding: "utf-8" })
|
|
90
|
+
.trim().split("\n").filter(Boolean).map(Number).filter((p) => p !== myPid);
|
|
91
|
+
for (const pid of pids) {
|
|
92
|
+
try { process.kill(pid, "SIGTERM"); } catch {}
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
killOtherInstances();
|
|
99
|
+
const data = await parseStdin();
|
|
100
|
+
if (!data?.session_id) {
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const costData = extractCostData(data);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
upsertSnapshot(db, { ...costData, raw_json: JSON.stringify(data) });
|
|
109
|
+
if (costData.total_cost_usd !== null) {
|
|
110
|
+
upsertSessionSummary(db, {
|
|
111
|
+
session_id: costData.session_id,
|
|
112
|
+
total_cost_usd: costData.total_cost_usd,
|
|
113
|
+
model: costData.model,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
db.close();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
process.stderr.write(`[cost-capture] DB error: ${e.message}\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const config = loadConfig();
|
|
122
|
+
if (config.originalStatusLine?.command) {
|
|
123
|
+
try {
|
|
124
|
+
const { spawnSync } = await import("node:child_process");
|
|
125
|
+
const result = spawnSync("sh", ["-c", config.originalStatusLine.command], {
|
|
126
|
+
input: JSON.stringify(data),
|
|
127
|
+
encoding: "utf-8",
|
|
128
|
+
timeout: 2000,
|
|
129
|
+
env: { ...process.env, COLUMNS: process.env.COLUMNS ?? "80" },
|
|
130
|
+
});
|
|
131
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
132
|
+
} catch {
|
|
133
|
+
// Original statusline failed — not our problem
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ClassifiedMessage, MessageCategory, RawJsonlRecord } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Hard noise: system/summary/file-history-snapshot/queue-operation/attachment/last-prompt/permission-mode types,
|
|
4
|
+
* sidechain, synthetic assistant, hard noise tags, interruptions.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isHardNoise(record: RawJsonlRecord): boolean;
|
|
7
|
+
/** Compact messages are marked by isCompactSummary flag */
|
|
8
|
+
export declare function isCompactMessage(record: RawJsonlRecord): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* System messages: user-type messages that contain command output
|
|
11
|
+
* (local-command-stdout/stderr). These arrive as type="user" in JSONL
|
|
12
|
+
* but represent command output, not user input.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isSystemMessage(record: RawJsonlRecord): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* User messages: type=user, isMeta=false, has text/image content
|
|
17
|
+
* (not just tool_result blocks). Meta messages (tool results) are
|
|
18
|
+
* classified as assistant because they represent assistant context.
|
|
19
|
+
* Tool-result-only records (isMeta=null, content is array of tool_result)
|
|
20
|
+
* are also classified as assistant — they're CLI tool outputs, not user input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isUserMessage(record: RawJsonlRecord): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Classify a single JSONL record into a category using the priority cascade:
|
|
25
|
+
* 1. hardNoise — noise types, sidechain, synthetic, hard noise tags, interruptions
|
|
26
|
+
* 2. compact — isCompactSummary === true
|
|
27
|
+
* 3. system — user messages with command output (local-command-stdout/stderr)
|
|
28
|
+
* 4. user — type=user, not meta, has text/image content
|
|
29
|
+
* 5. assistant — everything else (catch-all)
|
|
30
|
+
*/
|
|
31
|
+
export declare function classifyMessage(record: RawJsonlRecord): MessageCategory;
|
|
32
|
+
/**
|
|
33
|
+
* Classify an array of messages, returning ClassifiedMessage objects
|
|
34
|
+
* that pair each record with its category.
|
|
35
|
+
*/
|
|
36
|
+
export declare function classifyMessages(records: RawJsonlRecord[]): ClassifiedMessage[];
|
|
37
|
+
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../src/classifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA4CjF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAwB3D;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAK/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAe7D;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,CAMvE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,iBAAiB,EAAE,CAK/E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.test.d.ts","sourceRoot":"","sources":["../src/classifier.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|