halfcopilot 0.0.1

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.
Files changed (50) hide show
  1. package/dist/halfcop.d.ts +6 -0
  2. package/dist/halfcop.d.ts.map +1 -0
  3. package/dist/halfcop.js +1103 -0
  4. package/dist/halfcop.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +255 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tui/app.d.ts +11 -0
  10. package/dist/tui/app.d.ts.map +1 -0
  11. package/dist/tui/app.js +105 -0
  12. package/dist/tui/app.js.map +1 -0
  13. package/dist/tui/components/ChatView.d.ts +12 -0
  14. package/dist/tui/components/ChatView.d.ts.map +1 -0
  15. package/dist/tui/components/ChatView.js +70 -0
  16. package/dist/tui/components/ChatView.js.map +1 -0
  17. package/dist/tui/components/InputField.d.ts +8 -0
  18. package/dist/tui/components/InputField.d.ts.map +1 -0
  19. package/dist/tui/components/InputField.js +24 -0
  20. package/dist/tui/components/InputField.js.map +1 -0
  21. package/dist/tui/components/StatusBar.d.ts +18 -0
  22. package/dist/tui/components/StatusBar.d.ts.map +1 -0
  23. package/dist/tui/components/StatusBar.js +57 -0
  24. package/dist/tui/components/StatusBar.js.map +1 -0
  25. package/dist/tui/components/ToolApproval.d.ts +11 -0
  26. package/dist/tui/components/ToolApproval.d.ts.map +1 -0
  27. package/dist/tui/components/ToolApproval.js +41 -0
  28. package/dist/tui/components/ToolApproval.js.map +1 -0
  29. package/dist/tui/components/index.d.ts +5 -0
  30. package/dist/tui/components/index.d.ts.map +1 -0
  31. package/dist/tui/components/index.js +5 -0
  32. package/dist/tui/components/index.js.map +1 -0
  33. package/dist/tui/index.d.ts +3 -0
  34. package/dist/tui/index.d.ts.map +1 -0
  35. package/dist/tui/index.js +3 -0
  36. package/dist/tui/index.js.map +1 -0
  37. package/package.json +43 -0
  38. package/src/__tests__/cli.test.ts +73 -0
  39. package/src/halfcop.ts +1373 -0
  40. package/src/index.ts +348 -0
  41. package/src/tui/app.tsx +160 -0
  42. package/src/tui/components/ChatView.tsx +130 -0
  43. package/src/tui/components/InputField.tsx +41 -0
  44. package/src/tui/components/StatusBar.tsx +92 -0
  45. package/src/tui/components/ToolApproval.tsx +80 -0
  46. package/src/tui/components/index.ts +4 -0
  47. package/src/tui/index.ts +7 -0
  48. package/tsconfig.json +20 -0
  49. package/tsconfig.tsbuildinfo +1 -0
  50. package/vitest.config.ts +7 -0
