@subvalue/cli 0.1.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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/PRE_RELEASE.md +51 -0
  3. package/PRIVACY.md +45 -0
  4. package/README.md +119 -0
  5. package/dist/PRICING.md +46 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/packages/cli/src/index.js +24 -0
  8. package/dist/packages/cli/src/runtime.js +22 -0
  9. package/dist/packages/cli/src/server.js +75 -0
  10. package/dist/packages/core/src/demo.js +10 -0
  11. package/dist/packages/core/src/metadata.js +14 -0
  12. package/dist/packages/core/src/pricing/current.js +326 -0
  13. package/dist/packages/core/src/pricing/index.js +84 -0
  14. package/dist/packages/core/src/providers/claude/index.js +24 -0
  15. package/dist/packages/core/src/providers/codex/index.js +51 -0
  16. package/dist/packages/core/src/scanner.js +48 -0
  17. package/dist/packages/core/src/security.js +41 -0
  18. package/dist/packages/core/src/storage.js +63 -0
  19. package/dist/packages/core/src/subscriptions.js +22 -0
  20. package/dist/packages/core/src/summary.js +55 -0
  21. package/dist/packages/core/src/types.js +29 -0
  22. package/dist/packages/receipt/src/index.js +90 -0
  23. package/dist/packages/ui/src/brand.js +5 -0
  24. package/dist/public/app.css +65 -0
  25. package/dist/public/app.js +1 -0
  26. package/dist/public/apps/web/src/landing.js +10 -0
  27. package/dist/public/apps/web/src/main.js +47 -0
  28. package/dist/public/brands/SOURCES.txt +14 -0
  29. package/dist/public/brands/anthropic-symbol-ivory.svg +4 -0
  30. package/dist/public/brands/claude-code-clawd.svg +3 -0
  31. package/dist/public/brands/claude-code-ivory.svg +28 -0
  32. package/dist/public/brands/codex-outline.svg +5 -0
  33. package/dist/public/brands/openai-blossom-white.svg +15 -0
  34. package/dist/public/favicon.svg +1 -0
  35. package/dist/public/fonts/Inter-LICENSE.txt +93 -0
  36. package/dist/public/fonts/Plex-LICENSE.txt +93 -0
  37. package/dist/public/fonts/inter-latin.woff2 +0 -0
  38. package/dist/public/fonts/plex-mono.woff2 +0 -0
  39. package/dist/public/icon.svg +1 -0
  40. package/dist/public/index.html +1 -0
  41. package/dist/public/landing.css +82 -0
  42. package/dist/public/landing.html +1 -0
  43. package/dist/public/landing.js +1 -0
  44. package/dist/public/logo/app-icon.svg +1 -0
  45. package/dist/public/logo/mark-green.svg +1 -0
  46. package/dist/public/logo/mark-monochrome.svg +1 -0
  47. package/dist/public/logo/mark-white.svg +1 -0
  48. package/dist/public/packages/core/src/subscriptions.js +22 -0
  49. package/dist/public/packages/receipt/src/index.js +90 -0
  50. package/dist/public/packages/ui/src/brand.js +5 -0
  51. package/dist/public/privacy.txt +45 -0
  52. package/package.json +7 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SubValue 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/PRE_RELEASE.md ADDED
