@ww_nero/mini-cli 1.0.62 → 1.0.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ww_nero/mini-cli",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "description": "极简的 AI 命令行助手",
5
5
  "bin": {
6
6
  "mini": "bin/mini.js"
package/src/config.js CHANGED
@@ -1,376 +1,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_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}', resume: 'codex --full-auto resume' },
81
- { base: 'claude', prompt: 'claude --permission-mode acceptEdits {prompt}', resume: 'claude --permission-mode acceptEdits --continue' },
82
- { base: 'gemini', prompt: 'gemini -m pro --approval-mode=auto_edit -i {prompt}', resume: 'gemini -m pro --approval-mode=auto_edit --resume' }
83
- ];
84
-
85
- const DEFAULT_SETTINGS = {
86
- mcpServers: {},
87
- tools: {},
88
- commands: [],
89
- maxToolTokens: DEFAULT_MAX_TOOL_TOKENS,
90
- compactTokenThreshold: DEFAULT_COMPACT_TOKEN_THRESHOLD,
91
- allowedCommands: [...DEFAULT_ALLOWED_COMMANDS],
92
- worktrees: 2
93
- };
94
-
95
- const DEFAULT_MINI_CONTENT = '# 在此填写全局系统指令。\n';
96
-
97
- class ConfigError extends Error {
98
- constructor(message, configPath) {
99
- super(message);
100
- this.name = 'ConfigError';
101
- this.configPath = configPath;
102
- }
103
- }
104
-
105
- const isPlainObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
106
-
107
- const ensureDir = (dirPath) => {
108
- if (!fs.existsSync(dirPath)) {
109
- fs.mkdirSync(dirPath, { recursive: true });
110
- }
111
- };
112
-
113
- const writeJsonFile = (filePath, data) => {
114
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
115
- };
116
-
117
- const ensureFile = (filePath, data) => {
118
- if (fs.existsSync(filePath)) return false;
119
- if (typeof data === 'string') {
120
- fs.writeFileSync(filePath, data, 'utf8');
121
- } else {
122
- writeJsonFile(filePath, data);
123
- }
124
- return true;
125
- };
126
-
127
- const getConfigPath = (key) => path.join(CONFIG_DIR, FILE_NAMES[key]);
128
- const getDefaultConfigPath = () => getConfigPath('chat');
129
-
130
- const readJsonFile = (filePath, fallback = {}) => {
131
- try {
132
- const raw = fs.readFileSync(filePath, 'utf8');
133
- if (!raw.trim()) return fallback;
134
- return JSON.parse(raw);
135
- } catch (_) {
136
- return fallback;
137
- }
138
- };
139
-
140
- const extractEndpointList = (parsed) => {
141
- if (Array.isArray(parsed)) return parsed;
142
- if (isPlainObject(parsed)) {
143
- if (Array.isArray(parsed.endpoints)) return parsed.endpoints;
144
- if (parsed.endpoint) return [parsed.endpoint];
145
- }
146
- return [];
147
- };
148
-
149
- const ensureConfigFiles = () => {
150
- ensureDir(CONFIG_DIR);
151
- const createdFiles = [];
152
-
153
- if (ensureFile(getConfigPath('chat'), { endpoints: DEFAULT_CHAT_ENDPOINTS })) {
154
- createdFiles.push(FILE_NAMES.chat);
155
- }
156
-
157
- if (ensureFile(getConfigPath('commit'), { model: DEFAULT_COMMIT_MODEL })) {
158
- createdFiles.push(FILE_NAMES.commit);
159
- }
160
-
161
- if (ensureFile(getConfigPath('mcp'), { mcpServers: {} })) {
162
- createdFiles.push(FILE_NAMES.mcp);
163
- }
164
-
165
- if (ensureFile(getConfigPath('settings'), DEFAULT_SETTINGS)) {
166
- createdFiles.push(FILE_NAMES.settings);
167
- }
168
-
169
- if (ensureFile(getConfigPath('worktree'), { commands: DEFAULT_WORKTREE_COMMANDS })) {
170
- createdFiles.push(FILE_NAMES.worktree);
171
- }
172
-
173
- if (ensureFile(getConfigPath('mini'), DEFAULT_MINI_CONTENT)) {
174
- createdFiles.push(FILE_NAMES.mini);
175
- }
176
-
177
- return { configDir: CONFIG_DIR, createdFiles };
178
- };
179
-
180
- const normalizePositiveInteger = (value, fallback) => {
181
- const numberValue = Number(value);
182
- if (Number.isFinite(numberValue) && numberValue > 0) {
183
- return Math.trunc(numberValue);
184
- }
185
- return fallback;
186
- };
187
-
188
- const normalizeField = (value) => (typeof value === 'string' ? value.trim() : '');
189
- const createEndpointSignature = (entry = {}) => {
190
- const pick = (key) => normalizeField(entry[key]).toLowerCase();
191
- return ['name', 'model', 'baseUrl', 'key'].map(pick).join('::');
192
- };
193
-
194
- const normalizeEndpoint = (raw = {}, index = 0) => {
195
- if (!isPlainObject(raw)) return { endpoint: null };
196
-
197
- const key = normalizeField(raw.key);
198
- const baseUrl = normalizeField(raw.baseUrl);
199
- const model = normalizeField(raw.model);
200
- if (!baseUrl || !key || !model) {
201
- return { endpoint: null };
202
- }
203
-
204
- const nameRaw = normalizeField(raw.alias || raw.name);
205
- const name = nameRaw || model || `model_${index + 1}`;
206
- const think = raw.think === true;
207
- const price = Number.isFinite(Number(raw.price)) ? Number(raw.price) : 0;
208
- const options = isPlainObject(raw.options) ? raw.options : undefined;
209
-
210
- const endpoint = {
211
- baseUrl,
212
- key,
213
- model,
214
- name,
215
- alias: name,
216
- think,
217
- price,
218
- ...(options ? { options } : {})
219
- };
220
-
221
- return {
222
- endpoint: {
223
- ...endpoint,
224
- signature: createEndpointSignature({ ...raw, ...endpoint })
225
- }
226
- };
227
- };
228
-
229
- const loadEndpointConfig = () => {
230
- const ensureResult = ensureConfigFiles();
231
- const configPath = getConfigPath('chat');
232
- const parsed = readJsonFile(configPath, { endpoints: [] });
233
- const rawList = extractEndpointList(parsed);
234
-
235
- const endpoints = rawList
236
- .map((entry, idx) => normalizeEndpoint(entry, idx).endpoint)
237
- .filter(Boolean)
238
- .map((endpoint, idx) => ({ ...endpoint, name: endpoint.name || endpoint.alias || endpoint.model || `model_${idx + 1}` }));
239
-
240
- const validEndpoints = endpoints.filter((ep) => ep.key && !ep.key.toLowerCase().includes('your-api-key'));
241
- if (validEndpoints.length === 0) {
242
- const suffix = ensureResult.createdFiles.includes(FILE_NAMES.chat)
243
- ? '已自动生成模板,请填写 baseUrl/key/model 后重新运行 mini。'
244
- : '请在配置中提供至少一个有效的 baseUrl、key、model。';
245
- throw new ConfigError(`${configPath} 缺少可用的模型配置。${suffix}`, configPath);
246
- }
247
-
248
- return { endpoints: validEndpoints, configPath, created: ensureResult.createdFiles.includes(FILE_NAMES.chat) };
249
- };
250
-
251
- const resolveEndpointCollection = (parsed) => {
252
- if (Array.isArray(parsed)) {
253
- return parsed;
254
- }
255
- if (parsed && typeof parsed === 'object' && Array.isArray(parsed.endpoints)) {
256
- return parsed.endpoints;
257
- }
258
- return null;
259
- };
260
-
261
- const promoteEndpointInConfig = (configPath, signature) => {
262
- if (!configPath || !signature) {
263
- return false;
264
- }
265
- try {
266
- if (!fs.existsSync(configPath)) {
267
- return false;
268
- }
269
- const rawContent = fs.readFileSync(configPath, 'utf8') || '';
270
- if (!rawContent.trim()) {
271
- return false;
272
- }
273
- const parsed = JSON.parse(rawContent);
274
- const targetList = resolveEndpointCollection(parsed);
275
- if (!Array.isArray(targetList) || targetList.length <= 1) {
276
- return false;
277
- }
278
- const matchIndex = targetList.findIndex((entry) => createEndpointSignature(entry) === signature);
279
- if (matchIndex <= 0) {
280
- return matchIndex === 0;
281
- }
282
- const [selected] = targetList.splice(matchIndex, 1);
283
- targetList.unshift(selected);
284
- const serialized = Array.isArray(parsed) ? targetList : { ...parsed, endpoints: targetList };
285
- writeJsonFile(configPath, serialized);
286
- return true;
287
- } catch (error) {
288
- return false;
289
- }
290
- };
291
-
292
- const ensureArrayOfStrings = (value, fallback = []) => {
293
- if (!Array.isArray(value)) {
294
- return [...fallback];
295
- }
296
- return value.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean);
297
- };
298
-
299
- const getAllMcpServerNames = () => {
300
- const mcpPath = getConfigPath('mcp');
301
- const parsed = readJsonFile(mcpPath, { mcpServers: {} });
302
- const mcpServers = parsed.mcpServers && typeof parsed.mcpServers === 'object'
303
- ? parsed.mcpServers
304
- : {};
305
- return Object.keys(mcpServers);
306
- };
307
-
308
- const loadSettings = ({ defaultTools = [] } = {}) => {
309
- const ensureResult = ensureConfigFiles();
310
- const settingsPath = getConfigPath('settings');
311
- const parsed = readJsonFile(settingsPath, DEFAULT_SETTINGS);
312
-
313
- const settings = {
314
- mcps: (() => {
315
- const allMcpNames = getAllMcpServerNames();
316
- const mcpConfig = parsed.mcpServers;
317
-
318
- // 只支持对象格式,默认全部启用,只有明确设置为 false 才禁用
319
- if (!isPlainObject(mcpConfig)) {
320
- return allMcpNames;
321
- }
322
-
323
- return allMcpNames.filter((name) => mcpConfig[name] !== false);
324
- })(),
325
- tools: (() => {
326
- const toolConfig = parsed.tools;
327
-
328
- // 只支持对象格式,默认全部启用,只有明确设置为 false 才禁用
329
- if (!isPlainObject(toolConfig)) {
330
- return ensureArrayOfStrings(defaultTools);
331
- }
332
-
333
- return defaultTools.filter((name) => toolConfig[name] !== false);
334
- })(),
335
- commands: ensureArrayOfStrings(parsed.commands),
336
- commandPath: detectCommandPath(),
337
- maxToolTokens: normalizePositiveInteger(
338
- parsed.maxToolTokens,
339
- DEFAULT_MAX_TOOL_TOKENS
340
- ),
341
- allowedCommands: (() => {
342
- const list = ensureArrayOfStrings(parsed.allowedCommands);
343
- return list.length ? Array.from(new Set(list)) : [...DEFAULT_ALLOWED_COMMANDS];
344
- })(),
345
- compactTokenThreshold: normalizePositiveInteger(
346
- parsed.compactTokenThreshold,
347
- DEFAULT_COMPACT_TOKEN_THRESHOLD
348
- ),
349
- worktrees: normalizePositiveInteger(
350
- parsed.worktrees,
351
- DEFAULT_SETTINGS.worktrees
352
- )
353
- };
354
-
355
- settings.toolOutputTokenLimit = settings.maxToolTokens;
356
-
357
- return {
358
- settingsPath,
359
- settings,
360
- created: ensureResult.createdFiles.includes(FILE_NAMES.settings)
361
- };
362
- };
363
-
364
- module.exports = {
365
- ConfigError,
366
- loadEndpointConfig,
367
- getDefaultConfigPath,
368
- promoteEndpointInConfig,
369
- loadSettings,
370
- ensureConfigFiles,
371
- getConfigPath,
372
- DEFAULT_ALLOWED_COMMANDS,
373
- DEFAULT_MAX_TOOL_TOKENS,
374
- DEFAULT_COMPACT_TOKEN_THRESHOLD,
375
- COMPACT_SUMMARY_PROMPT
376
- };
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
+ };
package/src/tools/bash.js CHANGED
@@ -2,10 +2,6 @@ const { spawn } = require('child_process');
2
2
  const { resolveWorkspacePath } = require('../utils/helpers');
