@di-code/coding-agent 0.1.4 → 0.1.7

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.
Files changed (111) hide show
  1. package/README.md +79 -42
  2. package/dist/cli.d.ts +15 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +96 -17
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/clipboard-image.d.ts +3 -3
  7. package/dist/core/clipboard-image.d.ts.map +1 -1
  8. package/dist/core/clipboard-image.js +26 -13
  9. package/dist/core/clipboard-image.js.map +1 -1
  10. package/dist/core/resources/loader.d.ts.map +1 -1
  11. package/dist/core/resources/loader.js +15 -42
  12. package/dist/core/resources/loader.js.map +1 -1
  13. package/dist/core/resources/types.d.ts +6 -9
  14. package/dist/core/resources/types.d.ts.map +1 -1
  15. package/dist/core/resources/types.js.map +1 -1
  16. package/dist/core/session/session-manager.d.ts +12 -1
  17. package/dist/core/session/session-manager.d.ts.map +1 -1
  18. package/dist/core/session/session-manager.js +66 -3
  19. package/dist/core/session/session-manager.js.map +1 -1
  20. package/dist/core/session/session-storage.d.ts.map +1 -1
  21. package/dist/core/session/session-storage.js +29 -15
  22. package/dist/core/session/session-storage.js.map +1 -1
  23. package/dist/core/session/types.d.ts +6 -1
  24. package/dist/core/session/types.d.ts.map +1 -1
  25. package/dist/core/session/types.js +1 -1
  26. package/dist/core/session/types.js.map +1 -1
  27. package/dist/core/session.d.ts +23 -1
  28. package/dist/core/session.d.ts.map +1 -1
  29. package/dist/core/session.js +86 -13
  30. package/dist/core/session.js.map +1 -1
  31. package/dist/core/system-prompt.js +2 -2
  32. package/dist/core/system-prompt.js.map +1 -1
  33. package/dist/core/tools/file-search.d.ts +14 -0
  34. package/dist/core/tools/file-search.d.ts.map +1 -0
  35. package/dist/core/tools/file-search.js +99 -0
  36. package/dist/core/tools/file-search.js.map +1 -0
  37. package/dist/core/tools/glob.d.ts +11 -0
  38. package/dist/core/tools/glob.d.ts.map +1 -0
  39. package/dist/core/tools/glob.js +44 -0
  40. package/dist/core/tools/glob.js.map +1 -0
  41. package/dist/core/tools/grep.d.ts +13 -0
  42. package/dist/core/tools/grep.d.ts.map +1 -0
  43. package/dist/core/tools/grep.js +67 -0
  44. package/dist/core/tools/grep.js.map +1 -0
  45. package/dist/core/tools/load-skill.d.ts +10 -0
  46. package/dist/core/tools/load-skill.d.ts.map +1 -0
  47. package/dist/core/tools/load-skill.js +22 -0
  48. package/dist/core/tools/load-skill.js.map +1 -0
  49. package/dist/core/tools/path-boundary.d.ts +2 -0
  50. package/dist/core/tools/path-boundary.d.ts.map +1 -1
  51. package/dist/core/tools/path-boundary.js +4 -0
  52. package/dist/core/tools/path-boundary.js.map +1 -1
  53. package/dist/core/user-data.d.ts +3 -0
  54. package/dist/core/user-data.d.ts.map +1 -0
  55. package/dist/core/user-data.js +8 -0
  56. package/dist/core/user-data.js.map +1 -0
  57. package/dist/entry.js +13 -0
  58. package/dist/entry.js.map +1 -1
  59. package/dist/extensions/runtime.js +2 -2
  60. package/dist/extensions/runtime.js.map +1 -1
  61. package/dist/i18n.d.ts +5 -0
  62. package/dist/i18n.d.ts.map +1 -0
  63. package/dist/i18n.js +225 -0
  64. package/dist/i18n.js.map +1 -0
  65. package/dist/index.d.ts +3 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +3 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/main.d.ts +17 -0
  70. package/dist/main.d.ts.map +1 -1
  71. package/dist/main.js +243 -11
  72. package/dist/main.js.map +1 -1
  73. package/dist/mcp/config.d.ts +33 -0
  74. package/dist/mcp/config.d.ts.map +1 -0
  75. package/dist/mcp/config.js +211 -0
  76. package/dist/mcp/config.js.map +1 -0
  77. package/dist/mcp/loader.d.ts +21 -0
  78. package/dist/mcp/loader.d.ts.map +1 -0
  79. package/dist/mcp/loader.js +60 -0
  80. package/dist/mcp/loader.js.map +1 -0
  81. package/dist/mcp/tool-adapter.d.ts +5 -0
  82. package/dist/mcp/tool-adapter.d.ts.map +1 -0
  83. package/dist/mcp/tool-adapter.js +149 -0
  84. package/dist/mcp/tool-adapter.js.map +1 -0
  85. package/dist/modes/interactive-components.d.ts +2 -2
  86. package/dist/modes/interactive-components.d.ts.map +1 -1
  87. package/dist/modes/interactive-components.js +30 -33
  88. package/dist/modes/interactive-components.js.map +1 -1
  89. package/dist/modes/interactive-state.d.ts.map +1 -1
  90. package/dist/modes/interactive-state.js +2 -1
  91. package/dist/modes/interactive-state.js.map +1 -1
  92. package/dist/modes/interactive.d.ts +9 -0
  93. package/dist/modes/interactive.d.ts.map +1 -1
  94. package/dist/modes/interactive.js +209 -63
  95. package/dist/modes/interactive.js.map +1 -1
  96. package/dist/modes/tree-selector.d.ts +30 -0
  97. package/dist/modes/tree-selector.d.ts.map +1 -0
  98. package/dist/modes/tree-selector.js +205 -0
  99. package/dist/modes/tree-selector.js.map +1 -0
  100. package/dist/plugins/loader.d.ts +19 -1
  101. package/dist/plugins/loader.d.ts.map +1 -1
  102. package/dist/plugins/loader.js +21 -2
  103. package/dist/plugins/loader.js.map +1 -1
  104. package/dist/provider-onboarding.d.ts.map +1 -1
  105. package/dist/provider-onboarding.js +119 -22
  106. package/dist/provider-onboarding.js.map +1 -1
  107. package/dist/startup.d.ts +34 -2
  108. package/dist/startup.d.ts.map +1 -1
  109. package/dist/startup.js +278 -9
  110. package/dist/startup.js.map +1 -1
  111. package/package.json +6 -4
