pi-provider-kimi-code 0.6.0 → 0.6.2

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/src/config.ts CHANGED
@@ -1,23 +1,100 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
+ import { PROVIDER_ID } from "./constants.ts";
5
+
4
6
  export const KIMI_TOOL_NAMES = ["moonshot_search", "moonshot_fetch", "kimi_datasource"] as const;
5
7
 
6
8
  export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
7
9
 
8
- export type KimiConfigSource = "project" | "home" | "default";
10
+ export type KimiConfigSource = "runtime" | "env" | "project" | "home" | "default";
11
+
12
+ export type KimiInputModality = "text" | "image" | "video";
13
+
14
+ export interface ModelReasoningEntry {
15
+ effort: string | null;
16
+ enabled: boolean;
17
+ }
18
+
19
+ export type ModelReasoningMap = Record<string, ModelReasoningEntry>;
20
+
21
+ export interface ModelGeneration {
22
+ temperature?: number;
23
+ topP?: number;
24
+ maxCompletionTokens?: number;
25
+ }
26
+
27
+ export interface ModelConfig {
28
+ contextWindow: number;
29
+ maxTokens: number;
30
+ input: KimiInputModality[];
31
+ reasoning: boolean;
32
+ reasoningMap: ModelReasoningMap;
33
+ thinkingKeep: "all" | "last" | "none" | null;
34
+ generation: ModelGeneration;
35
+ }
36
+
37
+ export interface KimiResolvedModelConfig extends ModelConfig {
38
+ supportsThinkingType?: "only" | "no" | "both";
39
+ }
9
40
 
10
41
  export interface KimiCodeConfig {
42
+ model: ModelConfig;
11
43
  tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
44
+ uploads: { thresholdBytes: number };
45
+ protocol: "openai" | "anthropic";
12
46
  }
13
47
 
48
+ export type KimiCodeConfigPatch = Partial<{
49
+ model: Partial<{
50
+ contextWindow: unknown;
51
+ maxTokens: unknown;
52
+ input: unknown;
53
+ reasoning: unknown;
54
+ reasoningMap: unknown;
55
+ thinkingKeep: unknown;
56
+ generation: unknown;
57
+ }>;
58
+ tools: Partial<Record<KimiToolName, Partial<{ enabled: unknown; default_collapsed: unknown }>>>;
59
+ uploads: Partial<{ thresholdBytes: unknown }>;
60
+ protocol: unknown;
61
+ }>;
62
+
14
63
  export interface KimiCodeConfigSources {
64
+ model: {
65
+ contextWindow: KimiConfigSource;
66
+ maxTokens: KimiConfigSource;
67
+ input: KimiConfigSource;
68
+ reasoning: KimiConfigSource;
69
+ reasoningMap: KimiConfigSource;
70
+ thinkingKeep: KimiConfigSource;
71
+ generation: {
72
+ temperature: KimiConfigSource;
73
+ topP: KimiConfigSource;
74
+ maxCompletionTokens: KimiConfigSource;
75
+ };
76
+ };
15
77
  tools: Record<KimiToolName, { enabled: KimiConfigSource; default_collapsed: KimiConfigSource }>;
78
+ uploads: { thresholdBytes: KimiConfigSource };
79
+ protocol: KimiConfigSource;
16
80
  }
17
81
 
18
82
  export interface LoadKimiCodeConfigOptions {
19
83
  cwd: string;
20
84
  home: string;
85
+ env?: NodeJS.ProcessEnv;
86
+ }
87
+
88
+ export class ConfigError extends Error {
89
+ public readonly configPath: string;
90
+ public readonly pointer?: string;
91
+
92
+ constructor(message: string, configPath: string, pointer?: string) {
93
+ super(pointer ? `${configPath}${pointer}: ${message}` : `${configPath}: ${message}`);
94
+ this.name = "ConfigError";
95
+ this.configPath = configPath;
96
+ this.pointer = pointer;
97
+ }
21
98
  }
22
99
 
