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/index.ts
CHANGED
|
@@ -15,34 +15,30 @@
|
|
|
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
|
-
import {
|
|
45
|
-
import { getCommonHeaders } from "./src/device.ts";
|
|
41
|
+
import { PROVIDER_ID, PROVIDER_VERSION, getBaseUrl, getKimiApiType } from "./src/constants.ts";
|
|
46
42
|
import {
|
|
47
43
|
type KimiOAuthCredentials,
|
|
48
44
|
type KimiOAuthExtras,
|
|
@@ -51,30 +47,24 @@ import {
|
|
|
51
47
|
discoverKimiModelMetadata,
|
|
52
48
|
resolveKimiModelConfig,
|
|
53
49
|
} from "./src/models.ts";
|
|
54
|
-
import { loginKimiCode,
|
|
50
|
+
import { loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
|
|
51
|
+
import { isKimiProjectConfigApproved } from "./src/project-trust.ts";
|
|
52
|
+
import {
|
|
53
|
+
type KimiConfigScope,
|
|
54
|
+
buildSettingsTheme,
|
|
55
|
+
formatByteSize,
|
|
56
|
+
formatScopeDescription,
|
|
57
|
+
parseByteSizeInput,
|
|
58
|
+
} from "./src/settings-ui.ts";
|
|
55
59
|
import { setStoreResolvedKimiConfig, streamSimpleKimi } from "./src/stream.ts";
|
|
60
|
+
import { fetchKimiUsageSummary, getKimiUsageToken } from "./src/usage.ts";
|
|
56
61
|
import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
|
|
57
62
|
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
63
|
interface KimiRuntimeState {
|
|
75
64
|
cwd: string;
|
|
76
65
|
config: KimiCodeConfig;
|
|
77
66
|
modelExtras: KimiOAuthExtras;
|
|
67
|
+
projectTrusted: boolean;
|
|
78
68
|
overrides?: KimiCodeConfigPatch;
|
|
79
69
|
}
|
|
80
70
|
|
|
@@ -111,10 +101,18 @@ function registerConfiguredMoonshotTools(
|
|
|
111
101
|
pi.setActiveTools([...activeTools]);
|
|
112
102
|
}
|
|
113
103
|
|
|
114
|
-
function reloadEffectiveKimiRuntimeConfig(
|
|
115
|
-
|
|
104
|
+
function reloadEffectiveKimiRuntimeConfig(
|
|
105
|
+
state: KimiRuntimeState,
|
|
106
|
+
cwd: string,
|
|
107
|
+
projectTrusted: boolean,
|
|
108
|
+
): KimiCodeConfig {
|
|
109
|
+
const config = loadKimiCodeConfig(
|
|
110
|
+
{ cwd, home: os.homedir(), includeProject: projectTrusted },
|
|
111
|
+
state.overrides,
|
|
112
|
+
);
|
|
116
113
|
state.cwd = cwd;
|
|
117
114
|
state.config = config;
|
|
115
|
+
state.projectTrusted = projectTrusted;
|
|
118
116
|
setStoreResolvedKimiConfig({
|
|
119
117
|
model: resolveKimiModelConfig(config.model, state.modelExtras),
|
|
120
118
|
protocol: config.protocol,
|
|
@@ -127,171 +125,13 @@ function applyEffectiveKimiRuntimeConfig(
|
|
|
127
125
|
pi: ExtensionAPI,
|
|
128
126
|
state: KimiRuntimeState,
|
|
129
127
|
cwd: string,
|
|
130
|
-
options: { updateActiveTools: boolean },
|
|
128
|
+
options: { updateActiveTools: boolean; projectTrusted: boolean },
|
|
131
129
|
): KimiCodeConfig {
|
|
132
|
-
const config = reloadEffectiveKimiRuntimeConfig(state, cwd);
|
|
130
|
+
const config = reloadEffectiveKimiRuntimeConfig(state, cwd, options.projectTrusted);
|
|
133
131
|
registerConfiguredMoonshotTools(pi, config, options);
|
|
134
132
|
return config;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
function getKimiUsageToken(): string | null {
|
|
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
135
|
async function refreshModelExtras(state: KimiRuntimeState): Promise<void> {
|
|
296
136
|
const token = getKimiUsageToken();
|
|
297
137
|
if (!token) return;
|
|
@@ -301,150 +141,205 @@ async function refreshModelExtras(state: KimiRuntimeState): Promise<void> {
|
|
|
301
141
|
}
|
|
302
142
|
}
|
|
303
143
|
|
|
304
|
-
async function
|
|
144
|
+
async function openSettingsMenu(
|
|
305
145
|
pi: ExtensionAPI,
|
|
306
146
|
ctx: ExtensionCommandContext,
|
|
307
147
|
state: KimiRuntimeState,
|
|
308
148
|
): Promise<void> {
|
|
309
|
-
|
|
310
|
-
let [usage] = await Promise.all([fetchKimiUsageSummary(), refreshModelExtras(state)]);
|
|
311
|
-
ctx.ui.notify(usage);
|
|
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
|
-
}
|
|
149
|
+
const [usage] = await Promise.all([fetchKimiUsageSummary(), refreshModelExtras(state)]);
|
|
334
150
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
151
|
+
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
152
|
+
const homeDraft = loadHomeKimiCodeConfig(os.homedir());
|
|
153
|
+
const drafts: Record<KimiConfigScope, KimiCodeConfig> = {
|
|
154
|
+
project: projectTrusted ? loadProjectKimiCodeConfig(ctx.cwd) : homeDraft,
|
|
155
|
+
home: homeDraft,
|
|
156
|
+
};
|
|
157
|
+
let scope: KimiConfigScope = projectTrusted ? "project" : "home";
|
|
158
|
+
let dirty = false;
|
|
159
|
+
|
|
160
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
161
|
+
const settingsTheme = buildSettingsTheme(theme);
|
|
162
|
+
|
|
163
|
+
let list: SettingsList;
|
|
164
|
+
|
|
165
|
+
const toolLabel = (toolName: KimiToolName) => {
|
|
166
|
+
if (toolName === "moonshot_search") return "Search tool";
|
|
167
|
+
if (toolName === "moonshot_fetch") return "Fetch tool";
|
|
168
|
+
return "Real-world data API";
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const formatToolMenuValue = (toolName: KimiToolName) => {
|
|
172
|
+
const tool = drafts[scope].tools[toolName];
|
|
173
|
+
if (!tool.enabled) return "disabled";
|
|
174
|
+
return tool.default_collapsed ? "enabled without preview" : "enabled with preview";
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const refreshDisplays = () => {
|
|
178
|
+
scopeItem.description = formatScopeDescription(scope, ctx.cwd);
|
|
179
|
+
for (const toolName of KIMI_TOOL_NAMES) {
|
|
180
|
+
list.updateValue(toolName, formatToolMenuValue(toolName));
|
|
181
|
+
}
|
|
182
|
+
list.updateValue("protocol", drafts[scope].protocol);
|
|
183
|
+
list.updateValue("uploadThreshold", formatByteSize(drafts[scope].uploads.thresholdBytes));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const save = () => {
|
|
187
|
+
if (scope === "project" && !projectTrusted) {
|
|
188
|
+
ctx.ui.notify("Project config cannot be saved until the project is trusted.", "warning");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
saveScopeKimiCodeConfig(scope, ctx.cwd, drafts[scope]);
|
|
193
|
+
applyEffectiveKimiRuntimeConfig(pi, state, ctx.cwd, {
|
|
194
|
+
updateActiveTools: true,
|
|
195
|
+
projectTrusted: scope === "project" ? true : projectTrusted,
|
|
196
|
+
});
|
|
197
|
+
dirty = true;
|
|
198
|
+
} catch (error: unknown) {
|
|
199
|
+
ctx.ui.notify((error as Error).message, "error");
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const onChange = (id: string, newValue: string) => {
|
|
204
|
+
if (id === "scope") {
|
|
205
|
+
scope = newValue as KimiConfigScope;
|
|
206
|
+
refreshDisplays();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (id === "protocol") {
|
|
210
|
+
drafts[scope].protocol = newValue as KimiCodeConfig["protocol"];
|
|
211
|
+
list.updateValue(id, newValue);
|
|
212
|
+
save();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (id === "uploadThreshold") {
|
|
216
|
+
const bytes = parseByteSizeInput(newValue);
|
|
217
|
+
if (bytes === undefined || bytes <= 0) {
|
|
218
|
+
ctx.ui.notify("Upload threshold must be a positive size, for example 2 MiB", "error");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
drafts[scope].uploads.thresholdBytes = bytes;
|
|
222
|
+
list.updateValue(id, formatByteSize(bytes));
|
|
223
|
+
save();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const toolMatch = /^(?<tool>.+):(?<field>enabled|collapsed)$/.exec(id);
|
|
227
|
+
if (toolMatch?.groups) {
|
|
228
|
+
const toolName = toolMatch.groups.tool as KimiToolName;
|
|
229
|
+
const field = toolMatch.groups.field as "enabled" | "collapsed";
|
|
230
|
+
if (field === "enabled") {
|
|
231
|
+
drafts[scope].tools[toolName].enabled = newValue === "true";
|
|
232
|
+
} else {
|
|
233
|
+
drafts[scope].tools[toolName].default_collapsed = newValue !== "true";
|
|
234
|
+
}
|
|
235
|
+
list.updateValue(toolName, formatToolMenuValue(toolName));
|
|
236
|
+
save();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const scopeItem: SettingItem = {
|
|
241
|
+
id: "scope",
|
|
242
|
+
label: "Config scope",
|
|
243
|
+
description: projectTrusted
|
|
244
|
+
? formatScopeDescription(scope, ctx.cwd)
|
|
245
|
+
: "Project config disabled until the project is trusted; editing home config only",
|
|
246
|
+
currentValue: scope,
|
|
247
|
+
values: projectTrusted ? ["project", "home"] : ["home"],
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const items: SettingItem[] = [scopeItem];
|
|
251
|
+
for (const toolName of KIMI_TOOL_NAMES) {
|
|
252
|
+
items.push({
|
|
253
|
+
id: toolName,
|
|
254
|
+
label: toolLabel(toolName),
|
|
255
|
+
description: `Configure ${toolName} registration and preview defaults`,
|
|
256
|
+
currentValue: formatToolMenuValue(toolName),
|
|
257
|
+
submenu: (_current, submenuDone) =>
|
|
258
|
+
new SettingsList(
|
|
259
|
+
[
|
|
260
|
+
{
|
|
261
|
+
id: `${toolName}:enabled`,
|
|
262
|
+
label: "Enabled",
|
|
263
|
+
description: `Register ${toolName} at session start`,
|
|
264
|
+
currentValue: String(drafts[scope].tools[toolName].enabled),
|
|
265
|
+
values: ["true", "false"],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: `${toolName}:collapsed`,
|
|
269
|
+
label: "Show preview",
|
|
270
|
+
description: `Show ${toolName} result previews by default`,
|
|
271
|
+
currentValue: String(!drafts[scope].tools[toolName].default_collapsed),
|
|
272
|
+
values: ["true", "false"],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
2,
|
|
276
|
+
settingsTheme,
|
|
277
|
+
onChange,
|
|
278
|
+
() => submenuDone(),
|
|
279
|
+
),
|
|
280
|
+
});
|
|
356
281
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
}
|
|
282
|
+
items.push({
|
|
283
|
+
id: "protocol",
|
|
284
|
+
label: "Protocol",
|
|
285
|
+
description: "API protocol for Kimi requests",
|
|
286
|
+
currentValue: drafts[scope].protocol,
|
|
287
|
+
values: ["openai", "anthropic"],
|
|
288
|
+
});
|
|
289
|
+
items.push({
|
|
290
|
+
id: "uploadThreshold",
|
|
291
|
+
label: "Upload threshold",
|
|
292
|
+
description: "Max size for inline file uploads",
|
|
293
|
+
currentValue: formatByteSize(drafts[scope].uploads.thresholdBytes),
|
|
294
|
+
submenu: (_current, submenuDone) => {
|
|
295
|
+
const input = new Input();
|
|
296
|
+
input.setValue(formatByteSize(drafts[scope].uploads.thresholdBytes));
|
|
297
|
+
input.onSubmit = (value) => {
|
|
298
|
+
const bytes = parseByteSizeInput(value);
|
|
299
|
+
if (bytes === undefined || bytes <= 0) {
|
|
300
|
+
ctx.ui.notify("Upload threshold must be a positive size, for example 2 MiB", "error");
|
|
301
|
+
submenuDone();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
submenuDone(formatByteSize(bytes));
|
|
305
|
+
};
|
|
306
|
+
input.onEscape = () => submenuDone();
|
|
307
|
+
return input;
|
|
308
|
+
},
|
|
309
|
+
});
|
|
402
310
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
state: KimiRuntimeState,
|
|
407
|
-
scope: KimiConfigScope,
|
|
408
|
-
config: KimiCodeConfig,
|
|
409
|
-
toolName: KimiToolName,
|
|
410
|
-
): Promise<KimiCodeConfig> {
|
|
411
|
-
let current = config;
|
|
412
|
-
while (true) {
|
|
413
|
-
const tool = current.tools[toolName];
|
|
414
|
-
const choice = await ctx.ui.select(
|
|
415
|
-
`Edit ${toolName}\n\n${formatToolStatus(current, toolName)}`,
|
|
416
|
-
[
|
|
417
|
-
tool.enabled ? `Disable ${toolName}` : `Enable ${toolName}`,
|
|
418
|
-
tool.default_collapsed ? "Expand previews by default" : "Collapse previews by default",
|
|
419
|
-
"Back",
|
|
420
|
-
],
|
|
421
|
-
);
|
|
422
|
-
if (!choice || choice === "Back") return current;
|
|
423
|
-
if (choice.startsWith("Enable") || choice.startsWith("Disable")) {
|
|
424
|
-
current = toggleEnabled(current, toolName);
|
|
425
|
-
} else if (choice.endsWith("previews by default")) {
|
|
426
|
-
current = toggleCollapsed(current, toolName);
|
|
427
|
-
}
|
|
428
|
-
saveAndApplyKimiCodeConfig(pi, ctx, state, scope, current);
|
|
429
|
-
ctx.ui.notify(`Saved ${toolName} config`, "info");
|
|
430
|
-
return current;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
311
|
+
list = new SettingsList(items, items.length, settingsTheme, onChange, () => done(), {
|
|
312
|
+
enableSearch: true,
|
|
313
|
+
});
|
|
433
314
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
315
|
+
return {
|
|
316
|
+
items,
|
|
317
|
+
onChange,
|
|
318
|
+
render(width: number) {
|
|
319
|
+
const usageLines = usage.split("\n").map((line) => truncateToWidth(` ${line}`, width));
|
|
320
|
+
return [
|
|
321
|
+
truncateToWidth(
|
|
322
|
+
theme.fg("accent", theme.bold(`Kimi settings (provider v${PROVIDER_VERSION})`)),
|
|
323
|
+
width,
|
|
324
|
+
),
|
|
325
|
+
"",
|
|
326
|
+
truncateToWidth(theme.fg("accent", theme.bold("Kimi usage")), width),
|
|
327
|
+
...usageLines,
|
|
328
|
+
"",
|
|
329
|
+
...list.render(width),
|
|
330
|
+
];
|
|
331
|
+
},
|
|
332
|
+
handleInput(data: string) {
|
|
333
|
+
list.handleInput?.(data);
|
|
334
|
+
tui.requestRender();
|
|
335
|
+
},
|
|
336
|
+
invalidate() {
|
|
337
|
+
list.invalidate();
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
});
|
|
444
341
|
|
|
445
|
-
|
|
446
|
-
if (scope === "project") return loadProjectKimiCodeConfig(cwd);
|
|
447
|
-
return loadHomeKimiCodeConfig(os.homedir());
|
|
342
|
+
if (dirty) await ctx.reload();
|
|
448
343
|
}
|
|
449
344
|
|
|
450
345
|
function saveScopeKimiCodeConfig(
|
|
@@ -459,167 +354,83 @@ function saveScopeKimiCodeConfig(
|
|
|
459
354
|
}
|
|
460
355
|
}
|
|
461
356
|
|
|
462
|
-
function
|
|
463
|
-
const
|
|
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;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function formatProtocolStatus(config: KimiCodeConfig): string {
|
|
560
|
-
return `protocol: ${config.protocol}`;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function formatUploadThresholdStatus(config: KimiCodeConfig): string {
|
|
564
|
-
return `upload threshold: ${formatByteSize(config.uploads.thresholdBytes)}`;
|
|
565
|
-
}
|
|
357
|
+
function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
358
|
+
const model = applyKimiOAuthExtrasToModel(
|
|
359
|
+
buildKimiModelFromConfig(state.config.model),
|
|
360
|
+
state.modelExtras,
|
|
361
|
+
);
|
|
566
362
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
363
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
364
|
+
baseUrl: getBaseUrl(state.config.protocol),
|
|
365
|
+
apiKey: "$KIMI_API_KEY",
|
|
366
|
+
api: getKimiApiType(state.config.protocol),
|
|
367
|
+
streamSimple: streamSimpleKimi,
|
|
368
|
+
|
|
369
|
+
models: [model],
|
|
370
|
+
|
|
371
|
+
oauth: {
|
|
372
|
+
name: "Kimi Code (OAuth)",
|
|
373
|
+
login: loginKimiCode,
|
|
374
|
+
refreshToken: refreshKimiCodeToken,
|
|
375
|
+
getApiKey: (cred) => cred.access,
|
|
376
|
+
// Reflect server-side model identity on the registered model after login
|
|
377
|
+
// / refresh. We never rewrite the model id (pi-side `/model` selections
|
|
378
|
+
// and persisted sessions reference it); only the human-facing name, the
|
|
379
|
+
// context window, and an out-of-band `wireModelId` carried into the
|
|
380
|
+
// request payload by streamSimpleKimi.
|
|
381
|
+
modifyModels: (models, cred) => {
|
|
382
|
+
const extras = cred as KimiOAuthCredentials;
|
|
383
|
+
state.modelExtras = extras;
|
|
384
|
+
reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
|
|
385
|
+
return models.map((model) => {
|
|
386
|
+
if (model.id !== "kimi-for-coding") return model;
|
|
387
|
+
return applyKimiOAuthExtrasToModel(model, extras);
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
});
|
|
572
392
|
}
|
|
573
393
|
|
|
574
394
|
export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
575
395
|
return async (pi: ExtensionAPI) => {
|
|
576
396
|
const cwd = process.cwd();
|
|
577
|
-
const config = loadKimiCodeConfig(
|
|
578
|
-
|
|
397
|
+
const config = loadKimiCodeConfig(
|
|
398
|
+
{ cwd, home: os.homedir(), includeProject: false },
|
|
399
|
+
overrides,
|
|
400
|
+
);
|
|
579
401
|
const discoveryToken = getKimiUsageToken();
|
|
580
402
|
const discovered = discoveryToken
|
|
581
403
|
? await discoverKimiModelMetadata(discoveryToken, config.protocol)
|
|
582
404
|
: {};
|
|
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
|
-
});
|
|
405
|
+
const state: KimiRuntimeState = {
|
|
406
|
+
cwd,
|
|
407
|
+
config,
|
|
408
|
+
modelExtras: discovered,
|
|
409
|
+
projectTrusted: false,
|
|
410
|
+
overrides,
|
|
411
|
+
};
|
|
412
|
+
reloadEffectiveKimiRuntimeConfig(state, cwd, false);
|
|
413
|
+
registerKimiProvider(pi, state);
|
|
616
414
|
|
|
617
415
|
registerConfiguredMoonshotTools(pi, state.config, { updateActiveTools: false });
|
|
618
416
|
|
|
417
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
418
|
+
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
419
|
+
applyEffectiveKimiRuntimeConfig(pi, state, ctx.cwd, {
|
|
420
|
+
updateActiveTools: true,
|
|
421
|
+
projectTrusted,
|
|
422
|
+
});
|
|
423
|
+
registerKimiProvider(pi, state);
|
|
424
|
+
});
|
|
425
|
+
|
|
619
426
|
pi.registerCommand("kimi-settings", {
|
|
620
427
|
description: "Show Kimi usage and configure optional Kimi tools",
|
|
621
428
|
handler: async (_args, ctx) => {
|
|
622
|
-
|
|
429
|
+
if (ctx.mode !== "tui") {
|
|
430
|
+
ctx.ui.notify("/kimi-settings requires TUI mode", "error");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
await openSettingsMenu(pi, ctx, state);
|
|
623
434
|
},
|
|
624
435
|
});
|
|
625
436
|
};
|