pi-provider-kimi-code 0.6.2 → 0.6.5
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 +7 -5
- package/index.ts +308 -469
- package/package.json +4 -4
- package/src/config.ts +18 -5
- package/src/constants.ts +2 -2
- package/src/device.ts +41 -0
- package/src/models.ts +100 -28
- package/src/oauth.ts +137 -35
- package/src/payload.ts +38 -25
- package/src/project-trust.ts +24 -0
- package/src/settings-ui.ts +204 -0
- package/src/stream.ts +25 -6
- package/src/tools/common.ts +6 -4
- package/src/tools/moonshot.ts +13 -35
- package/src/usage.ts +366 -0
package/index.ts
CHANGED
|
@@ -15,66 +15,59 @@
|
|
|
15
15
|
* login/refresh handlers, stream-level auth refresh
|
|
16
16
|
* src/models.ts — /v1/models discovery + extras-merging helpers
|
|
17
17
|
* src/payload.ts — payload pipeline + file upload + transforms
|
|
18
|
+
* src/project-trust.ts — project config approval compatibility helpers
|
|
18
19
|
* src/stream.ts — empty-response filter + streamSimpleKimi orchestrator
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
import {
|
|
22
|
-
AuthStorage,
|
|
23
23
|
type ExtensionAPI,
|
|
24
24
|
type ExtensionCommandContext,
|
|
25
25
|
type ExtensionFactory,
|
|
26
26
|
} from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { Input, SettingsList, type SettingItem, truncateToWidth } from "@earendil-works/pi-tui";
|
|
27
28
|
import os from "node:os";
|
|
28
|
-
import { relative } from "node:path";
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
31
|
type KimiCodeConfig,
|
|
32
32
|
type KimiCodeConfigPatch,
|
|
33
33
|
KIMI_TOOL_NAMES,
|
|
34
|
-
getProjectKimiCodeConfigPath,
|
|
35
|
-
getGlobalKimiCodeConfigPath,
|
|
36
34
|
loadHomeKimiCodeConfig,
|
|
37
35
|
loadKimiCodeConfig,
|
|
38
|
-
loadKimiCodeConfigSources,
|
|
39
36
|
loadProjectKimiCodeConfig,
|
|
40
37
|
saveHomeKimiCodeConfig,
|
|
41
38
|
saveProjectKimiCodeConfig,
|
|
42
39
|
type KimiToolName,
|
|
43
40
|
} from "./src/config.ts";
|
|
44
41
|
import { PROVIDER_ID, PROVIDER_VERSION, getBaseUrl, getKimiApiType } from "./src/constants.ts";
|
|
45
|
-
import { getCommonHeaders } from "./src/device.ts";
|
|
46
42
|
import {
|
|
47
43
|
type KimiOAuthCredentials,
|
|
48
44
|
type KimiOAuthExtras,
|
|
49
45
|
buildKimiModelFromConfig,
|
|
50
46
|
applyKimiOAuthExtrasToModel,
|
|
47
|
+
KIMI_CODING_HIGHSPEED_MODEL_ID,
|
|
48
|
+
KIMI_CODING_MODEL_ID,
|
|
51
49
|
discoverKimiModelMetadata,
|
|
50
|
+
getKimiModelMetadata,
|
|
52
51
|
resolveKimiModelConfig,
|
|
53
52
|
} from "./src/models.ts";
|
|
54
|
-
import {
|
|
53
|
+
import { getKimiApiKey, loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
|
|
54
|
+
import { isKimiProjectConfigApproved } from "./src/project-trust.ts";
|
|
55
|
+
import {
|
|
56
|
+
type KimiConfigScope,
|
|
57
|
+
buildSettingsTheme,
|
|
58
|
+
formatByteSize,
|
|
59
|
+
formatScopeDescription,
|
|
60
|
+
parseByteSizeInput,
|
|
61
|
+
} from "./src/settings-ui.ts";
|
|
55
62
|
import { setStoreResolvedKimiConfig, streamSimpleKimi } from "./src/stream.ts";
|
|
63
|
+
import { fetchKimiUsageSummary, getKimiUsageToken } from "./src/usage.ts";
|
|
56
64
|
import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
|
|
57
65
|
import { buildKimiDatasourceTool } from "./src/tools/datasource.ts";
|
|
58
|
-
type KimiConfigScope = "project" | "home";
|
|
59
|
-
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
60
|
-
LEVEL_FREE: "Free",
|
|
61
|
-
LEVEL_BASIC: "Adagio",
|
|
62
|
-
LEVEL_STANDARD: "Moderato",
|
|
63
|
-
LEVEL_INTERMEDIATE: "Allegretto",
|
|
64
|
-
LEVEL_ADVANCED: "Allegro",
|
|
65
|
-
LEVEL_PREMIUM: "Vivace",
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
interface UsageRow {
|
|
69
|
-
label: string;
|
|
70
|
-
used: number;
|
|
71
|
-
limit: number;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
66
|
interface KimiRuntimeState {
|
|
75
67
|
cwd: string;
|
|
76
68
|
config: KimiCodeConfig;
|
|
77
69
|
modelExtras: KimiOAuthExtras;
|
|
70
|
+
projectTrusted: boolean;
|
|
78
71
|
overrides?: KimiCodeConfigPatch;
|
|
79
72
|
}
|
|
80
73
|
|
|
@@ -111,12 +104,23 @@ function registerConfiguredMoonshotTools(
|
|
|
111
104
|
pi.setActiveTools([...activeTools]);
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
function reloadEffectiveKimiRuntimeConfig(
|
|
115
|
-
|
|
107
|
+
function reloadEffectiveKimiRuntimeConfig(
|
|
108
|
+
state: KimiRuntimeState,
|
|
109
|
+
cwd: string,
|
|
110
|
+
projectTrusted: boolean,
|
|
111
|
+
): KimiCodeConfig {
|
|
112
|
+
const config = loadKimiCodeConfig(
|
|
113
|
+
{ cwd, home: os.homedir(), includeProject: projectTrusted },
|
|
114
|
+
state.overrides,
|
|
115
|
+
);
|
|
116
116
|
state.cwd = cwd;
|
|
117
117
|
state.config = config;
|
|
118
|
+
state.projectTrusted = projectTrusted;
|
|
118
119
|
setStoreResolvedKimiConfig({
|
|
119
|
-
model: resolveKimiModelConfig(
|
|
120
|
+
model: resolveKimiModelConfig(
|
|
121
|
+
config.model,
|
|
122
|
+
getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
|
|
123
|
+
),
|
|
120
124
|
protocol: config.protocol,
|
|
121
125
|
uploads: config.uploads,
|
|
122
126
|
});
|
|
@@ -127,324 +131,225 @@ function applyEffectiveKimiRuntimeConfig(
|
|
|
127
131
|
pi: ExtensionAPI,
|
|
128
132
|
state: KimiRuntimeState,
|
|
129
133
|
cwd: string,
|
|
130
|
-
options: { updateActiveTools: boolean },
|
|
134
|
+
options: { updateActiveTools: boolean; projectTrusted: boolean },
|
|
131
135
|
): KimiCodeConfig {
|
|
132
|
-
const config = reloadEffectiveKimiRuntimeConfig(state, cwd);
|
|
136
|
+
const config = reloadEffectiveKimiRuntimeConfig(state, cwd, options.projectTrusted);
|
|
133
137
|
registerConfiguredMoonshotTools(pi, config, options);
|
|
134
138
|
return config;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
function
|
|
138
|
-
const credential = AuthStorage.create().get(PROVIDER_ID);
|
|
139
|
-
if (credential?.type === "oauth" && credential.access) return credential.access;
|
|
140
|
-
const apiKey = process.env.KIMI_API_KEY?.trim();
|
|
141
|
-
return apiKey || null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function toNumber(value: unknown): number | null {
|
|
145
|
-
const number = Number(value);
|
|
146
|
-
return Number.isFinite(number) ? number : null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function parseUsageRow(value: unknown, fallbackLabel: string): UsageRow | null {
|
|
150
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
151
|
-
const record = value as Record<string, unknown>;
|
|
152
|
-
const limit = toNumber(record.limit);
|
|
153
|
-
const usedValue = toNumber(record.used);
|
|
154
|
-
const remaining = toNumber(record.remaining);
|
|
155
|
-
const used = usedValue ?? (limit !== null && remaining !== null ? limit - remaining : null);
|
|
156
|
-
if (limit === null && used === null) return null;
|
|
157
|
-
return {
|
|
158
|
-
label: String(record.name || record.title || fallbackLabel),
|
|
159
|
-
used: used ?? 0,
|
|
160
|
-
limit: limit ?? 0,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function parseUsageSummary(payload: unknown): string {
|
|
165
|
-
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
166
|
-
return "Usage: unavailable";
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const record = payload as Record<string, unknown>;
|
|
170
|
-
const lines: string[] = [];
|
|
171
|
-
const membership = parseMembership(record);
|
|
172
|
-
if (membership) lines.push(membership);
|
|
173
|
-
|
|
174
|
-
const rows: UsageRow[] = [];
|
|
175
|
-
const summary = parseUsageRow(record.usage, "Weekly limit");
|
|
176
|
-
if (summary) rows.push(summary);
|
|
177
|
-
|
|
178
|
-
if (Array.isArray(record.limits)) {
|
|
179
|
-
for (const [index, item] of record.limits.entries()) {
|
|
180
|
-
const detail =
|
|
181
|
-
typeof item === "object" && item !== null && !Array.isArray(item)
|
|
182
|
-
? ((item as Record<string, unknown>).detail ?? item)
|
|
183
|
-
: item;
|
|
184
|
-
const row = parseUsageRow(detail, index === 0 ? "5h rate limit" : `Limit #${index + 1}`);
|
|
185
|
-
if (row) rows.push(row);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
lines.push(...rows.map(formatUsageRow));
|
|
190
|
-
return lines.length === 0 ? "Usage: no usage data" : lines.join("\n");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function parseMembership(record: Record<string, unknown>): string | null {
|
|
194
|
-
const user = record.user;
|
|
195
|
-
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
196
|
-
const membership = (user as Record<string, unknown>).membership;
|
|
197
|
-
if (typeof membership !== "object" || membership === null || Array.isArray(membership)) {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
const level = (membership as Record<string, unknown>).level;
|
|
201
|
-
if (typeof level !== "string" || !level) return null;
|
|
202
|
-
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
203
|
-
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function formatUsageRow(row: UsageRow): string {
|
|
207
|
-
if (row.limit <= 0) return `${row.label}: ${row.used} used`;
|
|
208
|
-
const remaining = Math.max(0, Math.min(row.limit - row.used, row.limit));
|
|
209
|
-
const percent = Math.round((remaining / row.limit) * 100);
|
|
210
|
-
return `${row.label}: ${quotaBar(remaining, row.limit)} ${percent}% left (${remaining}/${row.limit})`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function quotaBar(remaining: number, limit: number): string {
|
|
214
|
-
const width = 20;
|
|
215
|
-
const filled = Math.max(0, Math.min(width, Math.round((remaining / limit) * width)));
|
|
216
|
-
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
|
|
220
|
-
return fetch("https://api.kimi.com/coding/v1/usages", {
|
|
221
|
-
method: "GET",
|
|
222
|
-
headers: {
|
|
223
|
-
...getCommonHeaders(),
|
|
224
|
-
Authorization: `Bearer ${token}`,
|
|
225
|
-
},
|
|
226
|
-
signal,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function fetchKimiUsageSummary(): Promise<string> {
|
|
231
|
-
const token = getKimiUsageToken();
|
|
232
|
-
if (!token) return "Usage: missing credentials. Run /login kimi-coding.";
|
|
233
|
-
|
|
234
|
-
const controller = new AbortController();
|
|
235
|
-
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
236
|
-
try {
|
|
237
|
-
let response = await fetchKimiUsage(token, controller.signal);
|
|
238
|
-
if (response.status === 401) {
|
|
239
|
-
const refreshed = await refreshKimiAuthToken(token);
|
|
240
|
-
if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
|
|
241
|
-
}
|
|
242
|
-
if (!response.ok) return `Usage: fetch failed (${response.status})`;
|
|
243
|
-
return parseUsageSummary(await response.json());
|
|
244
|
-
} catch (error) {
|
|
245
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
-
return `Usage: fetch failed (${message})`;
|
|
247
|
-
} finally {
|
|
248
|
-
clearTimeout(timeout);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function moonshotStatus(config: KimiCodeConfig): string {
|
|
253
|
-
return KIMI_TOOL_NAMES.map((toolName) => {
|
|
254
|
-
const tool = config.tools[toolName];
|
|
255
|
-
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
256
|
-
const collapsed = tool.default_collapsed ? "collapsed" : "expanded";
|
|
257
|
-
return `${toolName}: ${enabled}, default ${collapsed}`;
|
|
258
|
-
}).join("\n");
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function toggleEnabled(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
|
|
262
|
-
return {
|
|
263
|
-
...config,
|
|
264
|
-
tools: {
|
|
265
|
-
...config.tools,
|
|
266
|
-
[toolName]: {
|
|
267
|
-
...config.tools[toolName],
|
|
268
|
-
enabled: !config.tools[toolName].enabled,
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function toggleCollapsed(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
|
|
275
|
-
return {
|
|
276
|
-
...config,
|
|
277
|
-
tools: {
|
|
278
|
-
...config.tools,
|
|
279
|
-
[toolName]: {
|
|
280
|
-
...config.tools[toolName],
|
|
281
|
-
default_collapsed: !config.tools[toolName].default_collapsed,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function setProtocol(config: KimiCodeConfig, protocol: KimiCodeConfig["protocol"]): KimiCodeConfig {
|
|
288
|
-
return { ...config, protocol };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function setUploadThreshold(config: KimiCodeConfig, thresholdBytes: number): KimiCodeConfig {
|
|
292
|
-
return { ...config, uploads: { ...config.uploads, thresholdBytes } };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function refreshModelExtras(state: KimiRuntimeState): Promise<void> {
|
|
141
|
+
async function refreshModelExtras(state: KimiRuntimeState): Promise<boolean> {
|
|
296
142
|
const token = getKimiUsageToken();
|
|
297
|
-
if (!token) return;
|
|
143
|
+
if (!token) return false;
|
|
298
144
|
const extras = await discoverKimiModelMetadata(token, state.config.protocol);
|
|
299
|
-
if (Object.keys(extras).length
|
|
300
|
-
|
|
301
|
-
|
|
145
|
+
if (Object.keys(extras).length === 0) return false;
|
|
146
|
+
Object.assign(state.modelExtras, extras);
|
|
147
|
+
return true;
|
|
302
148
|
}
|
|
303
149
|
|
|
304
|
-
async function
|
|
150
|
+
async function openSettingsMenu(
|
|
305
151
|
pi: ExtensionAPI,
|
|
306
152
|
ctx: ExtensionCommandContext,
|
|
307
153
|
state: KimiRuntimeState,
|
|
308
154
|
): Promise<void> {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
while (true) {
|
|
314
|
-
const choice = await ctx.ui.select(buildKimiMainTitle(config, ctx.cwd, state.modelExtras), [
|
|
315
|
-
`Edit project config (${relative(ctx.cwd, getProjectKimiCodeConfigPath(ctx.cwd))})`,
|
|
316
|
-
`Edit home config (${homeRelative(getGlobalKimiCodeConfigPath(os.homedir()))})`,
|
|
317
|
-
"Refresh usage",
|
|
318
|
-
"Done",
|
|
319
|
-
]);
|
|
320
|
-
|
|
321
|
-
if (!choice || choice === "Done") return;
|
|
322
|
-
if (choice === "Refresh usage") {
|
|
323
|
-
usage = await fetchKimiUsageSummary();
|
|
324
|
-
ctx.ui.notify(usage);
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (choice.startsWith("Edit project config")) {
|
|
328
|
-
config = await editConfigScope(pi, ctx, state, "project");
|
|
329
|
-
} else if (choice.startsWith("Edit home config")) {
|
|
330
|
-
config = await editConfigScope(pi, ctx, state, "home");
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function editConfigScope(
|
|
336
|
-
pi: ExtensionAPI,
|
|
337
|
-
ctx: ExtensionCommandContext,
|
|
338
|
-
state: KimiRuntimeState,
|
|
339
|
-
scope: KimiConfigScope,
|
|
340
|
-
): Promise<KimiCodeConfig> {
|
|
341
|
-
let current = loadScopeKimiCodeConfig(scope, ctx.cwd);
|
|
342
|
-
while (true) {
|
|
343
|
-
const items = KIMI_TOOL_NAMES.map((name) => toolMenuItem(current, name));
|
|
344
|
-
items.push(protocolMenuItem(current), uploadThresholdMenuItem(current), "Back");
|
|
345
|
-
const choice = await ctx.ui.select(buildConfigScopeTitle(scope, current, ctx.cwd), items);
|
|
346
|
-
if (!choice || choice === "Back") {
|
|
347
|
-
return loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
|
|
348
|
-
}
|
|
349
|
-
const toolName = KIMI_TOOL_NAMES.find((name) => choice.startsWith(name));
|
|
350
|
-
if (toolName) {
|
|
351
|
-
current = await editMoonshotTool(pi, ctx, state, scope, current, toolName);
|
|
352
|
-
} else if (choice.startsWith("Protocol")) {
|
|
353
|
-
current = await editProtocol(pi, ctx, state, scope, current);
|
|
354
|
-
} else if (choice.startsWith("Upload threshold")) {
|
|
355
|
-
current = await editUploadThreshold(pi, ctx, state, scope, current);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function editProtocol(
|
|
361
|
-
pi: ExtensionAPI,
|
|
362
|
-
ctx: ExtensionCommandContext,
|
|
363
|
-
state: KimiRuntimeState,
|
|
364
|
-
scope: KimiConfigScope,
|
|
365
|
-
config: KimiCodeConfig,
|
|
366
|
-
): Promise<KimiCodeConfig> {
|
|
367
|
-
const choice = await ctx.ui.select(`Edit protocol\n\n${formatProtocolStatus(config)}`, [
|
|
368
|
-
"Use openai protocol",
|
|
369
|
-
"Use anthropic protocol",
|
|
370
|
-
"Back",
|
|
155
|
+
const [usage, modelsRefreshed] = await Promise.all([
|
|
156
|
+
fetchKimiUsageSummary(),
|
|
157
|
+
refreshModelExtras(state),
|
|
371
158
|
]);
|
|
372
|
-
if (
|
|
373
|
-
const protocol = choice.includes("anthropic") ? "anthropic" : "openai";
|
|
374
|
-
const current = setProtocol(config, protocol);
|
|
375
|
-
saveAndApplyKimiCodeConfig(pi, ctx, state, scope, current);
|
|
376
|
-
ctx.ui.notify("Saved protocol config", "info");
|
|
377
|
-
return current;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function editUploadThreshold(
|
|
381
|
-
pi: ExtensionAPI,
|
|
382
|
-
ctx: ExtensionCommandContext,
|
|
383
|
-
state: KimiRuntimeState,
|
|
384
|
-
scope: KimiConfigScope,
|
|
385
|
-
config: KimiCodeConfig,
|
|
386
|
-
): Promise<KimiCodeConfig> {
|
|
387
|
-
const input = await ctx.ui.input(
|
|
388
|
-
"Edit upload threshold\n\nExamples: 512 KiB, 2 MiB, 1.5 MB. Plain numbers are MiB.",
|
|
389
|
-
formatByteSize(config.uploads.thresholdBytes),
|
|
390
|
-
);
|
|
391
|
-
if (input === undefined) return config;
|
|
392
|
-
const thresholdBytes = parseByteSizeInput(input);
|
|
393
|
-
if (thresholdBytes === undefined || thresholdBytes <= 0) {
|
|
394
|
-
ctx.ui.notify("Upload threshold must be a positive size, for example 2 MiB", "error");
|
|
395
|
-
return config;
|
|
396
|
-
}
|
|
397
|
-
const current = setUploadThreshold(config, thresholdBytes);
|
|
398
|
-
saveAndApplyKimiCodeConfig(pi, ctx, state, scope, current);
|
|
399
|
-
ctx.ui.notify("Saved upload threshold config", "info");
|
|
400
|
-
return current;
|
|
401
|
-
}
|
|
159
|
+
if (modelsRefreshed) registerKimiProvider(pi, state);
|
|
402
160
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
161
|
+
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
162
|
+
const homeDraft = loadHomeKimiCodeConfig(os.homedir());
|
|
163
|
+
const drafts: Record<KimiConfigScope, KimiCodeConfig> = {
|
|
164
|
+
project: projectTrusted ? loadProjectKimiCodeConfig(ctx.cwd) : homeDraft,
|
|
165
|
+
home: homeDraft,
|
|
166
|
+
};
|
|
167
|
+
let scope: KimiConfigScope = projectTrusted ? "project" : "home";
|
|
168
|
+
let dirty = false;
|
|
169
|
+
|
|
170
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
171
|
+
const settingsTheme = buildSettingsTheme(theme);
|
|
172
|
+
|
|
173
|
+
let list: SettingsList;
|
|
174
|
+
|
|
175
|
+
const toolLabel = (toolName: KimiToolName) => {
|
|
176
|
+
if (toolName === "moonshot_search") return "Search tool";
|
|
177
|
+
if (toolName === "moonshot_fetch") return "Fetch tool";
|
|
178
|
+
return "Real-world data API";
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const formatToolMenuValue = (toolName: KimiToolName) => {
|
|
182
|
+
const tool = drafts[scope].tools[toolName];
|
|
183
|
+
if (!tool.enabled) return "disabled";
|
|
184
|
+
return tool.default_collapsed ? "enabled without preview" : "enabled with preview";
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const refreshDisplays = () => {
|
|
188
|
+
scopeItem.description = formatScopeDescription(scope, ctx.cwd);
|
|
189
|
+
for (const toolName of KIMI_TOOL_NAMES) {
|
|
190
|
+
list.updateValue(toolName, formatToolMenuValue(toolName));
|
|
191
|
+
}
|
|
192
|
+
list.updateValue("protocol", drafts[scope].protocol);
|
|
193
|
+
list.updateValue("uploadThreshold", formatByteSize(drafts[scope].uploads.thresholdBytes));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const save = () => {
|
|
197
|
+
if (scope === "project" && !projectTrusted) {
|
|
198
|
+
ctx.ui.notify("Project config cannot be saved until the project is trusted.", "warning");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
saveScopeKimiCodeConfig(scope, ctx.cwd, drafts[scope]);
|
|
203
|
+
applyEffectiveKimiRuntimeConfig(pi, state, ctx.cwd, {
|
|
204
|
+
updateActiveTools: true,
|
|
205
|
+
projectTrusted: scope === "project" ? true : projectTrusted,
|
|
206
|
+
});
|
|
207
|
+
dirty = true;
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
ctx.ui.notify((error as Error).message, "error");
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const onChange = (id: string, newValue: string) => {
|
|
214
|
+
if (id === "scope") {
|
|
215
|
+
scope = newValue as KimiConfigScope;
|
|
216
|
+
refreshDisplays();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (id === "protocol") {
|
|
220
|
+
drafts[scope].protocol = newValue as KimiCodeConfig["protocol"];
|
|
221
|
+
list.updateValue(id, newValue);
|
|
222
|
+
save();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (id === "uploadThreshold") {
|
|
226
|
+
const bytes = parseByteSizeInput(newValue);
|
|
227
|
+
if (bytes === undefined || bytes <= 0) {
|
|
228
|
+
ctx.ui.notify("Upload threshold must be a positive size, for example 2 MiB", "error");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
drafts[scope].uploads.thresholdBytes = bytes;
|
|
232
|
+
list.updateValue(id, formatByteSize(bytes));
|
|
233
|
+
save();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const toolMatch = /^(?<tool>.+):(?<field>enabled|collapsed)$/.exec(id);
|
|
237
|
+
if (toolMatch?.groups) {
|
|
238
|
+
const toolName = toolMatch.groups.tool as KimiToolName;
|
|
239
|
+
const field = toolMatch.groups.field as "enabled" | "collapsed";
|
|
240
|
+
if (field === "enabled") {
|
|
241
|
+
drafts[scope].tools[toolName].enabled = newValue === "true";
|
|
242
|
+
} else {
|
|
243
|
+
drafts[scope].tools[toolName].default_collapsed = newValue !== "true";
|
|
244
|
+
}
|
|
245
|
+
list.updateValue(toolName, formatToolMenuValue(toolName));
|
|
246
|
+
save();
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const scopeItem: SettingItem = {
|
|
251
|
+
id: "scope",
|
|
252
|
+
label: "Config scope",
|
|
253
|
+
description: projectTrusted
|
|
254
|
+
? formatScopeDescription(scope, ctx.cwd)
|
|
255
|
+
: "Project config disabled until the project is trusted; editing home config only",
|
|
256
|
+
currentValue: scope,
|
|
257
|
+
values: projectTrusted ? ["project", "home"] : ["home"],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const items: SettingItem[] = [scopeItem];
|
|
261
|
+
for (const toolName of KIMI_TOOL_NAMES) {
|
|
262
|
+
items.push({
|
|
263
|
+
id: toolName,
|
|
264
|
+
label: toolLabel(toolName),
|
|
265
|
+
description: `Configure ${toolName} registration and preview defaults`,
|
|
266
|
+
currentValue: formatToolMenuValue(toolName),
|
|
267
|
+
submenu: (_current, submenuDone) =>
|
|
268
|
+
new SettingsList(
|
|
269
|
+
[
|
|
270
|
+
{
|
|
271
|
+
id: `${toolName}:enabled`,
|
|
272
|
+
label: "Enabled",
|
|
273
|
+
description: `Register ${toolName} at session start`,
|
|
274
|
+
currentValue: String(drafts[scope].tools[toolName].enabled),
|
|
275
|
+
values: ["true", "false"],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: `${toolName}:collapsed`,
|
|
279
|
+
label: "Show preview",
|
|
280
|
+
description: `Show ${toolName} result previews by default`,
|
|
281
|
+
currentValue: String(!drafts[scope].tools[toolName].default_collapsed),
|
|
282
|
+
values: ["true", "false"],
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
2,
|
|
286
|
+
settingsTheme,
|
|
287
|
+
onChange,
|
|
288
|
+
() => submenuDone(),
|
|
289
|
+
),
|
|
290
|
+
});
|
|
427
291
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
292
|
+
items.push({
|
|
293
|
+
id: "protocol",
|
|
294
|
+
label: "Protocol",
|
|
295
|
+
description: "API protocol for Kimi requests",
|
|
296
|
+
currentValue: drafts[scope].protocol,
|
|
297
|
+
values: ["openai", "anthropic"],
|
|
298
|
+
});
|
|
299
|
+
items.push({
|
|
300
|
+
id: "uploadThreshold",
|
|
301
|
+
label: "Upload threshold",
|
|
302
|
+
description: "Max size for inline file uploads",
|
|
303
|
+
currentValue: formatByteSize(drafts[scope].uploads.thresholdBytes),
|
|
304
|
+
submenu: (_current, submenuDone) => {
|
|
305
|
+
const input = new Input();
|
|
306
|
+
input.setValue(formatByteSize(drafts[scope].uploads.thresholdBytes));
|
|
307
|
+
input.onSubmit = (value) => {
|
|
308
|
+
const bytes = parseByteSizeInput(value);
|
|
309
|
+
if (bytes === undefined || bytes <= 0) {
|
|
310
|
+
ctx.ui.notify("Upload threshold must be a positive size, for example 2 MiB", "error");
|
|
311
|
+
submenuDone();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
submenuDone(formatByteSize(bytes));
|
|
315
|
+
};
|
|
316
|
+
input.onEscape = () => submenuDone();
|
|
317
|
+
return input;
|
|
318
|
+
},
|
|
319
|
+
});
|
|
433
320
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
321
|
+
list = new SettingsList(items, items.length, settingsTheme, onChange, () => done(), {
|
|
322
|
+
enableSearch: true,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
items,
|
|
327
|
+
onChange,
|
|
328
|
+
render(width: number) {
|
|
329
|
+
const usageLines = usage.split("\n").map((line) => truncateToWidth(` ${line}`, width));
|
|
330
|
+
return [
|
|
331
|
+
truncateToWidth(
|
|
332
|
+
theme.fg("accent", theme.bold(`Kimi settings (provider v${PROVIDER_VERSION})`)),
|
|
333
|
+
width,
|
|
334
|
+
),
|
|
335
|
+
"",
|
|
336
|
+
truncateToWidth(theme.fg("accent", theme.bold("Kimi usage")), width),
|
|
337
|
+
...usageLines,
|
|
338
|
+
"",
|
|
339
|
+
...list.render(width),
|
|
340
|
+
];
|
|
341
|
+
},
|
|
342
|
+
handleInput(data: string) {
|
|
343
|
+
list.handleInput?.(data);
|
|
344
|
+
tui.requestRender();
|
|
345
|
+
},
|
|
346
|
+
invalidate() {
|
|
347
|
+
list.invalidate();
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
});
|
|
444
351
|
|
|
445
|
-
|
|
446
|
-
if (scope === "project") return loadProjectKimiCodeConfig(cwd);
|
|
447
|
-
return loadHomeKimiCodeConfig(os.homedir());
|
|
352
|
+
if (dirty) await ctx.reload();
|
|
448
353
|
}
|
|
449
354
|
|
|
450
355
|
function saveScopeKimiCodeConfig(
|
|
@@ -459,167 +364,101 @@ function saveScopeKimiCodeConfig(
|
|
|
459
364
|
}
|
|
460
365
|
}
|
|
461
366
|
|
|
462
|
-
function
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
`Model: ${modelName}`,
|
|
469
|
-
`Protocol: ${config.protocol} (${sources.protocol})`,
|
|
470
|
-
`Upload threshold: ${formatByteSize(config.uploads.thresholdBytes)} (${sources.uploads.thresholdBytes})`,
|
|
471
|
-
"",
|
|
472
|
-
"Effective tools:",
|
|
473
|
-
...KIMI_TOOL_NAMES.map((toolName) => {
|
|
474
|
-
const enabled = config.tools[toolName].enabled ? "enabled" : "disabled";
|
|
475
|
-
return `- ${toolName}: ${enabled} (${sources.tools[toolName].enabled})`;
|
|
476
|
-
}),
|
|
477
|
-
].join("\n");
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function buildConfigScopeTitle(
|
|
481
|
-
scope: KimiConfigScope,
|
|
482
|
-
config: KimiCodeConfig,
|
|
483
|
-
cwd: string,
|
|
484
|
-
): string {
|
|
485
|
-
const path =
|
|
486
|
-
scope === "project"
|
|
487
|
-
? relative(cwd, getProjectKimiCodeConfigPath(cwd))
|
|
488
|
-
: homeRelative(getGlobalKimiCodeConfigPath(os.homedir()));
|
|
489
|
-
return [
|
|
490
|
-
`Edit ${scope} config`,
|
|
491
|
-
`File: ${path}`,
|
|
492
|
-
"",
|
|
493
|
-
formatProtocolStatus(config),
|
|
494
|
-
formatUploadThresholdStatus(config),
|
|
495
|
-
"",
|
|
496
|
-
moonshotStatus(config),
|
|
497
|
-
].join("\n");
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function homeRelative(filePath: string): string {
|
|
501
|
-
const home = os.homedir();
|
|
502
|
-
return filePath.startsWith(`${home}/`) ? `~/${filePath.slice(home.length + 1)}` : filePath;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function toolMenuItem(config: KimiCodeConfig, toolName: KimiToolName): string {
|
|
506
|
-
return `${toolName} -> ${formatToolStatus(config, toolName)}`;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function protocolMenuItem(config: KimiCodeConfig): string {
|
|
510
|
-
return `Protocol -> ${config.protocol}`;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function uploadThresholdMenuItem(config: KimiCodeConfig): string {
|
|
514
|
-
return `Upload threshold -> ${formatByteSize(config.uploads.thresholdBytes)}`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function formatByteSize(bytes: number): string {
|
|
518
|
-
const units = [
|
|
519
|
-
{ suffix: "GiB", size: 1024 ** 3 },
|
|
520
|
-
{ suffix: "MiB", size: 1024 ** 2 },
|
|
521
|
-
{ suffix: "KiB", size: 1024 },
|
|
522
|
-
];
|
|
523
|
-
for (const unit of units) {
|
|
524
|
-
if (bytes >= unit.size && bytes % unit.size === 0) {
|
|
525
|
-
return `${bytes / unit.size} ${unit.suffix}`;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
for (const unit of units) {
|
|
529
|
-
if (bytes >= unit.size) {
|
|
530
|
-
return `${(bytes / unit.size).toFixed(2).replace(/\.00$/, "")} ${unit.suffix}`;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return `${bytes} B`;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function parseByteSizeInput(input: string): number | undefined {
|
|
537
|
-
const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(b|bytes?|kib|kb|mib|mb|gib|gb)?$/i);
|
|
538
|
-
if (!match) return undefined;
|
|
539
|
-
const value = Number(match[1]);
|
|
540
|
-
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
541
|
-
const unit = (match[2] ?? "mib").toLowerCase();
|
|
542
|
-
const multipliers: Record<string, number> = {
|
|
543
|
-
b: 1,
|
|
544
|
-
byte: 1,
|
|
545
|
-
bytes: 1,
|
|
546
|
-
kib: 1024,
|
|
547
|
-
kb: 1000,
|
|
548
|
-
mib: 1024 ** 2,
|
|
549
|
-
mb: 1000 ** 2,
|
|
550
|
-
gib: 1024 ** 3,
|
|
551
|
-
gb: 1000 ** 3,
|
|
552
|
-
};
|
|
553
|
-
const multiplier = multipliers[unit];
|
|
554
|
-
if (!multiplier) return undefined;
|
|
555
|
-
const bytes = Math.round(value * multiplier);
|
|
556
|
-
return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : undefined;
|
|
367
|
+
function filterAvailableKimiModels<T extends { id: string }>(
|
|
368
|
+
models: T[],
|
|
369
|
+
extras: KimiOAuthExtras,
|
|
370
|
+
): T[] {
|
|
371
|
+
const available = extras.modelCatalog ? new Set(Object.keys(extras.modelCatalog)) : null;
|
|
372
|
+
return available ? models.filter((model) => available.has(model.id)) : models;
|
|
557
373
|
}
|
|
558
374
|
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
375
|
+
function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
376
|
+
const standardModel = applyKimiOAuthExtrasToModel(
|
|
377
|
+
buildKimiModelFromConfig(state.config.model),
|
|
378
|
+
getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
|
|
379
|
+
);
|
|
380
|
+
const highSpeedModel = applyKimiOAuthExtrasToModel(
|
|
381
|
+
buildKimiModelFromConfig(state.config.model, KIMI_CODING_HIGHSPEED_MODEL_ID),
|
|
382
|
+
getKimiModelMetadata(state.modelExtras, KIMI_CODING_HIGHSPEED_MODEL_ID),
|
|
383
|
+
);
|
|
566
384
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
385
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
386
|
+
baseUrl: getBaseUrl(state.config.protocol),
|
|
387
|
+
apiKey: "$KIMI_API_KEY",
|
|
388
|
+
api: getKimiApiType(state.config.protocol),
|
|
389
|
+
streamSimple: streamSimpleKimi,
|
|
390
|
+
|
|
391
|
+
models: filterAvailableKimiModels([standardModel, highSpeedModel], state.modelExtras),
|
|
392
|
+
|
|
393
|
+
oauth: {
|
|
394
|
+
name: "Kimi Code (OAuth)",
|
|
395
|
+
login: loginKimiCode,
|
|
396
|
+
refreshToken: refreshKimiCodeToken,
|
|
397
|
+
getApiKey: getKimiApiKey,
|
|
398
|
+
// Reflect server-side model identity on the registered model after login
|
|
399
|
+
// / refresh. We never rewrite the model id (pi-side `/model` selections
|
|
400
|
+
// and persisted sessions reference it); only the human-facing name, the
|
|
401
|
+
// context window, and an out-of-band `wireModelId` carried into the
|
|
402
|
+
// request payload by streamSimpleKimi.
|
|
403
|
+
modifyModels: (models, cred) => {
|
|
404
|
+
const extras = cred as KimiOAuthCredentials;
|
|
405
|
+
state.modelExtras = extras;
|
|
406
|
+
reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
|
|
407
|
+
const available = extras.modelCatalog ? new Set(Object.keys(extras.modelCatalog)) : null;
|
|
408
|
+
return models
|
|
409
|
+
.filter(
|
|
410
|
+
(model) => model.provider !== PROVIDER_ID || !available || available.has(model.id),
|
|
411
|
+
)
|
|
412
|
+
.map((model) =>
|
|
413
|
+
model.provider === PROVIDER_ID
|
|
414
|
+
? applyKimiOAuthExtrasToModel(model, getKimiModelMetadata(extras, model.id))
|
|
415
|
+
: model,
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
});
|
|
572
420
|
}
|
|
573
421
|
|
|
574
422
|
export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
575
423
|
return async (pi: ExtensionAPI) => {
|
|
576
424
|
const cwd = process.cwd();
|
|
577
|
-
const config = loadKimiCodeConfig(
|
|
578
|
-
|
|
425
|
+
const config = loadKimiCodeConfig(
|
|
426
|
+
{ cwd, home: os.homedir(), includeProject: false },
|
|
427
|
+
overrides,
|
|
428
|
+
);
|
|
579
429
|
const discoveryToken = getKimiUsageToken();
|
|
580
430
|
const discovered = discoveryToken
|
|
581
431
|
? await discoverKimiModelMetadata(discoveryToken, config.protocol)
|
|
582
432
|
: {};
|
|
583
|
-
const state: KimiRuntimeState = {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
models: [model],
|
|
594
|
-
|
|
595
|
-
oauth: {
|
|
596
|
-
name: "Kimi Code (OAuth)",
|
|
597
|
-
login: loginKimiCode,
|
|
598
|
-
refreshToken: refreshKimiCodeToken,
|
|
599
|
-
getApiKey: (cred) => cred.access,
|
|
600
|
-
// Reflect server-side model identity on the registered model after login
|
|
601
|
-
// / refresh. We never rewrite the model id (pi-side `/model` selections
|
|
602
|
-
// and persisted sessions reference it); only the human-facing name, the
|
|
603
|
-
// context window, and an out-of-band `wireModelId` carried into the
|
|
604
|
-
// request payload by streamSimpleKimi.
|
|
605
|
-
modifyModels: (models, cred) => {
|
|
606
|
-
const extras = cred as KimiOAuthCredentials;
|
|
607
|
-
state.modelExtras = extras;
|
|
608
|
-
reloadEffectiveKimiRuntimeConfig(state, state.cwd);
|
|
609
|
-
return models.map((model) => {
|
|
610
|
-
if (model.id !== "kimi-for-coding") return model;
|
|
611
|
-
return applyKimiOAuthExtrasToModel(model, extras);
|
|
612
|
-
});
|
|
613
|
-
},
|
|
614
|
-
},
|
|
615
|
-
});
|
|
433
|
+
const state: KimiRuntimeState = {
|
|
434
|
+
cwd,
|
|
435
|
+
config,
|
|
436
|
+
modelExtras: discovered,
|
|
437
|
+
projectTrusted: false,
|
|
438
|
+
overrides,
|
|
439
|
+
};
|
|
440
|
+
reloadEffectiveKimiRuntimeConfig(state, cwd, false);
|
|
441
|
+
registerKimiProvider(pi, state);
|
|
616
442
|
|
|
617
443
|
registerConfiguredMoonshotTools(pi, state.config, { updateActiveTools: false });
|
|
618
444
|
|
|
445
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
446
|
+
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
447
|
+
applyEffectiveKimiRuntimeConfig(pi, state, ctx.cwd, {
|
|
448
|
+
updateActiveTools: true,
|
|
449
|
+
projectTrusted,
|
|
450
|
+
});
|
|
451
|
+
registerKimiProvider(pi, state);
|
|
452
|
+
});
|
|
453
|
+
|
|
619
454
|
pi.registerCommand("kimi-settings", {
|
|
620
455
|
description: "Show Kimi usage and configure optional Kimi tools",
|
|
621
456
|
handler: async (_args, ctx) => {
|
|
622
|
-
|
|
457
|
+
if (ctx.mode !== "tui") {
|
|
458
|
+
ctx.ui.notify("/kimi-settings requires TUI mode", "error");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
await openSettingsMenu(pi, ctx, state);
|
|
623
462
|
},
|
|
624
463
|
});
|
|
625
464
|
};
|