termdock 1.4.39 → 1.4.41
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/dist/client/assets/{abnfDiagram-VRR7QNED-CnSBvlEl.js → abnfDiagram-VRR7QNED-HDoBgxWT.js} +1 -1
- package/dist/client/assets/{arc-CALnqP2n.js → arc-BH6z7aWz.js} +1 -1
- package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-Cdm6dAQd.js → architectureDiagram-ZJ3FMSHR-gToNeE16.js} +1 -1
- package/dist/client/assets/{blockDiagram-677ZJIJ3-DtmrJpi1.js → blockDiagram-677ZJIJ3-ICqpBbPh.js} +1 -1
- package/dist/client/assets/{c4Diagram-LMCZKHZV-DUfPhQWl.js → c4Diagram-LMCZKHZV-Cs8laRo7.js} +1 -1
- package/dist/client/assets/channel-4xtLMVZG.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-Bw9S69zo.js → chunk-2Q5K7J3B-DjEsh7yd.js} +1 -1
- package/dist/client/assets/{chunk-32BRIVSS-kA50Go3u.js → chunk-32BRIVSS-CF5FlTY0.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-ott1soDa.js → chunk-5VM5RSS4-BMrWAk7U.js} +1 -1
- package/dist/client/assets/{chunk-EX3LRPZG-Dt9wOwBs.js → chunk-EX3LRPZG-BVmFb5Fk.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-C30-Z0yw.js → chunk-JWPE2WC7-V63J5zEV.js} +1 -1
- package/dist/client/assets/{chunk-MOJQB5TN-B1mAX1TP.js → chunk-MOJQB5TN-dYLmvHmF.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-B8ysyDpM.js → chunk-RYQCIY6F-BhB2sJ9d.js} +1 -1
- package/dist/client/assets/{chunk-V7JOEXUC-DQ_-bi4y.js → chunk-V7JOEXUC-MKtyloOY.js} +1 -1
- package/dist/client/assets/{chunk-VR4S4FIN-B4D6dY6b.js → chunk-VR4S4FIN-CXkxISIP.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-jyClBYKv.js → chunk-XXDRQBXY-Vxk1bfpg.js} +1 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-CqVMtFol.js +1 -0
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-CqVMtFol.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CfK-PvOQ.js → cose-bilkent-JH36ORCC-CCAJeGmr.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-C9B5lUbv.js → cynefin-VYW2F7L2-DYmlFCBl.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-TSTJHNR4-DEVoE6FG.js → cynefinDiagram-TSTJHNR4-BCuiD0Wu.js} +1 -1
- package/dist/client/assets/{dagre-VKFMJZFB-qmfg5oNl.js → dagre-VKFMJZFB-B89LOYbl.js} +1 -1
- package/dist/client/assets/{diagram-FQU43EPY-BFiSSiki.js → diagram-FQU43EPY-CdcNoNsV.js} +1 -1
- package/dist/client/assets/{diagram-G47NLZAW-DCyYqHvi.js → diagram-G47NLZAW-BCh4AId5.js} +1 -1
- package/dist/client/assets/{diagram-NH7WQ7WH-BW8VNzAI.js → diagram-NH7WQ7WH-C0poEQwQ.js} +1 -1
- package/dist/client/assets/{diagram-OA4YK3LP-D6nchqb6.js → diagram-OA4YK3LP-B_VA_1SP.js} +1 -1
- package/dist/client/assets/{diagram-WEI45ONY-ov91AGGg.js → diagram-WEI45ONY-gaYCX8cr.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-CCIWWBDH-Bpxswjdx.js → ebnfDiagram-CCIWWBDH-DSbWkrom.js} +1 -1
- package/dist/client/assets/{erDiagram-Q63AITRT-afC1trBs.js → erDiagram-Q63AITRT-BWGrM9sL.js} +1 -1
- package/dist/client/assets/{flowDiagram-23GEKE2U-CT0Zgy4g.js → flowDiagram-23GEKE2U-C-EtTJ8t.js} +1 -1
- package/dist/client/assets/{ganttDiagram-NO4QXBWP-DwaUqtjy.js → ganttDiagram-NO4QXBWP-CmP1iPcy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-THiUXP7D.js → gitGraphDiagram-IHSO6WYX-Cuflyp-G.js} +1 -1
- package/dist/client/assets/index-BQkNIHH4.css +1 -0
- package/dist/client/assets/index-Dpiz4y7Z.js +430 -0
- package/dist/client/assets/{infoDiagram-FWYZ7A6U-B9HtN83k.js → infoDiagram-FWYZ7A6U-BaXF979-.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-Bv9MGIw7.js → ishikawaDiagram-FXEZZL3T-WE7VJf7L.js} +1 -1
- package/dist/client/assets/{journeyDiagram-5HDEW3XC-BHF_IOk8.js → journeyDiagram-5HDEW3XC-DN9iYyrt.js} +1 -1
- package/dist/client/assets/{kanban-definition-HUTT4EX6-BnXI6zKI.js → kanban-definition-HUTT4EX6-B_EI8xmr.js} +1 -1
- package/dist/client/assets/{linear-D5-uPtP1.js → linear-DTI-gSSx.js} +1 -1
- package/dist/client/assets/{mermaid.core-C8AmiozH.js → mermaid.core-D77kMztY.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LN4V7U3C-C07CYbmD.js → mindmap-definition-LN4V7U3C-BAIvnYYM.js} +1 -1
- package/dist/client/assets/{pegDiagram-2B236MQR-CgxltL7c.js → pegDiagram-2B236MQR-R4-km5QO.js} +1 -1
- package/dist/client/assets/{pieDiagram-ENE6RG2P-C43JmmWs.js → pieDiagram-ENE6RG2P-C7vr0BSx.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-D4Dykc-0.js → quadrantDiagram-ABIIQ3AL-C6KweAEm.js} +1 -1
- package/dist/client/assets/{railroadDiagram-RFXS5EU6-xHLG6dKk.js → railroadDiagram-RFXS5EU6-BK5P1uqh.js} +1 -1
- package/dist/client/assets/{requirementDiagram-TGXJPOKE-XO7M19hA.js → requirementDiagram-TGXJPOKE-4Mu2Tvfk.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-HTMAVEWB-CTOauFrk.js → sankeyDiagram-HTMAVEWB-Dhz0DuCK.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-BmiBP2Ql.js → sequenceDiagram-DBY2YBRQ-BxWUNZp2.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-DPa4M1sV.js → sizeCapture-X5ZJPWSS-DiHIL8L9.js} +1 -1
- package/dist/client/assets/{stateDiagram-2N3HPSRC-B-ZG_536.js → stateDiagram-2N3HPSRC-BRZa8Zoe.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BtHI-mJm.js +1 -0
- package/dist/client/assets/{swimlanes-5IMT3BWC-BnrujRnH.js → swimlanes-5IMT3BWC-BMdtPcqS.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-D9BtKvgg.js +8 -0
- package/dist/client/assets/{timeline-definition-FHXFAJF6-B_CovjvP.js → timeline-definition-FHXFAJF6-bkd6SAQM.js} +1 -1
- package/dist/client/assets/{vennDiagram-L72KCM5P-DVZK2btB.js → vennDiagram-L72KCM5P-3_qHMkHF.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-EHGQE667-FXnlzV-g.js → wardleyDiagram-EHGQE667-DOL7-rw3.js} +1 -1
- package/dist/client/assets/{xychartDiagram-FW5EYKEG-Bom661tv.js → xychartDiagram-FW5EYKEG-D2kDx-Wq.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/quota/QuotaManager.js +142 -0
- package/dist/server/quota/providers/claude.js +125 -0
- package/dist/server/quota/providers/codex.js +175 -0
- package/dist/server/quota/providers/copilot.js +125 -0
- package/dist/server/quota/providers/gemini.js +52 -0
- package/dist/server/quota/providers/grok.js +76 -0
- package/dist/server/quota/providers/kimi.js +113 -0
- package/dist/server/quota/tokenDiscovery.js +239 -0
- package/dist/server/quota/types.js +8 -0
- package/dist/server/routes/terminal.js +47 -0
- package/package.json +1 -1
- package/dist/client/assets/channel-DBje1ooC.js +0 -1
- package/dist/client/assets/classDiagram-OUVF2IWQ-UVRtMcd4.js +0 -1
- package/dist/client/assets/classDiagram-v2-EOCWNBFH-UVRtMcd4.js +0 -1
- package/dist/client/assets/index-Dpxt3np6.js +0 -430
- package/dist/client/assets/index-d23ibKJx.css +0 -1
- package/dist/client/assets/stateDiagram-v2-6OUMAXLB-DLJqIMm-.js +0 -1
- package/dist/client/assets/swimlanesDiagram-G3AALYLV-BtyJTDhv.js +0 -8
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude / Anthropic subscription quota provider.
|
|
3
|
+
*
|
|
4
|
+
* Calls `GET https://api.anthropic.com/api/oauth/usage` with the OAuth
|
|
5
|
+
* Bearer token discovered from OpenCode auth.json or Claude settings.json.
|
|
6
|
+
* Returns per-window percentages (5h, 7d, Sonnet, Opus, Fable).
|
|
7
|
+
*/
|
|
8
|
+
import { discoverClaudeTokens } from '../tokenDiscovery.js';
|
|
9
|
+
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
10
|
+
async function fetchWithAuth(token) {
|
|
11
|
+
const headers = {
|
|
12
|
+
Authorization: `Bearer ${token}`,
|
|
13
|
+
Accept: 'application/json',
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'User-Agent': 'termdock/1.0',
|
|
16
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
17
|
+
};
|
|
18
|
+
const res = await fetch(USAGE_URL, { headers, signal: AbortSignal.timeout(10_000) });
|
|
19
|
+
if (res.status === 401)
|
|
20
|
+
throw new Error('Claude: token expired');
|
|
21
|
+
if (res.status === 429)
|
|
22
|
+
throw new Error('Claude: rate limited');
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new Error(`Claude: HTTP ${res.status}`);
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
export async function fetchClaudeQuota() {
|
|
28
|
+
const tokens = discoverClaudeTokens();
|
|
29
|
+
const result = {
|
|
30
|
+
slug: 'claude',
|
|
31
|
+
displayName: 'Claude',
|
|
32
|
+
usagePercent: 0,
|
|
33
|
+
windows: [],
|
|
34
|
+
fetchedAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
if (tokens.length === 0) {
|
|
37
|
+
result.error = 'No Claude token found';
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
let usageData = null;
|
|
41
|
+
let lastError = '';
|
|
42
|
+
for (const t of tokens) {
|
|
43
|
+
try {
|
|
44
|
+
usageData = await fetchWithAuth(t.token);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!usageData) {
|
|
52
|
+
result.error = lastError || 'Failed to fetch Claude usage';
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const windows = [];
|
|
56
|
+
let worstPercent = 0;
|
|
57
|
+
// 5-hour burst window
|
|
58
|
+
if (usageData.five_hour) {
|
|
59
|
+
const pct = usageData.five_hour.utilization;
|
|
60
|
+
windows.push({
|
|
61
|
+
label: '5-hour',
|
|
62
|
+
percent: pct,
|
|
63
|
+
resetAt: usageData.five_hour.resets_at,
|
|
64
|
+
windowLabel: '5h',
|
|
65
|
+
});
|
|
66
|
+
if (pct > worstPercent)
|
|
67
|
+
worstPercent = pct;
|
|
68
|
+
}
|
|
69
|
+
// 7-day rolling window (main quota)
|
|
70
|
+
if (usageData.seven_day) {
|
|
71
|
+
const pct = usageData.seven_day.utilization;
|
|
72
|
+
windows.push({
|
|
73
|
+
label: '7-day',
|
|
74
|
+
percent: pct,
|
|
75
|
+
resetAt: usageData.seven_day.resets_at,
|
|
76
|
+
windowLabel: '7d',
|
|
77
|
+
});
|
|
78
|
+
if (pct > worstPercent)
|
|
79
|
+
worstPercent = pct;
|
|
80
|
+
}
|
|
81
|
+
// Sonnet sub-quota
|
|
82
|
+
if (usageData.seven_day_sonnet) {
|
|
83
|
+
const pct = usageData.seven_day_sonnet.utilization;
|
|
84
|
+
windows.push({
|
|
85
|
+
label: 'Sonnet',
|
|
86
|
+
percent: pct,
|
|
87
|
+
resetAt: usageData.seven_day_sonnet.resets_at,
|
|
88
|
+
windowLabel: '7d',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Opus sub-quota
|
|
92
|
+
if (usageData.seven_day_opus) {
|
|
93
|
+
windows.push({
|
|
94
|
+
label: 'Opus',
|
|
95
|
+
percent: usageData.seven_day_opus.utilization,
|
|
96
|
+
resetAt: usageData.seven_day_opus.resets_at,
|
|
97
|
+
windowLabel: '7d',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Fable weekly (from limits array)
|
|
101
|
+
if (usageData.limits) {
|
|
102
|
+
const fable = usageData.limits.find((l) => l.kind === 'weekly_scoped' &&
|
|
103
|
+
l.scope?.model?.display_name?.toLowerCase() === 'fable');
|
|
104
|
+
if (fable?.percent !== undefined) {
|
|
105
|
+
windows.push({
|
|
106
|
+
label: 'Fable',
|
|
107
|
+
percent: fable.percent,
|
|
108
|
+
resetAt: fable.resets_at,
|
|
109
|
+
windowLabel: '7d',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
result.windows = windows;
|
|
114
|
+
result.usagePercent = Math.round(worstPercent);
|
|
115
|
+
result.remaining = Math.max(0, 100 - worstPercent);
|
|
116
|
+
result.entitlement = 100;
|
|
117
|
+
// Use the 7-day reset as the primary
|
|
118
|
+
if (usageData.seven_day?.resets_at) {
|
|
119
|
+
result.resetAt = usageData.seven_day.resets_at;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
export function isClaudeAvailable() {
|
|
124
|
+
return discoverClaudeTokens().length > 0;
|
|
125
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex / OpenAI subscription quota provider.
|
|
3
|
+
*
|
|
4
|
+
* Quota sources (tried in order):
|
|
5
|
+
* 1. chatgpt.com/backend-api/wham/usage — ChatGPT Plus/Pro web subscription
|
|
6
|
+
* 2. api.openai.com/v1/organization/usage — API platform billing
|
|
7
|
+
*
|
|
8
|
+
* Tokens discovered from: ~/.codex/auth.json > OpenCode auth.json > env vars.
|
|
9
|
+
*/
|
|
10
|
+
import { discoverCodexTokens } from '../tokenDiscovery.js';
|
|
11
|
+
const WHAM_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
12
|
+
const API_BILLING_URL = 'https://api.openai.com/v1/organization/usage';
|
|
13
|
+
const ORG_LIST_URL = 'https://api.openai.com/v1/organizations';
|
|
14
|
+
async function fetchWham(token) {
|
|
15
|
+
const res = await fetch(WHAM_URL, {
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${token}`,
|
|
18
|
+
Accept: 'application/json',
|
|
19
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
20
|
+
},
|
|
21
|
+
signal: AbortSignal.timeout(10_000),
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
return null;
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
async function fetchApiBilling(token) {
|
|
28
|
+
let orgId = '';
|
|
29
|
+
try {
|
|
30
|
+
const orgRes = await fetch(ORG_LIST_URL, {
|
|
31
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
32
|
+
signal: AbortSignal.timeout(10_000),
|
|
33
|
+
});
|
|
34
|
+
if (orgRes.ok) {
|
|
35
|
+
const orgs = await orgRes.json();
|
|
36
|
+
orgId = orgs.data?.[0]?.id || '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { /* optional */ }
|
|
40
|
+
const res = await fetch(API_BILLING_URL, {
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${token}`,
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
...(orgId ? { 'OpenAI-Organization': orgId } : {}),
|
|
45
|
+
},
|
|
46
|
+
signal: AbortSignal.timeout(10_000),
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return null;
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
return data.data?.[0]?.usage_tokens ?? null;
|
|
52
|
+
}
|
|
53
|
+
// ── Helpers ──
|
|
54
|
+
function unixToISO(ts) {
|
|
55
|
+
if (!ts)
|
|
56
|
+
return undefined;
|
|
57
|
+
return new Date(ts * 1000).toISOString();
|
|
58
|
+
}
|
|
59
|
+
function windowLabelFromSeconds(seconds) {
|
|
60
|
+
if (!seconds)
|
|
61
|
+
return '';
|
|
62
|
+
if (seconds <= 3600)
|
|
63
|
+
return 'Hourly';
|
|
64
|
+
if (seconds <= 86400)
|
|
65
|
+
return 'Daily';
|
|
66
|
+
if (seconds <= 604800)
|
|
67
|
+
return 'Weekly';
|
|
68
|
+
return 'Monthly';
|
|
69
|
+
}
|
|
70
|
+
function collectWindows(rateLimit, prefix = '') {
|
|
71
|
+
const results = [];
|
|
72
|
+
const add = (label, w) => {
|
|
73
|
+
if (!w)
|
|
74
|
+
return;
|
|
75
|
+
results.push({
|
|
76
|
+
label: prefix ? `${prefix} ${label}` : label,
|
|
77
|
+
percent: w.used_percent,
|
|
78
|
+
resetAt: unixToISO(w.reset_at),
|
|
79
|
+
windowLabel: windowLabelFromSeconds(w.limit_window_seconds),
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
add('Primary', rateLimit?.primary_window);
|
|
83
|
+
add('Secondary', rateLimit?.secondary_window);
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
// ── Main fetch ──
|
|
87
|
+
export async function fetchCodexQuota() {
|
|
88
|
+
const tokens = discoverCodexTokens();
|
|
89
|
+
const result = {
|
|
90
|
+
slug: 'codex',
|
|
91
|
+
displayName: 'Codex',
|
|
92
|
+
usagePercent: 0,
|
|
93
|
+
windows: [],
|
|
94
|
+
fetchedAt: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
if (tokens.length === 0) {
|
|
97
|
+
result.error = 'No token found';
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
for (const t of tokens) {
|
|
101
|
+
// Strategy 1: wham endpoint (ChatGPT Plus/Pro subscription)
|
|
102
|
+
try {
|
|
103
|
+
const wham = await fetchWham(t.token);
|
|
104
|
+
if (wham) {
|
|
105
|
+
const windows = [];
|
|
106
|
+
let worstPct = 0;
|
|
107
|
+
// Main rate limit
|
|
108
|
+
windows.push(...collectWindows(wham.rate_limit));
|
|
109
|
+
// Code review rate limit (separate quota for code review feature)
|
|
110
|
+
windows.push(...collectWindows(wham.code_review_rate_limit, 'Review'));
|
|
111
|
+
// Additional rate limits (GPT-5, image gen, etc.)
|
|
112
|
+
if (wham.additional_rate_limits) {
|
|
113
|
+
for (const [kind, rl] of Object.entries(wham.additional_rate_limits)) {
|
|
114
|
+
if (rl && (rl.primary_window || rl.secondary_window)) {
|
|
115
|
+
windows.push(...collectWindows(rl, kind));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (windows.length > 0) {
|
|
120
|
+
for (const w of windows) {
|
|
121
|
+
if (w.percent > worstPct)
|
|
122
|
+
worstPct = w.percent;
|
|
123
|
+
}
|
|
124
|
+
// Add plan type / credit info as context
|
|
125
|
+
if (wham.plan_type) {
|
|
126
|
+
const planLabel = wham.plan_type === 'plus' ? 'ChatGPT Plus'
|
|
127
|
+
: wham.plan_type === 'pro' ? 'ChatGPT Pro'
|
|
128
|
+
: wham.plan_type === 'free' ? 'ChatGPT Free'
|
|
129
|
+
: wham.plan_type;
|
|
130
|
+
// Prepend plan as first window entry
|
|
131
|
+
windows.unshift({
|
|
132
|
+
label: planLabel,
|
|
133
|
+
percent: 0,
|
|
134
|
+
windowLabel: wham.credits?.unlimited ? 'unlimited' : undefined,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
result.windows = windows;
|
|
138
|
+
result.usagePercent = Math.round(worstPct);
|
|
139
|
+
result.remaining = Math.max(0, 100 - worstPct);
|
|
140
|
+
result.entitlement = 100;
|
|
141
|
+
// Use primary reset time
|
|
142
|
+
if (wham.rate_limit?.primary_window?.reset_at) {
|
|
143
|
+
result.resetAt = unixToISO(wham.rate_limit.primary_window.reset_at);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Fall through to next strategy
|
|
151
|
+
}
|
|
152
|
+
// Strategy 2: API billing endpoint
|
|
153
|
+
try {
|
|
154
|
+
const usageTokens = await fetchApiBilling(t.token);
|
|
155
|
+
if (usageTokens !== null) {
|
|
156
|
+
result.windows = [{
|
|
157
|
+
label: 'API Usage',
|
|
158
|
+
percent: 0,
|
|
159
|
+
windowLabel: usageTokens >= 1_000_000
|
|
160
|
+
? `${(usageTokens / 1_000_000).toFixed(1)}M tokens`
|
|
161
|
+
: `${usageTokens.toLocaleString()} tokens`,
|
|
162
|
+
}];
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Continue
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
result.error = 'Unable to fetch Codex quota. If you use ChatGPT Plus, login via Codex CLI (`codex login`).';
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
export function isCodexAvailable() {
|
|
174
|
+
return discoverCodexTokens().length > 0;
|
|
175
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot subscription quota provider.
|
|
3
|
+
*
|
|
4
|
+
* Copilot's usage API is internal (not publicly documented). We attempt
|
|
5
|
+
* multiple endpoints that the Copilot extension and CLI use.
|
|
6
|
+
*/
|
|
7
|
+
import { discoverCopilotTokens } from '../tokenDiscovery.js';
|
|
8
|
+
const COPILOT_CHAT_USAGE = 'https://api.github.com/copilot/usage';
|
|
9
|
+
const COPILOT_QUOTA = 'https://api.github.com/user/copilot/quotas';
|
|
10
|
+
async function fetchWithAuth(token) {
|
|
11
|
+
const headers = {
|
|
12
|
+
Authorization: `Bearer ${token}`,
|
|
13
|
+
Accept: 'application/json',
|
|
14
|
+
'User-Agent': 'termdock/1.0',
|
|
15
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
16
|
+
};
|
|
17
|
+
// Try the quotas endpoint first
|
|
18
|
+
for (const url of [COPILOT_QUOTA, COPILOT_CHAT_USAGE]) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(url, {
|
|
21
|
+
headers,
|
|
22
|
+
signal: AbortSignal.timeout(10_000),
|
|
23
|
+
});
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
return res.json();
|
|
26
|
+
}
|
|
27
|
+
if (res.status === 401)
|
|
28
|
+
throw new Error('Copilot: token expired');
|
|
29
|
+
if (res.status === 403)
|
|
30
|
+
continue; // might not have copilot access
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (e instanceof Error && e.message.startsWith('Copilot:'))
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export async function fetchCopilotQuota() {
|
|
40
|
+
const tokens = discoverCopilotTokens();
|
|
41
|
+
const result = {
|
|
42
|
+
slug: 'copilot',
|
|
43
|
+
displayName: 'GitHub Copilot',
|
|
44
|
+
usagePercent: 0,
|
|
45
|
+
windows: [],
|
|
46
|
+
fetchedAt: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
if (tokens.length === 0) {
|
|
49
|
+
result.error = 'No GitHub Copilot token found';
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
let data = null;
|
|
53
|
+
let lastError = '';
|
|
54
|
+
for (const t of tokens) {
|
|
55
|
+
try {
|
|
56
|
+
data = await fetchWithAuth(t.token);
|
|
57
|
+
if (data)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!data) {
|
|
65
|
+
result.error = lastError || 'Failed to fetch Copilot usage';
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
const windows = [];
|
|
69
|
+
let worstPercent = 0;
|
|
70
|
+
let totalRemaining = 0;
|
|
71
|
+
let totalEntitlement = 0;
|
|
72
|
+
if (data.chat) {
|
|
73
|
+
const used = data.chat.used || 0;
|
|
74
|
+
const limit = data.chat.limit || 0;
|
|
75
|
+
if (limit > 0) {
|
|
76
|
+
const pct = (used / limit) * 100;
|
|
77
|
+
windows.push({
|
|
78
|
+
label: 'Chat',
|
|
79
|
+
percent: Math.round(pct * 10) / 10,
|
|
80
|
+
windowLabel: 'monthly',
|
|
81
|
+
});
|
|
82
|
+
if (pct > worstPercent)
|
|
83
|
+
worstPercent = pct;
|
|
84
|
+
totalRemaining += Math.max(0, limit - used);
|
|
85
|
+
totalEntitlement += limit;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (data.completions) {
|
|
89
|
+
const used = data.completions.used || 0;
|
|
90
|
+
const limit = data.completions.limit || 0;
|
|
91
|
+
if (limit > 0) {
|
|
92
|
+
const pct = (used / limit) * 100;
|
|
93
|
+
windows.push({
|
|
94
|
+
label: 'Completions',
|
|
95
|
+
percent: Math.round(pct * 10) / 10,
|
|
96
|
+
windowLabel: 'monthly',
|
|
97
|
+
});
|
|
98
|
+
if (pct > worstPercent)
|
|
99
|
+
worstPercent = pct;
|
|
100
|
+
totalRemaining += Math.max(0, limit - used);
|
|
101
|
+
totalEntitlement += limit;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (data.overage) {
|
|
105
|
+
if (data.overage.cost !== undefined) {
|
|
106
|
+
windows.push({
|
|
107
|
+
label: 'Overage',
|
|
108
|
+
percent: 0,
|
|
109
|
+
windowLabel: `$${data.overage.cost.toFixed(2)}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (windows.length === 0) {
|
|
114
|
+
result.error = 'Copilot API returned no usage data';
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
result.windows = windows;
|
|
118
|
+
result.usagePercent = Math.round(worstPercent);
|
|
119
|
+
result.remaining = totalRemaining || undefined;
|
|
120
|
+
result.entitlement = totalEntitlement || undefined;
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
export function isCopilotAvailable() {
|
|
124
|
+
return discoverCopilotTokens().length > 0;
|
|
125
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI subscription quota provider.
|
|
3
|
+
*
|
|
4
|
+
* Attempts Google Cloud / Gemini API usage endpoint.
|
|
5
|
+
*/
|
|
6
|
+
import { discoverGeminiTokens } from '../tokenDiscovery.js';
|
|
7
|
+
const USAGE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/usage';
|
|
8
|
+
async function fetchWithAuth(token) {
|
|
9
|
+
const res = await fetch(USAGE_URL, {
|
|
10
|
+
headers: {
|
|
11
|
+
'x-goog-api-key': token,
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
'User-Agent': 'termdock/1.0',
|
|
14
|
+
},
|
|
15
|
+
signal: AbortSignal.timeout(10_000),
|
|
16
|
+
});
|
|
17
|
+
if (res.status === 401 || res.status === 403)
|
|
18
|
+
throw new Error('Gemini: unauthorized');
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
throw new Error(`Gemini: HTTP ${res.status}`);
|
|
21
|
+
return res.json();
|
|
22
|
+
}
|
|
23
|
+
export async function fetchGeminiQuota() {
|
|
24
|
+
const tokens = discoverGeminiTokens();
|
|
25
|
+
const result = {
|
|
26
|
+
slug: 'gemini',
|
|
27
|
+
displayName: 'Gemini',
|
|
28
|
+
usagePercent: 0,
|
|
29
|
+
windows: [],
|
|
30
|
+
fetchedAt: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
if (tokens.length === 0) {
|
|
33
|
+
result.error = 'No Gemini API key found';
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
// Gemini free-tier quotas are rate-limit based, not percentage-based.
|
|
37
|
+
// The API key alone confirms the user has access.
|
|
38
|
+
// For now show a simple "active" indicator; real quota needs OAuth.
|
|
39
|
+
try {
|
|
40
|
+
await fetchWithAuth(tokens[0].token);
|
|
41
|
+
result.windows = [{ label: 'API', percent: 0, windowLabel: 'active' }];
|
|
42
|
+
result.remaining = undefined;
|
|
43
|
+
result.entitlement = undefined;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
result.error = e instanceof Error ? e.message : 'Gemini fetch failed';
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export function isGeminiAvailable() {
|
|
51
|
+
return discoverGeminiTokens().length > 0;
|
|
52
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok (xAI) subscription quota provider.
|
|
3
|
+
*/
|
|
4
|
+
import { discoverGrokTokens } from '../tokenDiscovery.js';
|
|
5
|
+
const USAGE_URL = 'https://api.x.ai/v1/usage';
|
|
6
|
+
async function fetchWithAuth(token) {
|
|
7
|
+
const res = await fetch(USAGE_URL, {
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${token}`,
|
|
10
|
+
Accept: 'application/json',
|
|
11
|
+
'User-Agent': 'termdock/1.0',
|
|
12
|
+
},
|
|
13
|
+
signal: AbortSignal.timeout(10_000),
|
|
14
|
+
});
|
|
15
|
+
if (res.status === 401)
|
|
16
|
+
throw new Error('Grok: token expired');
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
throw new Error(`Grok: HTTP ${res.status}`);
|
|
19
|
+
return res.json();
|
|
20
|
+
}
|
|
21
|
+
export async function fetchGrokQuota() {
|
|
22
|
+
const tokens = discoverGrokTokens();
|
|
23
|
+
const result = {
|
|
24
|
+
slug: 'grok',
|
|
25
|
+
displayName: 'Grok',
|
|
26
|
+
usagePercent: 0,
|
|
27
|
+
windows: [],
|
|
28
|
+
fetchedAt: Date.now(),
|
|
29
|
+
};
|
|
30
|
+
if (tokens.length === 0) {
|
|
31
|
+
result.error = 'No Grok API key found';
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
let data = null;
|
|
35
|
+
let lastError = '';
|
|
36
|
+
for (const t of tokens) {
|
|
37
|
+
try {
|
|
38
|
+
data = await fetchWithAuth(t.token);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!data?.usage) {
|
|
46
|
+
result.error = lastError || 'No Grok usage data';
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
const windows = [];
|
|
50
|
+
const usage = data.usage;
|
|
51
|
+
if (usage.monthly_percent !== undefined) {
|
|
52
|
+
windows.push({
|
|
53
|
+
label: 'Monthly',
|
|
54
|
+
percent: usage.monthly_percent,
|
|
55
|
+
resetAt: usage.monthly_reset,
|
|
56
|
+
windowLabel: 'monthly',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (usage.model_breakdown) {
|
|
60
|
+
for (const [model, pct] of Object.entries(usage.model_breakdown)) {
|
|
61
|
+
windows.push({
|
|
62
|
+
label: model,
|
|
63
|
+
percent: pct,
|
|
64
|
+
windowLabel: 'model',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
result.windows = windows;
|
|
69
|
+
result.usagePercent = Math.round(windows.length > 0 ? usage.monthly_percent || 0 : 0);
|
|
70
|
+
result.remaining = Math.max(0, 100 - result.usagePercent);
|
|
71
|
+
result.entitlement = 100;
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
export function isGrokAvailable() {
|
|
75
|
+
return discoverGrokTokens().length > 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi (Moonshot) subscription quota provider.
|
|
3
|
+
*
|
|
4
|
+
* Calls `GET https://api.kimi.com/coding/v1/usages` with the API key
|
|
5
|
+
* discovered from env or kimi-code config.
|
|
6
|
+
*/
|
|
7
|
+
import { discoverKimiTokens } from '../tokenDiscovery.js';
|
|
8
|
+
const USAGE_URL = 'https://api.kimi.com/coding/v1/usages';
|
|
9
|
+
async function fetchWithAuth(token) {
|
|
10
|
+
const res = await fetch(USAGE_URL, {
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${token}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
Accept: 'application/json',
|
|
15
|
+
'User-Agent': 'termdock/1.0',
|
|
16
|
+
},
|
|
17
|
+
signal: AbortSignal.timeout(10_000),
|
|
18
|
+
});
|
|
19
|
+
if (res.status === 401)
|
|
20
|
+
throw new Error('Kimi: token expired');
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
throw new Error(`Kimi: HTTP ${res.status}`);
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
export async function fetchKimiQuota() {
|
|
26
|
+
const tokens = discoverKimiTokens();
|
|
27
|
+
const result = {
|
|
28
|
+
slug: 'kimi',
|
|
29
|
+
displayName: 'Kimi',
|
|
30
|
+
usagePercent: 0,
|
|
31
|
+
windows: [],
|
|
32
|
+
fetchedAt: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
if (tokens.length === 0) {
|
|
35
|
+
result.error = 'No Kimi API key found';
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
let data = null;
|
|
39
|
+
let lastError = '';
|
|
40
|
+
for (const t of tokens) {
|
|
41
|
+
try {
|
|
42
|
+
data = await fetchWithAuth(t.token);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!data) {
|
|
50
|
+
result.error = lastError || 'Failed to fetch Kimi usage';
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
const windows = [];
|
|
54
|
+
let worstPercent = 0;
|
|
55
|
+
// Main usage
|
|
56
|
+
const usage = data.usage;
|
|
57
|
+
if (usage) {
|
|
58
|
+
const limit = parseInt(usage.limit || '0', 10);
|
|
59
|
+
const used = parseInt(usage.used || '0', 10);
|
|
60
|
+
const remaining = parseInt(usage.remaining || '0', 10);
|
|
61
|
+
if (limit > 0) {
|
|
62
|
+
const pct = (used / limit) * 100;
|
|
63
|
+
windows.push({
|
|
64
|
+
label: 'Token',
|
|
65
|
+
percent: Math.round(pct * 10) / 10,
|
|
66
|
+
resetAt: usage.resetTime,
|
|
67
|
+
windowLabel: 'total',
|
|
68
|
+
});
|
|
69
|
+
if (pct > worstPercent)
|
|
70
|
+
worstPercent = pct;
|
|
71
|
+
result.remaining = remaining;
|
|
72
|
+
result.entitlement = limit;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Per-window limits
|
|
76
|
+
if (data.limits) {
|
|
77
|
+
for (const limit of data.limits) {
|
|
78
|
+
if (limit.detail) {
|
|
79
|
+
const d = limit.detail;
|
|
80
|
+
const detailLimit = parseInt(d.limit || '0', 10);
|
|
81
|
+
const detailUsed = parseInt(d.used || '0', 10);
|
|
82
|
+
if (detailLimit > 0) {
|
|
83
|
+
const pct = (detailUsed / detailLimit) * 100;
|
|
84
|
+
const w = limit.window;
|
|
85
|
+
const label = w
|
|
86
|
+
? `${w.duration || ''}${w.timeUnit || ''}`.replace(/^(\d+)([a-z])/, '$1$2')
|
|
87
|
+
: 'Window';
|
|
88
|
+
windows.push({
|
|
89
|
+
label,
|
|
90
|
+
percent: Math.round(pct * 10) / 10,
|
|
91
|
+
resetAt: d.resetTime,
|
|
92
|
+
windowLabel: label,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (windows.length === 0) {
|
|
99
|
+
result.error = 'Kimi API returned no usage data';
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
result.windows = windows;
|
|
103
|
+
result.usagePercent = Math.round(worstPercent);
|
|
104
|
+
if (!result.entitlement) {
|
|
105
|
+
result.usagePercent = Math.round(worstPercent);
|
|
106
|
+
result.remaining = Math.max(0, 100 - worstPercent);
|
|
107
|
+
result.entitlement = 100;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
export function isKimiAvailable() {
|
|
112
|
+
return discoverKimiTokens().length > 0;
|
|
113
|
+
}
|