@ww_nero/mini-cli 1.0.95 → 1.0.97

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.95",
3
+ "version": "1.0.97",
4
4
  "description": "极简的 AI 命令行助手",
5
5
  "bin": {
6
6
  "mini": "bin/mini.js"
package/src/chat.js CHANGED
@@ -111,16 +111,35 @@ const splitDisplayLines = (text = '') => {
111
111
  return lines;
112
112
  };
113
113
 
114
+ const isToolError = (result) => {
115
+ if (typeof result === 'string' && result.startsWith('[Error]')) {
116
+ return true;
117
+ }
118
+ if (result && typeof result === 'object' && result.success === false) {
119
+ return true;
120
+ }
121
+ return false;
122
+ };
123
+
114
124
  const formatWriteOutput = (result) => {
125
+ if (isToolError(result)) {
126
+ const errorMsg = typeof result === 'string' ? result : (result.message || '写入失败');
127
+ return { isError: true, message: errorMsg.replace('[Error] ', '') };
128
+ }
115
129
  if (!result || typeof result !== 'object' || !result.success) {
116
- return null;
130
+ return { isError: true, message: '写入失败' };
117
131
  }
118
- return typeof result.content === 'string' ? result.content : String(result.content || '');
132
+ const lineCount = result.lineCount || countLines(result.content);
133
+ return { isError: false, lineCount };
119
134
  };
120
135
 
121
136
  const formatEditOutput = (result) => {
137
+ if (isToolError(result)) {
138
+ const errorMsg = typeof result === 'string' ? result : (result.message || '编辑失败');
139
+ return { isError: true, message: errorMsg.replace('[Error] ', '') };
140
+ }
122
141
  if (!result || typeof result !== 'object' || !result.success) {
123
- return null;
142
+ return { isError: true, message: '编辑失败' };
124
143
  }
125
144
  const lines = [];
126
145
  const parts = diffLines(result.search || '', result.replace || '');
@@ -133,7 +152,7 @@ const formatEditOutput = (result) => {
133
152
  }
134
153
  }
135
154
 
136
- return lines.join('\n');
155
+ return { isError: false, diff: lines.join('\n') };
137
156
  };
138
157
 
