oc2token 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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +399 -0
  3. package/dist/src/accounting/index.d.ts +1 -0
  4. package/dist/src/accounting/index.js +2 -0
  5. package/dist/src/accounting/index.js.map +1 -0
  6. package/dist/src/accounting/reducer.d.ts +21 -0
  7. package/dist/src/accounting/reducer.js +105 -0
  8. package/dist/src/accounting/reducer.js.map +1 -0
  9. package/dist/src/antigravity/discovery.d.ts +18 -0
  10. package/dist/src/antigravity/discovery.js +95 -0
  11. package/dist/src/antigravity/discovery.js.map +1 -0
  12. package/dist/src/antigravity/doctor.d.ts +6 -0
  13. package/dist/src/antigravity/doctor.js +78 -0
  14. package/dist/src/antigravity/doctor.js.map +1 -0
  15. package/dist/src/antigravity/index.d.ts +3 -0
  16. package/dist/src/antigravity/index.js +4 -0
  17. package/dist/src/antigravity/index.js.map +1 -0
  18. package/dist/src/antigravity/proto.d.ts +73 -0
  19. package/dist/src/antigravity/proto.js +259 -0
  20. package/dist/src/antigravity/proto.js.map +1 -0
  21. package/dist/src/antigravity/scanner.d.ts +8 -0
  22. package/dist/src/antigravity/scanner.js +552 -0
  23. package/dist/src/antigravity/scanner.js.map +1 -0
  24. package/dist/src/application.d.ts +73 -0
  25. package/dist/src/application.js +657 -0
  26. package/dist/src/application.js.map +1 -0
  27. package/dist/src/cache/filesystem.d.ts +8 -0
  28. package/dist/src/cache/filesystem.js +70 -0
  29. package/dist/src/cache/filesystem.js.map +1 -0
  30. package/dist/src/cache/index.d.ts +5 -0
  31. package/dist/src/cache/index.js +6 -0
  32. package/dist/src/cache/index.js.map +1 -0
  33. package/dist/src/cache/lock.d.ts +40 -0
  34. package/dist/src/cache/lock.js +227 -0
  35. package/dist/src/cache/lock.js.map +1 -0
  36. package/dist/src/cache/schema.d.ts +45 -0
  37. package/dist/src/cache/schema.js +308 -0
  38. package/dist/src/cache/schema.js.map +1 -0
  39. package/dist/src/cache/store.d.ts +46 -0
  40. package/dist/src/cache/store.js +396 -0
  41. package/dist/src/cache/store.js.map +1 -0
  42. package/dist/src/cache/types.d.ts +130 -0
  43. package/dist/src/cache/types.js +6 -0
  44. package/dist/src/cache/types.js.map +1 -0
  45. package/dist/src/cli.d.ts +8 -0
  46. package/dist/src/cli.js +874 -0
  47. package/dist/src/cli.js.map +1 -0
  48. package/dist/src/codex/discovery.d.ts +6 -0
  49. package/dist/src/codex/discovery.js +88 -0
  50. package/dist/src/codex/discovery.js.map +1 -0
  51. package/dist/src/codex/doctor.d.ts +6 -0
  52. package/dist/src/codex/doctor.js +52 -0
  53. package/dist/src/codex/doctor.js.map +1 -0
  54. package/dist/src/codex/index.d.ts +3 -0
  55. package/dist/src/codex/index.js +3 -0
  56. package/dist/src/codex/index.js.map +1 -0
  57. package/dist/src/codex/scanner.d.ts +6 -0
  58. package/dist/src/codex/scanner.js +487 -0
  59. package/dist/src/codex/scanner.js.map +1 -0
  60. package/dist/src/codex/types.d.ts +31 -0
  61. package/dist/src/codex/types.js +5 -0
  62. package/dist/src/codex/types.js.map +1 -0
  63. package/dist/src/collector/collector.d.ts +15 -0
  64. package/dist/src/collector/collector.js +597 -0
  65. package/dist/src/collector/collector.js.map +1 -0
  66. package/dist/src/collector/index.d.ts +3 -0
  67. package/dist/src/collector/index.js +3 -0
  68. package/dist/src/collector/index.js.map +1 -0
  69. package/dist/src/collector/types.d.ts +55 -0
  70. package/dist/src/collector/types.js +18 -0
  71. package/dist/src/collector/types.js.map +1 -0
  72. package/dist/src/dashboard/render/ansi.d.ts +53 -0
  73. package/dist/src/dashboard/render/ansi.js +106 -0
  74. package/dist/src/dashboard/render/ansi.js.map +1 -0
  75. package/dist/src/dashboard/render/dashboard.d.ts +13 -0
  76. package/dist/src/dashboard/render/dashboard.js +750 -0
  77. package/dist/src/dashboard/render/dashboard.js.map +1 -0
  78. package/dist/src/dashboard/render/format.d.ts +23 -0
  79. package/dist/src/dashboard/render/format.js +169 -0
  80. package/dist/src/dashboard/render/format.js.map +1 -0
  81. package/dist/src/dashboard/render/redraw.d.ts +13 -0
  82. package/dist/src/dashboard/render/redraw.js +24 -0
  83. package/dist/src/dashboard/render/redraw.js.map +1 -0
  84. package/dist/src/dashboard/render/types.d.ts +83 -0
  85. package/dist/src/dashboard/render/types.js +528 -0
  86. package/dist/src/dashboard/render/types.js.map +1 -0
  87. package/dist/src/dashboard/scheduler/index.d.ts +1 -0
  88. package/dist/src/dashboard/scheduler/index.js +2 -0
  89. package/dist/src/dashboard/scheduler/index.js.map +1 -0
  90. package/dist/src/dashboard/scheduler/refresh-scheduler.d.ts +46 -0
  91. package/dist/src/dashboard/scheduler/refresh-scheduler.js +137 -0
  92. package/dist/src/dashboard/scheduler/refresh-scheduler.js.map +1 -0
  93. package/dist/src/dashboard/settings/index.d.ts +25 -0
  94. package/dist/src/dashboard/settings/index.js +162 -0
  95. package/dist/src/dashboard/settings/index.js.map +1 -0
  96. package/dist/src/dashboard/state/index.d.ts +2 -0
  97. package/dist/src/dashboard/state/index.js +3 -0
  98. package/dist/src/dashboard/state/index.js.map +1 -0
  99. package/dist/src/dashboard/state/refresh-coordinator.d.ts +72 -0
  100. package/dist/src/dashboard/state/refresh-coordinator.js +346 -0
  101. package/dist/src/dashboard/state/refresh-coordinator.js.map +1 -0
  102. package/dist/src/dashboard/state/types.d.ts +68 -0
  103. package/dist/src/dashboard/state/types.js +2 -0
  104. package/dist/src/dashboard/state/types.js.map +1 -0
  105. package/dist/src/domain/contracts.d.ts +219 -0
  106. package/dist/src/domain/contracts.js +2 -0
  107. package/dist/src/domain/contracts.js.map +1 -0
  108. package/dist/src/domain/errors.d.ts +25 -0
  109. package/dist/src/domain/errors.js +65 -0
  110. package/dist/src/domain/errors.js.map +1 -0
  111. package/dist/src/domain/index.d.ts +5 -0
  112. package/dist/src/domain/index.js +6 -0
  113. package/dist/src/domain/index.js.map +1 -0
  114. package/dist/src/domain/records.d.ts +59 -0
  115. package/dist/src/domain/records.js +141 -0
  116. package/dist/src/domain/records.js.map +1 -0
  117. package/dist/src/domain/tokens.d.ts +51 -0
  118. package/dist/src/domain/tokens.js +93 -0
  119. package/dist/src/domain/tokens.js.map +1 -0
  120. package/dist/src/domain/windows.d.ts +59 -0
  121. package/dist/src/domain/windows.js +272 -0
  122. package/dist/src/domain/windows.js.map +1 -0
  123. package/dist/src/index.d.ts +9 -0
  124. package/dist/src/index.js +9 -0
  125. package/dist/src/index.js.map +1 -0
  126. package/dist/src/opencode/adapter.d.ts +47 -0
  127. package/dist/src/opencode/adapter.js +118 -0
  128. package/dist/src/opencode/adapter.js.map +1 -0
  129. package/dist/src/opencode/client.d.ts +64 -0
  130. package/dist/src/opencode/client.js +118 -0
  131. package/dist/src/opencode/client.js.map +1 -0
  132. package/dist/src/opencode/doctor.d.ts +28 -0
  133. package/dist/src/opencode/doctor.js +82 -0
  134. package/dist/src/opencode/doctor.js.map +1 -0
  135. package/dist/src/opencode/index.d.ts +5 -0
  136. package/dist/src/opencode/index.js +6 -0
  137. package/dist/src/opencode/index.js.map +1 -0
  138. package/dist/src/opencode/stats.d.ts +8 -0
  139. package/dist/src/opencode/stats.js +174 -0
  140. package/dist/src/opencode/stats.js.map +1 -0
  141. package/dist/src/opencode/transport.d.ts +54 -0
  142. package/dist/src/opencode/transport.js +621 -0
  143. package/dist/src/opencode/transport.js.map +1 -0
  144. package/dist/src/output/index.d.ts +20 -0
  145. package/dist/src/output/index.js +46 -0
  146. package/dist/src/output/index.js.map +1 -0
  147. package/dist/src/output/json.d.ts +72 -0
  148. package/dist/src/output/json.js +133 -0
  149. package/dist/src/output/json.js.map +1 -0
  150. package/dist/src/output/table.d.ts +4 -0
  151. package/dist/src/output/table.js +81 -0
  152. package/dist/src/output/table.js.map +1 -0
  153. package/dist/src/pricing/index.d.ts +1 -0
  154. package/dist/src/pricing/index.js +2 -0
  155. package/dist/src/pricing/index.js.map +1 -0
  156. package/dist/src/pricing/pricing.d.ts +19 -0
  157. package/dist/src/pricing/pricing.js +441 -0
  158. package/dist/src/pricing/pricing.js.map +1 -0
  159. package/package.json +27 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaan Yaren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,399 @@
