quotacap 0.0.40 → 0.0.42
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/adapters/claude.js +1 -1
- package/dist/adapters/codex.js +27 -18
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/kimi.d.ts +41 -0
- package/dist/adapters/kimi.js +451 -19
- package/dist/adapters/muse.js +1 -1
- package/dist/adapters/opencode-go.d.ts +1 -1
- package/dist/adapters/opencode-go.js +8 -1
- package/dist/adapters/pty.d.ts +9 -0
- package/dist/adapters/pty.js +16 -0
- package/dist/cli/advise.js +1 -0
- package/dist/cli/clients.d.ts +4 -0
- package/dist/cli/models.js +4 -0
- package/dist/cli/progress.d.ts +32 -0
- package/dist/cli/progress.js +70 -0
- package/dist/cli/providers.js +2 -1
- package/dist/cli/runtime.d.ts +5 -0
- package/dist/cli/runtime.js +18 -8
- package/dist/cli/status.js +1 -0
- package/dist/cli/takeover.d.ts +3 -0
- package/dist/cli/takeover.js +7 -1
- package/dist/cli/update.d.ts +4 -0
- package/dist/cli/update.js +8 -1
- package/dist/config.d.ts +5 -23
- package/dist/diagnostics/failure.js +37 -11
- package/dist/format/rows.d.ts +1 -1
- package/dist/format/rows.js +9 -6
- package/dist/mcp/server.d.ts +2 -2
- package/dist/service/index.js +3 -1
- package/dist/service/macos.d.ts +2 -0
- package/dist/service/macos.js +4 -1
- package/dist/service/systemd.d.ts +2 -0
- package/dist/service/systemd.js +5 -3
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +1 -1
- package/dist/web/src/components/AdviceDrawer.js +1 -1
- package/dist/web/src/components/FaultBanner.d.ts +10 -5
- package/dist/web/src/components/FaultBanner.js +19 -9
- package/dist/web/src/components/OpencodeGoBanner.d.ts +1 -1
- package/dist/web/src/components/OpencodeGoBanner.js +9 -2
- package/dist/web/src/components/PaceBar.d.ts +9 -4
- package/dist/web/src/components/PaceBar.js +26 -7
- package/dist/web/src/components/ProviderCard.js +2 -3
- package/dist/web/src/components/ProviderDrawer.js +3 -3
- package/dist/web/src/components/ProviderIcon.js +1 -1
- package/dist/web/src/components/ProviderRow.d.ts +1 -1
- package/dist/web/src/components/ProviderRow.js +2 -4
- package/dist/web/src/components/Recommendation.js +1 -1
- package/dist/web/src/components/ResetRail.js +1 -1
- package/dist/web/src/components/WindowsRow.d.ts +1 -1
- package/dist/web/src/components/WindowsRow.js +4 -4
- package/dist/web/src/components/settings/SettingsDrawer.js +1 -1
- package/dist/web/src/pages/Onboarding.js +2 -1
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +5 -5
- package/web/dist/assets/{index-Dk6l5h27.css → index-C6ltEeGL.css} +1 -1
- package/web/dist/assets/index-DRDDTtxF.js +13 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CsOiHZyD.js +0 -12
package/dist/adapters/claude.js
CHANGED
|
@@ -13,7 +13,7 @@ export function parseClaudeUsage(result, now = new Date()) {
|
|
|
13
13
|
const resets = new Date(resetsAt).getTime();
|
|
14
14
|
const periodStart = (parsedReset ? new Date(resets - 7 * 86400000) : new Date(now.getTime() - 7 * 86400000)).toISOString();
|
|
15
15
|
return {
|
|
16
|
-
provider: "claude", plan: "
|
|
16
|
+
provider: "claude", plan: "unknown", weeklyPct: usedPct, fiveHourPct: sessionPct,
|
|
17
17
|
resetsAt, periodStart, source: "cli", fetchedAt: now.toISOString(), raw: result,
|
|
18
18
|
};
|
|
19
19
|
}
|
package/dist/adapters/codex.js
CHANGED
|
@@ -5,25 +5,35 @@ const MONTHS = {
|
|
|
5
5
|
jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
|
|
6
6
|
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
|
|
7
7
|
};
|
|
8
|
+
// Codex 0.157 prints "9:14 AM on 30 Sep" and "9:43 PM". Older panels print
|
|
9
|
+
// "23:04 on 19 Sep" and "19:38". 12 AM is 00:00 and 12 PM is 12:00.
|
|
10
|
+
function clockHours(hours, mins, ampm) {
|
|
11
|
+
if (!Number.isFinite(hours) || !Number.isFinite(mins) || mins < 0 || mins > 59)
|
|
12
|
+
return null;
|
|
13
|
+
if (!ampm)
|
|
14
|
+
return hours >= 0 && hours <= 23 ? hours : null;
|
|
15
|
+
if (hours < 1 || hours > 12)
|
|
16
|
+
return null;
|
|
17
|
+
const pm = ampm.toLowerCase() === "pm";
|
|
18
|
+
if (hours === 12)
|
|
19
|
+
return pm ? 12 : 0;
|
|
20
|
+
return pm ? hours + 12 : hours;
|
|
21
|
+
}
|
|
8
22
|
function parseWeeklyReset(raw, now) {
|
|
9
|
-
const m = raw.trim().match(/^(\d{1,2}):(\d{2})
|
|
23
|
+
const m = raw.trim().match(/^(\d{1,2}):(\d{2})(?:\s*([AaPp][Mm]))?\s+on\s+(\d{1,2})\s+([A-Za-z]{3,9})$/);
|
|
10
24
|
if (!m)
|
|
11
25
|
return null;
|
|
12
|
-
const hours = parseInt(m[1], 10);
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const month = MONTHS[monStr];
|
|
17
|
-
if (month === undefined || !Number.isFinite(hours) || !Number.isFinite(mins) || !Number.isFinite(day))
|
|
18
|
-
return null;
|
|
19
|
-
if (hours < 0 || hours > 23 || mins < 0 || mins > 59 || day < 1 || day > 31)
|
|
26
|
+
const hours = clockHours(parseInt(m[1], 10), parseInt(m[2], 10), m[3]);
|
|
27
|
+
const day = parseInt(m[4], 10);
|
|
28
|
+
const month = MONTHS[m[5].slice(0, 3).toLowerCase()];
|
|
29
|
+
if (hours === null || month === undefined || !Number.isFinite(day) || day < 1 || day > 31)
|
|
20
30
|
return null;
|
|
21
31
|
let year = now.getFullYear();
|
|
22
|
-
let dt = new Date(year, month, day, hours,
|
|
32
|
+
let dt = new Date(year, month, day, hours, parseInt(m[2], 10), 0, 0);
|
|
23
33
|
if (Number.isNaN(dt.getTime()))
|
|
24
34
|
return null;
|
|
25
35
|
if (dt.getTime() < now.getTime()) {
|
|
26
|
-
dt = new Date(year + 1, month, day, hours,
|
|
36
|
+
dt = new Date(year + 1, month, day, hours, parseInt(m[2], 10), 0, 0);
|
|
27
37
|
if (Number.isNaN(dt.getTime()))
|
|
28
38
|
return null;
|
|
29
39
|
}
|
|
@@ -32,19 +42,18 @@ function parseWeeklyReset(raw, now) {
|
|
|
32
42
|
return dt.toISOString();
|
|
33
43
|
}
|
|
34
44
|
function parseFiveReset(raw, now) {
|
|
35
|
-
// The 5h reset is bare ("19:38") when it falls today and
|
|
36
|
-
// ("02:43 on 15 Sep", weekly shape) otherwise; accept both.
|
|
45
|
+
// The 5h reset is bare ("19:38" or "9:43 PM") when it falls today and
|
|
46
|
+
// date-qualified ("02:43 on 15 Sep", weekly shape) otherwise; accept both.
|
|
37
47
|
const qualified = parseWeeklyReset(raw, now);
|
|
38
48
|
if (qualified)
|
|
39
49
|
return qualified;
|
|
40
|
-
const m = raw.trim().match(/^(\d{1,2}):(\d{2})
|
|
50
|
+
const m = raw.trim().match(/^(\d{1,2}):(\d{2})(?:\s*([AaPp][Mm]))?$/);
|
|
41
51
|
if (!m)
|
|
42
52
|
return null;
|
|
43
|
-
const hours = parseInt(m[1], 10);
|
|
44
|
-
|
|
45
|
-
if (hours < 0 || hours > 23 || mins < 0 || mins > 59)
|
|
53
|
+
const hours = clockHours(parseInt(m[1], 10), parseInt(m[2], 10), m[3]);
|
|
54
|
+
if (hours === null)
|
|
46
55
|
return null;
|
|
47
|
-
let dt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours,
|
|
56
|
+
let dt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, parseInt(m[2], 10), 0, 0);
|
|
48
57
|
if (dt.getTime() < now.getTime()) {
|
|
49
58
|
dt = new Date(dt.getTime() + 86400000);
|
|
50
59
|
}
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -9,13 +9,13 @@ export declare function pollAll(enabled: string[], opts?: PollAllOptions): Promi
|
|
|
9
9
|
value: unknown;
|
|
10
10
|
reason?: undefined;
|
|
11
11
|
} | {
|
|
12
|
+
value?: undefined;
|
|
12
13
|
provider: string;
|
|
13
14
|
status: "skipped";
|
|
14
15
|
reason: any;
|
|
15
|
-
value?: undefined;
|
|
16
16
|
} | {
|
|
17
|
+
value?: undefined;
|
|
17
18
|
provider: string;
|
|
18
19
|
status: "rejected";
|
|
19
20
|
reason: any;
|
|
20
|
-
value?: undefined;
|
|
21
21
|
})[]>;
|
package/dist/adapters/kimi.d.ts
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
import type { ParsedQuota } from "./types.js";
|
|
2
|
+
export declare function kimiProbeDir(): string;
|
|
2
3
|
export declare function parseKimiTui(text: string, now?: Date): ParsedQuota;
|
|
4
|
+
export declare function parseKimiApiUsage(usagesBody: unknown, meBody?: unknown, now?: Date): ParsedQuota;
|
|
5
|
+
export interface KimiStoredCreds {
|
|
6
|
+
access_token?: string;
|
|
7
|
+
refresh_token?: string;
|
|
8
|
+
expires_at?: number;
|
|
9
|
+
token_type?: string;
|
|
10
|
+
scope?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface KimiAuthContext {
|
|
13
|
+
token: string;
|
|
14
|
+
credPath?: string;
|
|
15
|
+
creds?: KimiStoredCreds;
|
|
16
|
+
deviceId?: string;
|
|
17
|
+
oauthHost: string;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
}
|
|
20
|
+
export interface KimiProviderConfig {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
oauthHost?: string;
|
|
23
|
+
key?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads the Kimi provider's own entries from config.toml. Only keys inside
|
|
27
|
+
* `[providers."managed:kimi-code"]` and its `.oauth` sub-table count; comments
|
|
28
|
+
* and every other table are ignored.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readKimiProviderConfig(content: string): KimiProviderConfig;
|
|
31
|
+
export declare function resolveKimiAuth(env?: Record<string, string | undefined>, customKimiDir?: string): KimiAuthContext | null;
|
|
32
|
+
export declare function refreshKimiOAuthToken(oauthHost: string, refreshToken: string, deviceId?: string, signal?: AbortSignal): Promise<{
|
|
33
|
+
access_token: string;
|
|
34
|
+
refresh_token?: string;
|
|
35
|
+
expires_in?: number;
|
|
36
|
+
}>;
|
|
37
|
+
/** A refresh rotated the tokens but the result could not be saved for the Kimi CLI. */
|
|
38
|
+
export declare class KimiCredentialWriteError extends Error {
|
|
39
|
+
constructor(credPath: string, cause: unknown);
|
|
40
|
+
}
|
|
41
|
+
export declare function ensureFreshKimiToken(ctx: KimiAuthContext, signal?: AbortSignal): Promise<string>;
|
|
42
|
+
export declare function pollKimiApi(ctx: KimiAuthContext): Promise<ParsedQuota>;
|
|
43
|
+
export declare function pollKimiPty(): Promise<ParsedQuota>;
|
|
3
44
|
export declare const kimiAdapter: {
|
|
4
45
|
id: string;
|
|
5
46
|
requiresAuth: string;
|
package/dist/adapters/kimi.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { parseResetText } from "./parse.js";
|
|
3
5
|
import { runPty, stripAnsi } from "./pty.js";
|
|
4
6
|
import { adapterSignal } from "../runtime/spawn.js";
|
|
7
|
+
const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
8
|
+
const DEFAULT_OAUTH_HOST = "https://auth.kimi.ai";
|
|
9
|
+
const DEFAULT_BASE_URL = "https://api.kimi.ai/coding/v1";
|
|
10
|
+
const POLL_TIMEOUT_MS = 8000;
|
|
11
|
+
export function kimiProbeDir() {
|
|
12
|
+
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "kimi-probe");
|
|
13
|
+
}
|
|
5
14
|
export function parseKimiTui(text, now = new Date()) {
|
|
6
15
|
const cleaned = stripAnsi(text);
|
|
7
16
|
const weeklyRe = /Weekly limit\s+[^0-9]*(\d+)%\s+used\s+resets\s+(in\s+[^\n│\r]+)/i;
|
|
@@ -47,6 +56,28 @@ export function parseKimiTui(text, now = new Date()) {
|
|
|
47
56
|
if (lvl)
|
|
48
57
|
plan = lvl[1].trim().toLowerCase().replace(/^level_/i, "");
|
|
49
58
|
}
|
|
59
|
+
// Optional monthly window
|
|
60
|
+
const m = cleaned.match(/Monthly limit[^\d%]*(\d+)%\s+used(?:\s+resets\s+(in\s+[^\n│\r]+|[^\n│\r]+))?/i);
|
|
61
|
+
let monthlyFields = {};
|
|
62
|
+
if (m) {
|
|
63
|
+
const monthlyPct = parseInt(m[1], 10);
|
|
64
|
+
if (Number.isFinite(monthlyPct) && monthlyPct >= 0) {
|
|
65
|
+
const exhausted = monthlyPct >= 100;
|
|
66
|
+
const rawReset = m[2]?.trim();
|
|
67
|
+
const textToParse = rawReset
|
|
68
|
+
? rawReset.startsWith("in ")
|
|
69
|
+
? `resets ${rawReset}`
|
|
70
|
+
: `resets in ${rawReset}`
|
|
71
|
+
: "";
|
|
72
|
+
const resetIso = textToParse ? (parseResetText(textToParse, now) ?? undefined) : undefined;
|
|
73
|
+
monthlyFields = {
|
|
74
|
+
monthlyKind: "included",
|
|
75
|
+
monthlyStatus: exhausted ? "exhausted" : "ok",
|
|
76
|
+
monthlyPct: exhausted ? 100 : Math.min(100, monthlyPct),
|
|
77
|
+
monthlyResetsAt: resetIso,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
50
81
|
const periodStart = new Date(new Date(weeklyIso).getTime() - 7 * 86400000).toISOString();
|
|
51
82
|
return {
|
|
52
83
|
provider: "kimi",
|
|
@@ -58,30 +89,431 @@ export function parseKimiTui(text, now = new Date()) {
|
|
|
58
89
|
source: "tui",
|
|
59
90
|
fetchedAt: now.toISOString(),
|
|
60
91
|
raw: cleaned.slice(0, 4096),
|
|
92
|
+
...monthlyFields,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Exhaustion comes from the raw ratio; only the displayed pct is rounded, and
|
|
96
|
+
// capped at 99 below full so a rounded 100 never reads as an empty window.
|
|
97
|
+
// An unreadable reset stays undefined: the monthly window is independent of
|
|
98
|
+
// the weekly one, so the weekly reset is no stand-in for it.
|
|
99
|
+
function monthlyFromRatio(ratio, resetRaw) {
|
|
100
|
+
const exhausted = ratio >= 1;
|
|
101
|
+
const resetsMs = Date.parse(String(resetRaw ?? ""));
|
|
102
|
+
return {
|
|
103
|
+
monthlyKind: "included",
|
|
104
|
+
monthlyStatus: exhausted ? "exhausted" : "ok",
|
|
105
|
+
monthlyPct: exhausted ? 100 : Math.min(99, Math.max(0, Math.round(ratio * 100))),
|
|
106
|
+
monthlyResetsAt: Number.isFinite(resetsMs) ? new Date(resetsMs).toISOString() : undefined,
|
|
61
107
|
};
|
|
62
108
|
}
|
|
109
|
+
function parseKimiApiMonthly(u, legacyUsages) {
|
|
110
|
+
// 1. Check limits array for an explicit monthly duration (>= 28 days)
|
|
111
|
+
if (Array.isArray(u.limits)) {
|
|
112
|
+
for (const item of u.limits) {
|
|
113
|
+
if (item && typeof item === "object") {
|
|
114
|
+
const win = item.window;
|
|
115
|
+
const unit = typeof win?.timeUnit === "string" ? win.timeUnit : "";
|
|
116
|
+
const dur = Number(win?.duration);
|
|
117
|
+
const isMonthly = unit === "TIME_UNIT_MONTH" ||
|
|
118
|
+
(unit === "TIME_UNIT_DAY" && dur >= 28) ||
|
|
119
|
+
(unit === "TIME_UNIT_MINUTE" && dur >= 40320);
|
|
120
|
+
if (isMonthly) {
|
|
121
|
+
const detail = item.detail;
|
|
122
|
+
if (detail && typeof detail === "object") {
|
|
123
|
+
const used = Number(detail.used);
|
|
124
|
+
const limit = Number(detail.limit);
|
|
125
|
+
if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
|
|
126
|
+
return monthlyFromRatio(used / limit, detail.resetTime);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 2. Check usages.limit_month_total (Moonshot CLI schema)
|
|
134
|
+
if (legacyUsages && typeof legacyUsages === "object") {
|
|
135
|
+
const lMonth = legacyUsages.limit_month_total;
|
|
136
|
+
if (lMonth && typeof lMonth === "object") {
|
|
137
|
+
const ratio = Number(lMonth.used_ratio);
|
|
138
|
+
if (Number.isFinite(ratio) && ratio >= 0) {
|
|
139
|
+
return monthlyFromRatio(ratio, lMonth.reset_time);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 3. Check subscriptionBalance / subscription_balance (Moonshot web gateway schema)
|
|
144
|
+
const subBal = (u.subscriptionBalance ?? u.subscription_balance);
|
|
145
|
+
if (subBal && typeof subBal === "object") {
|
|
146
|
+
const ratio = Number(subBal.amountUsedRatio ?? subBal.amount_used_ratio);
|
|
147
|
+
if (Number.isFinite(ratio) && ratio >= 0) {
|
|
148
|
+
return monthlyFromRatio(ratio, subBal.expireTime ?? subBal.expire_time);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
export function parseKimiApiUsage(usagesBody, meBody, now = new Date()) {
|
|
154
|
+
if (typeof usagesBody !== "object" || usagesBody === null) {
|
|
155
|
+
throw new Error("kimi: invalid api response");
|
|
156
|
+
}
|
|
157
|
+
const u = usagesBody;
|
|
158
|
+
// 1. Weekly window (from usage object)
|
|
159
|
+
let weeklyPct = null;
|
|
160
|
+
let resetsAt = null;
|
|
161
|
+
const usage = u.usage;
|
|
162
|
+
if (usage && typeof usage === "object") {
|
|
163
|
+
const used = Number(usage.used);
|
|
164
|
+
const limit = Number(usage.limit);
|
|
165
|
+
if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
|
|
166
|
+
weeklyPct = Math.round((used / limit) * 100);
|
|
167
|
+
}
|
|
168
|
+
if (typeof usage.resetTime === "string" && usage.resetTime.length > 0) {
|
|
169
|
+
const parsed = Date.parse(usage.resetTime);
|
|
170
|
+
if (Number.isFinite(parsed))
|
|
171
|
+
resetsAt = new Date(parsed).toISOString();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// 2. 5-hour window (from limits array)
|
|
175
|
+
let sessionPct = null;
|
|
176
|
+
if (Array.isArray(u.limits)) {
|
|
177
|
+
for (const item of u.limits) {
|
|
178
|
+
if (item && typeof item === "object") {
|
|
179
|
+
const detail = item.detail;
|
|
180
|
+
if (detail && typeof detail === "object") {
|
|
181
|
+
const used = Number(detail.used);
|
|
182
|
+
const limit = Number(detail.limit);
|
|
183
|
+
if (Number.isFinite(used) && Number.isFinite(limit) && limit > 0) {
|
|
184
|
+
sessionPct = Math.round((used / limit) * 100);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Fallback to legacy usages object if limits/usage were omitted
|
|
192
|
+
const legacyUsages = u.usages;
|
|
193
|
+
if (legacyUsages && typeof legacyUsages === "object") {
|
|
194
|
+
if (weeklyPct === null) {
|
|
195
|
+
const l7d = legacyUsages.limit_7d;
|
|
196
|
+
if (l7d && typeof l7d.used_ratio === "number") {
|
|
197
|
+
weeklyPct = Math.round(l7d.used_ratio * 100);
|
|
198
|
+
}
|
|
199
|
+
if (l7d && typeof l7d.reset_time === "string" && !resetsAt) {
|
|
200
|
+
const parsed = Date.parse(l7d.reset_time);
|
|
201
|
+
if (Number.isFinite(parsed))
|
|
202
|
+
resetsAt = new Date(parsed).toISOString();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (sessionPct === null) {
|
|
206
|
+
const l5h = legacyUsages.limit_5h;
|
|
207
|
+
if (l5h && typeof l5h.used_ratio === "number") {
|
|
208
|
+
sessionPct = Math.round(l5h.used_ratio * 100);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (weeklyPct === null || weeklyPct < 0 || weeklyPct > 100) {
|
|
213
|
+
throw new Error("kimi: weekly pct not found in api response");
|
|
214
|
+
}
|
|
215
|
+
if (sessionPct === null || sessionPct < 0 || sessionPct > 100) {
|
|
216
|
+
throw new Error("kimi: 5h pct not found in api response");
|
|
217
|
+
}
|
|
218
|
+
if (!resetsAt || Number.isNaN(new Date(resetsAt).getTime())) {
|
|
219
|
+
throw new Error("kimi: weekly reset not found in api response");
|
|
220
|
+
}
|
|
221
|
+
// 3. Plan identity (from meBody)
|
|
222
|
+
let plan = "unknown";
|
|
223
|
+
if (meBody && typeof meBody === "object") {
|
|
224
|
+
const me = meBody;
|
|
225
|
+
if (typeof me.user_level_name === "string" && me.user_level_name.trim().length > 0) {
|
|
226
|
+
plan = me.user_level_name.trim().toLowerCase();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// 4. Optional monthly quota (from limit_month_total, limits, or subscriptionBalance)
|
|
230
|
+
const monthly = parseKimiApiMonthly(u, legacyUsages);
|
|
231
|
+
const periodStart = new Date(new Date(resetsAt).getTime() - 7 * 86400000).toISOString();
|
|
232
|
+
return {
|
|
233
|
+
provider: "kimi",
|
|
234
|
+
plan,
|
|
235
|
+
weeklyPct,
|
|
236
|
+
fiveHourPct: sessionPct,
|
|
237
|
+
resetsAt,
|
|
238
|
+
periodStart,
|
|
239
|
+
source: "api",
|
|
240
|
+
fetchedAt: now.toISOString(),
|
|
241
|
+
...monthly,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const KIMI_PROVIDER_TABLE = 'providers."managed:kimi-code"';
|
|
245
|
+
/**
|
|
246
|
+
* Reads the Kimi provider's own entries from config.toml. Only keys inside
|
|
247
|
+
* `[providers."managed:kimi-code"]` and its `.oauth` sub-table count; comments
|
|
248
|
+
* and every other table are ignored.
|
|
249
|
+
*/
|
|
250
|
+
export function readKimiProviderConfig(content) {
|
|
251
|
+
const out = {};
|
|
252
|
+
let table = "";
|
|
253
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
254
|
+
const line = raw.trim();
|
|
255
|
+
if (!line || line.startsWith("#"))
|
|
256
|
+
continue;
|
|
257
|
+
if (line.startsWith("[")) {
|
|
258
|
+
const header = line.match(/^\[\s*([^[\]]+?)\s*\]\s*(?:#.*)?$/);
|
|
259
|
+
table = header ? header[1] : "";
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/);
|
|
263
|
+
if (!kv || !kv[2].trim())
|
|
264
|
+
continue;
|
|
265
|
+
const [, k, v] = kv;
|
|
266
|
+
if (table === KIMI_PROVIDER_TABLE && k === "base_url")
|
|
267
|
+
out.baseUrl = v.trim();
|
|
268
|
+
if (table === `${KIMI_PROVIDER_TABLE}.oauth` && k === "oauth_host")
|
|
269
|
+
out.oauthHost = v.trim();
|
|
270
|
+
if (table === `${KIMI_PROVIDER_TABLE}.oauth` && k === "key")
|
|
271
|
+
out.key = v.trim();
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
export function resolveKimiAuth(env = process.env, customKimiDir) {
|
|
276
|
+
const fromEnv = env.KIMI_CODE_API_KEY || env.KIMI_API_KEY;
|
|
277
|
+
if (fromEnv && fromEnv.trim()) {
|
|
278
|
+
return {
|
|
279
|
+
token: fromEnv.trim(),
|
|
280
|
+
oauthHost: env.KIMI_CODE_OAUTH_HOST ?? DEFAULT_OAUTH_HOST,
|
|
281
|
+
baseUrl: env.KIMI_CODE_BASE_URL ?? DEFAULT_BASE_URL,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const kimiDir = customKimiDir ?? path.join(os.homedir(), ".kimi-code");
|
|
285
|
+
let deviceId = "";
|
|
286
|
+
try {
|
|
287
|
+
deviceId = fs.readFileSync(path.join(kimiDir, "device_id"), "utf8").trim();
|
|
288
|
+
}
|
|
289
|
+
catch { }
|
|
290
|
+
let config = {};
|
|
291
|
+
try {
|
|
292
|
+
config = readKimiProviderConfig(fs.readFileSync(path.join(kimiDir, "config.toml"), "utf8"));
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
295
|
+
// Explicit environment overrides win; otherwise only the Kimi provider's own
|
|
296
|
+
// table supplies hosts, so a token is never paired with another provider's URL.
|
|
297
|
+
const oauthHost = env.KIMI_CODE_OAUTH_HOST ?? config.oauthHost ?? DEFAULT_OAUTH_HOST;
|
|
298
|
+
const baseUrl = env.KIMI_CODE_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL;
|
|
299
|
+
let credPath = null;
|
|
300
|
+
if (config.key) {
|
|
301
|
+
// The config names its credential: use exactly that file or nothing.
|
|
302
|
+
const candidate = path.join(kimiDir, "credentials", `${path.basename(config.key)}.json`);
|
|
303
|
+
if (!fs.existsSync(candidate))
|
|
304
|
+
return null;
|
|
305
|
+
credPath = candidate;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// No reference in config: scan credentials directory for newest .json file
|
|
309
|
+
const credsDir = path.join(kimiDir, "credentials");
|
|
310
|
+
try {
|
|
311
|
+
const files = fs.readdirSync(credsDir).filter((f) => f.endsWith(".json"));
|
|
312
|
+
if (files.length > 0) {
|
|
313
|
+
files.sort((a, b) => {
|
|
314
|
+
const sA = fs.statSync(path.join(credsDir, a)).mtimeMs;
|
|
315
|
+
const sB = fs.statSync(path.join(credsDir, b)).mtimeMs;
|
|
316
|
+
return sB - sA;
|
|
317
|
+
});
|
|
318
|
+
credPath = path.join(credsDir, files[0]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
}
|
|
323
|
+
if (!credPath)
|
|
324
|
+
return null;
|
|
325
|
+
try {
|
|
326
|
+
const creds = JSON.parse(fs.readFileSync(credPath, "utf8"));
|
|
327
|
+
const hasToken = (typeof creds.access_token === "string" && creds.access_token.trim().length > 0) ||
|
|
328
|
+
(typeof creds.refresh_token === "string" && creds.refresh_token.trim().length > 0);
|
|
329
|
+
if (hasToken) {
|
|
330
|
+
return {
|
|
331
|
+
token: creds.access_token?.trim() ?? "",
|
|
332
|
+
credPath,
|
|
333
|
+
creds,
|
|
334
|
+
deviceId: deviceId || undefined,
|
|
335
|
+
oauthHost,
|
|
336
|
+
baseUrl,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch { }
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
export async function refreshKimiOAuthToken(oauthHost, refreshToken, deviceId, signal) {
|
|
344
|
+
const url = `${oauthHost.replace(/\/+$/, "")}/api/oauth/token`;
|
|
345
|
+
const body = new URLSearchParams({
|
|
346
|
+
client_id: CLIENT_ID,
|
|
347
|
+
grant_type: "refresh_token",
|
|
348
|
+
refresh_token: refreshToken,
|
|
349
|
+
});
|
|
350
|
+
const headers = {
|
|
351
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
352
|
+
Accept: "application/json",
|
|
353
|
+
"User-Agent": "kimi-code-cli/2.0.2",
|
|
354
|
+
"X-Msh-Version": "2.0.2",
|
|
355
|
+
};
|
|
356
|
+
if (deviceId)
|
|
357
|
+
headers["X-Msh-Device-Id"] = deviceId;
|
|
358
|
+
const res = await fetch(url, {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers,
|
|
361
|
+
body: body.toString(),
|
|
362
|
+
signal,
|
|
363
|
+
});
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
throw new Error(`kimi: token refresh failed (HTTP ${res.status})`);
|
|
366
|
+
}
|
|
367
|
+
const data = (await res.json());
|
|
368
|
+
if (typeof data.access_token !== "string" || !data.access_token.trim()) {
|
|
369
|
+
throw new Error("kimi: refresh response missing access_token");
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
access_token: data.access_token.trim(),
|
|
373
|
+
refresh_token: typeof data.refresh_token === "string" && data.refresh_token.trim() ? data.refresh_token.trim() : undefined,
|
|
374
|
+
expires_in: typeof data.expires_in === "number" ? data.expires_in : undefined,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** A refresh rotated the tokens but the result could not be saved for the Kimi CLI. */
|
|
378
|
+
export class KimiCredentialWriteError extends Error {
|
|
379
|
+
constructor(credPath, cause) {
|
|
380
|
+
super(`kimi: refreshed token could not be saved to ${credPath} (${cause?.message ?? cause}); run \`kimi login\``);
|
|
381
|
+
this.name = "KimiCredentialWriteError";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function readCredsFile(credPath) {
|
|
385
|
+
try {
|
|
386
|
+
const raw = fs.readFileSync(credPath, "utf8");
|
|
387
|
+
return { raw, creds: JSON.parse(raw) };
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
export async function ensureFreshKimiToken(ctx, signal) {
|
|
394
|
+
if (!ctx.credPath || !ctx.creds)
|
|
395
|
+
return ctx.token;
|
|
396
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
397
|
+
const isFresh = (c) => !!c.access_token && (c.expires_at ?? 0) > nowSec + 60;
|
|
398
|
+
if (isFresh(ctx.creds)) {
|
|
399
|
+
return ctx.creds.access_token;
|
|
400
|
+
}
|
|
401
|
+
// The Kimi CLI owns this file: re-read it, and adopt a token it refreshed since we loaded it.
|
|
402
|
+
const before = readCredsFile(ctx.credPath);
|
|
403
|
+
if (before && before.creds.access_token !== ctx.creds.access_token && isFresh(before.creds)) {
|
|
404
|
+
ctx.creds = before.creds;
|
|
405
|
+
ctx.token = before.creds.access_token;
|
|
406
|
+
return ctx.token;
|
|
407
|
+
}
|
|
408
|
+
const creds = before?.creds ?? ctx.creds;
|
|
409
|
+
if (!creds.refresh_token) {
|
|
410
|
+
if (creds.access_token)
|
|
411
|
+
return creds.access_token;
|
|
412
|
+
throw new Error("kimi: missing refresh_token");
|
|
413
|
+
}
|
|
414
|
+
const refreshed = await refreshKimiOAuthToken(ctx.oauthHost, creds.refresh_token, ctx.deviceId, signal);
|
|
415
|
+
const next = {
|
|
416
|
+
...creds,
|
|
417
|
+
access_token: refreshed.access_token,
|
|
418
|
+
refresh_token: refreshed.refresh_token ?? creds.refresh_token,
|
|
419
|
+
expires_at: nowSec + (refreshed.expires_in ?? 900),
|
|
420
|
+
};
|
|
421
|
+
ctx.creds = next;
|
|
422
|
+
ctx.token = refreshed.access_token;
|
|
423
|
+
// Another writer changed the file during the refresh: keep its copy rather than overwrite it.
|
|
424
|
+
if (readCredsFile(ctx.credPath)?.raw !== before?.raw)
|
|
425
|
+
return ctx.token;
|
|
426
|
+
// Atomic replace: write a private temp file beside the original, then rename over it.
|
|
427
|
+
const tmp = `${ctx.credPath}.quotacap-${process.pid}.tmp`;
|
|
428
|
+
try {
|
|
429
|
+
fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
430
|
+
fs.renameSync(tmp, ctx.credPath);
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
try {
|
|
434
|
+
fs.rmSync(tmp, { force: true });
|
|
435
|
+
}
|
|
436
|
+
catch { }
|
|
437
|
+
throw new KimiCredentialWriteError(ctx.credPath, e);
|
|
438
|
+
}
|
|
439
|
+
return ctx.token;
|
|
440
|
+
}
|
|
441
|
+
export async function pollKimiApi(ctx) {
|
|
442
|
+
const adapter = adapterSignal("kimi");
|
|
443
|
+
const timeout = AbortSignal.timeout(POLL_TIMEOUT_MS);
|
|
444
|
+
const signal = adapter ? AbortSignal.any([adapter, timeout]) : timeout;
|
|
445
|
+
const token = await ensureFreshKimiToken(ctx, signal);
|
|
446
|
+
const headers = {
|
|
447
|
+
Authorization: `Bearer ${token}`,
|
|
448
|
+
Accept: "application/json",
|
|
449
|
+
"User-Agent": "kimi-code-cli/2.0.2",
|
|
450
|
+
"X-Msh-Version": "2.0.2",
|
|
451
|
+
};
|
|
452
|
+
if (ctx.deviceId)
|
|
453
|
+
headers["X-Msh-Device-Id"] = ctx.deviceId;
|
|
454
|
+
const usagesUrl = `${ctx.baseUrl.replace(/\/+$/, "")}/usages`;
|
|
455
|
+
let usagesRes = await fetch(usagesUrl, { headers, signal });
|
|
456
|
+
// On 401, attempt one forced refresh if refresh_token is available
|
|
457
|
+
if (usagesRes.status === 401 && ctx.credPath && ctx.creds?.refresh_token) {
|
|
458
|
+
ctx.creds.expires_at = 0;
|
|
459
|
+
const freshToken = await ensureFreshKimiToken(ctx, signal);
|
|
460
|
+
headers.Authorization = `Bearer ${freshToken}`;
|
|
461
|
+
usagesRes = await fetch(usagesUrl, { headers, signal });
|
|
462
|
+
}
|
|
463
|
+
if (!usagesRes.ok) {
|
|
464
|
+
throw new Error(`kimi: usage request failed (HTTP ${usagesRes.status})`);
|
|
465
|
+
}
|
|
466
|
+
const usagesBody = await usagesRes.json();
|
|
467
|
+
let meBody = undefined;
|
|
468
|
+
try {
|
|
469
|
+
const meUrl = `${ctx.baseUrl.replace(/\/+$/, "")}/me`;
|
|
470
|
+
const meRes = await fetch(meUrl, { headers, signal });
|
|
471
|
+
if (meRes.ok) {
|
|
472
|
+
meBody = await meRes.json();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch { }
|
|
476
|
+
return parseKimiApiUsage(usagesBody, meBody);
|
|
477
|
+
}
|
|
478
|
+
export async function pollKimiPty() {
|
|
479
|
+
const cwd = kimiProbeDir();
|
|
480
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
481
|
+
const transcript = await runPty({
|
|
482
|
+
file: "kimi",
|
|
483
|
+
args: [],
|
|
484
|
+
cols: 140,
|
|
485
|
+
rows: 35,
|
|
486
|
+
cwd,
|
|
487
|
+
readyRegex: /Welcome to Kimi Code|context:/i,
|
|
488
|
+
readyTimeoutMs: 8000,
|
|
489
|
+
preReadyResponse: {
|
|
490
|
+
match: /Trust this folder\?/i,
|
|
491
|
+
reply: "\r",
|
|
492
|
+
},
|
|
493
|
+
input: "/usage\r",
|
|
494
|
+
completionRegex: /Weekly limit[\s\S]*?5h limit|5h limit[\s\S]*?Weekly limit/i,
|
|
495
|
+
timeoutMs: 8000,
|
|
496
|
+
maxBytes: 256 * 1024,
|
|
497
|
+
signal: adapterSignal("kimi"),
|
|
498
|
+
label: "kimi",
|
|
499
|
+
});
|
|
500
|
+
return parseKimiTui(transcript);
|
|
501
|
+
}
|
|
63
502
|
export const kimiAdapter = {
|
|
64
503
|
id: "kimi",
|
|
65
504
|
requiresAuth: "kimi login (CLI owns credentials)",
|
|
66
505
|
async poll() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
abortOn: /Trust this folder\?/i,
|
|
80
|
-
timeoutMs: 8000,
|
|
81
|
-
maxBytes: 256 * 1024,
|
|
82
|
-
signal: adapterSignal("kimi"),
|
|
83
|
-
label: "kimi",
|
|
84
|
-
});
|
|
85
|
-
return parseKimiTui(transcript);
|
|
506
|
+
const authCtx = resolveKimiAuth();
|
|
507
|
+
if (authCtx) {
|
|
508
|
+
try {
|
|
509
|
+
return await pollKimiApi(authCtx);
|
|
510
|
+
}
|
|
511
|
+
catch (e) {
|
|
512
|
+
// A lost rotated token leaves the CLI logged out: report it, do not mask it with the PTY path.
|
|
513
|
+
if (e instanceof KimiCredentialWriteError)
|
|
514
|
+
throw e;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return pollKimiPty();
|
|
86
518
|
},
|
|
87
519
|
};
|
package/dist/adapters/muse.js
CHANGED
|
@@ -177,7 +177,7 @@ export const museAdapter = {
|
|
|
177
177
|
unavailable: e,
|
|
178
178
|
usagePass: (timeoutMs) => usagePass(timeoutMs),
|
|
179
179
|
warmTurn: async (timeoutMs) => {
|
|
180
|
-
await trackedExecFile("muse", "muse", ["exec", "
|
|
180
|
+
await trackedExecFile("muse", "muse", ["exec", "1+1"], {
|
|
181
181
|
cwd: museProbeDir(),
|
|
182
182
|
env: { ...process.env, MUSE_NO_AUTO_UPDATE: "1" },
|
|
183
183
|
timeout: timeoutMs,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParsedQuota } from "./types.js";
|
|
2
|
-
export declare const OPENCODE_GO_CONSENT_NOTICE
|
|
2
|
+
export declare const OPENCODE_GO_CONSENT_NOTICE: string;
|
|
3
3
|
export declare function openCodeGoAuthPath(): string;
|
|
4
4
|
/** Detection is existence only — no content read before enablement. */
|
|
5
5
|
export declare function opencodeGoDetected(): boolean;
|