pi-provider-kimi-code 0.6.1 → 0.6.3
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/index.ts +277 -466
- package/package.json +4 -4
- package/src/config.ts +16 -5
- package/src/constants.ts +7 -2
- package/src/device.ts +2 -2
- package/src/project-trust.ts +24 -0
- package/src/settings-ui.ts +204 -0
- package/src/usage.ts +274 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-provider",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"pre-commit": "prek run"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
55
|
-
"@earendil-works/pi-tui": "^0.
|
|
53
|
+
"@earendil-works/pi-ai": "^0.79.10",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.79.10",
|
|
55
|
+
"@earendil-works/pi-tui": "^0.79.10",
|
|
56
56
|
"@j178/prek": "^0.4.4",
|
|
57
57
|
"@types/node": "^24.0.0",
|
|
58
58
|
"oxfmt": "^0.44.0",
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
|
|
|
9
9
|
|
|
10
10
|
export type KimiConfigSource = "runtime" | "env" | "project" | "home" | "default";
|
|
11
11
|
|
|
12
|
+
export const PI_CONFIG_DIR_NAME = ".pi";
|
|
13
|
+
|
|
12
14
|
export type KimiInputModality = "text" | "image" | "video";
|
|
13
15
|
|
|
14
16
|
export interface ModelReasoningEntry {
|
|
@@ -83,6 +85,7 @@ export interface LoadKimiCodeConfigOptions {
|
|
|
83
85
|
cwd: string;
|
|
84
86
|
home: string;
|
|
85
87
|
env?: NodeJS.ProcessEnv;
|
|
88
|
+
includeProject?: boolean;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export class ConfigError extends Error {
|
|
@@ -147,11 +150,11 @@ export function getRuntimeKimiCodeConfigOverride(): KimiCodeConfigPatch {
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
export function getGlobalKimiCodeConfigPath(home: string): string {
|
|
150
|
-
return join(home,
|
|
153
|
+
return join(home, PI_CONFIG_DIR_NAME, "providers", PROVIDER_ID, "config.json");
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export function getProjectKimiCodeConfigPath(cwd: string): string {
|
|
154
|
-
return join(cwd,
|
|
157
|
+
return join(cwd, PI_CONFIG_DIR_NAME, "providers", PROVIDER_ID, "config.json");
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
export function kimiCodeConfigPath(home: string): string {
|
|
@@ -483,15 +486,23 @@ function buildSources(
|
|
|
483
486
|
function loadLayers(
|
|
484
487
|
options: LoadKimiCodeConfigOptions,
|
|
485
488
|
): Array<{ source: KimiConfigSource; config: Record<string, unknown> }> {
|
|
486
|
-
|
|
489
|
+
const layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }> = [
|
|
487
490
|
{ source: "home", config: readConfigFile(getGlobalKimiCodeConfigPath(options.home)) },
|
|
488
|
-
|
|
491
|
+
];
|
|
492
|
+
if (options.includeProject !== false) {
|
|
493
|
+
layers.push({
|
|
494
|
+
source: "project",
|
|
495
|
+
config: readConfigFile(getProjectKimiCodeConfigPath(options.cwd)),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
layers.push(
|
|
489
499
|
{
|
|
490
500
|
source: "env",
|
|
491
501
|
config: envConfigPatch(options.env ?? process.env) as Record<string, unknown>,
|
|
492
502
|
},
|
|
493
503
|
{ source: "runtime", config: runtimeOverride as Record<string, unknown> },
|
|
494
|
-
|
|
504
|
+
);
|
|
505
|
+
return layers;
|
|
495
506
|
}
|
|
496
507
|
|
|
497
508
|
export function loadKimiCodeConfig(
|
package/src/constants.ts
CHANGED
|
@@ -41,8 +41,13 @@ export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
|
|
|
41
41
|
|
|
42
42
|
export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
|
|
43
43
|
|
|
44
|
-
export const
|
|
45
|
-
|
|
44
|
+
export const PROVIDER_VERSION = "0.6.3";
|
|
45
|
+
|
|
46
|
+
// Upstream kimi-code CLI version — used in User-Agent and X-Msh-Version
|
|
47
|
+
// headers to match the official client's identity. Update this when
|
|
48
|
+
// syncing with https://github.com/anthropics/kimi-code.
|
|
49
|
+
export const KIMI_UPSTREAM_VERSION = "0.18.0";
|
|
50
|
+
export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_UPSTREAM_VERSION}`;
|
|
46
51
|
export const KIMI_PLATFORM = "kimi_code_cli";
|
|
47
52
|
|
|
48
53
|
export function getKimiCodeHome(): string {
|
package/src/device.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { dirname } from "node:path";
|
|
|
11
11
|
import {
|
|
12
12
|
DEVICE_ID_PATH,
|
|
13
13
|
KIMI_CODE_USER_AGENT,
|
|
14
|
-
|
|
14
|
+
KIMI_UPSTREAM_VERSION,
|
|
15
15
|
KIMI_PLATFORM,
|
|
16
16
|
} from "./constants.ts";
|
|
17
17
|
|
|
@@ -156,7 +156,7 @@ export function getCommonHeaders(): Record<string, string> {
|
|
|
156
156
|
const headers = {
|
|
157
157
|
"User-Agent": KIMI_CODE_USER_AGENT,
|
|
158
158
|
"X-Msh-Platform": KIMI_PLATFORM,
|
|
159
|
-
"X-Msh-Version":
|
|
159
|
+
"X-Msh-Version": KIMI_UPSTREAM_VERSION,
|
|
160
160
|
"X-Msh-Device-Name": os.hostname(),
|
|
161
161
|
"X-Msh-Device-Model": DEVICE_MODEL,
|
|
162
162
|
"X-Msh-Os-Version": getOsVersion(),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function isKimiProjectConfigApproved(ctx: unknown, cwd: string): Promise<boolean> {
|
|
2
|
+
const check = (ctx as { isProjectTrusted?: () => boolean } | undefined)?.isProjectTrusted;
|
|
3
|
+
if (typeof check !== "function") return true;
|
|
4
|
+
|
|
5
|
+
let sessionTrusted: boolean;
|
|
6
|
+
try {
|
|
7
|
+
sessionTrusted = check.call(ctx);
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (!sessionTrusted) return false;
|
|
12
|
+
|
|
13
|
+
const mod = (await import("@earendil-works/pi-coding-agent")) as unknown as {
|
|
14
|
+
ProjectTrustStore?: new (agentDir: string) => { get(cwd: string): boolean | null };
|
|
15
|
+
getAgentDir?: () => string;
|
|
16
|
+
};
|
|
17
|
+
if (!mod.ProjectTrustStore || !mod.getAgentDir) return true;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return new mod.ProjectTrustStore(mod.getAgentDir()).get(cwd) === true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { SettingsListTheme } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type KimiCodeConfig,
|
|
8
|
+
type KimiCodeConfigSources,
|
|
9
|
+
KIMI_TOOL_NAMES,
|
|
10
|
+
type KimiToolName,
|
|
11
|
+
getGlobalKimiCodeConfigPath,
|
|
12
|
+
getProjectKimiCodeConfigPath,
|
|
13
|
+
} from "./config.ts";
|
|
14
|
+
import { PROVIDER_VERSION } from "./constants.ts";
|
|
15
|
+
import type { KimiOAuthExtras } from "./models.ts";
|
|
16
|
+
|
|
17
|
+
export type KimiConfigScope = "project" | "home";
|
|
18
|
+
|
|
19
|
+
export function moonshotStatus(config: KimiCodeConfig): string {
|
|
20
|
+
return KIMI_TOOL_NAMES.map((toolName) => {
|
|
21
|
+
const tool = config.tools[toolName];
|
|
22
|
+
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
23
|
+
const collapsed = tool.default_collapsed ? "collapsed" : "expanded";
|
|
24
|
+
return `${toolName}: ${enabled}, default ${collapsed}`;
|
|
25
|
+
}).join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toggleEnabled(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
|
|
29
|
+
return {
|
|
30
|
+
...config,
|
|
31
|
+
tools: {
|
|
32
|
+
...config.tools,
|
|
33
|
+
[toolName]: {
|
|
34
|
+
...config.tools[toolName],
|
|
35
|
+
enabled: !config.tools[toolName].enabled,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toggleCollapsed(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
tools: {
|
|
45
|
+
...config.tools,
|
|
46
|
+
[toolName]: {
|
|
47
|
+
...config.tools[toolName],
|
|
48
|
+
default_collapsed: !config.tools[toolName].default_collapsed,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setProtocol(
|
|
55
|
+
config: KimiCodeConfig,
|
|
56
|
+
protocol: KimiCodeConfig["protocol"],
|
|
57
|
+
): KimiCodeConfig {
|
|
58
|
+
return { ...config, protocol };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function setUploadThreshold(config: KimiCodeConfig, thresholdBytes: number): KimiCodeConfig {
|
|
62
|
+
return { ...config, uploads: { ...config.uploads, thresholdBytes } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildKimiMainTitle(
|
|
66
|
+
config: KimiCodeConfig,
|
|
67
|
+
extras: KimiOAuthExtras,
|
|
68
|
+
sources: KimiCodeConfigSources,
|
|
69
|
+
): string {
|
|
70
|
+
const modelName = extras.modelDisplay || "kimi-for-coding";
|
|
71
|
+
return [
|
|
72
|
+
`Kimi settings (provider v${PROVIDER_VERSION})`,
|
|
73
|
+
"",
|
|
74
|
+
`Model: ${modelName}`,
|
|
75
|
+
`Protocol: ${config.protocol} (${sources.protocol})`,
|
|
76
|
+
`Upload threshold: ${formatByteSize(config.uploads.thresholdBytes)} (${sources.uploads.thresholdBytes})`,
|
|
77
|
+
"",
|
|
78
|
+
"Effective tools:",
|
|
79
|
+
...KIMI_TOOL_NAMES.map((toolName) => {
|
|
80
|
+
const enabled = config.tools[toolName].enabled ? "enabled" : "disabled";
|
|
81
|
+
return `- ${toolName}: ${enabled} (${sources.tools[toolName].enabled})`;
|
|
82
|
+
}),
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildConfigScopeTitle(
|
|
87
|
+
scope: KimiConfigScope,
|
|
88
|
+
config: KimiCodeConfig,
|
|
89
|
+
filePath: string,
|
|
90
|
+
): string {
|
|
91
|
+
return [
|
|
92
|
+
`Edit ${scope} config`,
|
|
93
|
+
`File: ${filePath}`,
|
|
94
|
+
"",
|
|
95
|
+
formatProtocolStatus(config),
|
|
96
|
+
formatUploadThresholdStatus(config),
|
|
97
|
+
"",
|
|
98
|
+
moonshotStatus(config),
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function buildSettingsTheme(theme: Theme): SettingsListTheme {
|
|
103
|
+
return {
|
|
104
|
+
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
|
|
105
|
+
value: (text: string, selected: boolean) =>
|
|
106
|
+
selected ? theme.bold(theme.fg("accent", text)) : theme.fg("muted", text),
|
|
107
|
+
description: (text: string) => theme.fg("dim", text),
|
|
108
|
+
cursor: theme.fg("accent", "> "),
|
|
109
|
+
hint: (text: string) => theme.fg("dim", text),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatScopeDescription(
|
|
114
|
+
scope: KimiConfigScope,
|
|
115
|
+
cwd: string,
|
|
116
|
+
home = os.homedir(),
|
|
117
|
+
): string {
|
|
118
|
+
const filePath =
|
|
119
|
+
scope === "project" ? getProjectKimiCodeConfigPath(cwd) : getGlobalKimiCodeConfigPath(home);
|
|
120
|
+
const displayPath = scope === "home" ? homeRelative(filePath, home) : relative(cwd, filePath);
|
|
121
|
+
return `Writes to the ${scope} config file: ${displayPath}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function homeRelative(filePath: string, home = os.homedir()): string {
|
|
125
|
+
const normalizedHome = home.replace(/[\\/]+$/, "");
|
|
126
|
+
if (filePath === normalizedHome) return "~";
|
|
127
|
+
|
|
128
|
+
const slashPrefix = `${normalizedHome}/`;
|
|
129
|
+
if (filePath.startsWith(slashPrefix)) return `~/${filePath.slice(slashPrefix.length)}`;
|
|
130
|
+
|
|
131
|
+
const backslashPrefix = `${normalizedHome}\\`;
|
|
132
|
+
if (filePath.startsWith(backslashPrefix)) return `~\\${filePath.slice(backslashPrefix.length)}`;
|
|
133
|
+
|
|
134
|
+
return filePath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function toolMenuItem(config: KimiCodeConfig, toolName: KimiToolName): string {
|
|
138
|
+
return `${toolName} -> ${formatToolStatus(config, toolName)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function protocolMenuItem(config: KimiCodeConfig): string {
|
|
142
|
+
return `Protocol -> ${config.protocol}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function uploadThresholdMenuItem(config: KimiCodeConfig): string {
|
|
146
|
+
return `Upload threshold -> ${formatByteSize(config.uploads.thresholdBytes)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatByteSize(bytes: number): string {
|
|
150
|
+
const units = [
|
|
151
|
+
{ suffix: "GiB", size: 1024 ** 3 },
|
|
152
|
+
{ suffix: "MiB", size: 1024 ** 2 },
|
|
153
|
+
{ suffix: "KiB", size: 1024 },
|
|
154
|
+
];
|
|
155
|
+
for (const unit of units) {
|
|
156
|
+
if (bytes >= unit.size && bytes % unit.size === 0) {
|
|
157
|
+
return `${bytes / unit.size} ${unit.suffix}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const unit of units) {
|
|
161
|
+
if (bytes >= unit.size) {
|
|
162
|
+
return `${(bytes / unit.size).toFixed(2).replace(/\.00$/, "")} ${unit.suffix}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return `${bytes} B`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function parseByteSizeInput(input: string): number | undefined {
|
|
169
|
+
const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(b|bytes?|kib|kb|mib|mb|gib|gb)?$/i);
|
|
170
|
+
if (!match) return undefined;
|
|
171
|
+
const value = Number(match[1]);
|
|
172
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
173
|
+
const unit = (match[2] ?? "mib").toLowerCase();
|
|
174
|
+
const multipliers: Record<string, number> = {
|
|
175
|
+
b: 1,
|
|
176
|
+
byte: 1,
|
|
177
|
+
bytes: 1,
|
|
178
|
+
kib: 1024,
|
|
179
|
+
kb: 1000,
|
|
180
|
+
mib: 1024 ** 2,
|
|
181
|
+
mb: 1000 ** 2,
|
|
182
|
+
gib: 1024 ** 3,
|
|
183
|
+
gb: 1000 ** 3,
|
|
184
|
+
};
|
|
185
|
+
const multiplier = multipliers[unit];
|
|
186
|
+
if (!multiplier) return undefined;
|
|
187
|
+
const bytes = Math.round(value * multiplier);
|
|
188
|
+
return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function formatProtocolStatus(config: KimiCodeConfig): string {
|
|
192
|
+
return `protocol: ${config.protocol}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function formatUploadThresholdStatus(config: KimiCodeConfig): string {
|
|
196
|
+
return `upload threshold: ${formatByteSize(config.uploads.thresholdBytes)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatToolStatus(config: KimiCodeConfig, toolName: KimiToolName): string {
|
|
200
|
+
const tool = config.tools[toolName];
|
|
201
|
+
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
202
|
+
const collapsed = tool.default_collapsed ? "default collapsed" : "default expanded";
|
|
203
|
+
return `${enabled}, ${collapsed}`;
|
|
204
|
+
}
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
|
|
4
|
+
import { getCommonHeaders } from "./device.ts";
|
|
5
|
+
import { refreshKimiAuthToken } from "./oauth.ts";
|
|
6
|
+
|
|
7
|
+
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
8
|
+
LEVEL_FREE: "Free",
|
|
9
|
+
LEVEL_BASIC: "Adagio",
|
|
10
|
+
LEVEL_STANDARD: "Moderato",
|
|
11
|
+
LEVEL_INTERMEDIATE: "Allegretto",
|
|
12
|
+
LEVEL_ADVANCED: "Allegro",
|
|
13
|
+
LEVEL_PREMIUM: "Vivace",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const RESET_TIME_KEYS = [
|
|
17
|
+
"resetTime",
|
|
18
|
+
"reset_time",
|
|
19
|
+
"resetAt",
|
|
20
|
+
"reset_at",
|
|
21
|
+
"resetsAt",
|
|
22
|
+
"resets_at",
|
|
23
|
+
"nextResetTime",
|
|
24
|
+
"next_reset_time",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export interface UsageRow {
|
|
28
|
+
label: string;
|
|
29
|
+
used: number;
|
|
30
|
+
limit: number;
|
|
31
|
+
resetTime?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UsageFormatOptions {
|
|
35
|
+
now?: Date;
|
|
36
|
+
timeZone?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getKimiUsageToken(): string | null {
|
|
40
|
+
const credential = AuthStorage.create().get(PROVIDER_ID);
|
|
41
|
+
if (credential?.type === "oauth" && credential.access) return credential.access;
|
|
42
|
+
const apiKey = process.env.KIMI_API_KEY?.trim();
|
|
43
|
+
return apiKey || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toNumber(value: unknown): number | null {
|
|
47
|
+
const number = Number(value);
|
|
48
|
+
return Number.isFinite(number) ? number : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toStringValue(value: unknown): string | undefined {
|
|
52
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
53
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getResetTime(record: Record<string, unknown>): string | undefined {
|
|
58
|
+
for (const key of RESET_TIME_KEYS) {
|
|
59
|
+
const value = toStringValue(record[key]);
|
|
60
|
+
if (value) return value;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseUsageRow(value: unknown, fallbackLabel: string): UsageRow | null {
|
|
66
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
67
|
+
const record = value as Record<string, unknown>;
|
|
68
|
+
const limit = toNumber(record.limit);
|
|
69
|
+
const usedValue = toNumber(record.used);
|
|
70
|
+
const remaining = toNumber(record.remaining);
|
|
71
|
+
const used = usedValue ?? (limit !== null && remaining !== null ? limit - remaining : null);
|
|
72
|
+
if (limit === null && used === null) return null;
|
|
73
|
+
return {
|
|
74
|
+
label: String(record.name || record.title || fallbackLabel),
|
|
75
|
+
used: used ?? 0,
|
|
76
|
+
limit: limit ?? 0,
|
|
77
|
+
...(getResetTime(record) ? { resetTime: getResetTime(record) } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseUsageSummary(payload: unknown, options: UsageFormatOptions = {}): string {
|
|
82
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
83
|
+
return "Usage: unavailable";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const record = payload as Record<string, unknown>;
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
const membership = parseMembership(record);
|
|
89
|
+
if (membership) lines.push(membership, "");
|
|
90
|
+
|
|
91
|
+
const summary = parseUsageRow(record.usage, "Current week");
|
|
92
|
+
if (summary)
|
|
93
|
+
lines.push(formatUsageRow({ ...summary, label: normalizeUsageLabel(summary.label) }, options));
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(record.limits)) {
|
|
96
|
+
for (const [index, item] of record.limits.entries()) {
|
|
97
|
+
const itemRecord =
|
|
98
|
+
typeof item === "object" && item !== null && !Array.isArray(item)
|
|
99
|
+
? (item as Record<string, unknown>)
|
|
100
|
+
: undefined;
|
|
101
|
+
const detail = itemRecord ? (itemRecord.detail ?? itemRecord) : item;
|
|
102
|
+
const fallbackLabel = itemRecord
|
|
103
|
+
? formatWindowLabel(
|
|
104
|
+
itemRecord.window,
|
|
105
|
+
index === 0 ? "Current 5h window" : `Limit #${index + 1}`,
|
|
106
|
+
)
|
|
107
|
+
: index === 0
|
|
108
|
+
? "Current 5h window"
|
|
109
|
+
: `Limit #${index + 1}`;
|
|
110
|
+
const row = parseUsageRow(detail, fallbackLabel);
|
|
111
|
+
if (row) {
|
|
112
|
+
if (lines.length > 0) lines.push("");
|
|
113
|
+
lines.push(formatUsageRow(row, options));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
while (lines.at(-1) === "") lines.pop();
|
|
119
|
+
return lines.length === 0 ? "Usage: no usage data" : lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeUsageLabel(label: string): string {
|
|
123
|
+
return /weekly|week/i.test(label) ? "Current week" : label;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatWindowLabel(value: unknown, fallbackLabel: string): string {
|
|
127
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fallbackLabel;
|
|
128
|
+
const record = value as Record<string, unknown>;
|
|
129
|
+
const duration = toNumber(record.duration);
|
|
130
|
+
const unit = String(record.timeUnit ?? record.time_unit ?? "").toUpperCase();
|
|
131
|
+
if (!duration || !unit) return fallbackLabel;
|
|
132
|
+
|
|
133
|
+
const minutes = unit.includes("HOUR")
|
|
134
|
+
? duration * 60
|
|
135
|
+
: unit.includes("MINUTE")
|
|
136
|
+
? duration
|
|
137
|
+
: undefined;
|
|
138
|
+
if (!minutes) return fallbackLabel;
|
|
139
|
+
if (minutes % 60 === 0) return `Current ${minutes / 60}h window`;
|
|
140
|
+
return `Current ${minutes}m window`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function parseMembership(record: Record<string, unknown>): string | null {
|
|
144
|
+
const user = record.user;
|
|
145
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
146
|
+
const membership = (user as Record<string, unknown>).membership;
|
|
147
|
+
if (typeof membership !== "object" || membership === null || Array.isArray(membership)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const level = (membership as Record<string, unknown>).level;
|
|
151
|
+
if (typeof level !== "string" || !level) return null;
|
|
152
|
+
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
153
|
+
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatUsageRow(row: UsageRow, options: UsageFormatOptions = {}): string {
|
|
157
|
+
if (row.limit <= 0) return `${row.label}\n${row.used} used`;
|
|
158
|
+
const used = Math.max(0, Math.min(row.used, row.limit));
|
|
159
|
+
const percent = Math.round((used / row.limit) * 100);
|
|
160
|
+
const lines = [row.label, `${quotaBar(used, row.limit)} ${percent}% used`];
|
|
161
|
+
const reset = formatResetTime(row.resetTime, options);
|
|
162
|
+
if (reset) lines.push(`Resets ${reset}`);
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function formatResetTime(value: unknown, options: UsageFormatOptions = {}): string | null {
|
|
167
|
+
const text = toStringValue(value);
|
|
168
|
+
if (!text) return null;
|
|
169
|
+
const date = parseDate(text);
|
|
170
|
+
if (!date) return null;
|
|
171
|
+
|
|
172
|
+
const now = options.now ?? new Date();
|
|
173
|
+
const timeZone = options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
|
|
174
|
+
const sameDay = datePart(date, timeZone) === datePart(now, timeZone);
|
|
175
|
+
const time = formatTime(date, timeZone);
|
|
176
|
+
const prefix = sameDay ? "" : `${formatMonthDay(date, timeZone)} at `;
|
|
177
|
+
return `${prefix}${time} (${timeZone})`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseDate(value: string): Date | null {
|
|
181
|
+
const timestamp = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
182
|
+
const date = Number.isFinite(timestamp)
|
|
183
|
+
? new Date(timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp)
|
|
184
|
+
: new Date(value);
|
|
185
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function datePart(date: Date, timeZone: string): string {
|
|
189
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
190
|
+
day: "2-digit",
|
|
191
|
+
month: "2-digit",
|
|
192
|
+
timeZone,
|
|
193
|
+
year: "numeric",
|
|
194
|
+
}).format(date);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatMonthDay(date: Date, timeZone: string): string {
|
|
198
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
199
|
+
day: "numeric",
|
|
200
|
+
month: "short",
|
|
201
|
+
timeZone,
|
|
202
|
+
}).format(date);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function formatTime(date: Date, timeZone: string): string {
|
|
206
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
207
|
+
hour: "numeric",
|
|
208
|
+
hour12: true,
|
|
209
|
+
minute: "2-digit",
|
|
210
|
+
timeZone,
|
|
211
|
+
}).formatToParts(date);
|
|
212
|
+
const hour = parts.find((part) => part.type === "hour")?.value ?? "";
|
|
213
|
+
const minute = parts.find((part) => part.type === "minute")?.value ?? "";
|
|
214
|
+
const dayPeriod = parts.find((part) => part.type === "dayPeriod")?.value.toLowerCase() ?? "";
|
|
215
|
+
return `${hour}:${minute}${dayPeriod}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function quotaBar(used: number, limit: number): string {
|
|
219
|
+
const width = 50;
|
|
220
|
+
const ratio = Math.max(0, Math.min(1, used / limit));
|
|
221
|
+
const filled = Math.floor(ratio * width);
|
|
222
|
+
const partial = partialBlock(ratio * width - filled);
|
|
223
|
+
const empty = width - filled - (partial ? 1 : 0);
|
|
224
|
+
return `${"█".repeat(filled)}${partial}${" ".repeat(Math.max(0, empty))}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function partialBlock(value: number): string {
|
|
228
|
+
if (value < 0.125) return "";
|
|
229
|
+
if (value < 0.25) return "▏";
|
|
230
|
+
if (value < 0.375) return "▎";
|
|
231
|
+
if (value < 0.5) return "▍";
|
|
232
|
+
if (value < 0.625) return "▌";
|
|
233
|
+
if (value < 0.75) return "▋";
|
|
234
|
+
if (value < 0.875) return "▊";
|
|
235
|
+
return "▉";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function buildKimiUsageUrl(baseUrl = getBaseUrl("openai")): string {
|
|
239
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
240
|
+
return normalized.endsWith("/v1") ? `${normalized}/usages` : `${normalized}/v1/usages`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
|
|
244
|
+
return fetch(buildKimiUsageUrl(), {
|
|
245
|
+
method: "GET",
|
|
246
|
+
headers: {
|
|
247
|
+
...getCommonHeaders(),
|
|
248
|
+
Authorization: `Bearer ${token}`,
|
|
249
|
+
},
|
|
250
|
+
signal,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function fetchKimiUsageSummary(): Promise<string> {
|
|
255
|
+
const token = getKimiUsageToken();
|
|
256
|
+
if (!token) return "Usage: missing credentials. Run /login kimi-coding.";
|
|
257
|
+
|
|
258
|
+
const controller = new AbortController();
|
|
259
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
260
|
+
try {
|
|
261
|
+
let response = await fetchKimiUsage(token, controller.signal);
|
|
262
|
+
if (response.status === 401) {
|
|
263
|
+
const refreshed = await refreshKimiAuthToken(token);
|
|
264
|
+
if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
|
|
265
|
+
}
|
|
266
|
+
if (!response.ok) return `Usage: fetch failed (${response.status})`;
|
|
267
|
+
return parseUsageSummary(await response.json());
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
return `Usage: fetch failed (${message})`;
|
|
271
|
+
} finally {
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
}
|
|
274
|
+
}
|