23
100
  function makeDefaultTools(): KimiCodeConfig["tools"] {
@@ -29,15 +106,60 @@ function makeDefaultTools(): KimiCodeConfig["tools"] {
29
106
  }
30
107
 
31
108
  export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
109
+ model: {
110
+ contextWindow: 262144,
111
+ maxTokens: 32000,
112
+ input: ["text", "image"],
113
+ reasoning: true,
114
+ reasoningMap: {
115
+ none: { effort: null, enabled: false },
116
+ off: { effort: null, enabled: false },
117
+ minimal: { effort: "low", enabled: true },
118
+ low: { effort: "low", enabled: true },
119
+ medium: { effort: "medium", enabled: true },
120
+ high: { effort: "high", enabled: true },
121
+ xhigh: { effort: "high", enabled: true },
122
+ },
123
+ thinkingKeep: "all",
124
+ generation: {},
125
+ },
32
126
  tools: makeDefaultTools(),
127
+ uploads: { thresholdBytes: 1048576 },
128
+ protocol: "openai",
33
129
  };
34
130
 
131
+ let runtimeOverride: KimiCodeConfigPatch = {};
132
+
133
+ export function setRuntimeKimiCodeConfigOverride(patch: KimiCodeConfigPatch): void {
134
+ runtimeOverride = mergeConfigPatch(runtimeOverride, patch);
135
+ }
136
+
137
+ export function replaceRuntimeKimiCodeConfigOverride(patch: KimiCodeConfigPatch): void {
138
+ runtimeOverride = patch;
139
+ }
140
+
141
+ export function clearRuntimeKimiCodeConfigOverride(): void {
142
+ runtimeOverride = {};
143
+ }
144
+
145
+ export function getRuntimeKimiCodeConfigOverride(): KimiCodeConfigPatch {
146
+ return clone(runtimeOverride) as KimiCodeConfigPatch;
147
+ }
148
+
35
149
  export function getGlobalKimiCodeConfigPath(home: string): string {
36
- return join(home, ".pi", "pi-provider-kimi-code.json");
150
+ return join(home, ".pi", "providers", PROVIDER_ID, "config.json");
37
151
  }
38
152
 
39
153
  export function getProjectKimiCodeConfigPath(cwd: string): string {
40
- return join(cwd, ".pi", "pi-provider-kimi-code.json");
154
+ return join(cwd, ".pi", "providers", PROVIDER_ID, "config.json");
155
+ }
156
+
157
+ export function kimiCodeConfigPath(home: string): string {
158
+ return getGlobalKimiCodeConfigPath(home);
159
+ }
160
+
161
+ function clone<T>(value: T): T {
162
+ return JSON.parse(JSON.stringify(value)) as T;
41
163
  }
42
164
 
43
165
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -45,130 +167,377 @@ function isRecord(value: unknown): value is Record<string, unknown> {
45
167
  }
46
168
 
