@ww_nero/mini-cli 1.0.57 → 1.0.58

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