@di-code/coding-agent 0.1.3 → 0.1.6

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 (147) hide show
  1. package/README.md +91 -30
  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/context-builder.d.ts.map +1 -1
  11. package/dist/core/context-builder.js +6 -2
  12. package/dist/core/context-builder.js.map +1 -1
  13. package/dist/core/image-input.d.ts +1 -1
  14. package/dist/core/image-input.d.ts.map +1 -1
  15. package/dist/core/image-input.js +55 -10
  16. package/dist/core/image-input.js.map +1 -1
  17. package/dist/core/resources/loader.d.ts.map +1 -1
  18. package/dist/core/resources/loader.js +15 -42
  19. package/dist/core/resources/loader.js.map +1 -1
  20. package/dist/core/resources/types.d.ts +6 -9
  21. package/dist/core/resources/types.d.ts.map +1 -1
  22. package/dist/core/resources/types.js.map +1 -1
  23. package/dist/core/session/session-manager.d.ts +12 -1
  24. package/dist/core/session/session-manager.d.ts.map +1 -1
  25. package/dist/core/session/session-manager.js +66 -3
  26. package/dist/core/session/session-manager.js.map +1 -1
  27. package/dist/core/session/session-storage.d.ts.map +1 -1
  28. package/dist/core/session/session-storage.js +30 -15
  29. package/dist/core/session/session-storage.js.map +1 -1
  30. package/dist/core/session/types.d.ts +6 -1
  31. package/dist/core/session/types.d.ts.map +1 -1
  32. package/dist/core/session/types.js +1 -1
  33. package/dist/core/session/types.js.map +1 -1
  34. package/dist/core/session.d.ts +38 -3
  35. package/dist/core/session.d.ts.map +1 -1
  36. package/dist/core/session.js +167 -9
  37. package/dist/core/session.js.map +1 -1
  38. package/dist/core/system-prompt.js +2 -2
  39. package/dist/core/system-prompt.js.map +1 -1
  40. package/dist/core/tools/bash.d.ts +2 -2
  41. package/dist/core/tools/bash.d.ts.map +1 -1
  42. package/dist/core/tools/bash.js.map +1 -1
  43. package/dist/core/tools/edit-diff.d.ts +25 -0
  44. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  45. package/dist/core/tools/edit-diff.js +136 -0
  46. package/dist/core/tools/edit-diff.js.map +1 -0
  47. package/dist/core/tools/edit.d.ts +13 -4
  48. package/dist/core/tools/edit.d.ts.map +1 -1
  49. package/dist/core/tools/edit.js +64 -30
  50. package/dist/core/tools/edit.js.map +1 -1
  51. package/dist/core/tools/file-search.d.ts +14 -0
  52. package/dist/core/tools/file-search.d.ts.map +1 -0
  53. package/dist/core/tools/file-search.js +99 -0
  54. package/dist/core/tools/file-search.js.map +1 -0
  55. package/dist/core/tools/glob.d.ts +11 -0
  56. package/dist/core/tools/glob.d.ts.map +1 -0
  57. package/dist/core/tools/glob.js +44 -0
  58. package/dist/core/tools/glob.js.map +1 -0
  59. package/dist/core/tools/grep.d.ts +13 -0
  60. package/dist/core/tools/grep.d.ts.map +1 -0
  61. package/dist/core/tools/grep.js +67 -0
  62. package/dist/core/tools/grep.js.map +1 -0
  63. package/dist/core/tools/load-skill.d.ts +10 -0
  64. package/dist/core/tools/load-skill.d.ts.map +1 -0
  65. package/dist/core/tools/load-skill.js +22 -0
  66. package/dist/core/tools/load-skill.js.map +1 -0
  67. package/dist/core/tools/path-boundary.d.ts +2 -0
  68. package/dist/core/tools/path-boundary.d.ts.map +1 -1
  69. package/dist/core/tools/path-boundary.js +4 -0
  70. package/dist/core/tools/path-boundary.js.map +1 -1
  71. package/dist/core/tools/read.d.ts +2 -2
  72. package/dist/core/tools/read.d.ts.map +1 -1
  73. package/dist/core/tools/read.js.map +1 -1
  74. package/dist/core/tools/write.d.ts +2 -2
  75. package/dist/core/tools/write.d.ts.map +1 -1
  76. package/dist/core/tools/write.js.map +1 -1
  77. package/dist/core/user-data.d.ts +3 -0
  78. package/dist/core/user-data.d.ts.map +1 -0
  79. package/dist/core/user-data.js +8 -0
  80. package/dist/core/user-data.js.map +1 -0
  81. package/dist/entry.js +22 -2
  82. package/dist/entry.js.map +1 -1
  83. package/dist/extensions/runtime.js +2 -2
  84. package/dist/extensions/runtime.js.map +1 -1
  85. package/dist/i18n.d.ts +5 -0
  86. package/dist/i18n.d.ts.map +1 -0
  87. package/dist/i18n.js +223 -0
  88. package/dist/i18n.js.map +1 -0
  89. package/dist/index.d.ts +3 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +3 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/main.d.ts +20 -1
  94. package/dist/main.d.ts.map +1 -1
  95. package/dist/main.js +272 -17
  96. package/dist/main.js.map +1 -1
  97. package/dist/mcp/config.d.ts +33 -0
  98. package/dist/mcp/config.d.ts.map +1 -0
  99. package/dist/mcp/config.js +211 -0
  100. package/dist/mcp/config.js.map +1 -0
  101. package/dist/mcp/loader.d.ts +21 -0
  102. package/dist/mcp/loader.d.ts.map +1 -0
  103. package/dist/mcp/loader.js +60 -0
  104. package/dist/mcp/loader.js.map +1 -0
  105. package/dist/mcp/tool-adapter.d.ts +5 -0
  106. package/dist/mcp/tool-adapter.d.ts.map +1 -0
  107. package/dist/mcp/tool-adapter.js +149 -0
  108. package/dist/mcp/tool-adapter.js.map +1 -0
  109. package/dist/modes/interactive-components.d.ts +2 -0
  110. package/dist/modes/interactive-components.d.ts.map +1 -1
  111. package/dist/modes/interactive-components.js +55 -31
  112. package/dist/modes/interactive-components.js.map +1 -1
  113. package/dist/modes/interactive-diff.d.ts +8 -0
  114. package/dist/modes/interactive-diff.d.ts.map +1 -0
  115. package/dist/modes/interactive-diff.js +78 -0
  116. package/dist/modes/interactive-diff.js.map +1 -0
  117. package/dist/modes/interactive-layout.d.ts +4 -0
  118. package/dist/modes/interactive-layout.d.ts.map +1 -1
  119. package/dist/modes/interactive-layout.js +4 -0
  120. package/dist/modes/interactive-layout.js.map +1 -1
  121. package/dist/modes/interactive-state.d.ts +5 -0
  122. package/dist/modes/interactive-state.d.ts.map +1 -1
  123. package/dist/modes/interactive-state.js +65 -11
  124. package/dist/modes/interactive-state.js.map +1 -1
  125. package/dist/modes/interactive.d.ts +19 -0
  126. package/dist/modes/interactive.d.ts.map +1 -1
  127. package/dist/modes/interactive.js +351 -52
  128. package/dist/modes/interactive.js.map +1 -1
  129. package/dist/modes/tree-selector.d.ts +30 -0
  130. package/dist/modes/tree-selector.d.ts.map +1 -0
  131. package/dist/modes/tree-selector.js +205 -0
  132. package/dist/modes/tree-selector.js.map +1 -0
  133. package/dist/plugins/loader.d.ts +19 -1
  134. package/dist/plugins/loader.d.ts.map +1 -1
  135. package/dist/plugins/loader.js +21 -2
  136. package/dist/plugins/loader.js.map +1 -1
  137. package/dist/provider-onboarding.d.ts +8 -1
  138. package/dist/provider-onboarding.d.ts.map +1 -1
  139. package/dist/provider-onboarding.js +160 -40
  140. package/dist/provider-onboarding.js.map +1 -1
  141. package/dist/rpc-entry.js +1 -1
  142. package/dist/rpc-entry.js.map +1 -1
  143. package/dist/startup.d.ts +34 -3
  144. package/dist/startup.d.ts.map +1 -1
  145. package/dist/startup.js +407 -87
  146. package/dist/startup.js.map +1 -1
  147. package/package.json +7 -4