139
158
  const stringifyToolResult = (value) => {
@@ -172,6 +191,22 @@ const formatToolResultForDisplay = (text, maxLength = 100) => {
172
191
  return normalized.length > maxLength ? 'Done' : normalized;
173
192
  };
174
193
 
194
+ const getReadableErrorMessage = (error) => {
195
+ if (error && typeof error.message === 'string' && error.message.trim()) {
196
+ return error.message.trim();
197
+ }
198
+ if (typeof error === 'string' && error.trim()) {
199
+ return error.trim();
200
+ }
201
+ return '未知错误';
202
+ };
203
+
204
+ const buildEmptyAssistantResponseNotice = (usage) => {
205
+ const usageText = formatUsageTokens(usage);
206
+ const usageSuffix = usageText ? `(${usageText})` : '';
207
+ return `本次请求已结束,但模型未返回可显示内容${usageSuffix}。可能是接口返回空响应、只返回了空白内容,或流式输出在正文前结束。请重试,必要时检查模型服务日志。`;
208
+ };
209
+
175
210
  const indentToolResult = (text = '') => splitDisplayLines(text)
176
211
  .map((line) => ` ${line}`)
177
212
  .join('\n');
@@ -928,7 +963,8 @@ const startChatSession = async ({
928
963
  onReasoningToken: handleReasoningChunk,
929
964
  onUsage: updateLoadingPromptTokens,
930
965
  onRetry: (retryCount, error) => {
931
- const detail = error ? `(${error.message})` : '';
966
+ const detailMessage = error ? getReadableErrorMessage(error) : '';
967
+ const detail = detailMessage ? `(${detailMessage})` : '';
932
968
  process.stdout.write(`\n${chalk.yellow(`请求失败,${retryCount} 次重试中${detail}...`)}\n`);
933
969
  }
934
970
  });
@@ -983,7 +1019,7 @@ const startChatSession = async ({
983
1019
  try {
984
1020
  toolResult = await handler(parsedArgs);
985
1021
  } catch (toolError) {
986
- toolResult = `工具执行异常: ${toolError.message}`;
1022
+ toolResult = `[Error] 工具执行异常: ${toolError.message}`;
987
1023
  } finally {
988
1024
  clearLoadingPrompt();
989
1025
  }
@@ -994,30 +1030,33 @@ const startChatSession = async ({
994
1030
 
995
1031
  // Display tool output
996
1032
  if (functionName === 'write') {
997
- // For write, display full content in white
998
1033
  const writeOutput = formatWriteOutput(toolResult);
999
- if (writeOutput) {
1000
- console.log(writeOutput);
1034
+ if (writeOutput.isError) {
1035
+ console.log(chalk.red(indentToolResult(writeOutput.message)));
1036
+ } else {
1037
+ console.log(chalk.gray(`已写入,共${writeOutput.lineCount}行`));
1001
1038
  }
1002
1039
  } else if (functionName === 'edit') {
1003
- // For edit, display diff-style output
1004
1040
  const editOutput = formatEditOutput(toolResult);
1005
- if (editOutput) {
1006
- console.log(editOutput);
1041
+ if (editOutput.isError) {
1042
+ console.log(chalk.red(indentToolResult(editOutput.message)));
1043
+ } else if (editOutput.diff) {
1044
+ console.log(editOutput.diff);
1007
1045
  }
1008
1046
  } else if (functionName === 'read') {
1009
- // read 工具特殊处理:成功时显示行数,失败时显示错误信息
1010
- const normalized = rawToolContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
1011
- const lineCount = countLines(normalized);
1012
- if (normalized && !normalized.startsWith('读取失败') && !normalized.startsWith('无效路径') && !normalized.startsWith('filePath 参数不能为空')) {
1013
- console.log(chalk.gray(`已读取,共${lineCount}行`));
1047
+ const isError = rawToolContent.startsWith('[Error]');
1048
+ if (isError) {
1049
+ console.log(chalk.red(indentToolResult(rawToolContent.replace('[Error] ', ''))));
1014
1050
  } else {
1015
- console.log(chalk.gray(indentToolResult(rawToolContent)));
1051
+ const normalized = rawToolContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
1052
+ const lineCount = countLines(normalized);
1053
+ console.log(chalk.gray(`已读取,共${lineCount}行`));
1016
1054
  }
1017
1055
  } else {
1056
+ const isError = rawToolContent.startsWith('[Error]');
1018
1057
  const displayResult = formatToolResultForDisplay(rawToolContent, 100);
1019
1058
  if (displayResult) {
1020
- console.log(chalk.gray(indentToolResult(displayResult)));
1059
+ console.log((isError ? chalk.red : chalk.gray)(indentToolResult(displayResult.replace('[Error] ', ''))));
1021
1060
  }
1022
1061
  }
1023
1062
 
@@ -1070,8 +1109,18 @@ const startChatSession = async ({
1070
1109
  continue;
1071
1110
  }
1072
1111
 
1073
- conversationAdvanced = true;
1074
1112
  const finalText = sanitizeContentWithThink(streamState.content, result.content);
1113
+ if (!mergedReasoning && !finalText) {
1114
+ if (messages[messages.length - 1] === userMessage) {
1115
+ messages.pop();
1116
+ persistHistorySafely();
1117
+ }
1118
+ ensureNewline();
1119
+ console.log(chalk.yellow(buildEmptyAssistantResponseNotice(result.usage)));
1120
+ break;
1121
+ }
1122
+
1123
+ conversationAdvanced = true;
1075
1124
  renderAssistantOutput(mergedReasoning, finalText, ensureNewline);
1076
1125
  const assistantMessage = buildHistoryAssistantMessage(
1077
1126
  result.message,
@@ -1102,7 +1151,7 @@ const startChatSession = async ({
1102
1151
  console.log(chalk.yellow('对话已取消。'));
1103
1152
  }
1104
1153
  } else {
1105
- console.error(chalk.red(`请求失败: ${error.message}`));
1154
+ console.error(chalk.red(`请求失败: ${getReadableErrorMessage(error)}`));
1106
1155
  }
1107
1156
  } finally {
1108
1157
  clearLoadingPrompt();
package/src/tools/bash.js CHANGED
@@ -3,7 +3,7 @@ const { resolveWorkspacePath } = require('../utils/helpers');
3
3
 
4
4
  const executeCommand = async ({ command, workingDirectory = '.', isService = false } = {}, context = {}) => {
5
5
  if (!command || typeof command !== 'string' || !command.trim()) {
6
- return 'command 参数不能为空';
6
+ return '[Error] command 参数不能为空';
7
7
  }
8
8
 
9
9
  const normalizedCommand = command.replace(/\bpython3\b/g, 'python');
@@ -16,7 +16,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
16
16
  try {
17
17
  cwd = resolveWorkspacePath(context.workspaceRoot, workingDirectory || '.');
18
18
  } catch (error) {
19
- return `工作目录无效: ${error.message}`;
19
+ return `[Error] 工作目录无效: ${error.message}`;
20
20
  }
21
21
 
22
22
  return new Promise((resolve) => {
@@ -53,7 +53,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
53
53
  if (settled) return;
54
54
  settled = true;
55
55
  cleanup();
56
- resolve(`命令执行失败: ${error.message}`);
56
+ resolve(`[Error] 命令执行失败: ${error.message}`);
57
57
  };
58
58
 
59
59
  const handleProcessClose = (code) => {
@@ -66,7 +66,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
66
66
  } else {
67
67
  const errOutput = stderr.trim();
68
68
  const stdOutput = stdout.trim();
69
- resolve(`命令执行失败,退出码 ${code}${errOutput ? `\n错误: ${errOutput}` : ''}${stdOutput ? `\n输出: ${stdOutput}` : ''}`);
69
+ resolve(`[Error] 命令执行失败,退出码 ${code}${errOutput ? `\n错误: ${errOutput}` : ''}${stdOutput ? `\n输出: ${stdOutput}` : ''}`);
70
70
  }
71
71
  };
72
72
 
@@ -98,7 +98,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
98
98
  if (settled) return;
99
99
  child.kill('SIGTERM');
100
100
  cleanup();
101
- resolve(`命令执行超时 (超过 ${executionTimeout / 1000}s)`);
101
+ resolve(`[Error] 命令执行超时 (超过 ${executionTimeout / 1000}s)`);
102
102
  }, executionTimeout);
103
103
  }
104
104
  });
package/src/tools/edit.js CHANGED
@@ -10,27 +10,27 @@ const {
10
10
 
11
11
  const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
12
12
  if (!filePath || typeof filePath !== 'string') {
13
- return 'filePath 参数不能为空';
13
+ return '[Error] filePath 参数不能为空';
14
14
  }
15
15
  if (typeof search !== 'string' || search.trim() === '') {
16
- return 'search 参数不能为空';
16
+ return '[Error] search 参数不能为空';
17
17
  }
18
18
  if (typeof replace !== 'string') {
19
- return 'replace 参数必须是字符串,可为空字符串表示删除';
19
+ return '[Error] replace 参数必须是字符串,可为空字符串表示删除';
20
20
  }
21
21
 
22
22
  let absolutePath;
23
23
  try {
24
24
  absolutePath = resolveWorkspacePath(context.workspaceRoot, filePath);
25
25
  } catch (error) {
26
- return `无效路径: ${error.message}`;
26
+ return `[Error] 无效路径: ${error.message}`;
27
27
  }
28
28
 
29
29
  let originCode;
30
30
  try {
31
31
  originCode = await readTextFile(absolutePath);
32
32
  } catch (error) {
33
- return `读取文件失败 ${filePath}: ${error.message}`;
33
+ return `[Error] 读取文件失败 ${filePath}: ${error.message}`;
34
34
  }
35
35
 
36
36
  const normalizedOrigin = normalizeLineBreaks(originCode);
@@ -38,7 +38,7 @@ const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
38
38
  let normalizedReplace = normalizeLineBreaks(replace);
39
39
 
40
40
  if (!normalizedSearch) {
41
- return 'search 参数规范化后为空';
41
+ return '[Error] search 参数规范化后为空';
42
42
  }
43
43
 
44
44
  if (normalizedReplace) {
@@ -58,12 +58,12 @@ const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
58
58
  }
59
59
 
60
60
  if (!normalizedOrigin.includes(searchPattern)) {
61
- return `在文件 ${filePath} 中未找到要搜索的内容`;
61
+ return `[Error] 在文件 ${filePath} 中未找到要搜索的内容`;
62
62
  }
63
63
 
64
64
  const updated = normalizedOrigin.replaceAll(searchPattern, normalizedReplace);
65
65
  if (isCodeIdentical(originCode, updated)) {
66
- return `修改前后内容相同: ${filePath}`;
66
+ return `[Error] 修改前后内容相同: ${filePath}`;
67
67
  }
68
68
 
69
69
  const processed = processContent(updated);
@@ -78,7 +78,7 @@ const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
78
78
  replace: normalizedReplace
79
79
  };
80
80
  } catch (error) {
81
- return `搜索替换失败 ${filePath}: ${error.message}`;
81
+ return `[Error] 搜索替换失败 ${filePath}: ${error.message}`;
82
82
  }
83
83
  };
84
84
 
package/src/tools/mcp.js CHANGED
@@ -319,7 +319,7 @@ const resolveImageSource = (workspaceRoot, args = {}) => {
319
319
  const resolved = path.resolve(root, imageSource);
320
320
  const relative = path.relative(root, resolved);
321
321
  if (relative.startsWith('..') || path.isAbsolute(relative) || !fs.existsSync(resolved)) {
322
- return { error: '找不到图片' };
322
+ return { error: '[Error] 找不到图片' };
323
323
  }
324
324
  return { args: { ...args, image_source: resolved } };
325
325
  };
@@ -438,11 +438,11 @@ class McpManager {
438
438
  const isError = Boolean(result && result.isError);
439
439
  const output = formatMcpContent(result && result.content);
440
440
  if (isError) {
441
- return output ? `MCP 工具返回错误:${output}` : 'MCP 工具返回错误';
441
+ return output ? `[Error] MCP 工具返回错误:${output}` : '[Error] MCP 工具返回错误';
442
442
  }
443
443
  return output || 'MCP 工具无输出';
444
444
  } catch (error) {
445
- return `调用 MCP 工具失败:${error.message}`;
445
+ return `[Error] 调用 MCP 工具失败:${error.message}`;
446
446
  }
447
447
  }
448
448
  };
package/src/tools/read.js CHANGED
@@ -2,20 +2,20 @@ const { resolveWorkspacePath, readTextFile } = require('../utils/helpers');
2
2
 
3
3
  const readFile = async ({ filePath } = {}, context = {}) => {
4
4
  if (!filePath || typeof filePath !== 'string') {
5
- return 'filePath 参数不能为空';
5
+ return '[Error] filePath 参数不能为空';
6
6
  }
7
7
 
8
8
  let absolutePath;
9
9
  try {
10
10
  absolutePath = resolveWorkspacePath(context.workspaceRoot, filePath);
11
11
  } catch (error) {
12
- return `无效路径: ${error.message}`;
12
+ return `[Error] 无效路径: ${error.message}`;
13
13
  }
14
14
 
15
15
  try {
16
16
  return await readTextFile(absolutePath);
17
17
  } catch (error) {
18
- return `读取失败 ${filePath}: ${error.message}`;
18
+ return `[Error] 读取失败 ${filePath}: ${error.message}`;
19
19
  }
20
20
  };
21
21
 
@@ -16,7 +16,7 @@ const handler = async ({ skillName, filePath } = {}) => {
16
16
  content: result.content
17
17
  };
18
18
  } catch (error) {
19
- return `读取 skill 失败:${error.message}`;
19
+ return `[Error] 读取 skill 失败:${error.message}`;
20
20
  }
21
21
  };
22
22
 
@@ -11,17 +11,17 @@ const countLines = (text = '') => {
11
11
 
12
12
  const writeFile = async ({ filePath, content } = {}, context = {}) => {
13
13
  if (!filePath || typeof filePath !== 'string') {
14
- return 'filePath 参数不能为空';
14
+ return '[Error] filePath 参数不能为空';
15
15
  }
16
16
  if (typeof content !== 'string') {
17
- return 'content 必须是字符串';
17
+ return '[Error] content 必须是字符串';
18
18
  }
19
19
 
20
20
  let absolutePath;
21
21
  try {
22
22
  absolutePath = resolveWorkspacePath(context.workspaceRoot, filePath);
23
23
  } catch (error) {
24
- return `无效路径: ${error.message}`;
24
+ return `[Error] 无效路径: ${error.message}`;
25
25
  }
26
26
 
27
27
  try {
@@ -33,7 +33,7 @@ const writeFile = async ({ filePath, content } = {}, context = {}) => {
33
33
  lineCount: countLines(content)
34
34
  };
35
35
  } catch (error) {
36
- return `写入失败 ${filePath}: ${error.message}`;
36
+ return `[Error] 写入失败 ${filePath}: ${error.message}`;
37
37
  }
38
38
  };
39
39