@minko-ai/minko 0.0.0-stage → 0.1.0
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/README.md +89 -2
- package/bin/minko.js +12 -0
- package/package.json +34 -4
- package/src/agent/loop.ts +159 -0
- package/src/agent/prompt.ts +45 -0
- package/src/cli/app.ts +396 -0
- package/src/cli/argv.ts +139 -0
- package/src/cli/configCommand.ts +175 -0
- package/src/config/paths.ts +20 -0
- package/src/config/store.ts +149 -0
- package/src/index.ts +120 -0
- package/src/providers/anthropic.ts +215 -0
- package/src/providers/openai.ts +158 -0
- package/src/providers/sse.ts +57 -0
- package/src/providers/types.ts +59 -0
- package/src/session/store.ts +125 -0
- package/src/theme.ts +41 -0
- package/src/tools/fs.ts +143 -0
- package/src/tools/registry.ts +19 -0
- package/src/tools/shell.ts +120 -0
- package/src/tools/types.ts +26 -0
- package/src/ui/ansi.ts +110 -0
- package/src/ui/banner.ts +39 -0
- package/src/ui/input.ts +54 -0
- package/src/ui/md.ts +160 -0
- package/src/ui/spinner.ts +58 -0
- package/src/util/errors.ts +48 -0
package/src/cli/app.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import * as readline from 'node:readline/promises';
|
|
2
|
+
import type { Interface } from 'node:readline/promises';
|
|
3
|
+
import { Painter, truncateVisible } from '../ui/ansi.ts';
|
|
4
|
+
import { Spinner } from '../ui/spinner.ts';
|
|
5
|
+
import { StreamRenderer } from '../ui/md.ts';
|
|
6
|
+
import { printBanner, formatDuration, formatTokens } from '../ui/banner.ts';
|
|
7
|
+
import type { AgentHooks, AgentOutcome } from '../agent/loop.ts';
|
|
8
|
+
import { runAgent } from '../agent/loop.ts';
|
|
9
|
+
import { systemPrompt } from '../agent/prompt.ts';
|
|
10
|
+
import { ALL_TOOLS } from '../tools/registry.ts';
|
|
11
|
+
import type { ChatMessage, Provider } from '../providers/types.ts';
|
|
12
|
+
import type { Config } from '../config/store.ts';
|
|
13
|
+
import type { ApprovalRequest } from '../tools/types.ts';
|
|
14
|
+
import { SessionStore, titleFromMessages } from '../session/store.ts';
|
|
15
|
+
import type { SessionMeta } from '../session/store.ts';
|
|
16
|
+
import { friendlyError } from '../util/errors.ts';
|
|
17
|
+
import { HELP_TEXT } from './argv.ts';
|
|
18
|
+
|
|
19
|
+
export type AppOptions = {
|
|
20
|
+
config: Config;
|
|
21
|
+
provider: Provider;
|
|
22
|
+
providerName: string;
|
|
23
|
+
model: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
auto: boolean;
|
|
26
|
+
print: boolean;
|
|
27
|
+
prompt?: string;
|
|
28
|
+
continueSession: boolean;
|
|
29
|
+
resumeId?: string;
|
|
30
|
+
setModel: (model: string) => Provider;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type SessionState = {
|
|
34
|
+
meta: SessionMeta;
|
|
35
|
+
history: ChatMessage[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function runApp(options: AppOptions): Promise<number> {
|
|
39
|
+
const store = new SessionStore();
|
|
40
|
+
const session = openSession(store, options);
|
|
41
|
+
if (!session) { return 1; }
|
|
42
|
+
|
|
43
|
+
const interactive = !options.print && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
44
|
+
if (!interactive) {
|
|
45
|
+
return await runOneShot(options, store, session);
|
|
46
|
+
}
|
|
47
|
+
return await runInteractive(options, store, session);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function openSession(store: SessionStore, options: AppOptions): SessionState | null {
|
|
51
|
+
if (options.resumeId) {
|
|
52
|
+
const record = store.load(options.resumeId);
|
|
53
|
+
if (!record) {
|
|
54
|
+
process.stderr.write('找不到会话 ' + options.resumeId + '\n');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return { meta: record.meta, history: record.messages };
|
|
58
|
+
}
|
|
59
|
+
if (options.continueSession) {
|
|
60
|
+
const meta = store.latest(options.cwd);
|
|
61
|
+
if (meta) {
|
|
62
|
+
const record = store.load(meta.id);
|
|
63
|
+
if (record) { return { meta: record.meta, history: record.messages }; }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const meta = store.create(options.cwd, options.model);
|
|
67
|
+
return { meta, history: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fullMessages(session: SessionState, options: AppOptions): ChatMessage[] {
|
|
71
|
+
return [
|
|
72
|
+
{ role: 'system', content: systemPrompt({ cwd: options.cwd }) },
|
|
73
|
+
...session.history,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runOneShot(
|
|
78
|
+
options: AppOptions,
|
|
79
|
+
store: SessionStore,
|
|
80
|
+
session: SessionState,
|
|
81
|
+
): Promise<number> {
|
|
82
|
+
const prompt = options.prompt;
|
|
83
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
84
|
+
process.stderr.write('非交互模式需要给出提示词,例如:minko -p "列出当前目录"\n');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
session.history.push({ role: 'user', content: prompt });
|
|
88
|
+
session.meta.title = titleFromMessages(session.history);
|
|
89
|
+
store.save(session.meta, session.history);
|
|
90
|
+
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
process.on('SIGINT', () => controller.abort());
|
|
93
|
+
|
|
94
|
+
const hooks: AgentHooks = {
|
|
95
|
+
onText: (text) => process.stdout.write(text),
|
|
96
|
+
onToolStart: (call) => {
|
|
97
|
+
process.stderr.write('\n· ' + call.name + ' ' + summarizeArguments(call.arguments) + '\n');
|
|
98
|
+
},
|
|
99
|
+
onToolEnd: (call, result) => {
|
|
100
|
+
process.stderr.write(' ' + (result.ok ? '✓' : '✗') + ' ' + formatDuration(result.ms) + '\n');
|
|
101
|
+
},
|
|
102
|
+
approve: async (request: ApprovalRequest) => {
|
|
103
|
+
if (options.auto) { return true; }
|
|
104
|
+
process.stderr.write('\n· 需要确认:' + request.title + ' — ' + request.detail
|
|
105
|
+
+ '\n (一次性模式下默认拒绝;加 --auto 允许)\n');
|
|
106
|
+
return false;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const outcome = await runAgent({
|
|
112
|
+
provider: options.provider,
|
|
113
|
+
tools: ALL_TOOLS,
|
|
114
|
+
messages: fullMessages(session, options),
|
|
115
|
+
cwd: options.cwd,
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
approval: options.auto ? 'auto' : 'ask',
|
|
118
|
+
hooks,
|
|
119
|
+
});
|
|
120
|
+
process.stdout.write('\n');
|
|
121
|
+
session.history = withoutSystem(outcome.messages);
|
|
122
|
+
store.save(session.meta, session.history);
|
|
123
|
+
return 0;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
process.stderr.write('\n' + friendlyError(error) + '\n');
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runInteractive(
|
|
131
|
+
options: AppOptions,
|
|
132
|
+
store: SessionStore,
|
|
133
|
+
session: SessionState,
|
|
134
|
+
): Promise<number> {
|
|
135
|
+
const painter = new Painter();
|
|
136
|
+
let provider = options.provider;
|
|
137
|
+
let model = options.model;
|
|
138
|
+
|
|
139
|
+
printBanner(painter, {
|
|
140
|
+
model,
|
|
141
|
+
cwd: options.cwd,
|
|
142
|
+
session: session.meta.id,
|
|
143
|
+
continued: session.history.length > 0,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const rl = readline.createInterface({
|
|
147
|
+
input: process.stdin,
|
|
148
|
+
output: process.stdout,
|
|
149
|
+
terminal: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let running: AbortController | null = null;
|
|
153
|
+
let closing = false;
|
|
154
|
+
|
|
155
|
+
const shutdown = (): void => {
|
|
156
|
+
if (closing) { return; }
|
|
157
|
+
closing = true;
|
|
158
|
+
if (running) { running.abort(); }
|
|
159
|
+
rl.close();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
rl.on('SIGINT', () => {
|
|
163
|
+
if (running) { running.abort(); } else { shutdown(); }
|
|
164
|
+
});
|
|
165
|
+
process.on('SIGINT', () => {
|
|
166
|
+
if (running) { running.abort(); } else { shutdown(); }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const allowedTools = new Set<string>();
|
|
170
|
+
|
|
171
|
+
const approve = async (request: ApprovalRequest): Promise<boolean> => {
|
|
172
|
+
if (allowedTools.has(request.tool)) { return true; }
|
|
173
|
+
process.stdout.write('\n ' + painter.warn('⚠ ') + painter.bold(request.title) + '\n');
|
|
174
|
+
const lines = request.detail.split('\n').slice(0, 8);
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
process.stdout.write(' ' + painter.dim(truncateVisible(line, 100)) + '\n');
|
|
177
|
+
}
|
|
178
|
+
let answer = '';
|
|
179
|
+
try {
|
|
180
|
+
answer = (await rl.question(' ' + painter.muted('[y] 允许一次 [a] 总是允许 [n] 拒绝') + ' › '))
|
|
181
|
+
.trim().toLowerCase();
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (answer === 'a' || answer === 'always') {
|
|
186
|
+
allowedTools.add(request.tool);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return answer === 'y' || answer === 'yes' || answer === '是';
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const runTurn = async (input: string): Promise<void> => {
|
|
193
|
+
session.history.push({ role: 'user', content: input });
|
|
194
|
+
session.meta.title = titleFromMessages(session.history);
|
|
195
|
+
store.save(session.meta, session.history);
|
|
196
|
+
|
|
197
|
+
process.stdout.write('\n');
|
|
198
|
+
const spinner = new Spinner(painter);
|
|
199
|
+
const renderer = new StreamRenderer(painter, (chunk) => process.stdout.write(chunk));
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
running = controller;
|
|
202
|
+
const startedAt = Date.now();
|
|
203
|
+
let bullet = false;
|
|
204
|
+
let streamedAny = false;
|
|
205
|
+
|
|
206
|
+
const ensureBullet = (): void => {
|
|
207
|
+
spinner.stop();
|
|
208
|
+
if (!bullet) {
|
|
209
|
+
process.stdout.write(painter.primary('⏺ '));
|
|
210
|
+
bullet = true;
|
|
211
|
+
}
|
|
212
|
+
streamedAny = true;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const hooks: AgentHooks = {
|
|
216
|
+
onText: (text) => {
|
|
217
|
+
ensureBullet();
|
|
218
|
+
renderer.push(text);
|
|
219
|
+
},
|
|
220
|
+
onToolStart: (call) => {
|
|
221
|
+
spinner.stop();
|
|
222
|
+
renderer.finish();
|
|
223
|
+
bullet = false;
|
|
224
|
+
process.stdout.write('\n ' + painter.primary('⏺ ') + painter.soft(call.name)
|
|
225
|
+
+ ' ' + painter.muted(summarizeArguments(call.arguments)) + '\n');
|
|
226
|
+
spinner.start('执行中');
|
|
227
|
+
},
|
|
228
|
+
onToolEnd: (_call, result) => {
|
|
229
|
+
spinner.stop();
|
|
230
|
+
const mark = result.ok ? painter.ok('✓') : painter.error('✗');
|
|
231
|
+
process.stdout.write(' ' + mark + ' ' + painter.dimmed(formatDuration(result.ms)) + '\n');
|
|
232
|
+
if (result.output.trim().length > 0) {
|
|
233
|
+
const lines = result.output.replace(/\r\n/g, '\n').split('\n');
|
|
234
|
+
const shown = lines.slice(0, 6);
|
|
235
|
+
for (const line of shown) {
|
|
236
|
+
process.stdout.write(' ' + painter.fg('#8f7fb8', '│ ') + painter.dim(truncateVisible(line, 110)) + '\n');
|
|
237
|
+
}
|
|
238
|
+
if (lines.length > 6) {
|
|
239
|
+
process.stdout.write(' ' + painter.fg('#8f7fb8', '│ ') + painter.dimmed('… 还有 ' + (lines.length - 6) + ' 行') + '\n');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
approve,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
spinner.start('思考中');
|
|
247
|
+
try {
|
|
248
|
+
const outcome: AgentOutcome = await runAgent({
|
|
249
|
+
provider,
|
|
250
|
+
tools: ALL_TOOLS,
|
|
251
|
+
messages: fullMessages(session, options),
|
|
252
|
+
cwd: options.cwd,
|
|
253
|
+
signal: controller.signal,
|
|
254
|
+
approval: options.auto ? 'auto' : 'ask',
|
|
255
|
+
hooks,
|
|
256
|
+
});
|
|
257
|
+
spinner.stop();
|
|
258
|
+
renderer.finish();
|
|
259
|
+
if (!streamedAny) { process.stdout.write(painter.dimmed('(没有文字回复)') + '\n'); }
|
|
260
|
+
const elapsed = Date.now() - startedAt;
|
|
261
|
+
process.stdout.write(' ' + painter.dimmed('✻ ' + formatDuration(elapsed)
|
|
262
|
+
+ (outcome.usage.input > 0 || outcome.usage.output > 0
|
|
263
|
+
? ' · ' + formatTokens(outcome.usage.input) + ' 输入 / ' + formatTokens(outcome.usage.output) + ' 输出'
|
|
264
|
+
: '')) + '\n');
|
|
265
|
+
session.history = withoutSystem(outcome.messages);
|
|
266
|
+
store.save(session.meta, session.history);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
spinner.stop();
|
|
269
|
+
renderer.finish();
|
|
270
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
271
|
+
process.stdout.write('\n ' + painter.warn('已中断') + '\n');
|
|
272
|
+
} else {
|
|
273
|
+
process.stdout.write('\n ' + painter.error(friendlyError(error)) + '\n');
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
running = null;
|
|
277
|
+
process.stdout.write('\n');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (options.prompt && options.prompt.trim().length > 0) {
|
|
282
|
+
await runTurn(options.prompt.trim());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
while (!closing) {
|
|
286
|
+
let line = '';
|
|
287
|
+
try {
|
|
288
|
+
line = await rl.question(painter.primary('› '));
|
|
289
|
+
} catch {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
const text = line.trim();
|
|
293
|
+
if (text.length === 0) { continue; }
|
|
294
|
+
if (text.startsWith('/')) {
|
|
295
|
+
const done = await handleCommand(text, {
|
|
296
|
+
painter, session, store, options, rl,
|
|
297
|
+
setModel: (next: string) => {
|
|
298
|
+
provider = options.setModel(next);
|
|
299
|
+
model = next;
|
|
300
|
+
},
|
|
301
|
+
currentModel: () => model,
|
|
302
|
+
});
|
|
303
|
+
if (done) { break; }
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
await runTurn(text);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
rl.close();
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
type CommandContext = {
|
|
314
|
+
painter: Painter;
|
|
315
|
+
session: SessionState;
|
|
316
|
+
store: SessionStore;
|
|
317
|
+
options: AppOptions;
|
|
318
|
+
rl: Interface;
|
|
319
|
+
setModel: (model: string) => void;
|
|
320
|
+
currentModel: () => string;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
async function handleCommand(text: string, context: CommandContext): Promise<boolean> {
|
|
324
|
+
const { painter, session, store, options } = context;
|
|
325
|
+
const [name, ...rest] = text.slice(1).split(/\s+/);
|
|
326
|
+
const argument = rest.join(' ').trim();
|
|
327
|
+
|
|
328
|
+
switch (name) {
|
|
329
|
+
case 'exit':
|
|
330
|
+
case 'quit':
|
|
331
|
+
return true;
|
|
332
|
+
case 'help':
|
|
333
|
+
process.stdout.write('\n' + HELP_TEXT.split('\n').map((line) => ' ' + line).join('\n') + '\n\n');
|
|
334
|
+
return false;
|
|
335
|
+
case 'new': {
|
|
336
|
+
session.meta = store.create(options.cwd, options.model);
|
|
337
|
+
session.history = [];
|
|
338
|
+
process.stdout.write(' ' + painter.ok('✓ ') + painter.muted('新会话 ' + session.meta.id) + '\n');
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
case 'clear':
|
|
342
|
+
session.history = [];
|
|
343
|
+
store.save(session.meta, session.history);
|
|
344
|
+
process.stdout.write(' ' + painter.ok('✓ ') + painter.muted('上下文已清空') + '\n');
|
|
345
|
+
return false;
|
|
346
|
+
case 'cwd':
|
|
347
|
+
process.stdout.write(' ' + painter.muted(options.cwd) + '\n');
|
|
348
|
+
return false;
|
|
349
|
+
case 'model': {
|
|
350
|
+
if (argument.length === 0) {
|
|
351
|
+
process.stdout.write(' ' + painter.muted('当前模型:' + context.currentModel()) + '\n');
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
context.setModel(argument);
|
|
356
|
+
process.stdout.write(' ' + painter.ok('✓ ') + '已切换到 ' + painter.soft(argument)
|
|
357
|
+
+ painter.dimmed('(仅本次运行,持久化请用 minko config)') + '\n');
|
|
358
|
+
} catch (error) {
|
|
359
|
+
process.stdout.write(' ' + painter.error(friendlyError(error)) + '\n');
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
case 'sessions': {
|
|
364
|
+
const metas = store.list().slice(0, 10);
|
|
365
|
+
if (metas.length === 0) {
|
|
366
|
+
process.stdout.write(' ' + painter.muted('还没有保存的会话') + '\n');
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
for (const meta of metas) {
|
|
370
|
+
const current = meta.id === session.meta.id ? painter.primary('●') : painter.dimmed('○');
|
|
371
|
+
process.stdout.write(' ' + current + ' ' + painter.soft(meta.id) + ' '
|
|
372
|
+
+ painter.muted(meta.title) + painter.dimmed(' ' + meta.updatedAt.slice(0, 16).replace('T', ' ')) + '\n');
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
default:
|
|
377
|
+
process.stdout.write(' ' + painter.muted('未知命令 /' + name + ',试试 /help') + '\n');
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function withoutSystem(messages: ChatMessage[]): ChatMessage[] {
|
|
383
|
+
return messages.filter((message) => message.role !== 'system');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function summarizeArguments(raw: string): string {
|
|
387
|
+
if (raw.trim().length === 0) { return ''; }
|
|
388
|
+
try {
|
|
389
|
+
const args = JSON.parse(raw) as Record<string, unknown>;
|
|
390
|
+
if (typeof args.command === 'string') { return truncateVisible(args.command, 90); }
|
|
391
|
+
if (typeof args.path === 'string') { return truncateVisible(args.path, 90); }
|
|
392
|
+
return truncateVisible(raw.replace(/\s+/g, ' '), 90);
|
|
393
|
+
} catch {
|
|
394
|
+
return truncateVisible(raw.replace(/\s+/g, ' '), 90);
|
|
395
|
+
}
|
|
396
|
+
}
|
package/src/cli/argv.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type ConfigArgs = {
|
|
2
|
+
command: 'config';
|
|
3
|
+
preset?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
use?: string;
|
|
9
|
+
list: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ParsedArgs =
|
|
13
|
+
| { command: 'help' }
|
|
14
|
+
| { command: 'version' }
|
|
15
|
+
| { command: 'link' }
|
|
16
|
+
| { command: 'sessions'; json: boolean }
|
|
17
|
+
| ConfigArgs
|
|
18
|
+
| {
|
|
19
|
+
command: 'run';
|
|
20
|
+
prompt?: string;
|
|
21
|
+
print: boolean;
|
|
22
|
+
continue: boolean;
|
|
23
|
+
resume?: string;
|
|
24
|
+
auto: boolean;
|
|
25
|
+
model?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const BOOLEAN = new Set([
|
|
29
|
+
'-p', '--print', '-c', '--continue', '--auto',
|
|
30
|
+
'-h', '--help', '-V', '--version', '--json', '--list', '-l', '--link', '-link',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const VALUED = new Set([
|
|
34
|
+
'-r', '--resume', '--model', '--base-url', '--api-key',
|
|
35
|
+
'--provider', '--preset', '--name', '--use',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
39
|
+
const flags = new Map<string, string | true>();
|
|
40
|
+
const positional: string[] = [];
|
|
41
|
+
|
|
42
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
43
|
+
const token = argv[index];
|
|
44
|
+
if (token === '--') {
|
|
45
|
+
positional.push(...argv.slice(index + 1));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (token.startsWith('-') && token !== '-') {
|
|
49
|
+
const equals = token.indexOf('=');
|
|
50
|
+
if (equals > 0) {
|
|
51
|
+
const key = token.slice(0, equals);
|
|
52
|
+
if (!VALUED.has(key)) { throw new Error('未知选项:' + key); }
|
|
53
|
+
flags.set(key, token.slice(equals + 1));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (VALUED.has(token)) {
|
|
57
|
+
const value = argv[index + 1];
|
|
58
|
+
if (value === undefined) { throw new Error('选项 ' + token + ' 缺少值'); }
|
|
59
|
+
flags.set(token, value);
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (BOOLEAN.has(token)) {
|
|
64
|
+
flags.set(token, true);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw new Error('未知选项:' + token + '(运行 minko --help 查看用法)');
|
|
68
|
+
}
|
|
69
|
+
positional.push(token);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (flag(flags, '-h', '--help')) { return { command: 'help' }; }
|
|
73
|
+
if (flag(flags, '-V', '--version')) { return { command: 'version' }; }
|
|
74
|
+
if (flag(flags, '--link', '-link')) { return { command: 'link' }; }
|
|
75
|
+
|
|
76
|
+
const sub = positional[0];
|
|
77
|
+
if (sub === 'config') {
|
|
78
|
+
return {
|
|
79
|
+
command: 'config',
|
|
80
|
+
preset: text(flags, '--preset'),
|
|
81
|
+
apiKey: text(flags, '--api-key'),
|
|
82
|
+
model: text(flags, '--model'),
|
|
83
|
+
baseUrl: text(flags, '--base-url'),
|
|
84
|
+
name: text(flags, '--name'),
|
|
85
|
+
use: text(flags, '--use'),
|
|
86
|
+
list: Boolean(flag(flags, '--list', '-l')),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (sub === 'sessions') {
|
|
90
|
+
return { command: 'sessions', json: Boolean(flag(flags, '--json')) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const prompt = positional.length > 0 ? positional.join(' ') : undefined;
|
|
94
|
+
return {
|
|
95
|
+
command: 'run',
|
|
96
|
+
prompt,
|
|
97
|
+
print: Boolean(flag(flags, '-p', '--print')),
|
|
98
|
+
continue: Boolean(flags.has('-c') || flags.has('--continue')),
|
|
99
|
+
resume: text(flags, '-r') ?? text(flags, '--resume'),
|
|
100
|
+
auto: Boolean(flags.has('--auto')),
|
|
101
|
+
model: text(flags, '--model'),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function flag(flags: Map<string, string | true>, ...names: string[]): boolean {
|
|
106
|
+
return names.some((name) => flags.has(name));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function text(flags: Map<string, string | true>, ...names: string[]): string | undefined {
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
const value = flags.get(name);
|
|
112
|
+
if (typeof value === 'string') { return value; }
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const HELP_TEXT = [
|
|
118
|
+
'用法:minko [选项] [提示词]',
|
|
119
|
+
'',
|
|
120
|
+
'直接运行 minko 进入交互模式;带提示词则执行一轮后退出。',
|
|
121
|
+
'',
|
|
122
|
+
'选项',
|
|
123
|
+
' -p, --print 一次性输出,不进入交互',
|
|
124
|
+
' -c, --continue 继续当前目录最近一次会话',
|
|
125
|
+
' -r, --resume <id> 继续指定会话',
|
|
126
|
+
' --model <id> 本次使用指定模型',
|
|
127
|
+
' --auto 不再逐条确认写文件和命令(危险)',
|
|
128
|
+
' -h, --help 显示帮助',
|
|
129
|
+
' -V, --version 显示版本',
|
|
130
|
+
'',
|
|
131
|
+
'命令',
|
|
132
|
+
' minko config 配置服务商与 API Key',
|
|
133
|
+
' minko config --list 查看已配置的模型',
|
|
134
|
+
' minko sessions 列出保存的会话',
|
|
135
|
+
' minko -link 手机远控配对(规划中)',
|
|
136
|
+
'',
|
|
137
|
+
'会话内命令',
|
|
138
|
+
' /help /new /clear /model <id> /sessions /exit',
|
|
139
|
+
].join('\n');
|