3
3
  const { DEFAULT_ALLOWED_COMMANDS } = require('../config');
4
4
 
5
- const OUTPUT_MAX_LENGTH = 12000;
6
- const EXECUTION_TIMEOUT = 300000;
7
- const SERVICE_RETURN_DELAY = 5000;
8
-
9
5
  // Git 只读命令白名单
10
6
  const GIT_READONLY_COMMANDS = ['show', 'diff', 'log', 'status', 'branch', 'tag', 'ls-files', 'ls-tree', 'rev-parse', 'reflog', 'blame', 'shortlog', 'describe', 'config --get', 'config --list', 'remote', 'ls-remote', 'fetch --dry-run', 'grep'];
11
7
 
@@ -86,6 +82,10 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
86
82
  const allowedCommands = Array.isArray(context.allowedCommands) && context.allowedCommands.length > 0
87
83
  ? context.allowedCommands
88
84
  : DEFAULT_ALLOWED_COMMANDS;
85
+
86
+ const outputMaxLength = context.outputMaxLength || 12000;
87
+ const executionTimeout = context.executionTimeout || 300000;
88
+ const serviceBootWindow = context.serviceBootWindow || 5000;
89
89
  const commands = splitShellCommands(normalizedCommand);
90
90
  if (commands.length === 0) {
91
91
  return '未找到有效的命令';
@@ -129,15 +129,15 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
129
129
 
130
130
  child.stdout.on('data', (data) => {
131
131
  stdout += data.toString();
132
- if (stdout.length > OUTPUT_MAX_LENGTH) {
133
- stdout = stdout.slice(0, OUTPUT_MAX_LENGTH);
132
+ if (stdout.length > outputMaxLength) {
133
+ stdout = stdout.slice(0, outputMaxLength);
134
134
  }
135
135
  });
136
136
 
137
137
  child.stderr.on('data', (data) => {
138
138
  stderr += data.toString();
139
- if (stderr.length > OUTPUT_MAX_LENGTH) {
140
- stderr = stderr.slice(0, OUTPUT_MAX_LENGTH);
139
+ if (stderr.length > outputMaxLength) {
140
+ stderr = stderr.slice(0, outputMaxLength);
141
141
  }
142
142
  });
143
143
 
@@ -169,8 +169,8 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
169
169
  const stdOutput = stdout.trim();
170
170
  const errOutput = stderr.trim();
171
171
  const captured = stdOutput || errOutput ? `\n当前输出:\n${stdOutput}${errOutput ? `\n错误:\n${errOutput}` : ''}` : '\n暂无输出';
172
- resolve(`已等待 ${SERVICE_RETURN_DELAY / 1000}s,命令已在后台持续运行(PID: ${child.pid})。${captured}`);
173
- }, SERVICE_RETURN_DELAY);
172
+ resolve(`已等待 ${serviceBootWindow / 1000}s,命令已在后台持续运行(PID: ${child.pid})。${captured}`);
173
+ }, serviceBootWindow);
174
174
 