package/dist/startup.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { createAnthropicProvider, createDeepSeekProvider, createFauxProvider, createOpenAIChatCompletionsProvider, createOpenAIProvider, createZhipuProvider, } from "@di-code/ai";
4
+ import { createAnthropicProvider, createDeepSeekProvider, createFauxProvider, createKimiProvider, createOpenAIChatCompletionsProvider, createOpenAIProvider, createZhipuProvider, findBuiltinModel, } from "@di-code/ai";
5
+ import { isLocale } from "./i18n.js";
5
6
  const FAUX_RESPONSES = [
6
7
  {
7
8
  type: "success",
@@ -46,6 +47,43 @@ function nonNegativeNumber(value, path, fallback, settingsPath) {
46
47
  }
47
48
  return result;
48
49
  }
50
+ function parseChatCompletionsCompat(value, path, settingsPath) {
51
+ if (value === undefined)
52
+ return undefined;
53
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
54
+ throw new Error(`${settingsPath}: ${path} must be an object`);
55
+ }
56
+ const compat = value;
57
+ const optionalBoolean = (field) => {
58
+ const candidate = compat[field];
59
+ if (candidate === undefined)
60
+ return undefined;
61
+ if (typeof candidate !== "boolean")
62
+ throw new Error(`${settingsPath}: ${path}.${field} must be a boolean`);
63
+ return candidate;
64
+ };
65
+ const maxTokensField = compat.maxTokensField;
66
+ if (maxTokensField !== undefined && maxTokensField !== "max_tokens" && maxTokensField !== "max_completion_tokens") {
67
+ throw new Error(`${settingsPath}: ${path}.maxTokensField is invalid`);
68
+ }
69
+ const thinkingFormat = compat.thinkingFormat;
70
+ if (thinkingFormat !== undefined &&
71
+ thinkingFormat !== "zai" &&
72
+ thinkingFormat !== "deepseek" &&
73
+ thinkingFormat !== "kimi") {
74
+ throw new Error(`${settingsPath}: ${path}.thinkingFormat is invalid`);
75
+ }
76
+ const supportsUsageInStreaming = optionalBoolean("supportsUsageInStreaming");
77
+ const supportsReasoningEffort = optionalBoolean("supportsReasoningEffort");
78
+ const zaiToolStream = optionalBoolean("zaiToolStream");
79
+ return {
80
+ ...(maxTokensField === undefined ? {} : { maxTokensField }),
81
+ ...(thinkingFormat === undefined ? {} : { thinkingFormat }),
82
+ ...(supportsUsageInStreaming === undefined ? {} : { supportsUsageInStreaming }),
83
+ ...(supportsReasoningEffort === undefined ? {} : { supportsReasoningEffort }),
84
+ ...(zaiToolStream === undefined ? {} : { zaiToolStream }),
85
+ };
86
+ }
49
87
  function parseModels(value, path, providerId, providerApi, providerBaseUrl, settingsPath) {
50
88
  if (value === undefined)
51
89
  return undefined;
@@ -75,9 +113,9 @@ function parseModels(value, path, providerId, providerApi, providerBaseUrl, sett
75
113
  throw new Error(`${settingsPath}: ${modelPath}.reasoningEfforts must be a non-empty array`);
76
114
  }
77
115
  if (model.reasoning !== true ||
78
- configuredReasoningEfforts.some((effort) => effort !== "low" && effort !== "medium" && effort !== "high") ||
116
+ configuredReasoningEfforts.some((effort) => effort !== "low" && effort !== "medium" && effort !== "high" && effort !== "max") ||
79
117
  new Set(configuredReasoningEfforts).size !== configuredReasoningEfforts.length) {
80
- throw new Error(`${settingsPath}: ${modelPath}.reasoningEfforts requires reasoning=true and unique low, medium, or high values`);
118
+ throw new Error(`${settingsPath}: ${modelPath}.reasoningEfforts requires reasoning=true and unique low, medium, high, or max values`);
81
119
  }
82
120
  }