@@ -1 +1 @@
1
- {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAEA,OAAO,EAON,KAAK,KAAK,EACV,KAAK,QAAQ,EACb,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,SAAS,4BAA4B,EAAE,CAAC;CAC5D;AAQD,KAAK,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAehE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAE7E;AAmPD,wBAAgB,qBAAqB,CACpC,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,SAAS,4BAA4B,EAAE,GAChD,cAAc,CAgBhB;AAED,wBAAsB,wBAAwB,CAC7C,GAAG,EAAE,MAAM,EACX,WAAW,GAAE,WAAyB,GACpC,OAAO,CAAC,oBAAoB,CAAC,CAuB/B;AAED,wBAAsB,sBAAsB,CAC3C,GAAG,EAAE,MAAM,EACX,WAAW,GAAE,WAAyB,GACpC,OAAO,CAAC,WAAW,CAAC,CAEtB"}
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"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"}
package/dist/startup.js CHANGED
@@ -1,119 +1,397 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
3
- import { createAnthropicProvider, createDeepSeekProvider, createFauxProvider, 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";
4
6
  const FAUX_RESPONSES = [
5
- { type: "success", content: [{ type: "text", text: "Faux response." }] },
7
+ {
8
+ type: "success",
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: "你好,我是di-code,一个面向终端的 TypeScript AI Coding Agent,支持多 Provider 流式对话、工具调用、JSONL 会话持久化、交互式 TUI、插件扩展与 JSONL RPC 集成。",
13
+ },
14
+ ],
15
+ },
16
+ { type: "success", content: [{ type: "text", text: "当前回复为faux数据" }] },
6
17
  ];
7
- const SETTINGS_PATH = join(".di-code", "settings.json");
18
+ const SETTINGS_FILE_NAME = "settings.json";
19
+ const SETTINGS_PATH = join(".di-code", SETTINGS_FILE_NAME);
8
20
  export function resolveStartupArgs(args) {
9
21
  return args.length === 0 ? ["--interactive"] : args;
10
22
  }
