tokmon 0.13.0 → 0.14.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 +31 -13
- package/dist/cli.js +2141 -403
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.tsx
|
|
4
|
+
import { EventEmitter } from "events";
|
|
4
5
|
import { render } from "ink";
|
|
5
6
|
import { MouseProvider } from "@zenobius/ink-mouse";
|
|
6
7
|
|
|
@@ -22,8 +23,11 @@ var DEFAULTS = {
|
|
|
22
23
|
disabledProviders: [],
|
|
23
24
|
onboarded: false,
|
|
24
25
|
dashboardLayout: "grid",
|
|
25
|
-
defaultFocus: "all"
|
|
26
|
+
defaultFocus: "all",
|
|
27
|
+
ascii: "auto",
|
|
28
|
+
knownProviders: []
|
|
26
29
|
};
|
|
30
|
+
var LEGACY_KNOWN = ["claude", "codex", "cursor"];
|
|
27
31
|
var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
|
|
28
32
|
function configDir() {
|
|
29
33
|
if (process.platform === "win32") {
|
|
@@ -43,7 +47,7 @@ function cacheDir() {
|
|
|
43
47
|
}
|
|
44
48
|
return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
|
|
45
49
|
}
|
|
46
|
-
var PROVIDER_IDS = ["claude", "codex", "cursor"];
|
|
50
|
+
var PROVIDER_IDS = ["claude", "codex", "cursor", "pi", "opencode", "copilot", "antigravity", "gemini"];
|
|
47
51
|
function clampNum(v, fallback, min) {
|
|
48
52
|
return typeof v === "number" && Number.isFinite(v) && v >= min ? v : fallback;
|
|
49
53
|
}
|
|
@@ -84,7 +88,9 @@ async function loadConfig() {
|
|
|
84
88
|
// provider picker once, so existing users can opt into Codex/Cursor.
|
|
85
89
|
onboarded: parsed.onboarded === true,
|
|
86
90
|
dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
|
|
87
|
-
defaultFocus: parsed.defaultFocus === "last" ? "last" : "all"
|
|
91
|
+
defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
|
|
92
|
+
ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
|
|
93
|
+
knownProviders: Array.isArray(parsed.knownProviders) ? parsed.knownProviders.filter((p) => PROVIDER_IDS.includes(p)) : parsed.onboarded === true ? [...LEGACY_KNOWN] : []
|
|
88
94
|
};
|
|
89
95
|
} catch {
|
|
90
96
|
return { ...DEFAULTS };
|
|
@@ -505,9 +511,105 @@ function mergeTables(list) {
|
|
|
505
511
|
};
|
|
506
512
|
}
|
|
507
513
|
|
|
514
|
+
// src/glyphs.ts
|
|
515
|
+
var GLYPHS_UNICODE = {
|
|
516
|
+
spark: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"],
|
|
517
|
+
barFull: "\u2501",
|
|
518
|
+
barEmpty: "\u2500",
|
|
519
|
+
rule: "\u2500",
|
|
520
|
+
spinner: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
521
|
+
dot: "\u25CF",
|
|
522
|
+
dotSel: "\u25C9",
|
|
523
|
+
radioOff: "\u25CB",
|
|
524
|
+
dotAll: "\u2726",
|
|
525
|
+
caretR: "\u25B8",
|
|
526
|
+
caretL: "\u25C2",
|
|
527
|
+
play: "\u25B6",
|
|
528
|
+
arrowU: "\u2191",
|
|
529
|
+
arrowD: "\u2193",
|
|
530
|
+
arrowL: "\u2190",
|
|
531
|
+
arrowR: "\u2192",
|
|
532
|
+
shift: "\u21E7",
|
|
533
|
+
vbar: "\u258C",
|
|
534
|
+
treeMid: "\u251C\u2500",
|
|
535
|
+
treeEnd: "\u2514\u2500",
|
|
536
|
+
boxMark: "\u2502",
|
|
537
|
+
check: "\u2713",
|
|
538
|
+
warn: "\u26A0",
|
|
539
|
+
ellipsis: "\u2026",
|
|
540
|
+
middot: "\xB7",
|
|
541
|
+
emDash: "\u2014",
|
|
542
|
+
eur: "\u20AC",
|
|
543
|
+
gbp: "\xA3",
|
|
544
|
+
border: "round"
|
|
545
|
+
};
|
|
546
|
+
var GLYPHS_ASCII = {
|
|
547
|
+
spark: [".", ":", "-", "=", "+", "*", "#", "@"],
|
|
548
|
+
barFull: "#",
|
|
549
|
+
barEmpty: "-",
|
|
550
|
+
rule: "-",
|
|
551
|
+
spinner: ["|", "/", "-", "\\"],
|
|
552
|
+
dot: "*",
|
|
553
|
+
dotSel: "*",
|
|
554
|
+
radioOff: "o",
|
|
555
|
+
dotAll: "+",
|
|
556
|
+
caretR: ">",
|
|
557
|
+
caretL: "<",
|
|
558
|
+
play: ">",
|
|
559
|
+
arrowU: "^",
|
|
560
|
+
arrowD: "v",
|
|
561
|
+
arrowL: "<",
|
|
562
|
+
arrowR: ">",
|
|
563
|
+
shift: "^",
|
|
564
|
+
vbar: "|",
|
|
565
|
+
treeMid: "+-",
|
|
566
|
+
treeEnd: "`-",
|
|
567
|
+
boxMark: "|",
|
|
568
|
+
check: "x",
|
|
569
|
+
warn: "!",
|
|
570
|
+
ellipsis: "...",
|
|
571
|
+
middot: "-",
|
|
572
|
+
emDash: "-",
|
|
573
|
+
eur: "EUR",
|
|
574
|
+
gbp: "GBP",
|
|
575
|
+
border: "classic"
|
|
576
|
+
};
|
|
577
|
+
function detectUnicode(env, isTTY, platform) {
|
|
578
|
+
if (!isTTY) return false;
|
|
579
|
+
if (env.TERM === "dumb") return false;
|
|
580
|
+
if (platform === "win32") {
|
|
581
|
+
return Boolean(env.WT_SESSION || env.ConEmuANSI === "ON" || env.TERM_PROGRAM === "vscode" || /xterm/i.test(env.TERM ?? ""));
|
|
582
|
+
}
|
|
583
|
+
const loc = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
|
|
584
|
+
if (loc && /\.(iso|latin|ascii|cp\d|koi|gbk|big5)/i.test(loc)) return false;
|
|
585
|
+
if (/^(C|POSIX)$/i.test(loc)) return false;
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function resolveGlyphs(opts) {
|
|
589
|
+
let ascii;
|
|
590
|
+
if (opts.flag === "on") ascii = true;
|
|
591
|
+
else if (opts.flag === "off") ascii = false;
|
|
592
|
+
else {
|
|
593
|
+
const e = (opts.env.TOKMON_ASCII ?? "").toLowerCase();
|
|
594
|
+
if (/^(1|true|on|yes)$/.test(e)) ascii = true;
|
|
595
|
+
else if (/^(0|false|off|no)$/.test(e)) ascii = false;
|
|
596
|
+
else if (opts.config === "on") ascii = true;
|
|
597
|
+
else if (opts.config === "off") ascii = false;
|
|
598
|
+
else ascii = !detectUnicode(opts.env, opts.isTTY, opts.platform);
|
|
599
|
+
}
|
|
600
|
+
return ascii ? GLYPHS_ASCII : GLYPHS_UNICODE;
|
|
601
|
+
}
|
|
602
|
+
var active = GLYPHS_UNICODE;
|
|
603
|
+
function setGlyphs(set) {
|
|
604
|
+
active = set;
|
|
605
|
+
}
|
|
606
|
+
function glyphs() {
|
|
607
|
+
return active;
|
|
608
|
+
}
|
|
609
|
+
|
|
508
610
|
// src/app.tsx
|
|
509
|
-
import { useState as
|
|
510
|
-
import { Box as
|
|
611
|
+
import { useState as useState3, useEffect as useEffect3, useCallback, useRef as useRef2, useMemo } from "react";
|
|
612
|
+
import { Box as Box7, Text as Text7, useInput, useStdout, useApp } from "ink";
|
|
511
613
|
import { useMouse } from "@zenobius/ink-mouse";
|
|
512
614
|
|
|
513
615
|
// src/http.ts
|
|
@@ -746,12 +848,26 @@ function resetIn(iso) {
|
|
|
746
848
|
|
|
747
849
|
// src/providers/claude/billing.ts
|
|
748
850
|
var execFile = promisify(execFileCb);
|
|
851
|
+
function parseAuth(raw) {
|
|
852
|
+
try {
|
|
853
|
+
const creds = JSON.parse(raw);
|
|
854
|
+
const o = creds?.claudeAiOauth ?? creds;
|
|
855
|
+
const token = o?.accessToken;
|
|
856
|
+
if (typeof token !== "string" || !token) return null;
|
|
857
|
+
return {
|
|
858
|
+
token,
|
|
859
|
+
subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
|
|
860
|
+
rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0
|
|
861
|
+
};
|
|
862
|
+
} catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
749
866
|
async function readCredentialsFile(homeDir) {
|
|
750
867
|
for (const dir of claudeConfigDirs(homeDir)) {
|
|
751
868
|
try {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
if (token) return token;
|
|
869
|
+
const auth = parseAuth(await readFile3(join4(dir, ".credentials.json"), "utf-8"));
|
|
870
|
+
if (auth) return auth;
|
|
755
871
|
} catch {
|
|
756
872
|
}
|
|
757
873
|
}
|
|
@@ -765,38 +881,45 @@ async function readMacKeychain() {
|
|
|
765
881
|
"Claude Code-credentials",
|
|
766
882
|
"-w"
|
|
767
883
|
], { timeout: 5e3 });
|
|
768
|
-
|
|
769
|
-
return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
|
|
884
|
+
return parseAuth(stdout.trim());
|
|
770
885
|
} catch {
|
|
771
886
|
return null;
|
|
772
887
|
}
|
|
773
888
|
}
|
|
774
|
-
async function
|
|
889
|
+
async function getAuth(homeDir) {
|
|
775
890
|
const isDefault = !homeDir || homeDir === homedir3();
|
|
776
891
|
if (isDefault && process.platform === "darwin") {
|
|
777
|
-
const
|
|
778
|
-
if (
|
|
892
|
+
const auth = await readMacKeychain();
|
|
893
|
+
if (auth) return auth;
|
|
779
894
|
}
|
|
780
895
|
return readCredentialsFile(homeDir);
|
|
781
896
|
}
|
|
897
|
+
function planLabel(auth) {
|
|
898
|
+
const sub = auth.subscriptionType;
|
|
899
|
+
if (!sub) return null;
|
|
900
|
+
const base = sub.charAt(0).toUpperCase() + sub.slice(1);
|
|
901
|
+
const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
|
|
902
|
+
return tier ? `${base} ${tier[1]}x` : base;
|
|
903
|
+
}
|
|
782
904
|
var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
|
|
783
905
|
async function claudeBilling(account) {
|
|
784
|
-
const
|
|
785
|
-
if (!
|
|
906
|
+
const auth = await getAuth(account.homeDir);
|
|
907
|
+
if (!auth) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
|
|
908
|
+
const plan = planLabel(auth);
|
|
786
909
|
try {
|
|
787
910
|
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
788
911
|
headers: {
|
|
789
|
-
"Authorization": `Bearer ${token}`,
|
|
912
|
+
"Authorization": `Bearer ${auth.token}`,
|
|
790
913
|
"anthropic-beta": "oauth-2025-04-20",
|
|
791
914
|
"User-Agent": "tokmon"
|
|
792
915
|
},
|
|
793
916
|
signal: AbortSignal.timeout(1e4)
|
|
794
917
|
});
|
|
795
|
-
if (res.status === 429) return { plan
|
|
796
|
-
if (res.status === 401) return { plan
|
|
797
|
-
if (!res.ok) return { plan
|
|
918
|
+
if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll" };
|
|
919
|
+
if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code" };
|
|
920
|
+
if (!res.ok) return { plan, metrics: [], error: `API ${res.status}` };
|
|
798
921
|
const data = await readJson(res);
|
|
799
|
-
if (!data) return { plan
|
|
922
|
+
if (!data) return { plan, metrics: [], error: "Unexpected API response" };
|
|
800
923
|
const metrics = [];
|
|
801
924
|
if (data.five_hour) {
|
|
802
925
|
metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
|
|
@@ -815,9 +938,9 @@ async function claudeBilling(account) {
|
|
|
815
938
|
format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
|
|
816
939
|
});
|
|
817
940
|
}
|
|
818
|
-
return { plan
|
|
941
|
+
return { plan, metrics, error: null };
|
|
819
942
|
} catch {
|
|
820
|
-
return { plan
|
|
943
|
+
return { plan, metrics: [], error: "Network error" };
|
|
821
944
|
}
|
|
822
945
|
}
|
|
823
946
|
|
|
@@ -1017,7 +1140,7 @@ async function readKeychainAuth() {
|
|
|
1017
1140
|
return null;
|
|
1018
1141
|
}
|
|
1019
1142
|
}
|
|
1020
|
-
async function
|
|
1143
|
+
async function getAuth2(homeDir) {
|
|
1021
1144
|
for (const home of codexHomes(homeDir)) {
|
|
1022
1145
|
const auth = await readAuthFile(home);
|
|
1023
1146
|
if (auth) return auth;
|
|
@@ -1025,7 +1148,7 @@ async function getAuth(homeDir) {
|
|
|
1025
1148
|
if (process.platform === "darwin") return readKeychainAuth();
|
|
1026
1149
|
return null;
|
|
1027
1150
|
}
|
|
1028
|
-
function
|
|
1151
|
+
function planLabel2(planType) {
|
|
1029
1152
|
if (typeof planType !== "string" || !planType.trim()) return null;
|
|
1030
1153
|
const p = planType.trim().toLowerCase();
|
|
1031
1154
|
if (p === "prolite") return "Pro 5x";
|
|
@@ -1077,7 +1200,7 @@ async function liveBilling(auth) {
|
|
|
1077
1200
|
metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
|
|
1078
1201
|
}
|
|
1079
1202
|
if (metrics.length === 0) return null;
|
|
1080
|
-
return { plan:
|
|
1203
|
+
return { plan: planLabel2(data.plan_type), metrics, error: null };
|
|
1081
1204
|
} catch {
|
|
1082
1205
|
return null;
|
|
1083
1206
|
}
|
|
@@ -1134,10 +1257,10 @@ async function snapshotBilling(homeDir) {
|
|
|
1134
1257
|
metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
|
|
1135
1258
|
}
|
|
1136
1259
|
if (metrics.length === 0) return null;
|
|
1137
|
-
return { plan:
|
|
1260
|
+
return { plan: planLabel2(last.plan_type), metrics, error: null };
|
|
1138
1261
|
}
|
|
1139
1262
|
async function codexBilling(account) {
|
|
1140
|
-
const auth = await
|
|
1263
|
+
const auth = await getAuth2(account.homeDir);
|
|
1141
1264
|
if (auth) {
|
|
1142
1265
|
const live = await liveBilling(auth);
|
|
1143
1266
|
if (live) return live;
|
|
@@ -1177,29 +1300,75 @@ import { homedir as homedir5 } from "os";
|
|
|
1177
1300
|
import { execFile as execFileCb3 } from "child_process";
|
|
1178
1301
|
import { promisify as promisify3 } from "util";
|
|
1179
1302
|
var execFile3 = promisify3(execFileCb3);
|
|
1180
|
-
|
|
1303
|
+
var nativeDb;
|
|
1304
|
+
async function getNativeDb() {
|
|
1305
|
+
if (nativeDb !== void 0) return nativeDb;
|
|
1306
|
+
try {
|
|
1307
|
+
nativeDb = (await import("sqlite")).DatabaseSync;
|
|
1308
|
+
} catch {
|
|
1309
|
+
nativeDb = null;
|
|
1310
|
+
}
|
|
1311
|
+
return nativeDb;
|
|
1312
|
+
}
|
|
1313
|
+
function classify(msg) {
|
|
1314
|
+
if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
|
|
1315
|
+
if (/database is (locked|busy)|readonly/i.test(msg)) return "locked";
|
|
1316
|
+
if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
|
|
1317
|
+
return "error";
|
|
1318
|
+
}
|
|
1319
|
+
async function runSqlite(db, sql, params = []) {
|
|
1320
|
+
const DB = await getNativeDb();
|
|
1321
|
+
if (DB) {
|
|
1322
|
+
let handle;
|
|
1323
|
+
try {
|
|
1324
|
+
handle = new DB(db, { readOnly: true, timeout: 1500 });
|
|
1325
|
+
const rows = handle.prepare(sql).all(...params);
|
|
1326
|
+
return { status: "ok", rows };
|
|
1327
|
+
} catch {
|
|
1328
|
+
} finally {
|
|
1329
|
+
try {
|
|
1330
|
+
handle?.close();
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return runSqliteCli(db, sql, params);
|
|
1336
|
+
}
|
|
1337
|
+
function inlineParams(sql, params) {
|
|
1338
|
+
let i = 0;
|
|
1339
|
+
return sql.replace(/\?/g, () => {
|
|
1340
|
+
const p = params[i++];
|
|
1341
|
+
return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
async function runSqliteCli(db, sql, params) {
|
|
1181
1345
|
try {
|
|
1182
1346
|
const { stdout } = await execFile3(
|
|
1183
1347
|
"sqlite3",
|
|
1184
|
-
|
|
1348
|
+
// -json yields row objects; `.timeout` sets the busy handler silently
|
|
1349
|
+
// (a `-cmd 'PRAGMA busy_timeout=…'` would print its result row first).
|
|
1350
|
+
["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
|
|
1185
1351
|
{ timeout: 1e4, maxBuffer: 8 << 20 }
|
|
1186
1352
|
);
|
|
1187
|
-
|
|
1353
|
+
const text = stdout.trim();
|
|
1354
|
+
if (!text) return { status: "ok", rows: [] };
|
|
1355
|
+
try {
|
|
1356
|
+
return { status: "ok", rows: JSON.parse(text) };
|
|
1357
|
+
} catch {
|
|
1358
|
+
return { status: "error", rows: [] };
|
|
1359
|
+
}
|
|
1188
1360
|
} catch (e) {
|
|
1189
1361
|
const err = e;
|
|
1190
|
-
if (err?.code === "ENOENT") return { status: "missing",
|
|
1191
|
-
|
|
1192
|
-
if (/database is (locked|busy)/i.test(msg)) return { status: "locked", stdout: "" };
|
|
1193
|
-
if (/no such function|unknown option|no such table/i.test(msg)) return { status: "old", stdout: "" };
|
|
1194
|
-
return { status: "error", stdout: "" };
|
|
1362
|
+
if (err?.code === "ENOENT") return { status: "missing", rows: [] };
|
|
1363
|
+
return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
|
|
1195
1364
|
}
|
|
1196
1365
|
}
|
|
1197
1366
|
function sqliteStatusMessage(status) {
|
|
1198
1367
|
switch (status) {
|
|
1199
1368
|
case "missing":
|
|
1200
|
-
return "
|
|
1369
|
+
return "Cursor data not found \u2014 open Cursor";
|
|
1201
1370
|
case "old":
|
|
1202
|
-
return "
|
|
1371
|
+
return "Cursor DB unreadable";
|
|
1203
1372
|
case "locked":
|
|
1204
1373
|
return "Cursor DB busy \u2014 retrying next poll";
|
|
1205
1374
|
default:
|
|
@@ -1222,17 +1391,14 @@ async function cursorActivity(homeDir) {
|
|
|
1222
1391
|
const now = Date.now();
|
|
1223
1392
|
const res = await runSqlite(
|
|
1224
1393
|
db,
|
|
1225
|
-
`SELECT date(createdAt/1000,'unixepoch','localtime') d, count(*) c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
|
|
1394
|
+
`SELECT date(createdAt/1000,'unixepoch','localtime') AS d, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
|
|
1226
1395
|
);
|
|
1227
1396
|
if (res.status !== "ok") return null;
|
|
1228
|
-
const daily = res.stdout.trim();
|
|
1229
1397
|
const byDay = /* @__PURE__ */ new Map();
|
|
1230
1398
|
let month = 0;
|
|
1231
|
-
for (const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const n = Number(c) || 0;
|
|
1235
|
-
byDay.set(d, n);
|
|
1399
|
+
for (const row of res.rows) {
|
|
1400
|
+
const n = Number(row.c) || 0;
|
|
1401
|
+
byDay.set(String(row.d), n);
|
|
1236
1402
|
month += n;
|
|
1237
1403
|
}
|
|
1238
1404
|
const series = [];
|
|
@@ -1247,17 +1413,15 @@ async function cursorActivity(homeDir) {
|
|
|
1247
1413
|
// src/providers/cursor/composer.ts
|
|
1248
1414
|
async function cursorModelSpend(homeDir) {
|
|
1249
1415
|
const db = cursorStateDb(homeDir);
|
|
1250
|
-
const sql = "SELECT mk.key, sum(json_extract(mk.value,'$.costInCents')), sum(json_extract(mk.value,'$.amount')) 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
|
|
1251
|
-
const res = await runSqlite(db, sql
|
|
1416
|
+
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;";
|
|
1417
|
+
const res = await runSqlite(db, sql);
|
|
1252
1418
|
if (res.status !== "ok") return null;
|
|
1253
1419
|
const models = [];
|
|
1254
1420
|
let total = 0;
|
|
1255
|
-
for (const
|
|
1256
|
-
|
|
1257
|
-
const [name, cents, amt] = line.split(" ");
|
|
1258
|
-
const usd = (Number(cents) || 0) / 100;
|
|
1421
|
+
for (const row of res.rows) {
|
|
1422
|
+
const usd = (Number(row.cents) || 0) / 100;
|
|
1259
1423
|
if (usd <= 0) continue;
|
|
1260
|
-
models.push({ name, usd, requests: Number(amt) || 0 });
|
|
1424
|
+
models.push({ name: String(row.name ?? ""), usd, requests: Number(row.amt) || 0 });
|
|
1261
1425
|
total += usd;
|
|
1262
1426
|
}
|
|
1263
1427
|
if (total <= 0) return null;
|
|
@@ -1290,9 +1454,9 @@ async function detectCursor(homeDir) {
|
|
|
1290
1454
|
}
|
|
1291
1455
|
}
|
|
1292
1456
|
async function readState(db, key) {
|
|
1293
|
-
const
|
|
1294
|
-
const
|
|
1295
|
-
return { value:
|
|
1457
|
+
const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
|
|
1458
|
+
const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
|
|
1459
|
+
return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
|
|
1296
1460
|
}
|
|
1297
1461
|
async function connectPost(url, token) {
|
|
1298
1462
|
try {
|
|
@@ -1414,28 +1578,930 @@ var cursorProvider = {
|
|
|
1414
1578
|
fetchBilling: (account) => cursorBilling(account)
|
|
1415
1579
|
};
|
|
1416
1580
|
|
|
1581
|
+
// src/providers/pi/usage.ts
|
|
1582
|
+
import { readdir as readdir4, stat as fsStat4, access as access4 } from "fs/promises";
|
|
1583
|
+
import { createReadStream as createReadStream4 } from "fs";
|
|
1584
|
+
import { createInterface as createInterface4 } from "readline";
|
|
1585
|
+
import { join as join9 } from "path";
|
|
1586
|
+
import { homedir as homedir7 } from "os";
|
|
1587
|
+
function piSessionsDir(homeDir) {
|
|
1588
|
+
return join9(homeDir ?? homedir7(), ".pi", "agent", "sessions");
|
|
1589
|
+
}
|
|
1590
|
+
async function detectPi(homeDir) {
|
|
1591
|
+
try {
|
|
1592
|
+
await access4(piSessionsDir(homeDir));
|
|
1593
|
+
return true;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function pos(v) {
|
|
1599
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
1600
|
+
}
|
|
1601
|
+
async function parseFile3(path) {
|
|
1602
|
+
const entries = [];
|
|
1603
|
+
const rl = createInterface4({ input: createReadStream4(path), crlfDelay: Infinity });
|
|
1604
|
+
for await (const rawLine of rl) {
|
|
1605
|
+
if (!rawLine.includes('"usage"')) continue;
|
|
1606
|
+
try {
|
|
1607
|
+
const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
|
|
1608
|
+
const obj = JSON.parse(line);
|
|
1609
|
+
if (obj?.type !== "message") continue;
|
|
1610
|
+
const msg = obj.message;
|
|
1611
|
+
if (msg?.role !== "assistant" || !msg?.usage) continue;
|
|
1612
|
+
const u = msg.usage;
|
|
1613
|
+
const ts = new Date(obj.timestamp ?? msg.timestamp ?? 0).getTime();
|
|
1614
|
+
if (!Number.isFinite(ts)) continue;
|
|
1615
|
+
const input = safeNum(u.input);
|
|
1616
|
+
const output = safeNum(u.output);
|
|
1617
|
+
const cacheRead = safeNum(u.cacheRead);
|
|
1618
|
+
const cacheCreate = safeNum(u.cacheWrite);
|
|
1619
|
+
if (input + output + cacheRead + cacheCreate === 0) continue;
|
|
1620
|
+
const c = u.cost ?? {};
|
|
1621
|
+
const costInput = pos(c.input);
|
|
1622
|
+
const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - pos(c.cacheRead)) : 0;
|
|
1623
|
+
entries.push({
|
|
1624
|
+
ts,
|
|
1625
|
+
model: typeof msg.model === "string" && msg.model ? msg.model : "unknown",
|
|
1626
|
+
cost: pos(c.total),
|
|
1627
|
+
input,
|
|
1628
|
+
output,
|
|
1629
|
+
cacheCreate,
|
|
1630
|
+
cacheRead,
|
|
1631
|
+
cacheSavings
|
|
1632
|
+
});
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return entries;
|
|
1637
|
+
}
|
|
1638
|
+
async function loadEntries3(since, homeDir) {
|
|
1639
|
+
const dir = piSessionsDir(homeDir);
|
|
1640
|
+
const files = [];
|
|
1641
|
+
const seenIno = /* @__PURE__ */ new Set();
|
|
1642
|
+
let listing;
|
|
1643
|
+
try {
|
|
1644
|
+
listing = await readdir4(dir, { recursive: true });
|
|
1645
|
+
} catch {
|
|
1646
|
+
return [];
|
|
1647
|
+
}
|
|
1648
|
+
for (const f of listing) {
|
|
1649
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1650
|
+
const path = join9(dir, f);
|
|
1651
|
+
try {
|
|
1652
|
+
const s = await fsStat4(path);
|
|
1653
|
+
if (s.mtimeMs < since) continue;
|
|
1654
|
+
if (s.ino && process.platform !== "win32") {
|
|
1655
|
+
const idn = `${s.dev}:${s.ino}`;
|
|
1656
|
+
if (seenIno.has(idn)) continue;
|
|
1657
|
+
seenIno.add(idn);
|
|
1658
|
+
}
|
|
1659
|
+
files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return loadCachedEntries(files, parseFile3, since);
|
|
1664
|
+
}
|
|
1665
|
+
async function piDashboard(tz, homeDir) {
|
|
1666
|
+
const now = Date.now();
|
|
1667
|
+
const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
|
|
1668
|
+
return summarize(await loadEntries3(since, homeDir), tz);
|
|
1669
|
+
}
|
|
1670
|
+
async function piTable(tz, homeDir) {
|
|
1671
|
+
return tabulate(await loadEntries3(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// src/providers/pi/index.ts
|
|
1675
|
+
var piProvider = {
|
|
1676
|
+
id: "pi",
|
|
1677
|
+
name: "Pi",
|
|
1678
|
+
color: "blue",
|
|
1679
|
+
hasUsage: true,
|
|
1680
|
+
hasBilling: false,
|
|
1681
|
+
detect: (homeDir) => detectPi(homeDir),
|
|
1682
|
+
fetchSummary: (account, tz) => piDashboard(tz, account.homeDir),
|
|
1683
|
+
fetchTable: (account, tz) => piTable(tz, account.homeDir)
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
// src/providers/opencode/usage.ts
|
|
1687
|
+
import { access as access5 } from "fs/promises";
|
|
1688
|
+
import { join as join10 } from "path";
|
|
1689
|
+
import { homedir as homedir8 } from "os";
|
|
1690
|
+
function opencodeDbPaths(homeDir) {
|
|
1691
|
+
const base = homeDir ?? homedir8();
|
|
1692
|
+
const paths = [];
|
|
1693
|
+
if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join10(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
|
|
1694
|
+
paths.push(join10(base, ".local", "share", "opencode", "opencode.db"));
|
|
1695
|
+
if (process.platform === "darwin") paths.push(join10(base, "Library", "Application Support", "opencode", "opencode.db"));
|
|
1696
|
+
if (process.platform === "win32") {
|
|
1697
|
+
const lad = homeDir ? join10(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
|
|
1698
|
+
if (lad) paths.push(join10(lad, "opencode", "opencode.db"));
|
|
1699
|
+
}
|
|
1700
|
+
return [...new Set(paths)];
|
|
1701
|
+
}
|
|
1702
|
+
async function findDb(homeDir) {
|
|
1703
|
+
for (const p of opencodeDbPaths(homeDir)) {
|
|
1704
|
+
try {
|
|
1705
|
+
await access5(p);
|
|
1706
|
+
return p;
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
async function detectOpencode(homeDir) {
|
|
1713
|
+
return await findDb(homeDir) !== null;
|
|
1714
|
+
}
|
|
1715
|
+
var pos2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
1716
|
+
async function loadEntries4(since, homeDir) {
|
|
1717
|
+
const db = await findDb(homeDir);
|
|
1718
|
+
if (!db) return [];
|
|
1719
|
+
const sql = "SELECT time_created AS ts, json_extract(data,'$.modelID') AS model, json_extract(data,'$.cost') AS cost, json_extract(data,'$.tokens.input') AS input, json_extract(data,'$.tokens.output') AS output, json_extract(data,'$.tokens.reasoning') AS reasoning, json_extract(data,'$.tokens.cache.read') AS cacheRead, json_extract(data,'$.tokens.cache.write') AS cacheWrite FROM message WHERE json_valid(data) AND json_extract(data,'$.role')='assistant' AND json_type(data,'$.tokens')='object' AND time_created >= ?;";
|
|
1720
|
+
const res = await runSqlite(db, sql, [Math.floor(since)]);
|
|
1721
|
+
if (res.status !== "ok") return [];
|
|
1722
|
+
const entries = [];
|
|
1723
|
+
for (const row of res.rows) {
|
|
1724
|
+
const ts = pos2(row.ts);
|
|
1725
|
+
if (!ts) continue;
|
|
1726
|
+
const input = pos2(row.input);
|
|
1727
|
+
const output = pos2(row.output);
|
|
1728
|
+
const cacheRead = pos2(row.cacheRead);
|
|
1729
|
+
const cacheCreate = pos2(row.cacheWrite);
|
|
1730
|
+
if (input + output + cacheRead + cacheCreate === 0) continue;
|
|
1731
|
+
entries.push({
|
|
1732
|
+
ts,
|
|
1733
|
+
model: typeof row.model === "string" && row.model ? row.model : "unknown",
|
|
1734
|
+
cost: pos2(row.cost),
|
|
1735
|
+
input,
|
|
1736
|
+
output,
|
|
1737
|
+
cacheCreate,
|
|
1738
|
+
cacheRead,
|
|
1739
|
+
cacheSavings: 0
|
|
1740
|
+
// opencode stores only a total cost, no per-component split to derive savings
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
return entries;
|
|
1744
|
+
}
|
|
1745
|
+
async function opencodeDashboard(tz, homeDir) {
|
|
1746
|
+
const now = Date.now();
|
|
1747
|
+
const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
|
|
1748
|
+
return summarize(await loadEntries4(since, homeDir), tz);
|
|
1749
|
+
}
|
|
1750
|
+
async function opencodeTable(tz, homeDir) {
|
|
1751
|
+
return tabulate(await loadEntries4(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/providers/opencode/index.ts
|
|
1755
|
+
var opencodeProvider = {
|
|
1756
|
+
id: "opencode",
|
|
1757
|
+
name: "opencode",
|
|
1758
|
+
color: "yellow",
|
|
1759
|
+
hasUsage: true,
|
|
1760
|
+
hasBilling: false,
|
|
1761
|
+
detect: (homeDir) => detectOpencode(homeDir),
|
|
1762
|
+
fetchSummary: (account, tz) => opencodeDashboard(tz, account.homeDir),
|
|
1763
|
+
fetchTable: (account, tz) => opencodeTable(tz, account.homeDir)
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// src/providers/copilot/billing.ts
|
|
1767
|
+
import { execFile as execFileCb4 } from "child_process";
|
|
1768
|
+
import { access as access6, readFile as readFile5, readdir as readdir5 } from "fs/promises";
|
|
1769
|
+
import { join as join11 } from "path";
|
|
1770
|
+
import { homedir as homedir9 } from "os";
|
|
1771
|
+
import { promisify as promisify4 } from "util";
|
|
1772
|
+
var execFile4 = promisify4(execFileCb4);
|
|
1773
|
+
var USAGE_URL3 = "https://api.github.com/copilot_internal/user";
|
|
1774
|
+
var GH_KEYCHAIN_SERVICE = "gh:github.com";
|
|
1775
|
+
var GO_KEYRING_PREFIX = "go-keyring-base64:";
|
|
1776
|
+
function ghConfigDir(homeDir) {
|
|
1777
|
+
if (!homeDir) {
|
|
1778
|
+
const explicit = process.env.GH_CONFIG_DIR;
|
|
1779
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
1780
|
+
if (process.platform === "win32") {
|
|
1781
|
+
return join11(envDir("APPDATA") ?? join11(homedir9(), "AppData", "Roaming"), "GitHub CLI");
|
|
1782
|
+
}
|
|
1783
|
+
const xdg = envDir("XDG_CONFIG_HOME");
|
|
1784
|
+
return xdg ? join11(xdg, "gh") : join11(homedir9(), ".config", "gh");
|
|
1785
|
+
}
|
|
1786
|
+
return process.platform === "win32" ? join11(homeDir, "AppData", "Roaming", "GitHub CLI") : join11(homeDir, ".config", "gh");
|
|
1787
|
+
}
|
|
1788
|
+
function ghHostsPath(homeDir) {
|
|
1789
|
+
return join11(ghConfigDir(homeDir), "hosts.yml");
|
|
1790
|
+
}
|
|
1791
|
+
async function detectCopilot(homeDir) {
|
|
1792
|
+
try {
|
|
1793
|
+
await access6(ghHostsPath(homeDir));
|
|
1794
|
+
return true;
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
await execFile4("gh", ["--version"], { timeout: 3e3 });
|
|
1799
|
+
return true;
|
|
1800
|
+
} catch {
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function unquoteYamlValue(value) {
|
|
1805
|
+
const trimmed = value.trim();
|
|
1806
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
1807
|
+
return trimmed.slice(1, -1);
|
|
1808
|
+
}
|
|
1809
|
+
return trimmed;
|
|
1810
|
+
}
|
|
1811
|
+
function tokenFromHostsYaml(raw) {
|
|
1812
|
+
const lines = raw.split(/\r?\n/);
|
|
1813
|
+
let inGithub = false;
|
|
1814
|
+
let githubIndent = -1;
|
|
1815
|
+
for (const line of lines) {
|
|
1816
|
+
const match = line.match(/^(\s*)([^:#][^:]*):\s*(.*)$/);
|
|
1817
|
+
if (!match) continue;
|
|
1818
|
+
const indent = match[1].length;
|
|
1819
|
+
const key = match[2].trim();
|
|
1820
|
+
const value = match[3].trim();
|
|
1821
|
+
if (indent === 0) {
|
|
1822
|
+
inGithub = key === "github.com";
|
|
1823
|
+
githubIndent = inGithub ? indent : -1;
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
if (inGithub && indent > githubIndent && key === "oauth_token" && value) {
|
|
1827
|
+
return unquoteYamlValue(value);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
async function loadTokenFromHosts(homeDir) {
|
|
1833
|
+
try {
|
|
1834
|
+
const token = tokenFromHostsYaml(await readFile5(ghHostsPath(homeDir), "utf-8"));
|
|
1835
|
+
return token ? { token, source: "gh-hosts" } : null;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async function readMacKeychainService(service) {
|
|
1841
|
+
if (process.platform !== "darwin") return null;
|
|
1842
|
+
try {
|
|
1843
|
+
const { stdout } = await execFile4("security", [
|
|
1844
|
+
"find-generic-password",
|
|
1845
|
+
"-s",
|
|
1846
|
+
service,
|
|
1847
|
+
"-w"
|
|
1848
|
+
], { timeout: 5e3 });
|
|
1849
|
+
const raw = stdout.trim();
|
|
1850
|
+
if (!raw) return null;
|
|
1851
|
+
if (raw.startsWith(GO_KEYRING_PREFIX)) {
|
|
1852
|
+
return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
|
|
1853
|
+
}
|
|
1854
|
+
return raw;
|
|
1855
|
+
} catch {
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async function loadTokenFromGhKeychain() {
|
|
1860
|
+
const token = await readMacKeychainService(GH_KEYCHAIN_SERVICE);
|
|
1861
|
+
return token ? { token, source: "gh-keychain" } : null;
|
|
1862
|
+
}
|
|
1863
|
+
function vscodeUserDir(homeDir) {
|
|
1864
|
+
const home = homeDir ?? homedir9();
|
|
1865
|
+
if (process.platform === "darwin") return join11(home, "Library", "Application Support", "Code", "User");
|
|
1866
|
+
if (process.platform === "win32") return join11(home, "AppData", "Roaming", "Code", "User");
|
|
1867
|
+
return join11(home, ".config", "Code", "User");
|
|
1868
|
+
}
|
|
1869
|
+
function tokenFromText(raw) {
|
|
1870
|
+
const patterns = [
|
|
1871
|
+
/github\.com[^A-Za-z0-9_]+oauth_token[^A-Za-z0-9_]+([A-Za-z0-9_]{20,})/i,
|
|
1872
|
+
/github\.com[^A-Za-z0-9_]+(gh[opusr]_[A-Za-z0-9_]{20,})/i,
|
|
1873
|
+
/\b(gh[opusr]_[A-Za-z0-9_]{20,})\b/
|
|
1874
|
+
];
|
|
1875
|
+
for (const pattern of patterns) {
|
|
1876
|
+
const token = raw.match(pattern)?.[1];
|
|
1877
|
+
if (token) return token;
|
|
1878
|
+
}
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
async function loadTokenFromVsCode(homeDir) {
|
|
1882
|
+
const userDir = vscodeUserDir(homeDir);
|
|
1883
|
+
const candidates = [
|
|
1884
|
+
join11(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
|
|
1885
|
+
join11(userDir, "globalStorage", "github.copilot", "auth.json"),
|
|
1886
|
+
join11(userDir, "globalStorage", "state.vscdb")
|
|
1887
|
+
];
|
|
1888
|
+
try {
|
|
1889
|
+
for (const dirent of await readdir5(join11(userDir, "globalStorage"), { withFileTypes: true })) {
|
|
1890
|
+
if (dirent.isDirectory() && dirent.name.toLowerCase().includes("github")) {
|
|
1891
|
+
candidates.push(join11(userDir, "globalStorage", dirent.name, "auth.json"));
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
} catch {
|
|
1895
|
+
}
|
|
1896
|
+
for (const path of candidates) {
|
|
1897
|
+
try {
|
|
1898
|
+
const token = tokenFromText(await readFile5(path, "utf-8"));
|
|
1899
|
+
if (token) return { token, source: "vscode" };
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
async function loadToken(homeDir) {
|
|
1906
|
+
return await loadTokenFromHosts(homeDir) || await loadTokenFromGhKeychain() || await loadTokenFromVsCode(homeDir);
|
|
1907
|
+
}
|
|
1908
|
+
function redactToken(token) {
|
|
1909
|
+
return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
|
|
1910
|
+
}
|
|
1911
|
+
function resetDate(value) {
|
|
1912
|
+
return typeof value === "string" && value.trim() ? resetIn(value) : null;
|
|
1913
|
+
}
|
|
1914
|
+
function percentMetric2(label, snapshot, reset, primary) {
|
|
1915
|
+
if (!snapshot || typeof snapshot.percent_remaining !== "number") return null;
|
|
1916
|
+
const used = Math.min(100, Math.max(0, 100 - snapshot.percent_remaining));
|
|
1917
|
+
return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: reset, primary };
|
|
1918
|
+
}
|
|
1919
|
+
function countMetric(label, remaining, total, reset) {
|
|
1920
|
+
if (typeof remaining !== "number" || typeof total !== "number" || total <= 0) return null;
|
|
1921
|
+
return {
|
|
1922
|
+
label,
|
|
1923
|
+
used: Math.max(0, total - remaining),
|
|
1924
|
+
limit: total,
|
|
1925
|
+
format: { kind: "count" },
|
|
1926
|
+
resetsAt: reset
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
async function fetchUsage(token) {
|
|
1930
|
+
try {
|
|
1931
|
+
const res = await fetch(USAGE_URL3, {
|
|
1932
|
+
headers: {
|
|
1933
|
+
"Authorization": `token ${token}`,
|
|
1934
|
+
"Accept": "application/json",
|
|
1935
|
+
"Editor-Version": "vscode/1.96.2",
|
|
1936
|
+
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
|
1937
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
1938
|
+
"X-Github-Api-Version": "2025-04-01"
|
|
1939
|
+
},
|
|
1940
|
+
signal: AbortSignal.timeout(1e4)
|
|
1941
|
+
});
|
|
1942
|
+
if (!res.ok) return { data: null, status: res.status };
|
|
1943
|
+
return { data: await readJson(res), status: res.status };
|
|
1944
|
+
} catch {
|
|
1945
|
+
return { data: null, status: null };
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
async function copilotBilling(account) {
|
|
1949
|
+
const cred = await loadToken(account.homeDir);
|
|
1950
|
+
if (!cred) return { plan: null, metrics: [], error: "Not logged in \u2014 run gh auth login" };
|
|
1951
|
+
const { data, status } = await fetchUsage(cred.token);
|
|
1952
|
+
if (!data) {
|
|
1953
|
+
if (status === 401 || status === 403) {
|
|
1954
|
+
return { plan: null, metrics: [], error: `Token invalid (${redactToken(cred.token)}) \u2014 run gh auth login` };
|
|
1955
|
+
}
|
|
1956
|
+
if (status) return { plan: null, metrics: [], error: `Copilot API ${status}` };
|
|
1957
|
+
return { plan: null, metrics: [], error: "Network error" };
|
|
1958
|
+
}
|
|
1959
|
+
const plan = typeof data.copilot_plan === "string" && data.copilot_plan.trim() ? data.copilot_plan : null;
|
|
1960
|
+
const metrics = [];
|
|
1961
|
+
const quotaReset = resetDate(data.quota_reset_date);
|
|
1962
|
+
const snapshots = data.quota_snapshots;
|
|
1963
|
+
const premium = percentMetric2("Premium", snapshots?.premium_interactions, quotaReset, true);
|
|
1964
|
+
if (premium) metrics.push(premium);
|
|
1965
|
+
const chat = percentMetric2("Chat", snapshots?.chat, quotaReset);
|
|
1966
|
+
if (chat) metrics.push(chat);
|
|
1967
|
+
if (data.limited_user_quotas && data.monthly_quotas) {
|
|
1968
|
+
const reset = resetDate(data.limited_user_reset_date);
|
|
1969
|
+
const limitedChat = countMetric("Chat", data.limited_user_quotas.chat, data.monthly_quotas.chat, reset);
|
|
1970
|
+
if (limitedChat) metrics.push(limitedChat);
|
|
1971
|
+
const completions = countMetric("Completions", data.limited_user_quotas.completions, data.monthly_quotas.completions, reset);
|
|
1972
|
+
if (completions) metrics.push(completions);
|
|
1973
|
+
}
|
|
1974
|
+
if (metrics.length === 0) return { plan, metrics: [], error: "No usage data" };
|
|
1975
|
+
return { plan, metrics, error: null };
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/providers/copilot/index.ts
|
|
1979
|
+
var copilotProvider = {
|
|
1980
|
+
id: "copilot",
|
|
1981
|
+
name: "Copilot",
|
|
1982
|
+
color: "white",
|
|
1983
|
+
hasUsage: false,
|
|
1984
|
+
hasBilling: true,
|
|
1985
|
+
detect: (homeDir) => detectCopilot(homeDir),
|
|
1986
|
+
fetchBilling: (account) => copilotBilling(account)
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
// src/providers/antigravity/billing.ts
|
|
1990
|
+
import { access as access7, readdir as readdir7 } from "fs/promises";
|
|
1991
|
+
import { join as join13 } from "path";
|
|
1992
|
+
import { homedir as homedir11 } from "os";
|
|
1993
|
+
|
|
1994
|
+
// src/providers/cloud-code.ts
|
|
1995
|
+
import { readFile as readFile6, readdir as readdir6, realpath, stat } from "fs/promises";
|
|
1996
|
+
import { spawnSync } from "child_process";
|
|
1997
|
+
import { homedir as homedir10 } from "os";
|
|
1998
|
+
import { dirname, join as join12 } from "path";
|
|
1999
|
+
var CLOUD_CODE_URLS = [
|
|
2000
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
2001
|
+
"https://cloudcode-pa.googleapis.com"
|
|
2002
|
+
];
|
|
2003
|
+
var LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
|
|
2004
|
+
var FETCH_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
|
2005
|
+
var RETRIEVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
|
|
2006
|
+
var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
2007
|
+
var GOOGLE_OAUTH_CLIENT_REGEX = /OAUTH_CLIENT_ID\s*=\s*["']([0-9]{6,}-[a-z0-9]+\.apps\.googleusercontent\.com)["']\s*;?\s*(?:var|const|let)?\s*OAUTH_CLIENT_SECRET\s*=\s*["'](GOCSPX-[A-Za-z0-9_-]+)["']/s;
|
|
2008
|
+
var MAX_BUNDLE_READ = 32 * 1024 * 1024;
|
|
2009
|
+
var cachedClient;
|
|
2010
|
+
var OAUTH_TOKEN_KEY = "antigravityUnifiedStateSync.oauthToken";
|
|
2011
|
+
var OAUTH_TOKEN_SENTINEL = "oauthTokenInfoSentinelKey";
|
|
2012
|
+
var CC_MODEL_BLACKLIST = {
|
|
2013
|
+
MODEL_CHAT_20706: true,
|
|
2014
|
+
MODEL_CHAT_23310: true,
|
|
2015
|
+
MODEL_GOOGLE_GEMINI_2_5_FLASH: true,
|
|
2016
|
+
MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING: true,
|
|
2017
|
+
MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE: true,
|
|
2018
|
+
MODEL_GOOGLE_GEMINI_2_5_PRO: true,
|
|
2019
|
+
MODEL_PLACEHOLDER_M19: true,
|
|
2020
|
+
MODEL_PLACEHOLDER_M9: true,
|
|
2021
|
+
MODEL_PLACEHOLDER_M12: true
|
|
2022
|
+
};
|
|
2023
|
+
function readVarint(bytes, start) {
|
|
2024
|
+
let value = 0;
|
|
2025
|
+
let shift = 0;
|
|
2026
|
+
let pos3 = start;
|
|
2027
|
+
while (pos3 < bytes.length) {
|
|
2028
|
+
const b = bytes[pos3++];
|
|
2029
|
+
value += (b & 127) * Math.pow(2, shift);
|
|
2030
|
+
if ((b & 128) === 0) return { value, pos: pos3 };
|
|
2031
|
+
shift += 7;
|
|
2032
|
+
}
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
function readFields(bytes) {
|
|
2036
|
+
const fields = {};
|
|
2037
|
+
let pos3 = 0;
|
|
2038
|
+
while (pos3 < bytes.length) {
|
|
2039
|
+
const tag = readVarint(bytes, pos3);
|
|
2040
|
+
if (!tag) break;
|
|
2041
|
+
pos3 = tag.pos;
|
|
2042
|
+
const fieldNum = Math.floor(tag.value / 8);
|
|
2043
|
+
const wireType = tag.value % 8;
|
|
2044
|
+
if (wireType === 0) {
|
|
2045
|
+
const val = readVarint(bytes, pos3);
|
|
2046
|
+
if (!val) break;
|
|
2047
|
+
fields[fieldNum] = { type: 0, value: val.value };
|
|
2048
|
+
pos3 = val.pos;
|
|
2049
|
+
} else if (wireType === 1) {
|
|
2050
|
+
if (pos3 + 8 > bytes.length) break;
|
|
2051
|
+
pos3 += 8;
|
|
2052
|
+
} else if (wireType === 2) {
|
|
2053
|
+
const len = readVarint(bytes, pos3);
|
|
2054
|
+
if (!len) break;
|
|
2055
|
+
pos3 = len.pos;
|
|
2056
|
+
if (pos3 + len.value > bytes.length) break;
|
|
2057
|
+
fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
|
|
2058
|
+
pos3 += len.value;
|
|
2059
|
+
} else if (wireType === 5) {
|
|
2060
|
+
if (pos3 + 4 > bytes.length) break;
|
|
2061
|
+
pos3 += 4;
|
|
2062
|
+
} else {
|
|
2063
|
+
break;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return fields;
|
|
2067
|
+
}
|
|
2068
|
+
function utf8(data) {
|
|
2069
|
+
return Buffer.from(data).toString("utf8");
|
|
2070
|
+
}
|
|
2071
|
+
function decodeBase64(text) {
|
|
2072
|
+
try {
|
|
2073
|
+
return Buffer.from(text, "base64");
|
|
2074
|
+
} catch {
|
|
2075
|
+
return null;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
function unwrapKeyringBase64(raw) {
|
|
2079
|
+
const text = raw.trim();
|
|
2080
|
+
if (!text.startsWith("go-keyring-base64:")) return text;
|
|
2081
|
+
const decoded = decodeBase64(text.slice("go-keyring-base64:".length));
|
|
2082
|
+
return decoded ? utf8(decoded).trim() : text;
|
|
2083
|
+
}
|
|
2084
|
+
function unwrapOAuthSentinel(base64Text) {
|
|
2085
|
+
const outerBytes = decodeBase64(unwrapKeyringBase64(base64Text));
|
|
2086
|
+
if (!outerBytes) return null;
|
|
2087
|
+
const outer = readFields(outerBytes);
|
|
2088
|
+
if (outer[1]?.type !== 2 || !outer[1].data) return null;
|
|
2089
|
+
const wrapper = readFields(outer[1].data);
|
|
2090
|
+
const sentinel = wrapper[1]?.type === 2 && wrapper[1].data ? utf8(wrapper[1].data) : null;
|
|
2091
|
+
const payload = wrapper[2]?.type === 2 ? wrapper[2].data : null;
|
|
2092
|
+
if (sentinel !== OAUTH_TOKEN_SENTINEL || !payload) return null;
|
|
2093
|
+
const payloadFields = readFields(payload);
|
|
2094
|
+
if (payloadFields[1]?.type !== 2 || !payloadFields[1].data) return null;
|
|
2095
|
+
const innerText = utf8(payloadFields[1].data).trim();
|
|
2096
|
+
return innerText ? decodeBase64(innerText) : null;
|
|
2097
|
+
}
|
|
2098
|
+
async function readAntigravityOAuthToken(db) {
|
|
2099
|
+
const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [OAUTH_TOKEN_KEY]);
|
|
2100
|
+
if (r.status !== "ok") return { token: null, status: r.status };
|
|
2101
|
+
const raw = r.rows[0]?.value;
|
|
2102
|
+
if (typeof raw !== "string" || !raw.trim()) return { token: null, status: "ok" };
|
|
2103
|
+
const inner = unwrapOAuthSentinel(raw);
|
|
2104
|
+
if (!inner) return { token: null, status: "ok" };
|
|
2105
|
+
const fields = readFields(inner);
|
|
2106
|
+
const accessToken = fields[1]?.type === 2 && fields[1].data ? utf8(fields[1].data) : null;
|
|
2107
|
+
const refreshToken = fields[3]?.type === 2 && fields[3].data ? utf8(fields[3].data) : null;
|
|
2108
|
+
let expirySeconds = null;
|
|
2109
|
+
if (fields[4]?.type === 2 && fields[4].data) {
|
|
2110
|
+
const ts = readFields(fields[4].data);
|
|
2111
|
+
expirySeconds = ts[1]?.type === 0 && typeof ts[1].value === "number" ? ts[1].value : null;
|
|
2112
|
+
}
|
|
2113
|
+
if (!accessToken && !refreshToken) return { token: null, status: "ok" };
|
|
2114
|
+
return { token: { accessToken, refreshToken, expirySeconds }, status: "ok" };
|
|
2115
|
+
}
|
|
2116
|
+
function redact(token) {
|
|
2117
|
+
if (!token) return "none";
|
|
2118
|
+
return `...${token.slice(-4)}`;
|
|
2119
|
+
}
|
|
2120
|
+
function geminiBundleCandidates() {
|
|
2121
|
+
const candidates = [];
|
|
2122
|
+
const addBundle = (nodeModulesRoot) => {
|
|
2123
|
+
if (!nodeModulesRoot) return;
|
|
2124
|
+
candidates.push(join12(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
|
|
2125
|
+
};
|
|
2126
|
+
try {
|
|
2127
|
+
const which = spawnSync("command", ["-v", "gemini"], { encoding: "utf8", timeout: 5e3 });
|
|
2128
|
+
const resolved = typeof which.stdout === "string" ? which.stdout.trim().split("\n")[0]?.trim() : "";
|
|
2129
|
+
if (resolved) candidates.push(resolved);
|
|
2130
|
+
} catch {
|
|
2131
|
+
}
|
|
2132
|
+
const home = homedir10();
|
|
2133
|
+
addBundle("/opt/homebrew/lib/node_modules");
|
|
2134
|
+
addBundle("/usr/local/lib/node_modules");
|
|
2135
|
+
addBundle(join12(home, ".local", "share", "node_modules"));
|
|
2136
|
+
addBundle(join12(home, ".bun", "install", "global", "node_modules"));
|
|
2137
|
+
try {
|
|
2138
|
+
const prefix = spawnSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5e3 });
|
|
2139
|
+
const root = typeof prefix.stdout === "string" ? prefix.stdout.trim() : "";
|
|
2140
|
+
if (root && root !== "undefined") addBundle(join12(root, "lib", "node_modules"));
|
|
2141
|
+
} catch {
|
|
2142
|
+
}
|
|
2143
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
2144
|
+
}
|
|
2145
|
+
async function resolveBundleDir(candidate) {
|
|
2146
|
+
try {
|
|
2147
|
+
if (candidate.endsWith(`${join12("@google", "gemini-cli", "bundle")}`)) {
|
|
2148
|
+
return candidate;
|
|
2149
|
+
}
|
|
2150
|
+
const real = await realpath(candidate);
|
|
2151
|
+
return dirname(real);
|
|
2152
|
+
} catch {
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
async function scanBundleDir(dir) {
|
|
2157
|
+
let entries;
|
|
2158
|
+
try {
|
|
2159
|
+
entries = await readdir6(dir);
|
|
2160
|
+
} catch {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
const targets = entries.filter((name) => name === "gemini.js" || name.startsWith("chunk-") && name.endsWith(".js"));
|
|
2164
|
+
for (const name of targets) {
|
|
2165
|
+
const filePath = join12(dir, name);
|
|
2166
|
+
try {
|
|
2167
|
+
const info = await stat(filePath);
|
|
2168
|
+
if (!info.isFile() || info.size > MAX_BUNDLE_READ) continue;
|
|
2169
|
+
const contents = await readFile6(filePath, "utf8");
|
|
2170
|
+
if (!contents.includes("OAUTH_CLIENT_SECRET")) continue;
|
|
2171
|
+
const match = GOOGLE_OAUTH_CLIENT_REGEX.exec(contents);
|
|
2172
|
+
if (match) return { clientId: match[1], clientSecret: match[2] };
|
|
2173
|
+
} catch {
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
2178
|
+
async function discoverGoogleOAuthClient() {
|
|
2179
|
+
try {
|
|
2180
|
+
for (const candidate of geminiBundleCandidates()) {
|
|
2181
|
+
const dir = await resolveBundleDir(candidate);
|
|
2182
|
+
if (!dir) continue;
|
|
2183
|
+
const found = await scanBundleDir(dir);
|
|
2184
|
+
if (found) return found;
|
|
2185
|
+
}
|
|
2186
|
+
} catch {
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
async function resolveGoogleClient() {
|
|
2191
|
+
const envId = process.env.TOKMON_GOOGLE_CLIENT_ID?.trim();
|
|
2192
|
+
const envSecret = process.env.TOKMON_GOOGLE_CLIENT_SECRET?.trim();
|
|
2193
|
+
if (envId && envSecret) return { clientId: envId, clientSecret: envSecret };
|
|
2194
|
+
if (cachedClient === void 0) cachedClient = await discoverGoogleOAuthClient();
|
|
2195
|
+
return cachedClient;
|
|
2196
|
+
}
|
|
2197
|
+
async function refreshAccessToken(refreshToken) {
|
|
2198
|
+
if (!refreshToken) return null;
|
|
2199
|
+
const client = await resolveGoogleClient();
|
|
2200
|
+
if (!client) return null;
|
|
2201
|
+
const body = new URLSearchParams({
|
|
2202
|
+
client_id: client.clientId,
|
|
2203
|
+
client_secret: client.clientSecret,
|
|
2204
|
+
refresh_token: refreshToken,
|
|
2205
|
+
grant_type: "refresh_token"
|
|
2206
|
+
});
|
|
2207
|
+
try {
|
|
2208
|
+
const res = await fetch(GOOGLE_OAUTH_URL, {
|
|
2209
|
+
method: "POST",
|
|
2210
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2211
|
+
body,
|
|
2212
|
+
signal: AbortSignal.timeout(15e3)
|
|
2213
|
+
});
|
|
2214
|
+
if (!res.ok) return null;
|
|
2215
|
+
const json = await readJson(res);
|
|
2216
|
+
return typeof json?.access_token === "string" && json.access_token.trim() ? json.access_token.trim() : null;
|
|
2217
|
+
} catch {
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
async function requestCloudCodeJson(path, token, body) {
|
|
2222
|
+
for (const base of CLOUD_CODE_URLS) {
|
|
2223
|
+
try {
|
|
2224
|
+
const res = await fetch(`${base}${path}`, {
|
|
2225
|
+
method: "POST",
|
|
2226
|
+
headers: {
|
|
2227
|
+
Accept: "application/json",
|
|
2228
|
+
"Content-Type": "application/json",
|
|
2229
|
+
Authorization: `Bearer ${token}`,
|
|
2230
|
+
"User-Agent": "agy"
|
|
2231
|
+
},
|
|
2232
|
+
body: JSON.stringify(body ?? {}),
|
|
2233
|
+
signal: AbortSignal.timeout(15e3)
|
|
2234
|
+
});
|
|
2235
|
+
if (res.status === 401 || res.status === 403) return { _authFailed: true };
|
|
2236
|
+
if (!res.ok) continue;
|
|
2237
|
+
const json = await readJson(res);
|
|
2238
|
+
if (json && typeof json === "object") return json;
|
|
2239
|
+
} catch {
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
function readPlan(loadData) {
|
|
2245
|
+
const paid = typeof loadData?.paidTier?.name === "string" ? loadData.paidTier.name.trim() : "";
|
|
2246
|
+
if (paid) return paid;
|
|
2247
|
+
const current = typeof loadData?.currentTier?.name === "string" ? loadData.currentTier.name.trim() : "";
|
|
2248
|
+
return current || null;
|
|
2249
|
+
}
|
|
2250
|
+
function parseBuckets(data) {
|
|
2251
|
+
if (!Array.isArray(data?.buckets)) return [];
|
|
2252
|
+
return data.buckets.flatMap((bucket) => {
|
|
2253
|
+
const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
|
|
2254
|
+
if (!modelId) return [];
|
|
2255
|
+
return [{
|
|
2256
|
+
modelId,
|
|
2257
|
+
remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
|
|
2258
|
+
resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
|
|
2259
|
+
}];
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
function parseModelBuckets(data) {
|
|
2263
|
+
const models = data?.models;
|
|
2264
|
+
if (!models || typeof models !== "object") return [];
|
|
2265
|
+
return Object.keys(models).flatMap((key) => {
|
|
2266
|
+
const model = models[key];
|
|
2267
|
+
if (!model || typeof model !== "object" || model.isInternal) return [];
|
|
2268
|
+
const modelId = typeof model.model === "string" && model.model.trim() ? model.model.trim() : key;
|
|
2269
|
+
if (CC_MODEL_BLACKLIST[modelId]) return [];
|
|
2270
|
+
const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
|
|
2271
|
+
if (!displayName) return [];
|
|
2272
|
+
const quotaInfo = model.quotaInfo;
|
|
2273
|
+
return [{
|
|
2274
|
+
modelId: displayName,
|
|
2275
|
+
remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
|
|
2276
|
+
resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
|
|
2277
|
+
}];
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
async function fetchWithAccessToken(accessToken) {
|
|
2281
|
+
const loadData = await requestCloudCodeJson(LOAD_CODE_ASSIST_PATH, accessToken, {});
|
|
2282
|
+
if (!loadData) return { ok: false, plan: null, error: "Cloud Code API error" };
|
|
2283
|
+
if ("_authFailed" in loadData) return { ok: false, plan: null, error: "Token expired" };
|
|
2284
|
+
const plan = readPlan(loadData);
|
|
2285
|
+
const project = typeof loadData.cloudaicompanionProject === "string" && loadData.cloudaicompanionProject.trim() ? loadData.cloudaicompanionProject.trim() : null;
|
|
2286
|
+
let quotaData = project ? await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, { project }) : null;
|
|
2287
|
+
if (!quotaData || "_authFailed" in quotaData) {
|
|
2288
|
+
quotaData = await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, {});
|
|
2289
|
+
}
|
|
2290
|
+
if (!quotaData) return { ok: false, plan, error: "Cloud Code quota unavailable" };
|
|
2291
|
+
if ("_authFailed" in quotaData) return { ok: false, plan, error: "Token expired" };
|
|
2292
|
+
let buckets = parseBuckets(quotaData);
|
|
2293
|
+
if (buckets.length === 0) {
|
|
2294
|
+
const modelData = await requestCloudCodeJson(FETCH_MODELS_PATH, accessToken, {});
|
|
2295
|
+
if (modelData && !("_authFailed" in modelData)) buckets = parseModelBuckets(modelData);
|
|
2296
|
+
if (modelData && "_authFailed" in modelData) return { ok: false, plan, error: "Token expired" };
|
|
2297
|
+
}
|
|
2298
|
+
if (buckets.length === 0) return { ok: false, plan, error: "No quota data" };
|
|
2299
|
+
return { ok: true, plan, buckets };
|
|
2300
|
+
}
|
|
2301
|
+
async function fetchCloudCodeQuota(token, expiredMessage = "Token expired") {
|
|
2302
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
2303
|
+
let accessToken = token.accessToken?.trim() || null;
|
|
2304
|
+
if (accessToken && token.expirySeconds && token.expirySeconds <= nowSec) {
|
|
2305
|
+
accessToken = await refreshAccessToken(token.refreshToken);
|
|
2306
|
+
if (!accessToken) return { ok: false, plan: null, error: expiredMessage };
|
|
2307
|
+
}
|
|
2308
|
+
if (!accessToken) {
|
|
2309
|
+
accessToken = await refreshAccessToken(token.refreshToken);
|
|
2310
|
+
if (!accessToken) return { ok: false, plan: null, error: `Missing credentials (${redact(token.accessToken)})` };
|
|
2311
|
+
}
|
|
2312
|
+
const result = await fetchWithAccessToken(accessToken);
|
|
2313
|
+
if (result.ok || result.error !== "Token expired") return result;
|
|
2314
|
+
const refreshed = await refreshAccessToken(token.refreshToken);
|
|
2315
|
+
if (!refreshed) return { ok: false, plan: result.plan, error: expiredMessage };
|
|
2316
|
+
return fetchWithAccessToken(refreshed);
|
|
2317
|
+
}
|
|
2318
|
+
function normalizeLabel(label) {
|
|
2319
|
+
return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
2320
|
+
}
|
|
2321
|
+
function poolLabel(label) {
|
|
2322
|
+
const lower = normalizeLabel(label).toLowerCase();
|
|
2323
|
+
if (lower.includes("gemini") && lower.includes("pro")) return "Gemini Pro";
|
|
2324
|
+
if (lower.includes("gemini") && lower.includes("flash")) return "Gemini Flash";
|
|
2325
|
+
return "Claude";
|
|
2326
|
+
}
|
|
2327
|
+
function sortKey(label) {
|
|
2328
|
+
const lower = label.toLowerCase();
|
|
2329
|
+
if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
|
|
2330
|
+
if (lower.includes("gemini")) return `0b_${label}`;
|
|
2331
|
+
if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
|
|
2332
|
+
if (lower.includes("claude")) return `1b_${label}`;
|
|
2333
|
+
return `2_${label}`;
|
|
2334
|
+
}
|
|
2335
|
+
function cloudCodeBucketsToMetrics(buckets) {
|
|
2336
|
+
const pooled = /* @__PURE__ */ new Map();
|
|
2337
|
+
for (const bucket of buckets) {
|
|
2338
|
+
const label = poolLabel(bucket.modelId);
|
|
2339
|
+
const existing = pooled.get(label);
|
|
2340
|
+
if (!existing || bucket.remainingFraction < existing.remainingFraction) {
|
|
2341
|
+
pooled.set(label, { ...bucket, modelId: label });
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return [...pooled.values()].sort((a, b) => sortKey(a.modelId).localeCompare(sortKey(b.modelId))).map((bucket, i) => {
|
|
2345
|
+
const clamped = Math.max(0, Math.min(1, bucket.remainingFraction));
|
|
2346
|
+
return {
|
|
2347
|
+
label: bucket.modelId,
|
|
2348
|
+
used: Math.round((1 - clamped) * 100),
|
|
2349
|
+
limit: 100,
|
|
2350
|
+
format: { kind: "percent" },
|
|
2351
|
+
resetsAt: bucket.resetTime ? resetIn(bucket.resetTime) : null,
|
|
2352
|
+
primary: i === 0
|
|
2353
|
+
};
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
function cloudCodeSqliteError(status) {
|
|
2357
|
+
return status === "ok" ? "Not signed in \u2014 open Antigravity" : sqliteStatusMessage(status).replace(/Cursor/g, "Antigravity");
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/providers/antigravity/billing.ts
|
|
2361
|
+
async function exists(path) {
|
|
2362
|
+
try {
|
|
2363
|
+
await access7(path);
|
|
2364
|
+
return true;
|
|
2365
|
+
} catch {
|
|
2366
|
+
return false;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
async function firstExisting(paths) {
|
|
2370
|
+
for (const path of paths) {
|
|
2371
|
+
if (await exists(path)) return path;
|
|
2372
|
+
}
|
|
2373
|
+
return paths[0];
|
|
2374
|
+
}
|
|
2375
|
+
async function antigravityStateDb(homeDir) {
|
|
2376
|
+
const base = homeDir ?? homedir11();
|
|
2377
|
+
const tail = ["User", "globalStorage", "state.vscdb"];
|
|
2378
|
+
if (process.platform === "darwin") {
|
|
2379
|
+
const support = join13(base, "Library", "Application Support");
|
|
2380
|
+
const exact = [
|
|
2381
|
+
join13(support, "Antigravity IDE", ...tail),
|
|
2382
|
+
join13(support, "Antigravity", ...tail)
|
|
2383
|
+
];
|
|
2384
|
+
try {
|
|
2385
|
+
const entries = await readdir7(support, { withFileTypes: true });
|
|
2386
|
+
const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join13(support, e.name, ...tail));
|
|
2387
|
+
return firstExisting([...exact, ...matches]);
|
|
2388
|
+
} catch {
|
|
2389
|
+
return firstExisting(exact);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
if (process.platform === "win32") {
|
|
2393
|
+
const roaming = homeDir ? join13(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join13(base, "AppData", "Roaming");
|
|
2394
|
+
return firstExisting([
|
|
2395
|
+
join13(roaming, "Antigravity IDE", ...tail),
|
|
2396
|
+
join13(roaming, "Antigravity", ...tail)
|
|
2397
|
+
]);
|
|
2398
|
+
}
|
|
2399
|
+
const cfg = homeDir ? join13(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join13(base, ".config");
|
|
2400
|
+
return firstExisting([
|
|
2401
|
+
join13(cfg, "Antigravity IDE", ...tail),
|
|
2402
|
+
join13(cfg, "Antigravity", ...tail)
|
|
2403
|
+
]);
|
|
2404
|
+
}
|
|
2405
|
+
async function detectAntigravity(homeDir) {
|
|
2406
|
+
return exists(await antigravityStateDb(homeDir));
|
|
2407
|
+
}
|
|
2408
|
+
async function antigravityBilling(account) {
|
|
2409
|
+
try {
|
|
2410
|
+
const db = await antigravityStateDb(account.homeDir);
|
|
2411
|
+
const { token, status } = await readAntigravityOAuthToken(db);
|
|
2412
|
+
if (!token) return { plan: null, metrics: [], error: cloudCodeSqliteError(status) };
|
|
2413
|
+
const quota = await fetchCloudCodeQuota(token, "Token expired \u2014 open Antigravity");
|
|
2414
|
+
if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
|
|
2415
|
+
return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
|
|
2416
|
+
} catch {
|
|
2417
|
+
return { plan: null, metrics: [], error: "Antigravity billing unavailable" };
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// src/providers/antigravity/index.ts
|
|
2422
|
+
var antigravityProvider = {
|
|
2423
|
+
id: "antigravity",
|
|
2424
|
+
name: "Antigravity",
|
|
2425
|
+
color: "red",
|
|
2426
|
+
hasUsage: false,
|
|
2427
|
+
hasBilling: true,
|
|
2428
|
+
detect: (homeDir) => detectAntigravity(homeDir),
|
|
2429
|
+
fetchBilling: (account) => antigravityBilling(account)
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
// src/providers/gemini/billing.ts
|
|
2433
|
+
import { access as access8, readFile as readFile7 } from "fs/promises";
|
|
2434
|
+
import { join as join14 } from "path";
|
|
2435
|
+
import { homedir as homedir12 } from "os";
|
|
2436
|
+
function geminiCredsPath(homeDir) {
|
|
2437
|
+
return join14(homeDir ?? homedir12(), ".gemini", "oauth_creds.json");
|
|
2438
|
+
}
|
|
2439
|
+
async function detectGemini(homeDir) {
|
|
2440
|
+
try {
|
|
2441
|
+
await access8(geminiCredsPath(homeDir));
|
|
2442
|
+
return true;
|
|
2443
|
+
} catch {
|
|
2444
|
+
return false;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
async function readGeminiCreds(path) {
|
|
2448
|
+
try {
|
|
2449
|
+
return JSON.parse(await readFile7(path, "utf8"));
|
|
2450
|
+
} catch {
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
async function geminiBilling(account) {
|
|
2455
|
+
try {
|
|
2456
|
+
const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
|
|
2457
|
+
const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
|
|
2458
|
+
const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
|
|
2459
|
+
if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
|
|
2460
|
+
const quota = await fetchCloudCodeQuota({
|
|
2461
|
+
accessToken,
|
|
2462
|
+
refreshToken,
|
|
2463
|
+
expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
|
|
2464
|
+
}, "Token expired \u2014 run gemini");
|
|
2465
|
+
if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
|
|
2466
|
+
return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
|
|
2467
|
+
} catch {
|
|
2468
|
+
return { plan: null, metrics: [], error: "Gemini billing unavailable" };
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/providers/gemini/index.ts
|
|
2473
|
+
var geminiProvider = {
|
|
2474
|
+
id: "gemini",
|
|
2475
|
+
name: "Gemini",
|
|
2476
|
+
color: "greenBright",
|
|
2477
|
+
hasUsage: false,
|
|
2478
|
+
hasBilling: true,
|
|
2479
|
+
detect: (homeDir) => detectGemini(homeDir),
|
|
2480
|
+
fetchBilling: (account) => geminiBilling(account)
|
|
2481
|
+
};
|
|
2482
|
+
|
|
1417
2483
|
// src/providers/detect.ts
|
|
1418
2484
|
import { accessSync, constants } from "fs";
|
|
1419
|
-
import { join as
|
|
1420
|
-
import { homedir as
|
|
2485
|
+
import { join as join15, delimiter, isAbsolute as isAbsolute3 } from "path";
|
|
2486
|
+
import { homedir as homedir13 } from "os";
|
|
1421
2487
|
function searchDirs() {
|
|
1422
|
-
const home =
|
|
2488
|
+
const home = homedir13();
|
|
1423
2489
|
const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
1424
2490
|
const extra = process.platform === "win32" ? [
|
|
1425
|
-
process.env.APPDATA &&
|
|
1426
|
-
process.env.LOCALAPPDATA &&
|
|
1427
|
-
|
|
2491
|
+
process.env.APPDATA && join15(process.env.APPDATA, "npm"),
|
|
2492
|
+
process.env.LOCALAPPDATA && join15(process.env.LOCALAPPDATA, "pnpm"),
|
|
2493
|
+
join15(home, "scoop", "shims")
|
|
1428
2494
|
] : [
|
|
1429
2495
|
"/opt/homebrew/bin",
|
|
1430
2496
|
"/usr/local/bin",
|
|
1431
2497
|
"/usr/bin",
|
|
1432
2498
|
"/bin",
|
|
1433
2499
|
"/opt/local/bin",
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
2500
|
+
join15(home, ".local", "bin"),
|
|
2501
|
+
join15(home, "bin"),
|
|
2502
|
+
join15(home, ".npm-global", "bin"),
|
|
2503
|
+
join15(home, ".bun", "bin"),
|
|
2504
|
+
join15(home, ".local", "share", "pnpm")
|
|
1439
2505
|
];
|
|
1440
2506
|
return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
|
|
1441
2507
|
}
|
|
@@ -1452,7 +2518,7 @@ function onPath(names) {
|
|
|
1452
2518
|
for (const dir of searchDirs()) {
|
|
1453
2519
|
for (const n of names) {
|
|
1454
2520
|
for (const e of exts) {
|
|
1455
|
-
if (isExec(
|
|
2521
|
+
if (isExec(join15(dir, n + e))) return true;
|
|
1456
2522
|
}
|
|
1457
2523
|
}
|
|
1458
2524
|
}
|
|
@@ -1462,7 +2528,7 @@ function anyExists(paths) {
|
|
|
1462
2528
|
return paths.some((p) => !!p && isExec(p));
|
|
1463
2529
|
}
|
|
1464
2530
|
function installSignals(id) {
|
|
1465
|
-
const home =
|
|
2531
|
+
const home = homedir13();
|
|
1466
2532
|
const pf = process.env.ProgramFiles;
|
|
1467
2533
|
const pf86 = process.env["ProgramFiles(x86)"];
|
|
1468
2534
|
const lad = process.env.LOCALAPPDATA;
|
|
@@ -1470,8 +2536,8 @@ function installSignals(id) {
|
|
|
1470
2536
|
case "claude":
|
|
1471
2537
|
return onPath(["claude"]) || anyExists([
|
|
1472
2538
|
"/Applications/Claude.app",
|
|
1473
|
-
|
|
1474
|
-
lad &&
|
|
2539
|
+
join15(home, "Applications", "Claude.app"),
|
|
2540
|
+
lad && join15(lad, "Programs", "claude", "Claude.exe")
|
|
1475
2541
|
]);
|
|
1476
2542
|
case "codex": {
|
|
1477
2543
|
const bin = process.env.CODEX_BIN;
|
|
@@ -1481,25 +2547,44 @@ function installSignals(id) {
|
|
|
1481
2547
|
case "cursor":
|
|
1482
2548
|
return onPath(["cursor", "cursor-agent"]) || anyExists([
|
|
1483
2549
|
"/Applications/Cursor.app",
|
|
1484
|
-
|
|
1485
|
-
lad &&
|
|
1486
|
-
pf &&
|
|
1487
|
-
pf86 &&
|
|
2550
|
+
join15(home, "Applications", "Cursor.app"),
|
|
2551
|
+
lad && join15(lad, "Programs", "cursor", "Cursor.exe"),
|
|
2552
|
+
pf && join15(pf, "Cursor", "Cursor.exe"),
|
|
2553
|
+
pf86 && join15(pf86, "Cursor", "Cursor.exe"),
|
|
1488
2554
|
"/opt/Cursor/cursor",
|
|
1489
2555
|
"/usr/share/cursor/cursor",
|
|
1490
2556
|
"/usr/bin/cursor"
|
|
1491
2557
|
]);
|
|
2558
|
+
case "pi":
|
|
2559
|
+
return onPath(["pi"]);
|
|
2560
|
+
case "opencode":
|
|
2561
|
+
return onPath(["opencode"]);
|
|
2562
|
+
case "copilot":
|
|
2563
|
+
return onPath(["gh"]);
|
|
2564
|
+
case "antigravity":
|
|
2565
|
+
return onPath(["antigravity"]) || anyExists([
|
|
2566
|
+
"/Applications/Antigravity.app",
|
|
2567
|
+
join15(home, "Applications", "Antigravity.app"),
|
|
2568
|
+
lad && join15(lad, "Programs", "Antigravity", "Antigravity.exe")
|
|
2569
|
+
]);
|
|
2570
|
+
case "gemini":
|
|
2571
|
+
return onPath(["gemini"]);
|
|
1492
2572
|
default:
|
|
1493
2573
|
return false;
|
|
1494
2574
|
}
|
|
1495
2575
|
}
|
|
1496
2576
|
|
|
1497
2577
|
// src/providers/index.ts
|
|
1498
|
-
var PROVIDER_ORDER = ["claude", "codex", "cursor"];
|
|
2578
|
+
var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
|
|
1499
2579
|
var PROVIDERS = {
|
|
1500
2580
|
claude: claudeProvider,
|
|
1501
2581
|
codex: codexProvider,
|
|
1502
|
-
cursor: cursorProvider
|
|
2582
|
+
cursor: cursorProvider,
|
|
2583
|
+
pi: piProvider,
|
|
2584
|
+
opencode: opencodeProvider,
|
|
2585
|
+
copilot: copilotProvider,
|
|
2586
|
+
antigravity: antigravityProvider,
|
|
2587
|
+
gemini: geminiProvider
|
|
1503
2588
|
};
|
|
1504
2589
|
var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
|
|
1505
2590
|
async function detectProviders() {
|
|
@@ -1548,14 +2633,46 @@ function accountsByProvider(accounts) {
|
|
|
1548
2633
|
return groups;
|
|
1549
2634
|
}
|
|
1550
2635
|
|
|
2636
|
+
// src/snapshot.ts
|
|
2637
|
+
import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir3, rename as rename3 } from "fs/promises";
|
|
2638
|
+
import { join as join16 } from "path";
|
|
2639
|
+
function snapshotFile() {
|
|
2640
|
+
return join16(cacheDir(), "dashboard-snapshot.json");
|
|
2641
|
+
}
|
|
2642
|
+
async function loadSnapshot() {
|
|
2643
|
+
try {
|
|
2644
|
+
const obj = JSON.parse(await readFile8(snapshotFile(), "utf-8"));
|
|
2645
|
+
return obj && typeof obj === "object" ? obj : {};
|
|
2646
|
+
} catch {
|
|
2647
|
+
return {};
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
var saveQueue2 = Promise.resolve();
|
|
2651
|
+
function saveSnapshot(stats) {
|
|
2652
|
+
const obj = {};
|
|
2653
|
+
for (const [id, s] of stats) {
|
|
2654
|
+
if (s.dashboard || s.billing) obj[id] = { dashboard: s.dashboard ?? null, billing: s.billing ?? null };
|
|
2655
|
+
}
|
|
2656
|
+
saveQueue2 = saveQueue2.then(async () => {
|
|
2657
|
+
try {
|
|
2658
|
+
const dir = cacheDir();
|
|
2659
|
+
await mkdir3(dir, { recursive: true });
|
|
2660
|
+
const tmp = join16(dir, `dashboard-snapshot.json.${process.pid}.tmp`);
|
|
2661
|
+
await writeFile3(tmp, JSON.stringify(obj));
|
|
2662
|
+
await rename3(tmp, snapshotFile());
|
|
2663
|
+
} catch {
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
|
|
1551
2668
|
// src/ui/shared.tsx
|
|
1552
2669
|
import { useState, useEffect, useRef } from "react";
|
|
1553
2670
|
import { Box, Text } from "ink";
|
|
1554
2671
|
import { useOnMouseClick } from "@zenobius/ink-mouse";
|
|
1555
2672
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1556
|
-
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1557
2673
|
function truncateName(s, n) {
|
|
1558
|
-
|
|
2674
|
+
const ell = glyphs().ellipsis;
|
|
2675
|
+
return s.length > n ? s.slice(0, n - ell.length) + ell : s;
|
|
1559
2676
|
}
|
|
1560
2677
|
function ClickableBox({ onClick, children, ...props }) {
|
|
1561
2678
|
const ref = useRef(null);
|
|
@@ -1565,21 +2682,22 @@ function ClickableBox({ onClick, children, ...props }) {
|
|
|
1565
2682
|
return /* @__PURE__ */ jsx(Box, { ref, ...props, children });
|
|
1566
2683
|
}
|
|
1567
2684
|
function Spinner({ label }) {
|
|
2685
|
+
const frames = glyphs().spinner;
|
|
1568
2686
|
const [i, setI] = useState(0);
|
|
1569
2687
|
useEffect(() => {
|
|
1570
|
-
const id = setInterval(() => setI((n) => (n + 1) %
|
|
2688
|
+
const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
|
|
1571
2689
|
return () => clearInterval(id);
|
|
1572
2690
|
}, []);
|
|
1573
2691
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1574
2692
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
1575
|
-
|
|
2693
|
+
frames[i],
|
|
1576
2694
|
" "
|
|
1577
2695
|
] }),
|
|
1578
2696
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
|
|
1579
2697
|
] });
|
|
1580
2698
|
}
|
|
1581
|
-
function TabBar({ tabs, active, onSelect }) {
|
|
1582
|
-
return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i ===
|
|
2699
|
+
function TabBar({ tabs, active: active2, onSelect }) {
|
|
2700
|
+
return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(ClickableBox, { onClick: () => onSelect(i), marginRight: 1, children: i === active2 ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
|
|
1583
2701
|
" ",
|
|
1584
2702
|
t,
|
|
1585
2703
|
" "
|
|
@@ -1592,7 +2710,10 @@ function TabBar({ tabs, active, onSelect }) {
|
|
|
1592
2710
|
function PeakBadge({ peak }) {
|
|
1593
2711
|
const color = peak.state === "peak" ? "red" : "green";
|
|
1594
2712
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1595
|
-
/* @__PURE__ */
|
|
2713
|
+
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
2714
|
+
glyphs().dot,
|
|
2715
|
+
" "
|
|
2716
|
+
] }),
|
|
1596
2717
|
/* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
|
|
1597
2718
|
peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1598
2719
|
" (",
|
|
@@ -1608,24 +2729,24 @@ function fmtMinutes(mins) {
|
|
|
1608
2729
|
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
1609
2730
|
}
|
|
1610
2731
|
function currencySymbol(cur) {
|
|
1611
|
-
return cur === "EUR" ?
|
|
2732
|
+
return cur === "EUR" ? glyphs().eur : cur === "GBP" ? glyphs().gbp : "$";
|
|
1612
2733
|
}
|
|
1613
|
-
var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
1614
2734
|
function sparkline(values) {
|
|
1615
2735
|
if (values.length === 0) return "";
|
|
2736
|
+
const spark = glyphs().spark;
|
|
1616
2737
|
const max = Math.max(...values);
|
|
1617
|
-
if (max <= 0) return
|
|
2738
|
+
if (max <= 0) return spark[0].repeat(values.length);
|
|
1618
2739
|
return values.map((v) => {
|
|
1619
|
-
if (v <= 0) return
|
|
1620
|
-
const idx = Math.min(
|
|
1621
|
-
return
|
|
2740
|
+
if (v <= 0) return spark[0];
|
|
2741
|
+
const idx = Math.min(spark.length - 1, 1 + Math.round(v / max * (spark.length - 2)));
|
|
2742
|
+
return spark[idx];
|
|
1622
2743
|
}).join("");
|
|
1623
2744
|
}
|
|
1624
2745
|
function Bar({ pct: pct2, color, width = 24 }) {
|
|
1625
2746
|
const filled = Math.max(0, Math.min(width, Math.round(pct2 / 100 * width)));
|
|
1626
2747
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
1627
|
-
/* @__PURE__ */ jsx(Text, { color, children:
|
|
1628
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children:
|
|
2748
|
+
/* @__PURE__ */ jsx(Text, { color, children: glyphs().barFull.repeat(filled) }),
|
|
2749
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: glyphs().barEmpty.repeat(width - filled) })
|
|
1629
2750
|
] });
|
|
1630
2751
|
}
|
|
1631
2752
|
function metricValueText(m) {
|
|
@@ -1647,20 +2768,76 @@ import { Box as Box2, Text as Text2 } from "ink";
|
|
|
1647
2768
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1648
2769
|
var GAP = 2;
|
|
1649
2770
|
var MIN_CARD = 56;
|
|
1650
|
-
|
|
2771
|
+
var MIN_CARD_DENSE = 44;
|
|
2772
|
+
var CARD_H = { full: 14, compact: 12, mini: 8 };
|
|
2773
|
+
var VARIANT_ORDER = ["full", "compact", "mini"];
|
|
2774
|
+
var INDICATOR_ROWS = 1;
|
|
2775
|
+
var MAX_SINGLE_CARD = Math.round(MIN_CARD * 1.6);
|
|
2776
|
+
function chooseLayout(content, budget, n, single, cols) {
|
|
2777
|
+
if (n <= 0) return { ncols: 1, variant: "mini", cardsPerPage: 1, pageCount: 1 };
|
|
2778
|
+
const gridHeight = (rows, H2) => rows * H2 + Math.max(0, rows - 1);
|
|
2779
|
+
const colCap = single ? 1 : cols >= 3 * MIN_CARD_DENSE + 2 * GAP ? 3 : cols >= 2 * MIN_CARD + GAP ? 2 : 1;
|
|
2780
|
+
const maxCols = Math.max(1, Math.min(colCap, n));
|
|
2781
|
+
const cardWidthAt = (nc) => nc <= 1 ? content : Math.floor((content - GAP * (nc - 1)) / nc);
|
|
2782
|
+
const minWidthAt = (nc) => nc >= 3 ? MIN_CARD_DENSE : MIN_CARD;
|
|
2783
|
+
for (const variant of VARIANT_ORDER) {
|
|
2784
|
+
for (let nc = maxCols; nc >= 1; nc--) {
|
|
2785
|
+
if (nc > 1 && cardWidthAt(nc) < minWidthAt(nc)) continue;
|
|
2786
|
+
const rows = Math.ceil(n / nc);
|
|
2787
|
+
if (gridHeight(rows, CARD_H[variant]) <= budget) {
|
|
2788
|
+
return { ncols: nc, variant, cardsPerPage: n, pageCount: 1 };
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
let ncols = 1;
|
|
2793
|
+
for (let nc = maxCols; nc >= 1; nc--) {
|
|
2794
|
+
if (nc === 1 || cardWidthAt(nc) >= minWidthAt(nc)) {
|
|
2795
|
+
ncols = nc;
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
const H = CARD_H.mini;
|
|
2800
|
+
const fitBudget = budget - INDICATOR_ROWS;
|
|
2801
|
+
const rowsThatFit = Math.max(1, Math.floor((fitBudget + 1) / (H + 1)));
|
|
2802
|
+
const cardsPerPage = Math.max(1, rowsThatFit * ncols);
|
|
2803
|
+
const pageCount = Math.max(1, Math.ceil(n / cardsPerPage));
|
|
2804
|
+
return { ncols, variant: "mini", cardsPerPage, pageCount };
|
|
2805
|
+
}
|
|
2806
|
+
function DashboardView({ groups, stats, cols, budget, focusId, layout, page = 0 }) {
|
|
1651
2807
|
if (groups.length === 0) {
|
|
1652
|
-
return /* @__PURE__ */
|
|
2808
|
+
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2809
|
+
"No providers enabled ",
|
|
2810
|
+
glyphs().emDash,
|
|
2811
|
+
" press s to pick providers."
|
|
2812
|
+
] });
|
|
1653
2813
|
}
|
|
1654
2814
|
let shown = groups;
|
|
1655
2815
|
if (layout === "single" && focusId === null) shown = groups.slice(0, 1);
|
|
1656
|
-
const content = Math.max(MIN_CARD, cols - 4);
|
|
1657
2816
|
const single = focusId !== null || layout === "single";
|
|
1658
|
-
const
|
|
1659
|
-
const ncols
|
|
1660
|
-
|
|
1661
|
-
|
|
2817
|
+
const content = Math.max(MIN_CARD, cols - 4);
|
|
2818
|
+
const { ncols, variant, cardsPerPage, pageCount } = chooseLayout(content, budget, shown.length, single, cols);
|
|
2819
|
+
let cardW = ncols <= 1 ? content : Math.floor((content - GAP * (ncols - 1)) / ncols);
|
|
2820
|
+
if (ncols === 1 && cardW > MAX_SINGLE_CARD) cardW = MAX_SINGLE_CARD;
|
|
2821
|
+
const pg = pageCount > 1 ? (page % pageCount + pageCount) % pageCount : 0;
|
|
2822
|
+
const visible = pageCount > 1 ? shown.slice(pg * cardsPerPage, pg * cardsPerPage + cardsPerPage) : shown;
|
|
2823
|
+
return /* @__PURE__ */ jsxs2(Box2, { height: budget, flexDirection: "column", overflow: "hidden", children: [
|
|
2824
|
+
/* @__PURE__ */ jsx2(Box2, { width: content, flexWrap: "wrap", columnGap: GAP, rowGap: 1, children: visible.map((g) => /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: /* @__PURE__ */ jsx2(ProviderCard, { provider: g.provider, accounts: g.accounts, stats, width: cardW, variant }) }, g.provider)) }),
|
|
2825
|
+
pageCount > 1 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2826
|
+
" ",
|
|
2827
|
+
glyphs().middot,
|
|
2828
|
+
" page ",
|
|
2829
|
+
pg + 1,
|
|
2830
|
+
"/",
|
|
2831
|
+
pageCount,
|
|
2832
|
+
" ",
|
|
2833
|
+
glyphs().middot,
|
|
2834
|
+
" scroll ",
|
|
2835
|
+
glyphs().arrowU,
|
|
2836
|
+
glyphs().arrowD
|
|
2837
|
+
] })
|
|
2838
|
+
] });
|
|
1662
2839
|
}
|
|
1663
|
-
function ProviderCard({ provider, accounts, stats, width }) {
|
|
2840
|
+
function ProviderCard({ provider, accounts, stats, width, variant }) {
|
|
1664
2841
|
const meta = PROVIDERS[provider];
|
|
1665
2842
|
const items = accounts.map((a) => ({ account: a, s: stats.get(a.id) }));
|
|
1666
2843
|
const dashboards = items.map((i) => i.s?.dashboard).filter((d) => !!d);
|
|
@@ -1670,9 +2847,14 @@ function ProviderCard({ provider, accounts, stats, width }) {
|
|
|
1670
2847
|
const inner = width - 4;
|
|
1671
2848
|
const barW = Math.max(10, Math.min(46, inner - 20));
|
|
1672
2849
|
const hasSpark = !!agg && agg.series.some((v) => v > 0);
|
|
1673
|
-
|
|
2850
|
+
const showBars = variant !== "mini";
|
|
2851
|
+
const showSpark = variant === "full";
|
|
2852
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, borderStyle: glyphs().border, borderColor: meta.color, paddingX: 1, children: [
|
|
1674
2853
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1675
|
-
/* @__PURE__ */
|
|
2854
|
+
/* @__PURE__ */ jsxs2(Text2, { color: meta.color, children: [
|
|
2855
|
+
glyphs().dot,
|
|
2856
|
+
" "
|
|
2857
|
+
] }),
|
|
1676
2858
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: meta.color, children: meta.name }),
|
|
1677
2859
|
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
|
|
1678
2860
|
plan && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: plan })
|
|
@@ -1685,17 +2867,21 @@ function ProviderCard({ provider, accounts, stats, width }) {
|
|
|
1685
2867
|
/* @__PURE__ */ jsx2(KpiLine, { agg })
|
|
1686
2868
|
] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1687
2869
|
/* @__PURE__ */ jsx2(Box2, { height: 1 }),
|
|
1688
|
-
/* @__PURE__ */
|
|
2870
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2871
|
+
"Fetching usage",
|
|
2872
|
+
glyphs().ellipsis
|
|
2873
|
+
] })
|
|
1689
2874
|
] })),
|
|
1690
|
-
meta.hasBilling && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
2875
|
+
meta.hasBilling && showBars && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1691
2876
|
meta.hasUsage && /* @__PURE__ */ jsx2(Rule, { inner }),
|
|
1692
2877
|
/* @__PURE__ */ jsx2(LimitsBlock, { items, barW })
|
|
1693
2878
|
] }),
|
|
1694
|
-
|
|
2879
|
+
meta.hasBilling && !showBars && !meta.hasUsage && /* @__PURE__ */ jsx2(CompactBilling, { items }),
|
|
2880
|
+
hasSpark && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1695
2881
|
/* @__PURE__ */ jsx2(Rule, { inner }),
|
|
1696
2882
|
/* @__PURE__ */ jsx2(SparkFooter, { series: agg.series, month: agg.month.cost, color: meta.color })
|
|
1697
2883
|
] }),
|
|
1698
|
-
!meta.hasUsage && activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
2884
|
+
!meta.hasUsage && activity && showSpark && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1699
2885
|
/* @__PURE__ */ jsx2(Rule, { inner }),
|
|
1700
2886
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1701
2887
|
/* @__PURE__ */ jsx2(Box2, { width: 4, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "14d" }) }),
|
|
@@ -1703,14 +2889,29 @@ function ProviderCard({ provider, accounts, stats, width }) {
|
|
|
1703
2889
|
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1, justifyContent: "flex-end", children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: activity.summary }) })
|
|
1704
2890
|
] })
|
|
1705
2891
|
] }),
|
|
1706
|
-
!meta.hasUsage && !activity && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
2892
|
+
!meta.hasUsage && !activity && variant === "full" && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1707
2893
|
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
|
|
1708
|
-
/* @__PURE__ */
|
|
2894
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2895
|
+
"Billing only ",
|
|
2896
|
+
glyphs().emDash,
|
|
2897
|
+
" no local history"
|
|
2898
|
+
] })
|
|
1709
2899
|
] })
|
|
1710
2900
|
] });
|
|
1711
2901
|
}
|
|
2902
|
+
function CompactBilling({ items }) {
|
|
2903
|
+
const billing = items.map((i) => i.s?.billing).find(Boolean);
|
|
2904
|
+
if (!billing) return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2905
|
+
"Fetching",
|
|
2906
|
+
glyphs().ellipsis
|
|
2907
|
+
] });
|
|
2908
|
+
if (billing.error) return /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error });
|
|
2909
|
+
const m = billing.metrics[0];
|
|
2910
|
+
if (!m) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" });
|
|
2911
|
+
return /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: metricValueText(m) });
|
|
2912
|
+
}
|
|
1712
2913
|
function Rule({ inner }) {
|
|
1713
|
-
return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children:
|
|
2914
|
+
return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: glyphs().rule.repeat(Math.max(0, inner)) });
|
|
1714
2915
|
}
|
|
1715
2916
|
function SummaryRow({ label, s }) {
|
|
1716
2917
|
const cachedPct = s.tokens > 0 ? Math.round(s.cacheRead / s.tokens * 100) : 0;
|
|
@@ -1755,10 +2956,16 @@ function LimitsBlock({ items, barW }) {
|
|
|
1755
2956
|
const billing = s?.billing;
|
|
1756
2957
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: showName && idx > 0 ? 1 : 0, children: [
|
|
1757
2958
|
showName && /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1758
|
-
/* @__PURE__ */
|
|
2959
|
+
/* @__PURE__ */ jsxs2(Text2, { color: account.color, children: [
|
|
2960
|
+
glyphs().dot,
|
|
2961
|
+
" "
|
|
2962
|
+
] }),
|
|
1759
2963
|
/* @__PURE__ */ jsx2(Text2, { bold: true, children: truncateName(account.name, 22) })
|
|
1760
2964
|
] }),
|
|
1761
|
-
!billing ? /* @__PURE__ */
|
|
2965
|
+
!billing ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2966
|
+
"Fetching",
|
|
2967
|
+
glyphs().ellipsis
|
|
2968
|
+
] }) : billing.error ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: billing.error }) : billing.metrics.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "No data" }) : billing.metrics.map((m, i) => /* @__PURE__ */ jsx2(MetricRow, { m, color: account.color, barW }, `${m.label}${i}`))
|
|
1762
2969
|
] }, account.id);
|
|
1763
2970
|
}) });
|
|
1764
2971
|
}
|
|
@@ -1807,17 +3014,41 @@ function aggregate(list) {
|
|
|
1807
3014
|
}
|
|
1808
3015
|
return z;
|
|
1809
3016
|
}
|
|
3017
|
+
function TotalsRow({ groups, stats, cols }) {
|
|
3018
|
+
const zero = () => ({ cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 });
|
|
3019
|
+
const t = zero(), w = zero(), m = zero();
|
|
3020
|
+
for (const g of groups) {
|
|
3021
|
+
if (!PROVIDERS[g.provider].hasUsage) continue;
|
|
3022
|
+
for (const a of g.accounts) {
|
|
3023
|
+
const d = stats.get(a.id)?.dashboard;
|
|
3024
|
+
if (!d) continue;
|
|
3025
|
+
t.cost += d.today.cost;
|
|
3026
|
+
t.tokens += d.today.tokens;
|
|
3027
|
+
w.cost += d.week.cost;
|
|
3028
|
+
w.tokens += d.week.tokens;
|
|
3029
|
+
m.cost += d.month.cost;
|
|
3030
|
+
m.tokens += d.month.tokens;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const inner = cols - 4;
|
|
3034
|
+
const dot = glyphs().middot;
|
|
3035
|
+
const full = `${glyphs().dotAll} Today ${currency(t.cost)} (${tokens(t.tokens)} tok) ${dot} Week ${currency(w.cost)} (${tokens(w.tokens)} tok) ${dot} Month ${currency(m.cost)} (${tokens(m.tokens)} tok)`;
|
|
3036
|
+
const noTok = `${glyphs().dotAll} Today ${currency(t.cost)} ${dot} Week ${currency(w.cost)} ${dot} Month ${currency(m.cost)}`;
|
|
3037
|
+
const tight = `${glyphs().dotAll} ${currency(t.cost)} ${dot} ${currency(w.cost)} ${dot} ${currency(m.cost)}`;
|
|
3038
|
+
const text = full.length <= inner ? full : noTok.length <= inner ? noTok : tight;
|
|
3039
|
+
return /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: text }) });
|
|
3040
|
+
}
|
|
1810
3041
|
|
|
1811
3042
|
// src/ui/table.tsx
|
|
1812
3043
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
1813
3044
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1814
3045
|
var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1815
|
-
function TableProviderBar({ providers, active, onSelect }) {
|
|
3046
|
+
function TableProviderBar({ providers, active: active2, onSelect }) {
|
|
1816
3047
|
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1817
3048
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "provider " }),
|
|
1818
3049
|
providers.map((p) => {
|
|
1819
3050
|
const meta = PROVIDERS[p];
|
|
1820
|
-
return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p ===
|
|
3051
|
+
return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onSelect(p), marginRight: 1, children: p === active2 ? /* @__PURE__ */ jsxs3(Text3, { bold: true, color: meta.color, inverse: true, children: [
|
|
1821
3052
|
" ",
|
|
1822
3053
|
meta.name,
|
|
1823
3054
|
" "
|
|
@@ -1842,15 +3073,23 @@ function ControlBar({ views, period, sort, search, searching, showPeriod }) {
|
|
|
1842
3073
|
] }),
|
|
1843
3074
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "sort " }),
|
|
1844
3075
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "magenta", children: sort }),
|
|
1845
|
-
/* @__PURE__ */
|
|
3076
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
3077
|
+
" o cycle ",
|
|
3078
|
+
glyphs().middot,
|
|
3079
|
+
" "
|
|
3080
|
+
] }),
|
|
1846
3081
|
searching ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1847
3082
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/" }),
|
|
1848
3083
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: search }),
|
|
1849
|
-
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children:
|
|
3084
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: glyphs().vbar })
|
|
1850
3085
|
] }) : search ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
1851
3086
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "filter " }),
|
|
1852
3087
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "green", children: search }),
|
|
1853
|
-
/* @__PURE__ */
|
|
3088
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
3089
|
+
" (/ edit ",
|
|
3090
|
+
glyphs().middot,
|
|
3091
|
+
" esc clear)"
|
|
3092
|
+
] })
|
|
1854
3093
|
] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "/ filter" })
|
|
1855
3094
|
] });
|
|
1856
3095
|
}
|
|
@@ -1887,14 +3126,14 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
|
|
|
1887
3126
|
W.total > 0 && /* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Total", W.total) }),
|
|
1888
3127
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Cost", W.cost) })
|
|
1889
3128
|
] }),
|
|
1890
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
3129
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
|
|
1891
3130
|
visible.map((r, vi) => {
|
|
1892
3131
|
const idx = scrollStart + vi;
|
|
1893
3132
|
const selected = idx === clampedCursor;
|
|
1894
3133
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1895
3134
|
/* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
|
|
1896
3135
|
/* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "cyan", children: [
|
|
1897
|
-
selected ?
|
|
3136
|
+
selected ? `${glyphs().caretR} ` : " ",
|
|
1898
3137
|
col(fmtLabel(r.label), W.label, "left")
|
|
1899
3138
|
] }),
|
|
1900
3139
|
/* @__PURE__ */ jsx3(Text3, { dimColor: !selected, children: col(r.models.join(", "), W.models, "left") }),
|
|
@@ -1908,7 +3147,7 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
|
|
|
1908
3147
|
idx === expanded && /* @__PURE__ */ jsx3(RowDetail, { row: r, indent: W.label + 2 })
|
|
1909
3148
|
] }, r.label);
|
|
1910
3149
|
}),
|
|
1911
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
3150
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(lineW + 2) }),
|
|
1912
3151
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1913
3152
|
/* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
|
|
1914
3153
|
" ",
|
|
@@ -1924,7 +3163,17 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
|
|
|
1924
3163
|
] }),
|
|
1925
3164
|
/* @__PURE__ */ jsx3(Box3, { height: 1 }),
|
|
1926
3165
|
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1927
|
-
|
|
3166
|
+
glyphs().arrowU,
|
|
3167
|
+
glyphs().arrowD,
|
|
3168
|
+
" navigate ",
|
|
3169
|
+
glyphs().middot,
|
|
3170
|
+
" Enter detail ",
|
|
3171
|
+
glyphs().middot,
|
|
3172
|
+
" o sort ",
|
|
3173
|
+
glyphs().middot,
|
|
3174
|
+
" g/G top/bottom ",
|
|
3175
|
+
glyphs().middot,
|
|
3176
|
+
" ",
|
|
1928
3177
|
clampedCursor + 1,
|
|
1929
3178
|
"/",
|
|
1930
3179
|
rows.length
|
|
@@ -1934,7 +3183,7 @@ function TokenTable({ rows, cursor, expanded, maxRows, cols, onRowClick }) {
|
|
|
1934
3183
|
function RowDetail({ row, indent }) {
|
|
1935
3184
|
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingLeft: indent, children: row.breakdown.map((m, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1936
3185
|
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1937
|
-
i === row.breakdown.length - 1 ?
|
|
3186
|
+
i === row.breakdown.length - 1 ? glyphs().treeEnd : glyphs().treeMid,
|
|
1938
3187
|
" "
|
|
1939
3188
|
] }),
|
|
1940
3189
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: col(m.name, 16, "left") }),
|
|
@@ -1975,14 +3224,14 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
|
|
|
1975
3224
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Amount", W.amount) }),
|
|
1976
3225
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: col("Share", W.share) })
|
|
1977
3226
|
] }),
|
|
1978
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
3227
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
|
|
1979
3228
|
visible.map((r, vi) => {
|
|
1980
3229
|
const idx = scrollStart + vi;
|
|
1981
3230
|
const selected = idx === clamped;
|
|
1982
3231
|
const share = total > 0 ? r.usd / total * 100 : 0;
|
|
1983
3232
|
return /* @__PURE__ */ jsx3(ClickableBox, { onClick: () => onRowClick(idx), children: /* @__PURE__ */ jsxs3(Text3, { inverse: selected, children: [
|
|
1984
3233
|
/* @__PURE__ */ jsxs3(Text3, { color: selected ? void 0 : "magenta", children: [
|
|
1985
|
-
selected ?
|
|
3234
|
+
selected ? `${glyphs().caretR} ` : " ",
|
|
1986
3235
|
col(r.name, W.model, "left")
|
|
1987
3236
|
] }),
|
|
1988
3237
|
/* @__PURE__ */ jsx3(Text3, { bold: true, color: selected ? void 0 : "yellow", children: col(currency(r.usd), W.cost) }),
|
|
@@ -1990,7 +3239,7 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
|
|
|
1990
3239
|
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: col(share.toFixed(1) + "%", W.share) })
|
|
1991
3240
|
] }) }, r.name);
|
|
1992
3241
|
}),
|
|
1993
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
3242
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: glyphs().rule.repeat(W.model + W.cost + W.amount + W.share + 2) }),
|
|
1994
3243
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1995
3244
|
/* @__PURE__ */ jsxs3(Text3, { bold: true, color: "greenBright", children: [
|
|
1996
3245
|
" ",
|
|
@@ -2002,7 +3251,11 @@ function CursorSpendTable({ rows, cursor, maxRows, onRowClick }) {
|
|
|
2002
3251
|
] }),
|
|
2003
3252
|
/* @__PURE__ */ jsx3(Box3, { height: 1 }),
|
|
2004
3253
|
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2005
|
-
"local spend by model (composerData)
|
|
3254
|
+
"local spend by model (composerData) ",
|
|
3255
|
+
glyphs().middot,
|
|
3256
|
+
" est. API-equivalent ",
|
|
3257
|
+
glyphs().middot,
|
|
3258
|
+
" ",
|
|
2006
3259
|
clamped + 1,
|
|
2007
3260
|
"/",
|
|
2008
3261
|
rows.length
|
|
@@ -2019,43 +3272,174 @@ function fmtLabel(label) {
|
|
|
2019
3272
|
// src/ui/onboarding.tsx
|
|
2020
3273
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2021
3274
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2022
|
-
function Onboarding({ items, cursor, onToggle, onConfirm }) {
|
|
3275
|
+
function Onboarding({ items, cursor, onToggle, onConfirm, heading = "Welcome to tokmon", subheading = "Pick the tools you want to track. You can change this anytime in settings." }) {
|
|
2023
3276
|
const anyEnabled = items.some((it) => it.enabled);
|
|
2024
3277
|
const startIdx = items.length;
|
|
2025
3278
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
|
|
2026
|
-
/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children:
|
|
2027
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children:
|
|
3279
|
+
/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "greenBright", children: heading }) }),
|
|
3280
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: subheading }),
|
|
2028
3281
|
/* @__PURE__ */ jsx4(Box4, { height: 1 }),
|
|
2029
3282
|
items.map((it, i) => {
|
|
2030
3283
|
const selected = cursor === i;
|
|
2031
|
-
const box = it.enabled ?
|
|
3284
|
+
const box = it.enabled ? `[${glyphs().check}]` : "[ ]";
|
|
2032
3285
|
return /* @__PURE__ */ jsxs4(ClickableBox, { onClick: () => onToggle(i), children: [
|
|
2033
3286
|
/* @__PURE__ */ jsxs4(Text4, { color: selected ? "green" : void 0, children: [
|
|
2034
|
-
selected ?
|
|
3287
|
+
selected ? glyphs().caretR : " ",
|
|
2035
3288
|
" "
|
|
2036
3289
|
] }),
|
|
2037
3290
|
/* @__PURE__ */ jsx4(Text4, { bold: it.enabled, color: it.enabled ? it.color : void 0, dimColor: !it.enabled, children: box }),
|
|
2038
|
-
/* @__PURE__ */
|
|
2039
|
-
|
|
3291
|
+
/* @__PURE__ */ jsxs4(Text4, { color: it.color, children: [
|
|
3292
|
+
" ",
|
|
3293
|
+
glyphs().dot,
|
|
3294
|
+
" "
|
|
3295
|
+
] }),
|
|
3296
|
+
/* @__PURE__ */ jsx4(Box4, { width: 13, flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { bold: selected, dimColor: !it.detected && !it.enabled, wrap: "truncate", children: it.name }) }),
|
|
2040
3297
|
it.detected ? /* @__PURE__ */ jsx4(Text4, { color: "green", dimColor: true, children: "installed" }) : it.enabled ? /* @__PURE__ */ jsx4(Text4, { color: "yellow", dimColor: true, children: "manual" }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "not found" })
|
|
2041
3298
|
] }, it.id);
|
|
2042
3299
|
}),
|
|
2043
3300
|
/* @__PURE__ */ jsx4(Box4, { height: 1 }),
|
|
2044
3301
|
/* @__PURE__ */ jsxs4(ClickableBox, { onClick: onConfirm, children: [
|
|
2045
3302
|
/* @__PURE__ */ jsxs4(Text4, { color: cursor === startIdx ? "green" : void 0, children: [
|
|
2046
|
-
cursor === startIdx ?
|
|
3303
|
+
cursor === startIdx ? glyphs().caretR : " ",
|
|
2047
3304
|
" "
|
|
2048
3305
|
] }),
|
|
2049
|
-
/* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ?
|
|
3306
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: anyEnabled ? "greenBright" : void 0, dimColor: !anyEnabled, children: anyEnabled ? `${glyphs().play} Start tokmon` : `${glyphs().play} Start (nothing selected)` })
|
|
2050
3307
|
] }),
|
|
2051
3308
|
/* @__PURE__ */ jsx4(Box4, { height: 1 }),
|
|
2052
|
-
/* @__PURE__ */
|
|
3309
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
3310
|
+
glyphs().arrowU,
|
|
3311
|
+
glyphs().arrowD,
|
|
3312
|
+
" move ",
|
|
3313
|
+
glyphs().middot,
|
|
3314
|
+
" space toggle ",
|
|
3315
|
+
glyphs().middot,
|
|
3316
|
+
" enter start ",
|
|
3317
|
+
glyphs().middot,
|
|
3318
|
+
" q quit"
|
|
3319
|
+
] })
|
|
2053
3320
|
] });
|
|
2054
3321
|
}
|
|
2055
3322
|
|
|
2056
|
-
// src/ui/
|
|
3323
|
+
// src/ui/loading.tsx
|
|
3324
|
+
import { useState as useState2, useEffect as useEffect2 } from "react";
|
|
2057
3325
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
2058
|
-
import {
|
|
3326
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3327
|
+
function accountReady(s, providerId) {
|
|
3328
|
+
if (!s) return false;
|
|
3329
|
+
const p = PROVIDERS[providerId];
|
|
3330
|
+
if (p.hasBilling && s.billing?.error) return true;
|
|
3331
|
+
if (p.hasUsage && !s.dashboard) return false;
|
|
3332
|
+
if (p.hasBilling && !s.billing) return false;
|
|
3333
|
+
return true;
|
|
3334
|
+
}
|
|
3335
|
+
function groupTodayCost(items) {
|
|
3336
|
+
return items.reduce((sum, s) => {
|
|
3337
|
+
const d = s?.dashboard;
|
|
3338
|
+
return sum + (d?.today.cost ?? 0);
|
|
3339
|
+
}, 0);
|
|
3340
|
+
}
|
|
3341
|
+
function headlineFor(group, items) {
|
|
3342
|
+
const meta = PROVIDERS[group.provider];
|
|
3343
|
+
if (meta.hasUsage) return `${currency(groupTodayCost(items))} today`;
|
|
3344
|
+
const billing = items.map((s) => s?.billing).find(Boolean);
|
|
3345
|
+
if (!billing) return "no data";
|
|
3346
|
+
if (billing.error) return billing.error;
|
|
3347
|
+
const m = billing.metrics[0];
|
|
3348
|
+
if (m) return metricValueText(m);
|
|
3349
|
+
return billing.plan ?? "no data";
|
|
3350
|
+
}
|
|
3351
|
+
var STAGGER_FRAMES = 2;
|
|
3352
|
+
function LoadingView({ groups, stats, cols, rows }) {
|
|
3353
|
+
const sp = glyphs().spinner;
|
|
3354
|
+
const [frame, setFrame] = useState2(0);
|
|
3355
|
+
useEffect2(() => {
|
|
3356
|
+
const id = setInterval(() => setFrame((f) => f + 1), 80);
|
|
3357
|
+
return () => clearInterval(id);
|
|
3358
|
+
}, []);
|
|
3359
|
+
const nameW = Math.min(13, groups.reduce((w, g) => Math.max(w, PROVIDERS[g.provider].name.length), 0));
|
|
3360
|
+
const readyCount = groups.filter((g) => g.accounts.every((a) => accountReady(stats.get(a.id), g.provider))).length;
|
|
3361
|
+
const maxRows = Math.max(1, rows - 7);
|
|
3362
|
+
const visible = groups.slice(0, maxRows);
|
|
3363
|
+
const hidden = groups.length - visible.length;
|
|
3364
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
3365
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, color: "greenBright", children: [
|
|
3366
|
+
glyphs().dotSel,
|
|
3367
|
+
" tokmon"
|
|
3368
|
+
] }),
|
|
3369
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
|
|
3370
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3371
|
+
"Detecting installed tools",
|
|
3372
|
+
glyphs().ellipsis
|
|
3373
|
+
] }),
|
|
3374
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3375
|
+
" ",
|
|
3376
|
+
groups.length,
|
|
3377
|
+
" found"
|
|
3378
|
+
] })
|
|
3379
|
+
] }),
|
|
3380
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
3381
|
+
visible.map((g, i) => {
|
|
3382
|
+
const meta = PROVIDERS[g.provider];
|
|
3383
|
+
const items = g.accounts.map((a) => stats.get(a.id));
|
|
3384
|
+
const ready = g.accounts.every((a) => accountReady(stats.get(a.id), g.provider));
|
|
3385
|
+
const errored = items.some((s) => !!s?.billing?.error);
|
|
3386
|
+
const revealed = frame >= i * STAGGER_FRAMES;
|
|
3387
|
+
const name = truncateName(meta.name, nameW);
|
|
3388
|
+
const namePad = " ".repeat(Math.max(0, nameW - name.length));
|
|
3389
|
+
if (!revealed) {
|
|
3390
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
3391
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3392
|
+
glyphs().dot,
|
|
3393
|
+
" "
|
|
3394
|
+
] }),
|
|
3395
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3396
|
+
name,
|
|
3397
|
+
namePad
|
|
3398
|
+
] })
|
|
3399
|
+
] }, g.provider);
|
|
3400
|
+
}
|
|
3401
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
3402
|
+
/* @__PURE__ */ jsxs5(Text5, { color: meta.color, children: [
|
|
3403
|
+
glyphs().dot,
|
|
3404
|
+
" "
|
|
3405
|
+
] }),
|
|
3406
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: meta.color, children: name }),
|
|
3407
|
+
/* @__PURE__ */ jsx5(Text5, { children: namePad }),
|
|
3408
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
3409
|
+
errored ? /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
|
|
3410
|
+
glyphs().warn,
|
|
3411
|
+
" "
|
|
3412
|
+
] }) : ready ? /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
|
|
3413
|
+
glyphs().check,
|
|
3414
|
+
" "
|
|
3415
|
+
] }) : /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
|
|
3416
|
+
sp[frame % sp.length],
|
|
3417
|
+
" "
|
|
3418
|
+
] }),
|
|
3419
|
+
errored ? /* @__PURE__ */ jsx5(Text5, { color: "red", children: headlineFor(g, items) }) : ready ? /* @__PURE__ */ jsx5(Text5, { children: headlineFor(g, items) }) : /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3420
|
+
"loading",
|
|
3421
|
+
glyphs().ellipsis
|
|
3422
|
+
] })
|
|
3423
|
+
] }, g.provider);
|
|
3424
|
+
}),
|
|
3425
|
+
hidden > 0 && /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3426
|
+
"+",
|
|
3427
|
+
hidden,
|
|
3428
|
+
" more"
|
|
3429
|
+
] })
|
|
3430
|
+
] }),
|
|
3431
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3432
|
+
"loading ",
|
|
3433
|
+
readyCount,
|
|
3434
|
+
" / ",
|
|
3435
|
+
groups.length
|
|
3436
|
+
] }) })
|
|
3437
|
+
] });
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// src/ui/settings.tsx
|
|
3441
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
3442
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2059
3443
|
var GENERAL_ROWS = 6;
|
|
2060
3444
|
var PROVIDER_ROWS_START = GENERAL_ROWS;
|
|
2061
3445
|
var ACCOUNT_ROWS_START = GENERAL_ROWS + PROVIDER_ORDER.length;
|
|
@@ -2080,134 +3464,191 @@ function SettingsView({
|
|
|
2080
3464
|
accountForm,
|
|
2081
3465
|
activeAccountId
|
|
2082
3466
|
}) {
|
|
2083
|
-
if (accountForm) return /* @__PURE__ */
|
|
3467
|
+
if (accountForm) return /* @__PURE__ */ jsx6(AccountFormView, { form: accountForm, accounts: config2.accounts });
|
|
2084
3468
|
const editingTz = tzEdit !== null;
|
|
2085
3469
|
const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
|
|
2086
|
-
return /* @__PURE__ */
|
|
2087
|
-
/* @__PURE__ */
|
|
2088
|
-
/* @__PURE__ */
|
|
2089
|
-
/* @__PURE__ */
|
|
2090
|
-
/* @__PURE__ */
|
|
2091
|
-
/* @__PURE__ */
|
|
2092
|
-
/* @__PURE__ */
|
|
2093
|
-
|
|
3470
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
3471
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Settings" }),
|
|
3472
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: configLocation() }),
|
|
3473
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3474
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "General" }),
|
|
3475
|
+
/* @__PURE__ */ jsxs6(Row, { cursor, idx: 0, label: "Refresh interval", children: [
|
|
3476
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3477
|
+
glyphs().caretL,
|
|
2094
3478
|
" "
|
|
2095
3479
|
] }),
|
|
2096
|
-
/* @__PURE__ */
|
|
3480
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
|
|
2097
3481
|
config2.interval,
|
|
2098
3482
|
"s"
|
|
2099
3483
|
] }),
|
|
2100
|
-
/* @__PURE__ */
|
|
3484
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2101
3485
|
" ",
|
|
2102
|
-
|
|
3486
|
+
glyphs().caretR
|
|
2103
3487
|
] })
|
|
2104
3488
|
] }),
|
|
2105
|
-
/* @__PURE__ */
|
|
2106
|
-
/* @__PURE__ */
|
|
2107
|
-
|
|
3489
|
+
/* @__PURE__ */ jsxs6(Row, { cursor, idx: 1, label: "Billing poll", children: [
|
|
3490
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3491
|
+
glyphs().caretL,
|
|
2108
3492
|
" "
|
|
2109
3493
|
] }),
|
|
2110
|
-
/* @__PURE__ */
|
|
3494
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "yellow", children: [
|
|
2111
3495
|
config2.billingInterval,
|
|
2112
3496
|
"m"
|
|
2113
3497
|
] }),
|
|
2114
|
-
/* @__PURE__ */
|
|
3498
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2115
3499
|
" ",
|
|
2116
|
-
|
|
3500
|
+
glyphs().caretR
|
|
2117
3501
|
] })
|
|
2118
3502
|
] }),
|
|
2119
|
-
/* @__PURE__ */
|
|
2120
|
-
/* @__PURE__ */
|
|
2121
|
-
/* @__PURE__ */
|
|
2122
|
-
/* @__PURE__ */
|
|
2123
|
-
/* @__PURE__ */
|
|
2124
|
-
/* @__PURE__ */
|
|
2125
|
-
] }) : /* @__PURE__ */
|
|
2126
|
-
cursor === 3 && tzError && /* @__PURE__ */
|
|
3503
|
+
/* @__PURE__ */ jsx6(Row, { cursor, idx: 2, label: "Clear screen", children: /* @__PURE__ */ jsx6(Text6, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" }) }),
|
|
3504
|
+
/* @__PURE__ */ jsx6(Row, { cursor, idx: 3, label: "Timezone", children: editingTz ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
3505
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[" }),
|
|
3506
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: tzEdit }),
|
|
3507
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "_" }),
|
|
3508
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "]" })
|
|
3509
|
+
] }) : /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: tzDisplay }) }),
|
|
3510
|
+
cursor === 3 && tzError && /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
2127
3511
|
" ",
|
|
2128
3512
|
tzError
|
|
2129
3513
|
] }),
|
|
2130
|
-
/* @__PURE__ */
|
|
2131
|
-
/* @__PURE__ */
|
|
2132
|
-
|
|
3514
|
+
/* @__PURE__ */ jsxs6(Row, { cursor, idx: 4, label: "Dashboard", children: [
|
|
3515
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3516
|
+
glyphs().caretL,
|
|
2133
3517
|
" "
|
|
2134
3518
|
] }),
|
|
2135
|
-
/* @__PURE__ */
|
|
2136
|
-
/* @__PURE__ */
|
|
3519
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.dashboardLayout === "grid" ? "grid (all)" : "single (cycle)" }),
|
|
3520
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2137
3521
|
" ",
|
|
2138
|
-
|
|
3522
|
+
glyphs().caretR
|
|
2139
3523
|
] })
|
|
2140
3524
|
] }),
|
|
2141
|
-
/* @__PURE__ */
|
|
2142
|
-
/* @__PURE__ */
|
|
2143
|
-
|
|
3525
|
+
/* @__PURE__ */ jsxs6(Row, { cursor, idx: 5, label: "Default focus", children: [
|
|
3526
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3527
|
+
glyphs().caretL,
|
|
2144
3528
|
" "
|
|
2145
3529
|
] }),
|
|
2146
|
-
/* @__PURE__ */
|
|
2147
|
-
/* @__PURE__ */
|
|
3530
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: config2.defaultFocus === "all" ? "All" : "Last account" }),
|
|
3531
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2148
3532
|
" ",
|
|
2149
|
-
|
|
3533
|
+
glyphs().caretR
|
|
2150
3534
|
] })
|
|
2151
3535
|
] }),
|
|
2152
|
-
/* @__PURE__ */
|
|
2153
|
-
/* @__PURE__ */
|
|
3536
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3537
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Providers" }),
|
|
2154
3538
|
PROVIDER_ORDER.map((pid, i) => {
|
|
2155
3539
|
const idx = PROVIDER_ROWS_START + i;
|
|
2156
3540
|
const selected = cursor === idx;
|
|
2157
3541
|
const enabled = !config2.disabledProviders.includes(pid);
|
|
2158
3542
|
const p = PROVIDERS[pid];
|
|
2159
|
-
return /* @__PURE__ */
|
|
2160
|
-
/* @__PURE__ */
|
|
2161
|
-
selected ?
|
|
3543
|
+
return /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3544
|
+
/* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
|
|
3545
|
+
selected ? glyphs().caretR : " ",
|
|
3546
|
+
" "
|
|
3547
|
+
] }),
|
|
3548
|
+
/* @__PURE__ */ jsx6(Text6, { bold: enabled, color: enabled ? p.color : void 0, dimColor: !enabled, children: enabled ? `[${glyphs().check}]` : "[ ]" }),
|
|
3549
|
+
/* @__PURE__ */ jsxs6(Text6, { color: p.color, children: [
|
|
3550
|
+
" ",
|
|
3551
|
+
glyphs().dot,
|
|
2162
3552
|
" "
|
|
2163
3553
|
] }),
|
|
2164
|
-
/* @__PURE__ */
|
|
2165
|
-
/* @__PURE__ */
|
|
2166
|
-
/* @__PURE__ */ jsx5(Box5, { width: 9, children: /* @__PURE__ */ jsx5(Text5, { bold: selected, children: p.name }) }),
|
|
2167
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: enabled ? "tracking" : "off" })
|
|
3554
|
+
/* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { bold: selected, children: p.name }) }),
|
|
3555
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: enabled ? "tracking" : "off" })
|
|
2168
3556
|
] }, pid);
|
|
2169
3557
|
}),
|
|
2170
|
-
/* @__PURE__ */
|
|
2171
|
-
/* @__PURE__ */
|
|
2172
|
-
config2.accounts.length === 0 && /* @__PURE__ */
|
|
3558
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3559
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Accounts" }),
|
|
3560
|
+
config2.accounts.length === 0 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3561
|
+
" none configured ",
|
|
3562
|
+
glyphs().emDash,
|
|
3563
|
+
" enabled providers track automatically"
|
|
3564
|
+
] }),
|
|
2173
3565
|
config2.accounts.map((acc, i) => {
|
|
2174
3566
|
const idx = ACCOUNT_ROWS_START + i;
|
|
2175
3567
|
const selected = cursor === idx;
|
|
2176
3568
|
const isActive = acc.id === activeAccountId;
|
|
2177
3569
|
const provider = PROVIDERS[acc.providerId];
|
|
2178
|
-
return /* @__PURE__ */
|
|
2179
|
-
/* @__PURE__ */
|
|
2180
|
-
selected ?
|
|
3570
|
+
return /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3571
|
+
/* @__PURE__ */ jsxs6(Text6, { color: selected ? "green" : void 0, children: [
|
|
3572
|
+
selected ? glyphs().caretR : " ",
|
|
2181
3573
|
" "
|
|
2182
3574
|
] }),
|
|
2183
|
-
/* @__PURE__ */
|
|
2184
|
-
isActive ?
|
|
3575
|
+
/* @__PURE__ */ jsxs6(Text6, { color: acc.color || provider.color, children: [
|
|
3576
|
+
isActive ? glyphs().dot : glyphs().radioOff,
|
|
2185
3577
|
" "
|
|
2186
3578
|
] }),
|
|
2187
|
-
/* @__PURE__ */
|
|
2188
|
-
/* @__PURE__ */
|
|
2189
|
-
/* @__PURE__ */
|
|
3579
|
+
/* @__PURE__ */ jsx6(Box6, { width: 16, children: /* @__PURE__ */ jsx6(Text6, { bold: true, children: truncateName(acc.name, 15) }) }),
|
|
3580
|
+
/* @__PURE__ */ jsx6(Box6, { width: 9, children: /* @__PURE__ */ jsx6(Text6, { color: provider.color, children: provider.name }) }),
|
|
3581
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: truncateName(acc.homeDir, 24) })
|
|
2190
3582
|
] }, acc.id);
|
|
2191
3583
|
}),
|
|
2192
|
-
/* @__PURE__ */
|
|
2193
|
-
/* @__PURE__ */
|
|
2194
|
-
cursor === ACCOUNT_ROWS_START + config2.accounts.length ?
|
|
3584
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3585
|
+
/* @__PURE__ */ jsxs6(Text6, { color: cursor === ACCOUNT_ROWS_START + config2.accounts.length ? "green" : void 0, children: [
|
|
3586
|
+
cursor === ACCOUNT_ROWS_START + config2.accounts.length ? glyphs().caretR : " ",
|
|
2195
3587
|
" "
|
|
2196
3588
|
] }),
|
|
2197
|
-
/* @__PURE__ */
|
|
2198
|
-
/* @__PURE__ */
|
|
3589
|
+
/* @__PURE__ */ jsx6(Text6, { color: "greenBright", children: "+ " }),
|
|
3590
|
+
/* @__PURE__ */ jsx6(Text6, { children: "Add account" })
|
|
2199
3591
|
] }),
|
|
2200
|
-
/* @__PURE__ */
|
|
2201
|
-
editingTz ? /* @__PURE__ */
|
|
3592
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3593
|
+
editingTz ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3594
|
+
"type IANA name (e.g. Europe/London) ",
|
|
3595
|
+
glyphs().middot,
|
|
3596
|
+
" empty = System ",
|
|
3597
|
+
glyphs().middot,
|
|
3598
|
+
" Enter save ",
|
|
3599
|
+
glyphs().middot,
|
|
3600
|
+
" Esc cancel"
|
|
3601
|
+
] }) : cursor >= PROVIDER_ROWS_START && cursor < ACCOUNT_ROWS_START ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3602
|
+
glyphs().arrowU,
|
|
3603
|
+
glyphs().arrowD,
|
|
3604
|
+
" select ",
|
|
3605
|
+
glyphs().middot,
|
|
3606
|
+
" space toggle provider ",
|
|
3607
|
+
glyphs().middot,
|
|
3608
|
+
" s/Esc close"
|
|
3609
|
+
] }) : cursor >= ACCOUNT_ROWS_START && cursor < ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3610
|
+
glyphs().arrowU,
|
|
3611
|
+
glyphs().arrowD,
|
|
3612
|
+
" select ",
|
|
3613
|
+
glyphs().middot,
|
|
3614
|
+
" ",
|
|
3615
|
+
glyphs().shift,
|
|
3616
|
+
glyphs().arrowU,
|
|
3617
|
+
glyphs().arrowD,
|
|
3618
|
+
" reorder ",
|
|
3619
|
+
glyphs().middot,
|
|
3620
|
+
" Enter edit ",
|
|
3621
|
+
glyphs().middot,
|
|
3622
|
+
" space activate ",
|
|
3623
|
+
glyphs().middot,
|
|
3624
|
+
" d delete ",
|
|
3625
|
+
glyphs().middot,
|
|
3626
|
+
" s/Esc close"
|
|
3627
|
+
] }) : cursor === ACCOUNT_ROWS_START + config2.accounts.length ? /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3628
|
+
glyphs().arrowU,
|
|
3629
|
+
glyphs().arrowD,
|
|
3630
|
+
" select ",
|
|
3631
|
+
glyphs().middot,
|
|
3632
|
+
" Enter add account ",
|
|
3633
|
+
glyphs().middot,
|
|
3634
|
+
" s/Esc close"
|
|
3635
|
+
] }) : /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3636
|
+
glyphs().arrowU,
|
|
3637
|
+
glyphs().arrowD,
|
|
3638
|
+
" select ",
|
|
3639
|
+
glyphs().arrowL,
|
|
3640
|
+
glyphs().arrowR,
|
|
3641
|
+
" adjust Enter edit s/Esc close"
|
|
3642
|
+
] })
|
|
2202
3643
|
] });
|
|
2203
3644
|
}
|
|
2204
3645
|
function Row({ cursor, idx, label, children }) {
|
|
2205
|
-
return /* @__PURE__ */
|
|
2206
|
-
/* @__PURE__ */
|
|
2207
|
-
cursor === idx ?
|
|
3646
|
+
return /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3647
|
+
/* @__PURE__ */ jsxs6(Text6, { color: cursor === idx ? "green" : void 0, children: [
|
|
3648
|
+
cursor === idx ? glyphs().caretR : " ",
|
|
2208
3649
|
" "
|
|
2209
3650
|
] }),
|
|
2210
|
-
/* @__PURE__ */
|
|
3651
|
+
/* @__PURE__ */ jsx6(Box6, { width: 20, children: /* @__PURE__ */ jsx6(Text6, { children: label }) }),
|
|
2211
3652
|
children
|
|
2212
3653
|
] });
|
|
2213
3654
|
}
|
|
@@ -2216,24 +3657,24 @@ function AccountFormView({ form, accounts }) {
|
|
|
2216
3657
|
const accent = form.color;
|
|
2217
3658
|
const stepIndex = { provider: 1, name: 2, homeDir: 3, color: 4 };
|
|
2218
3659
|
const step = stepIndex[form.field];
|
|
2219
|
-
return /* @__PURE__ */
|
|
2220
|
-
/* @__PURE__ */
|
|
2221
|
-
/* @__PURE__ */
|
|
2222
|
-
/* @__PURE__ */
|
|
3660
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
3661
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3662
|
+
/* @__PURE__ */ jsx6(Text6, { color: accent, bold: true, children: glyphs().vbar }),
|
|
3663
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
2223
3664
|
" ",
|
|
2224
3665
|
form.mode === "add" ? "NEW ACCOUNT" : "EDIT ACCOUNT"
|
|
2225
3666
|
] }),
|
|
2226
|
-
/* @__PURE__ */
|
|
3667
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2227
3668
|
" step ",
|
|
2228
3669
|
step,
|
|
2229
3670
|
" of 4"
|
|
2230
3671
|
] })
|
|
2231
3672
|
] }),
|
|
2232
|
-
/* @__PURE__ */
|
|
2233
|
-
/* @__PURE__ */
|
|
2234
|
-
/* @__PURE__ */
|
|
2235
|
-
/* @__PURE__ */
|
|
2236
|
-
/* @__PURE__ */
|
|
3673
|
+
/* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Stepper, { active: form.field, accent }) }),
|
|
3674
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", borderStyle: glyphs().border, borderColor: accent, paddingX: 2, paddingY: 1, children: [
|
|
3675
|
+
/* @__PURE__ */ jsx6(ProviderField, { value: form.providerId, focused: form.field === "provider" }),
|
|
3676
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3677
|
+
/* @__PURE__ */ jsx6(
|
|
2237
3678
|
FormField,
|
|
2238
3679
|
{
|
|
2239
3680
|
label: "Name",
|
|
@@ -2244,12 +3685,12 @@ function AccountFormView({ form, accounts }) {
|
|
|
2244
3685
|
placeholder: "e.g. Work, Personal"
|
|
2245
3686
|
}
|
|
2246
3687
|
),
|
|
2247
|
-
/* @__PURE__ */
|
|
2248
|
-
/* @__PURE__ */
|
|
3688
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3689
|
+
/* @__PURE__ */ jsx6(
|
|
2249
3690
|
FormField,
|
|
2250
3691
|
{
|
|
2251
3692
|
label: "Home directory",
|
|
2252
|
-
hint:
|
|
3693
|
+
hint: `path containing the tool's data dir ${glyphs().middot} ~ for default`,
|
|
2253
3694
|
value: form.homeDir,
|
|
2254
3695
|
focused: form.field === "homeDir",
|
|
2255
3696
|
accent,
|
|
@@ -2257,100 +3698,134 @@ function AccountFormView({ form, accounts }) {
|
|
|
2257
3698
|
mono: true
|
|
2258
3699
|
}
|
|
2259
3700
|
),
|
|
2260
|
-
/* @__PURE__ */
|
|
2261
|
-
/* @__PURE__ */
|
|
2262
|
-
/* @__PURE__ */
|
|
2263
|
-
/* @__PURE__ */
|
|
2264
|
-
/* @__PURE__ */
|
|
2265
|
-
|
|
2266
|
-
|
|
3701
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3702
|
+
/* @__PURE__ */ jsx6(ColorField, { value: form.color, focused: form.field === "color" }),
|
|
3703
|
+
/* @__PURE__ */ jsx6(Box6, { height: 1 }),
|
|
3704
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3705
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3706
|
+
"id ",
|
|
3707
|
+
glyphs().boxMark,
|
|
3708
|
+
" "
|
|
3709
|
+
] }),
|
|
3710
|
+
/* @__PURE__ */ jsx6(Text6, { bold: true, color: accent, children: previewId || "account" }),
|
|
3711
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3712
|
+
" ",
|
|
3713
|
+
glyphs().boxMark,
|
|
3714
|
+
" auto-generated from name"
|
|
3715
|
+
] })
|
|
2267
3716
|
] })
|
|
2268
3717
|
] }),
|
|
2269
|
-
form.error && /* @__PURE__ */
|
|
2270
|
-
|
|
3718
|
+
form.error && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
3719
|
+
glyphs().warn,
|
|
3720
|
+
" ",
|
|
2271
3721
|
form.error
|
|
2272
3722
|
] }) }),
|
|
2273
|
-
/* @__PURE__ */
|
|
2274
|
-
/* @__PURE__ */
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
3723
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
|
|
3724
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3725
|
+
"tab/",
|
|
3726
|
+
glyphs().arrowU,
|
|
3727
|
+
glyphs().arrowD,
|
|
3728
|
+
" "
|
|
3729
|
+
] }),
|
|
3730
|
+
/* @__PURE__ */ jsx6(Text6, { children: "switch field" }),
|
|
3731
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3732
|
+
" ",
|
|
3733
|
+
glyphs().middot,
|
|
3734
|
+
" "
|
|
2284
3735
|
] }),
|
|
2285
|
-
/* @__PURE__ */
|
|
2286
|
-
/* @__PURE__ */
|
|
3736
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "enter " }),
|
|
3737
|
+
/* @__PURE__ */ jsx6(Text6, { children: form.field === "color" ? "save" : "next" }),
|
|
3738
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3739
|
+
" ",
|
|
3740
|
+
glyphs().middot,
|
|
3741
|
+
" "
|
|
3742
|
+
] }),
|
|
3743
|
+
(form.field === "color" || form.field === "provider") && /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
3744
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3745
|
+
glyphs().arrowL,
|
|
3746
|
+
glyphs().arrowR,
|
|
3747
|
+
" "
|
|
3748
|
+
] }),
|
|
3749
|
+
/* @__PURE__ */ jsx6(Text6, { children: form.field === "provider" ? "pick provider" : "pick color" }),
|
|
3750
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3751
|
+
" ",
|
|
3752
|
+
glyphs().middot,
|
|
3753
|
+
" "
|
|
3754
|
+
] })
|
|
3755
|
+
] }),
|
|
3756
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "esc " }),
|
|
3757
|
+
/* @__PURE__ */ jsx6(Text6, { children: "cancel" })
|
|
2287
3758
|
] })
|
|
2288
3759
|
] });
|
|
2289
3760
|
}
|
|
2290
|
-
function Stepper({ active, accent }) {
|
|
3761
|
+
function Stepper({ active: active2, accent }) {
|
|
2291
3762
|
const steps = [
|
|
2292
3763
|
{ id: "provider", label: "Provider" },
|
|
2293
3764
|
{ id: "name", label: "Name" },
|
|
2294
3765
|
{ id: "homeDir", label: "Home" },
|
|
2295
3766
|
{ id: "color", label: "Color" }
|
|
2296
3767
|
];
|
|
2297
|
-
const activeIdx = steps.findIndex((s) => s.id ===
|
|
2298
|
-
return /* @__PURE__ */
|
|
3768
|
+
const activeIdx = steps.findIndex((s) => s.id === active2);
|
|
3769
|
+
return /* @__PURE__ */ jsx6(Box6, { children: steps.map((s, i) => {
|
|
2299
3770
|
const done = i < activeIdx;
|
|
2300
3771
|
const cur = i === activeIdx;
|
|
2301
|
-
const dot = done ?
|
|
2302
|
-
return /* @__PURE__ */
|
|
2303
|
-
/* @__PURE__ */
|
|
3772
|
+
const dot = done ? glyphs().dot : cur ? glyphs().dotSel : glyphs().radioOff;
|
|
3773
|
+
return /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3774
|
+
/* @__PURE__ */ jsxs6(Text6, { color: cur || done ? accent : void 0, dimColor: !cur && !done, children: [
|
|
2304
3775
|
dot,
|
|
2305
3776
|
" "
|
|
2306
3777
|
] }),
|
|
2307
|
-
/* @__PURE__ */
|
|
2308
|
-
i < steps.length - 1 && /* @__PURE__ */
|
|
3778
|
+
/* @__PURE__ */ jsx6(Text6, { bold: cur, color: cur ? accent : void 0, dimColor: !cur, children: s.label }),
|
|
3779
|
+
i < steps.length - 1 && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
3780
|
+
" ",
|
|
3781
|
+
glyphs().rule,
|
|
3782
|
+
" "
|
|
3783
|
+
] })
|
|
2309
3784
|
] }, s.id);
|
|
2310
3785
|
}) });
|
|
2311
3786
|
}
|
|
2312
3787
|
function ProviderField({ value, focused }) {
|
|
2313
|
-
return /* @__PURE__ */
|
|
2314
|
-
/* @__PURE__ */
|
|
2315
|
-
focused ?
|
|
3788
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
3789
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? PROVIDERS[value].color : void 0, bold: focused, dimColor: !focused, children: [
|
|
3790
|
+
focused ? glyphs().caretR : " ",
|
|
2316
3791
|
" Provider"
|
|
2317
3792
|
] }) }),
|
|
2318
|
-
/* @__PURE__ */
|
|
2319
|
-
/* @__PURE__ */
|
|
3793
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3794
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
2320
3795
|
" ",
|
|
2321
|
-
focused ?
|
|
3796
|
+
focused ? glyphs().vbar : " ",
|
|
2322
3797
|
" "
|
|
2323
3798
|
] }),
|
|
2324
3799
|
PROVIDER_ORDER.map((pid) => {
|
|
2325
3800
|
const selected = pid === value;
|
|
2326
3801
|
const p = PROVIDERS[pid];
|
|
2327
|
-
return /* @__PURE__ */
|
|
3802
|
+
return /* @__PURE__ */ jsx6(Box6, { marginRight: 2, children: selected ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: p.color, children: [
|
|
2328
3803
|
"[",
|
|
2329
3804
|
p.name,
|
|
2330
3805
|
"]"
|
|
2331
|
-
] }) : /* @__PURE__ */
|
|
3806
|
+
] }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: p.name }) }, pid);
|
|
2332
3807
|
})
|
|
2333
3808
|
] }),
|
|
2334
|
-
/* @__PURE__ */
|
|
3809
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " which tool this account tracks" }) })
|
|
2335
3810
|
] });
|
|
2336
3811
|
}
|
|
2337
3812
|
function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
|
|
2338
3813
|
const isPlaceholder = value === "";
|
|
2339
3814
|
const display = isPlaceholder ? placeholder : value;
|
|
2340
|
-
return /* @__PURE__ */
|
|
2341
|
-
/* @__PURE__ */
|
|
2342
|
-
focused ?
|
|
3815
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
3816
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, bold: focused, dimColor: !focused, children: [
|
|
3817
|
+
focused ? glyphs().caretR : " ",
|
|
2343
3818
|
" ",
|
|
2344
3819
|
label
|
|
2345
3820
|
] }) }),
|
|
2346
|
-
/* @__PURE__ */
|
|
2347
|
-
/* @__PURE__ */
|
|
3821
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3822
|
+
/* @__PURE__ */ jsxs6(Text6, { color: focused ? accent : void 0, children: [
|
|
2348
3823
|
" ",
|
|
2349
|
-
focused ?
|
|
3824
|
+
focused ? glyphs().vbar : " ",
|
|
2350
3825
|
" "
|
|
2351
3826
|
] }),
|
|
2352
|
-
/* @__PURE__ */
|
|
2353
|
-
|
|
3827
|
+
/* @__PURE__ */ jsx6(
|
|
3828
|
+
Text6,
|
|
2354
3829
|
{
|
|
2355
3830
|
bold: focused && !isPlaceholder,
|
|
2356
3831
|
color: focused && !isPlaceholder ? accent : void 0,
|
|
@@ -2359,39 +3834,59 @@ function FormField({ label, hint, value, focused, accent, placeholder, mono }) {
|
|
|
2359
3834
|
children: display
|
|
2360
3835
|
}
|
|
2361
3836
|
),
|
|
2362
|
-
focused && /* @__PURE__ */
|
|
3837
|
+
focused && /* @__PURE__ */ jsx6(Text6, { color: accent, children: glyphs().vbar })
|
|
2363
3838
|
] }),
|
|
2364
|
-
/* @__PURE__ */
|
|
3839
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2365
3840
|
" ",
|
|
2366
3841
|
hint
|
|
2367
3842
|
] }) })
|
|
2368
3843
|
] });
|
|
2369
3844
|
}
|
|
2370
3845
|
function ColorField({ value, focused }) {
|
|
2371
|
-
return /* @__PURE__ */
|
|
2372
|
-
/* @__PURE__ */
|
|
2373
|
-
focused ?
|
|
3846
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
3847
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsxs6(Text6, { color: focused ? value : void 0, bold: focused, dimColor: !focused, children: [
|
|
3848
|
+
focused ? glyphs().caretR : " ",
|
|
2374
3849
|
" Accent color"
|
|
2375
3850
|
] }) }),
|
|
2376
|
-
/* @__PURE__ */
|
|
2377
|
-
/* @__PURE__ */
|
|
3851
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
3852
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
2378
3853
|
" ",
|
|
2379
|
-
focused ?
|
|
3854
|
+
focused ? glyphs().vbar : " ",
|
|
2380
3855
|
" "
|
|
2381
3856
|
] }),
|
|
2382
|
-
COLOR_PALETTE.map((c) => /* @__PURE__ */
|
|
3857
|
+
COLOR_PALETTE.map((c) => /* @__PURE__ */ jsx6(Box6, { marginRight: 1, children: c === value ? /* @__PURE__ */ jsxs6(Text6, { bold: true, color: c, children: [
|
|
3858
|
+
"[",
|
|
3859
|
+
glyphs().dot,
|
|
3860
|
+
"]"
|
|
3861
|
+
] }) : /* @__PURE__ */ jsxs6(Text6, { color: c, dimColor: !focused, children: [
|
|
3862
|
+
" ",
|
|
3863
|
+
glyphs().dot
|
|
3864
|
+
] }) }, c))
|
|
2383
3865
|
] }),
|
|
2384
|
-
/* @__PURE__ */
|
|
3866
|
+
/* @__PURE__ */ jsx6(Box6, { children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " shows on dashboard, account strip, borders" }) })
|
|
2385
3867
|
] });
|
|
2386
3868
|
}
|
|
2387
3869
|
|
|
2388
3870
|
// src/app.tsx
|
|
2389
|
-
import { Fragment as Fragment4, jsx as
|
|
3871
|
+
import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2390
3872
|
var TABS = ["Dashboard", "Table"];
|
|
2391
3873
|
var VIEWS = ["Daily", "Weekly", "Monthly"];
|
|
2392
|
-
var SORTS = [
|
|
2393
|
-
|
|
3874
|
+
var SORTS = [
|
|
3875
|
+
{ label: "date", dir: "up" },
|
|
3876
|
+
{ label: "date", dir: "down" },
|
|
3877
|
+
{ label: "cost", dir: "up" },
|
|
3878
|
+
{ label: "cost", dir: "down" }
|
|
3879
|
+
];
|
|
3880
|
+
var CURSOR_SORTS = [
|
|
3881
|
+
{ label: "cost", dir: "down" },
|
|
3882
|
+
{ label: "amount", dir: "down" },
|
|
3883
|
+
{ label: "model", dir: null }
|
|
3884
|
+
];
|
|
2394
3885
|
var IS_TTY = process.stdin.isTTY === true;
|
|
3886
|
+
var DEBOUNCE_MS = 300;
|
|
3887
|
+
var LOADER_GRACE_MS = 600;
|
|
3888
|
+
var LOADER_MAX_MS = 8e3;
|
|
3889
|
+
var LOADER_MIN_VISIBLE_MS = 700;
|
|
2395
3890
|
var DEFAULT_CONFIG = {
|
|
2396
3891
|
interval: 2,
|
|
2397
3892
|
billingInterval: 5,
|
|
@@ -2402,33 +3897,46 @@ var DEFAULT_CONFIG = {
|
|
|
2402
3897
|
disabledProviders: [],
|
|
2403
3898
|
onboarded: false,
|
|
2404
3899
|
dashboardLayout: "grid",
|
|
2405
|
-
defaultFocus: "all"
|
|
3900
|
+
defaultFocus: "all",
|
|
3901
|
+
ascii: "auto",
|
|
3902
|
+
knownProviders: []
|
|
2406
3903
|
};
|
|
2407
|
-
function
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
const [
|
|
2414
|
-
const [
|
|
2415
|
-
const [
|
|
2416
|
-
const [
|
|
2417
|
-
const [
|
|
2418
|
-
const [
|
|
2419
|
-
const [
|
|
2420
|
-
const [
|
|
2421
|
-
const [
|
|
2422
|
-
const [
|
|
2423
|
-
const [
|
|
2424
|
-
const [
|
|
2425
|
-
const [
|
|
2426
|
-
const [
|
|
2427
|
-
const [
|
|
2428
|
-
const [
|
|
2429
|
-
const [
|
|
2430
|
-
const [
|
|
2431
|
-
const [
|
|
3904
|
+
function applyStartup(c, cliInterval) {
|
|
3905
|
+
if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
|
|
3906
|
+
if (c.defaultFocus === "all") c = { ...c, activeAccountId: null };
|
|
3907
|
+
return c;
|
|
3908
|
+
}
|
|
3909
|
+
function App({ interval: cliInterval, initialConfig }) {
|
|
3910
|
+
const [config2, setConfig] = useState3(() => initialConfig ? applyStartup(initialConfig, cliInterval) : null);
|
|
3911
|
+
const [detected, setDetected] = useState3([]);
|
|
3912
|
+
const [stats, setStats] = useState3(/* @__PURE__ */ new Map());
|
|
3913
|
+
const [peak, setPeak] = useState3(null);
|
|
3914
|
+
const [table, setTable] = useState3(null);
|
|
3915
|
+
const [tableLoading, setTableLoading] = useState3(false);
|
|
3916
|
+
const [error, setError] = useState3(null);
|
|
3917
|
+
const [updated, setUpdated] = useState3(/* @__PURE__ */ new Date());
|
|
3918
|
+
const [tab, setTab] = useState3(0);
|
|
3919
|
+
const [view, setView] = useState3(0);
|
|
3920
|
+
const [cursor, setCursor] = useState3(0);
|
|
3921
|
+
const [expanded, setExpanded] = useState3(-1);
|
|
3922
|
+
const [sort, setSort] = useState3(1);
|
|
3923
|
+
const [tableProvider, setTableProvider] = useState3(null);
|
|
3924
|
+
const [search, setSearch] = useState3("");
|
|
3925
|
+
const [searchMode, setSearchMode] = useState3(false);
|
|
3926
|
+
const [cursorRows, setCursorRows] = useState3(null);
|
|
3927
|
+
const [showSettings, setShowSettings] = useState3(false);
|
|
3928
|
+
const [settingsCursor, setSettingsCursor] = useState3(0);
|
|
3929
|
+
const [tzEdit, setTzEdit] = useState3(null);
|
|
3930
|
+
const [tzError, setTzError] = useState3(null);
|
|
3931
|
+
const [accountForm, setAccountForm] = useState3(null);
|
|
3932
|
+
const [onboardSel, setOnboardSel] = useState3(null);
|
|
3933
|
+
const [onboardCursor, setOnboardCursor] = useState3(0);
|
|
3934
|
+
const [dashPage, setDashPage] = useState3(0);
|
|
3935
|
+
const [debouncePassed, setDebouncePassed] = useState3(false);
|
|
3936
|
+
const [graceHold, setGraceHold] = useState3(false);
|
|
3937
|
+
const [loaderShownAt, setLoaderShownAt] = useState3(null);
|
|
3938
|
+
const loaderDone = useRef2(false);
|
|
3939
|
+
const prevShowPicker = useRef2(false);
|
|
2432
3940
|
const { stdout } = useStdout();
|
|
2433
3941
|
const { exit } = useApp();
|
|
2434
3942
|
const rows = stdout?.rows ?? 24;
|
|
@@ -2442,6 +3950,8 @@ function App({ interval: cliInterval }) {
|
|
|
2442
3950
|
const accountsRef = useRef2([]);
|
|
2443
3951
|
accountsRef.current = accounts;
|
|
2444
3952
|
const rowCountRef = useRef2(0);
|
|
3953
|
+
const dashPageCountRef = useRef2(1);
|
|
3954
|
+
const seededRef = useRef2(false);
|
|
2445
3955
|
const accountsKey = accounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join("|");
|
|
2446
3956
|
const slots = accounts.length > 1 ? [{ id: null, name: "All", color: "whiteBright" }, ...accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }))] : accounts.map((a) => ({ id: a.id, name: a.name, color: a.color }));
|
|
2447
3957
|
const activeSlotIdx = (() => {
|
|
@@ -2452,31 +3962,117 @@ function App({ interval: cliInterval }) {
|
|
|
2452
3962
|
const focusId = slots[activeSlotIdx]?.id ?? null;
|
|
2453
3963
|
const visibleAccounts = focusId === null ? accounts : accounts.filter((a) => a.id === focusId);
|
|
2454
3964
|
const groups = accountsByProvider(visibleAccounts);
|
|
3965
|
+
const TOO_SMALL = cols < 40 || rows < 12;
|
|
3966
|
+
const allGroups = accountsByProvider(accounts);
|
|
3967
|
+
const allReady = accounts.length > 0 && accounts.every((a) => accountReady(stats.get(a.id), a.providerId));
|
|
3968
|
+
const hasStrip = slots.length > 1;
|
|
3969
|
+
const stripChipW = (s) => 2 + 2 + truncateName(s.name, 16).length + 2;
|
|
3970
|
+
const stripChars = slots.reduce((sum, s) => sum + stripChipW(s), 0);
|
|
3971
|
+
const stripLines = hasStrip ? Math.max(1, Math.ceil(stripChars / Math.max(
|
|
3972
|
+
1,
|
|
3973
|
+
cols - 4 - 7
|
|
3974
|
+
/*"focus "*/
|
|
3975
|
+
))) : 0;
|
|
3976
|
+
const headerRows = cols < 70 ? 2 : 1;
|
|
3977
|
+
const CHROME = 2 + headerRows + 3 + (hasStrip ? 1 + stripLines : 0) + 2 + 2;
|
|
3978
|
+
const gridBudget = Math.max(1, rows - CHROME);
|
|
3979
|
+
const dashLayout = chooseLayout(
|
|
3980
|
+
Math.max(56, cols - 4),
|
|
3981
|
+
gridBudget,
|
|
3982
|
+
groups.length,
|
|
3983
|
+
focusId !== null || cfg.dashboardLayout === "single",
|
|
3984
|
+
cols
|
|
3985
|
+
);
|
|
3986
|
+
const dashPageCount = dashLayout.pageCount;
|
|
3987
|
+
const dashPaginated = dashPageCount > 1;
|
|
3988
|
+
dashPageCountRef.current = dashPageCount;
|
|
2455
3989
|
const tableProvs = accountsByProvider(accounts).map((g) => g.provider);
|
|
2456
3990
|
const effTableProvider = tableProvider && tableProvs.includes(tableProvider) ? tableProvider : tableProvs[0] ?? null;
|
|
2457
3991
|
const tableIsCursor = !!effTableProvider && !PROVIDERS[effTableProvider].hasUsage;
|
|
2458
3992
|
const tableAccounts = effTableProvider ? accounts.filter((a) => a.providerId === effTableProvider) : [];
|
|
2459
3993
|
const SORTS_FOR = tableIsCursor ? CURSOR_SORTS : SORTS;
|
|
2460
3994
|
const needsOnboarding = configReady && !cfg.onboarded;
|
|
3995
|
+
const newProviders = configReady && cfg.onboarded ? PROVIDER_ORDER.filter((p) => !cfg.knownProviders.includes(p) && detected.includes(p)) : [];
|
|
3996
|
+
const showPicker = needsOnboarding || newProviders.length > 0;
|
|
3997
|
+
const minVisibleHold = loaderShownAt !== null && Date.now() - loaderShownAt < LOADER_MIN_VISIBLE_MS;
|
|
3998
|
+
const showLoader = configReady && !showPicker && !showSettings && !TOO_SMALL && accounts.length > 0 && (!allReady || graceHold || minVisibleHold) && (debouncePassed || loaderShownAt !== null) && !loaderDone.current;
|
|
3999
|
+
const pickerProviders = needsOnboarding ? PROVIDER_ORDER : newProviders;
|
|
2461
4000
|
const onboardEnabled = onboardSel ?? detected;
|
|
2462
|
-
const onboardItems =
|
|
4001
|
+
const onboardItems = pickerProviders.map((pid) => ({
|
|
2463
4002
|
id: pid,
|
|
2464
4003
|
name: PROVIDERS[pid].name,
|
|
2465
4004
|
color: PROVIDERS[pid].color,
|
|
2466
4005
|
detected: detected.includes(pid),
|
|
2467
4006
|
enabled: onboardEnabled.includes(pid)
|
|
2468
4007
|
}));
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
4008
|
+
useEffect3(() => {
|
|
4009
|
+
const wasPicker = prevShowPicker.current;
|
|
4010
|
+
prevShowPicker.current = showPicker;
|
|
4011
|
+
if (wasPicker && !showPicker) {
|
|
4012
|
+
loaderDone.current = false;
|
|
4013
|
+
setDebouncePassed(false);
|
|
4014
|
+
setGraceHold(false);
|
|
4015
|
+
setLoaderShownAt(null);
|
|
4016
|
+
}
|
|
4017
|
+
}, [showPicker]);
|
|
4018
|
+
useEffect3(() => {
|
|
4019
|
+
if (showLoader && loaderShownAt === null) setLoaderShownAt(Date.now());
|
|
4020
|
+
}, [showLoader, loaderShownAt]);
|
|
4021
|
+
useEffect3(() => {
|
|
4022
|
+
if (!initialConfig) loadConfig().then((c) => setConfig(applyStartup(c, cliInterval)));
|
|
2475
4023
|
detectProviders().then(setDetected);
|
|
2476
4024
|
}, []);
|
|
2477
|
-
|
|
2478
|
-
if (!configReady) return;
|
|
2479
|
-
|
|
4025
|
+
useEffect3(() => {
|
|
4026
|
+
if (seededRef.current || !configReady || showPicker || accounts.length === 0) return;
|
|
4027
|
+
seededRef.current = true;
|
|
4028
|
+
loadSnapshot().then((snap) => {
|
|
4029
|
+
setStats((prev) => {
|
|
4030
|
+
if (prev.size > 0) return prev;
|
|
4031
|
+
const next = new Map(prev);
|
|
4032
|
+
for (const acc of accountsRef.current) {
|
|
4033
|
+
const s = snap[acc.id];
|
|
4034
|
+
if (s && (s.dashboard || s.billing)) next.set(acc.id, { account: acc, dashboard: s.dashboard ?? null, billing: s.billing ?? null });
|
|
4035
|
+
}
|
|
4036
|
+
return next;
|
|
4037
|
+
});
|
|
4038
|
+
});
|
|
4039
|
+
}, [configReady, showPicker, accountsKey]);
|
|
4040
|
+
useEffect3(() => {
|
|
4041
|
+
if (stats.size === 0) return;
|
|
4042
|
+
const t = setTimeout(() => saveSnapshot(stats), 500);
|
|
4043
|
+
return () => clearTimeout(t);
|
|
4044
|
+
}, [stats]);
|
|
4045
|
+
useEffect3(() => {
|
|
4046
|
+
if (!configReady || showPicker || accounts.length === 0) return;
|
|
4047
|
+
if (allReady || loaderDone.current) return;
|
|
4048
|
+
const debounce = setTimeout(() => setDebouncePassed(true), DEBOUNCE_MS);
|
|
4049
|
+
const deadline = setTimeout(() => {
|
|
4050
|
+
loaderDone.current = true;
|
|
4051
|
+
setDebouncePassed(false);
|
|
4052
|
+
}, LOADER_MAX_MS);
|
|
4053
|
+
return () => {
|
|
4054
|
+
clearTimeout(debounce);
|
|
4055
|
+
clearTimeout(deadline);
|
|
4056
|
+
};
|
|
4057
|
+
}, [configReady, showPicker, accountsKey]);
|
|
4058
|
+
useEffect3(() => {
|
|
4059
|
+
if (!allReady || loaderDone.current) return;
|
|
4060
|
+
if (loaderShownAt === null) {
|
|
4061
|
+
loaderDone.current = true;
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
4064
|
+
setGraceHold(true);
|
|
4065
|
+
const minRemaining = Math.max(0, LOADER_MIN_VISIBLE_MS - (Date.now() - loaderShownAt));
|
|
4066
|
+
const hold = Math.max(LOADER_GRACE_MS, minRemaining);
|
|
4067
|
+
const t = setTimeout(() => {
|
|
4068
|
+
loaderDone.current = true;
|
|
4069
|
+
setGraceHold(false);
|
|
4070
|
+
}, hold);
|
|
4071
|
+
return () => clearTimeout(t);
|
|
4072
|
+
}, [allReady]);
|
|
4073
|
+
useEffect3(() => {
|
|
4074
|
+
if (!configReady || showPicker) return;
|
|
4075
|
+
let active2 = true;
|
|
2480
4076
|
let timer;
|
|
2481
4077
|
const load = async () => {
|
|
2482
4078
|
try {
|
|
@@ -2485,27 +4081,27 @@ function App({ interval: cliInterval }) {
|
|
|
2485
4081
|
if (!provider.hasUsage || !provider.fetchSummary) return;
|
|
2486
4082
|
try {
|
|
2487
4083
|
const dashboard = await provider.fetchSummary(acc, tz);
|
|
2488
|
-
if (
|
|
4084
|
+
if (active2) setStats((prev) => upsert(prev, acc, { dashboard }));
|
|
2489
4085
|
} catch {
|
|
2490
4086
|
}
|
|
2491
4087
|
}));
|
|
2492
|
-
if (
|
|
4088
|
+
if (active2) {
|
|
2493
4089
|
setError(null);
|
|
2494
4090
|
setUpdated(/* @__PURE__ */ new Date());
|
|
2495
4091
|
}
|
|
2496
4092
|
} finally {
|
|
2497
|
-
if (
|
|
4093
|
+
if (active2) timer = setTimeout(load, interval2);
|
|
2498
4094
|
}
|
|
2499
4095
|
};
|
|
2500
4096
|
load();
|
|
2501
4097
|
return () => {
|
|
2502
|
-
|
|
4098
|
+
active2 = false;
|
|
2503
4099
|
clearTimeout(timer);
|
|
2504
4100
|
};
|
|
2505
|
-
}, [interval2, tz, configReady, accountsKey]);
|
|
2506
|
-
|
|
2507
|
-
if (!configReady) return;
|
|
2508
|
-
let
|
|
4101
|
+
}, [interval2, tz, configReady, showPicker, accountsKey]);
|
|
4102
|
+
useEffect3(() => {
|
|
4103
|
+
if (!configReady || showPicker) return;
|
|
4104
|
+
let active2 = true;
|
|
2509
4105
|
let timer;
|
|
2510
4106
|
const load = async () => {
|
|
2511
4107
|
try {
|
|
@@ -2515,42 +4111,42 @@ function App({ interval: cliInterval }) {
|
|
|
2515
4111
|
if (!provider.hasBilling || !provider.fetchBilling) return;
|
|
2516
4112
|
try {
|
|
2517
4113
|
const billing = await provider.fetchBilling(acc);
|
|
2518
|
-
if (
|
|
4114
|
+
if (active2) setStats((prev) => upsert(prev, acc, { billing }));
|
|
2519
4115
|
} catch {
|
|
2520
4116
|
}
|
|
2521
4117
|
}));
|
|
2522
4118
|
const p = await peakP;
|
|
2523
|
-
if (
|
|
4119
|
+
if (active2 && p) setPeak(p);
|
|
2524
4120
|
} finally {
|
|
2525
|
-
if (
|
|
4121
|
+
if (active2) timer = setTimeout(load, billingMs);
|
|
2526
4122
|
}
|
|
2527
4123
|
};
|
|
2528
4124
|
load();
|
|
2529
4125
|
return () => {
|
|
2530
|
-
|
|
4126
|
+
active2 = false;
|
|
2531
4127
|
clearTimeout(timer);
|
|
2532
4128
|
};
|
|
2533
|
-
}, [billingMs, configReady, accountsKey]);
|
|
4129
|
+
}, [billingMs, configReady, showPicker, accountsKey]);
|
|
2534
4130
|
const tableKey = `${effTableProvider}|${tableAccounts.map((a) => `${a.id}:${a.homeDir ?? ""}`).join(",")}|${tz}`;
|
|
2535
|
-
|
|
4131
|
+
useEffect3(() => {
|
|
2536
4132
|
setTable(null);
|
|
2537
4133
|
setCursorRows(null);
|
|
2538
4134
|
setCursor(0);
|
|
2539
4135
|
setExpanded(-1);
|
|
2540
4136
|
setSort(tableIsCursor ? 0 : 1);
|
|
2541
4137
|
}, [tableKey]);
|
|
2542
|
-
|
|
4138
|
+
useEffect3(() => {
|
|
2543
4139
|
if (tab !== 1 || !effTableProvider) return;
|
|
2544
|
-
let
|
|
4140
|
+
let active2 = true;
|
|
2545
4141
|
let timer;
|
|
2546
4142
|
const fetchOnce = async () => {
|
|
2547
4143
|
try {
|
|
2548
4144
|
if (tableIsCursor) {
|
|
2549
4145
|
const s = await cursorModelSpend(tableAccounts[0]?.homeDir);
|
|
2550
|
-
if (
|
|
4146
|
+
if (active2) setCursorRows(s?.models ?? []);
|
|
2551
4147
|
} else {
|
|
2552
4148
|
const r = await fetchScopeTable(tableAccounts, tz);
|
|
2553
|
-
if (
|
|
4149
|
+
if (active2) setTable(r);
|
|
2554
4150
|
}
|
|
2555
4151
|
} catch {
|
|
2556
4152
|
}
|
|
@@ -2558,35 +4154,43 @@ function App({ interval: cliInterval }) {
|
|
|
2558
4154
|
const run = async () => {
|
|
2559
4155
|
setTableLoading(true);
|
|
2560
4156
|
await fetchOnce();
|
|
2561
|
-
if (!
|
|
4157
|
+
if (!active2) return;
|
|
2562
4158
|
setTableLoading(false);
|
|
2563
4159
|
const loop = async () => {
|
|
2564
4160
|
await fetchOnce();
|
|
2565
|
-
if (
|
|
4161
|
+
if (active2) timer = setTimeout(loop, Math.max(interval2, 1e4));
|
|
2566
4162
|
};
|
|
2567
4163
|
timer = setTimeout(loop, Math.max(interval2, 1e4));
|
|
2568
4164
|
};
|
|
2569
4165
|
run();
|
|
2570
4166
|
return () => {
|
|
2571
|
-
|
|
4167
|
+
active2 = false;
|
|
2572
4168
|
clearTimeout(timer);
|
|
2573
4169
|
};
|
|
2574
4170
|
}, [tab, tableKey, interval2]);
|
|
2575
|
-
|
|
4171
|
+
useEffect3(() => {
|
|
2576
4172
|
setCursor(0);
|
|
2577
4173
|
setExpanded(-1);
|
|
2578
4174
|
}, [search]);
|
|
4175
|
+
useEffect3(() => {
|
|
4176
|
+
setDashPage((p) => Math.min(p, dashPageCount - 1));
|
|
4177
|
+
}, [dashPageCount]);
|
|
2579
4178
|
const resetView = useCallback(() => {
|
|
2580
4179
|
setCursor(0);
|
|
2581
4180
|
setExpanded(-1);
|
|
2582
4181
|
}, []);
|
|
2583
4182
|
const clampRow = (n) => Math.max(0, Math.min(rowCountRef.current - 1, n));
|
|
2584
4183
|
const mouse = useMouse();
|
|
2585
|
-
|
|
4184
|
+
useEffect3(() => {
|
|
2586
4185
|
if (!IS_TTY) return;
|
|
2587
4186
|
mouse.enable();
|
|
2588
4187
|
const onScroll = (_pos, dir) => {
|
|
2589
|
-
|
|
4188
|
+
const up = dir === "scrollup";
|
|
4189
|
+
if (tab === 1) {
|
|
4190
|
+
setCursor((c) => up ? Math.max(0, c - 3) : c + 3);
|
|
4191
|
+
} else if (tab === 0 && dashPageCountRef.current > 1) {
|
|
4192
|
+
setDashPage((p) => up ? Math.max(0, p - 1) : Math.min(dashPageCountRef.current - 1, p + 1));
|
|
4193
|
+
}
|
|
2590
4194
|
};
|
|
2591
4195
|
mouse.events.on("scroll", onScroll);
|
|
2592
4196
|
return () => {
|
|
@@ -2601,8 +4205,8 @@ function App({ interval: cliInterval }) {
|
|
|
2601
4205
|
});
|
|
2602
4206
|
}
|
|
2603
4207
|
function toggleOnboard(i) {
|
|
2604
|
-
if (i < 0 || i >=
|
|
2605
|
-
const pid =
|
|
4208
|
+
if (i < 0 || i >= pickerProviders.length) return;
|
|
4209
|
+
const pid = pickerProviders[i];
|
|
2606
4210
|
setOnboardSel((prev) => {
|
|
2607
4211
|
const base = prev ?? detected;
|
|
2608
4212
|
return base.includes(pid) ? base.filter((p) => p !== pid) : [...base, pid];
|
|
@@ -2611,16 +4215,31 @@ function App({ interval: cliInterval }) {
|
|
|
2611
4215
|
function toggleProvider(pid) {
|
|
2612
4216
|
updateConfig((c) => ({
|
|
2613
4217
|
...c,
|
|
4218
|
+
// Toggling in settings is also an explicit decision → mark it known.
|
|
4219
|
+
knownProviders: c.knownProviders.includes(pid) ? c.knownProviders : [...c.knownProviders, pid],
|
|
2614
4220
|
disabledProviders: c.disabledProviders.includes(pid) ? c.disabledProviders.filter((p) => p !== pid) : [...c.disabledProviders, pid]
|
|
2615
4221
|
}));
|
|
2616
4222
|
}
|
|
2617
4223
|
function confirmOnboarding() {
|
|
2618
4224
|
const enabled = onboardEnabled;
|
|
2619
|
-
updateConfig((c) =>
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
4225
|
+
updateConfig((c) => {
|
|
4226
|
+
if (!c.onboarded) {
|
|
4227
|
+
return {
|
|
4228
|
+
...c,
|
|
4229
|
+
disabledProviders: PROVIDER_ORDER.filter((p) => !enabled.includes(p)),
|
|
4230
|
+
knownProviders: [...PROVIDER_ORDER],
|
|
4231
|
+
onboarded: true
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
const newlyDisabled = pickerProviders.filter((p) => !enabled.includes(p));
|
|
4235
|
+
return {
|
|
4236
|
+
...c,
|
|
4237
|
+
disabledProviders: [.../* @__PURE__ */ new Set([...c.disabledProviders, ...newlyDisabled])],
|
|
4238
|
+
knownProviders: [.../* @__PURE__ */ new Set([...c.knownProviders, ...pickerProviders])]
|
|
4239
|
+
};
|
|
4240
|
+
});
|
|
4241
|
+
setOnboardSel(null);
|
|
4242
|
+
setOnboardCursor(0);
|
|
2624
4243
|
}
|
|
2625
4244
|
function cycleAccount(dir) {
|
|
2626
4245
|
if (slots.length <= 1) return;
|
|
@@ -2724,12 +4343,12 @@ function App({ interval: cliInterval }) {
|
|
|
2724
4343
|
}
|
|
2725
4344
|
const totalSettingsRows = ACCOUNT_ROWS_START + cfg.accounts.length + 1;
|
|
2726
4345
|
useInput((input, key) => {
|
|
2727
|
-
if (
|
|
4346
|
+
if (showPicker) {
|
|
2728
4347
|
if (input === "q") {
|
|
2729
4348
|
exit();
|
|
2730
4349
|
return;
|
|
2731
4350
|
}
|
|
2732
|
-
const startIdx =
|
|
4351
|
+
const startIdx = pickerProviders.length;
|
|
2733
4352
|
if (key.upArrow) {
|
|
2734
4353
|
setOnboardCursor((c) => Math.max(0, c - 1));
|
|
2735
4354
|
return;
|
|
@@ -2968,6 +4587,16 @@ function App({ interval: cliInterval }) {
|
|
|
2968
4587
|
}
|
|
2969
4588
|
return;
|
|
2970
4589
|
}
|
|
4590
|
+
if (tab === 0 && dashPaginated) {
|
|
4591
|
+
if (input === "]" || key.downArrow || key.pageDown) {
|
|
4592
|
+
setDashPage((p) => Math.min(dashPageCount - 1, p + 1));
|
|
4593
|
+
return;
|
|
4594
|
+
}
|
|
4595
|
+
if (input === "[" || key.upArrow || key.pageUp) {
|
|
4596
|
+
setDashPage((p) => Math.max(0, p - 1));
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
2971
4600
|
if (tab === 1) {
|
|
2972
4601
|
if (input === "p") {
|
|
2973
4602
|
cycleTableProvider(1);
|
|
@@ -3046,36 +4675,58 @@ function App({ interval: cliInterval }) {
|
|
|
3046
4675
|
return;
|
|
3047
4676
|
}
|
|
3048
4677
|
}, { isActive: IS_TTY });
|
|
3049
|
-
if (error) return /* @__PURE__ */
|
|
3050
|
-
if (!config2) return /* @__PURE__ */
|
|
3051
|
-
if (
|
|
3052
|
-
return /* @__PURE__ */
|
|
4678
|
+
if (error) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "red", children: error }) });
|
|
4679
|
+
if (!config2) return /* @__PURE__ */ jsx7(Box7, { padding: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Loading..." }) });
|
|
4680
|
+
if (showPicker) {
|
|
4681
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, children: /* @__PURE__ */ jsx7(
|
|
4682
|
+
Onboarding,
|
|
4683
|
+
{
|
|
4684
|
+
items: onboardItems,
|
|
4685
|
+
cursor: onboardCursor,
|
|
4686
|
+
onToggle: toggleOnboard,
|
|
4687
|
+
onConfirm: confirmOnboarding,
|
|
4688
|
+
heading: needsOnboarding ? "Welcome to tokmon" : "New providers detected",
|
|
4689
|
+
subheading: needsOnboarding ? "Pick the tools you want to track. You can change this anytime in settings." : "tokmon found these installed since you last set up. Pick which to track."
|
|
4690
|
+
}
|
|
4691
|
+
) });
|
|
4692
|
+
}
|
|
4693
|
+
if (showLoader) {
|
|
4694
|
+
return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: /* @__PURE__ */ jsx7(LoadingView, { groups: allGroups, stats, cols, rows }) });
|
|
4695
|
+
}
|
|
4696
|
+
if (TOO_SMALL && !showSettings) {
|
|
4697
|
+
return /* @__PURE__ */ jsx7(TinyFallback, { groups, stats, rows, cols });
|
|
3053
4698
|
}
|
|
3054
4699
|
const tokenRows = sortRows(filterTokenRows(table ? [table.daily, table.weekly, table.monthly][view] : [], search), sort);
|
|
3055
4700
|
const cursorTableRows = sortCursorRows(filterCursorRows(cursorRows ?? [], search), sort);
|
|
3056
4701
|
rowCountRef.current = tableIsCursor ? cursorTableRows.length : tokenRows.length;
|
|
3057
|
-
return /* @__PURE__ */
|
|
3058
|
-
/* @__PURE__ */
|
|
3059
|
-
/* @__PURE__ */
|
|
3060
|
-
/* @__PURE__ */
|
|
3061
|
-
|
|
4702
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 2, paddingY: 1, height: rows, overflow: "hidden", children: [
|
|
4703
|
+
/* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", children: [
|
|
4704
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
4705
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
|
|
4706
|
+
glyphs().dotSel,
|
|
3062
4707
|
" tokmon"
|
|
3063
4708
|
] }),
|
|
3064
|
-
/* @__PURE__ */
|
|
3065
|
-
"
|
|
4709
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4710
|
+
" ",
|
|
4711
|
+
glyphs().middot,
|
|
4712
|
+
" every ",
|
|
3066
4713
|
cfg.interval,
|
|
3067
4714
|
"s"
|
|
3068
4715
|
] })
|
|
3069
4716
|
] }),
|
|
3070
|
-
/* @__PURE__ */
|
|
3071
|
-
peak && /* @__PURE__ */
|
|
3072
|
-
/* @__PURE__ */
|
|
3073
|
-
/* @__PURE__ */
|
|
4717
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
4718
|
+
peak && /* @__PURE__ */ jsxs7(Fragment4, { children: [
|
|
4719
|
+
/* @__PURE__ */ jsx7(PeakBadge, { peak }),
|
|
4720
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4721
|
+
" ",
|
|
4722
|
+
glyphs().middot,
|
|
4723
|
+
" "
|
|
4724
|
+
] })
|
|
3074
4725
|
] }),
|
|
3075
|
-
/* @__PURE__ */
|
|
4726
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: time(updated, tz) })
|
|
3076
4727
|
] })
|
|
3077
4728
|
] }),
|
|
3078
|
-
showSettings ? /* @__PURE__ */
|
|
4729
|
+
showSettings ? /* @__PURE__ */ jsx7(
|
|
3079
4730
|
SettingsView,
|
|
3080
4731
|
{
|
|
3081
4732
|
config: cfg,
|
|
@@ -3086,19 +4737,23 @@ function App({ interval: cliInterval }) {
|
|
|
3086
4737
|
accountForm,
|
|
3087
4738
|
activeAccountId: cfg.activeAccountId
|
|
3088
4739
|
}
|
|
3089
|
-
) : /* @__PURE__ */
|
|
3090
|
-
/* @__PURE__ */
|
|
3091
|
-
/* @__PURE__ */
|
|
4740
|
+
) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
|
|
4741
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, marginBottom: 1, children: [
|
|
4742
|
+
/* @__PURE__ */ jsx7(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
|
|
3092
4743
|
setTab(i);
|
|
3093
4744
|
resetView();
|
|
3094
4745
|
} }),
|
|
3095
|
-
/* @__PURE__ */
|
|
4746
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4747
|
+
" Tab/",
|
|
4748
|
+
glyphs().arrowL,
|
|
4749
|
+
glyphs().arrowR
|
|
4750
|
+
] })
|
|
3096
4751
|
] }),
|
|
3097
|
-
tab === 0 && /* @__PURE__ */
|
|
3098
|
-
/* @__PURE__ */
|
|
3099
|
-
slots.length > 1 && /* @__PURE__ */
|
|
3100
|
-
/* @__PURE__ */
|
|
3101
|
-
/* @__PURE__ */
|
|
4752
|
+
tab === 0 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
|
|
4753
|
+
/* @__PURE__ */ jsx7(DashboardView, { groups, stats, cols, budget: gridBudget, focusId, layout: cfg.dashboardLayout, page: dashPage }),
|
|
4754
|
+
slots.length > 1 && /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
4755
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "focus " }),
|
|
4756
|
+
/* @__PURE__ */ jsx7(
|
|
3102
4757
|
AccountStrip,
|
|
3103
4758
|
{
|
|
3104
4759
|
slots,
|
|
@@ -3109,30 +4764,35 @@ function App({ interval: cliInterval }) {
|
|
|
3109
4764
|
}
|
|
3110
4765
|
}
|
|
3111
4766
|
)
|
|
3112
|
-
] })
|
|
4767
|
+
] }),
|
|
4768
|
+
/* @__PURE__ */ jsx7(TotalsRow, { groups, stats, cols })
|
|
3113
4769
|
] }),
|
|
3114
|
-
tab === 1 && /* @__PURE__ */
|
|
3115
|
-
tableProvs.length > 0 && /* @__PURE__ */
|
|
4770
|
+
tab === 1 && /* @__PURE__ */ jsxs7(Fragment4, { children: [
|
|
4771
|
+
tableProvs.length > 0 && /* @__PURE__ */ jsx7(TableProviderBar, { providers: tableProvs, active: effTableProvider, onSelect: (p) => {
|
|
3116
4772
|
setTableProvider(p);
|
|
3117
4773
|
setCursor(0);
|
|
3118
4774
|
setExpanded(-1);
|
|
3119
4775
|
setSearch("");
|
|
3120
4776
|
setSearchMode(false);
|
|
3121
4777
|
} }),
|
|
3122
|
-
/* @__PURE__ */
|
|
3123
|
-
/* @__PURE__ */
|
|
4778
|
+
/* @__PURE__ */ jsx7(Box7, { height: 1 }),
|
|
4779
|
+
/* @__PURE__ */ jsx7(
|
|
3124
4780
|
ControlBar,
|
|
3125
4781
|
{
|
|
3126
4782
|
views: VIEWS,
|
|
3127
4783
|
period: view,
|
|
3128
|
-
sort: SORTS_FOR[sort % SORTS_FOR.length],
|
|
4784
|
+
sort: sortLabel(SORTS_FOR[sort % SORTS_FOR.length]),
|
|
3129
4785
|
search,
|
|
3130
4786
|
searching: searchMode,
|
|
3131
4787
|
showPeriod: !tableIsCursor
|
|
3132
4788
|
}
|
|
3133
4789
|
),
|
|
3134
|
-
/* @__PURE__ */
|
|
3135
|
-
!effTableProvider ? /* @__PURE__ */
|
|
4790
|
+
/* @__PURE__ */ jsx7(Box7, { height: 1 }),
|
|
4791
|
+
!effTableProvider ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4792
|
+
"No providers enabled ",
|
|
4793
|
+
glyphs().emDash,
|
|
4794
|
+
" press s to pick providers."
|
|
4795
|
+
] }) : tableLoading && !table && !cursorRows ? /* @__PURE__ */ jsx7(Spinner, { label: "Loading history" }) : tableIsCursor ? /* @__PURE__ */ jsx7(
|
|
3136
4796
|
CursorSpendTable,
|
|
3137
4797
|
{
|
|
3138
4798
|
rows: cursorTableRows,
|
|
@@ -3140,7 +4800,7 @@ function App({ interval: cliInterval }) {
|
|
|
3140
4800
|
maxRows: Math.max(1, rows - 16),
|
|
3141
4801
|
onRowClick: (idx) => setCursor(idx)
|
|
3142
4802
|
}
|
|
3143
|
-
) : /* @__PURE__ */
|
|
4803
|
+
) : /* @__PURE__ */ jsx7(
|
|
3144
4804
|
TokenTable,
|
|
3145
4805
|
{
|
|
3146
4806
|
rows: tokenRows,
|
|
@@ -3156,7 +4816,7 @@ function App({ interval: cliInterval }) {
|
|
|
3156
4816
|
)
|
|
3157
4817
|
] })
|
|
3158
4818
|
] }),
|
|
3159
|
-
(tab === 0 || showSettings) && /* @__PURE__ */
|
|
4819
|
+
(tab === 0 || showSettings) && /* @__PURE__ */ jsx7(Footer, { hasAccounts: slots.length > 1, paginated: tab === 0 && dashPaginated, cols })
|
|
3160
4820
|
] });
|
|
3161
4821
|
}
|
|
3162
4822
|
function upsert(prev, account, patch) {
|
|
@@ -3180,6 +4840,11 @@ async function fetchScopeTable(scope, tz) {
|
|
|
3180
4840
|
if (valid.length === 1) return valid[0];
|
|
3181
4841
|
return mergeTables(valid);
|
|
3182
4842
|
}
|
|
4843
|
+
function sortLabel(entry) {
|
|
4844
|
+
if (entry.dir === "up") return `${entry.label} ${glyphs().arrowU}`;
|
|
4845
|
+
if (entry.dir === "down") return `${entry.label} ${glyphs().arrowD}`;
|
|
4846
|
+
return entry.label;
|
|
4847
|
+
}
|
|
3183
4848
|
function sortRows(rows, sortIdx) {
|
|
3184
4849
|
const sorted = [...rows];
|
|
3185
4850
|
switch (sortIdx % SORTS.length) {
|
|
@@ -3217,47 +4882,113 @@ function sortCursorRows(rows, sortIdx) {
|
|
|
3217
4882
|
}
|
|
3218
4883
|
}
|
|
3219
4884
|
function AccountStrip({ slots, activeIdx, onSelect }) {
|
|
3220
|
-
return /* @__PURE__ */
|
|
3221
|
-
const
|
|
3222
|
-
const dot = s.id === null ?
|
|
4885
|
+
return /* @__PURE__ */ jsx7(Box7, { flexWrap: "wrap", children: slots.map((s, i) => {
|
|
4886
|
+
const active2 = i === activeIdx;
|
|
4887
|
+
const dot = s.id === null ? glyphs().dotAll : glyphs().dot;
|
|
3223
4888
|
const label = truncateName(s.name, 16);
|
|
3224
|
-
return /* @__PURE__ */
|
|
3225
|
-
/* @__PURE__ */
|
|
3226
|
-
/* @__PURE__ */
|
|
3227
|
-
/* @__PURE__ */
|
|
3228
|
-
/* @__PURE__ */
|
|
3229
|
-
|
|
4889
|
+
return /* @__PURE__ */ jsxs7(ClickableBox, { onClick: () => onSelect(i), marginRight: 2, children: [
|
|
4890
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: !active2, children: i }),
|
|
4891
|
+
/* @__PURE__ */ jsx7(Text7, { children: " " }),
|
|
4892
|
+
/* @__PURE__ */ jsx7(Text7, { color: s.color, bold: active2, dimColor: !active2, children: dot }),
|
|
4893
|
+
/* @__PURE__ */ jsx7(Text7, { children: " " }),
|
|
4894
|
+
active2 ? /* @__PURE__ */ jsx7(Text7, { bold: true, color: s.color, children: label }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: label })
|
|
3230
4895
|
] }, s.id ?? "__all__");
|
|
3231
4896
|
}) });
|
|
3232
4897
|
}
|
|
3233
|
-
function Footer({ hasAccounts }) {
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
/* @__PURE__ */
|
|
4898
|
+
function Footer({ hasAccounts, paginated, cols }) {
|
|
4899
|
+
const inner = cols - 4;
|
|
4900
|
+
const BASE2 = "by David Ilie (davidilie.com) \xB7 s=settings q=quit".length;
|
|
4901
|
+
const JUMP = "0-9=jump a/A=cycle ".length;
|
|
4902
|
+
const PAGE = "scroll=page ".length;
|
|
4903
|
+
const showJump = hasAccounts && inner >= BASE2 + JUMP + (paginated ? PAGE : 0);
|
|
4904
|
+
const showPage = paginated && inner >= BASE2 + (showJump ? JUMP : 0) + PAGE;
|
|
4905
|
+
return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexWrap: "nowrap", children: [
|
|
4906
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "by " }),
|
|
4907
|
+
/* @__PURE__ */ jsx7(Text7, { children: "David Ilie" }),
|
|
4908
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " (" }),
|
|
4909
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "davidilie.com" }),
|
|
4910
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4911
|
+
") ",
|
|
4912
|
+
glyphs().middot,
|
|
4913
|
+
" s=settings "
|
|
4914
|
+
] }),
|
|
4915
|
+
showJump && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "0-9=jump a/A=cycle " }),
|
|
4916
|
+
showPage && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "scroll=page " }),
|
|
4917
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "q=quit" })
|
|
4918
|
+
] });
|
|
4919
|
+
}
|
|
4920
|
+
function TinyFallback({ groups, stats, rows, cols }) {
|
|
4921
|
+
const maxLines = Math.max(1, rows - 4);
|
|
4922
|
+
const visible = groups.slice(0, maxLines);
|
|
4923
|
+
const hidden = groups.length - visible.length;
|
|
4924
|
+
const w = Math.max(8, cols - 2);
|
|
4925
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, height: rows, overflow: "hidden", children: [
|
|
4926
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: "greenBright", children: [
|
|
4927
|
+
glyphs().dotSel,
|
|
4928
|
+
" tokmon"
|
|
4929
|
+
] }),
|
|
4930
|
+
groups.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4931
|
+
"No providers ",
|
|
4932
|
+
glyphs().emDash,
|
|
4933
|
+
" s=settings"
|
|
4934
|
+
] }) : visible.map((g) => /* @__PURE__ */ jsx7(TinyRow, { provider: g.provider, accounts: g.accounts, stats, width: w }, g.provider)),
|
|
4935
|
+
hidden > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
4936
|
+
"+",
|
|
4937
|
+
hidden,
|
|
4938
|
+
" more (enlarge terminal)"
|
|
4939
|
+
] }),
|
|
4940
|
+
/* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
|
|
4941
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "s=settings q=quit" })
|
|
4942
|
+
] });
|
|
4943
|
+
}
|
|
4944
|
+
function TinyRow({ provider, accounts, stats, width }) {
|
|
4945
|
+
const meta = PROVIDERS[provider];
|
|
4946
|
+
const dashboards = accounts.map((a) => stats.get(a.id)?.dashboard).filter(Boolean);
|
|
4947
|
+
const billings = accounts.map((a) => stats.get(a.id)?.billing).filter(Boolean);
|
|
4948
|
+
const todayCost = dashboards.reduce((sum, d) => sum + (d?.today.cost ?? 0), 0);
|
|
4949
|
+
const pctMetric = billings.flatMap((b) => b?.metrics ?? []).find((m) => m.format.kind === "percent");
|
|
4950
|
+
const detail = meta.hasUsage ? `${currency(todayCost)} today` : pctMetric ? `${Math.round(pctMetric.used)}%` : "billing";
|
|
4951
|
+
const name = truncateName(meta.name, Math.max(4, width - 18));
|
|
4952
|
+
return /* @__PURE__ */ jsxs7(Box7, { width, children: [
|
|
4953
|
+
/* @__PURE__ */ jsxs7(Text7, { color: meta.color, children: [
|
|
4954
|
+
glyphs().dot,
|
|
4955
|
+
" "
|
|
4956
|
+
] }),
|
|
4957
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: meta.color, children: name }),
|
|
4958
|
+
/* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
|
|
4959
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: detail })
|
|
3242
4960
|
] });
|
|
3243
4961
|
}
|
|
3244
4962
|
|
|
3245
4963
|
// src/cli.tsx
|
|
3246
|
-
import { jsx as
|
|
4964
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
3247
4965
|
process.on("unhandledRejection", () => {
|
|
3248
4966
|
});
|
|
4967
|
+
EventEmitter.defaultMaxListeners = 100;
|
|
4968
|
+
var emitWarning = process.emitWarning.bind(process);
|
|
4969
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
4970
|
+
const msg = typeof warning === "string" ? warning : warning?.message;
|
|
4971
|
+
if (typeof msg === "string" && /SQLite is an experimental feature/i.test(msg)) return;
|
|
4972
|
+
return emitWarning(warning, ...rest);
|
|
4973
|
+
});
|
|
3249
4974
|
var args = process.argv.slice(2);
|
|
3250
4975
|
var interval;
|
|
4976
|
+
var asciiFlag = null;
|
|
3251
4977
|
for (let i = 0; i < args.length; i++) {
|
|
3252
4978
|
if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
|
|
3253
4979
|
interval = Math.max(500, Number(args[i + 1]) * 1e3);
|
|
3254
4980
|
i++;
|
|
3255
4981
|
}
|
|
4982
|
+
if (args[i] === "--ascii") asciiFlag = "on";
|
|
4983
|
+
if (args[i] === "--no-ascii") asciiFlag = "off";
|
|
3256
4984
|
if (args[i] === "--help" || args[i] === "-h") {
|
|
3257
|
-
console.log("tokmon - Terminal dashboard for
|
|
4985
|
+
console.log("tokmon - Terminal usage dashboard for your AI coding tools\n");
|
|
4986
|
+
console.log(" Claude \xB7 Codex \xB7 Cursor \xB7 Copilot \xB7 opencode \xB7 pi \xB7 Antigravity \xB7 Gemini\n");
|
|
3258
4987
|
console.log("Usage: tokmon [options]\n");
|
|
3259
4988
|
console.log("Options:");
|
|
3260
4989
|
console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
|
|
4990
|
+
console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
|
|
4991
|
+
console.log(" --no-ascii Force Unicode glyphs");
|
|
3261
4992
|
console.log(" -h, --help Show this help\n");
|
|
3262
4993
|
console.log("Keybindings:");
|
|
3263
4994
|
console.log(" Tab Switch Dashboard / Table");
|
|
@@ -3274,6 +5005,13 @@ var config = await loadConfig();
|
|
|
3274
5005
|
if (config.clearScreen && process.stdout.isTTY) {
|
|
3275
5006
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
3276
5007
|
}
|
|
3277
|
-
|
|
5008
|
+
setGlyphs(resolveGlyphs({
|
|
5009
|
+
flag: asciiFlag,
|
|
5010
|
+
env: process.env,
|
|
5011
|
+
config: config.ascii,
|
|
5012
|
+
isTTY: !!process.stdout.isTTY,
|
|
5013
|
+
platform: process.platform
|
|
5014
|
+
}));
|
|
5015
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsx8(MouseProvider, { children: /* @__PURE__ */ jsx8(App, { interval, initialConfig: config }) }));
|
|
3278
5016
|
await waitUntilExit();
|
|
3279
5017
|
await flushDisk();
|