1
+ <p align="center">
2
+ <img src="assets/screenshots/dashboard.svg" width="100%" alt="OC2Token dashboard — day view with three window cards, trend sparkline and breakdown tables" />
3
+ </p>
4
+
5
+ <h1 align="center">OC2Token</h1>
6
+
7
+ <p align="center">
8
+ <strong>macOS-first terminal dashboard for OpenCode&nbsp;2 token usage</strong><br/>
9
+ Unified, privacy-preserving telemetry for <code>opencode</code> · <code>codex</code> · <code>antigravity</code>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/oc2token"><img src="https://img.shields.io/npm/v/oc2token?style=flat-square&color=8B5CF6&label=npm" alt="npm version" /></a>
14
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-ff8700?style=flat-square" alt="MIT license" /></a>
15
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-00d7ff?style=flat-square" alt="node >=20" /></a>
16
+ <a href="#platform"><img src="https://img.shields.io/badge/platform-macOS%20%E2%80%A2%20Linux-lightgrey?style=flat-square" alt="macOS • Linux" /></a>
17
+ <a href="https://github.com/kaanyaren/OC2Token/issues"><img src="https://img.shields.io/github/issues/kaanyaren/OC2Token?style=flat-square&color=9a8fc2" alt="issues" /></a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <code>npx oc2token</code>&ensp;·&ensp;<code>oc2token day</code>&ensp;·&ensp;<code>oc2token --json</code>&ensp;·&ensp;<code>oc2token doctor</code>
22
+ </p>
23
+
24
+ ---
25
+
26
+ ### Why OC2Token?
27
+
28
+ OpenCode 2 records rich token telemetry locally, but the raw message stream is noisy and per-provider. **OC2Token** is a single, fast, terminal-native console that:
29
+
30
+ - aggregates **all three providers** (`opencode`, `codex`, `antigravity`) into one unified report
31
+ - shows three exact windows — **last 60 minutes · today · this week** — with local-day and ISO-week semantics
32
+ - visualises a **sparkline trend**, per-model / per-provider / per-project breakdowns, and coverage status without ever persisting prompts or keys
33
+ - stays instantly usable over pipes (`--json`, `--format table`) for CI and scripting
34
+
35
+ > `recorded_total = input + output + reasoning + cacheRead + cacheWrite` — an explicit accounting sum, **not** a billing or quota total.
36
+
37
+ ---
38
+
39
+ ## ✨ Highlights
40
+
41
+ - **Unified multi-provider** — one snapshot, one schema, three colours (purple · orange · cyan)
42
+ - **Three exact windows** rendered side-by-side, responsive down to 20 columns
43
+ - **CodeBurn-inspired palette** — violet structure, orange focus, cyan inputs; `--no-color` / `NO_COLOR` safe
44
+ - **Trend sparkline** per window, not one row per bucket
45
+ - **Privacy by design** — only normalized token counters are cached; no prompts, tool inputs, or session titles are stored
46
+ - **Resilient collection** — paginated assistant-message aggregation with retry, dedup by `tokenRevision`, fallback when the filtered stats API ignores the range
47
+ - **Scriptable** — stable JSON `schemaVersion: 3`, deterministic table output, exit codes `0` / `1` / `3`
48
+
49
+ ---
50
+
51
+ ## 📸 Preview
52
+
53
+ <table>
54
+ <tr>
55
+ <td width="50%">
56
+
57
+ **Dashboard — day (100 cols)**
58
+ <br/>Three cards + inline provider stack + trend
59
+
60
+ <img src="assets/screenshots/dashboard.svg" alt="Dashboard day view" />
61
+
62
+ </td>
63
+ <td width="50%">
64
+
65
+ **Settings overlay — `s`**
66
+ <br/>Toggle providers, scrub refresh interval
67
+
68
+ <img src="assets/screenshots/dashboard-settings.svg" alt="Settings overlay" />
69
+
70
+ </td>
71
+ </tr>
72
+ <tr>
73
+ <td>
74
+
75
+ **Projects — `p`**
76
+ <br/>Per-project totals scoped to the selected window
77
+
78
+ <img src="assets/screenshots/dashboard-projects.svg" alt="Projects overlay" />
79
+
80
+ </td>
81
+ <td>
82
+
83
+ **Narrow — 60 cols**
84
+ <br/>Stacks vertically, never wraps
85
+
86
+ <img src="assets/screenshots/dashboard-narrow.svg" alt="Narrow 60-col layout" />
87
+
88
+ </td>
89
+ </tr>
90
+ </table>
91
+
92
+ <details>
93
+ <summary>More screenshots</summary>
94
+
95
+ | View | Screenshot |
96
+ |------|------------|
97
+ | Hour (`1` / `oc2token hour`) | ![hour](assets/screenshots/dashboard-hour.svg) |
98
+ | Help (`?`) | ![help](assets/screenshots/dashboard-help.svg) |
99
+ | Table pipe (`--format table`) | ![table](assets/screenshots/table.svg) |
100
+
101
+ </details>
102
+
103
+ ---
104
+
105
+ ## 📦 Installation
106
+
107
+ ### npm / npx (recommended)
108
+
109
+ ```sh
110
+ npm install -g oc2token
111
+ # or without installing
112
+ npx oc2token
113
+ ```
114
+
115
+ Requires **Node.js ≥ 20**. The OpenCode 2 local service must be reachable; OC2Token talks to it via `@opencode-ai/client` (beta API subject to change).
116
+
117
+ ### From source
118
+
119
+ ```sh
120
+ git clone https://github.com/kaanyaren/OC2Token.git
121
+ cd OC2Token
122
+ npm install
123
+ npm run build
124
+ npm link # or: node dist/src/cli.js --help
125
+ ```
126
+
127
+ ### Verify
128
+
129
+ ```sh
130
+ oc2token doctor # human-readable health checks
131
+ oc2token doctor --json # machine-readable
132
+ oc2token --help
133
+ ```
134
+
135
+ ---
136
+
137
+ ## 🚀 Quick start
138
+
139
+ ```sh
140
+ oc2token # interactive dashboard, refreshes every 5 min
141
+ oc2token hour # start on the rolling last-60-minutes card
142
+ oc2token day # start on today's local-calendar day
143
+ oc2token week # start on the ISO week (Mon–Sun)
144
+
145
+ oc2token --once --json # one-shot JSON for scripting
146
+ oc2token --format table # deterministic plain table (great for pipes)
147
+ oc2token --refresh 0 # manual-only; press r to refresh
148
+ oc2token --timezone Europe/Istanbul # override the local zone
149
+ oc2token --no-color # or NO_COLOR=1 for plain output
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 🎮 Interactive dashboard
155
+
156
+ | Key / Click | Action |
157
+ |-------------|--------|
158
+ | `r` / `R` | Refresh now |
159
+ | `1` / `2` / `3` · **click top card** | Select **hour · day · week** |
160
+ | `Tab` / `←` `→` / `↑` `↓` | Cycle windows |
161
+ | `p` | Projects panel |
162
+ | `s` | Settings — toggle providers, adjust refresh |
163
+ | `?` | Help |
164
+ | `q` / `Ctrl+C` | Quit |
165
+
166
+ Settings persist under the cache directory (`~/.cache/oc2token` on macOS/Linux or `$XDG_CACHE_HOME`). At least one provider must stay enabled.
167
+
168
+ **Theme.** Purple carries structure, orange carries activity and focus, cyan carries inputs. Respects `NO_COLOR` and `--no-color`.
169
+
170
+ ---
171
+
172
+ ## 🖥️ CLI reference
173
+
174
+ ```
175
+ oc2token 0.1.0
176
+
177
+ Usage:
178
+ oc2token [hour|day|week]
179
+ oc2token [options]
180
+ oc2token doctor [--source <provider>] [--json]
181
+
182
+ Options:
183
+ --once Collect once and exit
184
+ --json Emit stable JSON and exit
185
+ --format <mode> auto, dashboard, table, or json
186
+ --refresh <seconds> Auto-refresh cadence; 0 is manual-only (60–14400)
187
+ --timezone <IANA> Zone for local day / ISO week boundaries
188
+ --project <id> Restrict collection to an OpenCode project
189
+ --cache-dir <path> Override the normalized metadata cache directory
190
+ --source <provider> Filter providers: opencode, codex, antigravity, or all (repeatable)
191
+ --no-color Disable ANSI colors
192
+ -h, --help Show help
193
+ -v, --version Show version
194
+ ```
195
+
196
+ ### Exit codes
197
+
198
+ | Code | Meaning |
199
+ |------|---------|
200
+ | `0` | Success, complete coverage |
201
+ | `1` | Connection / validation / usage error |
202
+ | `3` | Partial results — JSON emitted but coverage incomplete |
203
+
204
+ ### Filtering providers
205
+
206
+ ```sh
207
+ oc2token --source codex # only Codex
208
+ oc2token --source opencode --source codex # two providers
209
+ oc2token --source all # reset filter (default)
210
+ oc2token doctor --source antigravity --json
211
+ ```
212
+
213
+ Combine with `--once` / `--json` for scripting:
214
+
215
+ ```sh
216
+ oc2token --once --json --source codex | jq .totals.day.recorded_total
217
+ ```
218
+
219
+ ---
220
+
221
+ ## 🔌 Providers
222
+
223
+ | Provider | Source | Discovery |
224
+ |----------|--------|-----------|
225
+ | **opencode** | OpenCode 2 local service (`@opencode-ai/client`) | session/message pagination with `tokenRevision` dedup |
226
+ | **codex** | `~/.codex/sessions` rollouts (JSONL) | file scan + parse |
227
+ | **antigravity** | Antigravity SQLite + session JSON | proto + scanner |
228
+
229
+ All three emit the same `UsageRecord` shape and are merged by `Aggregator → CachedUsageSource → Dashboard`. `doctor` reports each independently:
230
+
231
+ ```sh
232
+ oc2token doctor
233
+ # oc2token doctor: OK
234
+ # ✓ opencode: OpenCode 2 service OK
235
+ # ✓ codex: 14 sessions scanned
236
+ # ✓ antigravity: 3 projects indexed
237
+ ```
238
+
239
+ ---
240
+
241
+ ## 📊 Output formats
242
+
243
+ ### Table (pipes & logs)
244
+
245
+ No ANSI, deterministic — safe for `grep`, `awk`, `less`:
246
+
247
+ ```sh
248
+ oc2token --format table
249
+ ```
250
+
251
+ <p align="center"><img src="assets/screenshots/table.svg" width="100%" alt="Table output" /></p>
252
+
253
+ ### JSON (`schemaVersion: 3`)
254
+
255
+ ```sh
256
+ oc2token --json | jq .
257
+ oc2token --once --json > snapshot.json
258
+ ```
259
+
260
+ Emits **all three windows**, `totalsByProvider`/`totalsByProject`, trends, and coverage in one stable contract:
261
+
262
+ ```json
263
+ {
264
+ "schemaVersion": 3,
265
+ "source": "unified",
266
+ "version": "0.1.0",
267
+ "windows": { "hour": { "kind": "hour", "from": "…", "to": "…", "label": "last 60 minutes" }, "day": {}, "week": {} },
268
+ "totals": { "hour": { "recorded_total": 15582 }, "day": {}, "week": {} },
269
+ "providersByWindow": { "day": [ { "name": "openai", "totals": {} } ] },
270
+ "totalsByProvider": { "day": { "openai": { "recorded_total": 21785 } } },
271
+ "coverage": { "complete": true, "sessionsScanned": 14, "sessionsDiscovered": 14 }
272
+ }
273
+ ```
274
+
275
+ Use `--format json` for pretty-printed JSON without exiting interactive mode fallback.
276
+
277
+ ---
278
+
279
+ ## ⚙️ Configuration
280
+
281
+ | Option | CLI | Env / Persisted |
282
+ |--------|-----|-----------------|
283
+ | Timezone | `--timezone Europe/Berlin` | Local system zone by default; validated as IANA |
284
+ | Refresh | `--refresh 300` (60–14400, `0` manual) | Persisted via Settings panel (`s`) |
285
+ | Cache dir | `--cache-dir /tmp/oc2` | `~/.cache/oc2token` / `$XDG_CACHE_HOME/oc2token` |
286
+ | Project filter | `--project my-project` | Scopes OpenCode collection only |
287
+ | Providers | `--source codex` | Persisted via Settings panel |
288
+ | Color | `--no-color` | `NO_COLOR=1` disables all ANSI |
289
+
290
+ A corrupt or future cache is ignored — the live service is always the authority; the cache is an optimization.
291
+
292
+ ---
293
+
294
+ ## 🔒 Privacy
295
+
296
+ OC2Token stores **only normalized usage metadata**:
297
+
298
+ - `input`, `output`, `reasoning`, `cacheRead`, `cacheWrite`, `recorded_total`, model / provider / project names, timestamps
299
+
300
+ It **does not** persist:
301
+
302
+ - prompts, tool input/output, API keys, session titles, message bodies
303
+
304
+ All collection is local-first; no network calls besides the loopback OpenCode service.
305
+
306
+ ---
307
+
308
+ ## 🧭 How it works
309
+
310
+ ```
311
+ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐
312
+ │ opencode │ │ codex │ │ antigravity │
313
+ │ transport │ │ scanner │ │ scanner │
314
+ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘
315
+ │ │ │
316
+ └────────────────┼────────────────┘
317
+ ▼
318
+ ┌─────────────┐
319
+ │ Aggregator │ dedup by sessionID/messageID + tokenRevision
320
+ └──────┬──────┘
321
+ ▼
322
+ ┌──────────────┐ file lock · schema 1 · atomic writes
323
+ │ CachedUsage │
324
+ └──────┬──────┘
325
+ ▼
326
+ ┌───────────────────┐
327
+ │ dashboard / json │ normalize → renderDashboard | renderTable | renderJSON
328
+ └───────────────────┘
329
+ ```
330
+
331
+ - Windows are derived with `Intl.DateTimeFormat` — including DST-aware local-day resolution.
332
+ - Filtered stats responses are range-validated; if the service ignores the range, OC2Token falls back to full pagination rather than mislabelling all-time usage as filtered.
333
+
334
+ ---
335
+
336
+ ## 🛠️ Development
337
+
338
+ ```sh
339
+ npm install
340
+ npm test # build + node --test
341
+ npm run typecheck
342
+ npm run pack:check # dry-run npm pack
343
+ npm run build
344
+ ```
345
+
346
+ Project layout:
347
+
348
+ ```
349
+ src/
350
+ accounting/ reducer for normalized records
351
+ collector/ parallel/serial message collection
352
+ opencode/ transport & doctor
353
+ codex/ rollout discovery & scanner
354
+ antigravity/ sqlite + proto + scanner
355
+ cache/ filesystem lock & store (schema 1)
356
+ dashboard/ render, state, settings
357
+ domain/ windows, tokens, records, contracts
358
+ output/ dashboard / table / json renderers
359
+ ```
360
+
361
+ **Testing.** The suite uses `node:test` with a serial oracle vs parallel aggregator equivalence check, ANSI-gating invariants, and window-boundary coverage.
362
+
363
+ ---
364
+
365
+ ## 🤝 Contributing
366
+
367
+ Contributions are welcome — bug reports, new provider adapters, render polish, docs.
368
+
369
+ 1. Fork & create a feature branch
370
+ 2. Add tests for new behaviour (`npm test`)
371
+ 3. Run `npm run typecheck`
372
+ 4. Open a PR with a clear description and screenshots for UI changes
373
+
374
+ Please read the [MIT License](LICENSE). By contributing you agree to license your contributions under the same terms.
375
+
376
+ > Be kind. This project follows the spirit of the [Contributor Covenant](https://www.contributor-covenant.org/).
377
+
378
+ ---
379
+
380
+ ## 🙏 Acknowledgments
381
+
382
+ - [OpenCode 2](https://github.com/sst/opencode) for the local token telemetry API
383
+ - The CodeBurn palette inspiration (violet · orange · cyan)
384
+ - Built with TypeScript, `node:test`, and a lot of terminal love
385
+
386
+ ---
387
+
388
+ ## 📄 License
389
+
390
+ MIT © 2026 [Kaan Yaren](https://github.com/kaanyaren)
391
+
392
+ See [LICENSE](LICENSE) for details.
393
+
394
+ ---
395
+
396
+ <p align="center">
397
+ <sub>OC2Token is not affiliated with OpenCode. <code>recorded_total</code> is an accounting sum — not a billing total.</sub><br/>
398
+ <sub><a href="https://github.com/kaanyaren/OC2Token">github.com/kaanyaren/OC2Token</a> · <code>npm i -g oc2token</code></sub>
399
+ </p>
@@ -0,0 +1 @@
1
+ export { UsageRecordReducer, compareFreshness, reduceUsageRecords, sumRecordComponents, } from "./reducer.js";
@@ -0,0 +1,2 @@
1
+ export { UsageRecordReducer, compareFreshness, reduceUsageRecords, sumRecordComponents, } from "./reducer.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/accounting/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { type UsageTotals } from "../domain/index.js";
2
+ import { type UsageRecord, type UsageWindow } from "../domain/index.js";
3
+ /**
4
+ * A deterministic, synchronous reducer for usage records.
5
+ *
6
+ * Workers never mutate this reducer while they are reading pages. Callers can
7
+ * feed records in any completion order and the winner is selected by observed
8
+ * time, revision, and finally completeness, rather than arrival order.
9
+ */
10
+ export declare class UsageRecordReducer {
11
+ private readonly byKey;
12
+ upsert(record: UsageRecord): boolean;
13
+ upsertMany(records: ReadonlyArray<UsageRecord>): void;
14
+ get size(): number;
15
+ get(key: string): UsageRecord | undefined;
16
+ records(): ReadonlyArray<UsageRecord>;
17
+ totals(window: UsageWindow, includeProvisional?: boolean): UsageTotals;
18
+ }
19
+ export declare function compareFreshness(left: UsageRecord, right: UsageRecord): number;
20
+ export declare function reduceUsageRecords(records: ReadonlyArray<UsageRecord>): ReadonlyArray<UsageRecord>;
21
+ export declare function sumRecordComponents(records: ReadonlyArray<UsageRecord>): UsageTotals;
@@ -0,0 +1,105 @@
1
+ import { parseTokenComponents, toUsageTotals, } from "../domain/index.js";
2
+ import { sumUsageRecords, } from "../domain/index.js";
3
+ const COMPONENTS = ["input", "output", "reasoning", "cacheRead", "cacheWrite"];
4
+ function compareRevision(left, right) {
5
+ if (left === right) {
6
+ return 0;
7
+ }
8
+ // Explicit numeric revisions are common in test transports and are the only
9
+ // safe way to compare revisions such as 9 and 10 lexically.
10
+ const leftNumber = /(?:^|:)(?:v)?(\d+)$/.exec(left)?.[1];
11
+ const rightNumber = /(?:^|:)(?:v)?(\d+)$/.exec(right)?.[1];
12
+ if (leftNumber !== undefined && rightNumber !== undefined) {
13
+ const difference = Number(leftNumber) - Number(rightNumber);
14
+ if (difference !== 0) {
15
+ return difference;
16
+ }
17
+ }
18
+ const leftScan = /^scan:(\d+):/.exec(left)?.[1];
19
+ const rightScan = /^scan:(\d+):/.exec(right)?.[1];
20
+ if (leftScan !== undefined && rightScan !== undefined) {
21
+ const difference = Number(leftScan) - Number(rightScan);
22
+ if (difference !== 0)
23
+ return difference;
24
+ }
25
+ return left < right ? -1 : 1;
26
+ }
27
+ /**
28
+ * A deterministic, synchronous reducer for usage records.
29
+ *
30
+ * Workers never mutate this reducer while they are reading pages. Callers can
31
+ * feed records in any completion order and the winner is selected by observed
32
+ * time, revision, and finally completeness, rather than arrival order.
33
+ */
34
+ export class UsageRecordReducer {
35
+ byKey = new Map();
36
+ upsert(record) {
37
+ const previous = this.byKey.get(record.key);
38
+ if (previous === undefined || compareFreshness(record, previous) > 0) {
39
+ this.byKey.set(record.key, record);
40
+ return true;
41
+ }
42
+ return false;
43
+ }
44
+ upsertMany(records) {
45
+ for (const record of records) {
46
+ this.upsert(record);
47
+ }
48
+ }
49
+ get size() {
50
+ return this.byKey.size;
51
+ }
52
+ get(key) {
53
+ return this.byKey.get(key);
54
+ }
55
+ records() {
56
+ return [...this.byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
57
+ }
58
+ totals(window, includeProvisional = false) {
59
+ return sumUsageRecords(this.records(), window, { includeProvisional });
60
+ }
61
+ }
62
+ export function compareFreshness(left, right) {
63
+ const observedDifference = left.observedAt.getTime() - right.observedAt.getTime();
64
+ if (observedDifference !== 0) {
65
+ return observedDifference;
66
+ }
67
+ const revisionDifference = compareRevision(left.tokenRevision, right.tokenRevision);
68
+ if (revisionDifference !== 0) {
69
+ return revisionDifference;
70
+ }
71
+ // A final observation supersedes a provisional observation at the same
72
+ // revision. This matters when a streaming message settles in one refresh.
73
+ if (left.completeness !== right.completeness) {
74
+ return left.completeness === "final" ? 1 : -1;
75
+ }
76
+ // Identical revisions should be byte-equivalent for accounting purposes. A
77
+ // stable key tie-break keeps the comparator total without using arrival order.
78
+ return left.key.localeCompare(right.key);
79
+ }
80
+ export function reduceUsageRecords(records) {
81
+ const reducer = new UsageRecordReducer();
82
+ reducer.upsertMany(records);
83
+ return reducer.records();
84
+ }
85
+ export function sumRecordComponents(records) {
86
+ const totals = {
87
+ input: 0,
88
+ output: 0,
89
+ reasoning: 0,
90
+ cacheRead: 0,
91
+ cacheWrite: 0,
92
+ };
93
+ for (const record of records) {
94
+ const normalized = parseTokenComponents(record);
95
+ for (const component of COMPONENTS) {
96
+ const next = totals[component] + normalized[component];
97
+ if (!Number.isSafeInteger(next)) {
98
+ throw new RangeError(`Summing ${component} exceeds the safe integer range`);
99
+ }
100
+ totals[component] = next;
101
+ }
102
+ }
103
+ return toUsageTotals(totals);
104
+ }
105
+ //# sourceMappingURL=reducer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reducer.js","sourceRoot":"","sources":["../../../src/accounting/reducer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,aAAa,GAId,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,eAAe,GAEhB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,CAAU,CAAC;AAExF,SAAS,eAAe,CAAC,IAAY,EAAE,KAAa;IAClD,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,4EAA4E;IAC5E,4DAA4D;IAC5D,MAAM,UAAU,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3D,IAAI,UAAU,KAAK,SAAS,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QAC5D,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;YACrB,OAAO,UAAU,CAAC;QACpB,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QACtD,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QACxD,IAAI,UAAU,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;IAC1C,CAAC;IAED,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,kBAAkB;IACZ,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAExD,MAAM,CAAC,MAAmB;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5C,IAAI,QAAQ,KAAK,SAAS,IAAI,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACrE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,UAAU,CAAC,OAAmC;QAC5C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO;QACL,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,CAAC,MAAmB,EAAE,kBAAkB,GAAG,KAAK;QACpD,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;IACzE,CAAC;CACF;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAiB,EAAE,KAAkB;IACpE,MAAM,kBAAkB,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;IAClF,IAAI,kBAAkB,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,MAAM,kBAAkB,GAAG,eAAe,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IACpF,IAAI,kBAAkB,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,uEAAuE;IACvE,0EAA0E;IAC1E,IAAI,IAAI,CAAC,YAAY,KAAK,KAAK,CAAC,YAAY,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAC,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,2EAA2E;IAC3E,+EAA+E;IAC/E,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,OAAmC;IAEnC,MAAM,OAAO,GAAG,IAAI,kBAAkB,EAAE,CAAC;IACzC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAmC;IACrE,MAAM,MAAM,GAAuC;QACjD,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC;QACT,SAAS,EAAE,CAAC;QACZ,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;KACd,CAAC;IAEF,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAChD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;YACvD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,UAAU,CAAC,WAAW,SAAS,iCAAiC,CAAC,CAAC;YAC9E,CAAC;YACD,MAAM,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare function defaultAntigravityHome(): string;
2
+ /**
3
+ * Scan Antigravity SQLite conversation databases.
4
+ *
5
+ * Mirrors codeburn `conversationRoots()` (dist/main.js:1674) scanning:
6
+ * - ~/.gemini/antigravity/conversations
7
+ * - ~/.gemini/antigravity-cli/conversations
8
+ * - ~/.gemini/antigravity-cli/implicit
9
+ * - ~/.gemini/antigravity-ide/conversations
10
+ * - ~/.gemini/antigravity-ide/implicit
11
+ *
12
+ * Only `*.db` files are returned; `-shm`/`-wal` sidecars are ignored
13
+ * (they do not end with `.db`). Supports `ANTIGRAVITY_HOME` override via
14
+ * `resolveAntigravityHome` or explicit `root` parameter.
15
+ * For test fixtures `ANTIGRAVITY_HOME` may point directly to a directory
16
+ * containing `*.db` files — that directory is scanned as well.
17
+ */
18
+ export declare function discoverAntigravityDatabases(root?: string): string[];
@@ -0,0 +1,95 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { readdirSync } from "node:fs";
4
+ export function defaultAntigravityHome() {
5
+ return join(homedir(), ".gemini");
6
+ }
7
+ function resolveAntigravityHome(root) {
8
+ if (root !== undefined) {
9
+ return root;
10
+ }
11
+ const env = process.env.ANTIGRAVITY_HOME;
12
+ if (typeof env === "string" && env.trim().length > 0) {
13
+ return env;
14
+ }
15
+ return defaultAntigravityHome();
16
+ }
17
+ /**
18
+ * Scan Antigravity SQLite conversation databases.
19
+ *
20
+ * Mirrors codeburn `conversationRoots()` (dist/main.js:1674) scanning:
21
+ * - ~/.gemini/antigravity/conversations
22
+ * - ~/.gemini/antigravity-cli/conversations
23
+ * - ~/.gemini/antigravity-cli/implicit
24
+ * - ~/.gemini/antigravity-ide/conversations
25
+ * - ~/.gemini/antigravity-ide/implicit
26
+ *
27
+ * Only `*.db` files are returned; `-shm`/`-wal` sidecars are ignored
28
+ * (they do not end with `.db`). Supports `ANTIGRAVITY_HOME` override via
29
+ * `resolveAntigravityHome` or explicit `root` parameter.
30
+ * For test fixtures `ANTIGRAVITY_HOME` may point directly to a directory
31
+ * containing `*.db` files — that directory is scanned as well.
32
+ */
33
+ export function discoverAntigravityDatabases(root) {
34
+ const base = resolveAntigravityHome(root);
35
+ const seen = new Set();
36
+ const result = [];
37
+ const tryDir = (dir) => {
38
+ let entries;
39
+ try {
40
+ entries = readdirSync(dir);
41
+ }
42
+ catch {
43
+ return;
44
+ }
45
+ for (const file of entries) {
46
+ // Only *.db — ignore -shm/-wal sidecars and .pb files
47
+ if (!file.toLowerCase().endsWith(".db")) {
48
+ continue;
49
+ }
50
+ // Extra guard for sidecars that might be named *.db-shm / *.db-wal
51
+ const lower = file.toLowerCase();
52
+ if (lower.endsWith("-shm") || lower.endsWith("-wal") || lower.endsWith(".db-shm") || lower.endsWith(".db-wal")) {
53
+ continue;
54
+ }
55
+ const full = join(dir, file);
56
+ if (seen.has(full)) {
57
+ continue;
58
+ }
59
+ seen.add(full);
60
+ result.push(full);
61
+ }
62
+ };
63
+ // Direct base scan — handles ANTIGRAVITY_HOME pointing to a conversations dir
64
+ // or a test fixture containing .db files flat.
65
+ tryDir(base);
66
+ // Known relative paths (codeburn conversationRoots) relative to homedir base.
67
+ // We probe both interpretations of `base`:
68
+ // - base is homedir (e.g. /Users/you) -> join(base, ".gemini", ...)
69
+ // - base is .gemini dir (e.g. /Users/you/.gemini) -> join(base, "antigravity", ...)
70
+ const relativeFromHomedir = [
71
+ join(".gemini", "antigravity", "conversations"),
72
+ join(".gemini", "antigravity-cli", "conversations"),
73
+ join(".gemini", "antigravity-cli", "implicit"),
74
+ join(".gemini", "antigravity-ide", "conversations"),
75
+ join(".gemini", "antigravity-ide", "implicit"),
76
+ ];
77
+ const relativeFromGemini = [
78
+ join("antigravity", "conversations"),
79
+ join("antigravity-cli", "conversations"),
80
+ join("antigravity-cli", "implicit"),
81
+ join("antigravity-ide", "conversations"),
82
+ join("antigravity-ide", "implicit"),
83
+ ];
84
+ for (const rel of relativeFromHomedir) {
85
+ tryDir(join(base, rel));
86
+ }
87
+ for (const rel of relativeFromGemini) {
88
+ tryDir(join(base, rel));
89
+ }
90
+ // Also handle case where base is already a .gemini/antigravity-cli/conversations path's parent
91
+ // by not duplicating — seen set handles it.
92
+ result.sort();
93
+ return result;
94
+ }
95
+ //# sourceMappingURL=discovery.js.map