@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.
- package/README.md +79 -42
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +96 -17
- package/dist/cli.js.map +1 -1
- package/dist/core/clipboard-image.d.ts +3 -3
- package/dist/core/clipboard-image.d.ts.map +1 -1
- package/dist/core/clipboard-image.js +26 -13
- package/dist/core/clipboard-image.js.map +1 -1
- package/dist/core/resources/loader.d.ts.map +1 -1
- package/dist/core/resources/loader.js +15 -42
- package/dist/core/resources/loader.js.map +1 -1
- package/dist/core/resources/types.d.ts +6 -9
- package/dist/core/resources/types.d.ts.map +1 -1
- package/dist/core/resources/types.js.map +1 -1
- package/dist/core/session/session-manager.d.ts +12 -1
- package/dist/core/session/session-manager.d.ts.map +1 -1
- package/dist/core/session/session-manager.js +66 -3
- package/dist/core/session/session-manager.js.map +1 -1
- package/dist/core/session/session-storage.d.ts.map +1 -1
- package/dist/core/session/session-storage.js +29 -15
- package/dist/core/session/session-storage.js.map +1 -1
- package/dist/core/session/types.d.ts +6 -1
- package/dist/core/session/types.d.ts.map +1 -1
- package/dist/core/session/types.js +1 -1
- package/dist/core/session/types.js.map +1 -1
- package/dist/core/session.d.ts +23 -1
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +86 -13
- package/dist/core/session.js.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/file-search.d.ts +14 -0
- package/dist/core/tools/file-search.d.ts.map +1 -0
- package/dist/core/tools/file-search.js +99 -0
- package/dist/core/tools/file-search.js.map +1 -0
- package/dist/core/tools/glob.d.ts +11 -0
- package/dist/core/tools/glob.d.ts.map +1 -0
- package/dist/core/tools/glob.js +44 -0
- package/dist/core/tools/glob.js.map +1 -0
- package/dist/core/tools/grep.d.ts +13 -0
- package/dist/core/tools/grep.d.ts.map +1 -0
- package/dist/core/tools/grep.js +67 -0
- package/dist/core/tools/grep.js.map +1 -0
- package/dist/core/tools/load-skill.d.ts +10 -0
- package/dist/core/tools/load-skill.d.ts.map +1 -0
- package/dist/core/tools/load-skill.js +22 -0
- package/dist/core/tools/load-skill.js.map +1 -0
- package/dist/core/tools/path-boundary.d.ts +2 -0
- package/dist/core/tools/path-boundary.d.ts.map +1 -1
- package/dist/core/tools/path-boundary.js +4 -0
- package/dist/core/tools/path-boundary.js.map +1 -1
- package/dist/core/user-data.d.ts +3 -0
- package/dist/core/user-data.d.ts.map +1 -0
- package/dist/core/user-data.js +8 -0
- package/dist/core/user-data.js.map +1 -0
- package/dist/entry.js +13 -0
- package/dist/entry.js.map +1 -1
- package/dist/extensions/runtime.js +2 -2
- package/dist/extensions/runtime.js.map +1 -1
- package/dist/i18n.d.ts +5 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +225 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +17 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +243 -11
- package/dist/main.js.map +1 -1
- package/dist/mcp/config.d.ts +33 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +211 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/loader.d.ts +21 -0
- package/dist/mcp/loader.d.ts.map +1 -0
- package/dist/mcp/loader.js +60 -0
- package/dist/mcp/loader.js.map +1 -0
- package/dist/mcp/tool-adapter.d.ts +5 -0
- package/dist/mcp/tool-adapter.d.ts.map +1 -0
- package/dist/mcp/tool-adapter.js +149 -0
- package/dist/mcp/tool-adapter.js.map +1 -0
- package/dist/modes/interactive-components.d.ts +2 -2
- package/dist/modes/interactive-components.d.ts.map +1 -1
- package/dist/modes/interactive-components.js +30 -33
- package/dist/modes/interactive-components.js.map +1 -1
- package/dist/modes/interactive-state.d.ts.map +1 -1
- package/dist/modes/interactive-state.js +2 -1
- package/dist/modes/interactive-state.js.map +1 -1
- package/dist/modes/interactive.d.ts +9 -0
- package/dist/modes/interactive.d.ts.map +1 -1
- package/dist/modes/interactive.js +209 -63
- package/dist/modes/interactive.js.map +1 -1
- package/dist/modes/tree-selector.d.ts +30 -0
- package/dist/modes/tree-selector.d.ts.map +1 -0
- package/dist/modes/tree-selector.js +205 -0
- package/dist/modes/tree-selector.js.map +1 -0
- package/dist/plugins/loader.d.ts +19 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +21 -2
- package/dist/plugins/loader.js.map +1 -1
- package/dist/provider-onboarding.d.ts.map +1 -1
- package/dist/provider-onboarding.js +119 -22
- package/dist/provider-onboarding.js.map +1 -1
- package/dist/startup.d.ts +34 -2
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +278 -9
- package/dist/startup.js.map +1 -1
- 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
|
|
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
|
-
/**
|
|
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
|
-
:
|
|
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
|
-
|
|
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;
|