11
- function optionalString(value, path) {
23
+ function optionalString(value, path, settingsPath) {
12
24
  if (value === undefined)
13
25
  return undefined;
14
26
  if (typeof value !== "string")
15
- throw new Error(`${SETTINGS_PATH}: ${path} must be a string`);
27
+ throw new Error(`${settingsPath}: ${path} must be a string`);
16
28
  return value;
17
29
  }
18
- function requiredString(value, path) {
19
- const result = optionalString(value, path);
30
+ function requiredString(value, path, settingsPath = SETTINGS_PATH) {
31
+ const result = optionalString(value, path, settingsPath);
20
32
  if (!result?.trim())
21
- throw new Error(`${SETTINGS_PATH}: ${path} must be a non-empty string`);
33
+ throw new Error(`${settingsPath}: ${path} must be a non-empty string`);
22
34
  return result.trim();
23
35
  }
24
- function positiveInteger(value, path, fallback) {
36
+ function positiveInteger(value, path, fallback, settingsPath) {
25
37
  const result = value ?? fallback;
26
38
  if (!Number.isInteger(result) || result <= 0) {
27
- throw new Error(`${SETTINGS_PATH}: ${path} must be a positive integer`);
39
+ throw new Error(`${settingsPath}: ${path} must be a positive integer`);
28
40
  }
29
41
  return result;
30
42
  }
31
- function nonNegativeNumber(value, path, fallback) {
43
+ function nonNegativeNumber(value, path, fallback, settingsPath) {
32
44
  const result = value ?? fallback;
33
45
  if (typeof result !== "number" || !Number.isFinite(result) || result < 0) {
34
- throw new Error(`${SETTINGS_PATH}: ${path} must be a non-negative finite number`);
46
+ throw new Error(`${settingsPath}: ${path} must be a non-negative finite number`);
35
47
  }
36
48
  return result;
37
49
  }
38
- function parseModels(value, path, providerId, providerApi, providerBaseUrl) {
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
+ }
87
+ function parseModels(value, path, providerId, providerApi, providerBaseUrl, settingsPath) {
39
88
  if (value === undefined)
40
89
  return undefined;
41
90
  if (!Array.isArray(value))
42
- throw new Error(`${SETTINGS_PATH}: ${path} must be an array`);
91
+ throw new Error(`${settingsPath}: ${path} must be an array`);
43
92
  return value.map((entry, index) => {
44
93
  const modelPath = `${path}[${index}]`;
45
94
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
46
- throw new Error(`${SETTINGS_PATH}: ${modelPath} must be an object`);
95
+ throw new Error(`${settingsPath}: ${modelPath} must be an object`);
47
96
  }
48
97
  const model = entry;
49
98
  const input = model.input ?? ["text"];
50
99
  if (!Array.isArray(input) || input.length === 0 || input.some((item) => item !== "text" && item !== "image")) {
51
- throw new Error(`${SETTINGS_PATH}: ${modelPath}.input must contain text and/or image`);
100
+ throw new Error(`${settingsPath}: ${modelPath}.input must contain text and/or image`);
52
101
  }
53
102
  const cost = model.cost ?? {};
54
103
  if (typeof cost !== "object" || cost === null || Array.isArray(cost)) {
55
- throw new Error(`${SETTINGS_PATH}: ${modelPath}.cost must be an object`);
104
+ throw new Error(`${settingsPath}: ${modelPath}.cost must be an object`);
56
105
  }
57
106
  const prices = cost;
58
107
  if (model.reasoning !== undefined && typeof model.reasoning !== "boolean") {
59
- throw new Error(`${SETTINGS_PATH}: ${modelPath}.reasoning must be a boolean`);
108
+ throw new Error(`${settingsPath}: ${modelPath}.reasoning must be a boolean`);
109
+ }
110
+ const configuredReasoningEfforts = model.reasoningEfforts;
111
+ if (configuredReasoningEfforts !== undefined) {
112
+ if (!Array.isArray(configuredReasoningEfforts) || configuredReasoningEfforts.length === 0) {
113
+ throw new Error(`${settingsPath}: ${modelPath}.reasoningEfforts must be a non-empty array`);
114
+ }
115
+ if (model.reasoning !== true ||
116
+ configuredReasoningEfforts.some((effort) => effort !== "low" && effort !== "medium" && effort !== "high" && effort !== "max") ||
117
+ new Set(configuredReasoningEfforts).size !== configuredReasoningEfforts.length) {
118
+ throw new Error(`${settingsPath}: ${modelPath}.reasoningEfforts requires reasoning=true and unique low, medium, high, or max values`);
119
+ }
60
120
  }
61
121
  if (model.cacheRetention !== undefined && model.cacheRetention !== "long") {
62
- throw new Error(`${SETTINGS_PATH}: ${modelPath}.cacheRetention must be "long" when provided`);
122
+ throw new Error(`${settingsPath}: ${modelPath}.cacheRetention must be "long" when provided`);
63
123
  }
64
124
  if (model.sessionAffinity !== undefined && model.sessionAffinity !== "codex") {
65
- throw new Error(`${SETTINGS_PATH}: ${modelPath}.sessionAffinity must be "codex" when provided`);
125
+ throw new Error(`${settingsPath}: ${modelPath}.sessionAffinity must be "codex" when provided`);
66
126
  }
67
- const api = optionalString(model.api, `${modelPath}.api`) ?? providerApi;
127
+ const api = optionalString(model.api, `${modelPath}.api`, settingsPath) ?? providerApi;
68
128
  if (!api)
69
- throw new Error(`${SETTINGS_PATH}: ${modelPath}.api is required`);
70
- const baseUrl = optionalString(model.baseUrl, `${modelPath}.baseUrl`) ?? providerBaseUrl;
71
- const id = requiredString(model.id, `${modelPath}.id`);
129
+ throw new Error(`${settingsPath}: ${modelPath}.api is required`);
130
+ if (!["faux", "openai-responses", "openai-chat-completions", "anthropic-messages"].includes(api)) {
131
+ throw new Error(`${settingsPath}: ${modelPath}.api is unsupported`);
132
+ }
133
+ const typedApi = api;
134
+ const reasoningEfforts = configuredReasoningEfforts === undefined && model.reasoning === true && api === "openai-responses"
135
+ ? ["low", "medium", "high"]
136
+ : configuredReasoningEfforts === undefined
137
+ ? undefined
138
+ : [...configuredReasoningEfforts];
139
+ const baseUrl = optionalString(model.baseUrl, `${modelPath}.baseUrl`, settingsPath) ?? providerBaseUrl;
140
+ const id = requiredString(model.id, `${modelPath}.id`, settingsPath);
141
+ const chatCompletionsCompat = parseChatCompletionsCompat(model.chatCompletionsCompat, `${modelPath}.chatCompletionsCompat`, settingsPath);
142
+ if (chatCompletionsCompat !== undefined && typedApi !== "openai-chat-completions") {
143
+ throw new Error(`${settingsPath}: ${modelPath}.chatCompletionsCompat requires api "openai-chat-completions"`);
144
+ }
72
145
  return {
73
146
  id,
74
- name: requiredString(model.name ?? id, `${modelPath}.name`),
147
+ name: requiredString(model.name ?? id, `${modelPath}.name`, settingsPath),
75
148
  provider: providerId,
76
- api,
149
+ api: typedApi,
77
150
  ...(baseUrl === undefined ? {} : { baseUrl }),
78
151
  input: [...input],
79
152
  reasoning: model.reasoning ?? false,
153
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
154
+ ...(chatCompletionsCompat ? { chatCompletionsCompat } : {}),
80
155
  ...(model.cacheRetention === "long" ? { cacheRetention: "long" } : {}),
81
156
  ...(model.sessionAffinity === "codex" ? { sessionAffinity: "codex" } : {}),
82
- contextWindow: positiveInteger(model.contextWindow, `${modelPath}.contextWindow`, 128_000),
83
- maxOutputTokens: positiveInteger(model.maxTokens ?? model.maxOutputTokens, `${modelPath}.maxTokens`, 16_384),
157
+ contextWindow: positiveInteger(model.contextWindow, `${modelPath}.contextWindow`, 128_000, settingsPath),
158
+ maxOutputTokens: positiveInteger(model.maxTokens ?? model.maxOutputTokens, `${modelPath}.maxTokens`, 16_384, settingsPath),
84
159
  cost: {
85
- input: nonNegativeNumber(prices.input, `${modelPath}.cost.input`, 0) / 1_000_000,
86
- output: nonNegativeNumber(prices.output, `${modelPath}.cost.output`, 0) / 1_000_000,
87
- cacheRead: nonNegativeNumber(prices.cacheRead, `${modelPath}.cost.cacheRead`, 0) / 1_000_000,
88
- cacheWrite: nonNegativeNumber(prices.cacheWrite, `${modelPath}.cost.cacheWrite`, 0) / 1_000_000,
160
+ input: nonNegativeNumber(prices.input, `${modelPath}.cost.input`, 0, settingsPath) / 1_000_000,
161
+ output: nonNegativeNumber(prices.output, `${modelPath}.cost.output`, 0, settingsPath) / 1_000_000,
162
+ cacheRead: nonNegativeNumber(prices.cacheRead, `${modelPath}.cost.cacheRead`, 0, settingsPath) / 1_000_000,
163
+ cacheWrite: nonNegativeNumber(prices.cacheWrite, `${modelPath}.cost.cacheWrite`, 0, settingsPath) / 1_000_000,
89
164
  },
90
165
  };
91
166
  });
92
167
  }
93
- function parseSettings(value) {
168
+ /** Validates a Custom onboarding endpoint without issuing a network request. */
169
+ export function validateCustomBaseUrl(value) {
170
+ const baseUrl = value.trim();
171
+ if (!baseUrl)
172
+ throw new Error("Base URL cannot be empty.");
173
+ let url;
174
+ try {
175
+ url = new URL(baseUrl);
176
+ }
177
+ catch {
178
+ throw new Error("Base URL must be an absolute http or https URL.");
179
+ }
180
+ if (url.protocol !== "http:" && url.protocol !== "https:")
181
+ throw new Error("Base URL must use http or https.");
182
+ if (url.username || url.password || url.search || url.hash) {
183
+ throw new Error("Base URL must not contain credentials, query, or hash.");
184
+ }
185
+ if (baseUrl.endsWith("/"))
186
+ throw new Error("Base URL must not end with /.");
187
+ return baseUrl;
188
+ }
189
+ function requireCustomValue(value, field) {
190
+ const trimmed = value.trim();
191
+ if (!trimmed)
192
+ throw new Error(`${field === "apiKey" ? "API key" : "Model ID"} cannot be empty.`);
193
+ return trimmed;
194
+ }
195
+ /** Builds the single user-managed Custom provider from validated onboarding input. */
196
+ export function createCustomProviderConfiguration(input) {
197
+ const baseUrl = validateCustomBaseUrl(input.baseUrl);
198
+ const modelId = requireCustomValue(input.modelId, "modelId");
199
+ const apiKey = requireCustomValue(input.apiKey, "apiKey");
200
+ const known = findBuiltinModel(input.api, modelId);
201
+ const model = known
202
+ ? {
203
+ ...known,
204
+ id: modelId,
205
+ provider: "custom",
206
+ api: input.api,
207
+ baseUrl,
208
+ }
209
+ : {
210
+ id: modelId,
211
+ name: modelId,
212
+ provider: "custom",
213
+ api: input.api,
214
+ baseUrl,
215
+ input: ["text"],
216
+ reasoning: false,
217
+ contextWindow: 128_000,
218
+ maxOutputTokens: 16_384,
219
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
220
+ };
221
+ return { id: "custom", name: "Custom", api: input.api, apiKey, baseUrl, models: [model] };
222
+ }
223
+ function serializeModel(model) {
224
+ return {
225
+ id: model.id,
226
+ name: model.name,
227
+ input: model.input,
228
+ reasoning: model.reasoning,
229
+ ...(model.reasoningEfforts ? { reasoningEfforts: model.reasoningEfforts } : {}),
230
+ ...(model.chatCompletionsCompat ? { chatCompletionsCompat: model.chatCompletionsCompat } : {}),
231
+ ...(model.cacheRetention ? { cacheRetention: model.cacheRetention } : {}),
232
+ ...(model.sessionAffinity ? { sessionAffinity: model.sessionAffinity } : {}),
233
+ contextWindow: model.contextWindow,
234
+ maxTokens: model.maxOutputTokens,
235
+ cost: Object.fromEntries(Object.entries(model.cost).map(([key, value]) => [key, value * 1_000_000])),
236
+ };
237
+ }
238
+ function parseSettings(value, settingsPath) {
94
239
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
95
- throw new Error(`${SETTINGS_PATH}: root value must be an object`);
240
+ throw new Error(`${settingsPath}: root value must be an object`);
96
241
  }
97
242
  const record = value;
98
243
  const providersValue = record.providers;
99
244
  if (typeof providersValue !== "object" || providersValue === null || Array.isArray(providersValue)) {
100
- throw new Error(`${SETTINGS_PATH}: providers must be an object`);
245
+ throw new Error(`${settingsPath}: providers must be an object`);
101
246
  }
102
247
  const providers = {};
103
248
  for (const [id, entry] of Object.entries(providersValue)) {
104
249
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
105
- throw new Error(`${SETTINGS_PATH}: providers.${id} must be an object`);
250
+ throw new Error(`${settingsPath}: providers.${id} must be an object`);
106
251
  }
107
252
  const provider = entry;
108
253
  providers[id] = {
109
- name: optionalString(provider.name, `providers.${id}.name`),
110
- baseUrl: optionalString(provider.baseUrl, `providers.${id}.baseUrl`),
111
- apiKey: optionalString(provider.apiKey, `providers.${id}.apiKey`),
112
- api: optionalString(provider.api, `providers.${id}.api`),
254
+ name: optionalString(provider.name, `providers.${id}.name`, settingsPath),
255
+ baseUrl: optionalString(provider.baseUrl, `providers.${id}.baseUrl`, settingsPath),
256
+ apiKey: optionalString(provider.apiKey, `providers.${id}.apiKey`, settingsPath),
257
+ api: optionalString(provider.api, `providers.${id}.api`, settingsPath),
113
258
  models: provider.models,
114
259
  };
115
260
  }
116
- return { providers };
261
+ const defaultProvider = record.defaultProvider === undefined
262
+ ? undefined
263
+ : requiredString(record.defaultProvider, "defaultProvider", settingsPath);
264
+ const defaultModel = record.defaultModel === undefined ? undefined : requiredString(record.defaultModel, "defaultModel", settingsPath);
265
+ if (record.locale !== undefined && !isLocale(record.locale)) {
266
+ throw new Error(`${settingsPath}: locale must be "en" or "zh-CN"`);
267
+ }
268
+ return {
269
+ providers,
270
+ ...(defaultProvider === undefined ? {} : { defaultProvider }),
271
+ ...(defaultModel === undefined ? {} : { defaultModel }),
272
+ ...(record.locale === undefined ? {} : { locale: record.locale }),
273
+ };
274
+ }
275
+ async function readSettingsFile(path, displayPath) {
276
+ try {
277
+ const source = await readFile(path, "utf8");
278
+ if (source.trim().length === 0)
279
+ return undefined;
280
+ return parseSettings(JSON.parse(source), displayPath);
281
+ }
282
+ catch (cause) {
283
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
284
+ return undefined;
285
+ if (cause instanceof SyntaxError)
286
+ throw new Error(`${displayPath}: invalid JSON`, { cause });
287
+ throw cause;
288
+ }
289
+ }
290
+ function mergeSettings(globalSettings, projectSettings) {
291
+ const globalProviders = globalSettings?.providers ?? {};
292
+ const projectProviders = projectSettings?.providers ?? {};
293
+ const providers = { ...globalProviders };
294
+ for (const [id, projectProvider] of Object.entries(projectProviders)) {
295
+ providers[id] = {
296
+ ...globalProviders[id],
297
+ ...(projectProvider.name === undefined ? {} : { name: projectProvider.name }),
298
+ ...(projectProvider.baseUrl === undefined ? {} : { baseUrl: projectProvider.baseUrl }),
299
+ ...(projectProvider.apiKey === undefined ? {} : { apiKey: projectProvider.apiKey }),
300
+ ...(projectProvider.api === undefined ? {} : { api: projectProvider.api }),
301
+ ...(projectProvider.models === undefined ? {} : { models: projectProvider.models }),
302
+ };
303
+ }
304
+ const defaultProvider = projectSettings?.defaultProvider ?? globalSettings?.defaultProvider;
305
+ const defaultModel = projectSettings?.defaultModel ??
306
+ (projectSettings?.defaultProvider === undefined ? globalSettings?.defaultModel : undefined);
307
+ const locale = globalSettings?.locale;
308
+ return {
309
+ providers,
310
+ ...(defaultProvider === undefined ? {} : { defaultProvider }),
311
+ ...(defaultModel === undefined ? {} : { defaultModel }),
312
+ ...(locale === undefined ? {} : { locale }),
313
+ };
314
+ }
315
+ /** Persists a user-level terminal language preference without changing project settings. */
316
+ export async function saveGlobalLocale(agentDir, locale) {
317
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
318
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
319
+ const settings = {
320
+ providers: existingSettings?.providers ?? {},
321
+ ...(existingSettings?.defaultProvider === undefined ? {} : { defaultProvider: existingSettings.defaultProvider }),
322
+ ...(existingSettings?.defaultModel === undefined ? {} : { defaultModel: existingSettings.defaultModel }),
323
+ locale,
324
+ };
325
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
326
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
327
+ }
328
+ export async function saveGlobalProviderApiKey(agentDir, providerId, api, apiKey, modelId) {
329
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
330
+ const trimmedApiKey = apiKey.trim();
331
+ if (!trimmedApiKey)
332
+ throw new Error(`${settingsFilePath}: apiKey must be a non-empty string`);
333
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
334
+ const settings = mergeSettings(existingSettings, {
335
+ providers: { [providerId]: { api, apiKey: trimmedApiKey } },
336
+ defaultProvider: providerId,
337
+ ...(modelId === undefined ? {} : { defaultModel: modelId }),
338
+ });
339
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
340
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
341
+ }
342
+ /**
343
+ * Replaces the user-managed `custom` provider while retaining other global
344
+ * providers and preferences. The key is intentionally stored only here, never
345
+ * in project settings or process environment.
346
+ */
347
+ export async function saveGlobalCustomProvider(agentDir, input) {
348
+ const configuration = createCustomProviderConfiguration(input);
349
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
350
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
351
+ const settings = mergeSettings(existingSettings, {
352
+ providers: {
353
+ custom: {
354
+ name: configuration.name,
355
+ api: configuration.api,
356
+ baseUrl: configuration.baseUrl,
357
+ apiKey: configuration.apiKey,
358
+ models: configuration.models?.map(serializeModel),
359
+ },
360
+ },
361
+ defaultProvider: "custom",
362
+ defaultModel: configuration.models?.[0]?.id,
363
+ });
364
+ await mkdir(agentDir, { recursive: true, mode: 0o700 });
365
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
366
+ return configuration;
367
+ }
368
+ /**
369
+ * Removes one persisted credential and clears it as the default when applicable.
370
+ * Environment variables and other Provider configuration remain untouched.
371
+ */
372
+ export async function removeGlobalProviderApiKey(agentDir, providerId) {
373
+ const settingsFilePath = join(agentDir, SETTINGS_FILE_NAME);
374
+ const existingSettings = await readSettingsFile(settingsFilePath, settingsFilePath);
375
+ const provider = existingSettings?.providers[providerId];
376
+ if (!provider || provider.apiKey === undefined)
377
+ return false;
378
+ const { apiKey: _apiKey, ...providerWithoutApiKey } = provider;
379
+ const clearsDefault = existingSettings.defaultProvider === providerId;
380
+ const settings = {
381
+ providers: {
382
+ ...existingSettings.providers,
383
+ [providerId]: providerWithoutApiKey,
384
+ },
385
+ ...(clearsDefault || existingSettings.defaultProvider === undefined
386
+ ? {}
387
+ : { defaultProvider: existingSettings.defaultProvider }),
388
+ ...(clearsDefault || existingSettings.defaultModel === undefined
389
+ ? {}
390
+ : { defaultModel: existingSettings.defaultModel }),
391
+ ...(existingSettings.locale === undefined ? {} : { locale: existingSettings.locale }),
392
+ };
393
+ await writeFile(settingsFilePath, `${JSON.stringify(settings, null, "\t")}\n`, { encoding: "utf8", mode: 0o600 });
394
+ return true;
117
395
  }
118
396
  function resolveConfigValue(value, env) {
119
397
  if (value === undefined)
@@ -144,7 +422,9 @@ function selectProviderModel(provider, providerId, env) {
144
422
  ? provider.models.find((candidate) => candidate.id === "gpt-4o")?.id
145
423
  : providerId === "zhipu"
146
424
  ? provider.models.find((candidate) => candidate.id === "glm-5.3")?.id
147
- : undefined) ||
425
+ : providerId === "kimi"
426
+ ? provider.models.find((candidate) => candidate.id === "k3")?.id
427
+ : undefined) ||
148
428
  provider.models[0]?.id;
149
429
  if (!modelId)
150
430
  throw new Error("DI_CODE_MODEL is required when the selected provider has no models");
@@ -172,62 +452,96 @@ function createBuiltInRuntime(env, providerId) {
172
452
  const provider = createZhipuProvider({ env });
173
453
  return { provider, model: selectProviderModel(provider, providerId, env) };
174
454
  }
455
+ if (providerId === "kimi") {
456
+ const provider = createKimiProvider({ env });
457
+ return { provider, model: selectProviderModel(provider, providerId, env) };
458
+ }
175
459
  return undefined;
176
460
  }
177
461
  function createConfiguredRuntime(env, configuration) {
178
462
  if (configuration.api !== "openai-responses" &&
179
- configuration.api !== "deepseek-responses" &&
180
- configuration.api !== "zhipu-chat-completions" &&
463
+ configuration.api !== "openai-chat-completions" &&
181
464
  configuration.api !== "anthropic-messages") {
182
- throw new Error(`Unsupported API "${configuration.api ?? "missing"}" for provider "${configuration.id}". Expected openai-responses, deepseek-responses, zhipu-chat-completions, or anthropic-messages.`);
465
+ throw new Error(`Unsupported API "${configuration.api ?? "missing"}" for provider "${configuration.id}". Expected openai-responses, openai-chat-completions, or anthropic-messages.`);
183
466
  }
184
467
  if (configuration.id !== "openai" &&
185
468
  configuration.id !== "deepseek" &&
186
469
  configuration.id !== "zhipu" &&
470
+ configuration.id !== "kimi" &&
187
471
  configuration.id !== "anthropic" &&
188
472
  !configuration.models) {
189
473
  throw new Error(`${SETTINGS_PATH}: providers.${configuration.id}.models is required for a custom provider`);
190
474
  }
191
- const provider = configuration.api === "anthropic-messages"
192
- ? createAnthropicProvider({
193
- env: configuration.id === "anthropic"
475
+ const provider = configuration.api === "openai-chat-completions"
476
+ ? createOpenAIChatCompletionsProvider({
477
+ env: configuration.id === "zhipu" || configuration.id === "deepseek" || configuration.id === "kimi"
194
478
  ? env
195
- : Object.fromEntries(Object.entries(env).filter(([name]) => name !== "ANTHROPIC_API_KEY" && name !== "ANTHROPIC_BASE_URL")),
479
+ : Object.fromEntries(Object.entries(env).filter(([name]) => name !== "OPENAI_API_KEY" && name !== "OPENAI_BASE_URL")),
196
480
  models: configuration.models,
197
481
  apiKey: resolveConfigValue(configuration.apiKey, env),
198
482
  baseUrl: configuration.models ? undefined : configuration.baseUrl,
199
483
  providerId: configuration.id,
200
- name: configuration.name,
484
+ name: configuration.name ?? configuration.id,
485
+ ...(configuration.id === "zhipu"
486
+ ? {
487
+ apiKeyEnvironmentVariable: "ZAI_API_KEY",
488
+ baseUrlEnvironmentVariable: "ZHIPU_BASE_URL",
489
+ defaultBaseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
490
+ }
491
+ : configuration.id === "deepseek"
492
+ ? {
493
+ apiKeyEnvironmentVariable: "DEEPSEEK_API_KEY",
494
+ baseUrlEnvironmentVariable: "DEEPSEEK_BASE_URL",
495
+ defaultBaseUrl: "https://api.deepseek.com",
496
+ }
497
+ : configuration.id === "kimi"
498
+ ? {
499
+ apiKeyEnvironmentVariable: "KIMI_API_KEY",
500
+ baseUrlEnvironmentVariable: "KIMI_BASE_URL",
501
+ defaultBaseUrl: "https://api.kimi.com/coding/v1",
502
+ }
503
+ : {}),
201
504
  })
202
- : configuration.id === "deepseek"
203
- ? createDeepSeekProvider({
204
- env,
505
+ : configuration.api === "anthropic-messages"
506
+ ? createAnthropicProvider({
507
+ env: configuration.id === "anthropic"
508
+ ? env
509
+ : Object.fromEntries(Object.entries(env).filter(([name]) => name !== "ANTHROPIC_API_KEY" && name !== "ANTHROPIC_BASE_URL")),
205
510
  models: configuration.models,
206
511
  apiKey: resolveConfigValue(configuration.apiKey, env),
207
512
  baseUrl: configuration.models ? undefined : configuration.baseUrl,
513
+ providerId: configuration.id,
514
+ name: configuration.name,
208
515
  })
209
- : configuration.id === "zhipu"
210
- ? createZhipuProvider({
516
+ : configuration.id === "deepseek"
517
+ ? createDeepSeekProvider({
211
518
  env,
212
519
  models: configuration.models,
213
520
  apiKey: resolveConfigValue(configuration.apiKey, env),
214
521
  baseUrl: configuration.models ? undefined : configuration.baseUrl,
215
522
  })
216
- : createOpenAIProvider({
217
- env: configuration.id === "openai"
218
- ? env
219
- : Object.fromEntries(Object.entries(env).filter(([name]) => name !== "OPENAI_API_KEY" && name !== "OPENAI_BASE_URL")),
220
- models: configuration.models,
221
- apiKey: resolveConfigValue(configuration.apiKey, env),
222
- baseUrl: configuration.models ? undefined : configuration.baseUrl,
223
- providerId: configuration.id,
224
- name: configuration.name,
225
- });
523
+ : configuration.id === "zhipu"
524
+ ? createZhipuProvider({
525
+ env,
526
+ models: configuration.models,
527
+ apiKey: resolveConfigValue(configuration.apiKey, env),
528
+ baseUrl: configuration.models ? undefined : configuration.baseUrl,
529
+ })
530
+ : createOpenAIProvider({
531
+ env: configuration.id === "openai"
532
+ ? env
533
+ : Object.fromEntries(Object.entries(env).filter(([name]) => name !== "OPENAI_API_KEY" && name !== "OPENAI_BASE_URL")),
534
+ models: configuration.models,
535
+ apiKey: resolveConfigValue(configuration.apiKey, env),
536
+ baseUrl: configuration.models ? undefined : configuration.baseUrl,
537
+ providerId: configuration.id,
538
+ name: configuration.name,
539
+ });
226
540
  const model = selectProviderModel(provider, configuration.id, env);
227
541
  return { provider, model };
228
542
  }
229
- export function resolveStartupRuntime(env, providers) {
230
- const selectedProviderId = env.DI_CODE_PROVIDER?.trim() || (providers.length === 1 ? providers[0]?.id : undefined);
543
+ export function resolveStartupRuntime(env, providers, defaults = {}) {
544
+ const selectedProviderId = env.DI_CODE_PROVIDER?.trim() || defaults.providerId || (providers.length === 1 ? providers[0]?.id : undefined);
231
545
  if (selectedProviderId === "faux")
232
546
  return createFauxRuntime();
233
547
  if (!selectedProviderId) {
@@ -236,29 +550,26 @@ export function resolveStartupRuntime(env, providers) {
236
550
  }
237
551
  throw new Error("DI_CODE_PROVIDER is required when more than one provider is configured.");
238
552
  }
553
+ const runtimeEnvironment = env.DI_CODE_MODEL?.trim() || defaults.providerId !== selectedProviderId || !defaults.modelId
554
+ ? env
555
+ : { ...env, DI_CODE_MODEL: defaults.modelId };
239
556
  const selectedProvider = providers.find((provider) => provider.id === selectedProviderId);
240
557
  if (selectedProvider)
241
- return createConfiguredRuntime(env, selectedProvider);
242
- const builtInRuntime = createBuiltInRuntime(env, selectedProviderId);
558
+ return createConfiguredRuntime(runtimeEnvironment, selectedProvider);
559
+ const builtInRuntime = createBuiltInRuntime(runtimeEnvironment, selectedProviderId);
243
560
  if (builtInRuntime)
244
561
  return builtInRuntime;
245
562
  throw new Error(`Unknown configured provider "${selectedProviderId}". Available providers: ${providers.map((provider) => provider.id).join(", ")}.`);
246
563
  }
247
- export async function loadStartupConfiguration(cwd, environment = process.env) {
248
- let settings;
249
- try {
250
- const source = await readFile(join(cwd, SETTINGS_PATH), "utf8");
251
- if (source.trim().length === 0)
252
- return { environment, providers: [] };
253
- settings = parseSettings(JSON.parse(source));
254
- }
255
- catch (cause) {
256
- if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") {
257
- return { environment, providers: [] };
258
- }
259
- if (cause instanceof SyntaxError)
260
- throw new Error(`${SETTINGS_PATH}: invalid JSON`, { cause });
261
- throw cause;
564
+ export async function loadStartupConfiguration(cwd, environment = process.env, agentDir = join(homedir(), ".di-code")) {
565
+ const [globalSettings, projectSettings] = await Promise.all([
566
+ readSettingsFile(join(agentDir, SETTINGS_FILE_NAME), join(agentDir, SETTINGS_FILE_NAME)),
567
+ readSettingsFile(join(cwd, SETTINGS_PATH), SETTINGS_PATH),
568
+ ]);
569
+ const settings = mergeSettings(globalSettings, projectSettings);
570
+ const requestedLocale = environment.DI_CODE_LOCALE?.trim();
571
+ if (requestedLocale !== undefined && requestedLocale !== "" && !isLocale(requestedLocale)) {
572
+ throw new Error('DI_CODE_LOCALE must be "en" or "zh-CN".');
262
573
  }
263
574
  const providers = Object.entries(settings.providers).map(([id, provider]) => ({
264
575
  id: requiredString(id, `providers.${id}`),
@@ -266,9 +577,18 @@ export async function loadStartupConfiguration(cwd, environment = process.env) {
266
577
  api: provider.api,
267
578
  apiKey: provider.apiKey,
268
579
  baseUrl: provider.baseUrl,
269
- models: parseModels(provider.models, `providers.${id}.models`, id, provider.api, provider.baseUrl),
580
+ models: parseModels(provider.models, `providers.${id}.models`, id, provider.api, provider.baseUrl, SETTINGS_PATH),
270
581
  }));
271
- return { environment, providers };
582
+ const defaults = settings.defaultProvider === undefined && settings.defaultModel === undefined
583
+ ? undefined
584
+ : { providerId: settings.defaultProvider, modelId: settings.defaultModel };
585
+ const locale = requestedLocale && isLocale(requestedLocale) ? requestedLocale : settings.locale;
586
+ return {
587
+ environment,
588
+ providers,
589
+ ...(locale === undefined ? {} : { locale }),
590
+ ...(defaults === undefined ? {} : { defaults }),
591
+ };
272
592
  }
273
593
  export async function loadStartupEnvironment(cwd, environment = process.env) {
274
594
  return (await loadStartupConfiguration(cwd, environment)).environment;