@ww_nero/mini-cli 1.0.63 → 1.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.js CHANGED
@@ -1,401 +1,405 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
-
5
- const CONFIG_DIR = path.join(os.homedir(), '.mini');
6
- const FILE_NAMES = {
7
- chat: 'chat.json',
8
- commit: 'commit.json',
9
- mcp: 'mcp.json',
10
- settings: 'settings.json',
11
- worktree: 'worktree.json',
12
- mini: 'MINI.md'
13
- };
14
-
15
- const DEFAULT_ALLOWED_COMMANDS = [
16
- 'rm', 'rmdir', 'touch', 'mkdir', 'cd', 'cp', 'mv', 'node', 'npm', 'pkill', 'kill',
17
- 'curl', 'ls', 'pwd', 'grep', 'cat', 'echo', 'sed', 'head', 'tail', 'find', 'true',
18
- 'false', 'pip', 'python', 'ps', 'lsof', 'git', 'pandoc'
19
- ];
20
-
21
- const DEFAULT_TOOL_RESPONSE_MAX_TOKENS = 65536;
22
- const DEFAULT_COMPACT_TOKEN_THRESHOLD = 65536;
23
- const DEFAULT_MCP_TOOL_TIMEOUT_MS = 10 * 60 * 1000;
24
- const DEFAULT_OUTPUT_MAX_LENGTH = 12000;
25
- const DEFAULT_EXECUTION_TIMEOUT = 300000;
26
- const DEFAULT_SERVICE_BOOT_WINDOW = 5000;
27
- const DEFAULT_LARGE_FILE_LINE_THRESHOLD = 2000;
28
- const COMPACT_SUMMARY_PROMPT = `请对以下对话进行总结,用于上下文压缩。请按以下格式输出:
29
-
30
- ## 问题背景
31
- 简要描述用户的原始问题和目标,包括涉及的重要文件路径。
32
-
33
- ## 已完成工作
34
- 列出已经完成的任务或环节:
35
- - 已搜索/阅读的文件
36
- - 已修改的代码
37
- - 已执行的操作
38
- - 重要的中间结果
39
-
40
- ## 待继续任务
41
- 根据对话上下文,描述接下来需要继续完成的工作:
42
- - 未完成的任务
43
- - 需要进一步处理的问题
44
- - 用户最后一次请求的具体内容
45
-
46
- 请确保总结完整、准确,以便在压缩后能够继续完成用户的任务。`;
47
- const detectCommandPath = () => {
48
- if (process.platform === 'win32') return 'C:/Windows/System32/wsl.exe';
49
- if (process.platform === 'darwin') return '/bin/zsh';
50
- return '/bin/bash';
51
- };
52
-
53
- const DEFAULT_CHAT_ENDPOINTS = [
54
- {
55
- baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
56
- key: 'your-api-key',
57
- model: 'gemini-flash-latest',
58
- alias: 'Gemini Flash',
59
- name: 'gemini-flash-latest',
60
- think: true,
61
- price: 0,
62
- options: {
63
- extra_body: {
64
- google: {
65
- thinking_config: {
66
- thinking_budget: -1,
67
- include_thoughts: false
68
- }
69
- }
70
- }
71
- }
72
- }
73
- ];
74
-
75
- const DEFAULT_COMMIT_MODEL = {
76
- baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
77
- key: 'your-api-key',
78
- model: 'gemini-flash-latest',
79
- options: {
80
- reasoning_effort: 'none'
81
- }
82
- };
83
-
84
- const DEFAULT_WORKTREE_COMMANDS = [
85
- { base: 'codex', prompt: 'codex --full-auto {prompt}', resume: 'codex --full-auto resume' },
86
- { base: 'claude', prompt: 'claude --permission-mode acceptEdits {prompt}', resume: 'claude --permission-mode acceptEdits --continue' },
87
- { base: 'gemini', prompt: 'gemini -m pro --approval-mode=auto_edit -i {prompt}', resume: 'gemini -m pro --approval-mode=auto_edit --resume' }
88
- ];
89
-
90
- const DEFAULT_SETTINGS = {
91
- mcpServers: {},
92
- tools: {},
93
- commands: [],
94
- maxToolTokens: DEFAULT_TOOL_RESPONSE_MAX_TOKENS,
95
- compactTokenThreshold: DEFAULT_COMPACT_TOKEN_THRESHOLD,
96
- allowedCommands: [...DEFAULT_ALLOWED_COMMANDS],
97
- mcpToolTimeout: DEFAULT_MCP_TOOL_TIMEOUT_MS,
98
- outputMaxLength: DEFAULT_OUTPUT_MAX_LENGTH,
99
- executionTimeout: DEFAULT_EXECUTION_TIMEOUT,
100
- serviceBootWindow: DEFAULT_SERVICE_BOOT_WINDOW,
101
- largeFileLineThreshold: DEFAULT_LARGE_FILE_LINE_THRESHOLD
102
- };
103
-
104
- const DEFAULT_MINI_CONTENT = '# 在此填写全局系统指令。\n';
105
-
106
- class ConfigError extends Error {
107
- constructor(message, configPath) {
108
- super(message);
109
- this.name = 'ConfigError';
110
- this.configPath = configPath;
111
- }
112
- }
113
-
114
- const isPlainObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
115
-
116
- const ensureDir = (dirPath) => {
117
- if (!fs.existsSync(dirPath)) {
118
- fs.mkdirSync(dirPath, { recursive: true });
119
- }
120
- };
121
-
122
- const writeJsonFile = (filePath, data) => {
123
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
124
- };
125
-
126
- const ensureFile = (filePath, data) => {
127
- if (fs.existsSync(filePath)) return false;
128
- if (typeof data === 'string') {
129
- fs.writeFileSync(filePath, data, 'utf8');
130
- } else {
131
- writeJsonFile(filePath, data);
132
- }
133
- return true;
134
- };
135
-
136
- const getConfigPath = (key) => path.join(CONFIG_DIR, FILE_NAMES[key]);
137
- const getDefaultConfigPath = () => getConfigPath('chat');
138
-
139
- const readJsonFile = (filePath, fallback = {}) => {
140
- try {
141
- const raw = fs.readFileSync(filePath, 'utf8');
142
- if (!raw.trim()) return fallback;
143
- return JSON.parse(raw);
144
- } catch (_) {
145
- return fallback;
146
- }
147
- };
148
-
149
- const extractEndpointList = (parsed) => {
150
- if (Array.isArray(parsed)) return parsed;
151
- if (isPlainObject(parsed)) {
152
- if (Array.isArray(parsed.endpoints)) return parsed.endpoints;
153
- if (parsed.endpoint) return [parsed.endpoint];
154
- }
155
- return [];
156
- };
157
-
158
- const ensureConfigFiles = () => {
159
- ensureDir(CONFIG_DIR);
160
- const createdFiles = [];
161
-
162
- if (ensureFile(getConfigPath('chat'), { endpoints: DEFAULT_CHAT_ENDPOINTS })) {
163
- createdFiles.push(FILE_NAMES.chat);
164
- }
165
-
166
- if (ensureFile(getConfigPath('commit'), { model: DEFAULT_COMMIT_MODEL })) {
167
- createdFiles.push(FILE_NAMES.commit);
168
- }
169
-
170
- if (ensureFile(getConfigPath('mcp'), { mcpServers: {} })) {
171
- createdFiles.push(FILE_NAMES.mcp);
172
- }
173
-
174
- if (ensureFile(getConfigPath('settings'), DEFAULT_SETTINGS)) {
175
- createdFiles.push(FILE_NAMES.settings);
176
- }
177
-
178
- if (ensureFile(getConfigPath('worktree'), { commands: DEFAULT_WORKTREE_COMMANDS })) {
179
- createdFiles.push(FILE_NAMES.worktree);
180
- }
181
-
182
- if (ensureFile(getConfigPath('mini'), DEFAULT_MINI_CONTENT)) {
183
- createdFiles.push(FILE_NAMES.mini);
184
- }
185
-
186
- return { configDir: CONFIG_DIR, createdFiles };
187
- };
188
-
189
- const normalizePositiveInteger = (value, fallback) => {
190
- const numberValue = Number(value);
191
- if (Number.isFinite(numberValue) && numberValue > 0) {
192
- return Math.trunc(numberValue);
193
- }
194
- return fallback;
195
- };
196
-
197
- const normalizeField = (value) => (typeof value === 'string' ? value.trim() : '');
198
- const createEndpointSignature = (entry = {}) => {
199
- const pick = (key) => normalizeField(entry[key]).toLowerCase();
200
- return ['name', 'model', 'baseUrl', 'key'].map(pick).join('::');
201
- };
202
-
203
- const normalizeEndpoint = (raw = {}, index = 0) => {
204
- if (!isPlainObject(raw)) return { endpoint: null };
205
-
206
- const key = normalizeField(raw.key);
207
- const baseUrl = normalizeField(raw.baseUrl);
208
- const model = normalizeField(raw.model);
209
- if (!baseUrl || !key || !model) {
210
- return { endpoint: null };
211
- }
212
-
213
- const nameRaw = normalizeField(raw.alias || raw.name);
214
- const name = nameRaw || model || `model_${index + 1}`;
215
- const think = raw.think === true;
216
- const price = Number.isFinite(Number(raw.price)) ? Number(raw.price) : 0;
217
- const options = isPlainObject(raw.options) ? raw.options : undefined;
218
-
219
- const endpoint = {
220
- baseUrl,
221
- key,
222
- model,
223
- name,
224
- alias: name,
225
- think,
226
- price,
227
- ...(options ? { options } : {})
228
- };
229
-
230
- return {
231
- endpoint: {
232
- ...endpoint,
233
- signature: createEndpointSignature({ ...raw, ...endpoint })
234
- }
235
- };
236
- };
237
-
238
- const loadEndpointConfig = () => {
239
- const ensureResult = ensureConfigFiles();
240
- const configPath = getConfigPath('chat');
241
- const parsed = readJsonFile(configPath, { endpoints: [] });
242
- const rawList = extractEndpointList(parsed);
243
-
244
- const endpoints = rawList
245
- .map((entry, idx) => normalizeEndpoint(entry, idx).endpoint)
246
- .filter(Boolean)
247
- .map((endpoint, idx) => ({ ...endpoint, name: endpoint.name || endpoint.alias || endpoint.model || `model_${idx + 1}` }));
248
-
249
- const validEndpoints = endpoints.filter((ep) => ep.key && !ep.key.toLowerCase().includes('your-api-key'));
250
- if (validEndpoints.length === 0) {
251
- const suffix = ensureResult.createdFiles.includes(FILE_NAMES.chat)
252
- ? '已自动生成模板,请填写 baseUrl/key/model 后重新运行 mini。'
253
- : '请在配置中提供至少一个有效的 baseUrl、key、model。';
254
- throw new ConfigError(`${configPath} 缺少可用的模型配置。${suffix}`, configPath);
255
- }
256
-
257
- return { endpoints: validEndpoints, configPath, created: ensureResult.createdFiles.includes(FILE_NAMES.chat) };
258
- };
259
-
260
- const resolveEndpointCollection = (parsed) => {
261
- if (Array.isArray(parsed)) {
262
- return parsed;
263
- }
264
- if (parsed && typeof parsed === 'object' && Array.isArray(parsed.endpoints)) {
265
- return parsed.endpoints;
266
- }
267
- return null;
268
- };
269
-
270
- const promoteEndpointInConfig = (configPath, signature) => {
271
- if (!configPath || !signature) {
272
- return false;
273
- }
274
- try {
275
- if (!fs.existsSync(configPath)) {
276
- return false;
277
- }
278
- const rawContent = fs.readFileSync(configPath, 'utf8') || '';
279
- if (!rawContent.trim()) {
280
- return false;
281
- }
282
- const parsed = JSON.parse(rawContent);
283
- const targetList = resolveEndpointCollection(parsed);
284
- if (!Array.isArray(targetList) || targetList.length <= 1) {
285
- return false;
286
- }
287
- const matchIndex = targetList.findIndex((entry) => createEndpointSignature(entry) === signature);
288
- if (matchIndex <= 0) {
289
- return matchIndex === 0;
290
- }
291
- const [selected] = targetList.splice(matchIndex, 1);
292
- targetList.unshift(selected);
293
- const serialized = Array.isArray(parsed) ? targetList : { ...parsed, endpoints: targetList };
294
- writeJsonFile(configPath, serialized);
295
- return true;
296
- } catch (error) {
297
- return false;
298
- }
299
- };
300
-
301
- const ensureArrayOfStrings = (value, fallback = []) => {
302
- if (!Array.isArray(value)) {
303
- return [...fallback];
304
- }
305
- return value.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean);
306
- };
307
-
308
- const getAllMcpServerNames = () => {
309
- const mcpPath = getConfigPath('mcp');
310
- const parsed = readJsonFile(mcpPath, { mcpServers: {} });
311
- const mcpServers = parsed.mcpServers && typeof parsed.mcpServers === 'object'
312
- ? parsed.mcpServers
313
- : {};
314
- return Object.keys(mcpServers);
315
- };
316
-
317
- const loadSettings = ({ defaultTools = [] } = {}) => {
318
- const ensureResult = ensureConfigFiles();
319
- const settingsPath = getConfigPath('settings');
320
- const parsed = readJsonFile(settingsPath, DEFAULT_SETTINGS);
321
-
322
- const settings = {
323
- mcps: (() => {
324
- const allMcpNames = getAllMcpServerNames();
325
- const mcpConfig = parsed.mcpServers;
326
-
327
- // 只支持对象格式,默认全部启用,只有明确设置为 false 才禁用
328
- if (!isPlainObject(mcpConfig)) {
329
- return allMcpNames;
330
- }
331
-
332
- return allMcpNames.filter((name) => mcpConfig[name] !== false);
333
- })(),
334
- tools: (() => {
335
- const toolConfig = parsed.tools;
336
-
337
- // 只支持对象格式,默认全部启用,只有明确设置为 false 才禁用
338
- if (!isPlainObject(toolConfig)) {
339
- return ensureArrayOfStrings(defaultTools);
340
- }
341
-
342
- return defaultTools.filter((name) => toolConfig[name] !== false);
343
- })(),
344
- commands: ensureArrayOfStrings(parsed.commands),
345
- commandPath: detectCommandPath(),
346
- maxToolTokens: normalizePositiveInteger(
347
- parsed.maxToolTokens,
348
- DEFAULT_TOOL_RESPONSE_MAX_TOKENS
349
- ),
350
- allowedCommands: (() => {
351
- const list = ensureArrayOfStrings(parsed.allowedCommands);
352
- return list.length ? Array.from(new Set(list)) : [...DEFAULT_ALLOWED_COMMANDS];
353
- })(),
354
- compactTokenThreshold: normalizePositiveInteger(
355
- parsed.compactTokenThreshold,
356
- DEFAULT_COMPACT_TOKEN_THRESHOLD
357
- ),
358
- mcpToolTimeout: normalizePositiveInteger(
359
- parsed.mcpToolTimeout,
360
- DEFAULT_MCP_TOOL_TIMEOUT_MS
361
- ),
362
- outputMaxLength: normalizePositiveInteger(
363
- parsed.outputMaxLength,
364
- DEFAULT_OUTPUT_MAX_LENGTH
365
- ),
366
- executionTimeout: normalizePositiveInteger(
367
- parsed.executionTimeout,
368
- DEFAULT_EXECUTION_TIMEOUT
369
- ),
370
- serviceBootWindow: normalizePositiveInteger(
371
- parsed.serviceBootWindow,
372
- DEFAULT_SERVICE_BOOT_WINDOW
373
- ),
374
- largeFileLineThreshold: normalizePositiveInteger(
375
- parsed.largeFileLineThreshold,
376
- DEFAULT_LARGE_FILE_LINE_THRESHOLD
377
- )
378
- };
379
-
380
- settings.toolOutputTokenLimit = settings.maxToolTokens;
381
-
382
- return {
383
- settingsPath,
384
- settings,
385
- created: ensureResult.createdFiles.includes(FILE_NAMES.settings)
386
- };
387
- };
388
-
389
- module.exports = {
390
- ConfigError,
391
- loadEndpointConfig,
392
- getDefaultConfigPath,
393
- promoteEndpointInConfig,
394
- loadSettings,
395
- ensureConfigFiles,
396
- getConfigPath,
397
- DEFAULT_ALLOWED_COMMANDS,
398
- DEFAULT_TOOL_RESPONSE_MAX_TOKENS,
399
- DEFAULT_COMPACT_TOKEN_THRESHOLD,
400
- COMPACT_SUMMARY_PROMPT
401
- };
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.mini');
6
+ const FILE_NAMES = {
7
+ chat: 'chat.json',
8
+ commit: 'commit.json',
9
+ mcp: 'mcp.json',
10
+ settings: 'settings.json',
11
+ worktree: 'worktree.json',
12
+ mini: 'MINI.md'
13
+ };
14
+
15
+ const DEFAULT_ALLOWED_COMMANDS = [
16
+ 'rm', 'rmdir', 'touch', 'mkdir', 'cd', 'cp', 'mv', 'node', 'npm', 'pkill', 'kill',
17
+ 'curl', 'ls', 'pwd', 'grep', 'cat', 'echo', 'sed', 'head', 'tail', 'find', 'true',
18
+ 'false', 'pip', 'python', 'ps', 'lsof', 'git', 'pandoc'
19
+ ];
20
+
21
+ const DEFAULT_TOOL_RESPONSE_MAX_TOKENS = 65536;
22
+ const DEFAULT_COMPACT_TOKEN_THRESHOLD = 65536;
23
+ const DEFAULT_MCP_TOOL_TIMEOUT_MS = 10 * 60 * 1000;
24
+ const DEFAULT_OUTPUT_MAX_LENGTH = 12000;
25
+ const DEFAULT_EXECUTION_TIMEOUT = 300000;
26
+ const DEFAULT_SERVICE_BOOT_WINDOW = 5000;
27
+ const DEFAULT_LARGE_FILE_LINE_THRESHOLD = 2000;
28
+ const COMPACT_SUMMARY_PROMPT = `请对以下对话进行总结,用于上下文压缩。请按以下格式输出:
29
+
30
+ ## 问题背景
31
+ 简要描述用户的原始问题和目标,包括涉及的重要文件路径。
32
+
33
+ ## 已完成工作
34
+ 列出已经完成的任务或环节:
35
+ - 已搜索/阅读的文件
36
+ - 已修改的代码
37
+ - 已执行的操作
38
+ - 重要的中间结果
39
+
40
+ ## 待继续任务
41
+ 根据对话上下文,描述接下来需要继续完成的工作:
42
+ - 未完成的任务
43
+ - 需要进一步处理的问题
44
+ - 用户最后一次请求的具体内容
45
+
46
+ 请确保总结完整、准确,以便在压缩后能够继续完成用户的任务。`;
47
+ const detectCommandPath = () => {
48
+ if (process.platform === 'win32') return 'C:/Windows/System32/wsl.exe';
49
+ if (process.platform === 'darwin') return '/bin/zsh';
50
+ return '/bin/bash';
51
+ };
52
+
53
+ const DEFAULT_CHAT_ENDPOINTS = [
54
+ {
55
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
56
+ key: 'your-api-key',
57
+ model: 'gemini-flash-latest',
58
+ alias: 'Gemini Flash',
59
+ name: 'gemini-flash-latest',
60
+ think: true,
61
+ price: 0,
62
+ options: {
63
+ extra_body: {
64
+ google: {
65
+ thinking_config: {
66
+ thinking_budget: -1,
67
+ include_thoughts: false
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ];
74
+
75
+ const DEFAULT_COMMIT_MODEL = {
76
+ baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
77
+ key: 'your-api-key',
78
+ model: 'gemini-flash-latest',
79
+ options: {
80
+ reasoning_effort: 'none'
81
+ }
82
+ };
83
+
84
+ const DEFAULT_WORKTREE_COMMANDS = [
85
+ { base: 'codex', prompt: 'codex --full-auto {prompt}', resume: 'codex --full-auto resume' },
86
+ { base: 'claude', prompt: 'claude --permission-mode acceptEdits {prompt}', resume: 'claude --permission-mode acceptEdits --continue' },
87
+ { base: 'gemini', prompt: 'gemini -m pro --approval-mode=auto_edit -i {prompt}', resume: 'gemini -m pro --approval-mode=auto_edit --resume' }
88
+ ];
89
+
90
+ const DEFAULT_SETTINGS = {
91
+ mcpServers: {},
92
+ tools: {},
93
+ commands: [],
94
+ maxToolTokens: DEFAULT_TOOL_RESPONSE_MAX_TOKENS,
95
+ compactTokenThreshold: DEFAULT_COMPACT_TOKEN_THRESHOLD,
96
+ allowedCommands: [...DEFAULT_ALLOWED_COMMANDS],
97
+ mcpToolTimeout: DEFAULT_MCP_TOOL_TIMEOUT_MS,
98
+ outputMaxLength: DEFAULT_OUTPUT_MAX_LENGTH,
99
+ executionTimeout: DEFAULT_EXECUTION_TIMEOUT,
100
+ serviceBootWindow: DEFAULT_SERVICE_BOOT_WINDOW,
101
+ largeFileLineThreshold: DEFAULT_LARGE_FILE_LINE_THRESHOLD
102
+ };
103
+
104
+ const DEFAULT_MINI_CONTENT = '# 在此填写全局系统指令。\n';
105
+
106
+ class ConfigError extends Error {
107
+ constructor(message, configPath) {
108
+ super(message);
109
+ this.name = 'ConfigError';
110
+ this.configPath = configPath;
111
+ }
112
+ }
113
+
114
+ const isPlainObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
115
+
116
+ const ensureDir = (dirPath) => {
117
+ if (!fs.existsSync(dirPath)) {
118
+ fs.mkdirSync(dirPath, { recursive: true });
119
+ }
120
+ };
121
+
122
+ const writeJsonFile = (filePath, data) => {
123
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
124
+ };
125
+
126
+ const ensureFile = (filePath, data) => {
127
+ if (fs.existsSync(filePath)) return false;
128
+ if (typeof data === 'string') {
129
+ fs.writeFileSync(filePath, data, 'utf8');
130
+ } else {
131
+ writeJsonFile(filePath, data);
132
+ }
133
+ return true;
134
+ };
135
+
136
+ const getConfigPath = (key) => path.join(CONFIG_DIR, FILE_NAMES[key]);
137
+ const getDefaultConfigPath = () => getConfigPath('chat');
138
+
139
+ const readJsonFile = (filePath, fallback = {}) => {
140
+ try {
141
+ const raw = fs.readFileSync(filePath, 'utf8');
142
+ if (!raw.trim()) return fallback;
143
+ return JSON.parse(raw);
144
+ } catch (_) {
145
+ return fallback;
146
+ }
147
+ };
148
+
149
+ const extractEndpointList = (parsed) => {
150
+ if (Array.isArray(parsed)) return parsed;
151
+ if (isPlainObject(parsed)) {
152
+ if (Array.isArray(parsed.endpoints)) return parsed.endpoints;
153
+ if (parsed.endpoint) return [parsed.endpoint];
154
+ }
155
+ return [];
156
+ };
157
+
158
+ const ensureConfigFiles = () => {
159
+ ensureDir(CONFIG_DIR);
160
+ const createdFiles = [];
161
+
162
+ if (ensureFile(getConfigPath('chat'), { endpoints: DEFAULT_CHAT_ENDPOINTS })) {
163
+ createdFiles.push(FILE_NAMES.chat);
164
+ }
165
+
166
+ if (ensureFile(getConfigPath('commit'), { model: DEFAULT_COMMIT_MODEL })) {
167
+ createdFiles.push(FILE_NAMES.commit);
168
+ }
169
+
170
+ if (ensureFile(getConfigPath('mcp'), { mcpServers: {} })) {
171
+ createdFiles.push(FILE_NAMES.mcp);
172
+ }
173
+
174
+ if (ensureFile(getConfigPath('settings'), DEFAULT_SETTINGS)) {
175
+ createdFiles.push(FILE_NAMES.settings);
176
+ }
177
+
178
+ if (ensureFile(getConfigPath('worktree'), { commands: DEFAULT_WORKTREE_COMMANDS })) {
179
+ createdFiles.push(FILE_NAMES.worktree);
180
+ }
181
+
182
+ if (ensureFile(getConfigPath('mini'), DEFAULT_MINI_CONTENT)) {
183
+ createdFiles.push(FILE_NAMES.mini);
184
+ }
185
+
186
+ return { configDir: CONFIG_DIR, createdFiles };
187
+ };
188
+
189
+ const normalizePositiveInteger = (value, fallback) => {
190
+ const numberValue = Number(value);
191
+ if (Number.isFinite(numberValue) && numberValue > 0) {
192
+ return Math.trunc(numberValue);
193
+ }
194
+ return fallback;
195
+ };
196
+
197
+ const normalizeField = (value) => (typeof value === 'string' ? value.trim() : '');
198
+ const createEndpointSignature = (entry = {}) => {
199
+ const nameVal = normalizeField(entry.alias || entry.name || entry.model);
200
+ const pick = (key) => {
201
+ if (key === 'name') return nameVal.toLowerCase();
202
+ return normalizeField(entry[key]).toLowerCase();
203
+ };
204
+ return ['name', 'model', 'baseUrl', 'key'].map(pick).join('::');
205
+ };
206
+
207
+ const normalizeEndpoint = (raw = {}, index = 0) => {
208
+ if (!isPlainObject(raw)) return { endpoint: null };
209
+
210
+ const key = normalizeField(raw.key);
211
+ const baseUrl = normalizeField(raw.baseUrl);
212
+ const model = normalizeField(raw.model);
213
+ if (!baseUrl || !key || !model) {
214
+ return { endpoint: null };
215
+ }
216
+
217
+ const nameRaw = normalizeField(raw.alias || raw.name);
218
+ const name = nameRaw || model || `model_${index + 1}`;
219
+ const think = raw.think === true;
220
+ const price = Number.isFinite(Number(raw.price)) ? Number(raw.price) : 0;
221
+ const options = isPlainObject(raw.options) ? raw.options : undefined;
222
+
223
+ const endpoint = {
224
+ baseUrl,
225
+ key,
226
+ model,
227
+ name,
228
+ alias: name,
229
+ think,
230
+ price,
231
+ ...(options ? { options } : {})
232
+ };
233
+
234
+ return {
235
+ endpoint: {
236
+ ...endpoint,
237
+ signature: createEndpointSignature({ ...raw, ...endpoint })
238
+ }
239
+ };
240
+ };
241
+
242
+ const loadEndpointConfig = () => {
243
+ const ensureResult = ensureConfigFiles();
244
+ const configPath = getConfigPath('chat');
245
+ const parsed = readJsonFile(configPath, { endpoints: [] });
246
+ const rawList = extractEndpointList(parsed);
247
+
248
+ const endpoints = rawList
249
+ .map((entry, idx) => normalizeEndpoint(entry, idx).endpoint)
250
+ .filter(Boolean)
251
+ .map((endpoint, idx) => ({ ...endpoint, name: endpoint.name || endpoint.alias || endpoint.model || `model_${idx + 1}` }));
252
+
253
+ const validEndpoints = endpoints.filter((ep) => ep.key && !ep.key.toLowerCase().includes('your-api-key'));
254
+ if (validEndpoints.length === 0) {
255
+ const suffix = ensureResult.createdFiles.includes(FILE_NAMES.chat)
256
+ ? '已自动生成模板,请填写 baseUrl/key/model 后重新运行 mini。'
257
+ : '请在配置中提供至少一个有效的 baseUrl、key、model。';
258
+ throw new ConfigError(`${configPath} 缺少可用的模型配置。${suffix}`, configPath);
259
+ }
260
+
261
+ return { endpoints: validEndpoints, configPath, created: ensureResult.createdFiles.includes(FILE_NAMES.chat) };
262
+ };
263
+
264
+ const resolveEndpointCollection = (parsed) => {
265
+ if (Array.isArray(parsed)) {
266
+ return parsed;
267
+ }
268
+ if (parsed && typeof parsed === 'object' && Array.isArray(parsed.endpoints)) {
269
+ return parsed.endpoints;
270
+ }
271
+ return null;
272
+ };
273
+
274
+ const promoteEndpointInConfig = (configPath, signature) => {
275
+ if (!configPath || !signature) {
276
+ return false;
277
+ }
278
+ try {
279
+ if (!fs.existsSync(configPath)) {
280
+ return false;
281
+ }
282
+ const rawContent = fs.readFileSync(configPath, 'utf8') || '';
283
+ if (!rawContent.trim()) {
284
+ return false;
285
+ }
286
+ const parsed = JSON.parse(rawContent);
287
+ const targetList = resolveEndpointCollection(parsed);
288
+ if (!Array.isArray(targetList) || targetList.length <= 1) {
289
+ return false;
290
+ }
291
+ const matchIndex = targetList.findIndex((entry) => createEndpointSignature(entry) === signature);
292
+ if (matchIndex <= 0) {
293
+ return matchIndex === 0;
294
+ }
295
+ const [selected] = targetList.splice(matchIndex, 1);
296
+ targetList.unshift(selected);
297
+ const serialized = Array.isArray(parsed) ? targetList : { ...parsed, endpoints: targetList };
298
+ writeJsonFile(configPath, serialized);
299
+ return true;
300
+ } catch (error) {
301
+ return false;
302
+ }
303
+ };
304
+
305
+ const ensureArrayOfStrings = (value, fallback = []) => {
306
+ if (!Array.isArray(value)) {
307
+ return [...fallback];
308
+ }
309
+ return value.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean);
310
+ };
311
+
312
+ const getAllMcpServerNames = () => {
313
+ const mcpPath = getConfigPath('mcp');
314
+ const parsed = readJsonFile(mcpPath, { mcpServers: {} });
315
+ const mcpServers = parsed.mcpServers && typeof parsed.mcpServers === 'object'
316
+ ? parsed.mcpServers
317
+ : {};
318
+ return Object.keys(mcpServers);
319
+ };
320
+
321
+ const loadSettings = ({ defaultTools = [] } = {}) => {
322
+ const ensureResult = ensureConfigFiles();
323
+ const settingsPath = getConfigPath('settings');
324
+ const parsed = readJsonFile(settingsPath, DEFAULT_SETTINGS);
325
+
326
+ const settings = {
327
+ mcps: (() => {
328
+ const allMcpNames = getAllMcpServerNames();
329
+ const mcpConfig = parsed.mcpServers;
330
+
331
+ // 只支持对象格式,默认全部启用,只有明确设置为 false 才禁用
332
+ if (!isPlainObject(mcpConfig)) {
333
+ return allMcpNames;
334
+ }
335
+
336
+ return allMcpNames.filter((name) => mcpConfig[name] !== false);
337
+ })(),
338
+ tools: (() => {
339
+ const toolConfig = parsed.tools;
340
+
341
+ // 只支持对象格式,默认全部启用,只有明确设置为 false 才禁用
342
+ if (!isPlainObject(toolConfig)) {
343
+ return ensureArrayOfStrings(defaultTools);
344
+ }
345
+
346
+ return defaultTools.filter((name) => toolConfig[name] !== false);
347
+ })(),
348
+ commands: ensureArrayOfStrings(parsed.commands),
349
+ commandPath: detectCommandPath(),
350
+ maxToolTokens: normalizePositiveInteger(
351
+ parsed.maxToolTokens,
352
+ DEFAULT_TOOL_RESPONSE_MAX_TOKENS
353
+ ),
354
+ allowedCommands: (() => {
355
+ const list = ensureArrayOfStrings(parsed.allowedCommands);
356
+ return list.length ? Array.from(new Set(list)) : [...DEFAULT_ALLOWED_COMMANDS];
357
+ })(),
358
+ compactTokenThreshold: normalizePositiveInteger(
359
+ parsed.compactTokenThreshold,
360
+ DEFAULT_COMPACT_TOKEN_THRESHOLD
361
+ ),
362
+ mcpToolTimeout: normalizePositiveInteger(
363
+ parsed.mcpToolTimeout,
364
+ DEFAULT_MCP_TOOL_TIMEOUT_MS
365
+ ),
366
+ outputMaxLength: normalizePositiveInteger(
367
+ parsed.outputMaxLength,
368
+ DEFAULT_OUTPUT_MAX_LENGTH
369
+ ),
370
+ executionTimeout: normalizePositiveInteger(
371
+ parsed.executionTimeout,
372
+ DEFAULT_EXECUTION_TIMEOUT
373
+ ),
374
+ serviceBootWindow: normalizePositiveInteger(
375
+ parsed.serviceBootWindow,
376
+ DEFAULT_SERVICE_BOOT_WINDOW
377
+ ),
378
+ largeFileLineThreshold: normalizePositiveInteger(
379
+ parsed.largeFileLineThreshold,
380
+ DEFAULT_LARGE_FILE_LINE_THRESHOLD
381
+ )
382
+ };
383
+
384
+ settings.toolOutputTokenLimit = settings.maxToolTokens;
385
+
386
+ return {
387
+ settingsPath,
388
+ settings,
389
+ created: ensureResult.createdFiles.includes(FILE_NAMES.settings)
390
+ };
391
+ };
392
+
393
+ module.exports = {
394
+ ConfigError,
395
+ loadEndpointConfig,
396
+ getDefaultConfigPath,
397
+ promoteEndpointInConfig,
398
+ loadSettings,
399
+ ensureConfigFiles,
400
+ getConfigPath,
401
+ DEFAULT_ALLOWED_COMMANDS,
402
+ DEFAULT_TOOL_RESPONSE_MAX_TOKENS,
403
+ DEFAULT_COMPACT_TOKEN_THRESHOLD,
404
+ COMPACT_SUMMARY_PROMPT
405
+ };