@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.
@@ -9,7 +9,7 @@ const getCurrentDate = () => {
9
9
  const toolSystemPrompt = `<current_date>${getCurrentDate()}</current_date>
10
10
 
11
11
  <basic_rules>
12
- * 需要调用\`write_todos\`工具,来创建和更新待办事项的进度。
12
+ * 需要调用\`todos\`工具,来创建和更新待办事项的进度。
13
13
  * 除代码内容外,应使用简体中文作为默认语言。
14
14
  </basic_rules>`;
15
15
 
package/src/tools/bash.js CHANGED
@@ -208,7 +208,7 @@ const createBashToolSchema = (context = {}) => {
208
208
  return {
209
209
  type: 'function',
210
210
  function: {
211
- name: 'execute_bash',
211
+ name: 'bash',
212
212
  description: descriptionParts.join(' '),
213
213
  parameters: {
214
214
  type: 'object',
@@ -235,7 +235,7 @@ const createBashToolSchema = (context = {}) => {
235
235
  };
236
236
 
237
237
  module.exports = {
238
- name: 'execute_bash',
238
+ name: 'bash',
239
239
  schema: createBashToolSchema,
240
240
  handler: executeCommand
241
241
  };
package/src/tools/read.js CHANGED
@@ -22,7 +22,7 @@ const readFile = async ({ filePath } = {}, context = {}) => {
22
22
  const schema = {
23
23
  type: 'function',
24
24
  function: {
25
- name: 'read_file',
25
+ name: 'read',
26
26
  description: '读取指定相对路径的文本类文件完整内容,不支持二进制文件',
27
27
  parameters: {
28
28
  type: 'object',
@@ -38,7 +38,7 @@ const schema = {
38
38
  };
39
39
 
40
40
  module.exports = {
41
- name: 'read_file',
41
+ name: 'read',
42
42
  schema,
43
43
  handler: readFile
44
44
  };
@@ -105,7 +105,7 @@ const replaceInFile = async ({ filePath, search, replace } = {}, context = {}) =
105
105
  const schema = {
106
106
  type: 'function',
107
107
  function: {
108
- name: 'search_and_replace',
108
+ name: 'replace',
109
109
  description: '在文件中搜索并替换指定的代码片段(简单字符串匹配,非正则)',
110
110
  parameters: {
111
111
  type: 'object',
@@ -129,7 +129,7 @@ const schema = {
129
129
  };
130
130
 
131
131
  module.exports = {
132
- name: 'search_and_replace',
132
+ name: 'replace',
133
133
  schema,
134
134
  handler: replaceInFile
135
135
  };
@@ -53,7 +53,7 @@ const writeTodos = async ({ todos } = {}) => {
53
53
  const schema = {
54
54
  type: 'function',
55
55
  function: {
56
- name: 'write_todos',
56
+ name: 'todos',
57
57
  description: '更新任务列表(创建、修改、完成)',
58
58
  parameters: {
59
59
  type: 'object',
@@ -83,7 +83,7 @@ const schema = {
83
83
  const getLastTodos = () => lastTodos;
84
84
 
85
85
  module.exports = {
86
- name: 'write_todos',
86
+ name: 'todos',
87
87
  schema,
88
88
  handler: writeTodos,
89
89
  getLastTodos
@@ -26,7 +26,7 @@ const writeFile = async ({ filePath, content } = {}, context = {}) => {
26
26
  const schema = {
27
27
  type: 'function',
28
28
  function: {
29
- name: 'write_file',
29
+ name: 'write',
30
30
  description: '向文件写入完整内容(不存在则创建)',
31
31
  parameters: {
32
32
  type: 'object',
@@ -46,7 +46,7 @@ const schema = {
46
46
  };
47
47
 
48
48
  module.exports = {
49
- name: 'write_file',
49
+ name: 'write',
50
50
  schema,
51
51
  handler: writeFile
52
52
  };
@@ -1,89 +1,119 @@
1
- const chalk = require('chalk');
2
-
3
- const SLASH_COMMANDS = [
4
- { key: 'model', value: '/model', description: '查看或切换模型' },
5
- { key: 'clear', value: '/clear', description: '清空上下文' },
6
- { key: 'resume', value: '/resume', description: '查看或恢复历史会话' },
7
- { key: 'exit', value: '/exit', description: '退出对话' }
8
- ];
9
-
10
- const getSlashCommandListText = () => SLASH_COMMANDS.map((entry) => entry.value).join('、');
11
-
12
- const createCommandHandler = ({ modelManager, resetMessages, closeSession, handleResume }) => {
13
- const handleClearCommand = () => {
14
- resetMessages();
15
- console.log(chalk.green('已清空上下文。'));
16
- };
17
-
18
- const handleModelCommand = (rawArgs = '') => {
19
- const args = rawArgs.trim();
20
- if (!args || ['list', 'ls'].includes(args.toLowerCase())) {
21
- modelManager.listModels();
22
- return;
23
- }
24
-
25
- const numeric = Number.parseInt(args, 10);
26
- if (!Number.isNaN(numeric) && String(numeric) === args) {
27
- const index = numeric - 1;
28
- if (!modelManager.setActiveModel(index, { updateDefault: true, announce: true })) {
29
- console.error(chalk.red(`序号 ${args} 不在 1-${modelManager.getTotalModels()} 范围内,已保持 ${modelManager.getDefaultModelDescription()}。`));
30
- modelManager.resetToDefault();
31
- }
32
- return;
33
- }
34
-
35
- modelManager.applyModelByName(args, { source: 'command', announceSuccess: true, showError: true });
36
- };
37
-
38
- const handleResumeCommand = (rawArgs = '') => {
39
- if (typeof handleResume === 'function') {
40
- handleResume(rawArgs);
41
- } else {
42
- console.log(chalk.yellow('当前构建未启用历史记录功能。'));
43
- }
44
- };
45
-
46
- const handleSlashCommand = (raw = '') => {
47
- if (!raw.startsWith('/')) {
48
- return { handled: false, shouldExit: false };
49
- }
50
- const trimmed = raw.slice(1).trim();
51
- const availableCommandsText = getSlashCommandListText() || '/model、/clear、/exit';
52
- if (!trimmed) {
53
- console.log(chalk.yellow(`请输入完整命令,例如 ${availableCommandsText}。`));
54
- return { handled: true, shouldExit: false };
55
- }
56
- const [command] = trimmed.split(' ');
57
- const args = trimmed.slice(command.length).trim();
58
- switch ((command || '').toLowerCase()) {
59
- case 'model':
60
- handleModelCommand(args);
61
- return { handled: true, shouldExit: false };
62
- case 'clear':
63
- handleClearCommand();
64
- return { handled: true, shouldExit: false };
65
- case 'resume':
66
- handleResumeCommand(args);
67
- return { handled: true, shouldExit: false };
68
- case 'exit':
69
- closeSession();
70
- return { handled: true, shouldExit: true };
71
- default:
72
- console.log(chalk.yellow(`未识别的命令 /${command},可用命令:${availableCommandsText}。`));
73
- return { handled: true, shouldExit: false };
74
- }
75
- };
76
-
77
- return {
78
- handleSlashCommand,
79
- handleClearCommand,
80
- handleModelCommand,
81
- handleResumeCommand
82
- };
83
- };
84
-
85
- module.exports = {
86
- SLASH_COMMANDS,
87
- getSlashCommandListText,
88
- createCommandHandler
89
- };
1
+ const chalk = require('chalk');
2
+ const { selectFromList } = require('./menu');
3
+
4
+ const SLASH_COMMANDS = [
5
+ { key: 'model', value: '/model', description: '查看或切换模型' },
6
+ { key: 'clear', value: '/clear', description: '清空上下文' },
7
+ { key: 'resume', value: '/resume', description: '查看或恢复历史会话' },
8
+ { key: 'exit', value: '/exit', description: '退出对话' }
9
+ ];
10
+
11
+ const getSlashCommandListText = () => SLASH_COMMANDS.map((entry) => entry.value).join('、');
12
+
13
+ const createCommandHandler = ({ modelManager, resetMessages, closeSession, handleResume, pauseSession, resumeSession }) => {
14
+ const handleClearCommand = () => {
15
+ resetMessages();
16
+ console.log(chalk.green('已清空上下文。'));
17
+ };
18
+
19
+ const handleModelCommand = async (rawArgs = '') => {
20
+ const args = rawArgs.trim();
21
+ if (!args || ['list', 'ls'].includes(args.toLowerCase())) {
22
+ const endpoints = modelManager.getModels();
23
+ const activeIndex = modelManager.getActiveIndex();
24
+ const defaultIndex = modelManager.getDefaultIndex();
25
+
26
+ if (pauseSession) pauseSession();
27
+ const selectedIndex = await selectFromList(endpoints, {
28
+ title: chalk.gray('请选择要切换的模型:'),
29
+ renderItem: (endpoint, idx, isSelected) => {
30
+ const markers = [];
31
+ if (idx === activeIndex) {
32
+ markers.push(chalk.green('当前'));
33
+ }
34
+ if (idx === defaultIndex) {
35
+ markers.push(chalk.cyan('默认'));
36
+ }
37
+ const markerText = markers.length > 0 ? ` [${markers.join('/')}]` : '';
38
+ const description = modelManager.describeEndpoint(endpoint);
39
+ const text = `${idx + 1}. ${description}${markerText}`;
40
+ return isSelected ? chalk.cyan(`> ${text}`) : ` ${text}`;
41
+ }
42
+ });
43
+ if (resumeSession) resumeSession();
44
+
45
+ if (selectedIndex !== null) {
46
+ if (selectedIndex !== activeIndex) {
47
+ modelManager.setActiveModel(selectedIndex, { updateDefault: true, announce: true });
48
+ } else {
49
+ console.log(chalk.gray('已保持当前模型。'));
50
+ }
51
+ }
52
+ return;
53
+ }
54
+
55
+ const numeric = Number.parseInt(args, 10);
56
+ if (!Number.isNaN(numeric) && String(numeric) === args) {
57
+ const index = numeric - 1;
58
+ if (!modelManager.setActiveModel(index, { updateDefault: true, announce: true })) {
59
+ console.error(chalk.red(`序号 ${args} 不在 1-${modelManager.getTotalModels()} 范围内,已保持 ${modelManager.getDefaultModelDescription()}。`));
60
+ modelManager.resetToDefault();
61
+ }
62
+ return;
63
+ }
64
+
65
+ modelManager.applyModelByName(args, { source: 'command', announceSuccess: true, showError: true });
66
+ };
67
+
68
+ const handleResumeCommand = async (rawArgs = '') => {
69
+ if (typeof handleResume === 'function') {
70
+ await handleResume(rawArgs);
71
+ } else {
72
+ console.log(chalk.yellow('当前构建未启用历史记录功能。'));
73
+ }
74
+ };
75
+
76
+ const handleSlashCommand = async (raw = '') => {
77
+ if (!raw.startsWith('/')) {
78
+ return { handled: false, shouldExit: false };
79
+ }
80
+ const trimmed = raw.slice(1).trim();
81
+ const availableCommandsText = getSlashCommandListText() || '/model、/clear、/exit';
82
+ if (!trimmed) {
83
+ console.log(chalk.yellow(`请输入完整命令,例如 ${availableCommandsText}。`));
84
+ return { handled: true, shouldExit: false };
85
+ }
86
+ const [command] = trimmed.split(' ');
87
+ const args = trimmed.slice(command.length).trim();
88
+ switch ((command || '').toLowerCase()) {
89
+ case 'model':
90
+ await handleModelCommand(args);
91
+ return { handled: true, shouldExit: false };
92
+ case 'clear':
93
+ handleClearCommand();
94
+ return { handled: true, shouldExit: false };
95
+ case 'resume':
96
+ await handleResumeCommand(args);
97
+ return { handled: true, shouldExit: false };
98
+ case 'exit':
99
+ closeSession();
100
+ return { handled: true, shouldExit: true };
101
+ default:
102
+ console.log(chalk.yellow(`未识别的命令 /${command},可用命令:${availableCommandsText}。`));
103
+ return { handled: true, shouldExit: false };
104
+ }
105
+ };
106
+
107
+ return {
108
+ handleSlashCommand,
109
+ handleClearCommand,
110
+ handleModelCommand,
111
+ handleResumeCommand
112
+ };
113
+ };
114
+
115
+ module.exports = {
116
+ SLASH_COMMANDS,
117
+ getSlashCommandListText,
118
+ createCommandHandler
119
+ };
@@ -0,0 +1,156 @@
1
+ const readline = require('readline');
2
+ const chalk = require('chalk');
3
+
4
+ // 简单的 ANSI 码去除正则
5
+ const stripAnsi = (str) => str.replace(
6
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
7
+ ''
8
+ );
9
+
10
+ // 计算字符串在终端显示的实际行数(考虑中文宽度和终端宽度)
11
+ const measureLines = (str) => {
12
+ const termWidth = process.stdout.columns || 80;
13
+ const clean = stripAnsi(str);
14
+ let width = 0;
15
+ for (let i = 0; i < clean.length; i++) {
16
+ const code = clean.charCodeAt(i);
17
+ // 简单的宽字符判断:ASCII 范围外视为 2 个宽度
18
+ width += (code > 255) ? 2 : 1;
19
+ }
20
+ return Math.ceil(width / termWidth) || 1;
21
+ };
22
+
23
+ /**
24
+ * 交互式列表选择
25
+ * @param {Array} items - 待选择的项数组
26
+ * @param {Object} options - 配置项
27
+ * @param {Function} options.renderItem - 渲染每项的回调 (item, index, isSelected) => string
28
+ * @param {number} options.pageSize - 每页显示的条数
29
+ * @param {string} options.title - 标题
30
+ * @returns {Promise<number|null>} - 返回选中的索引,取消则返回 null
31
+ */
32
+ const selectFromList = (items, options = {}) => {
33
+ return new Promise((resolve) => {
34
+ const {
35
+ renderItem = (item, idx, isSelected) => (isSelected ? chalk.cyan(`> ${item}`) : ` ${item}`),
36
+ pageSize = 10,
37
+ title = '',
38
+ exitOnInput = false
39
+ } = options;
40
+
41
+ if (!items || items.length === 0) {
42
+ resolve(null);
43
+ return;
44
+ }
45
+
46
+ let selectedIndex = 0;
47
+ let scrollOffset = 0;
48
+ const total = items.length;
49
+ let linesRendered = 0;
50
+
51
+ // 隐藏光标
52
+ process.stdout.write('\x1B[?25l');
53
+
54
+ if (title) {
55
+ console.log(title);
56
+ }
57
+
58
+ const render = () => {
59
+ // 清除之前渲染的行
60
+ if (linesRendered > 0) {
61
+ readline.moveCursor(process.stdout, 0, -(linesRendered - 1));
62
+ readline.cursorTo(process.stdout, 0);
63
+ readline.clearScreenDown(process.stdout);
64
+ }
65
+
66
+ const windowSize = Math.min(pageSize, total);
67
+
68
+ // 调整滚动偏移
69
+ if (selectedIndex < scrollOffset) {
70
+ scrollOffset = selectedIndex;
71
+ } else if (selectedIndex >= scrollOffset + windowSize) {
72
+ scrollOffset = selectedIndex - windowSize + 1;
73
+ }
74
+
75
+ const visibleItems = items.slice(scrollOffset, scrollOffset + windowSize);
76
+
77
+ linesRendered = 0;
78
+ visibleItems.forEach((item, index) => {
79
+ const actualIndex = scrollOffset + index;
80
+ const isSelected = actualIndex === selectedIndex;
81
+ const line = renderItem(item, actualIndex, isSelected);
82
+
83
+ // 计算该行实际占用的物理行数
84
+ const lineCount = measureLines(line);
85
+ process.stdout.write(line + '\n');
86
+ linesRendered += lineCount;
87
+ });
88
+
89
+ // 底部提示
90
+ const footerText = chalk.gray(' (↑/↓ 选择, Enter 确认, Esc 取消)');
91
+ const footerLines = measureLines(footerText);
92
+ process.stdout.write(footerText);
93
+ linesRendered += footerLines;
94
+ };
95
+
96
+ render();
97
+
98
+ const cleanup = () => {
99
+ process.stdout.write('\x1B[?25h'); // 恢复光标
100
+
101
+ // 清除菜单显示
102
+ if (linesRendered > 0) {
103
+ readline.moveCursor(process.stdout, 0, -(linesRendered - 1));
104
+ readline.cursorTo(process.stdout, 0);
105
+ readline.clearScreenDown(process.stdout);
106
+ }
107
+
108
+ // 清除 title 行(如果有)
109
+ if (title) {
110
+ // 计算 title 占用的行数
111
+ const titleLines = measureLines(title);
112
+ // 上移 titleLines 行
113
+ readline.moveCursor(process.stdout, 0, -titleLines);
114
+ readline.cursorTo(process.stdout, 0);
115
+ readline.clearScreenDown(process.stdout);
116
+ }
117
+
118
+ process.stdin.removeListener('keypress', handleKeypress);
119
+ if (process.stdin.isTTY) {
120
+ process.stdin.setRawMode(false);
121
+ // 不要 pause stdin,因为外层 readline 需要它
122
+ // process.stdin.pause();
123
+ }
124
+ };
125
+
126
+ const handleKeypress = (str, key) => {
127
+ if (!key) return;
128
+
129
+ if (key.name === 'up') {
130
+ selectedIndex = (selectedIndex - 1 + total) % total;
131
+ render();
132
+ } else if (key.name === 'down') {
133
+ selectedIndex = (selectedIndex + 1) % total;
134
+ render();
135
+ } else if (key.name === 'return') {
136
+ cleanup();
137
+ resolve(exitOnInput ? { action: 'select', index: selectedIndex } : selectedIndex);
138
+ } else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
139
+ cleanup();
140
+ resolve(exitOnInput ? { action: 'cancel' } : null);
141
+ } else if (exitOnInput) {
142
+ cleanup();
143
+ const inputVal = key.sequence || str || key.name;
144
+ resolve({ action: 'input', input: inputVal });
145
+ }
146
+ };
147
+
148
+ if (process.stdin.isTTY) {
149
+ process.stdin.setRawMode(true);
150
+ process.stdin.resume();
151
+ }
152
+ process.stdin.on('keypress', handleKeypress);
153
+ });
154
+ };
155
+
156
+ module.exports = { selectFromList };
@@ -117,7 +117,11 @@ const createModelManager = (endpoints, { configPath } = {}) => {
117
117
  resetToDefault,
118
118
  setActiveModel,
119
119
  applyModelByName,
120
- listModels
120
+ listModels,
121
+ getModels: () => endpoints,
122
+ getActiveIndex: () => activeEndpointIndex,
123
+ getDefaultIndex: () => defaultEndpointIndex,
124
+ describeEndpoint: (endpoint) => describeModel(endpoint)
121
125
  };
122
126
  };
123
127
 
@@ -33,19 +33,19 @@ const formatHeader = (name, args, options = {}) => {
33
33
  const statusTag = options.statusTag || '';
34
34
  const extraMeta = Array.isArray(options.extraMeta) ? options.extraMeta : [];
35
35
  const labelBase = `${chalk.yellow('🔧')} ${chalk.yellow(name)}`;
36
- const writeTag = name === 'write_file' ? formatWriteLineCountTag(args) : '';
36
+ const writeTag = name === 'write' ? formatWriteLineCountTag(args) : '';
37
37
  const labelWithWriteInfo = `${labelBase}${writeTag}`;
38
38
  const label = statusTag ? `${labelWithWriteInfo}${statusTag}` : labelWithWriteInfo;
39
39
  const metaParts = [...extraMeta];
40
40
 
41
- if (name === 'execute_bash') {
41
+ if (name === 'bash') {
42
42
  if (args.command) metaParts.push(`command="${truncateText(args.command)}"`);
43
43
  if (args.workingDirectory && args.workingDirectory !== '.') {
44
44
  metaParts.push(`dir=${args.workingDirectory}`);
45
45
  }
46
- } else if (['read_file', 'write_file', 'search_and_replace'].includes(name)) {
46
+ } else if (['read', 'write', 'replace'].includes(name)) {
47
47
  if (args.filePath) metaParts.push(args.filePath);
48
- } else if (name === 'write_todos') {
48
+ } else if (name === 'todos') {
49
49
  const count = Array.isArray(args.todos) ? args.todos.length : 0;
50
50
  metaParts.push(`${count} items`);
51
51
  } else if (name === 'convert') {