83
121
  if (model.cacheRetention !== undefined && model.cacheRetention !== "long") {
@@ -98,8 +136,18 @@ function parseModels(value, path, providerId, providerApi, providerBaseUrl, sett
98
136
  : configuredReasoningEfforts === undefined
99
137
  ? undefined
100
138
  : [...configuredReasoningEfforts];
139
+ const defaultReasoningEffort = model.defaultReasoningEffort;
140
+ if (defaultReasoningEffort !== undefined &&
141
+ (typeof defaultReasoningEffort !== "string" ||
142
+ !reasoningEfforts?.includes(defaultReasoningEffort))) {
143
+ throw new Error(`${settingsPath}: ${modelPath}.defaultReasoningEffort must be included in reasoningEfforts`);
144
+ }
101
145
  const baseUrl = optionalString(model.baseUrl, `${modelPath}.baseUrl`, settingsPath) ?? providerBaseUrl;
102
146
  const id = requiredString(model.id, `${modelPath}.id`, settingsPath);
147
+ const chatCompletionsCompat = parseChatCompletionsCompat(model.chatCompletionsCompat, `${modelPath}.chatCompletionsCompat`, settingsPath);
148
+ if (chatCompletionsCompat !== undefined && typedApi !== "openai-chat-completions") {
149
+ throw new Error(`${settingsPath}: ${modelPath}.chatCompletionsCompat requires api "openai-chat-completions"`);
150
+ }
103
151
  return {
104
152
  id,
105
153
  name: requiredString(model.name ?? id, `${modelPath}.name`, settingsPath),
@@ -109,6 +157,10 @@ function parseModels(value, path, providerId, providerApi, providerBaseUrl, sett
109
157
  input: [...input],
110
158
  reasoning: model.reasoning ?? false,
111
159
  ...(reasoningEfforts ? { reasoningEfforts } : {}),
160
+ ...(defaultReasoningEffort
161
+ ? { defaultReasoningEffort: defaultReasoningEffort }
162
+ : {}),
163
+ ...(chatCompletionsCompat ? { chatCompletionsCompat } : {}),
112
164
  ...(model.cacheRetention === "long" ? { cacheRetention: "long" } : {}),
113
165
  ...(model.sessionAffinity === "codex" ? { sessionAffinity: "codex" } : {}),
114
166
  contextWindow: positiveInteger(model.contextWindow, `${modelPath}.contextWindow`, 128_000, settingsPath),
@@ -122,6 +174,77 @@ function parseModels(value, path, providerId, providerApi, providerBaseUrl, sett
122
174
  };
123
175
  });
124
176
  }
177
+ /** Validates a Custom onboarding endpoint without issuing a network request. */
178
+ export function validateCustomBaseUrl(value) {
179
+ const baseUrl = value.trim();
180
+ if (!baseUrl)
181
+ throw new Error("Base URL cannot be empty.");
182
+ let url;
183
+ try {
184
+ url = new URL(baseUrl);
185
+ }
186
+ catch {
187
+ throw new Error("Base URL must be an absolute http or https URL.");
188
+ }
189
+ if (url.protocol !== "http:" && url.protocol !== "https:")
190
+ throw new Error("Base URL must use http or https.");
191
+ if (url.username || url.password || url.search || url.hash) {
192
+ throw new Error("Base URL must not contain credentials, query, or hash.");
193
+ }
194
+ if (baseUrl.endsWith("/"))
195
+ throw new Error("Base URL must not end with /.");
196
+ return baseUrl;
197
+ }
198
+ function requireCustomValue(value, field) {
199
+ const trimmed = value.trim();
200
+ if (!trimmed)
201
+ throw new Error(`${field === "apiKey" ? "API key" : "Model ID"} cannot be empty.`);
202
+ return trimmed;
203
+ }
204
+ /** Builds the single user-managed Custom provider from validated onboarding input. */
205
+ export function createCustomProviderConfiguration(input) {
206
+ const baseUrl = validateCustomBaseUrl(input.baseUrl);
207
+ const modelId = requireCustomValue(input.modelId, "modelId");
208
+ const apiKey = requireCustomValue(input.apiKey, "apiKey");
209
+ const known = findBuiltinModel(input.api, modelId);
210
+ const model = known
211
+ ? {
212
+ ...known,
213
+ id: modelId,
214
+ provider: "custom",
215
+ api: input.api,
216
+ baseUrl,
217
+ }
218
+ : {
219
+ id: modelId,
220
+ name: modelId,
221
+ provider: "custom",
222
+ api: input.api,
223
+ baseUrl,
224
+ input: ["text"],
225
+ reasoning: false,
226
+ contextWindow: 128_000,
227
+ maxOutputTokens: 16_384,
228
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
229
+ };
230
+ return { id: "custom", name: "Custom", api: input.api, apiKey, baseUrl, models: [model] };
231
+ }
232
+ function serializeModel(model) {
233
+ return {
234
+ id: model.id,
235
+ name: model.name,
236
+ input: model.input,
237
+ reasoning: model.reasoning,
238
+ ...(model.reasoningEfforts ? { reasoningEfforts: model.reasoningEfforts } : {}),
239
+ ...(model.defaultReasoningEffort ? { defaultReasoningEffort: model.defaultReasoningEffort } : {}),
240
+ ...(model.chatCompletionsCompat ? { chatCompletionsCompat: model.chatCompletionsCompat } : {}),
241
+ ...(model.cacheRetention ? { cacheRetention: model.cacheRetention } : {}),
242
+ ...(model.sessionAffinity ? { sessionAffinity: model.sessionAffinity } : {}),
243
+ contextWindow: model.contextWindow,
244
+ maxTokens: model.maxOutputTokens,
245
+ cost: Object.fromEntries(Object.entries(model.cost).map(([key, value]) => [key, value * 1_000_000])),
246
+ };
247
+ }
125
248
  function parseSettings(value, settingsPath) {
126
249
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
127
250
  throw new Error(`${settingsPath}: root value must be an object`);
@@ -149,12 +272,42 @@ function parseSettings(value, settingsPath) {
149
272
  ? undefined
150
273
  : requiredString(record.defaultProvider, "defaultProvider", settingsPath);
151
274
  const defaultModel = record.defaultModel === undefined ? undefined : requiredString(record.defaultModel, "defaultModel", settingsPath);
275
+ const thinkingLevels = parseThinkingLevels(record.thinkingLevels, settingsPath);
276
+ if (record.locale !== undefined && !isLocale(record.locale)) {
277
+ throw new Error(`${settingsPath}: locale must be "en" or "zh-CN"`);
278
+ }
152
279
  return {
153
280
  providers,
154
281
  ...(defaultProvider === undefined ? {} : { defaultProvider }),
155
282
  ...(defaultModel === undefined ? {} : { defaultModel }),
283
+ ...(thinkingLevels === undefined ? {} : { thinkingLevels }),
284
+ ...(record.locale === undefined ? {} : { locale: record.locale }),
156
285
  };
157
286
  }
287
+ function parseThinkingLevels(value, settingsPath) {
288
+ if (value === undefined)
289
+ return undefined;
290
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
291
+ throw new Error(`${settingsPath}: thinkingLevels must be an object`);
292
+ }
293
+ const preferences = {};
294
+ for (const [providerId, models] of Object.entries(value)) {
295
+ requiredString(providerId, "thinkingLevels provider ID", settingsPath);
296
+ if (typeof models !== "object" || models === null || Array.isArray(models)) {
297
+ throw new Error(`${settingsPath}: thinkingLevels.${providerId} must be an object`);
298
+ }
299
+ const modelPreferences = {};
300
+ for (const [modelId, level] of Object.entries(models)) {
301
+ requiredString(modelId, `thinkingLevels.${providerId} model ID`, settingsPath);
302
+ if (level !== "low" && level !== "medium" && level !== "high" && level !== "max") {
303
+ throw new Error(`${settingsPath}: thinkingLevels.${providerId}.${modelId} must be a valid thinking level`);
304
+ }
305
+ modelPreferences[modelId] = level;
306
+ }
307
+ preferences[providerId] = modelPreferences;
308
+ }
309
+ return preferences;
310
+ }
158
311
  async function readSettingsFile(path, displayPath) {
159
312
  try {
160
313
  const source = await readFile(path, "utf8");
@@ -187,12 +340,29 @@ function mergeSettings(globalSettings, projectSettings) {
187
340
  const defaultProvider = projectSettings?.defaultProvider ?? globalSettings?.defaultProvider;
188
341
  const defaultModel = projectSettings?.defaultModel ??
189
342
  (projectSettings?.defaultProvider === undefined ? globalSettings?.defaultModel : undefined);
343
+ const locale = globalSettings?.locale;
190
344
  return {
191
345
  providers,
192
346
  ...(defaultProvider === undefined ? {} : { defaultProvider }),
193
347
  ...(defaultModel === undefined ? {} : { defaultModel }),
348
+ ...(globalSettings?.thinkingLevels === undefined ? {} : { thinkingLevels: globalSettings.thinkingLevels }),
349
+ ...(locale === undefined ? {} : { locale }),
194
350
  };
195
351
  }
352
+ /** Persists a user-level terminal language preference without changing project settings. */
353
+ export async function saveGlobalLocale(agentDir, locale) {
354
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
355
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
356
+ const settings = {
357
+ providers: existingSettings?.providers ?? {},
358
+ ...(existingSettings?.defaultProvider === undefined ? {} : { defaultProvider: existingSettings.defaultProvider }),
359
+ ...(existingSettings?.defaultModel === undefined ? {} : { defaultModel: existingSettings.defaultModel }),
360
+ ...(existingSettings?.thinkingLevels === undefined ? {} : { thinkingLevels: existingSettings.thinkingLevels }),
361
+ locale,
362
+ };
363
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
364
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
365
+ }
196
366
  export async function saveGlobalProviderApiKey(agentDir, providerId, api, apiKey, modelId) {
197
367
  const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
198
368
  const trimmedApiKey = apiKey.trim();
@@ -207,7 +377,69 @@ export async function saveGlobalProviderApiKey(agentDir, providerId, api, apiKey
207
377
  await mkdir(agentDir, { recursive: true, mode: 0o700 });
208
378
  await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
209
379
  }
210
- /** Removes only the persisted credential for one provider; environment variables remain untouched. */
380
+ /** Persists the interactive model selection without changing credentials or Provider configuration. */
381
+ export async function saveGlobalModelSelection(agentDir, providerId, modelId) {
382
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
383
+ const normalizedProviderId = requiredString(providerId, "providerId", settingsFilePath);
384
+ const normalizedModelId = requiredString(modelId, "modelId", settingsFilePath);
385
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
386
+ const settings = mergeSettings(existingSettings, {
387
+ providers: {},
388
+ defaultProvider: normalizedProviderId,
389
+ defaultModel: normalizedModelId,
390
+ });
391
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
392
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
393
+ }
394
+ /** Persists a per-provider/model thinking preference in user settings. */
395
+ export async function saveGlobalThinkingLevel(agentDir, providerId, modelId, level) {
396
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
397
+ const normalizedProviderId = requiredString(providerId, "providerId", settingsFilePath);
398
+ const normalizedModelId = requiredString(modelId, "modelId", settingsFilePath);
399
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
400
+ const settings = {
401
+ ...mergeSettings(existingSettings, undefined),
402
+ thinkingLevels: {
403
+ ...(existingSettings?.thinkingLevels ?? {}),
404
+ [normalizedProviderId]: {
405
+ ...(existingSettings?.thinkingLevels?.[normalizedProviderId] ?? {}),
406
+ [normalizedModelId]: level,
407
+ },
408
+ },
409
+ };
410
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
411
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
412
+ }
413
+ /**
414
+ * Replaces the user-managed `custom` provider while retaining other global
415
+ * providers and preferences. The key is intentionally stored only here, never
416
+ * in project settings or process environment.
417
+ */
418
+ export async function saveGlobalCustomProvider(agentDir, input) {
419
+ const configuration = createCustomProviderConfiguration(input);
420
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
421
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
422
+ const settings = mergeSettings(existingSettings, {
423
+ providers: {
424
+ custom: {
425
+ name: configuration.name,
426
+ api: configuration.api,
427
+ baseUrl: configuration.baseUrl,
428
+ apiKey: configuration.apiKey,
429
+ models: configuration.models?.map(serializeModel),
430
+ },
431
+ },
432
+ defaultProvider: "custom",
433
+ defaultModel: configuration.models?.[0]?.id,
434
+ });
435
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
436
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
437
+ return configuration;
438
+ }
439
+ /**
440
+ * Removes one persisted credential and clears it as the default when applicable.
441
+ * Environment variables and other Provider configuration remain untouched.
442
+ */
211
443
  export async function removeGlobalProviderApiKey(agentDir, providerId) {
212
444
  const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
213
445
  const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
@@ -215,12 +447,20 @@ export async function removeGlobalProviderApiKey(agentDir, providerId) {
215
447
  if (!provider || provider.apiKey === undefined)
216
448
  return false;
217
449
  const { apiKey: _apiKey, ...providerWithoutApiKey } = provider;
450
+ const clearsDefault = existingSettings.defaultProvider === providerId;
218
451
  const settings = {
219
- ...existingSettings,
220
452
  providers: {
221
453
  ...existingSettings.providers,
222
454
  [providerId]: providerWithoutApiKey,
223
455
  },
456
+ ...(clearsDefault || existingSettings.defaultProvider === undefined
457
+ ? {}
458
+ : { defaultProvider: existingSettings.defaultProvider }),
459
+ ...(clearsDefault || existingSettings.defaultModel === undefined
460
+ ? {}
461
+ : { defaultModel: existingSettings.defaultModel }),
462
+ ...(existingSettings.thinkingLevels === undefined ? {} : { thinkingLevels: existingSettings.thinkingLevels }),
463
+ ...(existingSettings.locale === undefined ? {} : { locale: existingSettings.locale }),
224
464
  };
225
465
  await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
226
466
  return true;
@@ -254,7 +494,9 @@ function selectProviderModel(provider, providerId, env) {
254
494
  ? provider.models.find((candidate) => candidate.id === "gpt-4o")?.id
255
495
  : providerId === "zhipu"
256
496
  ? provider.models.find((candidate) => candidate.id === "glm-5.3")?.id
257
- : undefined) ||
497
+ : providerId === "kimi"
498
+ ? provider.models.find((candidate) => candidate.id === "k3")?.id
499
+ : undefined) ||
258
500
  provider.models[0]?.id;
259
501
  if (!modelId)
260
502
  throw new Error("DI_CODE_MODEL is required when the selected provider has no models");
@@ -282,6 +524,10 @@ function createBuiltInRuntime(env, providerId) {
282
524
  const provider = createZhipuProvider({ env });
283
525
  return { provider, model: selectProviderModel(provider, providerId, env) };
284
526
  }
527
+ if (providerId === "kimi") {
528
+ const provider = createKimiProvider({ env });
529
+ return { provider, model: selectProviderModel(provider, providerId, env) };
530
+ }
285
531
  return undefined;
286
532
  }
287
533
  function createConfiguredRuntime(env, configuration) {
@@ -293,13 +539,14 @@ function createConfiguredRuntime(env, configuration) {
293
539
  if (configuration.id !== "openai" &&
294
540
  configuration.id !== "deepseek" &&
295
541
  configuration.id !== "zhipu" &&
542
+ configuration.id !== "kimi" &&
296
543
  configuration.id !== "anthropic" &&
297
544
  !configuration.models) {
298
545
  throw new Error(`${SETTINGS_PATH}: providers.${configuration.id}.models is required for a custom provider`);
299
546
  }
300
547
  const provider = configuration.api === "openai-chat-completions"
301
548
  ? createOpenAIChatCompletionsProvider({
302
- env: configuration.id === "zhipu" || configuration.id === "deepseek"
549
+ env: configuration.id === "zhipu" || configuration.id === "deepseek" || configuration.id === "kimi"
303
550
  ? env
304
551
  : Object.fromEntries(Object.entries(env).filter(([name]) => name !== "OPENAI_API_KEY" && name !== "OPENAI_BASE_URL")),
305
552
  models: configuration.models,
@@ -319,7 +566,13 @@ function createConfiguredRuntime(env, configuration) {
319
566
  baseUrlEnvironmentVariable: "DEEPSEEK_BASE_URL",
320
567
  defaultBaseUrl: "https://api.deepseek.com",
321
568
  }
322
- : {}),
569
+ : configuration.id === "kimi"
570
+ ? {
571
+ apiKeyEnvironmentVariable: "KIMI_API_KEY",
572
+ baseUrlEnvironmentVariable: "KIMI_BASE_URL",
573
+ defaultBaseUrl: "https://api.kimi.com/coding/v1",
574
+ }
575
+ : {}),
323
576
  })
324
577
  : configuration.api === "anthropic-messages"
325
578
  ? createAnthropicProvider({
@@ -380,12 +633,21 @@ export function resolveStartupRuntime(env, providers, defaults = {}) {
380
633
  return builtInRuntime;
381
634
  throw new Error(`Unknown configured provider "${selectedProviderId}". Available providers: ${providers.map((provider) => provider.id).join(", ")}.`);
382
635
  }
636
+ /** Returns a stored preference only when the selected model still supports it. */
637
+ export function resolveThinkingLevelPreference(configuration, runtime) {
638
+ const preference = configuration?.thinkingLevels?.[runtime.provider.id]?.[runtime.model.id];
639
+ return preference !== undefined && runtime.model.reasoningEfforts?.includes(preference) ? preference : undefined;
640
+ }
383
641
  export async function loadStartupConfiguration(cwd, environment = process.env, agentDir = join(homedir(), ".di-code")) {
384
642
  const [globalSettings, projectSettings] = await Promise.all([
385
643
  readSettingsFile(join(agentDir, SETTINGS_FILE_NAME), join(agentDir, SETTINGS_FILE_NAME)),
386
644
  readSettingsFile(join(cwd, SETTINGS_PATH), SETTINGS_PATH),
387
645
  ]);
388
646
  const settings = mergeSettings(globalSettings, projectSettings);
647
+ const requestedLocale = environment.DI_CODE_LOCALE?.trim();
648
+ if (requestedLocale !== undefined && requestedLocale !== "" && !isLocale(requestedLocale)) {
649
+ throw new Error('DI_CODE_LOCALE must be "en" or "zh-CN".');
650
+ }
389
651
  const providers = Object.entries(settings.providers).map(([id, provider]) => ({
390
652
  id: requiredString(id, `providers.${id}`),
391
653
  name: provider.name,
@@ -397,7 +659,14 @@ export async function loadStartupConfiguration(cwd, environment = process.env, a
397
659
  const defaults = settings.defaultProvider === undefined && settings.defaultModel === undefined
398
660
  ? undefined
399
661
  : { providerId: settings.defaultProvider, modelId: settings.defaultModel };
400
- return { environment, providers, ...(defaults === undefined ? {} : { defaults }) };
662
+ const locale = requestedLocale && isLocale(requestedLocale) ? requestedLocale : settings.locale;
663
+ return {
664
+ environment,
665
+ providers,
666
+ ...(settings.thinkingLevels === undefined ? {} : { thinkingLevels: settings.thinkingLevels }),
667
+ ...(locale === undefined ? {} : { locale }),
668
+ ...(defaults === undefined ? {} : { defaults }),
669
+ };
401
670
  }
402
671
  export async function loadStartupEnvironment(cwd, environment = process.env) {
403
672
  return (await loadStartupConfiguration(cwd, environment)).environment;