@vladar107/claudescope 0.1.1 → 0.3.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/README.md CHANGED
@@ -1,62 +1,88 @@
1
1
  # Claudescope
2
2
 
3
3
  [![CI](https://github.com/vladar107/claudescope/actions/workflows/ci.yml/badge.svg)](https://github.com/vladar107/claudescope/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@vladar107/claudescope)](https://www.npmjs.com/package/@vladar107/claudescope)
5
+ [![node](https://img.shields.io/node/v/@vladar107/claudescope)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/@vladar107/claudescope)](./LICENSE)
4
7
 
5
- *A scope for your Claude Code sessions.*
8
+ *A scope for your AI coding-agent sessions.*
6
9
 
7
- A local, **read-only** web app to browse, read, search, and analyze your
8
- [Claude Code](https://claude.com/claude-code) session transcripts
9
- (`~/.claude/projects/**/*.jsonl`).
10
+ A local, **read-only**, multi-agent viewer to browse, read, search, and analyze
11
+ your AI coding-agent transcripts in one place — both
12
+ [Claude Code](https://claude.com/claude-code) (`~/.claude/projects/**/*.jsonl`)
13
+ and [OpenAI Codex](https://openai.com/codex) (`~/.codex/sessions/**/rollout-*.jsonl`).
14
+ Sessions from every agent that worked in a directory are **merged under one
15
+ project**, each tagged with the agent that produced it.
10
16
 
17
+ - **Multi-agent** — Claude Code and Codex sessions side by side, each labeled with an **agent badge**. A project that several agents touched shows one card with all its agent tags; drill in and **filter the session list by agent**.
11
18
  - **Browse** every session grouped by project — titles, dates, message/tool counts, token totals, cost, git branch, PR links.
12
- - **Read** a session as a clean threaded conversation: markdown, syntax-highlighted code, collapsible thinking, paired tool calls + results, **syntax-highlighted red/green diffs** for `Edit`/`MultiEdit`, attachments, and sidechain/subagent turns. A built-in **find-in-session** bar (⌘/Ctrl+F) searches the whole transcript — including collapsed thinking, tool, and subagent content — auto-expanding and highlighting matches, with a user/assistant filter.
13
- - **Review changes** via a **Files changed** tab that aggregates every `Edit`/`MultiEdit`/`Write` in the session by file, with per-file diffs and +/− counts (diffs load lazily per file).
19
+ - **Read** a session as a clean threaded conversation: markdown, syntax-highlighted code, collapsible thinking, paired tool calls + results, **syntax-highlighted red/green diffs** for edits, attachments, and sidechain/subagent turns. A built-in **find-in-session** bar (⌘/Ctrl+F) searches the whole transcript — including collapsed thinking, tool, and subagent content — auto-expanding and highlighting matches, with a user/assistant filter.
20
+ - **Review changes** via a **Files changed** tab that aggregates every edit/write in the session by file, with per-file diffs and +/− counts (diffs load lazily per file).
14
21
  - **Export / share** a session to Markdown — download or copy it, with an optional toggle to **redact** home-dir paths and likely secrets.
15
- - **Search** full-text across all sessions (DuckDB BM25), with highlighted snippets that deep-link to the exact message.
16
- - **Analyze** token usage and cost over time, by project, and by model — including cache-hit ratio.
22
+ - **Search** full-text across all sessions, all agents (DuckDB BM25), with highlighted snippets that deep-link to the exact message.
23
+ - **Analyze** token usage and cost over time, by project, by model, and **by agent** — including cache-hit ratio.
24
+ - **Light & dark themes** — follows your system appearance, with a manual toggle.
17
25
 
18
26
  > **Privacy:** Everything runs locally on `127.0.0.1`. The app **never** writes to
19
- > `~/.claude`. Its only persistent state lives in `~/.claudescope/` a DuckDB
20
- > index and a copy of the pricing file, both safe to delete anytime. The sole
21
- > outbound request is an optional daily check for a newer published version
22
- > (`claudescope update`); nothing about your transcripts ever leaves your machine.
27
+ > `~/.claude` or `~/.codex` both are read-only sources. Its only persistent
28
+ > state lives in `~/.claudescope/` — a DuckDB index and a copy of the pricing
29
+ > file, both safe to delete anytime. The sole outbound request is an optional
30
+ > daily check for a newer published version (`claudescope update`); nothing about
31
+ > your transcripts ever leaves your machine.
23
32
 
24
33
  ---
25
34
 
26
35
  ## Screenshots
27
36
 
28
37
  > The screenshots below use **synthetic demo data** — every project name, path,
29
- > and message is fabricated. Reproduce it locally with:
30
- > `node scripts/demo-seed.mjs && CLAUDE_PROJECTS_DIR=.demo/projects npm start`.
38
+ > and message is fabricated (`acme-web` is a multi-agent project: Claude Code +
39
+ > Codex). They render light or dark to match your system. Regenerate them with
40
+ > **`npm run screenshots`** (seeds the demo data, boots the app, and captures
41
+ > every view in both themes via Playwright).
31
42
 
32
- **Browse** — every project and its sessions at a glance: titles, dates, message &
33
- tool counts, token totals, cost, git branch, and PR links.
43
+ **Browse** — every project and its sessions at a glance, each tagged with the
44
+ agents that worked in it: titles, dates, message & tool counts, token totals,
45
+ cost, git branch, and PR links.
34
46
 
35
- ![Browse projects and sessions](docs/screenshots/browse.png)
47
+ <picture>
48
+ <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/browse-dark.png">
49
+ <img alt="Browse projects and sessions" src="docs/screenshots/browse-light.png">
50
+ </picture>
36
51
 
37
52
  **Read** — a session as a clean threaded conversation: markdown, collapsible
38
- thinking, **syntax-highlighted red/green diffs** for `Edit`/`MultiEdit`, nested
39
- **subagent** runs, per-message token chips, and a **find-in-session** bar (⌘/Ctrl+F)
40
- that auto-expands and highlights matches. **Conversation / Files-changed** tabs and
41
- an **⤓ Export** (Markdown, optional redaction) sit in the header.
42
-
43
- ![Session reader: tabs, Export, the in-session finder, and a highlighted diff](docs/screenshots/session.png)
44
-
45
- **Search** — full-text across every session (DuckDB BM25) with highlighted
46
- snippets and user/assistant filters; each result deep-links to the exact message.
47
-
48
- ![Full-text search across sessions](docs/screenshots/search.png)
49
-
50
- **Analyze** token & cost analytics over time, by project, and by model, with a
51
- cache-read breakdown. Click a chart legend to toggle a series.
52
-
53
- ![Token and cost analytics dashboard](docs/screenshots/analytics.png)
53
+ thinking, **syntax-highlighted red/green diffs** for edits, nested **subagent**
54
+ runs, per-message token chips, and a **find-in-session** bar (⌘/Ctrl+F) that
55
+ auto-expands and highlights matches. The breadcrumb links back to the project's
56
+ session list; **Conversation / Files-changed** tabs and an **⤓ Export** (Markdown,
57
+ optional redaction) sit in the header.
58
+
59
+ <picture>
60
+ <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/session-dark.png">
61
+ <img alt="Session reader: breadcrumb, tabs, Export, the in-session finder, thinking, and a subagent run" src="docs/screenshots/session-light.png">
62
+ </picture>
63
+
64
+ **Search** — full-text across every session and agent (DuckDB BM25) with
65
+ highlighted snippets and user/assistant filters; each result deep-links to the
66
+ exact message.
67
+
68
+ <picture>
69
+ <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/search-dark.png">
70
+ <img alt="Full-text search across sessions" src="docs/screenshots/search-light.png">
71
+ </picture>
72
+
73
+ **Analyze** — token & cost analytics over time, by project, by model, and **by
74
+ agent**, with a cache-read breakdown. Click a chart legend to toggle a series.
75
+
76
+ <picture>
77
+ <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/analytics-dark.png">
78
+ <img alt="Token and cost analytics dashboard" src="docs/screenshots/analytics-light.png">
79
+ </picture>
54
80
 
55
81
  ---
56
82
 
57
83
  ## Quick start
58
84
 
59
- **Prerequisite:** [Node.js](https://nodejs.org) **20 or newer** (`node -v`).
85
+ **Prerequisite:** [Node.js](https://nodejs.org) **22 or newer** (`node -v`).
60
86
 
61
87
  ### Install (recommended)
62
88
 
@@ -113,19 +139,24 @@ All optional — set via environment variables.
113
139
  | Variable | Default | Description |
114
140
  | --------------------- | ---------------------- | ---------------------------------------------------------------------- |
115
141
  | `PORT` | `4317` | Port the app listens on (or `--port <n>`). |
116
- | `CLAUDE_PROJECTS_DIR` | `~/.claude/projects` | Where to read session transcripts from. A leading `~` is expanded. |
142
+ | `CLAUDE_PROJECTS_DIR` | `~/.claude/projects` | Where to read Claude Code transcripts from. A leading `~` is expanded. |
143
+ | `CODEX_SESSIONS_DIR` | `~/.codex/sessions` | Where to read OpenAI Codex transcripts from. A leading `~` is expanded.|
117
144
  | `CLAUDESCOPE_HOME` | `~/.claudescope` | Where the app keeps its own state (index, pricing copy, logs, PID). |
118
145
  | `REINDEX_INTERVAL_MS` | `15000` | How often to auto-pick-up new/updated sessions. Set `0` to disable. |
119
146
 
147
+ Each agent source is optional — if a directory doesn't exist it's simply skipped,
148
+ so the app works whether you use one agent or both.
149
+
120
150
  Examples:
121
151
 
122
152
  ```bash
123
153
  claudescope --port 8080 # custom port
124
154
  CLAUDE_PROJECTS_DIR=/path/to/exported/projects claudescope # view someone else's transcripts
155
+ CODEX_SESSIONS_DIR=/path/to/codex/sessions claudescope # point at Codex sessions elsewhere
125
156
  claudescope --no-open # don't pop a browser tab
126
157
  ```
127
158
 
128
- The startup banner prints the resolved URL and the sessions directory in use, so
159
+ The startup banner prints the resolved URL and the source directories in use, so
129
160
  you can always confirm what it's reading.
130
161
 
131
162
  ### Cost methodology
@@ -147,10 +178,10 @@ just a `SUM` over events; a project/session total is the sum of its events.
147
178
  Rates live in `~/.claudescope/pricing.json` (seeded on first run from the copy
148
179
  shipped with the package; when running from source, `packages/server/pricing.json`).
149
180
  A model id resolves in this order:
150
- exact `models` entry → **family** match (`opus` / `sonnet` / `haiku` substring)
151
- `default`. The family step means version- or date-suffixed ids (e.g.
152
- `claude-haiku-4-5-20251001`) still price correctly. Shipped rates (USD per 1M tokens,
153
- from Anthropic's published API pricing):
181
+ exact `models` entry → **family** match (`opus` / `sonnet` / `haiku` / `gpt`
182
+ substring) → `default`. The family step means version- or date-suffixed ids (e.g.
183
+ `claude-haiku-4-5-20251001`, or a Codex `gpt-5.x-codex` id) still price correctly.
184
+ Shipped rates (USD per 1M tokens, from Anthropic and OpenAI published API pricing):
154
185
 
155
186
  | family / model | input | output | cache write (5m) | cache read |
156
187
  | ------------------- | ----- | ------ | ---------------- | ---------- |
@@ -158,6 +189,9 @@ from Anthropic's published API pricing):
158
189
  | Opus 4.1 / 4 | $15 | $75 | $18.75 | $1.50 |
159
190
  | Sonnet 4.x | $3 | $15 | $3.75 | $0.30 |
160
191
  | Haiku 4.5 | $1 | $5 | $1.25 | $0.10 |
192
+ | GPT-5 | $0.63 | $5 | — | $0.13 |
193
+ | GPT-5.4 | $2.50 | $15 | — | $0.50 |
194
+ | GPT-5.5 | $5 | $30 | — | $0.50 |
161
195
  | `<synthetic>` | $0 | $0 | $0 | $0 |
162
196
 
163
197
  - Edit `~/.claudescope/pricing.json` to update prices or add models, then re-index
@@ -166,9 +200,9 @@ from Anthropic's published API pricing):
166
200
  published pricing page. There's no official pricing *API*, so this is a
167
201
  best-effort scrape (it validates what it parses and won't write garbage) —
168
202
  review the diff afterwards. Use `--dry-run` to preview without writing.
169
- - The `opus`/`sonnet`/`haiku` family rules use **current** pricing; the deprecated
170
- Opus 4 / 4.1 ($15/$75) are pinned via exact `models` entries. Add an exact entry
171
- to override any specific model.
203
+ - The `opus`/`sonnet`/`haiku`/`gpt` family rules use **current** pricing; the
204
+ deprecated Opus 4 / 4.1 ($15/$75) and specific GPT-5 versions are pinned via
205
+ exact `models` entries. Add an exact entry to override any specific model.
172
206
 
173
207
  > **Caveat:** these are **list-price estimates** — they ignore any discounts,
174
208
  > service tier, or batch pricing, and the cache-write rate assumes the 5-minute
@@ -189,10 +223,14 @@ served from cache (legitimately high for Claude Code, which re-reads cached cont
189
223
  - **New sessions appear automatically.** The app re-scans on an interval
190
224
  (`REINDEX_INTERVAL_MS`, default 15s) and incrementally picks up new or updated
191
225
  transcripts — including the session you're currently running — without a
192
- restart. Reload the page to see the latest. Each scan is near-free when nothing
193
- changed; you can also force one with `POST /api/reindex`.
194
- - **Thinking blocks** appear empty because Claude Code stores only a signature,
195
- not the reasoning text the app notes this explicitly. (Not a bug.)
226
+ restart. In an open session, hit **⟳ Refresh** (or ⌘R / Ctrl+R) to pull the
227
+ latest messages in place without losing your scroll position. Each scan is
228
+ near-free when nothing changed; you can also force one with `POST /api/reindex`.
229
+ - **Thinking blocks** appear empty because Claude Code stores only a signature
230
+ (and Codex only encrypted reasoning), not the plaintext — the app notes this
231
+ explicitly. (Not a bug.)
232
+ - **Codex sessions have no stored title**, so the title falls back to the first
233
+ user message.
196
234
 
197
235
  ---
198
236
 
@@ -211,6 +249,12 @@ and analytics; a small TypeScript parser assembles the threaded view for a singl
211
249
  session. The index is a derived cache — if it's ever corrupted (e.g. the process
212
250
  is killed mid-write) the app discards and rebuilds it automatically.
213
251
 
252
+ Each agent is a **connector** (`packages/server/src/connectors/`). Claude Code
253
+ JSONL is projected per-row; Codex spreads a session across record types, so its
254
+ connector normalizes a rollout to canonical NDJSON first — after that the
255
+ indexing, search, cost, and threading paths are shared. Adding another agent is
256
+ adding another connector.
257
+
214
258
  ---
215
259
 
216
260
  ## Development
@@ -250,12 +294,24 @@ bundles, and publishes. Auth uses npm **Trusted Publishing** (OIDC) — no
250
294
 
251
295
  ---
252
296
 
297
+ ## Security & privacy
298
+
299
+ Claudescope runs entirely on your machine. It treats `~/.claude` and `~/.codex`
300
+ as **read-only**, **binds to `127.0.0.1` only**, sends **no telemetry**, and its sole outbound
301
+ request is a cached npm-registry version check for the update notice. See
302
+ [`SECURITY.md`](./SECURITY.md) for the full breakdown of filesystem, network,
303
+ shell, and self-update behavior — and how to report a vulnerability.
304
+
305
+ ---
306
+
253
307
  ## Troubleshooting
254
308
 
255
309
  - **App is empty / "sessions directory not found"** — `CLAUDE_PROJECTS_DIR`
256
- doesn't point at real transcripts. Check the banner and set it correctly.
310
+ (and/or `CODEX_SESSIONS_DIR`) doesn't point at real transcripts. Check the
311
+ banner and set it correctly. Either source can be absent; only the present one
312
+ is indexed.
257
313
  - **`Error: listen EADDRINUSE :4317`** — the port is taken; run `claudescope --port <n>`.
258
- - **Node version errors** — you need Node ≥ 20 (`node -v`).
314
+ - **Node version errors** — you need Node ≥ 22 (`node -v`).
259
315
  - **Stale or wrong data** — delete `~/.claudescope/index.duckdb*` and
260
316
  `claudescope restart` to rebuild the index from scratch.
261
317
  - **`@duckdb/node-api` install issues** — it ships prebuilt native binaries;
package/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __cr } from 'node:module';
3
3
  const require = __cr(import.meta.url);
4
- import{spawn as N,spawnSync as w}from"node:child_process";import{existsSync as y,mkdirSync as x,openSync as H,readFileSync as O,rmSync as P,writeFileSync as U}from"node:fs";import{dirname as F,join as g}from"node:path";import{parseArgs as B}from"node:util";import{fileURLToPath as M}from"node:url";import{copyFileSync as oe,existsSync as $,mkdirSync as ne}from"node:fs";import{homedir as u}from"node:os";import{dirname as L,join as s}from"node:path";import{fileURLToPath as j}from"node:url";var v=L(j(import.meta.url));function D(e,o){return e.find(n=>$(n))??o}var E=Number(process.env.PORT??4317),d=s(v,"..");function A(e){return e==="~"?u():e.startsWith("~/")?s(u(),e.slice(2)):e}var _=A(process.env.CLAUDE_PROJECTS_DIR??s(u(),".claude","projects")),ae=process.env.OPEN_BROWSER==="1",ce=Number(process.env.REINDEX_INTERVAL_MS??15e3),i=A(process.env.CLAUDESCOPE_HOME??s(u(),".claudescope")),le=process.env.DUCKDB_PATH??s(i,"index.duckdb"),pe=D([s(v,"pricing.default.json"),s(d,"pricing.json")],s(d,"pricing.json")),ue=process.env.PRICING_PATH??s(i,"pricing.json"),de=process.env.WEB_DIST_DIR??D([s(v,"web"),s(d,"..","web","dist")],s(d,"..","web","dist")),a="0.1.1";var V=F(M(import.meta.url)),J=g(V,"server.js"),c=g(i,"daemon.json"),f=g(i,"daemon.log"),S=g(i,"update-check.json"),m="@vladar107/claudescope",G=24*60*60*1e3;function h(){if(!y(c))return null;try{return JSON.parse(O(c,"utf8"))}catch{return null}}function p(e){try{return process.kill(e,0),!0}catch{return!1}}async function C(e){try{return(await fetch(`http://127.0.0.1:${e}/api/health`,{signal:AbortSignal.timeout(1500)})).ok}catch{return!1}}async function K(e,o){let n=Date.now()+o;for(;Date.now()<n;){if(await C(e))return!0;process.stdout.write("."),await new Promise(r=>setTimeout(r,500))}return!1}function b(e){let o=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";try{N(o,[e],{stdio:"ignore",detached:!0,shell:process.platform==="win32"}).unref()}catch{}}async function I(e,o){x(i,{recursive:!0});let n=h();if(n&&p(n.pid)&&await C(n.port)){console.log(`\u2713 claudescope is already running \u2192 ${n.url}`),o&&b(n.url);return}n&&!p(n.pid)&&P(c,{force:!0});let r=`http://localhost:${e}`,t=H(f,"a"),l=N(process.execPath,[J],{detached:!0,stdio:["ignore",t,t],env:{...process.env,PORT:String(e),OPEN_BROWSER:"0"}});if(l.unref(),U(c,JSON.stringify({pid:l.pid,port:e,url:r,version:a,startedAt:new Date().toISOString()},null,2)),process.stdout.write("\u203A Starting claudescope"),!await K(e,2e4)){console.error(`
4
+ import{spawn as N,spawnSync as y}from"node:child_process";import{existsSync as O,mkdirSync as $,openSync as F,readFileSync as C,rmSync as b,writeFileSync as x}from"node:fs";import{dirname as M,join as g}from"node:path";import{createInterface as V}from"node:readline/promises";import{parseArgs as J}from"node:util";import{fileURLToPath as G}from"node:url";import{copyFileSync as re,existsSync as j,mkdirSync as se}from"node:fs";import{homedir as p}from"node:os";import{dirname as H,join as s}from"node:path";import{fileURLToPath as B}from"node:url";var v=H(B(import.meta.url));function I(e,t){return e.find(o=>j(o))??t}var S=Number(process.env.PORT??4317),f=s(v,"..");function E(e){return e==="~"?p():e.startsWith("~/")?s(p(),e.slice(2)):e}var _=E(process.env.CLAUDE_PROJECTS_DIR??s(p(),".claude","projects")),le=E(process.env.CODEX_SESSIONS_DIR??s(p(),".codex","sessions")),pe=process.env.OPEN_BROWSER==="1",ue=Number(process.env.REINDEX_INTERVAL_MS??15e3),a=E(process.env.CLAUDESCOPE_HOME??s(p(),".claudescope")),de=process.env.DUCKDB_PATH??s(a,"index.duckdb"),fe=I([s(v,"pricing.default.json"),s(f,"pricing.json")],s(f,"pricing.json")),me=process.env.PRICING_PATH??s(a,"pricing.json"),ge=process.env.WEB_DIST_DIR??I([s(v,"web"),s(f,"..","web","dist")],s(f,"..","web","dist")),i="0.3.0";var W=M(G(import.meta.url)),K=g(W,"server.js"),c=g(a,"daemon.json"),m=g(a,"daemon.log"),w=g(a,"update-check.json"),u="@vladar107/claudescope",X=24*60*60*1e3;function h(){if(!O(c))return null;try{return JSON.parse(C(c,"utf8"))}catch{return null}}function d(e){try{return process.kill(e,0),!0}catch{return!1}}async function D(e){try{return(await fetch(`http://127.0.0.1:${e}/api/health`,{signal:AbortSignal.timeout(1500)})).ok}catch{return!1}}async function q(e,t){let o=Date.now()+t;for(;Date.now()<o;){if(await D(e))return!0;process.stdout.write("."),await new Promise(r=>setTimeout(r,500))}return!1}function P(e){let t=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";try{N(t,[e],{stdio:"ignore",detached:!0,shell:process.platform==="win32"}).unref()}catch{}}async function A(e,t){$(a,{recursive:!0});let o=h();if(o&&d(o.pid)&&await D(o.port)){console.log(`\u2713 claudescope is already running \u2192 ${o.url}`),t&&P(o.url);return}o&&!d(o.pid)&&b(c,{force:!0});let r=`http://localhost:${e}`,n=F(m,"a"),l=N(process.execPath,[K],{detached:!0,stdio:["ignore",n,n],env:{...process.env,PORT:String(e),OPEN_BROWSER:"0"}});if(l.unref(),x(c,JSON.stringify({pid:l.pid,port:e,url:r,version:i,startedAt:new Date().toISOString()},null,2)),process.stdout.write("\u203A Starting claudescope"),!await q(e,2e4)){console.error(`
5
5
  \u2717 Server did not become healthy in time. Inspect: claudescope logs`),process.exitCode=1;return}console.log(`
6
- \u2713 claudescope running \u2192 ${r}`),console.log(` Sessions: ${_} (read-only)`),o&&b(r),await k(!1)}function R(){let e=h();if(!e||!p(e.pid)){console.log("claudescope is not running."),P(c,{force:!0});return}try{process.kill(e.pid,"SIGTERM")}catch{}P(c,{force:!0}),console.log(`\u2713 Stopped claudescope (pid ${e.pid}).`)}async function W(){let e=h();e&&p(e.pid)&&await C(e.port)?console.log(`\u25CF running ${e.url} (pid ${e.pid}, v${e.version})`):console.log(`\u25CB stopped (installed v${a})`),await k(!0)}function X(){let e=h();e&&p(e.pid)?b(e.url):console.log("claudescope is not running. Start it with: claudescope start")}function Y(e){if(!y(f)){console.log("No logs yet.");return}e&&process.platform!=="win32"?w("tail",["-f",f],{stdio:"inherit"}):process.stdout.write(O(f,"utf8"))}function q(){console.log(`\u203A Updating ${m} to the latest version\u2026`);let e=process.platform==="win32"?"npm.cmd":"npm";if(w(e,["install","-g",`${m}@latest`],{stdio:"inherit"}).status!==0){console.error(`\u2717 Update failed. If you run via npx, just re-run \`npx ${m}\` to get the latest.`),process.exitCode=1;return}R(),console.log("\u2713 Updated. Restarting\u2026"),w("claudescope",["start"],{stdio:"inherit",shell:process.platform==="win32"})}function z(e,o){let n=e.split(".").map(t=>Number.parseInt(t,10)||0),r=o.split(".").map(t=>Number.parseInt(t,10)||0);for(let t=0;t<3;t++){if((n[t]??0)>(r[t]??0))return!0;if((n[t]??0)<(r[t]??0))return!1}return!1}async function Q(e){let o=Date.now();if(!e&&y(S))try{let l=JSON.parse(O(S,"utf8"));if(o-l.lastCheck<G)return l.latest}catch{}let n=`https://registry.npmjs.org/${m.replace("/","%2f")}/latest`,r=await fetch(n,{signal:AbortSignal.timeout(2500)});if(!r.ok)return null;let t=await r.json();return t.version?(x(i,{recursive:!0}),U(S,JSON.stringify({lastCheck:o,latest:t.version})),t.version):null}async function k(e){try{let o=await Q(e);o&&z(o,a)&&console.log(`
7
- \u2B06 Update available: v${a} \u2192 v${o}. Run: claudescope update`)}catch{}}function T(){console.log(`claudescope v${a} \u2014 local viewer for Claude Code transcripts
6
+ \u2713 claudescope running \u2192 ${r}`),console.log(` Sessions: ${_} (read-only)`),t&&P(r),await L(!1)}function R(){let e=h();if(!e||!d(e.pid)){console.log("claudescope is not running."),b(c,{force:!0});return}try{process.kill(e.pid,"SIGTERM")}catch{}b(c,{force:!0}),console.log(`\u2713 Stopped claudescope (pid ${e.pid}).`)}async function Y(){let e=h();e&&d(e.pid)&&await D(e.port)?console.log(`\u25CF running ${e.url} (pid ${e.pid}, v${e.version})`):console.log(`\u25CB stopped (installed v${i})`),await L(!0)}function z(){let e=h();e&&d(e.pid)?P(e.url):console.log("claudescope is not running. Start it with: claudescope start")}function Q(e){if(!O(m)){console.log("No logs yet.");return}e&&process.platform!=="win32"?y("tail",["-f",m],{stdio:"inherit"}):process.stdout.write(C(m,"utf8"))}async function Z(e,t){if(!process.stdin.isTTY)return t;let o=V({input:process.stdin,output:process.stdout});try{let r=(await o.question(`${e} ${t?"[Y/n]":"[y/N]"} `)).trim().toLowerCase();return r?r==="y"||r==="yes":t}finally{o.close()}}async function ee(e){let t=await U(!0);if(t&&!k(t,i)){console.log(`\u2713 Already on the latest version (v${i}).`);return}t||console.log("\u26A0 Could not reach the npm registry to confirm the latest version.");let o=t?`v${i} \u2192 v${t}`:`v${i} \u2192 latest`;if(console.log(`\u203A Will run: npm install -g ${u}@latest (${o})`),!e&&!await Z("Proceed?",!0)){console.log("Aborted.");return}console.log(`\u203A Updating ${u}\u2026`);let r=process.platform==="win32"?"npm.cmd":"npm";if(y(r,["install","-g",`${u}@latest`],{stdio:"inherit"}).status!==0){console.error(`\u2717 Update failed. If you run via npx, just re-run \`npx ${u}\` to get the latest.`),process.exitCode=1;return}R(),console.log("\u2713 Updated. Restarting\u2026"),y("claudescope",["start"],{stdio:"inherit",shell:process.platform==="win32"})}function k(e,t){let o=e.split(".").map(n=>Number.parseInt(n,10)||0),r=t.split(".").map(n=>Number.parseInt(n,10)||0);for(let n=0;n<3;n++){if((o[n]??0)>(r[n]??0))return!0;if((o[n]??0)<(r[n]??0))return!1}return!1}async function U(e){let t=Date.now();if(!e&&O(w))try{let l=JSON.parse(C(w,"utf8"));if(t-l.lastCheck<X)return l.latest}catch{}let o=`https://registry.npmjs.org/${u.replace("/","%2f")}/latest`,r=await fetch(o,{signal:AbortSignal.timeout(2500)});if(!r.ok)return null;let n=await r.json();return n.version?($(a,{recursive:!0}),x(w,JSON.stringify({lastCheck:t,latest:n.version})),n.version):null}async function L(e){try{let t=await U(e);t&&k(t,i)&&console.log(`
7
+ \u2B06 Update available: v${i} \u2192 v${t}. Run: claudescope update`)}catch{}}function T(){console.log(`claudescope v${i} \u2014 local viewer for Claude Code transcripts
8
8
 
9
9
  Usage: claudescope [command] [options]
10
10
 
@@ -15,15 +15,16 @@ Commands:
15
15
  status Show whether the server is running and if an update exists
16
16
  open Open the running app in your browser
17
17
  logs [-f] Print the server log (-f / --follow to tail it)
18
- update Upgrade to the latest published version and restart
18
+ update [-y] Upgrade to the latest published version and restart
19
19
  help Show this help
20
20
  version Print the installed version
21
21
 
22
22
  Options:
23
- --port <n> Port to listen on (default ${E}, or $PORT)
23
+ --port <n> Port to listen on (default ${S}, or $PORT)
24
24
  --no-open Don't open the browser on start
25
+ -y, --yes Skip the confirmation prompt (for \`update\`)
25
26
 
26
- State (index, pricing, logs, PID) lives in ${i}
27
+ State (index, pricing, logs, PID) lives in ${a}
27
28
  (override with $CLAUDESCOPE_HOME). Sessions are read from
28
- ${_} (override with $CLAUDE_PROJECTS_DIR).`)}async function Z(){let{values:e,positionals:o}=B({allowPositionals:!0,options:{port:{type:"string"},follow:{type:"boolean",short:"f"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"},"no-open":{type:"boolean"}}}),n=e.port?Number(e.port):E,r=!e["no-open"],t=o[0];switch(t||(t=e.help?"help":e.version?"version":"start"),t){case"start":await I(n,r);break;case"stop":R();break;case"restart":R(),await I(n,r);break;case"status":await W();break;case"open":X();break;case"logs":Y(!!e.follow);break;case"update":q();break;case"version":console.log(a);break;case"help":T();break;default:console.error(`Unknown command: ${t}
29
- `),T(),process.exitCode=1}}Z().catch(e=>{console.error(e),process.exit(1)});
29
+ ${_} (override with $CLAUDE_PROJECTS_DIR).`)}async function te(){let{values:e,positionals:t}=J({allowPositionals:!0,options:{port:{type:"string"},follow:{type:"boolean",short:"f"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"},yes:{type:"boolean",short:"y"},"no-open":{type:"boolean"}}}),o=e.port?Number(e.port):S,r=!e["no-open"],n=t[0];switch(n||(n=e.help?"help":e.version?"version":"start"),n){case"start":await A(o,r);break;case"stop":R();break;case"restart":R(),await A(o,r);break;case"status":await Y();break;case"open":z();break;case"logs":Q(!!e.follow);break;case"update":await ee(!!e.yes);break;case"version":console.log(i);break;case"help":T();break;default:console.error(`Unknown command: ${n}
30
+ `),T(),process.exitCode=1}}te().catch(e=>{console.error(e),process.exit(1)});
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@vladar107/claudescope",
3
- "version": "0.1.1",
4
- "description": "Local, read-only web app to browse, read, search, and analyze your Claude Code session transcripts.",
3
+ "version": "0.3.0",
4
+ "description": "Local, read-only web app to browse, read, search, and analyze your AI coding-agent transcripts — Claude Code and OpenAI Codex.",
5
5
  "keywords": [
6
6
  "claude",
7
7
  "claude-code",
8
8
  "anthropic",
9
+ "codex",
10
+ "openai",
9
11
  "transcripts",
10
12
  "sessions",
11
13
  "viewer",
@@ -32,7 +34,7 @@
32
34
  "README.md"
33
35
  ],
34
36
  "engines": {
35
- "node": ">=20"
37
+ "node": ">=22"
36
38
  },
37
39
  "dependencies": {
38
40
  "@duckdb/node-api": "^1.5.3-r.3"
@@ -2,12 +2,17 @@
2
2
  "models": {
3
3
  "claude-opus-4-1": { "input": 15, "output": 75, "cacheWrite": 18.75, "cacheRead": 1.5 },
4
4
  "claude-opus-4": { "input": 15, "output": 75, "cacheWrite": 18.75, "cacheRead": 1.5 },
5
- "<synthetic>": { "input": 0, "output": 0, "cacheWrite": 0, "cacheRead": 0 }
5
+ "<synthetic>": { "input": 0, "output": 0, "cacheWrite": 0, "cacheRead": 0 },
6
+ "gpt-5": { "input": 0.625, "output": 5, "cacheWrite": 0, "cacheRead": 0.125 },
7
+ "gpt-5.4": { "input": 2.5, "output": 15, "cacheWrite": 0, "cacheRead": 0.5 },
8
+ "gpt-5.5": { "input": 5, "output": 30, "cacheWrite": 0, "cacheRead": 0.5 },
9
+ "gpt-5.5-pro": { "input": 30, "output": 180, "cacheWrite": 0, "cacheRead": 30 }
6
10
  },
7
11
  "families": {
8
12
  "opus": { "input": 5, "output": 25, "cacheWrite": 6.25, "cacheRead": 0.5 },
9
13
  "sonnet": { "input": 3, "output": 15, "cacheWrite": 3.75, "cacheRead": 0.3 },
10
- "haiku": { "input": 1, "output": 5, "cacheWrite": 1.25, "cacheRead": 0.1 }
14
+ "haiku": { "input": 1, "output": 5, "cacheWrite": 1.25, "cacheRead": 0.1 },
15
+ "gpt": { "input": 2.5, "output": 15, "cacheWrite": 0, "cacheRead": 0.5 }
11
16
  },
12
17
  "default": { "input": 3, "output": 15, "cacheWrite": 3.75, "cacheRead": 0.3 }
13
18
  }