tokmon 0.20.0 → 0.20.1
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 +19 -8
- package/dist/{bootstrap-ink-R3PFUV5N.js → bootstrap-ink-AO3QA5BH.js} +5 -7
- package/dist/{chunk-MB6LRSEZ.js → chunk-5BW4H7WW.js} +116 -60
- package/dist/{chunk-A6RQRHRO.js → chunk-DZY72PPB.js} +10 -12
- package/dist/{chunk-MVMPQJ5S.js → chunk-XQEJ4WQ5.js} +9 -3
- package/dist/cli.js +4 -4
- package/dist/{config-64VVUH42.js → config-C6Z65JUP.js} +5 -3
- package/dist/{daemon-UB2PZDX6.js → daemon-OBQJO6D4.js} +3 -3
- package/dist/server-RL2JDFQY.js +9 -0
- package/dist/web/assets/{breakdown-DEws2QQ3.js → breakdown-Dhq6Rqnu.js} +1 -1
- package/dist/web/assets/{chart-CFynoGh5.js → chart-CDwPcOeB.js} +1 -1
- package/dist/web/assets/{index-vqWRGYnh.js → index-Bqe9b8BZ.js} +23 -23
- package/dist/web/assets/{timeline-a7y6nopU.js → timeline-D0GFoRzM.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -2
- package/dist/server-SP3FOHM2.js +0 -9
package/README.md
CHANGED
|
@@ -96,25 +96,25 @@ It binds to `127.0.0.1` only and reads the same data read-only — nothing leave
|
|
|
96
96
|
|
|
97
97
|
KPIs with inline sparklines, provider cards with live rate-limit bars, and a cost-over-time chart that spans your full history by default. Toggle **merged** (one combined total) vs **split** (a line per provider), **all-time** vs the selected period, and linear vs log.
|
|
98
98
|
|
|
99
|
-

|
|
100
100
|
|
|
101
101
|
### Analytics
|
|
102
102
|
|
|
103
103
|
A full-width, all-time daily-spend calendar — hover any day for a per-model spend breakdown — with at-a-glance stats (busiest day, daily average, top weekday, current streak), alongside cost-by-model, an interactive provider split, token composition, cache savings, and cumulative spend.
|
|
104
104
|
|
|
105
|
-

|
|
106
106
|
|
|
107
107
|
### Models
|
|
108
108
|
|
|
109
109
|
A leaderboard sortable by cost / tokens / calls, each row showing a per-model trend sparkline, cost-per-call, tokens, and calls — over tokens-by-model and cache-savings-by-model charts.
|
|
110
110
|
|
|
111
|
-

|
|
112
112
|
|
|
113
113
|
### Explore
|
|
114
114
|
|
|
115
115
|
The full daily / weekly / monthly table — searchable, sortable on every column, with expandable per-model breakdowns.
|
|
116
116
|
|
|
117
|
-

|
|
118
118
|
|
|
119
119
|
The dashboard is a prebuilt static bundle shipped in the package — no build step, fully offline.
|
|
120
120
|
|
|
@@ -219,12 +219,23 @@ tokmon runs entirely on your machine and reads everything **read-only**:
|
|
|
219
219
|
|
|
220
220
|
## How It Works
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
tokmon runs a small local **daemon** that does all the data collection. The terminal UI and the web dashboard are both thin clients of it, talking over a loopback-only WebSocket — so a single process does the work and the TUI and web always show the same numbers. The daemon starts automatically with the TUI (and standalone via `tokmon serve`), and idle-pauses when nothing is watching.
|
|
223
|
+
|
|
224
|
+
**Usage & cost**
|
|
225
|
+
- Parses each tool's local session logs — Claude / Codex / pi `JSONL`, Cursor / opencode `SQLite` — and aggregates cost and token usage per day, week, and month.
|
|
226
|
+
- Cost is an API-equivalent estimate from each model's published pricing, counting cached input at the discounted cache-read rate (not the full input rate, not free).
|
|
223
227
|
- A persistent parse cache keyed by file **mtime + size** makes repeat launches near-instant; edited or deleted files are re-read automatically.
|
|
224
|
-
- Dashboard summaries and table history load independently, so the UI stays responsive on large histories.
|
|
225
|
-
- Rate limits and spend are fetched from each provider's API on the billing poll interval.
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
**Accounts**
|
|
230
|
+
- Each enabled provider is detected automatically, and its real account identity — email and plan — is read from local auth (e.g. Claude `~/.claude.json`, the Codex `id_token`, Cursor's state DB). Extra accounts, like additional Claude homes, are auto-discovered too.
|
|
231
|
+
|
|
232
|
+
**Limits & billing**
|
|
233
|
+
- Rate limits and remaining spend/quota come from each provider's own official API, refreshed on the billing poll interval.
|
|
234
|
+
|
|
235
|
+
**Responsiveness**
|
|
236
|
+
- Dashboard summaries and table history load independently and refresh on separate intervals, so the UI stays responsive even on large histories.
|
|
237
|
+
|
|
238
|
+
Cross-platform: macOS, Linux, Windows. Everything is local and read-only — see [Privacy](#privacy).
|
|
228
239
|
|
|
229
240
|
## Requirements
|
|
230
241
|
|
|
@@ -18,11 +18,10 @@ import {
|
|
|
18
18
|
systemTimezone,
|
|
19
19
|
time,
|
|
20
20
|
tokens
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-5BW4H7WW.js";
|
|
22
22
|
import {
|
|
23
23
|
COLOR_PALETTE,
|
|
24
24
|
DEFAULTS,
|
|
25
|
-
cacheDir,
|
|
26
25
|
configLocation,
|
|
27
26
|
generateAccountId,
|
|
28
27
|
getTrackedAccountRows,
|
|
@@ -31,8 +30,9 @@ import {
|
|
|
31
30
|
normalizeConfig,
|
|
32
31
|
pickAccentColor,
|
|
33
32
|
sanitizeTyped,
|
|
34
|
-
saveConfigSync
|
|
35
|
-
|
|
33
|
+
saveConfigSync,
|
|
34
|
+
snapshotCacheFile
|
|
35
|
+
} from "./chunk-XQEJ4WQ5.js";
|
|
36
36
|
import {
|
|
37
37
|
glyphs
|
|
38
38
|
} from "./chunk-RF4GGQGM.js";
|
|
@@ -82,8 +82,6 @@ function reconcileDaemonConfig(previous, daemonConfig, pendingLocalConfig) {
|
|
|
82
82
|
|
|
83
83
|
// src/client/seed-cache.ts
|
|
84
84
|
import { readFile } from "fs/promises";
|
|
85
|
-
import { join } from "path";
|
|
86
|
-
var snapshotCacheFile = () => join(cacheDir(), "web-snapshot.json");
|
|
87
85
|
async function loadSeedSnapshot() {
|
|
88
86
|
try {
|
|
89
87
|
const parsed = JSON.parse(await readFile(snapshotCacheFile(), "utf-8"));
|
|
@@ -3196,7 +3194,7 @@ function App({ interval: cliInterval, initialConfig, baseUrl = null, wsToken = n
|
|
|
3196
3194
|
if (webStartingRef.current) return;
|
|
3197
3195
|
webStartingRef.current = true;
|
|
3198
3196
|
try {
|
|
3199
|
-
const { startWebServer } = await import("./server-
|
|
3197
|
+
const { startWebServer } = await import("./server-RL2JDFQY.js");
|
|
3200
3198
|
const ctrl = await startWebServer({ config: cfg, log: false });
|
|
3201
3199
|
webRef.current = ctrl;
|
|
3202
3200
|
openUrl(ctrl.url);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
PROVIDER_IDS,
|
|
3
4
|
cacheDir,
|
|
4
5
|
envDir,
|
|
5
6
|
expandHome,
|
|
6
7
|
isValidTimezone,
|
|
7
8
|
slugify
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XQEJ4WQ5.js";
|
|
9
10
|
|
|
10
11
|
// src/providers/usage-core.ts
|
|
11
12
|
import { readFile, writeFile, rename, mkdir } from "fs/promises";
|
|
@@ -112,6 +113,31 @@ function weekKey(ts, tz) {
|
|
|
112
113
|
return dayKey(startOfWeek(ts, tz), tz);
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// src/providers/_shared/metric.ts
|
|
117
|
+
var finite = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
118
|
+
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
119
|
+
function finitePositive(value) {
|
|
120
|
+
return finiteNumber(value) && value > 0 ? value : 0;
|
|
121
|
+
}
|
|
122
|
+
function safeNum(value) {
|
|
123
|
+
return finiteNumber(value) && value > 0 ? Math.floor(value) : 0;
|
|
124
|
+
}
|
|
125
|
+
function finitePositiveCoerced(value) {
|
|
126
|
+
const n = Number(value);
|
|
127
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
128
|
+
}
|
|
129
|
+
function percentMetric(label, used, resetsAt, primary) {
|
|
130
|
+
return {
|
|
131
|
+
label,
|
|
132
|
+
used: finite(used),
|
|
133
|
+
limit: 100,
|
|
134
|
+
format: { kind: "percent" },
|
|
135
|
+
resetsAt,
|
|
136
|
+
...primary === void 0 ? {} : { primary }
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var dollars = (cents) => finite(cents) / 100;
|
|
140
|
+
|
|
115
141
|
// src/providers/usage-core.ts
|
|
116
142
|
var SPARK_DAYS = 14;
|
|
117
143
|
var DAY_MS = 864e5;
|
|
@@ -226,9 +252,6 @@ async function loadCachedEntries(files, parse, since) {
|
|
|
226
252
|
if (dirty) scheduleFlush();
|
|
227
253
|
return dedupe(chunks.flat().filter((e) => e.ts >= since));
|
|
228
254
|
}
|
|
229
|
-
function safeNum(v) {
|
|
230
|
-
return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
|
|
231
|
-
}
|
|
232
255
|
function dashboardSince(tz) {
|
|
233
256
|
const now = Date.now();
|
|
234
257
|
return Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * DAY_MS);
|
|
@@ -236,9 +259,6 @@ function dashboardSince(tz) {
|
|
|
236
259
|
function tableSince(tz) {
|
|
237
260
|
return monthsAgoStart(Date.now(), 6, tz);
|
|
238
261
|
}
|
|
239
|
-
function finitePositive(v) {
|
|
240
|
-
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
241
|
-
}
|
|
242
262
|
function cleanEntry(e) {
|
|
243
263
|
return {
|
|
244
264
|
...e,
|
|
@@ -495,20 +515,6 @@ async function readJson(res) {
|
|
|
495
515
|
}
|
|
496
516
|
}
|
|
497
517
|
|
|
498
|
-
// src/providers/_shared/metric.ts
|
|
499
|
-
var finite = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
500
|
-
function percentMetric(label, used, resetsAt, primary) {
|
|
501
|
-
return {
|
|
502
|
-
label,
|
|
503
|
-
used: finite(used),
|
|
504
|
-
limit: 100,
|
|
505
|
-
format: { kind: "percent" },
|
|
506
|
-
resetsAt,
|
|
507
|
-
...primary === void 0 ? {} : { primary }
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
var dollars = (cents) => finite(cents) / 100;
|
|
511
|
-
|
|
512
518
|
// src/providers/_shared/time.ts
|
|
513
519
|
function msToIso(ms) {
|
|
514
520
|
return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
|
|
@@ -635,7 +641,6 @@ async function cursorActivity(homeDir) {
|
|
|
635
641
|
var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
|
|
636
642
|
var USAGE_URL = `${BASE}/GetCurrentPeriodUsage`;
|
|
637
643
|
var PLAN_URL = `${BASE}/GetPlanInfo`;
|
|
638
|
-
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
639
644
|
function cursorStateDb(homeDir) {
|
|
640
645
|
const base = homeDir ?? homedir2();
|
|
641
646
|
const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
|
|
@@ -786,10 +791,6 @@ async function cursorBillingCore(account) {
|
|
|
786
791
|
}
|
|
787
792
|
|
|
788
793
|
// src/providers/cursor/composer.ts
|
|
789
|
-
var finiteNonNegative = (value) => {
|
|
790
|
-
const n = Number(value);
|
|
791
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
792
|
-
};
|
|
793
794
|
async function cursorModelSpend(homeDir) {
|
|
794
795
|
const db = cursorStateDb(homeDir);
|
|
795
796
|
const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
|
|
@@ -798,9 +799,9 @@ async function cursorModelSpend(homeDir) {
|
|
|
798
799
|
const models = [];
|
|
799
800
|
let total = 0;
|
|
800
801
|
for (const row of res.rows) {
|
|
801
|
-
const usd =
|
|
802
|
+
const usd = finitePositiveCoerced(row.cents) / 100;
|
|
802
803
|
if (usd <= 0) continue;
|
|
803
|
-
models.push({ name: String(row.name ?? ""), usd, requests:
|
|
804
|
+
models.push({ name: String(row.name ?? ""), usd, requests: finitePositiveCoerced(row.amt) });
|
|
804
805
|
total += usd;
|
|
805
806
|
}
|
|
806
807
|
if (total <= 0) return null;
|
|
@@ -830,8 +831,8 @@ async function cursorUsageTable(tz, homeDir) {
|
|
|
830
831
|
for (const r of res.rows) {
|
|
831
832
|
const ts = Number(r.createdAt);
|
|
832
833
|
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
833
|
-
const usd =
|
|
834
|
-
const reqs =
|
|
834
|
+
const usd = finitePositiveCoerced(r.cents) / 100;
|
|
835
|
+
const reqs = finitePositiveCoerced(r.amt);
|
|
835
836
|
if (usd <= 0 && reqs <= 0) continue;
|
|
836
837
|
const model = String(r.model ?? "unknown");
|
|
837
838
|
put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
|
|
@@ -1180,7 +1181,7 @@ var PRICING2 = {
|
|
|
1180
1181
|
var ZERO_PRICE2 = { in: 0, cr: 0, out: 0 };
|
|
1181
1182
|
var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
|
|
1182
1183
|
function codexHomes(homeDir) {
|
|
1183
|
-
if (homeDir) return [join6(homeDir, ".codex")];
|
|
1184
|
+
if (homeDir) return [.../* @__PURE__ */ new Set([join6(homeDir, ".codex"), homeDir])];
|
|
1184
1185
|
const homes = [];
|
|
1185
1186
|
const codexHome = envDir("CODEX_HOME");
|
|
1186
1187
|
if (codexHome) homes.push(codexHome);
|
|
@@ -2559,6 +2560,9 @@ import { homedir as homedir11 } from "os";
|
|
|
2559
2560
|
function geminiCredsPath(homeDir) {
|
|
2560
2561
|
return join13(homeDir ?? homedir11(), ".gemini", "oauth_creds.json");
|
|
2561
2562
|
}
|
|
2563
|
+
function hasGeminiApiKey() {
|
|
2564
|
+
return ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_GENAI_API_KEY"].some((name) => typeof process.env[name] === "string" && process.env[name].trim() !== "");
|
|
2565
|
+
}
|
|
2562
2566
|
async function detectGemini(homeDir) {
|
|
2563
2567
|
try {
|
|
2564
2568
|
await access8(geminiCredsPath(homeDir));
|
|
@@ -2595,7 +2599,14 @@ async function geminiBilling(account) {
|
|
|
2595
2599
|
const identity = geminiIdentity(creds);
|
|
2596
2600
|
const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
|
|
2597
2601
|
const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
|
|
2598
|
-
if (!creds || !accessToken && !refreshToken)
|
|
2602
|
+
if (!creds || !accessToken && !refreshToken) {
|
|
2603
|
+
return {
|
|
2604
|
+
plan: null,
|
|
2605
|
+
metrics: [],
|
|
2606
|
+
error: hasGeminiApiKey() ? "API key auth \u2014 quota needs Google login (run gemini)" : "Not signed in \u2014 run gemini and log in with Google",
|
|
2607
|
+
...identity
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2599
2610
|
const quota = await fetchCloudCodeQuota({
|
|
2600
2611
|
accessToken,
|
|
2601
2612
|
refreshToken,
|
|
@@ -2714,7 +2725,7 @@ function installSignals(id) {
|
|
|
2714
2725
|
}
|
|
2715
2726
|
|
|
2716
2727
|
// src/providers/index.ts
|
|
2717
|
-
var PROVIDER_ORDER = [
|
|
2728
|
+
var PROVIDER_ORDER = [...PROVIDER_IDS];
|
|
2718
2729
|
var PROVIDERS = {
|
|
2719
2730
|
claude: claudeProvider,
|
|
2720
2731
|
codex: codexProvider,
|
|
@@ -2725,7 +2736,6 @@ var PROVIDERS = {
|
|
|
2725
2736
|
antigravity: antigravityProvider,
|
|
2726
2737
|
gemini: geminiProvider
|
|
2727
2738
|
};
|
|
2728
|
-
var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
|
|
2729
2739
|
async function detectProviders() {
|
|
2730
2740
|
const found = await Promise.all(
|
|
2731
2741
|
PROVIDER_ORDER.map(async (id) => {
|
|
@@ -2779,7 +2789,7 @@ function readClaudeIdentity2(homeDir) {
|
|
|
2779
2789
|
function hasClaudeState(homeDir) {
|
|
2780
2790
|
return existsSync(join15(homeDir, ".claude.json")) || existsSync(join15(homeDir, ".claude", ".credentials.json")) || existsSync(join15(homeDir, ".claude", "projects")) || existsSync(join15(homeDir, ".config", "claude", ".credentials.json")) || existsSync(join15(homeDir, ".config", "claude", "projects"));
|
|
2781
2791
|
}
|
|
2782
|
-
function candidateAlternateHomes() {
|
|
2792
|
+
function candidateAlternateHomes(prefix) {
|
|
2783
2793
|
const home = homedir13();
|
|
2784
2794
|
let entries;
|
|
2785
2795
|
try {
|
|
@@ -2788,8 +2798,9 @@ function candidateAlternateHomes() {
|
|
|
2788
2798
|
return [];
|
|
2789
2799
|
}
|
|
2790
2800
|
const out = [];
|
|
2801
|
+
const pattern = new RegExp(`^\\.${prefix}[_-]`);
|
|
2791
2802
|
for (const name of entries) {
|
|
2792
|
-
if (
|
|
2803
|
+
if (!pattern.test(name)) continue;
|
|
2793
2804
|
const path = join15(home, name);
|
|
2794
2805
|
try {
|
|
2795
2806
|
if (!statSync(path).isDirectory()) continue;
|
|
@@ -2806,10 +2817,57 @@ function labelForClaudeHome(homeDir) {
|
|
|
2806
2817
|
const raw = basename(homeDir).replace(/^\.claude[_-]?/, "").replace(/[_-]+/g, " ").trim();
|
|
2807
2818
|
return raw ? `Claude ${raw}` : "Claude";
|
|
2808
2819
|
}
|
|
2820
|
+
function decodeBase64UrlJson3(segment) {
|
|
2821
|
+
try {
|
|
2822
|
+
const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
2823
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
2824
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
2825
|
+
} catch {
|
|
2826
|
+
return null;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
function codexAuthPaths(homeDir) {
|
|
2830
|
+
return [join15(homeDir, ".codex", "auth.json"), join15(homeDir, "auth.json")];
|
|
2831
|
+
}
|
|
2832
|
+
function readCodexIdentity(homeDir) {
|
|
2833
|
+
for (const path of codexAuthPaths(homeDir)) {
|
|
2834
|
+
try {
|
|
2835
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
2836
|
+
const idToken = parsed?.tokens?.id_token;
|
|
2837
|
+
if (typeof idToken !== "string" || !idToken.includes(".")) continue;
|
|
2838
|
+
const payload = decodeBase64UrlJson3(idToken.split(".")[1]);
|
|
2839
|
+
if (!payload || typeof payload !== "object") continue;
|
|
2840
|
+
return {
|
|
2841
|
+
email: typeof payload?.email === "string" && payload.email.trim() ? payload.email.trim() : void 0,
|
|
2842
|
+
displayName: typeof payload?.name === "string" && payload.name.trim() ? payload.name.trim() : typeof payload?.given_name === "string" && payload.given_name.trim() ? payload.given_name.trim() : void 0
|
|
2843
|
+
};
|
|
2844
|
+
} catch {
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
return {};
|
|
2848
|
+
}
|
|
2849
|
+
function hasCodexAuth(homeDir) {
|
|
2850
|
+
for (const path of codexAuthPaths(homeDir)) {
|
|
2851
|
+
try {
|
|
2852
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
2853
|
+
const accessToken = parsed?.tokens?.access_token;
|
|
2854
|
+
if (typeof accessToken === "string" && accessToken.trim()) return true;
|
|
2855
|
+
} catch {
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
return false;
|
|
2859
|
+
}
|
|
2860
|
+
function labelForCodexHome(homeDir) {
|
|
2861
|
+
const identity = readCodexIdentity(homeDir);
|
|
2862
|
+
if (identity.email) return `Codex ${identity.email}`;
|
|
2863
|
+
if (identity.displayName) return `Codex ${identity.displayName}`;
|
|
2864
|
+
const raw = basename(homeDir).replace(/^\.codex[_-]?/, "").replace(/[_-]+/g, " ").trim();
|
|
2865
|
+
return raw ? `Codex ${raw}` : "Codex";
|
|
2866
|
+
}
|
|
2809
2867
|
function discoverClaudeAccounts(usedIds) {
|
|
2810
2868
|
const provider = PROVIDERS.claude;
|
|
2811
2869
|
const out = [];
|
|
2812
|
-
for (const homeDir of candidateAlternateHomes()) {
|
|
2870
|
+
for (const homeDir of candidateAlternateHomes("claude")) {
|
|
2813
2871
|
if (!hasClaudeState(homeDir)) continue;
|
|
2814
2872
|
const suffix = basename(homeDir).replace(/^\.claude[_-]?/, "") || basename(homeDir);
|
|
2815
2873
|
out.push({
|
|
@@ -2822,8 +2880,25 @@ function discoverClaudeAccounts(usedIds) {
|
|
|
2822
2880
|
}
|
|
2823
2881
|
return out;
|
|
2824
2882
|
}
|
|
2883
|
+
function discoverCodexAccounts(usedIds) {
|
|
2884
|
+
const provider = PROVIDERS.codex;
|
|
2885
|
+
const out = [];
|
|
2886
|
+
for (const homeDir of candidateAlternateHomes("codex")) {
|
|
2887
|
+
if (!hasCodexAuth(homeDir)) continue;
|
|
2888
|
+
const suffix = basename(homeDir).replace(/^\.codex[_-]?/, "") || basename(homeDir);
|
|
2889
|
+
out.push({
|
|
2890
|
+
id: uniqueId(`codex_${suffix}`, usedIds),
|
|
2891
|
+
providerId: "codex",
|
|
2892
|
+
name: labelForCodexHome(homeDir),
|
|
2893
|
+
color: provider.color,
|
|
2894
|
+
homeDir
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
return out;
|
|
2898
|
+
}
|
|
2825
2899
|
function discoverProviderAccounts(providerId, usedIds) {
|
|
2826
2900
|
if (providerId === "claude") return discoverClaudeAccounts(usedIds);
|
|
2901
|
+
if (providerId === "codex") return discoverCodexAccounts(usedIds);
|
|
2827
2902
|
return [];
|
|
2828
2903
|
}
|
|
2829
2904
|
function buildAccounts(config, detected) {
|
|
@@ -2868,12 +2943,6 @@ function accountsByProvider(accounts) {
|
|
|
2868
2943
|
return groups;
|
|
2869
2944
|
}
|
|
2870
2945
|
|
|
2871
|
-
// src/json-safe.ts
|
|
2872
|
-
function toJsonSafe(value) {
|
|
2873
|
-
const serialized = JSON.stringify(value);
|
|
2874
|
-
return serialized === void 0 ? null : JSON.parse(serialized);
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
2946
|
// src/peak.ts
|
|
2878
2947
|
async function fetchPeak() {
|
|
2879
2948
|
try {
|
|
@@ -2889,10 +2958,11 @@ async function fetchPeak() {
|
|
|
2889
2958
|
else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
|
|
2890
2959
|
else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
|
|
2891
2960
|
else return null;
|
|
2961
|
+
const minutesUntilChange = typeof data.minutesUntilChange === "number" && Number.isFinite(data.minutesUntilChange) ? data.minutesUntilChange : null;
|
|
2892
2962
|
return {
|
|
2893
2963
|
state,
|
|
2894
2964
|
label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
|
|
2895
|
-
minutesUntilChange
|
|
2965
|
+
minutesUntilChange
|
|
2896
2966
|
};
|
|
2897
2967
|
} catch {
|
|
2898
2968
|
return null;
|
|
@@ -2900,7 +2970,7 @@ async function fetchPeak() {
|
|
|
2900
2970
|
}
|
|
2901
2971
|
|
|
2902
2972
|
// src/rpc/contract.ts
|
|
2903
|
-
import { Schema
|
|
2973
|
+
import { Schema } from "effect";
|
|
2904
2974
|
import * as Rpc from "effect/unstable/rpc/Rpc";
|
|
2905
2975
|
import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
|
|
2906
2976
|
var TOKMON_WS_PATH = "/ws";
|
|
@@ -2919,16 +2989,6 @@ var RefreshScopeSchema = Schema.Literals([
|
|
|
2919
2989
|
"billing",
|
|
2920
2990
|
"peak"
|
|
2921
2991
|
]);
|
|
2922
|
-
var PROVIDER_IDS = [
|
|
2923
|
-
"claude",
|
|
2924
|
-
"codex",
|
|
2925
|
-
"cursor",
|
|
2926
|
-
"copilot",
|
|
2927
|
-
"pi",
|
|
2928
|
-
"opencode",
|
|
2929
|
-
"antigravity",
|
|
2930
|
-
"gemini"
|
|
2931
|
-
];
|
|
2932
2992
|
var ProviderIdSchema = Schema.Literals(PROVIDER_IDS);
|
|
2933
2993
|
var AccountSchema = Schema.Struct({
|
|
2934
2994
|
id: Schema.String,
|
|
@@ -2937,10 +2997,7 @@ var AccountSchema = Schema.Struct({
|
|
|
2937
2997
|
homeDir: Schema.String,
|
|
2938
2998
|
color: Schema.optionalKey(Schema.String)
|
|
2939
2999
|
});
|
|
2940
|
-
var jsonSafePassthrough = () => Schema.Unknown
|
|
2941
|
-
decode: SchemaGetter.transform((value) => value),
|
|
2942
|
-
encode: SchemaGetter.transform((value) => toJsonSafe(value))
|
|
2943
|
-
}));
|
|
3000
|
+
var jsonSafePassthrough = () => Schema.Unknown;
|
|
2944
3001
|
var ConfigSchema = Schema.Struct({
|
|
2945
3002
|
interval: Schema.Number,
|
|
2946
3003
|
billingInterval: Schema.Number,
|
|
@@ -3019,7 +3076,6 @@ export {
|
|
|
3019
3076
|
detectProviders,
|
|
3020
3077
|
buildAccounts,
|
|
3021
3078
|
accountsByProvider,
|
|
3022
|
-
toJsonSafe,
|
|
3023
3079
|
fetchPeak,
|
|
3024
3080
|
TOKMON_WS_PATH,
|
|
3025
3081
|
TOKMON_WS_METHODS,
|
|
@@ -7,16 +7,16 @@ import {
|
|
|
7
7
|
buildAccounts,
|
|
8
8
|
detectProviders,
|
|
9
9
|
fetchPeak,
|
|
10
|
-
resolveTimezone
|
|
11
|
-
|
|
12
|
-
} from "./chunk-MB6LRSEZ.js";
|
|
10
|
+
resolveTimezone
|
|
11
|
+
} from "./chunk-5BW4H7WW.js";
|
|
13
12
|
import {
|
|
14
13
|
cacheDir,
|
|
15
14
|
expandHome,
|
|
16
15
|
loadConfig,
|
|
17
16
|
normalizeConfig,
|
|
18
|
-
saveConfig
|
|
19
|
-
|
|
17
|
+
saveConfig,
|
|
18
|
+
snapshotCacheFile
|
|
19
|
+
} from "./chunk-XQEJ4WQ5.js";
|
|
20
20
|
|
|
21
21
|
// src/web/server.ts
|
|
22
22
|
import { createServer } from "http";
|
|
@@ -135,7 +135,7 @@ function assembleSnapshot(opts) {
|
|
|
135
135
|
color: namedHex(PROVIDERS[r.account.providerId].color)
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
-
return
|
|
138
|
+
return {
|
|
139
139
|
version: opts.version,
|
|
140
140
|
generatedAt: Date.now(),
|
|
141
141
|
tz: opts.tz,
|
|
@@ -144,7 +144,7 @@ function assembleSnapshot(opts) {
|
|
|
144
144
|
accounts,
|
|
145
145
|
seeded: opts.seeded ?? false,
|
|
146
146
|
peak: opts.peak ?? null
|
|
147
|
-
}
|
|
147
|
+
};
|
|
148
148
|
}
|
|
149
149
|
function tzFor(config) {
|
|
150
150
|
return resolveTimezone(config.timezone);
|
|
@@ -314,13 +314,11 @@ code{color:#e6b450}</style></head><body>
|
|
|
314
314
|
|
|
315
315
|
// src/web/data-engine.ts
|
|
316
316
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
317
|
-
import { join as join3 } from "path";
|
|
318
317
|
var TABLE_INTERVAL_MS = 3e5;
|
|
319
318
|
var PEAK_INTERVAL_MS = 3e5;
|
|
320
319
|
var IDLE_PAUSE_MS = 6e4;
|
|
321
320
|
var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
|
|
322
321
|
var REVEAL_THROTTLE_MS = 500;
|
|
323
|
-
var snapshotCacheFile = () => join3(cacheDir(), "web-snapshot.json");
|
|
324
322
|
function createDataEngine(opts) {
|
|
325
323
|
const { version } = opts;
|
|
326
324
|
let tz = opts.tz;
|
|
@@ -397,7 +395,7 @@ function createDataEngine(opts) {
|
|
|
397
395
|
lastPersist = Date.now();
|
|
398
396
|
try {
|
|
399
397
|
mkdirSync(cacheDir(), { recursive: true, mode: 448 });
|
|
400
|
-
const tmp =
|
|
398
|
+
const tmp = `${snapshotCacheFile()}.${process.pid}.tmp`;
|
|
401
399
|
writeFileSync(tmp, JSON.stringify(current), { mode: 384 });
|
|
402
400
|
renameSync(tmp, snapshotCacheFile());
|
|
403
401
|
} catch {
|
|
@@ -700,7 +698,7 @@ import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
|
700
698
|
// src/web/fs.ts
|
|
701
699
|
import { readdir, stat as stat2, realpath } from "fs/promises";
|
|
702
700
|
import { homedir } from "os";
|
|
703
|
-
import { join as
|
|
701
|
+
import { join as join3, resolve as resolvePath, isAbsolute, sep as sep2 } from "path";
|
|
704
702
|
function isContained(root, target) {
|
|
705
703
|
return target === root || target.startsWith(root + sep2);
|
|
706
704
|
}
|
|
@@ -723,7 +721,7 @@ async function listHomeDirectory(rawPath) {
|
|
|
723
721
|
for (const d of dirents) {
|
|
724
722
|
if (d.name.startsWith(".")) continue;
|
|
725
723
|
let dir = d.isDirectory();
|
|
726
|
-
const full =
|
|
724
|
+
const full = join3(abs, d.name);
|
|
727
725
|
if (d.isSymbolicLink()) {
|
|
728
726
|
let real2;
|
|
729
727
|
try {
|
|
@@ -6,6 +6,9 @@ import { mkdirSync, renameSync, writeFileSync } from "fs";
|
|
|
6
6
|
import { join, isAbsolute } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
|
|
9
|
+
// src/providers/types.ts
|
|
10
|
+
var PROVIDER_IDS = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
|
|
11
|
+
|
|
9
12
|
// src/config-schema.ts
|
|
10
13
|
var DEFAULTS = {
|
|
11
14
|
interval: 2,
|
|
@@ -23,8 +26,7 @@ var DEFAULTS = {
|
|
|
23
26
|
};
|
|
24
27
|
var LEGACY_KNOWN = ["claude", "codex", "cursor"];
|
|
25
28
|
var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
|
|
26
|
-
var PROVIDER_ORDER = [
|
|
27
|
-
var PROVIDER_IDS = [...PROVIDER_ORDER];
|
|
29
|
+
var PROVIDER_ORDER = [...PROVIDER_IDS];
|
|
28
30
|
var COLOR_PALETTE = [
|
|
29
31
|
"cyan",
|
|
30
32
|
"magenta",
|
|
@@ -188,6 +190,9 @@ function cacheDir() {
|
|
|
188
190
|
}
|
|
189
191
|
return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
|
|
190
192
|
}
|
|
193
|
+
function snapshotCacheFile() {
|
|
194
|
+
return join(cacheDir(), "web-snapshot.json");
|
|
195
|
+
}
|
|
191
196
|
async function loadConfig() {
|
|
192
197
|
let raw;
|
|
193
198
|
try {
|
|
@@ -246,9 +251,9 @@ function findAccount(config, id) {
|
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
export {
|
|
254
|
+
PROVIDER_IDS,
|
|
249
255
|
DEFAULTS,
|
|
250
256
|
ACCENT_COLORS,
|
|
251
|
-
PROVIDER_IDS,
|
|
252
257
|
COLOR_PALETTE,
|
|
253
258
|
PROVIDER_META,
|
|
254
259
|
getTrackedAccountRows,
|
|
@@ -262,6 +267,7 @@ export {
|
|
|
262
267
|
envDir,
|
|
263
268
|
configLocation,
|
|
264
269
|
cacheDir,
|
|
270
|
+
snapshotCacheFile,
|
|
265
271
|
loadConfig,
|
|
266
272
|
saveConfig,
|
|
267
273
|
saveConfigSync,
|
package/dist/cli.js
CHANGED
|
@@ -14,12 +14,12 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
14
14
|
var args = process.argv.slice(2);
|
|
15
15
|
var subcommand = args[0]?.toLowerCase();
|
|
16
16
|
if (subcommand === "__daemon") {
|
|
17
|
-
const { runDaemon } = await import("./daemon-
|
|
17
|
+
const { runDaemon } = await import("./daemon-OBQJO6D4.js");
|
|
18
18
|
await runDaemon(args.slice(1), { foreground: false });
|
|
19
19
|
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
20
20
|
}
|
|
21
21
|
if (subcommand === "serve" || subcommand === "web") {
|
|
22
|
-
const { runDaemon } = await import("./daemon-
|
|
22
|
+
const { runDaemon } = await import("./daemon-OBQJO6D4.js");
|
|
23
23
|
await runDaemon(args.slice(1), { foreground: true });
|
|
24
24
|
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
25
25
|
}
|
|
@@ -54,7 +54,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
54
54
|
process.exit(0);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
var { loadConfig } = await import("./config-
|
|
57
|
+
var { loadConfig } = await import("./config-C6Z65JUP.js");
|
|
58
58
|
var { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
|
|
59
59
|
var { attachOrSpawn } = await import("./daemon-handle-ZHECQZ6Q.js");
|
|
60
60
|
var config = await loadConfig();
|
|
@@ -68,5 +68,5 @@ setGlyphs(resolveGlyphs({
|
|
|
68
68
|
}));
|
|
69
69
|
var daemon = await attachOrSpawn();
|
|
70
70
|
var mode = daemon.kind === "spawned" ? "connected" : "degraded";
|
|
71
|
-
var { bootstrapInk } = await import("./bootstrap-ink-
|
|
71
|
+
var { bootstrapInk } = await import("./bootstrap-ink-AO3QA5BH.js");
|
|
72
72
|
await bootstrapInk({ interval, config, daemon, mode });
|
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
sanitizeTyped,
|
|
20
20
|
saveConfig,
|
|
21
21
|
saveConfigSync,
|
|
22
|
-
slugify
|
|
23
|
-
|
|
22
|
+
slugify,
|
|
23
|
+
snapshotCacheFile
|
|
24
|
+
} from "./chunk-XQEJ4WQ5.js";
|
|
24
25
|
export {
|
|
25
26
|
ACCENT_COLORS,
|
|
26
27
|
COLOR_PALETTE,
|
|
@@ -41,5 +42,6 @@ export {
|
|
|
41
42
|
sanitizeTyped,
|
|
42
43
|
saveConfig,
|
|
43
44
|
saveConfigSync,
|
|
44
|
-
slugify
|
|
45
|
+
slugify,
|
|
46
|
+
snapshotCacheFile
|
|
45
47
|
};
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import {
|
|
3
3
|
appVersion,
|
|
4
4
|
startWebServer
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-DZY72PPB.js";
|
|
6
6
|
import {
|
|
7
7
|
flushDisk
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5BW4H7WW.js";
|
|
9
9
|
import {
|
|
10
10
|
cacheDir,
|
|
11
11
|
loadConfig
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XQEJ4WQ5.js";
|
|
13
13
|
|
|
14
14
|
// src/web/open.ts
|
|
15
15
|
import { spawn } from "child_process";
|