package/src/halfcop.ts ADDED
@@ -0,0 +1,1373 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * HalfCopilot CLI - Beautiful Chat Interface
5
+ */
6
+
7
+ // Suppress noisy Node.js deprecation warnings (e.g. punycode from deps)
8
+ try { process.noDeprecation = true; } catch {}
9
+ process.removeAllListeners("warning");
10
+ process.on("warning", (w) => {
11
+ if (w.name === "DeprecationWarning" && String(w.message).includes("punycode")) return;
12
+ // biome-ignore lint/suspicious/noConsole: intentional
13
+ console.error(w.stack ?? w.message);
14
+ });
15
+
16
+ import { Command } from "commander";
17
+ import { loadConfig, type HalfCopilotConfig } from "@halfcopilot/config";
18
+ import { ProviderRegistry } from "@halfcopilot/provider";
19
+ import {
20
+ ToolRegistry,
21
+ createBuiltinTools,
22
+ PermissionChecker,
23
+ ToolExecutor,
24
+ } from "@halfcopilot/tools";
25
+ import { AgentLoop, HybridProvider, type AgentMode } from "@halfcopilot/core";
26
+ import { SkillRegistry, createBuiltinSkills } from "@halfcopilot/skills";
27
+ import readline from "readline";
28
+
29
+ const program = new Command();
30
+
31
+ program
32
+ .name("halfcop")
33
+ .description("HalfCopilot — Multi-model Agent Framework CLI")
34
+ .version("1.0.29");
35
+
36
+ interface AgentOptions {
37
+ model?: string;
38
+ provider?: string;
39
+ mode?: string;
40
+ hybrid?: boolean;
41
+ }
42
+
43
+ // Beautiful color palette
44
+ const c = {
45
+ reset: "\x1b[0m",
46
+ bold: "\x1b[1m",
47
+ dim: "\x1b[2m",
48
+ italic: "\x1b[3m",
49
+
50
+ // Colors
51
+ black: "\x1b[30m",
52
+ red: "\x1b[31m",
53
+ green: "\x1b[32m",
54
+ yellow: "\x1b[33m",
55
+ blue: "\x1b[34m",
56
+ magenta: "\x1b[35m",
57
+ cyan: "\x1b[36m",
58
+ white: "\x1b[37m",
59
+ gray: "\x1b[90m",
60
+
61
+ // Background
62
+ bgCyan: "\x1b[46m",
63
+ bgGreen: "\x1b[42m",
64
+ bgYellow: "\x1b[43m",
65
+ bgBlue: "\x1b[44m",
66
+ };
67
+
68
+ // Box drawing characters
69
+ const box = {
70
+ tl: "╭",
71
+ tr: "╮",
72
+ bl: "╰",
73
+ br: "╯",
74
+ h: "─",
75
+ v: "│",
76
+ ml: "├",
77
+ mr: "┤",
78
+ };
79
+
80
+ // Agent status types
81
+ type AgentStatus = "idle" | "thinking" | "executing" | "completed" | "error";
82
+
83
+ function sleep(ms: number) {
84
+ return new Promise((resolve) => setTimeout(resolve, ms));
85
+ }
86
+
87
+ // Braille dot spinning animation (Claude Code style)
88
+ function createThinkingAnimation() {
89
+ let interval: NodeJS.Timeout | null = null;
90
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
91
+ let i = 0;
92
+
93
+ const start = (message = "Thinking") => {
94
+ if (interval) return;
95
+ interval = setInterval(() => {
96
+ process.stdout.write(
97
+ `\r ${c.cyan}${frames[i % frames.length]}${c.reset} ${c.dim}${message}${c.reset} `,
98
+ );
99
+ i++;
100
+ }, 80);
101
+ };
102
+
103
+ const stop = () => {
104
+ if (interval) {
105
+ clearInterval(interval);
106
+ interval = null;
107
+ }
108
+ process.stdout.write("\r" + " ".repeat(50) + "\r");
109
+ };
110
+
111
+ return { start, stop };
112
+ }
113
+
114
+ // Animated loading indicator
115
+ async function showLoading(message: string, duration: number = 1500) {
116
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
117
+ const startTime = Date.now();
118
+ let i = 0;
119
+
120
+ process.stdout.write(`\r${c.cyan}${frames[0]} ${message}${c.reset}`);
121
+
122
+ while (Date.now() - startTime < duration) {
123
+ await sleep(80);
124
+ i = (i + 1) % frames.length;
125
+ process.stdout.write(`\r${c.cyan}${frames[i]} ${message}${c.reset}`);
126
+ }
127
+
128
+ process.stdout.write("\r" + " ".repeat(message.length + 4) + "\r");
129
+ }
130
+
131
+ function printBox(content: string, color: string = c.cyan, width: number = 50) {
132
+ const lines = content.split("\n");
133
+ const maxLen = Math.max(...lines.map((l) => l.length), width - 4);
134
+
135
+ console.log(
136
+ `${color}${box.tl}${box.h.repeat(maxLen + 2)}${box.tr}${c.reset}`,
137
+ );
138
+ for (const line of lines) {
139
+ const padding = " ".repeat(maxLen - line.length);
140
+ console.log(
141
+ `${color}${box.v}${c.reset} ${line}${padding} ${color}${box.v}${c.reset}`,
142
+ );
143
+ }
144
+ console.log(
145
+ `${color}${box.bl}${box.h.repeat(maxLen + 2)}${box.br}${c.reset}`,
146
+ );
147
+ }
148
+
149
+ // Simple banner - no ASCII art that breaks across terminals
150
+ function printHeader() {
151
+ console.log("");
152
+ console.log(` ${c.cyan}${c.bold}╭${"─".repeat(62)}╮${c.reset}`);
153
+ console.log(` ${c.cyan}${c.bold}│${" ".repeat(62)}│${c.reset}`);
154
+ // Banner: H A L F C O P I L O T (19 chars), centered in 62-char box
155
+ const banner = "H A L F C O P I L O T";
156
+ const bannerPad = Math.floor((62 - banner.length) / 2);
157
+ console.log(
158
+ ` ${c.cyan}${c.bold}│${" ".repeat(bannerPad)}${c.white}${c.bold}${banner}${c.reset}${c.cyan}${" ".repeat(62 - bannerPad - banner.length)}│${c.reset}`,
159
+ );
160
+ // Subtitle: 32 chars, pad 15 each side
161
+ const subtitle = "Multi-model Agent Framework CLI";
162
+ const subPad = Math.floor((62 - subtitle.length) / 2);
163
+ console.log(
164
+ ` ${c.cyan}${c.bold}│${" ".repeat(subPad)}${c.white}${subtitle}${c.reset}${c.cyan}${" ".repeat(62 - subPad - subtitle.length)}│${c.reset}`,
165
+ );
166
+ console.log(` ${c.cyan}${c.bold}│${" ".repeat(62)}│${c.reset}`);
167
+ console.log(` ${c.cyan}${c.bold}╰${"─".repeat(62)}╯${c.reset}`);
168
+ console.log("");
169
+ }
170
+
171
+ function printInfo(label: string, value: string) {
172
+ console.log(
173
+ ` ${c.gray}${label}:${c.reset} ${c.white}${c.bold}${value}${c.reset}`,
174
+ );
175
+ }
176
+
177
+ function printUserMessage(message: string) {
178
+ const maxLen = Math.min(message.length, 60);
179
+ const display =
180
+ message.substring(0, maxLen) + (message.length > maxLen ? "..." : "");
181
+ const lineLen = display.length + 4;
182
+
183
+ console.log(
184
+ `\n ${c.green}${box.tl}─── You ${box.h.repeat(Math.max(0, lineLen - 12))}${box.tr}${c.reset}`,
185
+ );
186
+ console.log(
187
+ ` ${c.green}${box.v}${c.reset} ${c.green}${c.bold}${display}${c.reset}${" ".repeat(Math.max(0, 60 - display.length))} ${c.green}${box.v}${c.reset}`,
188
+ );
189
+ console.log(
190
+ ` ${c.green}${box.bl}───${box.h.repeat(Math.max(0, lineLen - 1))}${box.br}${c.reset}\n`,
191
+ );
192
+ }
193
+
194
+ // Strip basic markdown syntax for clean terminal display
195
+ function stripMarkdown(text: string): string {
196
+ return text
197
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // **bold** -> text
198
+ .replace(/\*([^*]+)\*/g, "$1") // *italic* -> text
199
+ .replace(/`([^`]+)`/g, "$1") // `code` -> text
200
+ .replace(/^#+\s+(.*)$/gm, "$1") // # headings -> plain
201
+ .replace(/^>\s+(.*)$/gm, " ▸ $1") // > blockquote
202
+ .replace(/^-\s+/gm, " • ") // - list items
203
+ .replace(/\*\*([^*]+)\*\*/g, "$1"); // already handled above, but double-check
204
+ }
205
+
206
+ // Print text with code block detection
207
+ function printFormatted(text: string) {
208
+ const parts = text.split(/(```[\s\S]*?```)/);
209
+ for (const part of parts) {
210
+ if (part.startsWith("```")) {
211
+ const code = part.replace(/```\w*\n?/, "").replace(/```$/, "");
212
+ const lines = code.split("\n");
213
+ for (const line of lines) {
214
+ process.stdout.write(` ${c.gray}│ ${line}${c.reset}\n`);
215
+ }
216
+ } else {
217
+ process.stdout.write(part);
218
+ }
219
+ }
220
+ }
221
+
222
+ function printMarkdownBox(text: string) {
223
+ // Strip markdown first
224
+ const clean = stripMarkdown(text);
225
+ const displayLines = clean.split("\n").filter((l) => l.trim() !== "");
226
+ if (displayLines.length === 0) return;
227
+
228
+ const maxLen = Math.min(Math.max(...displayLines.map((l) => l.length)), 64);
229
+ const top = ` ${c.blue}${box.tl}─── HalfCopilot ${box.h.repeat(Math.max(0, maxLen - 21))}${box.tr}${c.reset}`;
230
+
231
+ console.log("\n" + top);
232
+ for (const line of displayLines) {
233
+ const padding = " ".repeat(Math.max(0, maxLen - line.length));
234
+ console.log(
235
+ ` ${c.blue}${box.v}${c.reset} ${c.white}${line}${c.reset}${padding} ${c.blue}${box.v}${c.reset}`,
236
+ );
237
+ }
238
+ const bot = ` ${c.blue}${box.bl}───${box.h.repeat(maxLen)}${box.br}${c.reset}`;
239
+ console.log(bot + "\n");
240
+ }
241
+
242
+ function displayThinkingBox(text: string): void {
243
+ const lines = text.split("\n").filter(Boolean);
244
+ if (!lines.length) return;
245
+ for (const line of lines) {
246
+ process.stdout.write(` ${c.dim}${c.italic}💭 ${line}${c.reset}\n`);
247
+ }
248
+ }
249
+
250
+ function printAssistantStart() {
251
+ process.stdout.write(`\n ${c.blue}${c.bold}🤖 ${c.reset}`);
252
+ }
253
+
254
+ function printAssistantEnd() {
255
+ console.log("\n");
256
+ }
257
+
258
+ function printAssistantText(text: string) {
259
+ printFormatted(text);
260
+ }
261
+
262
+ function printThinking() {
263
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
264
+ let i = 0;
265
+ let interval: NodeJS.Timeout | null = null;
266
+
267
+ const start = () => {
268
+ interval = setInterval(() => {
269
+ process.stdout.write(
270
+ `\r ${c.cyan}${frames[i % frames.length]} ${c.dim}Thinking...${c.reset} `,
271
+ );
272
+ i++;
273
+ }, 80);
274
+ };
275
+
276
+ const stop = () => {
277
+ if (interval) clearInterval(interval);
278
+ process.stdout.write("\r" + " ".repeat(30) + "\r");
279
+ };
280
+
281
+ return { start, stop };
282
+ }
283
+
284
+ // Status tracking variables
285
+ let currentStatus: AgentStatus = "idle";
286
+ let currentProvider = "";
287
+ let currentModel = "";
288
+ let currentMode = "auto";
289
+ let statusDescription = "Ready";
290
+ let sessionStartTime = 0;
291
+ let currentTurn = 0;
292
+ let maxTurns = 20;
293
+ let inputTokens = 0;
294
+ let outputTokens = 0;
295
+ let lastResponseTime = 0;
296
+ let responseStartTime = 0;
297
+
298
+ const statusColors: Record<AgentStatus, string> = {
299
+ idle: c.gray,
300
+ thinking: c.yellow,
301
+ executing: c.blue,
302
+ completed: c.green,
303
+ error: c.red,
304
+ };
305
+
306
+ const statusEmoji: Record<AgentStatus, string> = {
307
+ idle: "⚪",
308
+ thinking: "🟡",
309
+ executing: "🔵",
310
+ completed: "🟢",
311
+ error: "🔴",
312
+ };
313
+
314
+ const PERMISSION_INDICATORS: Record<string, string> = {
315
+ SAFE: "🟢",
316
+ WARN: "🟡",
317
+ UNSAFE: "🔴",
318
+ };
319
+
320
+ function formatDuration(ms: number): string {
321
+ if (ms < 1000) return `${ms}ms`;
322
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
323
+ const m = Math.floor(ms / 60000);
324
+ const s = Math.floor((ms % 60000) / 1000);
325
+ return `${m}m${s}s`;
326
+ }
327
+
328
+ function getTerminalWidth(): number {
329
+ return process.stdout.columns || 80;
330
+ }
331
+
332
+ function truncate(text: string, maxLen: number): string {
333
+ if (text.length <= maxLen) return text;
334
+ return text.slice(0, maxLen - 1) + "…";
335
+ }
336
+
337
+ // Enhanced status bar with rich session info
338
+ function printStatusBar() {
339
+ const cols = getTerminalWidth();
340
+ const elapsed =
341
+ sessionStartTime > 0 ? formatDuration(Date.now() - sessionStartTime) : "";
342
+ const tokens =
343
+ inputTokens + outputTokens > 0 ? `${inputTokens}↓${outputTokens}↑` : "";
344
+ const turnInfo = currentTurn > 0 ? `${currentTurn}/${maxTurns}` : "";
345
+
346
+ const leftParts: string[] = [];
347
+ if (currentProvider)
348
+ leftParts.push(`${c.gray}${currentProvider}/${currentModel}${c.reset}`);
349
+ if (turnInfo) leftParts.push(`${c.dim}${turnInfo}${c.reset}`);
350
+ const left = leftParts.join(" ");
351
+
352
+ const centerParts: string[] = [];
353
+ if (currentMode)
354
+ centerParts.push(`${c.cyan}[${currentMode.toUpperCase()}]${c.reset}`);
355
+ if (tokens) centerParts.push(`${c.green}${tokens}${c.reset}`);
356
+ if (elapsed) centerParts.push(`${c.dim}${elapsed}${c.reset}`);
357
+ const center = centerParts.join(" ");
358
+
359
+ const right = `${statusColors[currentStatus]}${statusEmoji[currentStatus]} ${truncate(statusDescription, 30)}${c.reset}`;
360
+
361
+ const paddedLeft = ` ${left}`;
362
+ const paddedRight = `${right} `;
363
+ const remaining = Math.max(1, cols - paddedLeft.length - paddedRight.length);
364
+ const centerPadded = center.slice(0, remaining);
365
+
366
+ console.log(
367
+ `${paddedLeft}${" ".repeat(Math.max(1, remaining - centerPadded.length))}${centerPadded}${paddedRight}`,
368
+ );
369
+ }
370
+
371
+ function updateStatus(status: AgentStatus, desc?: string) {
372
+ currentStatus = status;
373
+ if (desc) statusDescription = desc;
374
+ if (currentStatus === "completed" && !desc) {
375
+ statusDescription = "Ready";
376
+ }
377
+ }
378
+
379
+ function checkConfig(config: HalfCopilotConfig): boolean {
380
+ const providers = config?.providers;
381
+ if (!providers || Object.keys(providers).length === 0) {
382
+ console.log("");
383
+ console.log(
384
+ ` ${c.yellow}${c.bold}⚙️ 首次使用需要先配置模型 API Key${c.reset}`,
385
+ );
386
+ console.log("");
387
+ console.log(` ${c.white}运行以下命令进行交互式配置:${c.reset}`);
388
+ console.log("");
389
+ console.log(` ${c.green}${c.bold}halfcop setup${c.reset}`);
390
+ console.log("");
391
+ console.log(` ${c.dim}或手动创建 ~/.halfcopilot/settings.json${c.reset}`);
392
+ console.log("");
393
+ return false;
394
+ }
395
+ return true;
396
+ }
397
+
398
+ // Keypress-based tool approval (no readline question)
399
+ async function askApproval(
400
+ toolName: string,
401
+ _input: Record<string, unknown>,
402
+ ): Promise<boolean> {
403
+ return new Promise((resolve) => {
404
+ process.stdout.write(` ${c.yellow}Allow ${toolName}? (y/n): ${c.reset}`);
405
+ const handler = (_str: string | undefined, key: readline.Key) => {
406
+ if (key.name === "y") {
407
+ process.stdin.removeListener("keypress", handler);
408
+ process.stdout.write("y\n");
409
+ resolve(true);
410
+ } else if (key.name === "n") {
411
+ process.stdin.removeListener("keypress", handler);
412
+ process.stdout.write("n\n");
413
+ resolve(false);
414
+ }
415
+ };
416
+ process.stdin.on("keypress", handler);
417
+ });
418
+ }
419
+
420
+ function createAgent(options: AgentOptions = {}) {
421
+ const config = loadConfig();
422
+
423
+ // Check if any providers configured
424
+ if (!checkConfig(config)) {
425
+ process.exit(0);
426
+ }
427
+
428
+ const providerRegistry = new ProviderRegistry();
429
+ providerRegistry.createFromConfig(config);
430
+
431
+ const providerName = options.provider ?? config.defaultProvider ?? "xiaomi";
432
+ let provider = providerRegistry.get(providerName);
433
+
434
+ if (options.hybrid) {
435
+ provider = new HybridProvider(provider);
436
+ }
437
+
438
+ const toolRegistry = new ToolRegistry();
439
+ const builtinTools = createBuiltinTools();
440
+ builtinTools.forEach((t) => toolRegistry.register(t));
441
+
442
+ const skillRegistry = new SkillRegistry();
443
+ const builtinSkills = createBuiltinSkills();
444
+ builtinSkills.forEach((s) => skillRegistry.register(s));
445
+
446
+ const permissions = new PermissionChecker({
447
+ autoApproveSafe: config.permissions.autoApproveSafe,
448
+ allow: config.permissions.allow,
449
+ deny: config.permissions.deny,
450
+ });
451
+
452
+ const executor = new ToolExecutor(toolRegistry, permissions, askApproval);
453
+
454
+ const modelName = options.model ?? config.defaultModel ?? "mimo-v2.5-pro";
455
+
456
+ const agent = new AgentLoop({
457
+ provider,
458
+ providerName,
459
+ model: modelName,
460
+ tools: toolRegistry,
461
+ executor,
462
+ permissions,
463
+ maxTurns: config.maxTurns,
464
+ mode: (options.mode as AgentMode) ?? "auto",
465
+ });
466
+
467
+ return {
468
+ agent,
469
+ providerName,
470
+ config,
471
+ skillRegistry,
472
+ modelName,
473
+ providerRegistry,
474
+ };
475
+ }
476
+
477
+ async function runInteractive(options: AgentOptions = {}) {
478
+ const {
479
+ agent,
480
+ providerName,
481
+ config,
482
+ skillRegistry,
483
+ modelName,
484
+ providerRegistry,
485
+ } = createAgent(options);
486
+
487
+ currentProvider = providerName;
488
+ currentModel = modelName;
489
+ currentMode = options.mode ?? "auto";
490
+ maxTurns = config.maxTurns ?? 20;
491
+ currentTurn = 0;
492
+ inputTokens = 0;
493
+ outputTokens = 0;
494
+ sessionStartTime = Date.now();
495
+
496
+ printHeader();
497
+ printInfo("Provider", providerName);
498
+ printInfo("Model", modelName);
499
+ printInfo("Mode", options.mode ?? "auto");
500
+ printInfo("Max Turns", String(maxTurns));
501
+ console.log("");
502
+ console.log(
503
+ ` ${c.dim}Type to chat. /help for commands. "exit" to quit.${c.reset}`,
504
+ );
505
+ console.log("");
506
+
507
+ const agentRef: { current: AgentLoop } = { current: agent };
508
+
509
+ let isProcessing = false;
510
+
511
+ // Setup readline for input (readline manages raw mode internally)
512
+ const rl = readline.createInterface({
513
+ input: process.stdin,
514
+ output: process.stdout,
515
+ });
516
+
517
+ const PROMPT = ` ${c.green}${c.bold}❯${c.reset} `;
518
+
519
+ const ask = () => {
520
+ rl.question(PROMPT, async (input) => {
521
+ if (isProcessing) return;
522
+ const trimmed = input.trim();
523
+
524
+ if (!trimmed) {
525
+ ask();
526
+ return;
527
+ }
528
+
529
+ if (
530
+ trimmed.toLowerCase() === "exit" ||
531
+ trimmed.toLowerCase() === "quit"
532
+ ) {
533
+ console.log(`\n ${c.yellow}Bye! 👋${c.reset}`);
534
+ rl.close();
535
+ return;
536
+ }
537
+
538
+ if (trimmed.startsWith("/")) {
539
+ const result = await handleCommand(
540
+ trimmed,
541
+ options,
542
+ modelName,
543
+ providerName,
544
+ agentRef,
545
+ providerRegistry,
546
+ config,
547
+ );
548
+ if (result?.newModel) currentModel = result.newModel;
549
+ if (result?.newProvider) currentProvider = result.newProvider;
550
+ ask();
551
+ return;
552
+ }
553
+
554
+ await processInput(trimmed);
555
+ ask();
556
+ });
557
+ };
558
+
559
+ // ------- Agent processing -------
560
+
561
+ const processInput = async (input: string) => {
562
+ isProcessing = true;
563
+ const trimmed = input.trim();
564
+ if (!trimmed) {
565
+ isProcessing = false;
566
+ return;
567
+ }
568
+
569
+ let interrupted = false;
570
+ const onKeypress = (_str: string | undefined, key: readline.Key) => {
571
+ if (key.name === "escape") interrupted = true;
572
+ };
573
+ process.stdin.on("keypress", onKeypress);
574
+
575
+ const thinking = createThinkingAnimation();
576
+ thinking.start();
577
+
578
+ let responseStarted = false;
579
+ let thinkingDisplayed = false;
580
+ let loopEnded = false;
581
+ let atLineStart = false;
582
+ let tBuffer = "";
583
+
584
+ try {
585
+ for await (const event of agentRef.current.run(trimmed)) {
586
+ if (interrupted) {
587
+ loopEnded = true;
588
+ thinking.stop();
589
+ process.stdout.write(`\n ${c.yellow}⏹ Interrupted${c.reset}\n`);
590
+ atLineStart = true;
591
+ updateStatus("idle", "Interrupted");
592
+ break;
593
+ }
594
+
595
+ switch (event.type) {
596
+ case "thinking":
597
+ if (!responseStarted) {
598
+ if (!thinkingDisplayed) {
599
+ thinking.stop();
600
+ thinkingDisplayed = true;
601
+ }
602
+ let content = event.content ?? "";
603
+ content = content.replace(/<\/?think>/gi, "").trim();
604
+ if (content) displayThinkingBox(content);
605
+ }
606
+ break;
607
+ case "text": {
608
+ const chunk = event.content ?? "";
609
+ if (!responseStarted) {
610
+ thinking.stop();
611
+ }
612
+ const combined = tBuffer + chunk;
613
+ const openIdx = combined.indexOf("<think>");
614
+ const closeIdx = combined.indexOf("</think>");
615
+ const hasComplete =
616
+ openIdx >= 0 && closeIdx >= 0 && closeIdx > openIdx + 6;
617
+ if (!responseStarted && hasComplete) {
618
+ const thinkText = combined.slice(openIdx + 7, closeIdx).trim();
619
+ if (thinkText) displayThinkingBox(thinkText);
620
+ const after = combined.slice(closeIdx + 8).trimStart();
621
+ if (after) {
622
+ process.stdout.write(
623
+ `\n ${c.green}${c.bold}●${c.reset} `,
624
+ );
625
+ responseStarted = true;
626
+ atLineStart = false;
627
+ responseStartTime = Date.now();
628
+ const clean = after.replace(/^\n+/, "");
629
+ if (clean) {
630
+ const indented = clean.includes("\n")
631
+ ? clean.replace(/\n/g, "\n ")
632
+ : clean;
633
+ process.stdout.write(indented);
634
+ atLineStart = clean.endsWith("\n");
635
+ }
636
+ }
637
+ tBuffer = "";
638
+ break;
639
+ }
640
+ if (!responseStarted && openIdx >= 0) {
641
+ tBuffer = combined;
642
+ break;
643
+ }
644
+ if (!responseStarted) {
645
+ process.stdout.write(
646
+ `\n ${c.green}${c.bold}●${c.reset} `,
647
+ );
648
+ responseStarted = true;
649
+ atLineStart = false;
650
+ responseStartTime = Date.now();
651
+ const clean = combined.replace(/^\n+/, "");
652
+ const indented = clean.includes("\n")
653
+ ? clean.replace(/\n/g, "\n ")
654
+ : clean;
655
+ process.stdout.write(indented);
656
+ tBuffer = "";
657
+ break;
658
+ }
659
+ if (tBuffer) {
660
+ tBuffer = combined;
661
+ if (closeIdx >= 0 && openIdx >= 0 && closeIdx > openIdx + 6) {
662
+ const thinkText = tBuffer.slice(openIdx + 7, closeIdx).trim();
663
+ if (thinkText) displayThinkingBox(thinkText);
664
+ process.stdout.write(
665
+ `\n ${c.green}${c.bold}●${c.reset} `,
666
+ );
667
+ responseStarted = true;
668
+ atLineStart = false;
669
+ responseStartTime = Date.now();
670
+ const after = tBuffer.slice(closeIdx + 8).trimStart();
671
+ if (after) {
672
+ const clean = after.replace(/^\n+/, "");
673
+ if (clean) {
674
+ const indented = clean.includes("\n")
675
+ ? clean.replace(/\n/g, "\n ")
676
+ : clean;
677
+ process.stdout.write(indented);
678
+ atLineStart = clean.endsWith("\n");
679
+ }
680
+ }
681
+ tBuffer = "";
682
+ break;
683
+ }
684
+ break;
685
+ }
686
+ if (atLineStart) {
687
+ process.stdout.write(` `);
688
+ atLineStart = false;
689
+ }
690
+ if (chunk.includes("\n")) {
691
+ const indented = chunk.replace(/\n/g, "\n ");
692
+ process.stdout.write(indented);
693
+ atLineStart = chunk.endsWith("\n");
694
+ } else {
695
+ process.stdout.write(chunk);
696
+ atLineStart = false;
697
+ }
698
+ break;
699
+ }
700
+ case "tool_use":
701
+ if (!responseStarted) {
702
+ thinking.stop();
703
+ responseStarted = true;
704
+ }
705
+ const toolInput = event.toolInput ?? {};
706
+ const inputStr = JSON.stringify(toolInput);
707
+ process.stdout.write(
708
+ `\n ${c.cyan}🔧 ${event.toolName}${c.reset} ${c.yellow}${PERMISSION_INDICATORS.WARN}${c.reset} `,
709
+ );
710
+ if (inputStr.length < 60) {
711
+ process.stdout.write(`${c.dim}${inputStr}${c.reset}`);
712
+ } else {
713
+ const formatted = JSON.stringify(toolInput, null, 2);
714
+ process.stdout.write(
715
+ `\n${formatted
716
+ .split("\n")
717
+ .map((l: string) => ` ${c.dim}${l}${c.reset}`)
718
+ .join("\n")}`,
719
+ );
720
+ }
721
+ updateStatus("executing", event.toolName);
722
+ break;
723
+ case "tool_result":
724
+ const output = event.toolOutput;
725
+ const success = output !== undefined && output !== null;
726
+ if (success) {
727
+ process.stdout.write(` ${c.green}✓${c.reset}\n`);
728
+ } else {
729
+ const errSummary = (
730
+ typeof output === "string" ? output : String(output ?? "")
731
+ ).slice(0, 100);
732
+ process.stdout.write(
733
+ ` ${c.red}✗${c.reset} ${c.dim}${errSummary}${c.reset}\n`,
734
+ );
735
+ }
736
+ atLineStart = true;
737
+ updateStatus("executing", "Tool completed");
738
+ break;
739
+ case "error":
740
+ loopEnded = true;
741
+ thinking.stop();
742
+ process.stdout.write(
743
+ `\n ${c.red}✗ ${event.error?.message?.slice(0, 100) ?? "error"}${c.reset}\n`,
744
+ );
745
+ atLineStart = true;
746
+ updateStatus("error", event.error?.message?.slice(0, 50));
747
+ break;
748
+ case "done":
749
+ loopEnded = true;
750
+ if (responseStarted) {
751
+ const respTime =
752
+ responseStartTime > 0 ? Date.now() - responseStartTime : 0;
753
+ lastResponseTime = respTime;
754
+ process.stdout.write(
755
+ `\n ${c.dim}(${formatDuration(respTime)})${c.reset}\n\n`,
756
+ );
757
+ }
758
+ atLineStart = true;
759
+ updateStatus("completed", "Ready");
760
+ if (event.usage) {
761
+ inputTokens += event.usage.inputTokens ?? 0;
762
+ outputTokens += event.usage.outputTokens ?? 0;
763
+ }
764
+ break;
765
+ }
766
+ }
767
+
768
+ if (!loopEnded) thinking.stop();
769
+ } catch (err) {
770
+ thinking.stop();
771
+ const msg = err instanceof Error ? err.message : String(err);
772
+ process.stdout.write(
773
+ `\n ${c.red}✗ ${msg.replace(/^400 /, "").replace(/^429 /, "Quota exhausted — ").slice(0, 120)}${c.reset}\n`,
774
+ );
775
+ updateStatus("error", msg.slice(0, 50));
776
+ } finally {
777
+ process.stdin.removeListener("keypress", onKeypress);
778
+ isProcessing = false;
779
+ currentTurn++;
780
+ }
781
+ };
782
+
783
+ // Keep process alive, cleanup terminal on exit
784
+ await new Promise<void>((resolve) => {
785
+ rl.on("close", () => {
786
+ try {
787
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
788
+ } catch {}
789
+ process.stdin.pause();
790
+ resolve();
791
+ });
792
+ ask();
793
+ });
794
+ }
795
+
796
+ interface HandleCommandResult {
797
+ newModel?: string;
798
+ newProvider?: string;
799
+ }
800
+
801
+ async function handleCommand(
802
+ cmd: string,
803
+ opts: AgentOptions,
804
+ currentModel: string,
805
+ currentProvider: string,
806
+ agentRef: { current: AgentLoop },
807
+ providerRegistry: ProviderRegistry,
808
+ config: HalfCopilotConfig,
809
+ ): Promise<HandleCommandResult | void> {
810
+ const parts = cmd.split(" ");
811
+ const command = parts[0].toLowerCase();
812
+ const arg = parts.slice(1).join(" ");
813
+
814
+ switch (command) {
815
+ case "/model":
816
+ if (arg) {
817
+ opts.model = arg;
818
+ updateStatus("thinking", `Switching to ${arg}...`);
819
+ try {
820
+ // Re-create agent with new model
821
+ const providerName =
822
+ opts.provider ?? config.defaultProvider ?? "xiaomi";
823
+ const provider = providerRegistry.get(providerName);
824
+
825
+ const toolRegistry = new ToolRegistry();
826
+ const builtinTools = createBuiltinTools();
827
+ builtinTools.forEach((t) => toolRegistry.register(t));
828
+
829
+ const permissions = new PermissionChecker({
830
+ autoApproveSafe: config.permissions.autoApproveSafe,
831
+ allow: config.permissions.allow,
832
+ deny: config.permissions.deny,
833
+ });
834
+
835
+ const executor = new ToolExecutor(
836
+ toolRegistry,
837
+ permissions,
838
+ askApproval,
839
+ );
840
+
841
+ const newAgent = new AgentLoop({
842
+ provider,
843
+ providerName,
844
+ model: arg,
845
+ tools: toolRegistry,
846
+ executor,
847
+ permissions,
848
+ maxTurns: config.maxTurns,
849
+ mode: (opts.mode as AgentMode) ?? "auto",
850
+ });
851
+
852
+ agentRef.current = newAgent;
853
+ console.log(` ${c.green}✓ Model: ${arg}${c.reset}`);
854
+ return { newModel: arg };
855
+ } catch (err) {
856
+ console.log(` ${c.red}✗ Model not found: ${arg}${c.reset}`);
857
+ }
858
+ } else {
859
+ console.log(` ${c.yellow}Model: ${currentModel}${c.reset}`);
860
+ }
861
+ break;
862
+
863
+ case "/provider":
864
+ if (arg) {
865
+ opts.provider = arg;
866
+ updateStatus("thinking", `Switching to ${arg}...`);
867
+ try {
868
+ const newProvider = providerRegistry.get(arg);
869
+
870
+ const toolRegistry = new ToolRegistry();
871
+ const builtinTools = createBuiltinTools();
872
+ builtinTools.forEach((t) => toolRegistry.register(t));
873
+
874
+ const permissions = new PermissionChecker({
875
+ autoApproveSafe: config.permissions.autoApproveSafe,
876
+ allow: config.permissions.allow,
877
+ deny: config.permissions.deny,
878
+ });
879
+
880
+ const executor = new ToolExecutor(
881
+ toolRegistry,
882
+ permissions,
883
+ askApproval,
884
+ );
885
+
886
+ const newAgent = new AgentLoop({
887
+ provider: newProvider,
888
+ providerName: arg,
889
+ model: opts.model ?? config.defaultModel ?? "mimo-v2.5-pro",
890
+ tools: toolRegistry,
891
+ executor,
892
+ permissions,
893
+ maxTurns: config.maxTurns,
894
+ mode: (opts.mode as AgentMode) ?? "auto",
895
+ });
896
+
897
+ agentRef.current = newAgent;
898
+ console.log(` ${c.green}✓ Provider: ${arg}${c.reset}`);
899
+ return { newProvider: arg };
900
+ } catch (err) {
901
+ console.log(` ${c.red}✗ Provider not found: ${arg}${c.reset}`);
902
+ }
903
+ } else {
904
+ console.log(` ${c.yellow}Provider: ${currentProvider}${c.reset}`);
905
+ }
906
+ break;
907
+
908
+ case "/mode":
909
+ if (arg && ["plan", "act", "auto", "review"].includes(arg)) {
910
+ opts.mode = arg;
911
+ agentRef.current.setMode(arg as AgentMode);
912
+ console.log(` ${c.green}✓ Mode: ${arg}${c.reset}`);
913
+ currentMode = arg;
914
+ updateStatus("idle", `Mode: ${arg}`);
915
+ } else {
916
+ console.log(` ${c.yellow}Mode: ${currentMode}${c.reset}`);
917
+ console.log(` ${c.dim}Options: plan, act, auto, review${c.reset}`);
918
+ }
919
+ break;
920
+
921
+ case "/clear":
922
+ console.clear();
923
+ printHeader();
924
+ break;
925
+
926
+ case "/help":
927
+ console.log(`\n ${c.cyan}Commands:${c.reset}`);
928
+ console.log(` ${c.white}/model <name>${c.reset} - Switch model`);
929
+ console.log(` ${c.white}/provider <name>${c.reset} - Switch provider`);
930
+ console.log(
931
+ ` ${c.white}/mode <name>${c.reset} - Set mode (plan/act/auto/review)`,
932
+ );
933
+ console.log(` ${c.white}/clear${c.reset} - Clear screen`);
934
+ console.log(` ${c.white}/help${c.reset} - Show this help`);
935
+ console.log(` ${c.white}/exit${c.reset} - Quit\n`);
936
+ break;
937
+
938
+ default:
939
+ console.log(` ${c.red}Unknown: ${command}${c.reset}`);
940
+ }
941
+ }
942
+
943
+ async function runSingle(prompt: string, options: AgentOptions = {}) {
944
+ const { agent } = createAgent(options);
945
+
946
+ const thinking = printThinking();
947
+ let isFirstChunk = true;
948
+ let tBuffer = "";
949
+ let thinkRendered = false;
950
+
951
+ try {
952
+ for await (const event of agent.run(prompt)) {
953
+ switch (event.type) {
954
+ case "text": {
955
+ const chunk = event.content ?? "";
956
+ if (isFirstChunk) {
957
+ thinking.stop();
958
+ isFirstChunk = false;
959
+ }
960
+ const combined = tBuffer + chunk;
961
+ const openIdx = combined.indexOf("<think>");
962
+ const closeIdx = combined.indexOf("</think>");
963
+ const hasComplete =
964
+ openIdx >= 0 && closeIdx >= 0 && closeIdx > openIdx + 6;
965
+ if (!thinkRendered && hasComplete) {
966
+ const thinkText = combined.slice(openIdx + 7, closeIdx).trim();
967
+ if (thinkText) displayThinkingBox(thinkText);
968
+ const after = combined.slice(closeIdx + 8).trimStart();
969
+ if (after) {
970
+ process.stdout.write(
971
+ `\n ${c.green}${c.bold}●${c.reset} `,
972
+ );
973
+ const clean3 = after.replace(/^\n+/, "");
974
+ const indented3 = clean3.includes("\n")
975
+ ? clean3.replace(/\n/g, "\n ")
976
+ : clean3;
977
+ process.stdout.write(indented3);
978
+ thinkRendered = true;
979
+ }
980
+ tBuffer = "";
981
+ break;
982
+ }
983
+ if (!thinkRendered && openIdx >= 0) {
984
+ tBuffer = combined;
985
+ break;
986
+ }
987
+ if (!thinkRendered) {
988
+ process.stdout.write(`\n ${c.green}${c.bold}●${c.reset} `);
989
+ thinkRendered = true;
990
+ }
991
+ const clean2 = combined.replace(/^\n+/, "");
992
+ const indented2 = clean2.includes("\n")
993
+ ? clean2.replace(/\n/g, "\n ")
994
+ : clean2;
995
+ process.stdout.write(indented2);
996
+ break;
997
+ }
998
+ case "thinking":
999
+ if (isFirstChunk) {
1000
+ thinking.stop();
1001
+ isFirstChunk = false;
1002
+ }
1003
+ const tc = (event.content ?? "").replace(/<\/?think>/gi, "").trim();
1004
+ if (tc) displayThinkingBox(tc);
1005
+ break;
1006
+ case "tool_use":
1007
+ case "tool_result":
1008
+ break;
1009
+ }
1010
+ }
1011
+ console.log("");
1012
+ } catch (err) {
1013
+ thinking.stop();
1014
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
1015
+ }
1016
+ }
1017
+
1018
+ // Subcommands first
1019
+ program
1020
+ .command("chat")
1021
+ .description("Start interactive chat mode")
1022
+ .option("-m, --model <model>", "Model to use")
1023
+ .option("-p, --provider <provider>", "Provider to use")
1024
+ .option("--mode <mode>", "Agent mode (plan/review/act/auto)", "auto")
1025
+ .option("--hybrid", "Enable hybrid mode")
1026
+ .action(async (options) => {
1027
+ await runInteractive(options);
1028
+ });
1029
+
1030
+ program
1031
+ .command("run <prompt>")
1032
+ .description("Run a single prompt and exit")
1033
+ .option("-m, --model <model>", "Model to use")
1034
+ .option("-p, --provider <provider>", "Provider to use")
1035
+ .option("--mode <mode>", "Agent mode (plan/review/act/auto)", "act")
1036
+ .option("--hybrid", "Enable hybrid mode")
1037
+ .action(async (prompt, options) => {
1038
+ await runSingle(prompt, options);
1039
+ process.exit(0);
1040
+ });
1041
+
1042
+ program
1043
+ .command("models")
1044
+ .description("List available models")
1045
+ .action(() => {
1046
+ console.log("");
1047
+ console.log(` ${c.cyan}${c.bold}Available Models:${c.reset}`);
1048
+ console.log("");
1049
+
1050
+ const config = loadConfig();
1051
+ for (const [provider, pConfig] of Object.entries(config.providers)) {
1052
+ console.log(` ${c.green}${c.bold}${provider}${c.reset}`);
1053
+ for (const model of Object.keys(pConfig.models)) {
1054
+ console.log(` ${c.white}• ${model}${c.reset}`);
1055
+ }
1056
+ console.log("");
1057
+ }
1058
+ });
1059
+
1060
+ program
1061
+ .command("doctor")
1062
+ .description("Check configuration and environment")
1063
+ .action(() => {
1064
+ console.log("");
1065
+ console.log(` ${c.cyan}${c.bold}HalfCopilot Doctor${c.reset}`);
1066
+ console.log("");
1067
+
1068
+ try {
1069
+ const config = loadConfig();
1070
+ console.log(` ${c.green}✓${c.reset} Configuration loaded`);
1071
+ console.log(
1072
+ ` ${c.green}✓${c.reset} Providers: ${Object.keys(config.providers).join(", ")}`,
1073
+ );
1074
+ console.log(
1075
+ ` ${c.green}✓${c.reset} Default: ${config.defaultProvider}/${config.defaultModel}`,
1076
+ );
1077
+
1078
+ const toolRegistry = new ToolRegistry();
1079
+ createBuiltinTools().forEach((t) => toolRegistry.register(t));
1080
+ console.log(
1081
+ ` ${c.green}✓${c.reset} Tools: ${toolRegistry.list().length} available`,
1082
+ );
1083
+
1084
+ const skillRegistry = new SkillRegistry();
1085
+ createBuiltinSkills().forEach((s) => skillRegistry.register(s));
1086
+ console.log(
1087
+ ` ${c.green}✓${c.reset} Skills: ${skillRegistry.list().length} available`,
1088
+ );
1089
+
1090
+ console.log("");
1091
+ console.log(` ${c.green}${c.bold}All checks passed! ✓${c.reset}`);
1092
+ console.log("");
1093
+ } catch (err) {
1094
+ console.log(
1095
+ ` ${c.red}✗ Error: ${err instanceof Error ? err.message : err}${c.reset}`,
1096
+ );
1097
+ }
1098
+ });
1099
+
1100
+ program
1101
+ .command("skills")
1102
+ .description("List available skills")
1103
+ .action(() => {
1104
+ const skillRegistry = new SkillRegistry();
1105
+ createBuiltinSkills().forEach((s) => skillRegistry.register(s));
1106
+
1107
+ console.log("");
1108
+ console.log(` ${c.cyan}${c.bold}Available Skills:${c.reset}`);
1109
+ console.log("");
1110
+
1111
+ for (const skill of skillRegistry.list()) {
1112
+ console.log(` ${c.green}• ${skill.name}${c.reset}`);
1113
+ console.log(` ${c.dim}${skill.description}${c.reset}`);
1114
+ }
1115
+ console.log("");
1116
+ });
1117
+
1118
+ program
1119
+ .command("setup")
1120
+ .description("Interactive setup — configure API keys for model providers")
1121
+ .action(async () => {
1122
+ const fs = await import("fs");
1123
+ const pathModule = await import("path");
1124
+ const os = await import("os");
1125
+
1126
+ const configDir = pathModule.join(os.homedir(), ".halfcopilot");
1127
+ const configFile = pathModule.join(configDir, "settings.json");
1128
+
1129
+ // Load existing or create default
1130
+ let config: Record<string, unknown> = {};
1131
+ if (fs.existsSync(configFile)) {
1132
+ config = JSON.parse(fs.readFileSync(configFile, "utf-8")) as Record<
1133
+ string,
1134
+ unknown
1135
+ >;
1136
+ }
1137
+ if (!config.providers) {
1138
+ config.providers = {};
1139
+ }
1140
+ const providersCfg = config.providers as Record<
1141
+ string,
1142
+ Record<string, unknown>
1143
+ >;
1144
+
1145
+ const rl = readline.createInterface({
1146
+ input: process.stdin,
1147
+ output: process.stdout,
1148
+ });
1149
+ const ask = (q: string) =>
1150
+ new Promise<string>((resolve) => rl.question(q, resolve));
1151
+
1152
+ // Print header
1153
+ console.log("");
1154
+ console.log(
1155
+ `${c.cyan}${c.bold} ╭─────────────────────────────────────────────────────╮${c.reset}`,
1156
+ );
1157
+ console.log(
1158
+ `${c.cyan}${c.bold} │ │${c.reset}`,
1159
+ );
1160
+ console.log(
1161
+ `${c.cyan}${c.bold} │ ⚙️ HalfCopilot Setup │${c.reset}`,
1162
+ );
1163
+ console.log(
1164
+ `${c.cyan}${c.bold} │ │${c.reset}`,
1165
+ );
1166
+ console.log(
1167
+ `${c.cyan}${c.bold} ╰─────────────────────────────────────────────────────╯${c.reset}`,
1168
+ );
1169
+ console.log("");
1170
+
1171
+ // Provider templates - updated with accurate endpoints
1172
+ const providers: Array<{
1173
+ name: string;
1174
+ label: string;
1175
+ baseUrl: string;
1176
+ models: string[];
1177
+ desc: string;
1178
+ }> = [
1179
+ {
1180
+ name: "minimax",
1181
+ label: "MiniMax",
1182
+ desc: "M2.7 / M2.5 — 海螺AI同款",
1183
+ baseUrl: "https://api.minimaxi.com/v1",
1184
+ models: ["MiniMax-M2.7", "MiniMax-M2.5"],
1185
+ },
1186
+ {
1187
+ name: "xiaomi",
1188
+ label: "小米 MiMo",
1189
+ desc: "Token Plan API",
1190
+ baseUrl: "https://token-plan-cn.xiaomimimo.com/v1",
1191
+ models: ["mimo-v2.5-pro", "mimo-v2.5"],
1192
+ },
1193
+ {
1194
+ name: "deepseek",
1195
+ label: "DeepSeek",
1196
+ desc: "高性价比,深度推理",
1197
+ baseUrl: "https://api.deepseek.com/v1",
1198
+ models: ["deepseek-chat", "deepseek-reasoner"],
1199
+ },
1200
+ {
1201
+ name: "qwen",
1202
+ label: "通义千问 Qwen",
1203
+ desc: "阿里云出品",
1204
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
1205
+ models: ["qwen-turbo", "qwen-plus"],
1206
+ },
1207
+ {
1208
+ name: "openai",
1209
+ label: "OpenAI",
1210
+ desc: "GPT-4o / GPT-4o-mini",
1211
+ baseUrl: "https://api.openai.com/v1",
1212
+ models: ["gpt-4o", "gpt-4o-mini"],
1213
+ },
1214
+ {
1215
+ name: "anthropic",
1216
+ label: "Anthropic Claude",
1217
+ desc: "Claude Sonnet 4",
1218
+ baseUrl: "",
1219
+ models: ["claude-sonnet-4-20250514"],
1220
+ },
1221
+ ];
1222
+
1223
+ console.log(
1224
+ ` ${c.dim}选择你要配置的模型厂商,输入 API Key 即可。${c.reset}`,
1225
+ );
1226
+ console.log("");
1227
+ console.log(` ${c.cyan}${c.bold}📦 可用厂商:${c.reset}`);
1228
+ console.log("");
1229
+
1230
+ for (let i = 0; i < providers.length; i++) {
1231
+ const p = providers[i];
1232
+ const configured = providersCfg[p.name]
1233
+ ? ` ${c.green}(已配置)${c.reset}`
1234
+ : "";
1235
+ console.log(
1236
+ ` ${c.bold}${i + 1}${c.reset}. ${c.white}${p.label}${c.reset} — ${c.dim}${p.desc}${c.reset}${configured}`,
1237
+ );
1238
+ }
1239
+ console.log(` ${c.bold}0${c.reset}. ${c.dim}完成配置,退出${c.reset}`);
1240
+ console.log("");
1241
+
1242
+ let selectedIdx = -1;
1243
+
1244
+ // Keyboard navigation (basic, using enter)
1245
+ const choice = await ask(
1246
+ ` ${c.green}选择你要配置的厂商 (0-${providers.length}): ${c.reset}`,
1247
+ );
1248
+ selectedIdx = parseInt(choice.trim()) - 1;
1249
+
1250
+ if (
1251
+ isNaN(selectedIdx) ||
1252
+ selectedIdx < -1 ||
1253
+ selectedIdx >= providers.length
1254
+ ) {
1255
+ console.log(` ${c.red}无效选择${c.reset}`);
1256
+ rl.close();
1257
+ return;
1258
+ }
1259
+
1260
+ if (selectedIdx === -1) {
1261
+ console.log(` ${c.yellow}配置完成!${c.reset}`);
1262
+ rl.close();
1263
+ return;
1264
+ }
1265
+
1266
+ const selected = providers[selectedIdx];
1267
+
1268
+ console.log("");
1269
+ console.log(` ${c.cyan}配置 ${selected.label}${c.reset}`);
1270
+
1271
+ let apiKey: string;
1272
+
1273
+ if (selected.name === "xiaomi") {
1274
+ console.log("");
1275
+ console.log(` ${c.dim}小米 Token Plan API Key 示例格式:${c.reset}`);
1276
+ console.log(` ${c.dim}tp-xxxxxxxxxx...${c.reset}`);
1277
+ }
1278
+
1279
+ if (selected.name === "minimax") {
1280
+ console.log("");
1281
+ console.log(` ${c.dim}MiniMax API Key (来自 minimaxi.com):${c.reset}`);
1282
+ }
1283
+
1284
+ if (selected.name === "deepseek") {
1285
+ console.log("");
1286
+ console.log(
1287
+ ` ${c.dim}DeepSeek API Key (来自 api.deepseek.com):${c.reset}`,
1288
+ );
1289
+ }
1290
+
1291
+ apiKey = await ask(` ${c.green}API Key: ${c.reset}`);
1292
+
1293
+ if (!apiKey.trim()) {
1294
+ console.log(` ${c.yellow}已跳过${c.reset}`);
1295
+ rl.close();
1296
+ return;
1297
+ }
1298
+
1299
+ // Build models object with proper context windows
1300
+ const modelConfigs: Record<
1301
+ string,
1302
+ { contextWindow: number; maxOutput: number }
1303
+ > = {};
1304
+
1305
+ if (selected.name === "minimax") {
1306
+ modelConfigs["MiniMax-M2.7"] = {
1307
+ contextWindow: 128000,
1308
+ maxOutput: 16384,
1309
+ };
1310
+ modelConfigs["MiniMax-M2.5"] = {
1311
+ contextWindow: 128000,
1312
+ maxOutput: 16384,
1313
+ };
1314
+ } else if (selected.name === "deepseek") {
1315
+ modelConfigs["deepseek-chat"] = { contextWindow: 64000, maxOutput: 8192 };
1316
+ modelConfigs["deepseek-reasoner"] = {
1317
+ contextWindow: 64000,
1318
+ maxOutput: 8192,
1319
+ };
1320
+ } else if (selected.name === "xiaomi") {
1321
+ modelConfigs["mimo-v2.5-pro"] = {
1322
+ contextWindow: 128000,
1323
+ maxOutput: 16384,
1324
+ };
1325
+ modelConfigs["mimo-v2.5"] = { contextWindow: 128000, maxOutput: 16384 };
1326
+ } else {
1327
+ for (const m of selected.models) {
1328
+ modelConfigs[m] = { contextWindow: 128000, maxOutput: 8192 };
1329
+ }
1330
+ }
1331
+
1332
+ // Save provider config
1333
+ providersCfg[selected.name] = {
1334
+ type: selected.name === "anthropic" ? "anthropic" : "openai-compatible",
1335
+ ...(selected.baseUrl ? { baseUrl: selected.baseUrl } : {}),
1336
+ apiKey,
1337
+ models: modelConfigs,
1338
+ };
1339
+
1340
+ // Set as default if no default set
1341
+ const configAny = config as Record<string, unknown>;
1342
+ if (!configAny.defaultProvider) {
1343
+ configAny.defaultProvider = selected.name;
1344
+ configAny.defaultModel = selected.models[0];
1345
+ }
1346
+
1347
+ // Write config
1348
+ fs.mkdirSync(configDir, { recursive: true });
1349
+ fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
1350
+
1351
+ console.log("");
1352
+ console.log(
1353
+ ` ${c.green}${c.bold}✅ ${selected.label} 配置成功!${c.reset}`,
1354
+ );
1355
+ console.log(` ${c.dim}配置文件: ${configFile}${c.reset}`);
1356
+ console.log(
1357
+ ` ${c.dim}模型: ${Object.keys(modelConfigs).join(", ")}${c.reset}`,
1358
+ );
1359
+
1360
+ if (configAny.defaultProvider === selected.name) {
1361
+ console.log(` ${c.green}已设为默认厂商${c.reset}`);
1362
+ }
1363
+ console.log("");
1364
+
1365
+ rl.close();
1366
+ });
1367
+
1368
+ // Default action: when no command given, start interactive chat
1369
+ program.action(async () => {
1370
+ await runInteractive({});
1371
+ });
1372
+
1373
+ program.parse();