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