@vladar107/claudescope 0.4.1 → 0.5.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 +40 -24
- package/cli.js +11 -6
- package/package.json +1 -1
- package/pricing.default.json +1 -0
- package/server.js +160 -152
- package/web/assets/{index-CFGvQhPn.css → index-BolFDI5c.css} +1 -1
- package/web/assets/index-CcTODX7G.js +200 -0
- package/web/assets/index-CfGAFdVQ.js +149 -0
- package/web/index.html +2 -2
- package/web/assets/index-Cd1b_MtY.js +0 -199
- package/web/assets/index-lqhFCxBS.js +0 -13
package/README.md
CHANGED
|
@@ -38,10 +38,11 @@ Claudescope works whether you use one agent or all three. Adding another is just
|
|
|
38
38
|
|
|
39
39
|
> **Privacy:** Everything runs locally on `127.0.0.1`. The app **never** writes to
|
|
40
40
|
> `~/.claude`, `~/.codex`, or `~/.junie` — all are read-only sources. Its only persistent
|
|
41
|
-
> state lives in `~/.claudescope/` — a DuckDB index
|
|
42
|
-
>
|
|
43
|
-
> daily check for a newer published version
|
|
44
|
-
>
|
|
41
|
+
> state lives in `~/.claudescope/` — a DuckDB index, a copy of the pricing file, and
|
|
42
|
+
> a cached pricing snapshot (`pricing.fetched.json`), all safe to delete anytime. The
|
|
43
|
+
> sole outbound requests are an optional daily check for a newer published version
|
|
44
|
+
> (`claudescope update`) and an optional daily pricing refresh (`claudescope pricing
|
|
45
|
+
> update`); nothing about your transcripts ever leaves your machine.
|
|
45
46
|
|
|
46
47
|
---
|
|
47
48
|
|
|
@@ -139,8 +140,9 @@ claudescope restart # restart it
|
|
|
139
140
|
claudescope status # is it running? is an update available?
|
|
140
141
|
claudescope open # open the running app in your browser
|
|
141
142
|
claudescope logs -f # tail the server log
|
|
142
|
-
claudescope update
|
|
143
|
-
claudescope
|
|
143
|
+
claudescope update # upgrade to the latest published version and restart
|
|
144
|
+
claudescope pricing update # fetch current model prices (LiteLLM) into the local rate table
|
|
145
|
+
claudescope help # full usage
|
|
144
146
|
|
|
145
147
|
# options: --port <n> (default 4317, or $PORT)
|
|
146
148
|
# --no-open (don't open the browser on start)
|
|
@@ -205,13 +207,24 @@ cost = ( input_tokens × input_rate
|
|
|
205
207
|
The per-event cost is computed once at index time and stored, so analytics is
|
|
206
208
|
just a `SUM` over events; a project/session total is the sum of its events.
|
|
207
209
|
|
|
208
|
-
Rates
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
Rates are resolved in a layered lookup:
|
|
211
|
+
|
|
212
|
+
1. **Fetched exact id** — `~/.claudescope/pricing.fetched.json` (auto-refreshed
|
|
213
|
+
daily from [LiteLLM's community price table](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json),
|
|
214
|
+
covering Anthropic, OpenAI, Gemini, xAI, Mistral, and DeepSeek models).
|
|
215
|
+
2. **Local exact id** — `~/.claudescope/pricing.json` (seeded on first run from
|
|
216
|
+
the shipped default; user-editable; takes precedence over the fetched snapshot
|
|
217
|
+
for any id it defines explicitly).
|
|
218
|
+
3. **Family match** — `opus` / `sonnet` / `haiku` / `gemini` / `gpt` substring in
|
|
219
|
+
the model id → the matching family rate from `pricing.json`.
|
|
220
|
+
4. **Default** — the `default` entry in `pricing.json`.
|
|
221
|
+
|
|
222
|
+
The family step means version- or date-suffixed ids (e.g. `claude-haiku-4-5-20251001`,
|
|
223
|
+
`gpt-5.x-codex`, `gemini-2.5-flash`) still price correctly. `pricing.json` is the
|
|
224
|
+
user-editable fallback and override layer for families and the default rate; the
|
|
225
|
+
fetched snapshot provides exact per-model rates for all known models.
|
|
226
|
+
|
|
227
|
+
Shipped fallback rates (USD per 1M tokens):
|
|
215
228
|
|
|
216
229
|
| family / model | input | output | cache write (5m) | cache read |
|
|
217
230
|
| ------------------- | ----- | ------ | ---------------- | ---------- |
|
|
@@ -219,20 +232,21 @@ Shipped rates (USD per 1M tokens, from Anthropic and OpenAI published API pricin
|
|
|
219
232
|
| Opus 4.1 / 4 | $15 | $75 | $18.75 | $1.50 |
|
|
220
233
|
| Sonnet 4.x | $3 | $15 | $3.75 | $0.30 |
|
|
221
234
|
| Haiku 4.5 | $1 | $5 | $1.25 | $0.10 |
|
|
235
|
+
| Gemini 2.5 Pro-class| $1.25 | $10 | — | $0.31 |
|
|
222
236
|
| GPT-5 | $0.63 | $5 | — | $0.13 |
|
|
223
237
|
| GPT-5.4 | $2.50 | $15 | — | $0.50 |
|
|
224
238
|
| GPT-5.5 | $5 | $30 | — | $0.50 |
|
|
225
239
|
| `<synthetic>` | $0 | $0 | $0 | $0 |
|
|
226
240
|
|
|
227
|
-
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
- The `opus`/`sonnet`/`haiku`/`gpt` family rules use **current**
|
|
234
|
-
deprecated Opus 4 / 4.1 ($15/$75) and specific GPT-5 versions are
|
|
235
|
-
exact `models` entries. Add an exact entry to override any
|
|
241
|
+
- Rates **auto-refresh daily** in the background while the server runs. Run
|
|
242
|
+
`claudescope pricing update` to force a refresh at any time. New rates apply
|
|
243
|
+
to newly indexed events; existing indexed costs are unchanged.
|
|
244
|
+
- Edit `~/.claudescope/pricing.json` to override families, the default rate, or
|
|
245
|
+
pin specific model prices. Re-index (`POST /api/reindex` or `claudescope
|
|
246
|
+
restart`) to recompute stored costs at the new rates.
|
|
247
|
+
- The `opus`/`sonnet`/`haiku`/`gemini`/`gpt` family rules use **current**
|
|
248
|
+
pricing; the deprecated Opus 4 / 4.1 ($15/$75) and specific GPT-5 versions are
|
|
249
|
+
pinned via exact `models` entries. Add an exact entry to override any model.
|
|
236
250
|
|
|
237
251
|
> **Caveat:** these are **list-price estimates** — they ignore any discounts,
|
|
238
252
|
> service tier, or batch pricing, and the cache-write rate assumes the 5-minute
|
|
@@ -332,8 +346,10 @@ bundles, and publishes. Auth uses npm **Trusted Publishing** (OIDC) — no
|
|
|
332
346
|
## Security & privacy
|
|
333
347
|
|
|
334
348
|
Claudescope runs entirely on your machine. It treats `~/.claude`, `~/.codex`, and
|
|
335
|
-
`~/.junie` as **read-only**, **binds to `127.0.0.1` only**, sends **no telemetry
|
|
336
|
-
|
|
349
|
+
`~/.junie` as **read-only**, **binds to `127.0.0.1` only**, and sends **no telemetry**. Its only
|
|
350
|
+
outbound requests are a cached npm-registry version check for the update notice
|
|
351
|
+
and a daily fetch of public model pricing rates from LiteLLM (disable with
|
|
352
|
+
`PRICING_REFRESH_INTERVAL_MS=0`). See
|
|
337
353
|
[`SECURITY.md`](./SECURITY.md) for the full breakdown of filesystem, network,
|
|
338
354
|
shell, and self-update behavior — and how to report a vulnerability.
|
|
339
355
|
|
package/cli.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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
|
|
4
|
+
import{spawn as G,spawnSync as I}from"node:child_process";import{existsSync as D,mkdirSync as B,openSync as ue,readFileSync as N,realpathSync as de,rmSync as C,writeFileSync as J}from"node:fs";import{dirname as fe,join as E}from"node:path";import{createInterface as me}from"node:readline/promises";import{parseArgs as ge}from"node:util";import{fileURLToPath as he}from"node:url";import{copyFileSync as ke,existsSync as X,mkdirSync as De}from"node:fs";import{homedir as l}from"node:os";import{dirname as Y,join as i}from"node:path";import{fileURLToPath as q}from"node:url";var R=Y(q(import.meta.url));function T(e,t){return e.find(n=>X(n))??t}var v=Number(process.env.PORT??4317),h=i(R,"..");function _(e){return e==="~"?l():e.startsWith("~/")?i(l(),e.slice(2)):e}var b=_(process.env.CLAUDE_PROJECTS_DIR??i(l(),".claude","projects")),Te=_(process.env.CODEX_SESSIONS_DIR??i(l(),".codex","sessions")),$e=_(process.env.JUNIE_SESSIONS_DIR??i(l(),".junie","sessions")),Me=process.env.OPEN_BROWSER==="1",Ue=Number(process.env.REINDEX_INTERVAL_MS??15e3),a=_(process.env.CLAUDESCOPE_HOME??i(l(),".claudescope")),Fe=process.env.DUCKDB_PATH??i(a,"index.duckdb"),He=T([i(R,"pricing.default.json"),i(h,"pricing.json")],i(h,"pricing.json")),je=process.env.PRICING_PATH??i(a,"pricing.json"),p=process.env.FETCHED_PRICING_PATH??i(a,"pricing.fetched.json"),$=process.env.LITELLM_PRICING_URL??"https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",Ge=Number(process.env.PRICING_REFRESH_INTERVAL_MS??24*60*60*1e3),Be=process.env.WEB_DIST_DIR??T([i(R,"web"),i(h,"..","web","dist")],i(h,"..","web","dist")),c="0.5.0";import{mkdirSync as z,readFileSync as Q,renameSync as Z,writeFileSync as ee}from"node:fs";import{dirname as M,join as te}from"node:path";var oe=new Set(["anthropic","openai","gemini","xai","mistral","deepseek"]),ne=new Set(["chat","responses"]),re=1e6,se=1e4,ie=3e4;function y(e){if(typeof e!="number"||!Number.isFinite(e)||e<0)return null;let t=e*re;return t<=se?t:null}function U(e){return e==null?0:y(e)}function ce(e){let t={};if(typeof e!="object"||e===null)return t;for(let[n,r]of Object.entries(e)){if(n==="sample_spec"||n.includes("/")||typeof r!="object"||r===null)continue;let o=r;if(typeof o.litellm_provider!="string"||!oe.has(o.litellm_provider)||typeof o.mode!="string"||!ne.has(o.mode))continue;let s=y(o.input_cost_per_token),d=y(o.output_cost_per_token),f=U(o.cache_creation_input_token_cost),A=U(o.cache_read_input_token_cost);s===null||d===null||f===null||A===null||(t[n]={input:s,output:d,cacheWrite:f,cacheRead:A})}return t}function ae(e){let t=Object.keys(e),n=t.some(o=>o.startsWith("claude")),r=t.some(o=>/^(gpt|o\d|chatgpt|codex)/.test(o));if(!n||!r){let o=[!n&&"anthropic",!r&&"openai"].filter(Boolean).join(", ");throw new Error(`Fetched pricing failed validation: no ${o} model survived mapping (${t.length} model(s) total). LiteLLM schema may have drifted.`)}}function le(e){try{return JSON.parse(Q(e,"utf8"))?.models??{}}catch{return{}}}function pe(e,t){let n=0;for(let[r,o]of Object.entries(e)){let s=t[r];(!s||s.input!==o.input||s.output!==o.output||s.cacheWrite!==o.cacheWrite||s.cacheRead!==o.cacheRead)&&(n+=1)}return n}async function F(){let e=await fetch($,{signal:AbortSignal.timeout(ie)});if(!e.ok)throw new Error(`Pricing fetch failed: ${e.status} ${e.statusText}`);let t=await e.json(),n=ce(t);ae(n);let r=le(p),o=pe(n,r),s=new Date().toISOString(),d={fetchedAt:s,models:n};z(M(p),{recursive:!0});let f=te(M(p),`.pricing.fetched.${process.pid}.tmp`);return ee(f,`${JSON.stringify(d,null,2)}
|
|
5
|
+
`),Z(f,p),{fetchedAt:s,modelCount:Object.keys(n).length,changed:o,path:p}}var O=fe(he(import.meta.url)),_e=E(O,"server.js"),u=E(a,"daemon.json"),w=E(a,"daemon.log"),P=E(a,"update-check.json"),m="@vladar107/claudescope",we=24*60*60*1e3;function S(){if(!D(u))return null;try{return JSON.parse(N(u,"utf8"))}catch{return null}}function g(e){try{return process.kill(e,0),!0}catch{return!1}}async function x(e){try{return(await fetch(`http://127.0.0.1:${e}/api/health`,{signal:AbortSignal.timeout(1500)})).ok}catch{return!1}}async function Ee(e,t){let n=Date.now()+t;for(;Date.now()<n;){if(await x(e))return!0;process.stdout.write("."),await new Promise(r=>setTimeout(r,500))}return!1}function L(e){let t=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";try{G(t,[e],{stdio:"ignore",detached:!0,shell:process.platform==="win32"}).unref()}catch{}}async function H(e,t){B(a,{recursive:!0});let n=S();if(n&&g(n.pid)&&await x(n.port)){console.log(`\u2713 claudescope is already running \u2192 ${n.url}`),t&&L(n.url);return}n&&!g(n.pid)&&C(u,{force:!0});let r=`http://localhost:${e}`,o=ue(w,"a"),s=G(process.execPath,[_e],{detached:!0,stdio:["ignore",o,o],env:{...process.env,PORT:String(e),OPEN_BROWSER:"0"}});if(s.unref(),J(u,JSON.stringify({pid:s.pid,port:e,url:r,version:c,startedAt:new Date().toISOString()},null,2)),process.stdout.write("\u203A Starting claudescope"),!await Ee(e,2e4)){console.error(`
|
|
5
6
|
\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: ${
|
|
7
|
-
\u2B06 Update available: v${
|
|
7
|
+
\u2713 claudescope running \u2192 ${r}`),console.log(` Sessions: ${b} (read-only)`),t&&L(r),await K(!1)}function k(){let e=S();if(!e||!g(e.pid)){console.log("claudescope is not running."),C(u,{force:!0});return}try{process.kill(e.pid,"SIGTERM")}catch{}C(u,{force:!0}),console.log(`\u2713 Stopped claudescope (pid ${e.pid}).`)}async function Se(){let e=S();e&&g(e.pid)&&await x(e.port)?console.log(`\u25CF running ${e.url} (pid ${e.pid}, v${e.version})`):console.log(`\u25CB stopped (installed v${c})`),await K(!0)}function Re(){let e=S();e&&g(e.pid)?L(e.url):console.log("claudescope is not running. Start it with: claudescope start")}function ve(e){if(!D(w)){console.log("No logs yet.");return}e&&process.platform!=="win32"?I("tail",["-f",w],{stdio:"inherit"}):process.stdout.write(N(w,"utf8"))}async function be(e,t){if(!process.stdin.isTTY)return t;let n=me({input:process.stdin,output:process.stdout});try{let r=(await n.question(`${e} ${t?"[Y/n]":"[y/N]"} `)).trim().toLowerCase();return r?r==="y"||r==="yes":t}finally{n.close()}}function ye(){let e=O;try{e=de(O)}catch{}return e.includes("/nix/store/")?"nix":/[\\/]Cellar[\\/]claudescope[\\/]/.test(e)?"brew":"npm"}async function Pe(e){let t=await W(!0);if(t&&!V(t,c)){console.log(`\u2713 Already on the latest version (v${c}).`);return}let n=ye();if(n==="brew"){console.log("claudescope was installed via Homebrew."),console.log(" Run: brew upgrade vladar107/tap/claudescope");return}if(n==="nix"){console.log("claudescope was installed via Nix."),console.log(" Run: nix profile upgrade claudescope"),console.log(" (flake users: re-run `nix run --refresh github:vladar107/claudescope`)");return}t||console.log("\u26A0 Could not reach the npm registry to confirm the latest version.");let r=t?`v${c} \u2192 v${t}`:`v${c} \u2192 latest`;if(console.log(`\u203A Will run: npm install -g ${m}@latest (${r})`),!e&&!await be("Proceed?",!0)){console.log("Aborted.");return}console.log(`\u203A Updating ${m}\u2026`);let o=process.platform==="win32"?"npm.cmd":"npm";if(I(o,["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}k(),console.log("\u2713 Updated. Restarting\u2026"),I("claudescope",["start"],{stdio:"inherit",shell:process.platform==="win32"})}function V(e,t){let n=e.split(".").map(o=>Number.parseInt(o,10)||0),r=t.split(".").map(o=>Number.parseInt(o,10)||0);for(let o=0;o<3;o++){if((n[o]??0)>(r[o]??0))return!0;if((n[o]??0)<(r[o]??0))return!1}return!1}async function W(e){let t=Date.now();if(!e&&D(P))try{let s=JSON.parse(N(P,"utf8"));if(t-s.lastCheck<we)return s.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 o=await r.json();return o.version?(B(a,{recursive:!0}),J(P,JSON.stringify({lastCheck:t,latest:o.version})),o.version):null}async function K(e){try{let t=await W(e);t&&V(t,c)&&console.log(`
|
|
8
|
+
\u2B06 Update available: v${c} \u2192 v${t}. Run: claudescope update`)}catch{}}async function Ie(){try{let{modelCount:e,changed:t,path:n}=await F();console.log(`\u2713 Pricing updated: ${e} models (${t} changed) \u2192 ${n}`),console.log(" A running server picks up the new rates automatically (newly indexed events).")}catch(e){console.error(`\u2717 Pricing update failed: ${e instanceof Error?e.message:String(e)}`),console.error(" Existing rates are unchanged."),process.exitCode=1}}function Ce(){console.log(`Usage: claudescope pricing <subcommand>
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
update Fetch current model prices (LiteLLM) into the local rate table`)}function j(){console.log(`claudescope v${c} \u2014 local viewer for Claude Code transcripts
|
|
8
12
|
|
|
9
13
|
Usage: claudescope [command] [options]
|
|
10
14
|
|
|
@@ -16,15 +20,16 @@ Commands:
|
|
|
16
20
|
open Open the running app in your browser
|
|
17
21
|
logs [-f] Print the server log (-f / --follow to tail it)
|
|
18
22
|
update [-y] Upgrade to the latest published version and restart
|
|
23
|
+
pricing update Fetch current model prices (LiteLLM) into the local rate table
|
|
19
24
|
help Show this help
|
|
20
25
|
version Print the installed version
|
|
21
26
|
|
|
22
27
|
Options:
|
|
23
|
-
--port <n> Port to listen on (default ${
|
|
28
|
+
--port <n> Port to listen on (default ${v}, or $PORT)
|
|
24
29
|
--no-open Don't open the browser on start
|
|
25
30
|
-y, --yes Skip the confirmation prompt (for \`update\`)
|
|
26
31
|
|
|
27
32
|
State (index, pricing, logs, PID) lives in ${a}
|
|
28
33
|
(override with $CLAUDESCOPE_HOME). Sessions are read from
|
|
29
|
-
${
|
|
30
|
-
`),
|
|
34
|
+
${b} (override with $CLAUDE_PROJECTS_DIR).`)}async function Oe(){let{values:e,positionals:t}=ge({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"}}}),n=e.port?Number(e.port):v,r=!e["no-open"],o=t[0];switch(o||(o=e.help?"help":e.version?"version":"start"),o){case"start":await H(n,r);break;case"stop":k();break;case"restart":k(),await H(n,r);break;case"status":await Se();break;case"open":Re();break;case"logs":ve(!!e.follow);break;case"update":await Pe(!!e.yes);break;case"pricing":{t[1]==="update"?await Ie():Ce();break}case"version":console.log(c);break;case"help":j();break;default:console.error(`Unknown command: ${o}
|
|
35
|
+
`),j(),process.exitCode=1}}Oe().catch(e=>{console.error(e),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vladar107/claudescope",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local, read-only web app to browse, read, search, and analyze your AI coding-agent transcripts — Claude Code, OpenAI Codex, and JetBrains Junie.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/pricing.default.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"opus": { "input": 5, "output": 25, "cacheWrite": 6.25, "cacheRead": 0.5 },
|
|
13
13
|
"sonnet": { "input": 3, "output": 15, "cacheWrite": 3.75, "cacheRead": 0.3 },
|
|
14
14
|
"haiku": { "input": 1, "output": 5, "cacheWrite": 1.25, "cacheRead": 0.1 },
|
|
15
|
+
"gemini": { "input": 1.25, "output": 10, "cacheWrite": 0, "cacheRead": 0.31 },
|
|
15
16
|
"gpt": { "input": 2.5, "output": 15, "cacheWrite": 0, "cacheRead": 0.5 }
|
|
16
17
|
},
|
|
17
18
|
"default": { "input": 3, "output": 15, "cacheWrite": 3.75, "cacheRead": 0.3 }
|