175
175
  const serviceErrorHandler = (error) => {
176
176
  clearTimeout(timer);
@@ -190,8 +190,8 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
190
190
  if (settled) return;
191
191
  child.kill('SIGTERM');
192
192
  cleanup();
193
- resolve('命令执行超时 (超过 300s)');
194
- }, EXECUTION_TIMEOUT);
193
+ resolve(`命令执行超时 (超过 ${executionTimeout / 1000}s)`);
194
+ }, executionTimeout);
195
195
  }
196
196
  });
197
197
  };
@@ -15,7 +15,15 @@ const createToolRuntime = async (workspaceRoot, options = {}) => {
15
15
  const enabledMcps = Array.isArray(settings.mcps) ? settings.mcps : [];
16
16
  const allowedCommands = settings.allowedCommands;
17
17
 
18
- const context = { workspaceRoot, allowedCommands, ...options };
18
+ const context = {
19
+ workspaceRoot,
20
+ allowedCommands,
21
+ outputMaxLength: settings.outputMaxLength,
22
+ executionTimeout: settings.executionTimeout,
23
+ serviceBootWindow: settings.serviceBootWindow,
24
+ largeFileLineThreshold: settings.largeFileLineThreshold,
25
+ ...options
26
+ };
19
27
 
20
28
  const tools = [];
21
29
  const handlers = {};
@@ -35,7 +43,9 @@ const createToolRuntime = async (workspaceRoot, options = {}) => {
35
43
 
36
44
  TOOL_MODULES.forEach(registerTool);
37
45
 
38
- const mcpManager = await createMcpManager(workspaceRoot, enabledMcps);
46
+ const mcpManager = await createMcpManager(workspaceRoot, enabledMcps, {
47
+ mcpToolTimeout: settings.mcpToolTimeout
48
+ });
39
49
  let mcpConfigPath = null;
40
50
  const mcpToolNames = new Set(); // 记录所有 MCP 工具名称
41
51
  const enabledMcpNames = []; // 记录启用的 MCP 服务器名称
package/src/tools/mcp.js CHANGED
@@ -305,9 +305,10 @@ const formatMcpContent = (content) => {
305
305
  };
306
306
 
307
307
  class McpManager {
308
- constructor(workspaceRoot, allowedMcpNames = null) {
308
+ constructor(workspaceRoot, allowedMcpNames = null, options = {}) {
309
309
  this.workspaceRoot = workspaceRoot;
310
310
  this.allowedMcpNames = Array.isArray(allowedMcpNames) ? allowedMcpNames : null;
311
+ this.mcpToolTimeout = options.mcpToolTimeout || 600000;
311
312
  this.clients = [];
312
313
  }
313
314
 
@@ -407,7 +408,7 @@ class McpManager {
407
408
  },
408
409
  undefined,
409
410
  {
410
- timeout: 600000 // 10 分钟超时
411
+ timeout: this.mcpToolTimeout
411
412
  }
412
413
  );
413
414
  const isError = Boolean(result && result.isError);
@@ -460,11 +461,11 @@ class McpManager {
460
461
  }
461
462
  }
462
463
 
463
- const createMcpManager = async (workspaceRoot, allowedMcpNames = null) => {
464
+ const createMcpManager = async (workspaceRoot, allowedMcpNames = null, options = {}) => {
464
465
  if (Array.isArray(allowedMcpNames) && allowedMcpNames.length === 0) {
465
466
  return null;
466
467
  }
467
- const manager = new McpManager(workspaceRoot, allowedMcpNames);
468
+ const manager = new McpManager(workspaceRoot, allowedMcpNames, options);
468
469
  await manager.initialize();
469
470
  const tools = manager.getTools();
470
471
  if (!tools || tools.length === 0) {