@@ -0,0 +1,51 @@
1
+ # Package validation
2
+
3
+ The normal installation command is `npx @subvalue/cli`. These instructions validate a locally bundled package independently of the npm registry. No frontend installation or API key is required.
4
+
5
+ ## Get the package
6
+
7
+ Install Node.js **24.13 or newer** on Windows, macOS or Linux. Check `node --version`.
8
+
9
+ With access to the private repository, open the latest successful [Validate run](https://github.com/axelmtn/subvalue/actions/workflows/ci.yml). Download its `subvalue-cli-…` artifact and unzip it. Only `subvalue-cli-0.1.0.tgz` should be inside. The artifact expires after 14 days; a new Validate run regenerates it.
10
+
11
+ Put the `.tgz` in a new folder outside any repository. Open a terminal in that folder.
12
+
13
+ ```sh
14
+ npx --offline --yes --package ./subvalue-cli-0.1.0.tgz subvalue --dry-run
15
+ npx --offline --yes --package ./subvalue-cli-0.1.0.tgz subvalue
16
+ ```
17
+
18
+ The first command checks detection without reading session contents. The second scans real local usage, opens the browser, and serves only `127.0.0.1`. A different loopback port is selected if 4731 is occupied. No demo data is injected.
19
+
20
+ ## Check the app
21
+
22
+ 1. Confirm the detected providers. A detected provider may have no usage in the selected period.
23
+ 2. Enter your billing mode and subscription amount when needed. They are declarations, never inferred from credentials.
24
+ 3. Check Overview, 7D/30D/Month, Receipt and Settings. Pricing coverage describes imported usage, not every request ever made.
25
+ 4. Export PNG. Check readable amounts and the absence of project names, paths, prompts and session IDs.
26
+ 5. Use **Refresh usage** at the top of Overview after new activity. It updates local metadata without restarting the app. Period controls only filter cached data.
27
+ 6. Stop the CLI with Ctrl+C. Relaunch the same command. Unchanged files should reuse the persistent cache.
28
+
29
+ For scan timings and cache counts:
30
+
31
+ ```sh
32
+ npx --offline --yes --package ./subvalue-cli-0.1.0.tgz subvalue --verbose
33
+ ```
34
+
35
+ Default metadata storage:
36
+
37
+ - Windows: `%LOCALAPPDATA%/SubValue/`
38
+ - macOS: `~/Library/Application Support/SubValue/`
39
+ - Linux: `$XDG_DATA_HOME/subvalue/` or `~/.local/share/subvalue/`
40
+
41
+ Do not send source logs, databases, credentials or conversation content when reporting a problem. Report the OS, Node version, safe error text, scan timing and cache counts. Review screenshots before sharing them.
42
+
43
+ ## Build from the private checkout
44
+
45
+ ```sh
46
+ npm ci --ignore-scripts --no-audit --no-fund
47
+ npm run build
48
+ npm pack --workspace @subvalue/cli --ignore-scripts --pack-destination artifacts
49
+ ```
50
+
51
+ Move the resulting `.tgz` to the separate test folder and use the commands above. No npm publication is needed. Automatic validations cover Windows, Linux, macOS Apple Silicon and macOS Intel; a tester should also verify their normal desktop browser opens correctly.
package/PRIVACY.md ADDED
@@ -0,0 +1,45 @@
1
+ # Privacy and data access
2
+
3
+ SubValue processes usage on the user's machine. It has no account, cloud usage database, telemetry SDK or runtime upload endpoint.
4
+
5
+ ## Files read
6
+
7
+ Only these provider sources are opened for usage analysis:
8
+
9
+ - `~/.codex/sessions/**/*.jsonl`
10
+ - `~/.codex/archived_sessions/*.jsonl`
11
+ - `~/.claude/projects/**/*.jsonl`
12
+
13
+ The CLI also reads its own bundled assets and its own local application database. It checks source-directory/file metadata to detect providers and changes. It does not open credentials, `.env` files, shell history, browser profiles, repositories or project paths mentioned by a session. Symlinks and junctions are rejected. `--dry-run` detects directories without opening session contents.
14
+
15
+ Session files colocate counters and conversation text. Complete JSONL lines can therefore pass through memory during a scan; only approved metadata is projected into storage. Prompts, responses, attachments and source code are never persisted or displayed.
16
+
17
+ ## Local metadata retained
18
+
19
+ - Provider, timestamp, raw model identifier and token/cache counters.
20
+ - Service tier, speed and billable tool counts when present, for pricing accuracy.
21
+ - Quality flags, schema and scan diagnostics.
22
+ - Hashed event/source references and a hashed Claude request identity, for deduplication and provenance.
23
+ - File sizes, modification times, hashes of small byte windows and offsets, for incremental scans.
24
+ - Codex thread identity and cumulative-counter state in scan checkpoints, to preserve deduplication and counter resets. This identity is not sent to the dashboard.
25
+ - User-declared billing settings, provider visibility and onboarding state.
26
+
27
+ Normalized records omit session/thread identities and project associations. Checkpoints omit unnecessary session, turn and project fields. Existing normalized caches are migrated locally without re-reading source histories or changing event keys and token totals.
28
+
29
+ The browser receives aggregate totals, daily values, model coverage and safe source statuses. It receives no individual requests, project paths, source filenames or conversation text. Receipt exports contain only the selected period, generated date, provider totals and comparison/coverage.
30
+
31
+ ## Storage and deletion
32
+
33
+ Metadata is stored in SQLite in the OS application-data directory documented in README. It is not encrypted by SubValue; access relies on the user's OS permissions. It is retained for history and incremental scanning. To remove it, stop SubValue and delete only its application-data directory. This does not delete Codex or Claude files.
34
+
35
+ ## Network boundaries
36
+
37
+ The dashboard server binds only to `127.0.0.1`. Host/origin checks and a per-launch token protect local mutations. The local dashboard uses bundled fonts, scripts and assets and makes no external requests. Pricing is bundled, with no online lookup of user activity.
38
+
39
+ Installing or updating with npm/npx contacts the package registry. That installation traffic is separate from usage analysis. Following an external documentation link is an explicit browser action.
40
+
41
+ The public Vercel landing is separate from the local application. It has no usage API, cloud scanner or usage database; its product example is labeled example data. No analytics SDK is included. As the website host, Vercel can process ordinary website connection metadata (such as IP address and requested URL); the landing never receives local usage. See [Vercel's privacy policy](https://vercel.com/legal/privacy-policy).
42
+
43
+ ## Verification
44
+
45
+ Synthetic tests check approved file access, blocked links, discarded conversation fields, cache migration, deduplication, safe browser summaries and receipt privacy. Browser tests verify no external application requests and identical preview/export pixels. These checks are not a claim of an independent security certification.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # SubValue
2
+
3
+ Local, read-only Codex and Claude Code API-equivalent usage analysis. No account, API keys, cloud database, AI calls, uploads, or telemetry.
4
+
5
+ ## Install
6
+
7
+ Requires **Node.js 24.13 or newer** on Windows, macOS or Linux.
8
+
9
+ ```sh
10
+ npx @subvalue/cli --dry-run
11
+ npx @subvalue/cli
12
+ ```
13
+
14
+ The source repository is currently private. For optional installation from a local package, see [Package validation](PRE_RELEASE.md).
15
+
16
+ ## Run locally
17
+
18
+ Requires **Node.js 24.13 or newer**. Runtime and production build have no external dependencies.
19
+
20
+ ```sh
21
+ npm run build
22
+ npm start -- --dry-run
23
+ npm start
24
+ ```
25
+
26
+ The CLI scans both providers, stores normalized metadata in the OS application-data directory (`%LOCALAPPDATA%/SubValue` on Windows, `~/Library/Application Support/SubValue` on macOS, `$XDG_DATA_HOME/subvalue` or `~/.local/share/subvalue` on Linux), binds to `127.0.0.1:4731` (an OS-selected loopback port if occupied), and opens the default browser. It never modifies provider files.
27
+
28
+ ```sh
29
+ npm start -- --no-open
30
+ npm start -- --scan-only
31
+ npm start -- --verbose
32
+ npm run demo
33
+ ```
34
+
35
+ `--demo` is explicitly labeled fixture data, does not scan local providers, and does not create a usage database. `--port` selects the local port. `--dry-run` checks provider-directory metadata only: no session contents, database, or server.
36
+
37
+ ## Interface
38
+
39
+ - **Overview:** 7D / 30D / Month / Custom, API equivalent, user-declared billing, optional subscription comparison, providers and daily known API value.
40
+ - **Receipt:** local canvas preview and PNG export of the exact same pixels. No projects, paths, requests or sessions in exports.
41
+ - **Settings:** Sources, Billing, USD, Privacy / data. Inactive providers stay here, with optional manual inclusion.
42
+
43
+ Month compares the full monthly subscription against usage imported so far. Other ranges use calendar-day proration, including multi-month ranges. Amounts and API prices are USD; no invented exchange rates. Billing declarations do not overwrite historical records or prove their original billing mode. Settings offers bundled, verified ChatGPT/Claude plan presets and a custom amount. Annual presets retain the exact annual price divided by 12, with per-seat plans labeled.
44
+
45
+ ## Source access
46
+
47
+ Only these session inputs are opened by the scanner:
48
+
49
+ ```text
50
+ ~/.codex/sessions/**/*.jsonl
51
+ ~/.codex/archived_sessions/*.jsonl
52
+ ~/.claude/projects/**/*.jsonl
53
+ ```
54
+
55
+ No credentials, auth files, repositories, attachments, clipboard caches, shell history or browser data. The optional Claude stats cache is deliberately not imported in V1 because its history cannot be allocated safely by request/date. Directories referenced by session metadata are never opened. Project identifiers are hashed locally.
56
+
57
+ Transcripts contain conversation text alongside metadata. They must be streamed to extract usage. Conversation text is transient and is never persisted, logged, sent to the browser, or exported. Codex response records are skipped before JSON deserialization; Claude objects are immediately projected onto the metadata allowlist.
58
+
59
+ ## Accuracy and pricing
60
+
61
+ `0`, `UNKNOWN`, `NO DATA`, and partial history are distinct. Null fields are never filled with zero. Each record carries provenance, schema, quality and metadata-only source identity. Token completeness and pricing coverage are separate; percentage priced describes **imported records**, not all historical activity.
62
+
63
+ Codex excludes inherited ordinal prefixes, uses cumulative deltas, suppresses repeated counters, segments counter resets and preserves unallocated totals. Claude deduplicates message/request identities and does not add nested iteration usage. Conflicting duplicates fail closed as incomplete.
64
+
65
+ The versioned catalog supports mappings with confidence and primary-source provenance, half-open effective intervals, cache reads/writes, Claude cache durations, and long-context bands. Rates and release evidence were checked against [OpenAI pricing](https://developers.openai.com/api/docs/pricing) and [Claude pricing](https://platform.claude.com/docs/en/about-claude/pricing) on **2026-10-05**. Historical versions include the July 30 Luna/Terra reductions and August 21 Sol promotion. Exact public GPT-5.6/6 and Claude identifiers carry mapping provenance; long-context rates use the verified 272k request threshold. New-model launch days use their documented initial API-equivalent rate. Calendar-dated changes between rates remain unknown because their exact UTC switch time is not documented. Sol pricing after the confirmed promotion remains unavailable. See [the pricing evidence ledger](dist/PRICING.md). Internal/unknown model identifiers are never mapped by resemblance.
66
+
67
+ Overview and receipt display the sum of priceable events; the compact priced percentage identifies partial pricing, and unpriced model identities remain visible in Details. No guessed price or zero is assigned to unknown events. Both views share a display comparison of the known priced amount with the declared prorated subscription: Observed Value / You Saved is their difference, and Value Multiple is their ratio. The normalized summary still marks the full-usage cost and comparison unknown when pricing is incomplete. Partial coverage remains visible; these observed values do not infer a certain outcome or an exact break-even date. API and mixed billing have no subscription comparison. Totals from several subscriptions cannot include API/mixed providers in a subscription comparison.
68
+
69
+ ## Architecture
70
+
71
+ ```text
72
+ apps/web/ local UI + static public landing
73
+ packages/core/src/ normalized types, security, scanner, SQLite, summaries
74
+ providers/codex/ Codex adapter
75
+ providers/claude/ Claude adapter
76
+ pricing/ versioned catalog + mapping + calculations
77
+ packages/receipt/ shared receipt model and canvas renderer
78
+ packages/cli/ executable, loopback HTTP server, packaged assets
79
+ tests/ synthetic parser, pricing, storage and security fixtures
80
+ scripts/ native build + browser validation
81
+ ```
82
+
83
+ Streaming memory is bounded by a 16 MiB line limit. Checkpoints persist byte offsets, typed parser state, file identity and edge fingerprints, never raw trailing text. Unfinished lines are read again on the next scan. Appended files continue from the checkpoint; changed files are replaced transactionally. Missing sources retain cached history with a missing-file flag. Unchanged files require only small fingerprint checks, not a full stream. Provider-input links and junctions are rejected. Checkpoints cannot prove changes made deliberately while preserving metadata and the edge fingerprints; V1 assumes normally written local logs.
84
+
85
+ ## Development / validation
86
+
87
+ See [Privacy and data access](PRIVACY.md) for the exact fields retained locally and network boundaries.
88
+
89
+ Usage refreshes incrementally at each normal CLI launch. While the dashboard is open, use **Refresh usage** on Overview or in Settings. Period controls only filter the imported cache; they do not scan source logs. The local application must still be running.
90
+
91
+ Agents with a terminal on the user's machine can run the same CLI with Node.js 24.13+ and permission to read the approved usage roots and write SubValue's local metadata directory. Use `--scan-only --verbose` to update the cache and exit, or `--no-open` to serve the dashboard without opening a browser. This is CLI interoperability, not a dedicated MCP integration. Only Codex and Claude Code source logs are supported; Hermes or other agents can launch the CLI, but their own usage formats are not imported.
92
+
93
+ ```sh
94
+ npm test
95
+ npm run build
96
+ npm ci --ignore-scripts --no-audit --no-fund
97
+ npm run typecheck
98
+ npm run lint
99
+ npm run test:e2e
100
+ ```
101
+
102
+ Only development validation requires npm dependencies. Inter and IBM Plex Mono fonts are bundled under their included OFL licenses, with no CDN requests. The native production builder uses Node's TypeScript stripping and produces JavaScript modules, static CSS and assets; no compiler runs at CLI launch. Type stripping is **not** a TypeScript typecheck.
103
+
104
+ Browser validation uses an isolated headless browser with fixture data. Windows uses installed Edge; elsewhere install Playwright Chromium for development. `SUBVALUE_PLAYWRIGHT_MODULE` can reference an already-installed official Playwright module. No personal browser profile is used.
105
+
106
+ GitHub Actions validates Windows, Linux, and macOS on both Apple Silicon and Intel. Each runner checks types, lint, build, unit/integration tests, responsive browser pages, receipt PNG export, and the locally packed CLI from a temporary directory outside the checkout. The package requires Node.js 24.13 or newer on each platform.
107
+
108
+ Static landing output is `apps/web/dist`, suitable for a static host including Vercel. It contains no local API server or user data. Its preview is marked as an example. Public GitHub links remain release placeholders while the repository is private.
109
+
110
+ ## Package / release
111
+
112
+ ```sh
113
+ npm run build
114
+ npm pack --workspace @subvalue/cli --ignore-scripts --pack-destination artifacts
115
+ ```
116
+
117
+ Package contents are allowlisted to `dist/`, README, PRE_RELEASE, PRIVACY, and LICENSE. No lifecycle/postinstall scripts or runtime dependencies. Local databases, exports, attachments and test artifacts are ignored by Git and excluded from npm contents.
118
+
119
+ The initial npm release is published manually from the audited package. The GitHub repository stays private. npm does not support provenance for private source repositories; no provenance attestation is claimed for this release. The future release workflow remains disabled until trusted publishing is configured and `NPM_PUBLISH_ENABLED=true` is set. It uses a protected environment and OIDC rather than a permanent npm token.
@@ -0,0 +1,46 @@
1
+ # Pricing evidence — catalog 2026-10-05.2
2
+
3
+ USD per million tokens, standard first-party API equivalent. Exact public model IDs only. A mapping's HIGH confidence describes identity, not completeness of retained history. This catalog is bundled and makes no network requests.
4
+
5
+ | Model | Effective date | Input / cached input / output | Evidence |
6
+ |---|---|---|---|
7
+ | GPT-5.6 Sol | 2026-07-09 | 5 / 0.50 / 30 | [Launch](https://openai.com/index/gpt-5-6/) |
8
+ | GPT-5.6 Sol | 2026-08-21 | 4 / 0.40 / 20 | [API changelog, Aug 21](https://developers.openai.com/api/docs/changelog), [model](https://developers.openai.com/api/docs/models/gpt-5.6-sol) |
9
+ | GPT-5.6 Luna | 2026-07-09 | 1 / 0.10 / 6 | [Launch](https://openai.com/index/gpt-5-6/) |
10
+ | GPT-5.6 Luna | 2026-07-30 | 0.20 / 0.02 / 1.20 | [Price change](https://openai.com/index/advancing-the-price-performance-frontier-with-gpt-5-6/), [model](https://developers.openai.com/api/docs/models/gpt-5.6-luna) |
11
+ | GPT-6 Astra | 2026-09-03 | 10 / 1 / 50 | [API changelog](https://developers.openai.com/api/docs/changelog), [model](https://developers.openai.com/api/docs/models/gpt-6-astra) |
12
+ | GPT-6 Sol | 2026-09-22 | 2 / 0.20 / 10 | [API changelog, Sep 22](https://developers.openai.com/api/docs/changelog), [model](https://developers.openai.com/api/docs/models/gpt-6-sol) |
13
+ | GPT-6 Luna | 2026-09-22 | 0.10 / 0.01 / 0.50 | [API changelog, Sep 22](https://developers.openai.com/api/docs/changelog) |
14
+ | GPT-6.1 Sol | 2026-09-29 | 2 / 0.10 / 10 | [API changelog, Sep 29](https://developers.openai.com/api/docs/changelog), [model](https://developers.openai.com/api/docs/models/gpt-6.1-sol) |
15
+ | Claude Fable 5 | 2026-06-09; restored 2026-07-01 | 10 / 1 / 50 | [Announcement, including suspension](https://www.anthropic.com/news/claude-fable-5-mythos-5), [release notes](https://platform.claude.com/docs/en/release-notes/overview) |
16
+ | Claude Opus 4.8 | 2026-05-28 | 5 / 0.50 / 25 | [Announcement](https://www.anthropic.com/news/claude-opus-4-8) |
17
+ | Claude Opus 5 | 2026-07-24 | 5 / 0.50 / 25 | [Announcement](https://www.anthropic.com/news/claude-opus-5) |
18
+ | Claude Opus 4.7 / 4.6 / 4.5 | 2026-04-16 / 2026-02-05 / 2025-11-24 | 5 / 0.50 / 25 | [4.7](https://www.anthropic.com/news/claude-opus-4-7), [4.6](https://www.anthropic.com/news/claude-opus-4-6), [4.5](https://www.anthropic.com/news/claude-opus-4-5) |
19
+ | Claude Sonnet 4.5 / 4.6 | 2025-09-29 / 2026-02-17 | 3 / 0.30 / 15 | [4.5](https://www.anthropic.com/news/claude-sonnet-4-5), [4.6](https://www.anthropic.com/news/claude-sonnet-4-6) |
20
+
21
+ Cache policy: [OpenAI](https://developers.openai.com/api/docs/guides/prompt-caching), [Claude](https://platform.claude.com/docs/en/about-claude/pricing). OpenAI cache writes replace the ordinary input rate, at 1.25× input; cached input and writes are subsets of reported input. Claude input, cache read and cache creation are separate. Claude writes cost 1.25× (5m) or 2× (1h) input. Unknown durations stay unavailable.
22
+
23
+ GPT-5.6 and GPT-6 long-context requests above 272,000 input tokens use 2× input/cache rates and 1.5× output. The entire request receives the applicable band; reasoning is an output breakdown, never a second charge. An aggregate counter delta with inconsistent last-request metadata cannot determine its request context and remains unavailable.
24
+
25
+ Claude Opus/Sonnet 4.6 long-context premium was removed [March 13](https://claude.com/blog/1m-context-ga). Earlier requests above 200k remain unavailable in this catalog, rather than receiving today's standard rate. Sonnet 4.5 and Opus 4.5 above 200k also remain unavailable pending complete premium/availability evidence.
26
+
27
+ A new model has one documented initial rate: launch-day API-equivalent estimates use that rate. Dates of actual changes between rates remain unpriced when the exact switch instant is unknown. Sol's promotional rate is confirmed at least through November 21: November 22 onward stays unavailable until evidence extends or replaces that version. Fable's suspended interval is unpriced. GPT-5.3-Codex is retained as an October 5 current snapshot only, pending historical rate evidence.
28
+
29
+ `codex-auto-review`, unknown aliases, nonstandard tiers/speeds and server tool charges remain unavailable. No inferred subscription billing, regional surcharge, discounts, batch rate or tax. These are standard API-equivalent estimates, not reconstructed invoices.
30
+
31
+
32
+ ## Catalog completeness audit — October 5, 2026
33
+
34
+ The [OpenAI standard text/code pricing table](https://developers.openai.com/api/docs/pricing) was checked in full, including its embedded table data: all 40 rows have matching input, cached-input, cache-write (where published), and output rates. A separate checked fixture verifies every row. Six additional older Codex models are covered from their individual official model pages: GPT-5-Codex, GPT-5.1-Codex, GPT-5.1-Codex-Max, GPT-5.1-Codex-Mini, GPT-5.2-Codex and Codex-Mini-Latest. GPT-5.3-Codex, GPT-5.6-Cyber and Chat-Latest are also explicitly covered. Older/current-only snapshots start October 5; current prices are never silently backdated to unverified historical periods. No prefix-based or invented snapshot aliases.
35
+
36
+ GPT-5.6 Terra initially cost 2.50 / 0.25 / 15 ([July 9 launch](https://openai.com/index/gpt-5-6/)), then 2 / 0.20 / 12 from the [July 30 reduction](https://openai.com/index/advancing-the-price-performance-frontier-with-gpt-5-6/). Its real price-transition day remains uncertain. GPT-5.4/5.5, including Pro, use the documented >272k long-context band; unsupported Pro caching remains unknown. Cyber is bounded to the verified standard-context band.
37
+
38
+ All 15 active standard models in [Claude's pricing table](https://platform.claude.com/docs/en/about-claude/pricing) are covered, including Fable/Mythos 5.1, Opus 5.5, Sonnet 5/5.5 and Haiku 4.5. Their launch dates come from [official release notes](https://platform.claude.com/docs/en/release-notes/overview). Fable/Mythos 5.1 cache reads are 0.025x input ($0.25/MTok); Opus 5.5 reads are 0.05x ($0.20/MTok), rather than the ordinary 0.1x. Claude writes retain 1.25x/2x for 5m/1h. Sonnet 5's introductory $2/$10 tariff became permanent August 10: the previously planned September increase is not applied.
39
+
40
+ Retired Claude Opus 4/4.1, Sonnet 4 and Sonnet 3.7 have explicit historical tariffs bounded by their [official retirement dates](https://platform.claude.com/docs/en/about-claude/model-deprecations). Haiku 3.5's exact identity is recognized, but its historical price reduction date is not established: its currently published $0.80/$4 is not backdated to the $1/$5 launch. Mythos Preview's published $25/$125 input/output tariff is an October 5 snapshot; unverified cache rates remain unknown. Models retired before Claude Code and unsupported dated aliases are not claimed to have complete historical prices.
41
+
42
+ This audit covers first-party standard text/code token equivalents. Audio, video, image-generation, embeddings, research-specialist models and separate tool charges require categories absent from these coding logs and are outside this catalog. Subscription models, Fast/Batch/Flex tiers, negotiated/region-specific prices and future promotions are not inferred. Codex-Auto-Review and GPT-5.3-Codex-Spark have no verified public API equivalent in this audit.
43
+
44
+ ## Subscription presets
45
+
46
+ USD plans were checked October 5 against [ChatGPT pricing](https://learn.chatgpt.com/docs/pricing), [Claude pricing](https://claude.com/pricing) and [Claude Max tiers](https://support.claude.com/en/articles/11049741-what-is-the-max-plan). Presets are bundled locally, selected manually and do not rewrite existing amounts. Annual plans use the exact annual total divided by 12 (Claude Pro: $200/year, not $17/month); per-seat rates are explicitly labeled. Enterprise/Edu and usage-dependent enterprise bills use Custom amount. Presets describe current plans, not a reconstructed history of subscription contracts.
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./packages/cli/src/index.js";
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import {fileURLToPath} from 'node:url';
4
+ import {dataDirectory,openBrowser} from './runtime.js';
5
+ import {detectedProviders,sourceRoots} from '../../core/src/security.js';
6
+
7
+ import {scan} from '../../core/src/scanner.js';
8
+ import {startServer} from './server.js';
9
+ const argv=process.argv.slice(2);const allowed=new Set(['--dry-run','--demo','--no-open','--scan-only','--help','--port','--data-dir','--verbose']);
10
+ function value(flag ) {const i=argv.indexOf(flag);return i<0?undefined:argv[i+1];}
11
+ async function main(){
12
+ for(let i=0;i<argv.length;i++){if(!allowed.has(argv[i]))throw new Error('Unknown option. Use --help.');if(['--port','--data-dir'].includes(argv[i])){if(!argv[i+1]||argv[i+1].startsWith('--'))throw new Error('Option requires a value');i++;}}
13
+ if(argv.includes('--help')){console.log('SubValue\n\nsubvalue [--dry-run] [--no-open] [--scan-only] [--verbose] [--port 4731] [--data-dir DIRECTORY]\nsubvalue --demo\n\nRequires Node.js 24.13+. No account or API keys.');return;}
14
+ if(argv.includes('--dry-run')){const d=detectedProviders();console.log(`Codex ${d.codex?'Detected':'Not detected'}\nClaude Code ${d.claude?'Detected':'Not detected'}\n\nSources:`);for(const r of sourceRoots())console.log(r.safeLabel);return;}
15
+ const demo=argv.includes('--demo');const port=Number(value('--port')??4731);if(!Number.isInteger(port)||port<0||port>65535)throw new Error('Invalid port');
16
+ let store ;
17
+ console.log('SubValue\n');
18
+ if(!demo){const {Store}=await import('../../core/src/storage.js');store=new Store(value('--data-dir')??dataDirectory());let last=0;const verbose=argv.includes('--verbose');const started=Date.now();console.log('Scanning local usage…');const result=await scan(store,{onProgress:p=>{if(Date.now()-last>1000||p.phase==='complete'){last=Date.now();const text=`${p.provider}: ${p.files}/${p.total} files · ${Math.round(p.bytes/1e6)} MB`;if(verbose)console.log(text);else if(process.stdout.isTTY)process.stdout.write(`\r${text.padEnd(65)}`);}}});if(!verbose&&process.stdout.isTTY)process.stdout.write('\r'+' '.repeat(65)+'\r');for(const s of result)console.log(`${s.provider==='codex'?'Codex ':'Claude Code'} ${s.diagnostics.records?'✓':s.detected?'No usage found':'Not detected'}`);if(verbose)console.log(`${((Date.now()-started)/1000).toFixed(1)}s · ${result.reduce((n,s)=>n+s.cached,0)} files cached`);if(argv.includes('--scan-only')){store.close();return;}}
19
+ const running=await startServer({store,port,publicDirectory:path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../../../public'),demo}).catch(e=>{store?.close();throw e;});
20
+ let closing=false;const close=async()=>{if(closing)return;closing=true;await running.close();store?.close();process.exit(0);};process.on('SIGINT',()=>void close());process.on('SIGTERM',()=>void close());
21
+ if(demo)console.log('DEMO — fixture data');
22
+ if(!argv.includes('--no-open')){console.log('Opening dashboard…');console.log(running.url);if(!await openBrowser(running.url))console.log(`Open in your browser: ${running.url}`);}else console.log(running.url);
23
+ }
24
+ main().catch(()=>{console.error('SubValue could not start. Check the port, data-directory permissions, and Node version. No source files were modified.');process.exitCode=1;});
@@ -0,0 +1,22 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import {spawn} from 'node:child_process';
4
+
5
+ export function dataDirectory(platform=process.platform,home=os.homedir(),env =process.env) {
6
+ if(platform==='win32')return path.join(env.LOCALAPPDATA&&path.isAbsolute(env.LOCALAPPDATA)?env.LOCALAPPDATA:path.join(home,'AppData','Local'),'SubValue');
7
+ if(platform==='darwin')return path.join(home,'Library','Application Support','SubValue');
8
+ return path.join(env.XDG_DATA_HOME&&path.isAbsolute(env.XDG_DATA_HOME)?env.XDG_DATA_HOME:path.join(home,'.local','share'),'subvalue');
9
+ }
10
+
11
+ export function openBrowser(url ,launch=spawn,platform=process.platform) {
12
+ // URLs are generated by our loopback server; no shell interpolation is used.
13
+ if(!/^http:\/\/127\.0\.0\.1:\d+$/.test(url))return Promise.resolve(false);
14
+ const executable=platform==='win32'?'rundll32':platform==='darwin'?'open':'xdg-open';
15
+ const args=platform==='win32'?['url.dll,FileProtocolHandler',url]:[url];
16
+ return new Promise(resolve=>{
17
+ const child=launch(executable,args,{stdio:'ignore',windowsHide:true});
18
+ let finished=false;const done=(ok )=>{if(finished)return;finished=true;clearTimeout(timeout);resolve(ok);};
19
+ const timeout=setTimeout(()=>{child.unref();done(true);},3000);
20
+ child.once('error',()=>done(false));child.once('exit',code=>done(code===0));
21
+ });
22
+ }
@@ -0,0 +1,75 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {randomBytes,timingSafeEqual} from 'node:crypto';
5
+
6
+ import {scan, } from '../../core/src/scanner.js';
7
+ import {dateRange,summarize} from '../../core/src/summary.js';
8
+ import {demoData} from '../../core/src/demo.js';
9
+
10
+
11
+
12
+ import {safeDirectory} from '../../core/src/security.js';
13
+ import {subscriptionPlan} from '../../core/src/subscriptions.js';
14
+ const mime ={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.svg':'image/svg+xml','.woff2':'font/woff2','.txt':'text/plain; charset=utf-8'};
15
+ export function validateSettings(value ) {
16
+ if(!value||value.currency!=='USD'||typeof value.onboarded!=='boolean')throw new Error('Invalid settings');
17
+ const s ={onboarded:value.onboarded,currency:'USD',billing:{} ,include:{} };
18
+ for(const provider of ['codex','claude'] ){const b=value.billing?.[provider];if(!b||!['UNKNOWN','SUBSCRIPTION','API','MIXED'].includes(b.mode)||typeof value.include?.[provider]!=='boolean')throw new Error('Invalid billing settings');if(b.monthly!==null&&(typeof b.monthly!=='number'||!Number.isFinite(b.monthly)||b.monthly<0||b.monthly>1000000))throw new Error('Invalid subscription amount');
19
+ const plan=b.planId==null?undefined:subscriptionPlan(provider,b.planId);if(b.planId!=null&&!plan)throw new Error('Invalid subscription plan');
20
+ if(b.mode==='SUBSCRIPTION'&&plan&&(b.monthly===null||Math.abs(b.monthly-plan.monthly)>.005))throw new Error('Subscription amount does not match the selected plan');
21
+ s.billing[provider]={mode:b.mode,monthly:b.mode==='SUBSCRIPTION'?(plan?.monthly??b.monthly):null};if(b.mode==='SUBSCRIPTION'&&b.planId!==undefined)s.billing[provider].planId=plan?.id??null;s.include[provider]=value.include[provider];}return s;
22
+ }
23
+ export async function startServer(options ){
24
+ const secret=randomBytes(32).toString('hex');const demo=options.demo?demoData():null;let demoSettings=demo?.settings;let scanning=false;let progress =null;let sessionOrigin='';
25
+ let revision='',history =null;const summaries=new Map ();
26
+ const invalidate=()=>{revision='';history=null;summaries.clear();};
27
+ const server=http.createServer(async(req,res)=>{
28
+ const headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer','Cross-Origin-Resource-Policy':'same-origin','X-Frame-Options':'DENY','Content-Security-Policy':"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"};
29
+ const json=(status ,data )=>{res.writeHead(status,{...headers,'Content-Type':'application/json'});res.end(JSON.stringify(data));};
30
+ const expectedHost=new URL(sessionOrigin).host;
31
+ if(req.headers.host!==expectedHost){json(403,{error:'Host denied'});return;}
32
+ if(req.headers.origin&&req.headers.origin!==sessionOrigin){json(403,{error:'Origin denied'});return;}
33
+ if(req.headers['sec-fetch-site']==='cross-site'){json(403,{error:'Cross-site access denied'});return;}
34
+ const url=new URL(req.url??'/',sessionOrigin);
35
+ try{
36
+ if(url.pathname.startsWith('/api/')){
37
+ // The bootstrap is same-origin only. Mutations require its per-launch token.
38
+ if(url.pathname==='/api/bootstrap'&&req.method==='GET'){json(200,{token:secret,settings:demoSettings??options.store .settings(),sources:demo?.statuses??options.store .statuses(),demo:!!demo,lastScan:options.store?.get('lastScan')??null,scanning,progress});return;}
39
+ if(url.pathname==='/api/summary'&&req.method==='GET'){
40
+ if(scanning){json(503,{error:'Scan in progress'});return;}
41
+ const range=dateRange(url.searchParams.get('period')??'30d',new Date(),url.searchParams.get('from')??undefined,url.searchParams.get('to')??undefined);
42
+ // History bounds use all normalized metadata, never original transcripts.
43
+ const currentRevision=demo?'demo':options.store .revision();if(currentRevision!==revision){invalidate();revision=currentRevision;}
44
+ const key=JSON.stringify(range);let result=summaries.get(key);
45
+ if(!result){history??=demo?.records??options.store .records();result=summarize(history,demo?.statuses??options.store .statuses(),demoSettings??options.store .settings(),range,demo?.catalog,!!demo);if(summaries.size>=16)summaries.delete(summaries.keys().next().value );summaries.set(key,result);}
46
+ json(200,result);return;
47
+ }
48
+ const given=req.headers['x-subvalue-token'];if(typeof given!=='string'||given.length!==secret.length||!timingSafeEqual(Buffer.from(given),Buffer.from(secret))){json(403,{error:'Session token required'});return;}
49
+ if(url.pathname==='/api/settings'&&req.method==='POST'){
50
+ let body='';for await(const chunk of req){body+=chunk;if(body.length>16384){json(413,{error:'Request too large'});return;}}
51
+ const settings=validateSettings(JSON.parse(body));if(demo)demoSettings=settings;else options.store .saveSettings(settings);invalidate();json(200,{saved:true});return;
52
+ }
53
+ if(url.pathname==='/api/rescan'&&req.method==='POST'){
54
+ if(demo){json(200,{scanning:false});return;}if(scanning){json(409,{error:'Scan already running'});return;}scanning=true;json(202,{scanning:true});
55
+ void scan(options.store ,{home:options.home,onProgress:p=>{progress=p;}}).catch(()=>{progress=null;}).finally(()=>{invalidate();scanning=false;});return;
56
+ }
57
+ json(404,{error:'Not found'});return;
58
+ }
59
+ if(req.method!=='GET'&&req.method!=='HEAD'){json(405,{error:'Method not allowed'});return;}
60
+ const name=url.pathname==='/'||['/overview','/receipt','/settings'].includes(url.pathname)?'index.html':url.pathname==='/landing'?'landing.html':url.pathname.slice(1);
61
+ if(!/^[a-zA-Z0-9_./-]+$/.test(name)||name.includes('..')||!mime[path.extname(name)]){json(404,{error:'Not found'});return;}
62
+ const base=path.resolve(options.publicDirectory),target=path.resolve(base,name);
63
+ if(!target.startsWith(base+path.sep)||!safeDirectory(path.dirname(target))||!fs.existsSync(target)){json(404,{error:'Not found'});return;}
64
+ const before=fs.lstatSync(target);if(!before.isFile()||before.isSymbolicLink()||!fs.realpathSync(target).startsWith(fs.realpathSync(base)+path.sep)){json(404,{error:'Not found'});return;}
65
+ if(req.method==='HEAD'){res.writeHead(200,{...headers,'Content-Type':mime[path.extname(target)]});res.end();return;}
66
+ const fd=fs.openSync(target,fs.constants.O_RDONLY|(fs.constants.O_NOFOLLOW??0)),after=fs.fstatSync(fd);
67
+ if(before.ino!==after.ino||before.dev!==after.dev){fs.closeSync(fd);json(404,{error:'Not found'});return;}
68
+ res.writeHead(200,{...headers,'Content-Type':mime[path.extname(target)]});
69
+ const stream=fs.createReadStream(target,{fd,autoClose:true});stream.on('error',()=>res.destroy());res.once('close',()=>stream.destroy());stream.pipe(res);
70
+ }catch{json(400,{error:'Unable to complete request'});}
71
+ });
72
+ const listen=(port )=>new Promise ((resolve,reject)=>{const error=(e )=>reject(e);server.once('error',error);server.listen(port,'127.0.0.1',()=>{server.removeListener('error',error);const address=server.address();if(!address||typeof address==='string')return reject(new Error('No listening address'));sessionOrigin=`http://127.0.0.1:${address.port}`;resolve();});});
73
+ try{await listen(options.port);}catch(e){if(options.port===0||!['EADDRINUSE','EACCES'].includes((e ).code??''))throw e;await listen(0);}
74
+ return {server,url:sessionOrigin,close:()=>new Promise ((resolve,reject)=>server.close(e=>e?reject(e):resolve()))};
75
+ }
@@ -0,0 +1,10 @@
1
+ import {baseRecord} from './metadata.js';
2
+ import {catalog, } from './pricing/index.js';
3
+ import {defaultSettings,emptyDiagnostics, } from './types.js';
4
+ export function demoData(now=new Date()) {
5
+ const records =[];const monthDays=now.getDate();const weights=Array.from({length:monthDays},(_,i)=>[4,6,5,8,7,10,6,8,11,9][i%10]);const sum=weights.reduce((a,b)=>a+b,0);let assigned=0;
6
+ for(let i=0;i<monthDays;i++){const r=baseRecord('codex','fixture-only',`fixture-${i}`);const output=i===monthDays-1?20530000-assigned:Math.floor(20530000*weights[i]/sum);assigned+=output;r.timestamp=new Date(now.getFullYear(),now.getMonth(),i+1,12).toISOString();r.model_raw='gpt-5.3-codex';r.session_id='fixture-session';r.input_tokens=0;r.cached_input_tokens=0;r.cache_creation_tokens=0;r.output_tokens=output;r.reasoning_tokens=0;r.total_tokens=output;r.source_schema='DEMO/fixture';r.notes=['demo-fixture'];records.push(r);}
7
+ const settings=defaultSettings();settings.onboarded=true;settings.billing.codex={mode:'SUBSCRIPTION',monthly:200};
8
+ const statuses=(['codex','claude'] ).map(provider=>({provider,detected:true,files:provider==='codex'?1:0,scanned:0,cached:0,errors:0,skippedLinks:0,missingFiles:0,diagnostics:emptyDiagnostics()}));
9
+ const demoCatalog ={...catalog,version:'DEMO — fixture prices',prices:catalog.prices.map(p=>({...p,effective_from:'2000-01-01T00:00:00.000Z'}))};return {records,settings,statuses,catalog:demoCatalog};
10
+ }
@@ -0,0 +1,14 @@
1
+ import {createHash} from 'node:crypto';
2
+
3
+ export const hash = (...parts ) => createHash('sha256').update(JSON.stringify(parts)).digest('hex');
4
+ export const token = (v ) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 ? v : null;
5
+ export const identifier = (v ) => typeof v === 'string' && /^[a-zA-Z0-9._:@/-]{1,160}$/.test(v) ? v : null;
6
+ export const modelId = (v ) => typeof v === 'string' && /^[a-zA-Z0-9._:-]{1,100}$/.test(v) ? v : null;
7
+ export const timestamp = (v ) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(v) && Number.isFinite(Date.parse(v)) ? new Date(v).toISOString() : null;
8
+ export function baseRecord(provider , ref , event ) {
9
+ return {id:hash(provider,event),provider,source_event_id:event,source_file_reference:ref,timestamp:null,session_id:null,request_id:null,thread_id:null,model_raw:null,model_canonical:null,billing_mode:'UNKNOWN',input_tokens:null,cached_input_tokens:null,cache_creation_tokens:null,cache_creation_5m_tokens:null,cache_creation_1h_tokens:null,output_tokens:null,reasoning_tokens:null,unallocated_total_tokens:null,total_tokens:null,project_identifier_hash:null,quality:'HIGH',source_schema:'unknown',notes:[],service_tier:null,speed:null,web_search_requests:null,web_fetch_requests:null};
10
+ }
11
+ export function track(d , r ) {
12
+ d.records++;
13
+ if(r.timestamp){d.first = !d.first || r.timestamp < d.first ? r.timestamp : d.first;d.last = !d.last || r.timestamp > d.last ? r.timestamp : d.last;}
14
+ }