@ww_nero/mini-cli 1.0.56
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/bin/mini.js +3 -0
- package/package.json +38 -0
- package/src/chat.js +1008 -0
- package/src/config.js +371 -0
- package/src/index.js +38 -0
- package/src/llm.js +147 -0
- package/src/prompt/tool.js +18 -0
- package/src/request.js +328 -0
- package/src/tools/bash.js +241 -0
- package/src/tools/convert.js +297 -0
- package/src/tools/index.js +66 -0
- package/src/tools/mcp.js +478 -0
- package/src/tools/python/html_to_png.py +100 -0
- package/src/tools/python/html_to_pptx.py +163 -0
- package/src/tools/python/pdf_to_png.py +58 -0
- package/src/tools/python/pptx_to_pdf.py +107 -0
- package/src/tools/read.js +44 -0
- package/src/tools/replace.js +135 -0
- package/src/tools/todos.js +90 -0
- package/src/tools/write.js +52 -0
- package/src/utils/cliOptions.js +8 -0
- package/src/utils/commands.js +89 -0
- package/src/utils/git.js +89 -0
- package/src/utils/helpers.js +93 -0
- package/src/utils/history.js +181 -0
- package/src/utils/model.js +127 -0
- package/src/utils/output.js +76 -0
- package/src/utils/renderer.js +92 -0
- package/src/utils/settings.js +90 -0
- package/src/utils/think.js +211 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsPromises = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { resolveWorkspacePath } = require('../utils/helpers');
|
|
6
|
+
|
|
7
|
+
const EXECUTION_TIMEOUT = 300000;
|
|
8
|
+
const PYTHON_SCRIPT_ROOT = path.join(__dirname, 'python');
|
|
9
|
+
|
|
10
|
+
const TYPE_META = {
|
|
11
|
+
pdf_to_png: {
|
|
12
|
+
inputType: 'file',
|
|
13
|
+
outputType: 'directory',
|
|
14
|
+
description: '将 PDF 每一页转成 PNG 图片'
|
|
15
|
+
},
|
|
16
|
+
html_to_pptx: {
|
|
17
|
+
inputType: 'list',
|
|
18
|
+
outputType: 'file',
|
|
19
|
+
description: '多个 HTML 截屏后合并为单 PPTX,保持原顺序'
|
|
20
|
+
},
|
|
21
|
+
html_to_png: {
|
|
22
|
+
inputType: 'file',
|
|
23
|
+
outputType: 'file',
|
|
24
|
+
description: '将单个 HTML 文件转换为 PNG 图片'
|
|
25
|
+
},
|
|
26
|
+
pptx_to_pdf: {
|
|
27
|
+
inputType: 'file',
|
|
28
|
+
outputType: 'file',
|
|
29
|
+
description: '通过 LibreOffice 直接将 PPT/PPTX 导出为 PDF'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PYTHON_SCRIPTS = {
|
|
34
|
+
pdf_to_png: 'pdf_to_png.py',
|
|
35
|
+
html_to_pptx: 'html_to_pptx.py',
|
|
36
|
+
html_to_png: 'html_to_png.py',
|
|
37
|
+
pptx_to_pdf: 'pptx_to_pdf.py'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const TYPE_ENUM = Object.keys(TYPE_META);
|
|
41
|
+
|
|
42
|
+
const shellQuote = (value = '') => {
|
|
43
|
+
const str = String(value);
|
|
44
|
+
const escaped = str.replace(/(["$`\\])/g, '\\$1');
|
|
45
|
+
return `"${escaped}"`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const normalizeListInput = (input) => {
|
|
49
|
+
if (Array.isArray(input)) {
|
|
50
|
+
return input;
|
|
51
|
+
}
|
|
52
|
+
if (typeof input === 'string' && input.trim()) {
|
|
53
|
+
const trimmed = input.trim();
|
|
54
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(trimmed);
|
|
57
|
+
if (Array.isArray(parsed)) {
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// fall through to treating as single path
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [trimmed];
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const ensureOutputLocation = async (targetPath, kind) => {
|
|
70
|
+
if (kind === 'directory') {
|
|
71
|
+
await fsPromises.mkdir(targetPath, { recursive: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await fsPromises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const runBashCommand = (command, cwd) => new Promise((resolve, reject) => {
|
|
78
|
+
const child = spawn('bash', ['-lc', command], {
|
|
79
|
+
cwd,
|
|
80
|
+
env: process.env,
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let stdout = '';
|
|
85
|
+
let stderr = '';
|
|
86
|
+
let finished = false;
|
|
87
|
+
|
|
88
|
+
const finalize = (error) => {
|
|
89
|
+
if (finished) return;
|
|
90
|
+
finished = true;
|
|
91
|
+
clearTimeout(timeoutId);
|
|
92
|
+
if (error) {
|
|
93
|
+
reject(error);
|
|
94
|
+
} else {
|
|
95
|
+
resolve(stdout.trim());
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
child.stdout.on('data', (chunk) => {
|
|
100
|
+
stdout += chunk.toString();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.stderr.on('data', (chunk) => {
|
|
104
|
+
stderr += chunk.toString();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.on('error', (error) => {
|
|
108
|
+
finalize(error);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.on('close', (code) => {
|
|
112
|
+
if (code === 0) {
|
|
113
|
+
finalize();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const message = stderr.trim() || stdout.trim() || `命令执行失败,退出码 ${code}`;
|
|
117
|
+
finalize(new Error(message));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const timeoutId = setTimeout(() => {
|
|
121
|
+
child.kill('SIGTERM');
|
|
122
|
+
finalize(new Error('命令执行超时 (超过 300s)'));
|
|
123
|
+
}, EXECUTION_TIMEOUT);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const runPythonScript = async (scriptPath, scriptArgs = [], workspaceRoot) => {
|
|
127
|
+
const quotedScript = shellQuote(scriptPath);
|
|
128
|
+
const argsString = scriptArgs.join(' ');
|
|
129
|
+
const appendArgs = argsString ? ` ${argsString}` : '';
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
return await runBashCommand(`python3 ${quotedScript}${appendArgs}`, workspaceRoot);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const message = error?.message || '';
|
|
135
|
+
if (/python3: command not found/.test(message) || /python3: not found/.test(message)) {
|
|
136
|
+
return runBashCommand(`python ${quotedScript}${appendArgs}`, workspaceRoot);
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const resolveInputPaths = (inputType, input, workspaceRoot, typeName) => {
|
|
143
|
+
if (inputType === 'list') {
|
|
144
|
+
const list = normalizeListInput(input);
|
|
145
|
+
if (list.length === 0) {
|
|
146
|
+
throw new Error(`${typeName} 需要至少一个输入路径`);
|
|
147
|
+
}
|
|
148
|
+
const resolvedList = list.map((item, index) => {
|
|
149
|
+
if (typeof item !== 'string' || !item.trim()) {
|
|
150
|
+
throw new Error(`第 ${index + 1} 个输入路径无效`);
|
|
151
|
+
}
|
|
152
|
+
const resolved = resolveWorkspacePath(workspaceRoot, item.trim());
|
|
153
|
+
if (!fs.existsSync(resolved)) {
|
|
154
|
+
throw new Error(`输入文件不存在: ${resolved}`);
|
|
155
|
+
}
|
|
156
|
+
return resolved;
|
|
157
|
+
});
|
|
158
|
+
return { list: resolvedList };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof input !== 'string' || !input.trim()) {
|
|
162
|
+
throw new Error(`${typeName} 需要传入单个输入文件路径`);
|
|
163
|
+
}
|
|
164
|
+
const resolved = resolveWorkspacePath(workspaceRoot, input.trim());
|
|
165
|
+
if (!fs.existsSync(resolved)) {
|
|
166
|
+
throw new Error(`输入文件不存在: ${resolved}`);
|
|
167
|
+
}
|
|
168
|
+
return { file: resolved };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const formatInputPreview = (input) => {
|
|
172
|
+
if (Array.isArray(input)) {
|
|
173
|
+
return input.join(', ');
|
|
174
|
+
}
|
|
175
|
+
return typeof input === 'string' ? input : '';
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const convertHandler = async ({ type, input, output } = {}, context = {}) => {
|
|
179
|
+
const { workspaceRoot } = context;
|
|
180
|
+
if (!workspaceRoot) {
|
|
181
|
+
return '转换失败: 未找到工作区目录';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!type || typeof type !== 'string') {
|
|
185
|
+
return '转换失败: type 参数无效';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const config = TYPE_META[type];
|
|
189
|
+
if (!config) {
|
|
190
|
+
return `转换失败: 不支持的 type ${type}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof output !== 'string' || !output.trim()) {
|
|
194
|
+
return '转换失败: output 必须是非空字符串路径';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let outputPath;
|
|
198
|
+
try {
|
|
199
|
+
outputPath = resolveWorkspacePath(workspaceRoot, output.trim());
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return `转换失败: ${error.message}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const pptxTypes = new Set(['html_to_pptx']);
|
|
205
|
+
if (pptxTypes.has(type) && path.extname(outputPath).toLowerCase() !== '.pptx') {
|
|
206
|
+
return `转换失败: ${type} 的 output 必须是 .pptx 文件`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let inputResolution;
|
|
210
|
+
try {
|
|
211
|
+
inputResolution = resolveInputPaths(config.inputType, input, workspaceRoot, type);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return `转换失败: ${error.message}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const inputPreview = formatInputPreview(input);
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
await ensureOutputLocation(outputPath, config.outputType);
|
|
220
|
+
|
|
221
|
+
if (PYTHON_SCRIPTS[type]) {
|
|
222
|
+
const scriptPath = path.join(PYTHON_SCRIPT_ROOT, PYTHON_SCRIPTS[type]);
|
|
223
|
+
if (!fs.existsSync(scriptPath)) {
|
|
224
|
+
throw new Error(`找不到 Python 脚本: ${PYTHON_SCRIPTS[type]}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let scriptArgs = [];
|
|
228
|
+
if (config.inputType === 'list') {
|
|
229
|
+
scriptArgs = [
|
|
230
|
+
'--inputs',
|
|
231
|
+
...inputResolution.list.map(item => shellQuote(item)),
|
|
232
|
+
'--output',
|
|
233
|
+
shellQuote(outputPath)
|
|
234
|
+
];
|
|
235
|
+
} else {
|
|
236
|
+
scriptArgs = [
|
|
237
|
+
'--input',
|
|
238
|
+
shellQuote(inputResolution.file),
|
|
239
|
+
config.outputType === 'directory' ? '--output-dir' : '--output',
|
|
240
|
+
shellQuote(outputPath)
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
await runPythonScript(scriptPath, scriptArgs, workspaceRoot);
|
|
244
|
+
} else {
|
|
245
|
+
throw new Error('未知的转换模式');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [
|
|
249
|
+
'转换完成',
|
|
250
|
+
`type: ${type}`,
|
|
251
|
+
`input: ${inputPreview}`,
|
|
252
|
+
`output: ${output}`
|
|
253
|
+
].join('\n');
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const message = error?.message || String(error);
|
|
256
|
+
return `转换失败: ${message}`;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const schema = {
|
|
261
|
+
type: 'function',
|
|
262
|
+
function: {
|
|
263
|
+
name: 'convert',
|
|
264
|
+
description: [
|
|
265
|
+
`统一文件转换工具。type 支持:${TYPE_ENUM.join('、')}。`,
|
|
266
|
+
'所有路径都是指相对项目根目录的路径,例如:src/index.md、README.md、src/outputs等。'
|
|
267
|
+
].join(' '),
|
|
268
|
+
parameters: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
type: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
enum: TYPE_ENUM,
|
|
274
|
+
description: `转换类型,可选值: ${TYPE_ENUM.join(', ')}`
|
|
275
|
+
},
|
|
276
|
+
input: {
|
|
277
|
+
description: '输入路径:当 inputType 为 list(如 html_to_pptx)时为路径列表,否则为单文件路径。',
|
|
278
|
+
oneOf: [
|
|
279
|
+
{ type: 'string' },
|
|
280
|
+
{ type: 'array', items: { type: 'string' } }
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
output: {
|
|
284
|
+
type: 'string',
|
|
285
|
+
description: '输出路径:PDF->PNG 时为目录路径,其余类型为单文件路径。'
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
required: ['type', 'input', 'output']
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
name: 'convert',
|
|
295
|
+
schema,
|
|
296
|
+
handler: convertHandler
|
|
297
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const bash = require('./bash');
|
|
2
|
+
const read = require('./read');
|
|
3
|
+
const write = require('./write');
|
|
4
|
+
const replace = require('./replace');
|
|
5
|
+
const todos = require('./todos');
|
|
6
|
+
const convert = require('./convert');
|
|
7
|
+
const { createMcpManager } = require('./mcp');
|
|
8
|
+
const { loadSettings } = require('../config');
|
|
9
|
+
|
|
10
|
+
const TOOL_MODULES = [bash, read, write, replace, todos, convert];
|
|
11
|
+
|
|
12
|
+
const createToolRuntime = async (workspaceRoot, options = {}) => {
|
|
13
|
+
const defaultToolNames = TOOL_MODULES.map((tool) => tool.name);
|
|
14
|
+
const { settings } = loadSettings({ defaultTools: defaultToolNames });
|
|
15
|
+
const enabledTools = new Set(settings.tools && settings.tools.length > 0 ? settings.tools : defaultToolNames);
|
|
16
|
+
const enabledMcps = Array.isArray(settings.mcps) ? settings.mcps : [];
|
|
17
|
+
const allowedCommands = settings.allowedCommands;
|
|
18
|
+
|
|
19
|
+
const context = { workspaceRoot, allowedCommands, ...options };
|
|
20
|
+
|
|
21
|
+
const tools = [];
|
|
22
|
+
const handlers = {};
|
|
23
|
+
const enabledToolNames = []; // 记录实际启用的工具名称
|
|
24
|
+
|
|
25
|
+
const registerTool = (toolModule) => {
|
|
26
|
+
if (!enabledTools.has(toolModule.name)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const schema = typeof toolModule.schema === 'function' ? toolModule.schema(context) : toolModule.schema;
|
|
30
|
+
if (schema) {
|
|
31
|
+
tools.push(schema);
|
|
32
|
+
enabledToolNames.push(toolModule.name); // 记录启用的工具
|
|
33
|
+
}
|
|
34
|
+
handlers[toolModule.name] = async (params = {}) => toolModule.handler(params, context);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
TOOL_MODULES.forEach(registerTool);
|
|
38
|
+
|
|
39
|
+
const mcpManager = await createMcpManager(workspaceRoot, enabledMcps);
|
|
40
|
+
let mcpConfigPath = null;
|
|
41
|
+
const mcpToolNames = new Set(); // 记录所有 MCP 工具名称
|
|
42
|
+
const enabledMcpNames = []; // 记录启用的 MCP 服务器名称
|
|
43
|
+
if (mcpManager) {
|
|
44
|
+
mcpManager.getTools().forEach((mcpTool) => {
|
|
45
|
+
tools.push(mcpTool.schema);
|
|
46
|
+
handlers[mcpTool.name] = mcpTool.handler;
|
|
47
|
+
if (mcpTool.isMcpTool) {
|
|
48
|
+
mcpToolNames.add(mcpTool.name);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
mcpConfigPath = mcpManager.getConfigPath();
|
|
52
|
+
enabledMcpNames.push(...mcpManager.getEnabledServerNames()); // 获取启用的 MCP 名称
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dispose = async () => {
|
|
56
|
+
if (mcpManager && typeof mcpManager.dispose === 'function') {
|
|
57
|
+
await mcpManager.dispose();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return { tools, handlers, dispose, mcpConfigPath, mcpToolNames, enabledToolNames, enabledMcpNames };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
createToolRuntime
|
|
66
|
+
};
|