@ww_nero/mini-cli 1.0.90 → 1.0.92

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.90",
3
+ "version": "1.0.92",
4
4
  "description": "极简的 AI 命令行助手",
5
5
  "bin": {
6
6
  "mini": "bin/mini.js"
package/src/chat.js CHANGED
@@ -26,7 +26,7 @@ const {
26
26
  mergeReasoningContent,
27
27
  renderAssistantOutput,
28
28
  sanitizeContentWithThink,
29
- printUsageTokens
29
+ formatUsageTokens
30
30
  } = require('./utils/output');
31
31
  const {
32
32
  createHistoryFilePath,
@@ -151,42 +151,31 @@ const enforceToolOutputLimit = (text, tokenLimit) => {
151
151
  const content = typeof text === 'string' ? text : stringifyToolResult(text);
152
152
  const safeContent = typeof content === 'string' ? content : '';
153
153
  try {
154
- const tokens = encode(safeContent);
155
- const tokenCount = tokens.length;
156
- if (tokenCount > tokenLimit) {
157
- return {
158
- content: '输出内容太长,请优化工具调用方式。',
159
- truncated: true,
160
- originalTokenCount: tokenCount,
161
- limit: tokenLimit
162
- };
154
+ if (encode(safeContent).length > tokenLimit) {
155
+ return '输出内容太长,请优化工具调用方式。';
163
156
  }
164
- return {
165
- content: safeContent,
166
- truncated: false,
167
- tokenCount
168
- };
157
+ return safeContent;
169
158
  } catch (_) {
170
159
  // 忽略分词异常,保持原始输出
171
- return {
172
- content: safeContent,
173
- truncated: false,
174
- tokenCount: null
175
- };
160
+ return safeContent;
176
161
  }
177
162
  };
178
163
 
179
- const truncateForDisplay = (text, maxLength = 100) => {
180
- if (!text || typeof text !== 'string') {
164
+ const formatToolResultForDisplay = (text, maxLength = 100) => {
165
+ if (typeof text !== 'string') {
181
166
  return '';
182
167
  }
183
- const normalized = text.replace(/\s+/g, ' ').trim();
184
- if (normalized.length <= maxLength) {
185
- return normalized;
168
+ const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
169
+ if (!normalized) {
170
+ return '';
186
171
  }
187
- return normalized.substring(0, maxLength) + '...';
172
+ return normalized.length > maxLength ? 'Done' : normalized;
188
173
  };
189
174
 
175
+ const indentToolResult = (text = '') => splitDisplayLines(text)
176
+ .map((line) => ` ${line}`)
177
+ .join('\n');
178
+
190
179
  const supportsCursorControl = () => Boolean(process.stdout && process.stdout.isTTY);
191
180
 
192
181
  let loadingCursorActive = false;
@@ -609,21 +598,29 @@ const startChatSession = async ({
609
598
  const DEFAULT_PROMPT = `${chalk.cyan('用户>')} `;
610
599
  rl.setPrompt(DEFAULT_PROMPT);
611
600
 
612
- const LOADING_PROMPT_BASE_TEXT = '⌛ 回答中...(按 Esc 取消)';
601
+ const LOADING_PROMPT_BASE_TEXT = '思考中...';
613
602
  const FLOW_GRADIENT_COLORS = ['#FFF7CC', '#FFE58F', '#FFD666', '#FFC53D', '#FFA940', '#FF7A45'];
614
603
  const FLOW_DIM_COLOR = '#5F5F6A';
615
- const loadingPromptCharacters = Array.from(LOADING_PROMPT_BASE_TEXT);
616
- const loadingPromptCharCount = loadingPromptCharacters.length || 1;
617
604
  const supportsHexColor = typeof chalk.hex === 'function';
618
605
  const flowGradientFns = supportsHexColor
619
606
  ? FLOW_GRADIENT_COLORS.map((hex) => chalk.hex(hex))
620
607
  : [];
621
608
  const dimTextFn = supportsHexColor ? chalk.hex(FLOW_DIM_COLOR) : chalk.yellow;
622
609
 
610
+ const buildLoadingPromptText = () => {
611
+ if (!loadingPromptTokensText) {
612
+ return `${LOADING_PROMPT_BASE_TEXT}(按Esc取消)`;
613
+ }
614
+ return `${LOADING_PROMPT_BASE_TEXT}(${loadingPromptTokensText},按Esc取消)`;
615
+ };
616
+
623
617
  const formatLoadingPrompt = (frame) => {
618
+ const promptText = buildLoadingPromptText();
624
619
  if (!supportsHexColor) {
625
- return chalk.yellow(LOADING_PROMPT_BASE_TEXT);
620
+ return chalk.yellow(promptText);
626
621
  }
622
+ const loadingPromptCharacters = Array.from(promptText);
623
+ const loadingPromptCharCount = loadingPromptCharacters.length || 1;
627
624
  const gradientLength = Math.min(flowGradientFns.length, loadingPromptCharCount);
628
625
  const startIndex = frame % loadingPromptCharCount;
629
626
  return loadingPromptCharacters.map((char, index) => {
@@ -639,6 +636,7 @@ const startChatSession = async ({
639
636
  let loadingPromptFrame = 0;
640
637
  let loadingPromptTimer = null;
641
638
  let loadingPromptVisible = false;
639
+ let loadingPromptTokensText = '';
642
640
 
643
641
  const getCurrentLoadingPromptText = () => formatLoadingPrompt(loadingPromptFrame);
644
642
 
@@ -646,7 +644,7 @@ const startChatSession = async ({
646
644
  if (!loadingPromptVisible || !supportsHexColor) {
647
645
  return;
648
646
  }
649
- loadingPromptFrame = (loadingPromptFrame + 1) % loadingPromptCharCount;
647
+ loadingPromptFrame += 1;
650
648
  refreshLoadingPrompt();
651
649
  };
652
650
 
@@ -692,6 +690,15 @@ const startChatSession = async ({
692
690
  writeLoadingPrompt();
693
691
  };
694
692
 
693
+ const updateLoadingPromptTokens = (usage) => {
694
+ const nextText = formatUsageTokens(usage) || '';
695
+ if (nextText === loadingPromptTokensText) {
696
+ return;
697
+ }
698
+ loadingPromptTokensText = nextText;
699
+ refreshLoadingPrompt();
700
+ };
701
+
695
702
  const clearLoadingPrompt = () => {
696
703
  if (!loadingPromptVisible) {
697
704
  return;
@@ -870,7 +877,6 @@ const startChatSession = async ({
870
877
  };
871
878
 
872
879
  while (true) {
873
- process.stdout.write('\n');
874
880
  showLoadingPrompt();
875
881
  const streamState = {
876
882
  thinkState: createThinkParserState(),
@@ -878,13 +884,12 @@ const startChatSession = async ({
878
884
  reasoningFromThink: ''
879
885
  };
880
886
  let reasoningFromModel = '';
881
- let printedNewline = false;
887
+ let outputStarted = false;
882
888
 
883
889
  const ensureNewline = () => {
884
- if (!printedNewline) {
890
+ if (!outputStarted) {
885
891
  clearLoadingPrompt();
886
- process.stdout.write('\n');
887
- printedNewline = true;
892
+ outputStarted = true;
888
893
  }
889
894
  };
890
895
 
@@ -921,6 +926,7 @@ const startChatSession = async ({
921
926
  tools: toolSchemas,
922
927
  onToken: handleStreamChunk,
923
928
  onReasoningToken: handleReasoningChunk,
929
+ onUsage: updateLoadingPromptTokens,
924
930
  onRetry: (retryCount, error) => {
925
931
  const detail = error ? `(${error.message})` : '';
926
932
  process.stdout.write(`\n${chalk.yellow(`请求失败,${retryCount} 次重试中${detail}...`)}\n`);
@@ -928,6 +934,7 @@ const startChatSession = async ({
928
934
  });
929
935
 
930
936
  finalizeStreamState();
937
+ updateLoadingPromptTokens(result.usage);
931
938
 
932
939
  const rawReasoning = (reasoningFromModel || result.reasoningContent || '').trim();
933
940
  const mergedReasoning = mergeReasoningContent(
@@ -962,6 +969,7 @@ const startChatSession = async ({
962
969
  ? parsedArgs
963
970
  : { raw: typeof rawArgs === 'string' ? rawArgs : '' };
964
971
 
972
+ ensureNewline();
965
973
  renderToolCall({ functionName, args: displayArgs }, mcpToolNames);
966
974
 
967
975
  let toolResult;
@@ -981,8 +989,8 @@ const startChatSession = async ({
981
989
  }
982
990
  }
983
991
 
984
- const resultInfo = enforceToolOutputLimit(stringifyToolResult(toolResult), toolOutputTokenLimit);
985
- const toolContent = resultInfo.content;
992
+ const rawToolContent = stringifyToolResult(toolResult);
993
+ const toolContent = enforceToolOutputLimit(rawToolContent, toolOutputTokenLimit);
986
994
 
987
995
  // Display tool output
988
996
  if (functionName === 'write') {
@@ -997,31 +1005,21 @@ const startChatSession = async ({
997
1005
  if (editOutput) {
998
1006
  console.log(editOutput);
999
1007
  }
1000
- } else if (functionName === 'skills') {
1001
- const preview = truncateForDisplay(toolContent, 200);
1002
- if (preview) {
1003
- console.log(chalk.gray(` ${preview}`));
1004
- }
1005
1008
  } else {
1006
- // For other tools, show preview in gray
1007
- const preview = truncateForDisplay(toolContent, 100);
1008
- if (preview) {
1009
- console.log(chalk.gray(` ${preview}`));
1010
- }
1011
-
1012
- // If truncated, show warning
1013
- if (resultInfo.truncated) {
1014
- console.log(chalk.yellow(` ⚠ 输出已截断 (${resultInfo.originalTokenCount} tokens > ${resultInfo.limit} limit)`));
1009
+ const displayResult = formatToolResultForDisplay(rawToolContent, 100);
1010
+ if (displayResult) {
1011
+ console.log(chalk.gray(indentToolResult(displayResult)));
1015
1012
  }
1016
1013
  }
1017
1014
 
1015
+ process.stdout.write('\n');
1016
+
1018
1017
  messages.push({
1019
1018
  role: 'tool',
1020
1019
  tool_call_id: toolCall.id,
1021
1020
  content: toolContent
1022
1021
  });
1023
1022
  }
1024
- printUsageTokens(result.usage);
1025
1023
  persistHistorySafely();
1026
1024
 
1027
1025
  // 检测是否需要触发 compact
@@ -1073,7 +1071,6 @@ const startChatSession = async ({
1073
1071
  );
1074
1072
  messages.push(assistantMessage);
1075
1073
  persistHistorySafely();
1076
- printUsageTokens(result.usage);
1077
1074
  break;
1078
1075
  }
1079
1076
  };
package/src/llm.js CHANGED
@@ -68,7 +68,7 @@ const sanitizeOptions = (options = {}) => {
68
68
  }, {});
69
69
  };
70
70
 
71
- const chatCompletion = async ({ endpoint, messages, abortController, onToken, onReasoningToken, onRetry, onComplete, tools = [] }) => {
71
+ const chatCompletion = async ({ endpoint, messages, abortController, onToken, onReasoningToken, onUsage, onRetry, onComplete, tools = [] }) => {
72
72
  const controller = abortController || new AbortController();
73
73
  const includeToolCalls = Array.isArray(tools) && tools.length > 0;
74
74
 
@@ -112,6 +112,11 @@ const chatCompletion = async ({ endpoint, messages, abortController, onToken, on
112
112
  onReasoningToken(chunk, aggregate);
113
113
  }
114
114
  },
115
+ onUsage: (usage) => {
116
+ if (onUsage) {
117
+ onUsage(usage);
118
+ }
119
+ },
115
120
  onComplete: () => {
116
121
  if (onComplete) {
117
122
  onComplete(fullContent);
package/src/request.js CHANGED
@@ -88,6 +88,7 @@ const processStreamResponse = (response, options = {}) => {
88
88
  const {
89
89
  onContent = null,
90
90
  onReasoningContent = null,
91
+ onUsage = null,
91
92
  onComplete = null,
92
93
  abortController = null,
93
94
  includeToolCalls = false
@@ -138,6 +139,9 @@ const processStreamResponse = (response, options = {}) => {
138
139
  const payload = JSON.parse(event.data);
139
140
  if (payload.usage && typeof payload.usage === 'object') {
140
141
  usage = payload.usage;
142
+ if (onUsage) {
143
+ onUsage(usage);
144
+ }
141
145
  }
142
146
  const choice = payload.choices && payload.choices[0];
143
147
  if (!choice || !choice.delta) return;
@@ -1,7 +1,7 @@
1
1
  const chalk = require('chalk');
2
2
  const { Marked } = require('marked');
3
3
  const { markedTerminal } = require('marked-terminal');
4
- const { splitThinkContent, summarizeReasoning } = require('./think');
4
+ const { splitThinkContent } = require('./think');
5
5
 
6
6
  const ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
7
7
 
@@ -70,10 +70,14 @@ const mergeReasoningContent = (...segments) => {
70
70
  return normalized.join('\n');
71
71
  };
72
72
 
73
- const printReasoningSummary = (text, ensureNewline) => {
74
- if (!text) return;
73
+ const printReasoningContent = (text, ensureNewline) => {
74
+ const normalized = typeof text === 'string'
75
+ ? text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim()
76
+ : '';
77
+
78
+ if (!normalized) return;
75
79
  ensureNewline();
76
- console.log(chalk.gray(`思考:${summarizeReasoning(text)}`));
80
+ console.log(chalk.gray(normalized));
77
81
  };
78
82
 
79
83
  const printAssistantContent = (text, ensureNewline) => {
@@ -89,7 +93,7 @@ const printAssistantContent = (text, ensureNewline) => {
89
93
  };
90
94
 
91
95
  const renderAssistantOutput = (reasoning, content, ensureNewline) => {
92
- printReasoningSummary(reasoning, ensureNewline);
96
+ printReasoningContent(reasoning, ensureNewline);
93
97
  printAssistantContent(content, ensureNewline);
94
98
  };
95
99
 
@@ -112,23 +116,23 @@ const formatTokensToK = (totalTokens) => {
112
116
  return `${(totalTokens / 1000).toFixed(1)}k`;
113
117
  };
114
118
 
115
- const printUsageTokens = (usage) => {
119
+ const formatUsageTokens = (usage) => {
116
120
  if (!usage || typeof usage !== 'object') {
117
- return;
121
+ return null;
118
122
  }
119
123
  const totalTokens = usage.total_tokens;
120
124
  const formatted = formatTokensToK(totalTokens);
121
125
  if (!formatted) {
122
- return;
126
+ return null;
123
127
  }
124
- console.log(chalk.gray(`已使用tokens: ${formatted}`));
128
+ return `会话tokens: ${formatted}`;
125
129
  };
126
130
 
127
131
  module.exports = {
128
132
  mergeReasoningContent,
129
- printReasoningSummary,
133
+ printReasoningContent,
130
134
  printAssistantContent,
131
135
  renderAssistantOutput,
132
136
  sanitizeContentWithThink,
133
- printUsageTokens
137
+ formatUsageTokens
134
138
  };
@@ -194,21 +194,10 @@ const splitThinkContent = (text = '') => {
194
194
  return { content, reasoning };
195
195
  };
196
196
 
197
- const summarizeReasoning = (text = '', maxLength = 100) => {
198
- if (!text) return '';
199
- const normalized = text.replace(/\s+/g, ' ').trim();
200
- if (!normalized) return '';
201
- if (normalized.length > maxLength) {
202
- return normalized.substring(0, maxLength) + '...';
203
- }
204
- return normalized;
205
- };
206
-
207
197
  module.exports = {
208
198
  THINK_TAGS,
209
199
  createThinkParserState,
210
200
  processThinkChunk,
211
201
  flushThinkState,
212
- splitThinkContent,
213
- summarizeReasoning
202
+ splitThinkContent
214
203
  };