47
169
  function readConfigFile(path: string): Record<string, unknown> {
170
+ if (!existsSync(path)) return {};
171
+
172
+ let contents: string;
48
173
  try {
49
- if (!existsSync(path)) return {};
50
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
51
- return isRecord(parsed) ? parsed : {};
174
+ contents = readFileSync(path, "utf8");
52
175
  } catch (error) {
53
- console.error(`[kimi-coding] failed to read config file ${path}:`, error);
54
- return {};
176
+ throw new ConfigError(
177
+ `failed to read config: ${error instanceof Error ? error.message : String(error)}`,
178
+ path,
179
+ );
180
+ }
181
+
182
+ try {
183
+ const parsed = JSON.parse(contents) as unknown;
184
+ if (isRecord(parsed)) return parsed;
185
+ throw new ConfigError("config file must be a JSON object", path);
186
+ } catch (error) {
187
+ if (error instanceof ConfigError) throw error;
188
+ throw new ConfigError(
189
+ `invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
190
+ path,
191
+ );
55
192
  }
56
193
  }
57
194
 
58
195
  function readConfigFileQuiet(path: string): Record<string, unknown> {
59
196
  try {
60
- if (!existsSync(path)) return {};
61
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
62
- return isRecord(parsed) ? parsed : {};
63
- } catch {
197
+ return readConfigFile(path);
198
+ } catch (error) {
199
+ console.error(`[kimi-coding] failed to read config file ${path}:`, error);
64
200
  return {};
65
201
  }
66
202
  }
67
203
 
68
- function mergeConfig(
69
- base: Record<string, unknown>,
70
- override: Record<string, unknown>,
71
- ): Record<string, unknown> {
204
+ function mergeConfigPatch<T extends Record<string, unknown>>(
205
+ base: T,
206
+ patch: Record<string, unknown>,
207
+ ): T {
72
208
  const result: Record<string, unknown> = { ...base };
73
- for (const [key, value] of Object.entries(override)) {
209
+ for (const [key, value] of Object.entries(patch)) {
210
+ if (value === undefined) continue;
74
211
  const current = result[key];
75
212
  if (isRecord(current) && isRecord(value)) {
76
- result[key] = mergeConfig(current, value);
213
+ result[key] = mergeConfigPatch(current, value);
77
214
  } else {
78
215
  result[key] = value;
79
216
  }
80
217
  }
218
+ return result as T;
219
+ }
220
+
221
+ function envConfigPatch(env: NodeJS.ProcessEnv): KimiCodeConfigPatch {
222
+ const patch: KimiCodeConfigPatch = {};
223
+
224
+ const maxContext = parsePositiveNumber(env.KIMI_MODEL_MAX_CONTEXT_SIZE);
225
+ if (maxContext !== undefined) patch.model = { ...patch.model, contextWindow: maxContext };
226
+
227
+ const temp = parseFiniteNumber(env.KIMI_MODEL_TEMPERATURE);
228
+ const topP = parseFiniteNumber(env.KIMI_MODEL_TOP_P);
229
+ const maxCompletionTokens = parsePositiveNumber(env.KIMI_MODEL_MAX_COMPLETION_TOKENS);
230
+ const thinkingKeep = env.KIMI_MODEL_THINKING_KEEP?.trim();
231
+ if (
232
+ temp !== undefined ||
233
+ topP !== undefined ||
234
+ maxCompletionTokens !== undefined ||
235
+ thinkingKeep
236
+ ) {
237
+ patch.model = {
238
+ ...patch.model,
239
+ ...(thinkingKeep ? { thinkingKeep } : {}),
240
+ generation: {
241
+ ...(temp !== undefined ? { temperature: temp } : {}),
242
+ ...(topP !== undefined ? { topP } : {}),
243
+ ...(maxCompletionTokens !== undefined ? { maxCompletionTokens } : {}),
244
+ },
245
+ };
246
+ }
247
+
248
+ const uploadThreshold = parsePositiveNumber(env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
249
+ if (uploadThreshold !== undefined) patch.uploads = { thresholdBytes: uploadThreshold };
250
+
251
+ const protocol = env.KIMI_CODE_PROTOCOL?.trim();
252
+ if (protocol) patch.protocol = protocol;
253
+
254
+ const capabilities = env.KIMI_MODEL_CAPABILITIES;
255
+ if (capabilities) {
256
+ const caps = new Set(
257
+ capabilities
258
+ .split(",")
259
+ .map((cap) => cap.trim().toLowerCase())
260
+ .filter(Boolean),
261
+ );
262
+ patch.model = {
263
+ ...patch.model,
264
+ reasoning: caps.has("thinking") || caps.has("always_thinking"),
265
+ input: caps.has("image_in") ? ["text", "image"] : ["text"],
266
+ };
267
+ }
268
+
269
+ return patch;
270
+ }
271
+
272
+ function parsePositiveNumber(value: string | undefined): number | undefined {
273
+ if (!value) return undefined;
274
+ const parsed = Number(value);
275
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
276
+ }
277
+
278
+ function parseFiniteNumber(value: string | undefined): number | undefined {
279
+ if (!value) return undefined;
280
+ const parsed = Number(value);
281
+ return Number.isFinite(parsed) ? parsed : undefined;
282
+ }
283
+
284
+ function fail(configPath: string, pointer: string, message: string): never {
285
+ throw new ConfigError(message, configPath, pointer);
286
+ }
287
+
288
+ function requireRecord(raw: unknown, configPath: string, pointer: string): Record<string, unknown> {
289
+ if (isRecord(raw)) return raw;
290
+ return fail(
291
+ configPath,
292
+ pointer,
293
+ `expected an object, got ${Array.isArray(raw) ? "array" : typeof raw}`,
294
+ );
295
+ }
296
+
297
+ function requirePositiveNumber(raw: unknown, configPath: string, pointer: string): number {
298
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return raw;
299
+ return fail(configPath, pointer, `expected a positive number, got ${JSON.stringify(raw)}`);
300
+ }
301
+
302
+ function requireBoolean(raw: unknown, configPath: string, pointer: string): boolean {
303
+ if (typeof raw === "boolean") return raw;
304
+ return fail(configPath, pointer, `expected a boolean, got ${JSON.stringify(raw)}`);
305
+ }
306
+
307
+ function requireProtocol(
308
+ raw: unknown,
309
+ configPath: string,
310
+ pointer: string,
311
+ ): KimiCodeConfig["protocol"] {
312
+ if (raw === "openai" || raw === "anthropic") return raw;
313
+ return fail(configPath, pointer, `expected "openai" | "anthropic", got ${JSON.stringify(raw)}`);
314
+ }
315
+
316
+ function requireInputArray(raw: unknown, configPath: string, pointer: string): KimiInputModality[] {
317
+ const valid = new Set(["text", "image", "video"]);
318
+ if (!Array.isArray(raw)) return fail(configPath, pointer, `expected an array, got ${typeof raw}`);
319
+ if (raw.length === 0) return fail(configPath, pointer, "expected a non-empty array");
320
+ return raw.map((value, index) => {
321
+ if (typeof value !== "string" || !valid.has(value)) {
322
+ return fail(
323
+ configPath,
324
+ `${pointer}/${index}`,
325
+ `expected one of text, image, video, got ${JSON.stringify(value)}`,
326
+ );
327
+ }
328
+ return value as KimiInputModality;
329
+ });
330
+ }
331
+
332
+ function requireReasoningMap(raw: unknown, configPath: string, pointer: string): ModelReasoningMap {
333
+ const record = requireRecord(raw, configPath, pointer);
334
+ const result: ModelReasoningMap = {};
335
+ for (const [key, value] of Object.entries(record)) {
336
+ const entryPointer = `${pointer}/${key}`;
337
+ const entry = requireRecord(value, configPath, entryPointer);
338
+ result[key] = {
339
+ effort:
340
+ entry.effort === null || typeof entry.effort === "string"
341
+ ? entry.effort
342
+ : fail(configPath, `${entryPointer}/effort`, "expected a string or null"),
343
+ enabled: requireBoolean(entry.enabled, configPath, `${entryPointer}/enabled`),
344
+ };
345
+ }
81
346
  return result;
82
347
  }
83
348
 
84
- function readEnabled(config: Record<string, unknown>, toolName: string): boolean {
85
- const tools = config.tools;
86
- if (!isRecord(tools)) return false;
87
- const tool = tools[toolName];
88
- if (!isRecord(tool)) return false;
89
- return tool.enabled === true;
349
+ function requireThinkingKeep(
350
+ raw: unknown,
351
+ configPath: string,
352
+ pointer: string,
353
+ ): ModelConfig["thinkingKeep"] {
354
+ if (raw === null || raw === "all" || raw === "last" || raw === "none") return raw;
355
+ return fail(
356
+ configPath,
357
+ pointer,
358
+ `expected "all" | "last" | "none" | null, got ${JSON.stringify(raw)}`,
359
+ );
360
+ }
361
+
362
+ function requireGeneration(raw: unknown, configPath: string, pointer: string): ModelGeneration {
363
+ const record = requireRecord(raw, configPath, pointer);
364
+ const result: ModelGeneration = {};
365
+ const knownKeys = ["temperature", "topP", "maxCompletionTokens"] as const;
366
+ for (const key of knownKeys) {
367
+ const value = record[key];
368
+ if (value === undefined || value === null) continue;
369
+ if (typeof value !== "number" || !Number.isFinite(value)) {
370
+ fail(configPath, `${pointer}/${key}`, `expected a number, got ${JSON.stringify(value)}`);
371
+ }
372
+ result[key] = value;
373
+ }
374
+ return result;
90
375
  }
91
376
 
92
- function readDefaultCollapsed(config: Record<string, unknown>, toolName: string): boolean {
93
- const tools = config.tools;
94
- if (!isRecord(tools)) return true;
95
- const tool = tools[toolName];
96
- if (!isRecord(tool)) return true;
97
- return tool.default_collapsed !== false;
377
+ function validateModelConfig(raw: unknown, configPath: string, pointer: string): ModelConfig {
378
+ const model = requireRecord(raw, configPath, pointer);
379
+ return {
380
+ contextWindow: requirePositiveNumber(
381
+ model.contextWindow,
382
+ configPath,
383
+ `${pointer}/contextWindow`,
384
+ ),
385
+ maxTokens: requirePositiveNumber(model.maxTokens, configPath, `${pointer}/maxTokens`),
386
+ input: requireInputArray(model.input, configPath, `${pointer}/input`),
387
+ reasoning: requireBoolean(model.reasoning, configPath, `${pointer}/reasoning`),
388
+ reasoningMap: requireReasoningMap(model.reasoningMap, configPath, `${pointer}/reasoningMap`),
389
+ thinkingKeep: requireThinkingKeep(model.thinkingKeep, configPath, `${pointer}/thinkingKeep`),
390
+ generation: requireGeneration(model.generation, configPath, `${pointer}/generation`),
391
+ };
98
392
  }
99
393
 
100
- function hasToolField(
101
- config: Record<string, unknown>,
102
- toolName: KimiToolName,
103
- field: "enabled" | "default_collapsed",
104
- ): boolean {
105
- const tools = config.tools;
106
- if (!isRecord(tools)) return false;
107
- const tool = tools[toolName];
108
- if (!isRecord(tool)) return false;
109
- return Object.hasOwn(tool, field);
394
+ function validateToolsConfig(
395
+ raw: unknown,
396
+ configPath: string,
397
+ pointer: string,
398
+ ): KimiCodeConfig["tools"] {
399
+ const tools = requireRecord(raw, configPath, pointer);
400
+ const result = {} as KimiCodeConfig["tools"];
401
+ for (const name of KIMI_TOOL_NAMES) {
402
+ const tool = requireRecord(tools[name], configPath, `${pointer}/${name}`);
403
+ result[name] = {
404
+ enabled: requireBoolean(tool.enabled, configPath, `${pointer}/${name}/enabled`),
405
+ default_collapsed: requireBoolean(
406
+ tool.default_collapsed,
407
+ configPath,
408
+ `${pointer}/${name}/default_collapsed`,
409
+ ),
410
+ };
411
+ }
412
+ return result;
110
413
  }
111
414
 
112
- function readToolConfig(
113
- config: Record<string, unknown>,
114
- toolName: KimiToolName,
115
- ): { enabled: boolean; default_collapsed: boolean } {
415
+ export function validateKimiCodeConfig(
416
+ raw: unknown,
417
+ configPath = "<kimi-code-config>",
418
+ ): KimiCodeConfig {
419
+ const config = requireRecord(raw, configPath, "");
116
420
  return {
117
- enabled: readEnabled(config, toolName),
118
- default_collapsed: readDefaultCollapsed(config, toolName),
421
+ model: validateModelConfig(config.model, configPath, "/model"),
422
+ tools: validateToolsConfig(config.tools, configPath, "/tools"),
423
+ uploads: {
424
+ thresholdBytes: requirePositiveNumber(
425
+ requireRecord(config.uploads, configPath, "/uploads").thresholdBytes,
426
+ configPath,
427
+ "/uploads/thresholdBytes",
428
+ ),
429
+ },
430
+ protocol: requireProtocol(config.protocol, configPath, "/protocol"),
119
431
  };
120
432
  }
121
433
 
122
- export function loadKimiCodeConfig(options: LoadKimiCodeConfigOptions): KimiCodeConfig {
123
- const globalConfig = readConfigFile(getGlobalKimiCodeConfigPath(options.home));
124
- const projectConfig = readConfigFile(getProjectKimiCodeConfigPath(options.cwd));
125
- const merged = mergeConfig(globalConfig, projectConfig);
434
+ function hasPath(config: Record<string, unknown>, path: readonly string[]): boolean {
435
+ let current: unknown = config;
436
+ for (const key of path) {
437
+ if (!isRecord(current) || !Object.hasOwn(current, key)) return false;
438
+ current = current[key];
439
+ }
440
+ return true;
441
+ }
126
442
 
127
- const tools = {} as KimiCodeConfig["tools"];
128
- for (const name of KIMI_TOOL_NAMES) {
129
- tools[name] = readToolConfig(merged, name);
443
+ function sourceForPath(
444
+ layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }>,
445
+ path: readonly string[],
446
+ ): KimiConfigSource {
447
+ for (let i = layers.length - 1; i >= 0; i--) {
448
+ if (hasPath(layers[i].config, path)) return layers[i].source;
130
449
  }
131
- return { tools };
450
+ return "default";
132
451
  }
133
452
 
134
- export function loadKimiCodeConfigSources(
135
- options: LoadKimiCodeConfigOptions,
453
+ function buildSources(
454
+ layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }>,
136
455
  ): KimiCodeConfigSources {
137
- const homeConfig = readConfigFileQuiet(getGlobalKimiCodeConfigPath(options.home));
138
- const projectConfig = readConfigFileQuiet(getProjectKimiCodeConfigPath(options.cwd));
139
456
  const tools = {} as KimiCodeConfigSources["tools"];
140
457
  for (const name of KIMI_TOOL_NAMES) {
141
458
  tools[name] = {
142
- enabled: hasToolField(projectConfig, name, "enabled")
143
- ? "project"
144
- : hasToolField(homeConfig, name, "enabled")
145
- ? "home"
146
- : "default",
147
- default_collapsed: hasToolField(projectConfig, name, "default_collapsed")
148
- ? "project"
149
- : hasToolField(homeConfig, name, "default_collapsed")
150
- ? "home"
151
- : "default",
459
+ enabled: sourceForPath(layers, ["tools", name, "enabled"]),
460
+ default_collapsed: sourceForPath(layers, ["tools", name, "default_collapsed"]),
152
461
  };
153
462
  }
154
- return { tools };
463
+ return {
464
+ model: {
465
+ contextWindow: sourceForPath(layers, ["model", "contextWindow"]),
466
+ maxTokens: sourceForPath(layers, ["model", "maxTokens"]),
467
+ input: sourceForPath(layers, ["model", "input"]),
468
+ reasoning: sourceForPath(layers, ["model", "reasoning"]),
469
+ reasoningMap: sourceForPath(layers, ["model", "reasoningMap"]),
470
+ thinkingKeep: sourceForPath(layers, ["model", "thinkingKeep"]),
471
+ generation: {
472
+ temperature: sourceForPath(layers, ["model", "generation", "temperature"]),
473
+ topP: sourceForPath(layers, ["model", "generation", "topP"]),
474
+ maxCompletionTokens: sourceForPath(layers, ["model", "generation", "maxCompletionTokens"]),
475
+ },
476
+ },
477
+ tools,
478
+ uploads: { thresholdBytes: sourceForPath(layers, ["uploads", "thresholdBytes"]) },
479
+ protocol: sourceForPath(layers, ["protocol"]),
480
+ };
155
481
  }
156
482
 
157
- export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
158
- return loadKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd));
483
+ function loadLayers(
484
+ options: LoadKimiCodeConfigOptions,
485
+ ): Array<{ source: KimiConfigSource; config: Record<string, unknown> }> {
486
+ return [
487
+ { source: "home", config: readConfigFile(getGlobalKimiCodeConfigPath(options.home)) },
488
+ { source: "project", config: readConfigFile(getProjectKimiCodeConfigPath(options.cwd)) },
489
+ {
490
+ source: "env",
491
+ config: envConfigPatch(options.env ?? process.env) as Record<string, unknown>,
492
+ },
493
+ { source: "runtime", config: runtimeOverride as Record<string, unknown> },
494
+ ];
159
495
  }
160
496
 
161
- export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
162
- return loadKimiCodeConfigFile(getGlobalKimiCodeConfigPath(home));
497
+ export function loadKimiCodeConfig(
498
+ options: LoadKimiCodeConfigOptions,
499
+ overrides?: KimiCodeConfigPatch,
500
+ ): KimiCodeConfig {
501
+ let merged = clone(DEFAULT_KIMI_CODE_CONFIG) as unknown as Record<string, unknown>;
502
+ for (const layer of loadLayers(options)) {
503
+ merged = mergeConfigPatch(merged, layer.config);
504
+ }
505
+ if (overrides) {
506
+ merged = mergeConfigPatch(merged, overrides as Record<string, unknown>);
507
+ }
508
+ return validateKimiCodeConfig(merged);
163
509
  }
164
510
 
165
- function loadKimiCodeConfigFile(path: string): KimiCodeConfig {
166
- const projectConfig = readConfigFileQuiet(path);
167
- const tools = {} as KimiCodeConfig["tools"];
168
- for (const name of KIMI_TOOL_NAMES) {
169
- tools[name] = readToolConfig(projectConfig, name);
170
- }
171
- return { tools };
511
+ export function loadKimiCodeConfigSources(
512
+ options: LoadKimiCodeConfigOptions,
513
+ ): KimiCodeConfigSources {
514
+ return buildSources(loadLayers(options));
515
+ }
516
+
517
+ export function ensureKimiCodeConfig(pathOrHome: string): boolean {
518
+ const path = pathOrHome.endsWith(".json") ? pathOrHome : getGlobalKimiCodeConfigPath(pathOrHome);
519
+ if (existsSync(path)) return false;
520
+ mkdirSync(dirname(path), { recursive: true });
521
+ writeFileSync(path, `${JSON.stringify(DEFAULT_KIMI_CODE_CONFIG, null, 2)}\n`, "utf8");
522
+ return true;
523
+ }
524
+
525
+ export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
526
+ return validateKimiCodeConfig(
527
+ mergeConfigPatch(
528
+ clone(DEFAULT_KIMI_CODE_CONFIG) as unknown as Record<string, unknown>,
529
+ readConfigFileQuiet(getProjectKimiCodeConfigPath(cwd)),
530
+ ),
531
+ );
532
+ }
533
+
534
+ export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
535
+ return validateKimiCodeConfig(
536
+ mergeConfigPatch(
537
+ clone(DEFAULT_KIMI_CODE_CONFIG) as unknown as Record<string, unknown>,
538
+ readConfigFileQuiet(getGlobalKimiCodeConfigPath(home)),
539
+ ),
540
+ );
172
541
  }
173
542
 
174
543
  export function saveProjectKimiCodeConfig(cwd: string, config: KimiCodeConfig): void {
@@ -180,17 +549,6 @@ export function saveHomeKimiCodeConfig(home: string, config: KimiCodeConfig): vo
180
549
  }
181
550
 
182
551
  function saveKimiCodeConfigFile(path: string, config: KimiCodeConfig): void {
183
- const raw = readConfigFileQuiet(path);
184
- const tools: Record<string, unknown> = {
185
- ...(isRecord(raw.tools) ? raw.tools : {}),
186
- };
187
- for (const name of KIMI_TOOL_NAMES) {
188
- tools[name] = { ...config.tools[name] };
189
- }
190
- const next = {
191
- ...raw,
192
- tools,
193
- };
194
552
  mkdirSync(dirname(path), { recursive: true });
195
- writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
553
+ writeFileSync(path, `${JSON.stringify(validateKimiCodeConfig(config, path), null, 2)}\n`, "utf8");
196
554
  }