shenxiang-ai-cli 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.
Files changed (2) hide show
  1. package/dist/index.js +2813 -0
  2. package/package.json +54 -0
package/dist/index.js ADDED
@@ -0,0 +1,2813 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/chat.ts
7
+ import readline from "readline";
8
+
9
+ // src/core/providers/openai-compatible.ts
10
+ import OpenAI from "openai";
11
+ var OpenAICompatibleProvider = class {
12
+ name;
13
+ constructor(name) {
14
+ this.name = name;
15
+ }
16
+ async *chatStream(messages, tools, config3) {
17
+ const isKimi = this.name === "kimi";
18
+ if (isKimi) {
19
+ yield* this.kimiChatStream(messages, tools, config3);
20
+ return;
21
+ }
22
+ const client = new OpenAI({
23
+ apiKey: config3.apiKey,
24
+ baseURL: config3.baseUrl
25
+ });
26
+ const openaiTools = tools.map((t2) => ({
27
+ type: "function",
28
+ function: {
29
+ name: t2.name,
30
+ description: t2.description,
31
+ parameters: t2.parameters
32
+ }
33
+ }));
34
+ const openaiMessages = messages.map((m) => {
35
+ if (m.role === "tool") {
36
+ return {
37
+ role: "tool",
38
+ content: m.content || "",
39
+ tool_call_id: m.tool_call_id || ""
40
+ };
41
+ }
42
+ if (m.role === "assistant" && m.tool_calls) {
43
+ return {
44
+ role: "assistant",
45
+ content: m.content,
46
+ tool_calls: m.tool_calls.map((tc) => ({
47
+ id: tc.id,
48
+ type: "function",
49
+ function: {
50
+ name: tc.function.name,
51
+ arguments: tc.function.arguments
52
+ }
53
+ }))
54
+ };
55
+ }
56
+ return {
57
+ role: m.role,
58
+ content: m.content || ""
59
+ };
60
+ });
61
+ try {
62
+ const stream = await client.chat.completions.create({
63
+ model: config3.model,
64
+ messages: openaiMessages,
65
+ tools: openaiTools.length > 0 ? openaiTools : void 0,
66
+ stream: true,
67
+ stream_options: { include_usage: true },
68
+ temperature: 0.3,
69
+ max_tokens: 8192
70
+ });
71
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
72
+ let tokenUsage;
73
+ for await (const chunk of stream) {
74
+ if (chunk.usage) {
75
+ tokenUsage = {
76
+ promptTokens: chunk.usage.prompt_tokens || 0,
77
+ completionTokens: chunk.usage.completion_tokens || 0,
78
+ totalTokens: chunk.usage.total_tokens || 0
79
+ };
80
+ }
81
+ const choice = chunk.choices[0];
82
+ if (!choice) continue;
83
+ const delta = choice.delta;
84
+ if (delta.content) {
85
+ yield { type: "text", content: delta.content };
86
+ }
87
+ if (delta.tool_calls) {
88
+ for (const tc of delta.tool_calls) {
89
+ const idx = tc.index;
90
+ if (!toolCallAccumulator.has(idx)) {
91
+ toolCallAccumulator.set(idx, { id: tc.id || "", functionName: tc.function?.name || "", arguments: "" });
92
+ }
93
+ const acc = toolCallAccumulator.get(idx);
94
+ if (tc.id) acc.id = tc.id;
95
+ if (tc.function?.name) acc.functionName = tc.function.name;
96
+ if (tc.function?.arguments) acc.arguments += tc.function.arguments;
97
+ }
98
+ }
99
+ if (choice.finish_reason === "tool_calls" || choice.finish_reason === "stop") {
100
+ for (const [_, acc] of toolCallAccumulator) {
101
+ yield { type: "tool_call", toolCall: { id: acc.id, type: "function", function: { name: acc.functionName, arguments: acc.arguments } } };
102
+ }
103
+ yield { type: "done", usage: tokenUsage };
104
+ return;
105
+ }
106
+ }
107
+ for (const [_, acc] of toolCallAccumulator) {
108
+ yield { type: "tool_call", toolCall: { id: acc.id, type: "function", function: { name: acc.functionName, arguments: acc.arguments } } };
109
+ }
110
+ yield { type: "done", usage: tokenUsage };
111
+ } catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ yield { type: "error", error: msg };
114
+ }
115
+ }
116
+ /**
117
+ * Kimi-specific streaming using raw fetch to handle reasoning_content
118
+ */
119
+ async *kimiChatStream(messages, tools, config3) {
120
+ const baseUrl = config3.baseUrl || "https://api.moonshot.cn/v1";
121
+ const kimiMessages = messages.map((m) => {
122
+ if (m.role === "tool") {
123
+ return {
124
+ role: "tool",
125
+ content: m.content || "",
126
+ tool_call_id: m.tool_call_id || ""
127
+ };
128
+ }
129
+ if (m.role === "assistant" && m.tool_calls) {
130
+ return {
131
+ role: "assistant",
132
+ content: m.content || "",
133
+ reasoning_content: m.reasoning_content || "",
134
+ tool_calls: m.tool_calls.map((tc) => ({
135
+ id: tc.id,
136
+ type: "function",
137
+ function: {
138
+ name: tc.function.name,
139
+ arguments: tc.function.arguments
140
+ }
141
+ }))
142
+ };
143
+ }
144
+ if (m.role === "assistant") {
145
+ const msg = {
146
+ role: "assistant",
147
+ content: m.content || ""
148
+ };
149
+ if (m.reasoning_content !== void 0) {
150
+ msg.reasoning_content = m.reasoning_content;
151
+ }
152
+ return msg;
153
+ }
154
+ return {
155
+ role: m.role,
156
+ content: m.content || ""
157
+ };
158
+ });
159
+ const kimiTools = tools.map((t2) => ({
160
+ type: "function",
161
+ function: {
162
+ name: t2.name,
163
+ description: t2.description,
164
+ parameters: t2.parameters
165
+ }
166
+ }));
167
+ const body = {
168
+ model: config3.model,
169
+ messages: kimiMessages,
170
+ stream: true,
171
+ stream_options: { include_usage: true },
172
+ temperature: 1,
173
+ max_tokens: 8192
174
+ };
175
+ if (kimiTools.length > 0) {
176
+ body.tools = kimiTools;
177
+ }
178
+ try {
179
+ const response = await fetch(`${baseUrl}/chat/completions`, {
180
+ method: "POST",
181
+ headers: {
182
+ "Content-Type": "application/json",
183
+ "Authorization": `Bearer ${config3.apiKey}`
184
+ },
185
+ body: JSON.stringify(body)
186
+ });
187
+ if (!response.ok) {
188
+ const errText = await response.text();
189
+ yield { type: "error", error: `${response.status} ${errText}` };
190
+ return;
191
+ }
192
+ const reader = response.body?.getReader();
193
+ if (!reader) {
194
+ yield { type: "error", error: "No response stream" };
195
+ return;
196
+ }
197
+ const decoder = new TextDecoder();
198
+ let buffer = "";
199
+ const toolCallAccumulator = /* @__PURE__ */ new Map();
200
+ let tokenUsage;
201
+ let reasoningContent = "";
202
+ while (true) {
203
+ const { done, value } = await reader.read();
204
+ if (done) break;
205
+ buffer += decoder.decode(value, { stream: true });
206
+ const lines = buffer.split("\n");
207
+ buffer = lines.pop() || "";
208
+ for (const line of lines) {
209
+ const trimmed = line.trim();
210
+ if (!trimmed.startsWith("data: ")) continue;
211
+ const data = trimmed.slice(6);
212
+ if (data === "[DONE]") continue;
213
+ let parsed;
214
+ try {
215
+ parsed = JSON.parse(data);
216
+ } catch {
217
+ continue;
218
+ }
219
+ if (parsed.usage) {
220
+ tokenUsage = {
221
+ promptTokens: parsed.usage.prompt_tokens || 0,
222
+ completionTokens: parsed.usage.completion_tokens || 0,
223
+ totalTokens: parsed.usage.total_tokens || 0
224
+ };
225
+ }
226
+ const choice = parsed.choices?.[0];
227
+ if (!choice) continue;
228
+ const delta = choice.delta || {};
229
+ if (delta.reasoning_content) {
230
+ reasoningContent += delta.reasoning_content;
231
+ }
232
+ if (delta.content) {
233
+ yield { type: "text", content: delta.content };
234
+ }
235
+ if (delta.tool_calls) {
236
+ for (const tc of delta.tool_calls) {
237
+ const idx = tc.index ?? 0;
238
+ if (!toolCallAccumulator.has(idx)) {
239
+ toolCallAccumulator.set(idx, { id: tc.id || "", functionName: tc.function?.name || "", arguments: "" });
240
+ }
241
+ const acc = toolCallAccumulator.get(idx);
242
+ if (tc.id) acc.id = tc.id;
243
+ if (tc.function?.name) acc.functionName = tc.function.name;
244
+ if (tc.function?.arguments) acc.arguments += tc.function.arguments;
245
+ }
246
+ }
247
+ if (choice.finish_reason === "tool_calls" || choice.finish_reason === "stop") {
248
+ for (const [_, acc] of toolCallAccumulator) {
249
+ const toolCall = {
250
+ id: acc.id,
251
+ type: "function",
252
+ function: { name: acc.functionName, arguments: acc.arguments }
253
+ };
254
+ yield { type: "tool_call", toolCall };
255
+ }
256
+ yield {
257
+ type: "done",
258
+ usage: tokenUsage,
259
+ content: reasoningContent || void 0
260
+ };
261
+ return;
262
+ }
263
+ }
264
+ }
265
+ for (const [_, acc] of toolCallAccumulator) {
266
+ yield { type: "tool_call", toolCall: { id: acc.id, type: "function", function: { name: acc.functionName, arguments: acc.arguments } } };
267
+ }
268
+ yield { type: "done", usage: tokenUsage, content: reasoningContent || void 0 };
269
+ } catch (err) {
270
+ const msg = err instanceof Error ? err.message : String(err);
271
+ yield { type: "error", error: msg };
272
+ }
273
+ }
274
+ };
275
+
276
+ // src/core/providers/anthropic.ts
277
+ var AnthropicProvider = class {
278
+ name = "anthropic";
279
+ async *chatStream(messages, tools, config3) {
280
+ const baseUrl = config3.baseUrl || "https://api.anthropic.com";
281
+ const systemMessage = messages.find((m) => m.role === "system");
282
+ const nonSystemMessages = messages.filter((m) => m.role !== "system");
283
+ const anthropicMessages = nonSystemMessages.map((m) => {
284
+ if (m.role === "assistant" && m.tool_calls) {
285
+ return {
286
+ role: "assistant",
287
+ content: [
288
+ ...m.content ? [{ type: "text", text: m.content }] : [],
289
+ ...m.tool_calls.map((tc) => ({
290
+ type: "tool_use",
291
+ id: tc.id,
292
+ name: tc.function.name,
293
+ input: JSON.parse(tc.function.arguments)
294
+ }))
295
+ ]
296
+ };
297
+ }
298
+ if (m.role === "tool") {
299
+ return {
300
+ role: "user",
301
+ content: [{
302
+ type: "tool_result",
303
+ tool_use_id: m.tool_call_id,
304
+ content: m.content || ""
305
+ }]
306
+ };
307
+ }
308
+ return { role: m.role, content: m.content || "" };
309
+ });
310
+ const anthropicTools = tools.map((t2) => ({
311
+ name: t2.name,
312
+ description: t2.description,
313
+ input_schema: t2.parameters
314
+ }));
315
+ const body = {
316
+ model: config3.model,
317
+ max_tokens: 8192,
318
+ system: systemMessage?.content || "",
319
+ messages: anthropicMessages,
320
+ tools: anthropicTools.length > 0 ? anthropicTools : void 0,
321
+ stream: true,
322
+ temperature: 0.3
323
+ };
324
+ try {
325
+ const response = await fetch(`${baseUrl}/v1/messages`, {
326
+ method: "POST",
327
+ headers: {
328
+ "Content-Type": "application/json",
329
+ "x-api-key": config3.apiKey,
330
+ "anthropic-version": "2023-06-01"
331
+ },
332
+ body: JSON.stringify(body)
333
+ });
334
+ if (!response.ok) {
335
+ const errorText = await response.text();
336
+ yield { type: "error", error: `Anthropic API error (${response.status}): ${errorText}` };
337
+ return;
338
+ }
339
+ const reader = response.body?.getReader();
340
+ if (!reader) {
341
+ yield { type: "error", error: "No response stream available" };
342
+ return;
343
+ }
344
+ const decoder = new TextDecoder();
345
+ let buffer = "";
346
+ let currentToolId = "";
347
+ let currentToolName = "";
348
+ let currentToolArgs = "";
349
+ while (true) {
350
+ const { done, value } = await reader.read();
351
+ if (done) break;
352
+ buffer += decoder.decode(value, { stream: true });
353
+ const lines = buffer.split("\n");
354
+ buffer = lines.pop() || "";
355
+ for (const line of lines) {
356
+ if (!line.startsWith("data: ")) continue;
357
+ const data = line.slice(6).trim();
358
+ if (data === "[DONE]") continue;
359
+ try {
360
+ const event = JSON.parse(data);
361
+ if (event.type === "content_block_start") {
362
+ if (event.content_block?.type === "tool_use") {
363
+ currentToolId = event.content_block.id;
364
+ currentToolName = event.content_block.name;
365
+ currentToolArgs = "";
366
+ }
367
+ } else if (event.type === "content_block_delta") {
368
+ if (event.delta?.type === "text_delta") {
369
+ yield { type: "text", content: event.delta.text };
370
+ } else if (event.delta?.type === "input_json_delta") {
371
+ currentToolArgs += event.delta.partial_json || "";
372
+ }
373
+ } else if (event.type === "content_block_stop") {
374
+ if (currentToolId) {
375
+ const toolCall = {
376
+ id: currentToolId,
377
+ type: "function",
378
+ function: {
379
+ name: currentToolName,
380
+ arguments: currentToolArgs || "{}"
381
+ }
382
+ };
383
+ yield { type: "tool_call", toolCall };
384
+ currentToolId = "";
385
+ currentToolName = "";
386
+ currentToolArgs = "";
387
+ }
388
+ } else if (event.type === "message_stop") {
389
+ yield { type: "done" };
390
+ return;
391
+ }
392
+ } catch {
393
+ }
394
+ }
395
+ }
396
+ yield { type: "done" };
397
+ } catch (err) {
398
+ const msg = err instanceof Error ? err.message : String(err);
399
+ yield { type: "error", error: msg };
400
+ }
401
+ }
402
+ };
403
+
404
+ // src/core/providers/types.ts
405
+ var MODEL_PROVIDERS = {
406
+ // Kimi (月之暗面 Moonshot)
407
+ "kimi-k2.5": { provider: "kimi", displayName: "Kimi K2.5 (\u6700\u65B0\u591A\u6A21\u6001)" },
408
+ "kimi-k2-0905-preview": { provider: "kimi", displayName: "Kimi K2" },
409
+ "moonshot-v1-auto": { provider: "kimi", displayName: "Moonshot v1 Auto" },
410
+ // OpenAI
411
+ "gpt-4o": { provider: "openai", displayName: "GPT-4o" },
412
+ "gpt-4o-mini": { provider: "openai", displayName: "GPT-4o Mini" },
413
+ // Anthropic
414
+ "claude-sonnet-4-20250514": { provider: "anthropic", displayName: "Claude Sonnet 4" },
415
+ "claude-3-5-sonnet-20241022": { provider: "anthropic", displayName: "Claude 3.5 Sonnet" },
416
+ // DeepSeek
417
+ "deepseek-chat": { provider: "deepseek", displayName: "DeepSeek Chat" },
418
+ "deepseek-coder": { provider: "deepseek", displayName: "DeepSeek Coder" },
419
+ // Qwen (通义千问)
420
+ "qwen-turbo": { provider: "qwen", displayName: "\u901A\u4E49\u5343\u95EE Turbo" },
421
+ "qwen-plus": { provider: "qwen", displayName: "\u901A\u4E49\u5343\u95EE Plus" }
422
+ };
423
+
424
+ // src/core/providers/index.ts
425
+ var providers = {
426
+ openai: new OpenAICompatibleProvider("openai"),
427
+ deepseek: new OpenAICompatibleProvider("deepseek"),
428
+ qwen: new OpenAICompatibleProvider("qwen"),
429
+ kimi: new OpenAICompatibleProvider("kimi"),
430
+ anthropic: new AnthropicProvider()
431
+ };
432
+ var DEFAULT_BASE_URLS = {
433
+ openai: "https://api.openai.com/v1",
434
+ deepseek: "https://api.deepseek.com",
435
+ qwen: "https://dashscope.aliyuncs.com/compatible-mode/v1",
436
+ kimi: "https://api.moonshot.cn/v1",
437
+ anthropic: "https://api.anthropic.com"
438
+ };
439
+ function getProvider(model) {
440
+ const info = MODEL_PROVIDERS[model];
441
+ if (!info) {
442
+ return providers.openai;
443
+ }
444
+ return providers[info.provider] || providers.openai;
445
+ }
446
+ function getProviderName(model) {
447
+ return MODEL_PROVIDERS[model]?.provider || "openai";
448
+ }
449
+ function getDefaultBaseUrl(providerName) {
450
+ return DEFAULT_BASE_URLS[providerName] || DEFAULT_BASE_URLS.openai;
451
+ }
452
+ function listModels() {
453
+ return Object.entries(MODEL_PROVIDERS).map(([id, info]) => ({
454
+ id,
455
+ name: info.displayName,
456
+ provider: info.provider
457
+ }));
458
+ }
459
+
460
+ // src/core/tools/file-read.ts
461
+ import fs from "fs/promises";
462
+ import path from "path";
463
+ import { glob } from "glob";
464
+ var readFileTool = {
465
+ definition: {
466
+ name: "read_file",
467
+ description: "Read the contents of a file. Returns the file content with line numbers.",
468
+ parameters: {
469
+ type: "object",
470
+ properties: {
471
+ path: {
472
+ type: "string",
473
+ description: "Relative path to the file from the project root"
474
+ },
475
+ start_line: {
476
+ type: "number",
477
+ description: "Start line number (1-based). Optional."
478
+ },
479
+ end_line: {
480
+ type: "number",
481
+ description: "End line number (1-based, inclusive). Optional."
482
+ }
483
+ },
484
+ required: ["path"]
485
+ }
486
+ },
487
+ handler: async (args) => {
488
+ try {
489
+ const filePath = path.resolve(process.cwd(), args.path);
490
+ const stat = await fs.stat(filePath);
491
+ if (stat.size > 1024 * 1024) {
492
+ return {
493
+ success: false,
494
+ output: "",
495
+ error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Use start_line/end_line to read a portion.`
496
+ };
497
+ }
498
+ const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
499
+ ".png",
500
+ ".jpg",
501
+ ".jpeg",
502
+ ".gif",
503
+ ".ico",
504
+ ".webp",
505
+ ".svg",
506
+ ".woff",
507
+ ".woff2",
508
+ ".ttf",
509
+ ".eot",
510
+ ".zip",
511
+ ".tar",
512
+ ".gz",
513
+ ".7z",
514
+ ".rar",
515
+ ".exe",
516
+ ".dll",
517
+ ".so",
518
+ ".dylib",
519
+ ".pdf",
520
+ ".doc",
521
+ ".docx",
522
+ ".xls",
523
+ ".xlsx",
524
+ ".mp3",
525
+ ".mp4",
526
+ ".wav",
527
+ ".avi",
528
+ ".mov"
529
+ ]);
530
+ const ext = path.extname(filePath).toLowerCase();
531
+ if (BINARY_EXTENSIONS.has(ext)) {
532
+ return {
533
+ success: false,
534
+ output: "",
535
+ error: `Binary file (${ext}) cannot be read as text.`
536
+ };
537
+ }
538
+ const content = await fs.readFile(filePath, "utf-8");
539
+ const lines = content.split("\n");
540
+ const start = Math.max(1, args.start_line || 1);
541
+ const end = Math.min(lines.length, args.end_line || lines.length);
542
+ const effectiveEnd = !args.start_line && !args.end_line && lines.length > 500 ? 500 : end;
543
+ const numbered = lines.slice(start - 1, effectiveEnd).map((line, i) => `${String(start + i).padStart(5)}| ${line}`).join("\n");
544
+ let output = `File: ${args.path} (${lines.length} lines)
545
+ ${numbered}`;
546
+ if (effectiveEnd < lines.length && !args.end_line) {
547
+ output += `
548
+ ... (${lines.length - effectiveEnd} more lines, use start_line/end_line to read more)`;
549
+ }
550
+ return { success: true, output };
551
+ } catch (err) {
552
+ const msg = err instanceof Error ? err.message : String(err);
553
+ if (msg.includes("ENOENT")) {
554
+ return { success: false, output: "", error: `File not found: ${args.path}` };
555
+ }
556
+ if (msg.includes("EACCES")) {
557
+ return { success: false, output: "", error: `Permission denied: ${args.path}` };
558
+ }
559
+ return { success: false, output: "", error: `Failed to read file: ${msg}` };
560
+ }
561
+ },
562
+ requiresConfirmation: false
563
+ };
564
+ var listFilesTool = {
565
+ definition: {
566
+ name: "list_files",
567
+ description: "List files and directories in a path. Returns a tree-like structure.",
568
+ parameters: {
569
+ type: "object",
570
+ properties: {
571
+ path: {
572
+ type: "string",
573
+ description: 'Relative directory path to list. Defaults to "."'
574
+ },
575
+ pattern: {
576
+ type: "string",
577
+ description: 'Glob pattern to filter files. E.g. "**/*.ts"'
578
+ },
579
+ max_depth: {
580
+ type: "number",
581
+ description: "Maximum directory depth. Default 3."
582
+ }
583
+ },
584
+ required: []
585
+ }
586
+ },
587
+ handler: async (args) => {
588
+ try {
589
+ const targetPath = path.resolve(process.cwd(), args.path || ".");
590
+ const pattern = args.pattern || "**/*";
591
+ const maxDepth = args.max_depth || 3;
592
+ const files = await glob(pattern, {
593
+ cwd: targetPath,
594
+ maxDepth,
595
+ ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**", ".nuxt/**", "coverage/**"],
596
+ mark: true
597
+ });
598
+ if (files.length === 0) {
599
+ return { success: true, output: "No files found matching the pattern." };
600
+ }
601
+ const sorted = files.sort();
602
+ const output = `Directory: ${args.path || "."}
603
+ Files (${sorted.length}):
604
+ ${sorted.map((f) => ` ${f}`).join("\n")}`;
605
+ return { success: true, output };
606
+ } catch (err) {
607
+ const msg = err instanceof Error ? err.message : String(err);
608
+ return { success: false, output: "", error: `Failed to list files: ${msg}` };
609
+ }
610
+ },
611
+ requiresConfirmation: false
612
+ };
613
+
614
+ // src/core/tools/file-write.ts
615
+ import fs2 from "fs/promises";
616
+ import path2 from "path";
617
+ var writeFileTool = {
618
+ definition: {
619
+ name: "write_file",
620
+ description: "Create or overwrite a file with the given content. Parent directories are created automatically.",
621
+ parameters: {
622
+ type: "object",
623
+ properties: {
624
+ path: {
625
+ type: "string",
626
+ description: "Relative path to the file from the project root"
627
+ },
628
+ content: {
629
+ type: "string",
630
+ description: "The full content to write to the file"
631
+ }
632
+ },
633
+ required: ["path", "content"]
634
+ }
635
+ },
636
+ handler: async (args) => {
637
+ try {
638
+ const filePath = path2.resolve(process.cwd(), args.path);
639
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true });
640
+ await fs2.writeFile(filePath, args.content, "utf-8");
641
+ const lines = args.content.split("\n").length;
642
+ return {
643
+ success: true,
644
+ output: `File written: ${args.path} (${lines} lines)`
645
+ };
646
+ } catch (err) {
647
+ const msg = err instanceof Error ? err.message : String(err);
648
+ return { success: false, output: "", error: `Failed to write file: ${msg}` };
649
+ }
650
+ },
651
+ requiresConfirmation: true
652
+ };
653
+ var editFileTool = {
654
+ definition: {
655
+ name: "edit_file",
656
+ description: "Edit a file by replacing a specific string with new content. The old_string must match exactly (including whitespace and indentation).",
657
+ parameters: {
658
+ type: "object",
659
+ properties: {
660
+ path: {
661
+ type: "string",
662
+ description: "Relative path to the file"
663
+ },
664
+ old_string: {
665
+ type: "string",
666
+ description: "The exact string to find and replace"
667
+ },
668
+ new_string: {
669
+ type: "string",
670
+ description: "The replacement string"
671
+ }
672
+ },
673
+ required: ["path", "old_string", "new_string"]
674
+ }
675
+ },
676
+ handler: async (args) => {
677
+ try {
678
+ const filePath = path2.resolve(process.cwd(), args.path);
679
+ const content = await fs2.readFile(filePath, "utf-8");
680
+ const oldStr = args.old_string;
681
+ const newStr = args.new_string;
682
+ if (!content.includes(oldStr)) {
683
+ return {
684
+ success: false,
685
+ output: "",
686
+ error: `String not found in file. Make sure old_string matches exactly.`
687
+ };
688
+ }
689
+ const count = content.split(oldStr).length - 1;
690
+ if (count > 1) {
691
+ return {
692
+ success: false,
693
+ output: "",
694
+ error: `Found ${count} occurrences of old_string. Please provide more context to uniquely identify the target.`
695
+ };
696
+ }
697
+ const newContent = content.replace(oldStr, newStr);
698
+ await fs2.writeFile(filePath, newContent, "utf-8");
699
+ const addedLines = newStr.split("\n").length;
700
+ const removedLines = oldStr.split("\n").length;
701
+ return {
702
+ success: true,
703
+ output: `File edited: ${args.path} (+${addedLines} -${removedLines} lines)`
704
+ };
705
+ } catch (err) {
706
+ const msg = err instanceof Error ? err.message : String(err);
707
+ return { success: false, output: "", error: `Failed to edit file: ${msg}` };
708
+ }
709
+ },
710
+ requiresConfirmation: true
711
+ };
712
+ var deleteFileTool = {
713
+ definition: {
714
+ name: "delete_file",
715
+ description: "Delete a file from the project.",
716
+ parameters: {
717
+ type: "object",
718
+ properties: {
719
+ path: {
720
+ type: "string",
721
+ description: "Relative path to the file to delete"
722
+ }
723
+ },
724
+ required: ["path"]
725
+ }
726
+ },
727
+ handler: async (args) => {
728
+ try {
729
+ const filePath = path2.resolve(process.cwd(), args.path);
730
+ await fs2.unlink(filePath);
731
+ return { success: true, output: `File deleted: ${args.path}` };
732
+ } catch (err) {
733
+ const msg = err instanceof Error ? err.message : String(err);
734
+ return { success: false, output: "", error: `Failed to delete file: ${msg}` };
735
+ }
736
+ },
737
+ requiresConfirmation: true
738
+ };
739
+
740
+ // src/core/tools/search.ts
741
+ import fs3 from "fs/promises";
742
+ import path3 from "path";
743
+ import { glob as glob2 } from "glob";
744
+ var searchCodeTool = {
745
+ definition: {
746
+ name: "search_code",
747
+ description: "Search for a pattern in project files. Returns matching lines with context. Similar to grep/ripgrep.",
748
+ parameters: {
749
+ type: "object",
750
+ properties: {
751
+ pattern: {
752
+ type: "string",
753
+ description: "Text or regex pattern to search for"
754
+ },
755
+ file_pattern: {
756
+ type: "string",
757
+ description: 'Glob pattern to filter files. E.g. "**/*.ts", "src/**/*.vue". Defaults to all files.'
758
+ },
759
+ case_sensitive: {
760
+ type: "string",
761
+ description: 'Whether search is case sensitive. "true" or "false". Default "true".'
762
+ }
763
+ },
764
+ required: ["pattern"]
765
+ }
766
+ },
767
+ handler: async (args) => {
768
+ try {
769
+ const searchPattern = args.pattern;
770
+ const filePattern = args.file_pattern || "**/*";
771
+ const caseSensitive = args.case_sensitive !== "false";
772
+ const files = await glob2(filePattern, {
773
+ cwd: process.cwd(),
774
+ nodir: true,
775
+ ignore: [
776
+ "node_modules/**",
777
+ ".git/**",
778
+ "dist/**",
779
+ ".next/**",
780
+ ".nuxt/**",
781
+ "coverage/**",
782
+ "*.lock",
783
+ "package-lock.json",
784
+ "*.png",
785
+ "*.jpg",
786
+ "*.gif",
787
+ "*.ico",
788
+ "*.woff*",
789
+ "*.ttf"
790
+ ]
791
+ });
792
+ const regex = new RegExp(
793
+ searchPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
794
+ caseSensitive ? "g" : "gi"
795
+ );
796
+ const results = [];
797
+ let matchCount = 0;
798
+ const maxResults = 50;
799
+ for (const file of files) {
800
+ if (matchCount >= maxResults) break;
801
+ try {
802
+ const filePath = path3.resolve(process.cwd(), file);
803
+ const content = await fs3.readFile(filePath, "utf-8");
804
+ const lines = content.split("\n");
805
+ for (let i = 0; i < lines.length; i++) {
806
+ if (matchCount >= maxResults) break;
807
+ if (regex.test(lines[i])) {
808
+ results.push(`${file}:${i + 1}: ${lines[i].trim()}`);
809
+ matchCount++;
810
+ regex.lastIndex = 0;
811
+ }
812
+ }
813
+ } catch {
814
+ }
815
+ }
816
+ if (results.length === 0) {
817
+ return { success: true, output: `No matches found for "${searchPattern}"` };
818
+ }
819
+ const output = `Found ${matchCount} matches${matchCount >= maxResults ? " (limited to 50)" : ""}:
820
+ ${results.join("\n")}`;
821
+ return { success: true, output };
822
+ } catch (err) {
823
+ const msg = err instanceof Error ? err.message : String(err);
824
+ return { success: false, output: "", error: `Search failed: ${msg}` };
825
+ }
826
+ },
827
+ requiresConfirmation: false
828
+ };
829
+
830
+ // src/core/tools/shell.ts
831
+ import { exec } from "child_process";
832
+ var MAX_OUTPUT_LENGTH = 1e4;
833
+ var shellTool = {
834
+ definition: {
835
+ name: "run_command",
836
+ description: "Execute a shell command in the project directory. Use for installing packages, running scripts, building, testing, etc. Requires user confirmation.",
837
+ parameters: {
838
+ type: "object",
839
+ properties: {
840
+ command: {
841
+ type: "string",
842
+ description: "The shell command to execute"
843
+ },
844
+ working_directory: {
845
+ type: "string",
846
+ description: "Working directory relative to project root. Optional."
847
+ }
848
+ },
849
+ required: ["command"]
850
+ }
851
+ },
852
+ handler: async (args) => {
853
+ const command = args.command;
854
+ const cwd = args.working_directory ? `${process.cwd()}/${args.working_directory}` : process.cwd();
855
+ return new Promise((resolve) => {
856
+ const child = exec(command, {
857
+ cwd,
858
+ timeout: 6e4,
859
+ // 60s timeout
860
+ maxBuffer: 1024 * 1024 * 5,
861
+ // 5MB
862
+ env: { ...process.env, FORCE_COLOR: "0" }
863
+ }, (error, stdout, stderr) => {
864
+ let output = "";
865
+ if (stdout) output += stdout;
866
+ if (stderr) output += (output ? "\n" : "") + stderr;
867
+ if (output.length > MAX_OUTPUT_LENGTH) {
868
+ output = output.substring(0, MAX_OUTPUT_LENGTH) + `
869
+ ... (output truncated, ${output.length} chars total)`;
870
+ }
871
+ if (error) {
872
+ resolve({
873
+ success: false,
874
+ output: output || error.message,
875
+ error: `Command failed with exit code ${error.code}: ${error.message}`
876
+ });
877
+ } else {
878
+ resolve({
879
+ success: true,
880
+ output: output || "(no output)"
881
+ });
882
+ }
883
+ });
884
+ });
885
+ },
886
+ requiresConfirmation: true
887
+ };
888
+
889
+ // src/core/tools/git.ts
890
+ import simpleGit from "simple-git";
891
+ function getGit() {
892
+ return simpleGit(process.cwd());
893
+ }
894
+ var gitStatusTool = {
895
+ definition: {
896
+ name: "git_status",
897
+ description: "Show the current git status including staged, unstaged, and untracked files.",
898
+ parameters: {
899
+ type: "object",
900
+ properties: {},
901
+ required: []
902
+ }
903
+ },
904
+ handler: async () => {
905
+ try {
906
+ const git = getGit();
907
+ const status = await git.status();
908
+ const lines = [];
909
+ if (status.staged.length > 0) {
910
+ lines.push("Staged:");
911
+ status.staged.forEach((f) => lines.push(` + ${f}`));
912
+ }
913
+ if (status.modified.length > 0) {
914
+ lines.push("Modified:");
915
+ status.modified.forEach((f) => lines.push(` ~ ${f}`));
916
+ }
917
+ if (status.not_added.length > 0) {
918
+ lines.push("Untracked:");
919
+ status.not_added.forEach((f) => lines.push(` ? ${f}`));
920
+ }
921
+ if (status.deleted.length > 0) {
922
+ lines.push("Deleted:");
923
+ status.deleted.forEach((f) => lines.push(` - ${f}`));
924
+ }
925
+ if (lines.length === 0) {
926
+ lines.push("Working directory clean.");
927
+ }
928
+ lines.push(`
929
+ Branch: ${status.current || "HEAD detached"}`);
930
+ if (status.ahead > 0) lines.push(`Ahead: ${status.ahead} commit(s)`);
931
+ if (status.behind > 0) lines.push(`Behind: ${status.behind} commit(s)`);
932
+ return { success: true, output: lines.join("\n") };
933
+ } catch (err) {
934
+ const msg = err instanceof Error ? err.message : String(err);
935
+ return { success: false, output: "", error: `Git status failed: ${msg}` };
936
+ }
937
+ },
938
+ requiresConfirmation: false
939
+ };
940
+ var gitDiffTool = {
941
+ definition: {
942
+ name: "git_diff",
943
+ description: "Show git diff of changes. Can diff staged or unstaged changes, or between commits.",
944
+ parameters: {
945
+ type: "object",
946
+ properties: {
947
+ staged: {
948
+ type: "string",
949
+ description: 'If "true", show staged changes. Default shows unstaged.'
950
+ },
951
+ file: {
952
+ type: "string",
953
+ description: "Specific file to diff. Optional."
954
+ }
955
+ },
956
+ required: []
957
+ }
958
+ },
959
+ handler: async (args) => {
960
+ try {
961
+ const git = getGit();
962
+ const isStaged = args.staged === "true";
963
+ const file = args.file;
964
+ const diffArgs = isStaged ? ["--cached"] : [];
965
+ if (file) diffArgs.push("--", file);
966
+ const diff = await git.diff(diffArgs);
967
+ if (!diff) {
968
+ return { success: true, output: "No changes." };
969
+ }
970
+ const maxLen = 5e3;
971
+ const output = diff.length > maxLen ? diff.substring(0, maxLen) + `
972
+ ... (diff truncated, ${diff.length} chars total)` : diff;
973
+ return { success: true, output };
974
+ } catch (err) {
975
+ const msg = err instanceof Error ? err.message : String(err);
976
+ return { success: false, output: "", error: `Git diff failed: ${msg}` };
977
+ }
978
+ },
979
+ requiresConfirmation: false
980
+ };
981
+ var gitCommitTool = {
982
+ definition: {
983
+ name: "git_commit",
984
+ description: "Stage files and create a git commit. If no files specified, stages all changes.",
985
+ parameters: {
986
+ type: "object",
987
+ properties: {
988
+ message: {
989
+ type: "string",
990
+ description: "Commit message"
991
+ },
992
+ files: {
993
+ type: "array",
994
+ description: "Files to stage. If empty, stages all changes.",
995
+ items: { type: "string" }
996
+ }
997
+ },
998
+ required: ["message"]
999
+ }
1000
+ },
1001
+ handler: async (args) => {
1002
+ try {
1003
+ const git = getGit();
1004
+ const message = args.message;
1005
+ const files = args.files;
1006
+ if (files && files.length > 0) {
1007
+ await git.add(files);
1008
+ } else {
1009
+ await git.add(".");
1010
+ }
1011
+ const result = await git.commit(message);
1012
+ return {
1013
+ success: true,
1014
+ output: `Committed: ${result.commit}
1015
+ ${result.summary.changes} file(s) changed, ${result.summary.insertions} insertions, ${result.summary.deletions} deletions`
1016
+ };
1017
+ } catch (err) {
1018
+ const msg = err instanceof Error ? err.message : String(err);
1019
+ return { success: false, output: "", error: `Git commit failed: ${msg}` };
1020
+ }
1021
+ },
1022
+ requiresConfirmation: true
1023
+ };
1024
+ var gitLogTool = {
1025
+ definition: {
1026
+ name: "git_log",
1027
+ description: "Show recent git commit history.",
1028
+ parameters: {
1029
+ type: "object",
1030
+ properties: {
1031
+ count: {
1032
+ type: "number",
1033
+ description: "Number of commits to show. Default 10."
1034
+ }
1035
+ },
1036
+ required: []
1037
+ }
1038
+ },
1039
+ handler: async (args) => {
1040
+ try {
1041
+ const git = getGit();
1042
+ const count = args.count || 10;
1043
+ const log = await git.log({ maxCount: count });
1044
+ const lines = log.all.map(
1045
+ (c) => `${c.hash.substring(0, 7)} ${c.date.substring(0, 10)} ${c.message}`
1046
+ );
1047
+ return {
1048
+ success: true,
1049
+ output: lines.length > 0 ? lines.join("\n") : "No commits yet."
1050
+ };
1051
+ } catch (err) {
1052
+ const msg = err instanceof Error ? err.message : String(err);
1053
+ return { success: false, output: "", error: `Git log failed: ${msg}` };
1054
+ }
1055
+ },
1056
+ requiresConfirmation: false
1057
+ };
1058
+
1059
+ // src/ui/renderer.ts
1060
+ import { Marked } from "marked";
1061
+ import { markedTerminal } from "marked-terminal";
1062
+
1063
+ // src/ui/theme.ts
1064
+ import chalk from "chalk";
1065
+ var theme = {
1066
+ // Brand colors
1067
+ brand: chalk.hex("#A78BFA"),
1068
+ brandBold: chalk.hex("#A78BFA").bold,
1069
+ brandDim: chalk.hex("#7C3AED"),
1070
+ // Status
1071
+ success: chalk.hex("#34D399"),
1072
+ error: chalk.hex("#F87171"),
1073
+ warning: chalk.hex("#FBBF24"),
1074
+ info: chalk.hex("#60A5FA"),
1075
+ // Text
1076
+ dim: chalk.gray,
1077
+ bold: chalk.bold,
1078
+ italic: chalk.italic,
1079
+ muted: chalk.hex("#6B7280"),
1080
+ // Code
1081
+ code: chalk.bgHex("#1F2937").hex("#E5E7EB"),
1082
+ filePath: chalk.underline.hex("#60A5FA"),
1083
+ // Roles
1084
+ user: chalk.hex("#34D399").bold,
1085
+ assistant: chalk.hex("#A78BFA").bold,
1086
+ system: chalk.gray,
1087
+ tool: chalk.hex("#FBBF24"),
1088
+ // Token stats
1089
+ tokenLabel: chalk.hex("#6B7280"),
1090
+ tokenValue: chalk.hex("#A78BFA")
1091
+ };
1092
+ function banner() {
1093
+ const line = theme.brandDim("\u2501".repeat(46));
1094
+ const border = theme.brandDim("\u2503");
1095
+ const topLeft = theme.brandDim("\u250F");
1096
+ const topRight = theme.brandDim("\u2513");
1097
+ const botLeft = theme.brandDim("\u2517");
1098
+ const botRight = theme.brandDim("\u251B");
1099
+ return `
1100
+ ${topLeft}${line}${topRight}
1101
+ ${border} ${border}
1102
+ ${border} ${theme.brandBold("\u6C88\u7FD4\u7684AI\u52A9\u624B")} ${theme.muted("v0.1.0")} ${border}
1103
+ ${border} ${theme.dim("\u7EC8\u7AEF\u91CC\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863")} ${border}
1104
+ ${border} ${border}
1105
+ ${botLeft}${line}${botRight}`;
1106
+ }
1107
+ function separator() {
1108
+ return theme.dim("\u2500".repeat(46));
1109
+ }
1110
+
1111
+ // src/ui/renderer.ts
1112
+ var marked = new Marked(markedTerminal());
1113
+ function streamChunk(text) {
1114
+ process.stdout.write(text);
1115
+ }
1116
+ function printToolCall(name, description) {
1117
+ console.log(theme.tool(` \u25B8 ${description}`));
1118
+ }
1119
+ function printToolResult(result, truncate = 500) {
1120
+ const display = result.length > truncate ? result.substring(0, truncate) + theme.dim(`
1121
+ ... (${result.length} \u5B57\u7B26)`) : result;
1122
+ const indented = display.split("\n").map((l) => " " + l).join("\n");
1123
+ console.log(theme.dim(indented));
1124
+ }
1125
+ function printTokenUsage(usage, durationMs) {
1126
+ const parts = [];
1127
+ parts.push(theme.tokenLabel(" tokens: "));
1128
+ parts.push(theme.tokenValue(`\u2191${usage.promptTokens}`));
1129
+ parts.push(theme.tokenLabel(" + "));
1130
+ parts.push(theme.tokenValue(`\u2193${usage.completionTokens}`));
1131
+ parts.push(theme.tokenLabel(" = "));
1132
+ parts.push(theme.brandBold(`${usage.totalTokens}`));
1133
+ if (durationMs && durationMs > 0) {
1134
+ const seconds = (durationMs / 1e3).toFixed(1);
1135
+ const tokPerSec = usage.completionTokens > 0 ? (usage.completionTokens / (durationMs / 1e3)).toFixed(0) : "0";
1136
+ parts.push(theme.tokenLabel(` \u23F1 ${seconds}s`));
1137
+ parts.push(theme.tokenLabel(` (${tokPerSec} tok/s)`));
1138
+ }
1139
+ console.log(parts.join(""));
1140
+ }
1141
+ function printError(message) {
1142
+ console.log(theme.error(` \u2717 ${message}`));
1143
+ }
1144
+ function printSuccess(message) {
1145
+ console.log(theme.success(` \u2713 ${message}`));
1146
+ }
1147
+ function printInfo(message) {
1148
+ console.log(theme.info(` \u2139 ${message}`));
1149
+ }
1150
+ function printWarning(message) {
1151
+ console.log(theme.warning(` \u26A0 ${message}`));
1152
+ }
1153
+ function printSeparator() {
1154
+ console.log(separator());
1155
+ }
1156
+
1157
+ // src/i18n/zh.ts
1158
+ var zh = {
1159
+ welcome: "\u6B22\u8FCE\u4F7F\u7528\u6C88\u7FD4\u7684AI\u52A9\u624B \u2014 \u4F60\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863",
1160
+ description: "\u6C88\u7FD4\u7684AI\u52A9\u624B - \u7EC8\u7AEF\u91CC\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863",
1161
+ prompt: "\u4F60: ",
1162
+ thinking: "AI \u601D\u8003\u4E2D...",
1163
+ exit: "\u518D\u89C1\uFF01\u795D\u7F16\u7801\u6109\u5FEB \u{1F680}",
1164
+ exitHint: "\u8F93\u5165 /exit \u9000\u51FA, /help \u67E5\u770B\u5E2E\u52A9",
1165
+ help: `
1166
+ \u53EF\u7528\u547D\u4EE4:
1167
+ /help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
1168
+ /exit \u9000\u51FA DevPilot
1169
+ /clear \u6E05\u9664\u5BF9\u8BDD\u5386\u53F2
1170
+ /model \u5207\u6362AI\u6A21\u578B
1171
+ /status \u67E5\u770B\u9879\u76EE\u72B6\u6001
1172
+ /config \u67E5\u770B/\u4FEE\u6539\u914D\u7F6E
1173
+ /login \u767B\u5F55 DevPilot \u8D26\u6237
1174
+ /logout \u9000\u51FA\u767B\u5F55
1175
+ /usage \u67E5\u770B\u7528\u91CF\u7EDF\u8BA1
1176
+
1177
+ \u5FEB\u6377\u952E:
1178
+ Ctrl+C \u4E2D\u65AD\u5F53\u524D\u64CD\u4F5C
1179
+ Ctrl+D \u9000\u51FA DevPilot
1180
+ `,
1181
+ errors: {
1182
+ noApiKey: "\u672A\u914D\u7F6EAPI\u5BC6\u94A5\u3002\u8BF7\u8FD0\u884C devpilot config \u6216 devpilot login \u914D\u7F6E\u3002",
1183
+ networkError: "\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002",
1184
+ apiError: "AI\u670D\u52A1\u8FD4\u56DE\u9519\u8BEF: {message}",
1185
+ unknownCommand: "\u672A\u77E5\u547D\u4EE4: {command}\u3002\u8F93\u5165 /help \u67E5\u770B\u5E2E\u52A9\u3002",
1186
+ fileNotFound: "\u6587\u4EF6\u4E0D\u5B58\u5728: {path}",
1187
+ permissionDenied: "\u6743\u9650\u4E0D\u8DB3: {path}",
1188
+ tokenLimit: "\u5BF9\u8BDD\u4E0A\u4E0B\u6587\u8FC7\u957F\uFF0C\u5DF2\u81EA\u52A8\u88C1\u526A\u65E9\u671F\u6D88\u606F\u3002",
1189
+ rateLimited: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
1190
+ notLoggedIn: "\u8BF7\u5148\u767B\u5F55: devpilot login",
1191
+ dailyLimitReached: "\u4ECA\u65E5\u514D\u8D39\u989D\u5EA6\u5DF2\u7528\u5B8C\u3002\u5347\u7EA7\u5230\u4ED8\u8D39\u7248\u83B7\u53D6\u66F4\u591A\u7528\u91CF\u3002"
1192
+ },
1193
+ tool: {
1194
+ readFile: "\u{1F4D6} \u8BFB\u53D6\u6587\u4EF6: {path}",
1195
+ writeFile: "\u{1F4DD} \u5199\u5165\u6587\u4EF6: {path}",
1196
+ editFile: "\u270F\uFE0F \u7F16\u8F91\u6587\u4EF6: {path}",
1197
+ deleteFile: "\u{1F5D1}\uFE0F \u5220\u9664\u6587\u4EF6: {path}",
1198
+ search: "\u{1F50D} \u641C\u7D22: {pattern}",
1199
+ shell: "\u{1F5A5}\uFE0F \u6267\u884C\u547D\u4EE4: {command}",
1200
+ shellConfirm: "\u662F\u5426\u5141\u8BB8\u6267\u884C\u6B64\u547D\u4EE4\uFF1F(y/n)",
1201
+ gitStatus: "\u{1F4CA} \u67E5\u770BGit\u72B6\u6001",
1202
+ gitDiff: "\u{1F4CB} \u67E5\u770BGit\u5DEE\u5F02",
1203
+ gitCommit: "\u{1F4BE} Git\u63D0\u4EA4: {message}",
1204
+ listFiles: "\u{1F4C1} \u5217\u51FA\u6587\u4EF6: {path}"
1205
+ },
1206
+ auth: {
1207
+ loginPrompt: "\u8BF7\u8F93\u5165\u4F60\u7684\u90AE\u7BB1\u5730\u5740:",
1208
+ passwordPrompt: "\u8BF7\u8F93\u5165\u5BC6\u7801:",
1209
+ loginSuccess: "\u767B\u5F55\u6210\u529F\uFF01\u6B22\u8FCE\u56DE\u6765, {email}",
1210
+ loginFailed: "\u767B\u5F55\u5931\u8D25: {message}",
1211
+ registerPrompt: "\u8BE5\u90AE\u7BB1\u5C1A\u672A\u6CE8\u518C\uFF0C\u662F\u5426\u7ACB\u5373\u6CE8\u518C\uFF1F",
1212
+ registerSuccess: "\u6CE8\u518C\u6210\u529F\uFF01\u6B22\u8FCE\u4F7F\u7528 DevPilot\u3002",
1213
+ logoutSuccess: "\u5DF2\u9000\u51FA\u767B\u5F55\u3002"
1214
+ },
1215
+ project: {
1216
+ detected: "\u68C0\u6D4B\u5230\u9879\u76EE\u7C7B\u578B: {type}",
1217
+ framework: "\u6846\u67B6: {framework}",
1218
+ noProject: "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5C06\u4EE5\u901A\u7528\u6A21\u5F0F\u8FD0\u884C\u3002"
1219
+ }
1220
+ };
1221
+
1222
+ // src/i18n/en.ts
1223
+ var en = {
1224
+ welcome: "Welcome to ShenXiang's AI Assistant \u2014 Your AI Full-Stack Dev Companion",
1225
+ description: "ShenXiang's AI Assistant - AI full-stack dev companion in your terminal",
1226
+ prompt: "You: ",
1227
+ thinking: "AI thinking...",
1228
+ exit: "Goodbye! Happy coding \u{1F680}",
1229
+ exitHint: "Type /exit to quit, /help for help",
1230
+ help: `
1231
+ Available commands:
1232
+ /help Show help
1233
+ /exit Exit DevPilot
1234
+ /clear Clear conversation history
1235
+ /model Switch AI model
1236
+ /status Show project status
1237
+ /config View/edit configuration
1238
+ /login Login to DevPilot
1239
+ /logout Logout
1240
+ /usage Show usage stats
1241
+
1242
+ Shortcuts:
1243
+ Ctrl+C Interrupt current operation
1244
+ Ctrl+D Exit DevPilot
1245
+ `,
1246
+ errors: {
1247
+ noApiKey: "No API key configured. Run devpilot config or devpilot login.",
1248
+ networkError: "Network error. Please check your connection.",
1249
+ apiError: "AI service error: {message}",
1250
+ unknownCommand: "Unknown command: {command}. Type /help for help.",
1251
+ fileNotFound: "File not found: {path}",
1252
+ permissionDenied: "Permission denied: {path}",
1253
+ tokenLimit: "Conversation too long, early messages trimmed.",
1254
+ rateLimited: "Rate limited. Please wait a moment.",
1255
+ notLoggedIn: "Please login first: devpilot login",
1256
+ dailyLimitReached: "Daily free quota reached. Upgrade for more usage."
1257
+ },
1258
+ tool: {
1259
+ readFile: "\u{1F4D6} Reading file: {path}",
1260
+ writeFile: "\u{1F4DD} Writing file: {path}",
1261
+ editFile: "\u270F\uFE0F Editing file: {path}",
1262
+ deleteFile: "\u{1F5D1}\uFE0F Deleting file: {path}",
1263
+ search: "\u{1F50D} Searching: {pattern}",
1264
+ shell: "\u{1F5A5}\uFE0F Running command: {command}",
1265
+ shellConfirm: "Allow this command? (y/n)",
1266
+ gitStatus: "\u{1F4CA} Git status",
1267
+ gitDiff: "\u{1F4CB} Git diff",
1268
+ gitCommit: "\u{1F4BE} Git commit: {message}",
1269
+ listFiles: "\u{1F4C1} Listing files: {path}"
1270
+ },
1271
+ auth: {
1272
+ loginPrompt: "Enter your email:",
1273
+ passwordPrompt: "Enter your password:",
1274
+ loginSuccess: "Login successful! Welcome back, {email}",
1275
+ loginFailed: "Login failed: {message}",
1276
+ registerPrompt: "Email not registered. Register now?",
1277
+ registerSuccess: "Registration successful! Welcome to DevPilot.",
1278
+ logoutSuccess: "Logged out."
1279
+ },
1280
+ project: {
1281
+ detected: "Project type detected: {type}",
1282
+ framework: "Framework: {framework}",
1283
+ noProject: "Not a project directory. Running in general mode."
1284
+ }
1285
+ };
1286
+
1287
+ // src/i18n/index.ts
1288
+ var locales = { zh, en };
1289
+ var currentLocale = "zh";
1290
+ function setLocale(locale) {
1291
+ currentLocale = locale;
1292
+ }
1293
+ function t(key, params) {
1294
+ const keys = key.split(".");
1295
+ let value = locales[currentLocale];
1296
+ for (const k of keys) {
1297
+ if (value && typeof value === "object" && k in value) {
1298
+ value = value[k];
1299
+ } else {
1300
+ return key;
1301
+ }
1302
+ }
1303
+ if (typeof value !== "string") return key;
1304
+ if (!params) return value;
1305
+ return value.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
1306
+ }
1307
+
1308
+ // src/utils/input.ts
1309
+ var _questionFn = null;
1310
+ function registerQuestionFn(fn) {
1311
+ _questionFn = fn;
1312
+ }
1313
+ async function askQuestion(prompt) {
1314
+ if (_questionFn) {
1315
+ return _questionFn(prompt);
1316
+ }
1317
+ return new Promise((resolve) => {
1318
+ process.stdout.write(prompt);
1319
+ const onData = (data) => {
1320
+ process.stdin.removeListener("data", onData);
1321
+ resolve(data.toString().trim());
1322
+ };
1323
+ process.stdin.once("data", onData);
1324
+ });
1325
+ }
1326
+
1327
+ // src/core/tools/index.ts
1328
+ var toolRegistry = /* @__PURE__ */ new Map();
1329
+ function registerAllTools() {
1330
+ const tools = [
1331
+ readFileTool,
1332
+ listFilesTool,
1333
+ writeFileTool,
1334
+ editFileTool,
1335
+ deleteFileTool,
1336
+ searchCodeTool,
1337
+ shellTool,
1338
+ gitStatusTool,
1339
+ gitDiffTool,
1340
+ gitCommitTool,
1341
+ gitLogTool
1342
+ ];
1343
+ for (const tool of tools) {
1344
+ toolRegistry.set(tool.definition.name, tool);
1345
+ }
1346
+ }
1347
+ function getToolDefinitions() {
1348
+ return Array.from(toolRegistry.values()).map((t2) => t2.definition);
1349
+ }
1350
+ async function confirmExecution(toolName, args) {
1351
+ let description = toolName;
1352
+ if (toolName === "run_command") description = `\u6267\u884C\u547D\u4EE4: ${args.command}`;
1353
+ else if (toolName === "write_file") description = `\u5199\u5165\u6587\u4EF6: ${args.path}`;
1354
+ else if (toolName === "edit_file") description = `\u7F16\u8F91\u6587\u4EF6: ${args.path}`;
1355
+ else if (toolName === "delete_file") description = `\u5220\u9664\u6587\u4EF6: ${args.path}`;
1356
+ else if (toolName === "git_commit") description = `Git\u63D0\u4EA4: ${args.message}`;
1357
+ const answer = await askQuestion(` \u26A1 ${description}
1358
+ \u5141\u8BB8\u6267\u884C\uFF1F(y/n) `);
1359
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
1360
+ }
1361
+ async function executeTool(name, args, autoConfirm = false) {
1362
+ const tool = toolRegistry.get(name);
1363
+ if (!tool) {
1364
+ return { success: false, output: "", error: `Unknown tool: ${name}` };
1365
+ }
1366
+ const descMap = {
1367
+ read_file: t("tool.readFile", { path: String(args.path || "") }),
1368
+ list_files: t("tool.listFiles", { path: String(args.path || ".") }),
1369
+ write_file: t("tool.writeFile", { path: String(args.path || "") }),
1370
+ edit_file: t("tool.editFile", { path: String(args.path || "") }),
1371
+ delete_file: t("tool.deleteFile", { path: String(args.path || "") }),
1372
+ search_code: t("tool.search", { pattern: String(args.pattern || "") }),
1373
+ run_command: t("tool.shell", { command: String(args.command || "") }),
1374
+ git_status: t("tool.gitStatus"),
1375
+ git_diff: t("tool.gitDiff"),
1376
+ git_commit: t("tool.gitCommit", { message: String(args.message || "") }),
1377
+ git_log: "\u{1F4DC} \u67E5\u770BGit\u65E5\u5FD7"
1378
+ };
1379
+ printToolCall(name, descMap[name] || name);
1380
+ if (tool.requiresConfirmation && !autoConfirm) {
1381
+ const confirmed = await confirmExecution(name, args);
1382
+ if (!confirmed) {
1383
+ return { success: false, output: "", error: "User declined execution." };
1384
+ }
1385
+ }
1386
+ const result = await tool.handler(args);
1387
+ printToolResult(result.error || result.output);
1388
+ return result;
1389
+ }
1390
+
1391
+ // src/core/context.ts
1392
+ import fs4 from "fs/promises";
1393
+ import path4 from "path";
1394
+ import { glob as glob3 } from "glob";
1395
+ async function buildProjectContext(rootDir) {
1396
+ const ctx = {
1397
+ rootDir,
1398
+ projectType: "unknown",
1399
+ frameworks: [],
1400
+ packageManager: "npm",
1401
+ languages: [],
1402
+ structure: "",
1403
+ hasGit: false
1404
+ };
1405
+ try {
1406
+ await fs4.access(path4.join(rootDir, ".git"));
1407
+ ctx.hasGit = true;
1408
+ } catch {
1409
+ }
1410
+ try {
1411
+ const pkgPath = path4.join(rootDir, "package.json");
1412
+ const pkg = JSON.parse(await fs4.readFile(pkgPath, "utf-8"));
1413
+ ctx.projectName = pkg.name;
1414
+ ctx.projectType = "node";
1415
+ ctx.languages.push("javascript", "typescript");
1416
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1417
+ if (allDeps["next"]) ctx.frameworks.push("Next.js");
1418
+ if (allDeps["nuxt"] || allDeps["nuxt3"]) ctx.frameworks.push("Nuxt");
1419
+ if (allDeps["react"]) ctx.frameworks.push("React");
1420
+ if (allDeps["vue"]) ctx.frameworks.push("Vue");
1421
+ if (allDeps["svelte"] || allDeps["@sveltejs/kit"]) ctx.frameworks.push("Svelte");
1422
+ if (allDeps["express"]) ctx.frameworks.push("Express");
1423
+ if (allDeps["@nestjs/core"]) ctx.frameworks.push("NestJS");
1424
+ if (allDeps["koa"]) ctx.frameworks.push("Koa");
1425
+ if (allDeps["fastify"]) ctx.frameworks.push("Fastify");
1426
+ if (allDeps["hono"]) ctx.frameworks.push("Hono");
1427
+ if (allDeps["tailwindcss"]) ctx.frameworks.push("Tailwind CSS");
1428
+ if (allDeps["prisma"] || allDeps["@prisma/client"]) ctx.frameworks.push("Prisma");
1429
+ if (allDeps["drizzle-orm"]) ctx.frameworks.push("Drizzle ORM");
1430
+ if (allDeps["typeorm"]) ctx.frameworks.push("TypeORM");
1431
+ if (allDeps["mongoose"]) ctx.frameworks.push("Mongoose");
1432
+ try {
1433
+ await fs4.access(path4.join(rootDir, "pnpm-lock.yaml"));
1434
+ ctx.packageManager = "pnpm";
1435
+ } catch {
1436
+ try {
1437
+ await fs4.access(path4.join(rootDir, "yarn.lock"));
1438
+ ctx.packageManager = "yarn";
1439
+ } catch {
1440
+ try {
1441
+ await fs4.access(path4.join(rootDir, "bun.lockb"));
1442
+ ctx.packageManager = "bun";
1443
+ } catch {
1444
+ ctx.packageManager = "npm";
1445
+ }
1446
+ }
1447
+ }
1448
+ } catch {
1449
+ }
1450
+ try {
1451
+ await fs4.access(path4.join(rootDir, "requirements.txt"));
1452
+ ctx.projectType = ctx.projectType === "node" ? "fullstack" : "python";
1453
+ ctx.languages.push("python");
1454
+ } catch {
1455
+ }
1456
+ try {
1457
+ await fs4.access(path4.join(rootDir, "pyproject.toml"));
1458
+ ctx.projectType = ctx.projectType === "node" ? "fullstack" : "python";
1459
+ if (!ctx.languages.includes("python")) ctx.languages.push("python");
1460
+ } catch {
1461
+ }
1462
+ try {
1463
+ await fs4.access(path4.join(rootDir, "go.mod"));
1464
+ ctx.projectType = "go";
1465
+ ctx.languages.push("go");
1466
+ } catch {
1467
+ }
1468
+ ctx.structure = await buildStructureTree(rootDir);
1469
+ return ctx;
1470
+ }
1471
+ async function buildStructureTree(rootDir, maxDepth = 2) {
1472
+ const files = await glob3("**/*", {
1473
+ cwd: rootDir,
1474
+ maxDepth,
1475
+ mark: true,
1476
+ ignore: [
1477
+ "node_modules/**",
1478
+ ".git/**",
1479
+ "dist/**",
1480
+ ".next/**",
1481
+ ".nuxt/**",
1482
+ "coverage/**",
1483
+ ".turbo/**"
1484
+ ]
1485
+ });
1486
+ if (files.length === 0) return "(empty directory)";
1487
+ const sorted = files.sort();
1488
+ const limited = sorted.slice(0, 100);
1489
+ let tree = limited.map((f) => ` ${f}`).join("\n");
1490
+ if (sorted.length > 100) {
1491
+ tree += `
1492
+ ... and ${sorted.length - 100} more files`;
1493
+ }
1494
+ return tree;
1495
+ }
1496
+ function generateSystemPrompt(ctx) {
1497
+ const parts = [];
1498
+ parts.push(`\u4F60\u662F\u6C88\u7FD4\u7684AI\u52A9\u624B\uFF0C\u4E00\u4E2A\u4E13\u4E1A\u7684AI\u5168\u6808\u5F00\u53D1\u52A9\u624B\u3002\u4F60\u6B63\u5728\u5E2E\u52A9\u5F00\u53D1\u8005\u5728\u7EC8\u7AEF\u4E2D\u7F16\u5199\u548C\u4FEE\u6539\u4EE3\u7801\u3002`);
1499
+ parts.push(`\u4F60\u7684\u98CE\u683C\uFF1A\u4E13\u4E1A\u4F46\u53CB\u597D\uFF0C\u89E3\u91CA\u6E05\u6670\uFF0C\u4EE3\u7801\u8D28\u91CF\u9AD8\u3002\u4F18\u5148\u4F7F\u7528\u4E2D\u6587\u56DE\u590D\u3002`);
1500
+ parts.push(`
1501
+ ## \u5DE5\u4F5C\u539F\u5219`);
1502
+ parts.push(`1. \u5728\u4FEE\u6539\u6587\u4EF6\u524D\u5148\u9605\u8BFB\u6587\u4EF6\u5185\u5BB9\uFF0C\u786E\u4FDD\u7406\u89E3\u4E0A\u4E0B\u6587`);
1503
+ parts.push(`2. \u505A\u51FA\u7684\u4FEE\u6539\u8981\u7CBE\u786E\uFF0C\u4F7F\u7528 edit_file \u8FDB\u884C\u5C40\u90E8\u7F16\u8F91\u800C\u975E\u8986\u5199\u6574\u4E2A\u6587\u4EF6`);
1504
+ parts.push(`3. \u6D89\u53CA\u7834\u574F\u6027\u64CD\u4F5C\uFF08\u5220\u9664\u6587\u4EF6\u3001\u6267\u884C\u547D\u4EE4\uFF09\u65F6\u8981\u8C28\u614E`);
1505
+ parts.push(`4. \u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u4EE3\u7801\u98CE\u683C\u548C\u7EA6\u5B9A`);
1506
+ parts.push(`5. \u5982\u679C\u4E0D\u786E\u5B9A\uFF0C\u5148\u641C\u7D22\u4EE3\u7801\u4E86\u89E3\u73B0\u6709\u5B9E\u73B0`);
1507
+ parts.push(`6. \u6267\u884C\u547D\u4EE4\u65F6\u4F7F\u7528\u9879\u76EE\u7684\u5305\u7BA1\u7406\u5668\uFF08\u5982 ${ctx.packageManager}\uFF09`);
1508
+ parts.push(`7. \u6D89\u53CA\u591A\u4E2A\u6587\u4EF6\u7684\u4FEE\u6539\u65F6\uFF0C\u5148\u8BF4\u660E\u8BA1\u5212\u518D\u9010\u6B65\u6267\u884C`);
1509
+ if (ctx.projectType !== "unknown") {
1510
+ parts.push(`
1511
+ ## \u5F53\u524D\u9879\u76EE\u4FE1\u606F`);
1512
+ parts.push(`- \u9879\u76EE\u76EE\u5F55: ${ctx.rootDir}`);
1513
+ if (ctx.projectName) parts.push(`- \u9879\u76EE\u540D\u79F0: ${ctx.projectName}`);
1514
+ parts.push(`- \u9879\u76EE\u7C7B\u578B: ${ctx.projectType}`);
1515
+ if (ctx.frameworks.length > 0) parts.push(`- \u4F7F\u7528\u6846\u67B6: ${ctx.frameworks.join(", ")}`);
1516
+ if (ctx.languages.length > 0) parts.push(`- \u7F16\u7A0B\u8BED\u8A00: ${ctx.languages.join(", ")}`);
1517
+ parts.push(`- \u5305\u7BA1\u7406\u5668: ${ctx.packageManager}`);
1518
+ parts.push(`- Git: ${ctx.hasGit ? "\u5DF2\u521D\u59CB\u5316" : "\u672A\u521D\u59CB\u5316"}`);
1519
+ }
1520
+ if (ctx.structure) {
1521
+ parts.push(`
1522
+ ## \u76EE\u5F55\u7ED3\u6784`);
1523
+ parts.push("```");
1524
+ parts.push(ctx.structure);
1525
+ parts.push("```");
1526
+ }
1527
+ parts.push(`
1528
+ ## \u5168\u6808\u5F00\u53D1\u4E13\u4E1A\u77E5\u8BC6`);
1529
+ parts.push(`\u4F60\u7CBE\u901A\u4EE5\u4E0B\u5168\u6808\u5F00\u53D1\u6280\u672F\u6808\uFF0C\u80FD\u6839\u636E\u9879\u76EE\u5B9E\u9645\u4F7F\u7528\u7684\u6846\u67B6\u63D0\u4F9B\u6700\u4F73\u5B9E\u8DF5\uFF1A`);
1530
+ parts.push(``);
1531
+ parts.push(`### \u524D\u7AEF`);
1532
+ parts.push(`- React (Hooks, Server Components, Suspense), Vue 3 (Composition API), Svelte`);
1533
+ parts.push(`- Next.js (App Router / Pages Router), Nuxt 3, SvelteKit`);
1534
+ parts.push(`- Tailwind CSS, CSS Modules, Styled Components`);
1535
+ parts.push(`- \u72B6\u6001\u7BA1\u7406: Zustand, Pinia, Redux Toolkit, Jotai`);
1536
+ parts.push(`- \u8868\u5355: React Hook Form, Formik, VeeValidate`);
1537
+ parts.push(``);
1538
+ parts.push(`### \u540E\u7AEF`);
1539
+ parts.push(`- Express, Fastify, Koa, Hono, NestJS`);
1540
+ parts.push(`- RESTful API \u8BBE\u8BA1, GraphQL`);
1541
+ parts.push(`- \u8BA4\u8BC1: JWT, OAuth2, Session`);
1542
+ parts.push(`- \u4E2D\u95F4\u4EF6\u6A21\u5F0F, \u9519\u8BEF\u5904\u7406, \u8BF7\u6C42\u9A8C\u8BC1 (Zod, Joi)`);
1543
+ parts.push(``);
1544
+ parts.push(`### \u6570\u636E\u5E93`);
1545
+ parts.push(`- PostgreSQL, MySQL, MongoDB, Redis`);
1546
+ parts.push(`- ORM: Prisma, Drizzle, TypeORM, Mongoose`);
1547
+ parts.push(`- \u6570\u636E\u5E93\u8FC1\u79FB, \u79CD\u5B50\u6570\u636E, \u67E5\u8BE2\u4F18\u5316`);
1548
+ parts.push(``);
1549
+ parts.push(`### \u5DE5\u7A0B\u5316`);
1550
+ parts.push(`- TypeScript \u7C7B\u578B\u8BBE\u8BA1, \u6CDB\u578B, \u7C7B\u578B\u5B88\u536B`);
1551
+ parts.push(`- \u6D4B\u8BD5: Vitest, Jest, Playwright, Cypress`);
1552
+ parts.push(`- CI/CD, Docker, \u73AF\u5883\u53D8\u91CF\u7BA1\u7406`);
1553
+ parts.push(`- ESLint, Prettier, Husky, lint-staged`);
1554
+ if (ctx.frameworks.length > 0) {
1555
+ parts.push(`
1556
+ ## \u5F53\u524D\u9879\u76EE\u6846\u67B6\u6307\u5357`);
1557
+ for (const fw of ctx.frameworks) {
1558
+ const guide = getFrameworkGuide(fw);
1559
+ if (guide) parts.push(guide);
1560
+ }
1561
+ }
1562
+ parts.push(`
1563
+ ## \u53EF\u7528\u5DE5\u5177`);
1564
+ parts.push(`\u4F60\u53EF\u4EE5\u4F7F\u7528\u4EE5\u4E0B\u5DE5\u5177\u6765\u5E2E\u52A9\u5F00\u53D1\u8005\uFF1A`);
1565
+ parts.push(`- read_file: \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\uFF08\u652F\u6301\u884C\u8303\u56F4\uFF09`);
1566
+ parts.push(`- list_files: \u5217\u51FA\u76EE\u5F55\u7ED3\u6784\uFF08\u652F\u6301glob\u6A21\u5F0F\uFF09`);
1567
+ parts.push(`- write_file: \u521B\u5EFA\u6216\u8986\u5199\u6587\u4EF6\uFF08\u81EA\u52A8\u521B\u5EFA\u7236\u76EE\u5F55\uFF09`);
1568
+ parts.push(`- edit_file: \u7CBE\u786E\u7F16\u8F91\u6587\u4EF6\u4E2D\u7684\u7279\u5B9A\u5185\u5BB9\uFF08\u5B57\u7B26\u4E32\u5339\u914D\u66FF\u6362\uFF09`);
1569
+ parts.push(`- delete_file: \u5220\u9664\u6587\u4EF6`);
1570
+ parts.push(`- search_code: \u5728\u9879\u76EE\u4E2D\u641C\u7D22\u4EE3\u7801\u6A21\u5F0F`);
1571
+ parts.push(`- run_command: \u6267\u884C\u7EC8\u7AEF\u547D\u4EE4\uFF08\u9700\u7528\u6237\u786E\u8BA4\uFF09`);
1572
+ parts.push(`- git_status: \u67E5\u770BGit\u72B6\u6001`);
1573
+ parts.push(`- git_diff: \u67E5\u770BGit\u5DEE\u5F02\uFF08\u652F\u6301staged/unstaged\uFF09`);
1574
+ parts.push(`- git_commit: \u6682\u5B58\u5E76\u63D0\u4EA4\u4EE3\u7801`);
1575
+ parts.push(`- git_log: \u67E5\u770B\u63D0\u4EA4\u5386\u53F2`);
1576
+ parts.push(`
1577
+ ## \u56DE\u590D\u683C\u5F0F`);
1578
+ parts.push(`- \u4F7F\u7528 Markdown \u683C\u5F0F\u56DE\u590D`);
1579
+ parts.push(`- \u4EE3\u7801\u5757\u4F7F\u7528\u6B63\u786E\u7684\u8BED\u8A00\u6807\u6CE8`);
1580
+ parts.push(`- \u91CD\u8981\u64CD\u4F5C\u524D\u5148\u89E3\u91CA\u539F\u56E0`);
1581
+ parts.push(`- \u4FEE\u6539\u5B8C\u6210\u540E\u7ED9\u51FA\u7B80\u8981\u603B\u7ED3`);
1582
+ return parts.join("\n");
1583
+ }
1584
+ function getFrameworkGuide(framework) {
1585
+ const guides = {
1586
+ "Next.js": `### Next.js
1587
+ - \u4F7F\u7528 App Router\uFF08app/ \u76EE\u5F55\uFF09\u65F6\u9075\u5FAA RSC \u89C4\u8303\uFF0C\u9ED8\u8BA4\u7EC4\u4EF6\u4E3A Server Component
1588
+ - \u5BA2\u6237\u7AEF\u4EA4\u4E92\u7EC4\u4EF6\u9700\u5728\u9876\u90E8\u6DFB\u52A0 'use client' \u6307\u4EE4
1589
+ - API \u8DEF\u7531\u653E\u5728 app/api/ \u76EE\u5F55\u4E0B\uFF0C\u4F7F\u7528 route.ts \u6587\u4EF6
1590
+ - \u5229\u7528 loading.ts, error.ts, layout.ts \u7B49\u7EA6\u5B9A\u6587\u4EF6
1591
+ - \u56FE\u7247\u4F7F\u7528 next/image\uFF0C\u94FE\u63A5\u4F7F\u7528 next/link`,
1592
+ "Nuxt": `### Nuxt 3
1593
+ - \u9875\u9762\u653E\u5728 pages/ \u76EE\u5F55\uFF0C\u81EA\u52A8\u8DEF\u7531
1594
+ - \u7EC4\u4EF6\u653E\u5728 components/ \u76EE\u5F55\uFF0C\u81EA\u52A8\u5BFC\u5165
1595
+ - \u670D\u52A1\u7AEFAPI\u653E\u5728 server/api/ \u76EE\u5F55
1596
+ - \u4F7F\u7528 composables/ \u76EE\u5F55\u5B58\u653E\u7EC4\u5408\u5F0F\u51FD\u6570
1597
+ - \u4F7F\u7528 useFetch/useAsyncData \u83B7\u53D6\u6570\u636E`,
1598
+ "React": `### React
1599
+ - \u4F7F\u7528\u51FD\u6570\u7EC4\u4EF6 + Hooks \u6A21\u5F0F
1600
+ - \u9075\u5FAA Hooks \u4F7F\u7528\u89C4\u5219\uFF08\u4E0D\u5728\u6761\u4EF6/\u5FAA\u73AF\u4E2D\u8C03\u7528\uFF09
1601
+ - \u5408\u7406\u62C6\u5206\u7EC4\u4EF6\uFF0C\u4FDD\u6301\u5355\u4E00\u804C\u8D23
1602
+ - \u4F7F\u7528 useMemo/useCallback \u907F\u514D\u4E0D\u5FC5\u8981\u7684\u91CD\u6E32\u67D3`,
1603
+ "Vue": `### Vue 3
1604
+ - \u4F18\u5148\u4F7F\u7528 Composition API (setup / <script setup>)
1605
+ - \u4F7F\u7528 ref/reactive \u7BA1\u7406\u54CD\u5E94\u5F0F\u72B6\u6001
1606
+ - \u4F7F\u7528 computed \u521B\u5EFA\u6D3E\u751F\u72B6\u6001
1607
+ - \u9075\u5FAA Props \u5355\u5411\u6570\u636E\u6D41`,
1608
+ "Express": `### Express
1609
+ - \u4F7F\u7528\u8DEF\u7531\u5206\u7EC4\u548C\u4E2D\u95F4\u4EF6
1610
+ - \u7EDF\u4E00\u9519\u8BEF\u5904\u7406\u4E2D\u95F4\u4EF6
1611
+ - \u8F93\u5165\u9A8C\u8BC1\u4F7F\u7528 zod \u6216 joi
1612
+ - \u8DEF\u7531\u53C2\u6570\u7C7B\u578B\u5B89\u5168`,
1613
+ "NestJS": `### NestJS
1614
+ - \u9075\u5FAA\u6A21\u5757\u5316\u67B6\u6784\uFF08Module, Controller, Service\uFF09
1615
+ - \u4F7F\u7528 DTO \u8FDB\u884C\u8BF7\u6C42\u9A8C\u8BC1
1616
+ - \u5229\u7528\u4F9D\u8D56\u6CE8\u5165
1617
+ - Guards \u548C Interceptors \u5904\u7406\u6A2A\u5207\u5173\u6CE8\u70B9`,
1618
+ "Prisma": `### Prisma
1619
+ - Schema \u4FEE\u6539\u540E\u8FD0\u884C prisma generate
1620
+ - \u4F7F\u7528 prisma migrate dev \u7BA1\u7406\u8FC1\u79FB
1621
+ - \u5229\u7528 Prisma Client \u7684\u7C7B\u578B\u5B89\u5168\u67E5\u8BE2
1622
+ - \u4F7F\u7528 include/select \u4F18\u5316\u67E5\u8BE2`,
1623
+ "Drizzle ORM": `### Drizzle ORM
1624
+ - Schema \u5B9A\u4E49\u5728 TypeScript \u4E2D
1625
+ - \u4F7F\u7528 drizzle-kit \u7BA1\u7406\u8FC1\u79FB
1626
+ - \u5229\u7528\u67E5\u8BE2\u6784\u5EFA\u5668\u7684\u7C7B\u578B\u63A8\u65AD
1627
+ - \u4F7F\u7528 .where() \u94FE\u5F0F\u8C03\u7528\u6784\u5EFA\u67E5\u8BE2`,
1628
+ "Tailwind CSS": `### Tailwind CSS
1629
+ - \u4F7F\u7528 utility-first \u65B9\u5F0F\u7F16\u5199\u6837\u5F0F
1630
+ - \u590D\u6742\u7EC4\u4EF6\u63D0\u53D6\u81EA\u5B9A\u4E49\u7EC4\u4EF6\u7C7B
1631
+ - \u5229\u7528 @apply \u51CF\u5C11\u91CD\u590D
1632
+ - \u54CD\u5E94\u5F0F\u8BBE\u8BA1\u4F7F\u7528 sm/md/lg/xl \u524D\u7F00`
1633
+ };
1634
+ return guides[framework] || null;
1635
+ }
1636
+
1637
+ // src/utils/config.ts
1638
+ import Conf from "conf";
1639
+ var defaults = {
1640
+ locale: "zh",
1641
+ model: "kimi-k2.5",
1642
+ apiBaseUrl: "http://localhost:3210"
1643
+ };
1644
+ var config = new Conf({
1645
+ projectName: "devpilot",
1646
+ defaults
1647
+ });
1648
+ function getConfig() {
1649
+ return {
1650
+ locale: config.get("locale"),
1651
+ model: config.get("model"),
1652
+ apiBaseUrl: config.get("apiBaseUrl"),
1653
+ authToken: config.get("authToken"),
1654
+ userEmail: config.get("userEmail"),
1655
+ openaiApiKey: config.get("openaiApiKey"),
1656
+ anthropicApiKey: config.get("anthropicApiKey"),
1657
+ deepseekApiKey: config.get("deepseekApiKey"),
1658
+ kimiApiKey: config.get("kimiApiKey")
1659
+ };
1660
+ }
1661
+ function setConfig(key, value) {
1662
+ config.set(key, value);
1663
+ }
1664
+ function getConfigPath() {
1665
+ return config.path;
1666
+ }
1667
+
1668
+ // src/core/agent.ts
1669
+ import ora from "ora";
1670
+ var MAX_CONTEXT_MESSAGES = 50;
1671
+ var MAX_TOOL_ITERATIONS = 20;
1672
+ var Agent = class {
1673
+ messages = [];
1674
+ tools = [];
1675
+ context = null;
1676
+ isRunning = false;
1677
+ /** Accumulated token usage for the entire chat turn */
1678
+ turnUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1679
+ turnStartTime = 0;
1680
+ /**
1681
+ * Initialize the agent: register tools, build project context
1682
+ */
1683
+ async init() {
1684
+ registerAllTools();
1685
+ this.tools = getToolDefinitions();
1686
+ this.context = await buildProjectContext(process.cwd());
1687
+ const systemPrompt = generateSystemPrompt(this.context);
1688
+ this.messages = [{ role: "system", content: systemPrompt }];
1689
+ }
1690
+ /**
1691
+ * Get project context
1692
+ */
1693
+ getContext() {
1694
+ return this.context;
1695
+ }
1696
+ /**
1697
+ * Send a user message and get AI response (with tool execution loop)
1698
+ */
1699
+ async chat(userMessage) {
1700
+ if (this.isRunning) {
1701
+ printWarning("\u6B63\u5728\u5904\u7406\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u4E0A\u4E00\u6761\u6D88\u606F\u5B8C\u6210...");
1702
+ return "";
1703
+ }
1704
+ this.isRunning = true;
1705
+ this.turnUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1706
+ this.turnStartTime = Date.now();
1707
+ try {
1708
+ this.messages.push({ role: "user", content: userMessage });
1709
+ this.trimMessages();
1710
+ const response = await this.runAgentLoop();
1711
+ const duration = Date.now() - this.turnStartTime;
1712
+ if (this.turnUsage.totalTokens > 0) {
1713
+ printTokenUsage(this.turnUsage, duration);
1714
+ }
1715
+ return response;
1716
+ } finally {
1717
+ this.isRunning = false;
1718
+ }
1719
+ }
1720
+ /**
1721
+ * The main agent loop: call AI, execute tools, repeat
1722
+ */
1723
+ async runAgentLoop() {
1724
+ let iteration = 0;
1725
+ let finalResponse = "";
1726
+ while (iteration < MAX_TOOL_ITERATIONS) {
1727
+ iteration++;
1728
+ const config3 = this.getProviderConfig();
1729
+ const provider = getProvider(config3.model);
1730
+ let responseText = "";
1731
+ const toolCalls = [];
1732
+ let hasError = false;
1733
+ let reasoningContent = "";
1734
+ const spinner = ora({ text: t("thinking"), spinner: "dots" }).start();
1735
+ let spinnerStopped = false;
1736
+ try {
1737
+ for await (const chunk of provider.chatStream(this.messages, this.tools, config3)) {
1738
+ if (!spinnerStopped && (chunk.type === "text" || chunk.type === "tool_call")) {
1739
+ spinner.stop();
1740
+ spinnerStopped = true;
1741
+ if (chunk.type === "text") {
1742
+ process.stdout.write("\n");
1743
+ }
1744
+ }
1745
+ switch (chunk.type) {
1746
+ case "text":
1747
+ responseText += chunk.content || "";
1748
+ streamChunk(chunk.content || "");
1749
+ break;
1750
+ case "tool_call":
1751
+ if (chunk.toolCall) {
1752
+ toolCalls.push(chunk.toolCall);
1753
+ }
1754
+ break;
1755
+ case "error":
1756
+ hasError = true;
1757
+ printError(t("errors.apiError", { message: chunk.error || "Unknown error" }));
1758
+ break;
1759
+ case "done":
1760
+ if (chunk.usage) {
1761
+ this.turnUsage.promptTokens += chunk.usage.promptTokens;
1762
+ this.turnUsage.completionTokens += chunk.usage.completionTokens;
1763
+ this.turnUsage.totalTokens += chunk.usage.totalTokens;
1764
+ }
1765
+ if (chunk.content) {
1766
+ reasoningContent = chunk.content;
1767
+ }
1768
+ break;
1769
+ }
1770
+ }
1771
+ } catch (err) {
1772
+ if (!spinnerStopped) spinner.stop();
1773
+ const msg = err instanceof Error ? err.message : String(err);
1774
+ printError(t("errors.apiError", { message: msg }));
1775
+ return "";
1776
+ }
1777
+ if (!spinnerStopped) spinner.stop();
1778
+ if (responseText) {
1779
+ process.stdout.write("\n");
1780
+ }
1781
+ if (hasError) return "";
1782
+ if (toolCalls.length === 0) {
1783
+ const msg = { role: "assistant", content: responseText };
1784
+ if (reasoningContent) msg.reasoning_content = reasoningContent;
1785
+ this.messages.push(msg);
1786
+ finalResponse = responseText;
1787
+ break;
1788
+ }
1789
+ const assistantMsg = {
1790
+ role: "assistant",
1791
+ content: responseText || null,
1792
+ tool_calls: toolCalls
1793
+ };
1794
+ if (reasoningContent) assistantMsg.reasoning_content = reasoningContent;
1795
+ this.messages.push(assistantMsg);
1796
+ console.log();
1797
+ for (const tc of toolCalls) {
1798
+ let args;
1799
+ try {
1800
+ args = JSON.parse(tc.function.arguments);
1801
+ } catch {
1802
+ args = {};
1803
+ }
1804
+ const result = await executeTool(tc.function.name, args);
1805
+ this.messages.push({
1806
+ role: "tool",
1807
+ content: result.error || result.output,
1808
+ tool_call_id: tc.id,
1809
+ name: tc.function.name
1810
+ });
1811
+ }
1812
+ }
1813
+ if (iteration >= MAX_TOOL_ITERATIONS) {
1814
+ printWarning("\u5DF2\u8FBE\u5230\u6700\u5927\u5DE5\u5177\u8C03\u7528\u6B21\u6570\uFF0C\u505C\u6B62\u6267\u884C\u3002");
1815
+ }
1816
+ return finalResponse;
1817
+ }
1818
+ /**
1819
+ * Get provider configuration based on current settings
1820
+ */
1821
+ getProviderConfig() {
1822
+ const cfg = getConfig();
1823
+ const model = cfg.model;
1824
+ const providerName = getProviderName(model);
1825
+ let apiKey = "";
1826
+ if (cfg.authToken) {
1827
+ apiKey = cfg.authToken;
1828
+ } else {
1829
+ switch (providerName) {
1830
+ case "openai":
1831
+ apiKey = cfg.openaiApiKey || "";
1832
+ break;
1833
+ case "anthropic":
1834
+ apiKey = cfg.anthropicApiKey || "";
1835
+ break;
1836
+ case "deepseek":
1837
+ apiKey = cfg.deepseekApiKey || "";
1838
+ break;
1839
+ case "kimi":
1840
+ apiKey = cfg.kimiApiKey || "";
1841
+ break;
1842
+ case "qwen":
1843
+ apiKey = cfg.openaiApiKey || "";
1844
+ break;
1845
+ default:
1846
+ apiKey = cfg.openaiApiKey || "";
1847
+ }
1848
+ }
1849
+ let baseUrl;
1850
+ if (cfg.authToken) {
1851
+ baseUrl = `${cfg.apiBaseUrl}/api/ai/proxy`;
1852
+ } else {
1853
+ baseUrl = getDefaultBaseUrl(providerName);
1854
+ }
1855
+ return { apiKey, baseUrl, model };
1856
+ }
1857
+ /**
1858
+ * Trim old messages to prevent context overflow
1859
+ */
1860
+ trimMessages() {
1861
+ if (this.messages.length > MAX_CONTEXT_MESSAGES) {
1862
+ const systemMsg = this.messages[0];
1863
+ const recentMessages = this.messages.slice(-MAX_CONTEXT_MESSAGES + 1);
1864
+ this.messages = [systemMsg, ...recentMessages];
1865
+ printWarning(t("errors.tokenLimit"));
1866
+ }
1867
+ }
1868
+ /**
1869
+ * Clear conversation history (keep system prompt)
1870
+ */
1871
+ clearHistory() {
1872
+ const systemMsg = this.messages[0];
1873
+ this.messages = [systemMsg];
1874
+ }
1875
+ /**
1876
+ * Get current model name
1877
+ */
1878
+ getCurrentModel() {
1879
+ return getConfig().model;
1880
+ }
1881
+ /**
1882
+ * Cancel running operation
1883
+ */
1884
+ cancel() {
1885
+ this.isRunning = false;
1886
+ }
1887
+ };
1888
+
1889
+ // src/commands/chat.ts
1890
+ function question(rl, prompt) {
1891
+ return new Promise((resolve, reject) => {
1892
+ const onClose = () => reject(new Error("readline closed"));
1893
+ rl.once("close", onClose);
1894
+ rl.question(prompt, (answer) => {
1895
+ rl.removeListener("close", onClose);
1896
+ resolve(answer);
1897
+ });
1898
+ });
1899
+ }
1900
+ async function chatCommand() {
1901
+ const config3 = getConfig();
1902
+ setLocale(config3.locale);
1903
+ console.log(banner());
1904
+ const currentModel = config3.model;
1905
+ console.log(theme.dim(` \u6A21\u578B ${theme.brandBold(currentModel)} \xB7 ${t("exitHint")}`));
1906
+ const hasAnyKey = config3.authToken || config3.kimiApiKey || config3.openaiApiKey || config3.anthropicApiKey || config3.deepseekApiKey;
1907
+ if (!hasAnyKey) {
1908
+ console.log();
1909
+ printWarning("\u5C1A\u672A\u914D\u7F6EAPI\u5BC6\u94A5\u3002\u8BF7\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u4E4B\u4E00:");
1910
+ console.log(theme.dim(" sxai set-key kimi <your-key> # Kimi (\u6708\u4E4B\u6697\u9762)"));
1911
+ console.log(theme.dim(" sxai set-key deepseek <your-key>"));
1912
+ console.log(theme.dim(" sxai set-key openai <your-key>"));
1913
+ console.log(theme.dim(" sxai config # \u4EA4\u4E92\u5F0F\u914D\u7F6E"));
1914
+ }
1915
+ const agent = new Agent();
1916
+ try {
1917
+ await agent.init();
1918
+ const projectCtx = agent.getContext();
1919
+ if (projectCtx && projectCtx.projectType !== "unknown") {
1920
+ const parts = [projectCtx.projectType];
1921
+ if (projectCtx.frameworks.length > 0) {
1922
+ parts.push(projectCtx.frameworks.join(", "));
1923
+ }
1924
+ console.log(theme.dim(` \u9879\u76EE ${theme.info(parts.join(" \xB7 "))}`));
1925
+ }
1926
+ printSeparator();
1927
+ console.log();
1928
+ } catch (err) {
1929
+ printError(`\u521D\u59CB\u5316\u5931\u8D25: ${err instanceof Error ? err.message : err}`);
1930
+ process.exit(1);
1931
+ }
1932
+ const rl = readline.createInterface({
1933
+ input: process.stdin,
1934
+ output: process.stdout,
1935
+ terminal: true
1936
+ });
1937
+ registerQuestionFn((prompt2) => question(rl, prompt2));
1938
+ let ctrlCCount = 0;
1939
+ process.on("SIGINT", () => {
1940
+ ctrlCCount++;
1941
+ if (ctrlCCount >= 2) {
1942
+ console.log();
1943
+ printSuccess(t("exit"));
1944
+ process.exit(0);
1945
+ }
1946
+ console.log(theme.dim("\n(\u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA)"));
1947
+ setTimeout(() => {
1948
+ ctrlCCount = 0;
1949
+ }, 2e3);
1950
+ });
1951
+ const prompt = theme.user(t("prompt"));
1952
+ while (true) {
1953
+ let input;
1954
+ try {
1955
+ input = await question(rl, prompt);
1956
+ } catch {
1957
+ break;
1958
+ }
1959
+ input = input.trim();
1960
+ if (!input) continue;
1961
+ ctrlCCount = 0;
1962
+ if (input.startsWith("/")) {
1963
+ const result = await handleSlashCommand(input, agent);
1964
+ if (result === "exit") break;
1965
+ continue;
1966
+ }
1967
+ try {
1968
+ await agent.chat(input);
1969
+ } catch (err) {
1970
+ if (err instanceof Error && err.message.includes("API key")) {
1971
+ printError("API\u5BC6\u94A5\u672A\u914D\u7F6E\u6216\u65E0\u6548\u3002\u8BF7\u8FD0\u884C sxai config \u914D\u7F6E\u3002");
1972
+ } else if (err instanceof Error && (err.message.includes("ECONNREFUSED") || err.message.includes("fetch failed"))) {
1973
+ printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216API\u670D\u52A1\u662F\u5426\u53EF\u7528\u3002");
1974
+ } else {
1975
+ printError(`\u9519\u8BEF: ${err instanceof Error ? err.message : err}`);
1976
+ }
1977
+ }
1978
+ console.log();
1979
+ }
1980
+ rl.close();
1981
+ console.log();
1982
+ printSuccess(t("exit"));
1983
+ process.exit(0);
1984
+ }
1985
+ async function handleSlashCommand(input, agent) {
1986
+ const [cmd, ...args] = input.split(" ");
1987
+ switch (cmd) {
1988
+ case "/exit":
1989
+ case "/quit":
1990
+ case "/q":
1991
+ return "exit";
1992
+ case "/help":
1993
+ case "/h":
1994
+ console.log(t("help"));
1995
+ break;
1996
+ case "/clear":
1997
+ agent.clearHistory();
1998
+ printSuccess("\u5BF9\u8BDD\u5386\u53F2\u5DF2\u6E05\u9664\u3002");
1999
+ break;
2000
+ case "/model": {
2001
+ const models = listModels();
2002
+ const currentModel = agent.getCurrentModel();
2003
+ console.log("\n\u53EF\u7528\u6A21\u578B:");
2004
+ for (const m of models) {
2005
+ const marker = m.id === currentModel ? theme.success(" \u25C9") : " \u25CB";
2006
+ console.log(`${marker} ${theme.bold(m.name)} ${theme.dim(`(${m.id})`)}`);
2007
+ }
2008
+ if (args[0]) {
2009
+ const target = args[0];
2010
+ if (models.find((m) => m.id === target)) {
2011
+ setConfig("model", target);
2012
+ printSuccess(`\u6A21\u578B\u5DF2\u5207\u6362\u4E3A: ${target}`);
2013
+ } else {
2014
+ printError(`\u672A\u77E5\u6A21\u578B: ${target}`);
2015
+ }
2016
+ } else {
2017
+ console.log(theme.dim("\n\u4F7F\u7528 /model <model-id> \u5207\u6362\u6A21\u578B"));
2018
+ }
2019
+ break;
2020
+ }
2021
+ case "/status": {
2022
+ const ctx = agent.getContext();
2023
+ if (ctx) {
2024
+ console.log(`
2025
+ \u9879\u76EE: ${ctx.projectName || ctx.rootDir}`);
2026
+ console.log(`\u7C7B\u578B: ${ctx.projectType}`);
2027
+ console.log(`\u6846\u67B6: ${ctx.frameworks.join(", ") || "\u65E0"}`);
2028
+ console.log(`\u8BED\u8A00: ${ctx.languages.join(", ") || "\u672A\u77E5"}`);
2029
+ console.log(`\u5305\u7BA1\u7406\u5668: ${ctx.packageManager}`);
2030
+ console.log(`Git: ${ctx.hasGit ? "\u662F" : "\u5426"}`);
2031
+ }
2032
+ break;
2033
+ }
2034
+ case "/config": {
2035
+ const config3 = getConfig();
2036
+ console.log("\n\u5F53\u524D\u914D\u7F6E:");
2037
+ console.log(` \u6A21\u578B: ${config3.model}`);
2038
+ console.log(` \u8BED\u8A00: ${config3.locale}`);
2039
+ console.log(` \u540E\u7AEF: ${config3.apiBaseUrl}`);
2040
+ console.log(` \u767B\u5F55: ${config3.userEmail || "\u672A\u767B\u5F55"}`);
2041
+ console.log(` Kimi Key: ${config3.kimiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2042
+ console.log(` OpenAI Key: ${config3.openaiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2043
+ console.log(` Anthropic Key: ${config3.anthropicApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2044
+ console.log(` DeepSeek Key: ${config3.deepseekApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2045
+ break;
2046
+ }
2047
+ case "/login":
2048
+ printInfo("\u8BF7\u4F7F\u7528 sxai login \u547D\u4EE4\u767B\u5F55\u3002");
2049
+ break;
2050
+ case "/logout":
2051
+ setConfig("authToken", void 0);
2052
+ setConfig("userEmail", void 0);
2053
+ printSuccess(t("auth.logoutSuccess"));
2054
+ break;
2055
+ default:
2056
+ printError(t("errors.unknownCommand", { command: cmd }));
2057
+ }
2058
+ }
2059
+
2060
+ // src/commands/config.ts
2061
+ import inquirer from "inquirer";
2062
+ async function configCommand() {
2063
+ const config3 = getConfig();
2064
+ console.log(`
2065
+ \u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84: ${getConfigPath()}
2066
+ `);
2067
+ const { action } = await inquirer.prompt([{
2068
+ type: "list",
2069
+ name: "action",
2070
+ message: "\u9009\u62E9\u8981\u914D\u7F6E\u7684\u9879\u76EE:",
2071
+ choices: [
2072
+ { name: "\u8BBE\u7F6EAI\u6A21\u578B", value: "model" },
2073
+ { name: "\u8BBE\u7F6EKimi API Key (\u6708\u4E4B\u6697\u9762)", value: "kimi" },
2074
+ { name: "\u8BBE\u7F6EOpenAI API Key", value: "openai" },
2075
+ { name: "\u8BBE\u7F6EAnthropic API Key", value: "anthropic" },
2076
+ { name: "\u8BBE\u7F6EDeepSeek API Key", value: "deepseek" },
2077
+ { name: "\u8BBE\u7F6E\u540E\u7AEF\u670D\u52A1\u5730\u5740", value: "apiBaseUrl" },
2078
+ { name: "\u8BBE\u7F6E\u754C\u9762\u8BED\u8A00", value: "locale" },
2079
+ { name: "\u67E5\u770B\u5F53\u524D\u914D\u7F6E", value: "show" },
2080
+ { name: "\u9000\u51FA", value: "exit" }
2081
+ ]
2082
+ }]);
2083
+ switch (action) {
2084
+ case "model": {
2085
+ const models = listModels();
2086
+ const { model } = await inquirer.prompt([{
2087
+ type: "list",
2088
+ name: "model",
2089
+ message: "\u9009\u62E9AI\u6A21\u578B:",
2090
+ choices: models.map((m) => ({
2091
+ name: `${m.name} (${m.provider})`,
2092
+ value: m.id
2093
+ })),
2094
+ default: config3.model
2095
+ }]);
2096
+ setConfig("model", model);
2097
+ printSuccess(`\u6A21\u578B\u5DF2\u8BBE\u7F6E\u4E3A: ${model}`);
2098
+ break;
2099
+ }
2100
+ case "kimi": {
2101
+ const { key } = await inquirer.prompt([{
2102
+ type: "password",
2103
+ name: "key",
2104
+ message: "\u8F93\u5165Kimi API Key (\u6708\u4E4B\u6697\u9762):",
2105
+ mask: "*"
2106
+ }]);
2107
+ if (key) {
2108
+ setConfig("kimiApiKey", key);
2109
+ printSuccess("Kimi API Key \u5DF2\u4FDD\u5B58\u3002");
2110
+ }
2111
+ break;
2112
+ }
2113
+ case "openai": {
2114
+ const { key } = await inquirer.prompt([{
2115
+ type: "password",
2116
+ name: "key",
2117
+ message: "\u8F93\u5165OpenAI API Key:",
2118
+ mask: "*"
2119
+ }]);
2120
+ if (key) {
2121
+ setConfig("openaiApiKey", key);
2122
+ printSuccess("OpenAI API Key \u5DF2\u4FDD\u5B58\u3002");
2123
+ }
2124
+ break;
2125
+ }
2126
+ case "anthropic": {
2127
+ const { key } = await inquirer.prompt([{
2128
+ type: "password",
2129
+ name: "key",
2130
+ message: "\u8F93\u5165Anthropic API Key:",
2131
+ mask: "*"
2132
+ }]);
2133
+ if (key) {
2134
+ setConfig("anthropicApiKey", key);
2135
+ printSuccess("Anthropic API Key \u5DF2\u4FDD\u5B58\u3002");
2136
+ }
2137
+ break;
2138
+ }
2139
+ case "deepseek": {
2140
+ const { key } = await inquirer.prompt([{
2141
+ type: "password",
2142
+ name: "key",
2143
+ message: "\u8F93\u5165DeepSeek API Key:",
2144
+ mask: "*"
2145
+ }]);
2146
+ if (key) {
2147
+ setConfig("deepseekApiKey", key);
2148
+ printSuccess("DeepSeek API Key \u5DF2\u4FDD\u5B58\u3002");
2149
+ }
2150
+ break;
2151
+ }
2152
+ case "apiBaseUrl": {
2153
+ const { url } = await inquirer.prompt([{
2154
+ type: "input",
2155
+ name: "url",
2156
+ message: "\u8F93\u5165\u540E\u7AEF\u670D\u52A1\u5730\u5740:",
2157
+ default: config3.apiBaseUrl
2158
+ }]);
2159
+ setConfig("apiBaseUrl", url);
2160
+ printSuccess(`\u540E\u7AEF\u5730\u5740\u5DF2\u8BBE\u7F6E\u4E3A: ${url}`);
2161
+ break;
2162
+ }
2163
+ case "locale": {
2164
+ const { locale } = await inquirer.prompt([{
2165
+ type: "list",
2166
+ name: "locale",
2167
+ message: "\u9009\u62E9\u754C\u9762\u8BED\u8A00:",
2168
+ choices: [
2169
+ { name: "\u4E2D\u6587", value: "zh" },
2170
+ { name: "English", value: "en" }
2171
+ ],
2172
+ default: config3.locale
2173
+ }]);
2174
+ setConfig("locale", locale);
2175
+ printSuccess(`\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A: ${locale}`);
2176
+ break;
2177
+ }
2178
+ case "show": {
2179
+ console.log("\n\u5F53\u524D\u914D\u7F6E:");
2180
+ console.log(` \u6A21\u578B: ${config3.model}`);
2181
+ console.log(` \u8BED\u8A00: ${config3.locale}`);
2182
+ console.log(` \u540E\u7AEF: ${config3.apiBaseUrl}`);
2183
+ console.log(` \u767B\u5F55: ${config3.userEmail || "\u672A\u767B\u5F55"}`);
2184
+ console.log(` Kimi Key: ${config3.kimiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2185
+ console.log(` OpenAI Key: ${config3.openaiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2186
+ console.log(` Anthropic Key: ${config3.anthropicApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2187
+ console.log(` DeepSeek Key: ${config3.deepseekApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
2188
+ break;
2189
+ }
2190
+ case "exit":
2191
+ break;
2192
+ }
2193
+ }
2194
+
2195
+ // src/commands/login.ts
2196
+ import inquirer2 from "inquirer";
2197
+ async function loginCommand() {
2198
+ const config3 = getConfig();
2199
+ if (config3.authToken && config3.userEmail) {
2200
+ printInfo(`\u5F53\u524D\u5DF2\u767B\u5F55: ${config3.userEmail}`);
2201
+ const { relogin } = await inquirer2.prompt([{
2202
+ type: "confirm",
2203
+ name: "relogin",
2204
+ message: "\u662F\u5426\u91CD\u65B0\u767B\u5F55\uFF1F",
2205
+ default: false
2206
+ }]);
2207
+ if (!relogin) return;
2208
+ }
2209
+ console.log(theme.brandBold("\n\u{1F511} \u767B\u5F55\u6C88\u7FD4\u7684AI\u52A9\u624B\n"));
2210
+ const { email } = await inquirer2.prompt([{
2211
+ type: "input",
2212
+ name: "email",
2213
+ message: "\u90AE\u7BB1:",
2214
+ validate: (input) => {
2215
+ if (!input.includes("@")) return "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740";
2216
+ return true;
2217
+ }
2218
+ }]);
2219
+ const { password } = await inquirer2.prompt([{
2220
+ type: "password",
2221
+ name: "password",
2222
+ message: "\u5BC6\u7801:",
2223
+ mask: "*"
2224
+ }]);
2225
+ try {
2226
+ const response = await fetch(`${config3.apiBaseUrl}/api/auth/login`, {
2227
+ method: "POST",
2228
+ headers: { "Content-Type": "application/json" },
2229
+ body: JSON.stringify({ email, password })
2230
+ });
2231
+ if (response.ok) {
2232
+ const data = await response.json();
2233
+ setConfig("authToken", data.token);
2234
+ setConfig("userEmail", data.user.email);
2235
+ console.log();
2236
+ printSuccess("\u767B\u5F55\u6210\u529F\uFF01");
2237
+ console.log(theme.dim(` \u90AE\u7BB1: ${data.user.email}`));
2238
+ console.log(theme.dim(` \u8BA1\u5212: ${data.user.plan}`));
2239
+ if (data.user.role === "admin") {
2240
+ console.log(theme.error(` \u89D2\u8272: \u7BA1\u7406\u5458`));
2241
+ }
2242
+ console.log();
2243
+ } else if (response.status === 404) {
2244
+ printInfo("\u8BE5\u90AE\u7BB1\u5C1A\u672A\u6CE8\u518C");
2245
+ const { register } = await inquirer2.prompt([{
2246
+ type: "confirm",
2247
+ name: "register",
2248
+ message: "\u662F\u5426\u7ACB\u5373\u6CE8\u518C\uFF1F",
2249
+ default: true
2250
+ }]);
2251
+ if (register) {
2252
+ const regResponse = await fetch(`${config3.apiBaseUrl}/api/auth/register`, {
2253
+ method: "POST",
2254
+ headers: { "Content-Type": "application/json" },
2255
+ body: JSON.stringify({ email, password })
2256
+ });
2257
+ if (regResponse.ok) {
2258
+ const data = await regResponse.json();
2259
+ setConfig("authToken", data.token);
2260
+ setConfig("userEmail", data.user.email);
2261
+ printSuccess("\u6CE8\u518C\u5E76\u767B\u5F55\u6210\u529F\uFF01");
2262
+ } else {
2263
+ const err = await regResponse.json();
2264
+ printError(`\u6CE8\u518C\u5931\u8D25: ${err.error || "\u672A\u77E5\u9519\u8BEF"}`);
2265
+ }
2266
+ }
2267
+ } else {
2268
+ const err = await response.json();
2269
+ printError(`\u767B\u5F55\u5931\u8D25: ${err.error || "\u672A\u77E5\u9519\u8BEF"}`);
2270
+ }
2271
+ } catch (err) {
2272
+ printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u8FD0\u884C\u3002");
2273
+ console.log(theme.dim(` \u540E\u7AEF\u5730\u5740: ${config3.apiBaseUrl}`));
2274
+ console.log(theme.dim(" \u4F7F\u7528 sxai config \u4FEE\u6539\u540E\u7AEF\u5730\u5740"));
2275
+ }
2276
+ }
2277
+
2278
+ // src/commands/register.ts
2279
+ import inquirer3 from "inquirer";
2280
+ async function registerCommand() {
2281
+ const config3 = getConfig();
2282
+ if (config3.authToken && config3.userEmail) {
2283
+ printInfo(`\u5F53\u524D\u5DF2\u767B\u5F55: ${config3.userEmail}`);
2284
+ const { proceed } = await inquirer3.prompt([{
2285
+ type: "confirm",
2286
+ name: "proceed",
2287
+ message: "\u662F\u5426\u6CE8\u518C\u65B0\u8D26\u6237\uFF1F\uFF08\u5C06\u8986\u76D6\u5F53\u524D\u767B\u5F55\u72B6\u6001\uFF09",
2288
+ default: false
2289
+ }]);
2290
+ if (!proceed) return;
2291
+ }
2292
+ console.log(theme.brandBold("\n\u{1F4DD} \u6CE8\u518C\u6C88\u7FD4\u7684AI\u52A9\u624B\u8D26\u6237\n"));
2293
+ const { email } = await inquirer3.prompt([{
2294
+ type: "input",
2295
+ name: "email",
2296
+ message: "\u90AE\u7BB1:",
2297
+ validate: (input) => {
2298
+ if (!input.includes("@")) return "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740";
2299
+ return true;
2300
+ }
2301
+ }]);
2302
+ const { name } = await inquirer3.prompt([{
2303
+ type: "input",
2304
+ name: "name",
2305
+ message: "\u6635\u79F0 (\u53EF\u9009):",
2306
+ default: email.split("@")[0]
2307
+ }]);
2308
+ const { password } = await inquirer3.prompt([{
2309
+ type: "password",
2310
+ name: "password",
2311
+ message: "\u5BC6\u7801 (\u81F3\u5C116\u4F4D):",
2312
+ mask: "*",
2313
+ validate: (input) => {
2314
+ if (input.length < 6) return "\u5BC6\u7801\u81F3\u5C116\u4F4D";
2315
+ return true;
2316
+ }
2317
+ }]);
2318
+ const { confirmPassword } = await inquirer3.prompt([{
2319
+ type: "password",
2320
+ name: "confirmPassword",
2321
+ message: "\u786E\u8BA4\u5BC6\u7801:",
2322
+ mask: "*"
2323
+ }]);
2324
+ if (password !== confirmPassword) {
2325
+ printError("\u4E24\u6B21\u5BC6\u7801\u4E0D\u4E00\u81F4");
2326
+ return;
2327
+ }
2328
+ try {
2329
+ const response = await fetch(`${config3.apiBaseUrl}/api/auth/register`, {
2330
+ method: "POST",
2331
+ headers: { "Content-Type": "application/json" },
2332
+ body: JSON.stringify({ email, password, name })
2333
+ });
2334
+ if (response.ok) {
2335
+ const data = await response.json();
2336
+ setConfig("authToken", data.token);
2337
+ setConfig("userEmail", data.user.email);
2338
+ console.log();
2339
+ printSuccess("\u6CE8\u518C\u6210\u529F\uFF01");
2340
+ console.log(theme.dim(` \u90AE\u7BB1: ${data.user.email}`));
2341
+ console.log(theme.dim(` \u6635\u79F0: ${data.user.name}`));
2342
+ console.log(theme.dim(` \u8BA1\u5212: ${data.user.plan}`));
2343
+ console.log(theme.dim(` \u6BCF\u65E5\u8BF7\u6C42: ${data.user.dailyLimit}\u6B21`));
2344
+ console.log(theme.dim(` \u514D\u8D39Token: ${formatTokens(data.user.totalFreeTokens)}`));
2345
+ console.log();
2346
+ console.log(theme.info("\u73B0\u5728\u53EF\u4EE5\u76F4\u63A5\u4F7F\u7528 sxai \u5F00\u59CB\u5BF9\u8BDD\u4E86\uFF01"));
2347
+ } else {
2348
+ const err = await response.json();
2349
+ printError(`\u6CE8\u518C\u5931\u8D25: ${err.error || "\u672A\u77E5\u9519\u8BEF"}`);
2350
+ }
2351
+ } catch (err) {
2352
+ printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u8FD0\u884C\u3002");
2353
+ console.log(theme.dim(` \u540E\u7AEF\u5730\u5740: ${config3.apiBaseUrl}`));
2354
+ console.log(theme.dim(" \u4F7F\u7528 sxai config \u4FEE\u6539\u540E\u7AEF\u5730\u5740"));
2355
+ }
2356
+ }
2357
+ function formatTokens(n) {
2358
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2359
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2360
+ return String(n);
2361
+ }
2362
+
2363
+ // src/commands/account.ts
2364
+ async function accountCommand() {
2365
+ const config3 = getConfig();
2366
+ if (!config3.authToken) {
2367
+ printWarning("\u5C1A\u672A\u767B\u5F55\u3002\u8BF7\u5148\u6CE8\u518C\u6216\u767B\u5F55:");
2368
+ console.log(theme.dim(" sxai register # \u6CE8\u518C\u65B0\u8D26\u6237"));
2369
+ console.log(theme.dim(" sxai login # \u767B\u5F55\u5DF2\u6709\u8D26\u6237"));
2370
+ return;
2371
+ }
2372
+ try {
2373
+ const response = await fetch(`${config3.apiBaseUrl}/api/auth/me`, {
2374
+ headers: { "Authorization": `Bearer ${config3.authToken}` }
2375
+ });
2376
+ if (response.status === 401) {
2377
+ printError("\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55: sxai login");
2378
+ return;
2379
+ }
2380
+ if (!response.ok) {
2381
+ printError("\u83B7\u53D6\u8D26\u6237\u4FE1\u606F\u5931\u8D25");
2382
+ return;
2383
+ }
2384
+ const data = await response.json();
2385
+ const { user, quota, usage } = data;
2386
+ console.log();
2387
+ console.log(theme.brandBold(" \u{1F4CB} \u8D26\u6237\u4FE1\u606F"));
2388
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2389
+ console.log(` \u90AE\u7BB1 ${theme.info(user.email)}`);
2390
+ console.log(` \u6635\u79F0 ${user.name || "-"}`);
2391
+ console.log(` \u89D2\u8272 ${user.role === "admin" ? theme.error("\u7BA1\u7406\u5458") : "\u666E\u901A\u7528\u6237"}`);
2392
+ console.log(` \u8BA1\u5212 ${planLabel(user.plan)}`);
2393
+ console.log(` \u72B6\u6001 ${user.isActive ? theme.success("\u2713 \u6B63\u5E38") : theme.error("\u2717 \u5DF2\u7981\u7528")}`);
2394
+ console.log(` \u6CE8\u518C ${new Date(user.createdAt).toLocaleDateString("zh-CN")}`);
2395
+ if (quota) {
2396
+ console.log();
2397
+ console.log(theme.brandBold(" \u{1F4CA} \u989D\u5EA6"));
2398
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2399
+ const dailyBar = progressBar(quota.daily.used, quota.daily.limit);
2400
+ console.log(` \u4ECA\u65E5\u8BF7\u6C42 ${dailyBar} ${quota.daily.used}/${quota.daily.limit}`);
2401
+ const monthBar = progressBar(quota.monthlyTokens.used, quota.monthlyTokens.limit);
2402
+ console.log(` \u672C\u6708Token ${monthBar} ${fmtTokens(quota.monthlyTokens.used)}/${fmtTokens(quota.monthlyTokens.limit)}`);
2403
+ const lifeBar = progressBar(quota.lifetimeTokens.used, quota.lifetimeTokens.limit);
2404
+ console.log(` \u514D\u8D39\u989D\u5EA6 ${lifeBar} ${fmtTokens(quota.lifetimeTokens.used)}/${fmtTokens(quota.lifetimeTokens.limit)}`);
2405
+ if (!quota.allowed) {
2406
+ console.log();
2407
+ printWarning(quota.reason || "\u989D\u5EA6\u5DF2\u7528\u5B8C");
2408
+ }
2409
+ }
2410
+ if (usage) {
2411
+ console.log();
2412
+ console.log(theme.brandBold(" \u{1F4C8} \u7528\u91CF\u7EDF\u8BA1"));
2413
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2414
+ console.log(` \u4ECA\u65E5 ${usage.today.requests}\u6B21\u8BF7\u6C42 \xB7 ${fmtTokens(usage.today.tokens)} tokens`);
2415
+ console.log(` \u672C\u6708 ${usage.thisMonth.requests}\u6B21\u8BF7\u6C42 \xB7 ${fmtTokens(usage.thisMonth.tokens)} tokens`);
2416
+ console.log(` \u7D2F\u8BA1 ${usage.lifetime.requests}\u6B21\u8BF7\u6C42 \xB7 ${fmtTokens(usage.lifetime.tokens)} tokens`);
2417
+ }
2418
+ console.log();
2419
+ } catch (err) {
2420
+ printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u8FD0\u884C\u3002");
2421
+ console.log(theme.dim(` \u540E\u7AEF\u5730\u5740: ${config3.apiBaseUrl}`));
2422
+ }
2423
+ }
2424
+ function planLabel(plan) {
2425
+ switch (plan) {
2426
+ case "free":
2427
+ return theme.dim("\u514D\u8D39\u7248");
2428
+ case "pro":
2429
+ return theme.success("\u4E13\u4E1A\u7248");
2430
+ case "enterprise":
2431
+ return theme.info("\u4F01\u4E1A\u7248");
2432
+ default:
2433
+ return plan;
2434
+ }
2435
+ }
2436
+ function fmtTokens(n) {
2437
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2438
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2439
+ return String(n);
2440
+ }
2441
+ function progressBar(used, limit, width = 16) {
2442
+ if (limit === 0) return theme.dim("[" + "\u2591".repeat(width) + "]");
2443
+ const ratio = Math.min(1, used / limit);
2444
+ const filled = Math.round(ratio * width);
2445
+ const empty = width - filled;
2446
+ const color = ratio < 0.6 ? theme.success : ratio < 0.85 ? theme.warning : theme.error;
2447
+ return color("[" + "\u2588".repeat(filled) + "\u2591".repeat(empty) + "]");
2448
+ }
2449
+
2450
+ // src/commands/admin.ts
2451
+ import inquirer4 from "inquirer";
2452
+ async function adminCommand() {
2453
+ const config3 = getConfig();
2454
+ if (!config3.authToken) {
2455
+ printWarning("\u8BF7\u5148\u767B\u5F55\u7BA1\u7406\u5458\u8D26\u6237: sxai login");
2456
+ return;
2457
+ }
2458
+ console.log(theme.brandBold("\n\u2699\uFE0F \u7BA1\u7406\u5458\u63A7\u5236\u53F0\n"));
2459
+ const { action } = await inquirer4.prompt([{
2460
+ type: "list",
2461
+ name: "action",
2462
+ message: "\u9009\u62E9\u64CD\u4F5C:",
2463
+ choices: [
2464
+ { name: "\u{1F4CB} \u67E5\u770B\u6240\u6709\u7528\u6237", value: "list" },
2465
+ { name: "\u{1F3AF} \u8BBE\u7F6E\u7528\u6237\u989D\u5EA6", value: "quota" },
2466
+ { name: "\u{1F4CA} \u4FEE\u6539\u7528\u6237\u8BA1\u5212", value: "plan" },
2467
+ { name: "\u{1F451} \u7BA1\u7406\u7528\u6237\u89D2\u8272", value: "role" },
2468
+ { name: "\u{1F504} \u91CD\u7F6E\u7528\u91CF\u8BA1\u6570", value: "reset" },
2469
+ { name: "\u{1F6AB} \u542F\u7528/\u7981\u7528\u7528\u6237", value: "status" },
2470
+ { name: "\u{1F4C8} \u5E73\u53F0\u7EDF\u8BA1", value: "stats" },
2471
+ { name: "\u9000\u51FA", value: "exit" }
2472
+ ]
2473
+ }]);
2474
+ switch (action) {
2475
+ case "list":
2476
+ await listUsers(config3);
2477
+ break;
2478
+ case "quota":
2479
+ await setUserQuota(config3);
2480
+ break;
2481
+ case "plan":
2482
+ await setUserPlan(config3);
2483
+ break;
2484
+ case "role":
2485
+ await setUserRole(config3);
2486
+ break;
2487
+ case "reset":
2488
+ await resetUsage(config3);
2489
+ break;
2490
+ case "status":
2491
+ await toggleUserStatus(config3);
2492
+ break;
2493
+ case "stats":
2494
+ await showStats(config3);
2495
+ break;
2496
+ case "exit":
2497
+ break;
2498
+ }
2499
+ }
2500
+ async function adminFetch(config3, path5, options) {
2501
+ const response = await fetch(`${config3.apiBaseUrl}${path5}`, {
2502
+ ...options,
2503
+ headers: {
2504
+ "Content-Type": "application/json",
2505
+ "Authorization": `Bearer ${config3.authToken}`,
2506
+ ...options?.headers || {}
2507
+ }
2508
+ });
2509
+ if (response.status === 403) {
2510
+ printError("\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650\u3002\u5F53\u524D\u8D26\u6237\u4E0D\u662F\u7BA1\u7406\u5458\u3002");
2511
+ return null;
2512
+ }
2513
+ if (response.status === 401) {
2514
+ printError("\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55: sxai login");
2515
+ return null;
2516
+ }
2517
+ return response;
2518
+ }
2519
+ async function listUsers(config3) {
2520
+ try {
2521
+ const resp = await adminFetch(config3, "/api/admin/users");
2522
+ if (!resp) return;
2523
+ const data = await resp.json();
2524
+ console.log(theme.dim(`
2525
+ \u5171 ${data.pagination.total} \u4E2A\u7528\u6237
2526
+ `));
2527
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2528
+ console.log(` ${"\u90AE\u7BB1".padEnd(28)} ${"\u8BA1\u5212".padEnd(8)} ${"\u89D2\u8272".padEnd(8)} ${"\u65E5\u9650".padEnd(6)} ${"\u514D\u8D39\u989D\u5EA6".padEnd(12)} ${"\u5DF2\u7528"}`);
2529
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2530
+ for (const u of data.users) {
2531
+ const status = u.isActive ? "" : theme.error(" [\u7981\u7528]");
2532
+ const roleTag = u.role === "admin" ? theme.error("\u7BA1\u7406\u5458") : "\u7528\u6237 ";
2533
+ const planTag = u.plan === "free" ? theme.dim("\u514D\u8D39 ") : u.plan === "pro" ? theme.success("\u4E13\u4E1A ") : theme.info("\u4F01\u4E1A ");
2534
+ console.log(
2535
+ ` ${(u.email + status).padEnd(28)} ${planTag} ${roleTag} ${String(u.dailyLimit).padEnd(6)} ${fmtTokens2(u.totalFreeTokens).padEnd(12)} ${fmtTokens2(u.totalTokensUsed)}`
2536
+ );
2537
+ }
2538
+ console.log();
2539
+ } catch {
2540
+ printError("\u83B7\u53D6\u7528\u6237\u5217\u8868\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2541
+ }
2542
+ }
2543
+ async function setUserQuota(config3) {
2544
+ const users = await fetchUserList(config3);
2545
+ if (!users) return;
2546
+ const { userId } = await inquirer4.prompt([{
2547
+ type: "list",
2548
+ name: "userId",
2549
+ message: "\u9009\u62E9\u7528\u6237:",
2550
+ choices: users.map((u) => ({ name: `${u.email} (${u.plan})`, value: u.id }))
2551
+ }]);
2552
+ const selectedUser = users.find((u) => u.id === userId);
2553
+ console.log(theme.dim(`
2554
+ \u5F53\u524D\u989D\u5EA6: \u65E5\u9650=${selectedUser.dailyLimit}\u6B21 \u6708Token=${fmtTokens2(selectedUser.monthlyTokenLimit)} \u514D\u8D39\u603B\u989D=${fmtTokens2(selectedUser.totalFreeTokens)}
2555
+ `));
2556
+ const { dailyLimit } = await inquirer4.prompt([{
2557
+ type: "number",
2558
+ name: "dailyLimit",
2559
+ message: "\u6BCF\u65E5\u8BF7\u6C42\u4E0A\u9650:",
2560
+ default: selectedUser.dailyLimit
2561
+ }]);
2562
+ const { monthlyTokenLimit } = await inquirer4.prompt([{
2563
+ type: "number",
2564
+ name: "monthlyTokenLimit",
2565
+ message: "\u6BCF\u6708Token\u4E0A\u9650:",
2566
+ default: selectedUser.monthlyTokenLimit
2567
+ }]);
2568
+ const { totalFreeTokens } = await inquirer4.prompt([{
2569
+ type: "number",
2570
+ name: "totalFreeTokens",
2571
+ message: "\u514D\u8D39Token\u603B\u989D:",
2572
+ default: selectedUser.totalFreeTokens
2573
+ }]);
2574
+ try {
2575
+ const resp = await adminFetch(config3, `/api/admin/users/${userId}/quota`, {
2576
+ method: "PUT",
2577
+ body: JSON.stringify({ dailyLimit, monthlyTokenLimit, totalFreeTokens })
2578
+ });
2579
+ if (!resp) return;
2580
+ if (resp.ok) {
2581
+ const data = await resp.json();
2582
+ printSuccess(data.message);
2583
+ } else {
2584
+ const err = await resp.json();
2585
+ printError(err.error);
2586
+ }
2587
+ } catch {
2588
+ printError("\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2589
+ }
2590
+ }
2591
+ async function setUserPlan(config3) {
2592
+ const users = await fetchUserList(config3);
2593
+ if (!users) return;
2594
+ const { userId } = await inquirer4.prompt([{
2595
+ type: "list",
2596
+ name: "userId",
2597
+ message: "\u9009\u62E9\u7528\u6237:",
2598
+ choices: users.map((u) => ({ name: `${u.email} (\u5F53\u524D: ${u.plan})`, value: u.id }))
2599
+ }]);
2600
+ const { plan } = await inquirer4.prompt([{
2601
+ type: "list",
2602
+ name: "plan",
2603
+ message: "\u9009\u62E9\u8BA1\u5212:",
2604
+ choices: [
2605
+ { name: "\u514D\u8D39\u7248 (50\u6B21/\u5929, 500K tokens/\u6708, 1M \u603B\u989D)", value: "free" },
2606
+ { name: "\u4E13\u4E1A\u7248 (500\u6B21/\u5929, 5M tokens/\u6708, 50M \u603B\u989D)", value: "pro" },
2607
+ { name: "\u4F01\u4E1A\u7248 (5000\u6B21/\u5929, 50M tokens/\u6708, 500M \u603B\u989D)", value: "enterprise" }
2608
+ ]
2609
+ }]);
2610
+ try {
2611
+ const resp = await adminFetch(config3, `/api/admin/users/${userId}/plan`, {
2612
+ method: "PUT",
2613
+ body: JSON.stringify({ plan })
2614
+ });
2615
+ if (!resp) return;
2616
+ if (resp.ok) {
2617
+ const data = await resp.json();
2618
+ printSuccess(data.message);
2619
+ } else {
2620
+ const err = await resp.json();
2621
+ printError(err.error);
2622
+ }
2623
+ } catch {
2624
+ printError("\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2625
+ }
2626
+ }
2627
+ async function setUserRole(config3) {
2628
+ const users = await fetchUserList(config3);
2629
+ if (!users) return;
2630
+ const { userId } = await inquirer4.prompt([{
2631
+ type: "list",
2632
+ name: "userId",
2633
+ message: "\u9009\u62E9\u7528\u6237:",
2634
+ choices: users.map((u) => ({
2635
+ name: `${u.email} (${u.role === "admin" ? "\u7BA1\u7406\u5458" : "\u666E\u901A\u7528\u6237"})`,
2636
+ value: u.id
2637
+ }))
2638
+ }]);
2639
+ const { role } = await inquirer4.prompt([{
2640
+ type: "list",
2641
+ name: "role",
2642
+ message: "\u8BBE\u7F6E\u89D2\u8272:",
2643
+ choices: [
2644
+ { name: "\u666E\u901A\u7528\u6237", value: "user" },
2645
+ { name: "\u7BA1\u7406\u5458", value: "admin" }
2646
+ ]
2647
+ }]);
2648
+ try {
2649
+ const resp = await adminFetch(config3, `/api/admin/users/${userId}/role`, {
2650
+ method: "PUT",
2651
+ body: JSON.stringify({ role })
2652
+ });
2653
+ if (!resp) return;
2654
+ if (resp.ok) {
2655
+ const data = await resp.json();
2656
+ printSuccess(data.message);
2657
+ } else {
2658
+ const err = await resp.json();
2659
+ printError(err.error);
2660
+ }
2661
+ } catch {
2662
+ printError("\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2663
+ }
2664
+ }
2665
+ async function resetUsage(config3) {
2666
+ const users = await fetchUserList(config3);
2667
+ if (!users) return;
2668
+ const { userId } = await inquirer4.prompt([{
2669
+ type: "list",
2670
+ name: "userId",
2671
+ message: "\u9009\u62E9\u7528\u6237:",
2672
+ choices: users.map((u) => ({
2673
+ name: `${u.email} (\u5DF2\u7528 ${fmtTokens2(u.totalTokensUsed)} tokens)`,
2674
+ value: u.id
2675
+ }))
2676
+ }]);
2677
+ const { confirm } = await inquirer4.prompt([{
2678
+ type: "confirm",
2679
+ name: "confirm",
2680
+ message: "\u786E\u5B9A\u8981\u91CD\u7F6E\u8BE5\u7528\u6237\u7684\u7D2F\u8BA1\u7528\u91CF\u5417\uFF1F",
2681
+ default: false
2682
+ }]);
2683
+ if (!confirm) return;
2684
+ try {
2685
+ const resp = await adminFetch(config3, `/api/admin/users/${userId}/reset-usage`, {
2686
+ method: "POST"
2687
+ });
2688
+ if (!resp) return;
2689
+ if (resp.ok) {
2690
+ printSuccess("\u7528\u91CF\u5DF2\u91CD\u7F6E");
2691
+ } else {
2692
+ const err = await resp.json();
2693
+ printError(err.error);
2694
+ }
2695
+ } catch {
2696
+ printError("\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2697
+ }
2698
+ }
2699
+ async function toggleUserStatus(config3) {
2700
+ const users = await fetchUserList(config3);
2701
+ if (!users) return;
2702
+ const { userId } = await inquirer4.prompt([{
2703
+ type: "list",
2704
+ name: "userId",
2705
+ message: "\u9009\u62E9\u7528\u6237:",
2706
+ choices: users.map((u) => ({
2707
+ name: `${u.email} (${u.isActive ? "\u6B63\u5E38" : "\u5DF2\u7981\u7528"})`,
2708
+ value: u.id
2709
+ }))
2710
+ }]);
2711
+ const user = users.find((u) => u.id === userId);
2712
+ const newStatus = !user.isActive;
2713
+ try {
2714
+ const resp = await adminFetch(config3, `/api/admin/users/${userId}/status`, {
2715
+ method: "PUT",
2716
+ body: JSON.stringify({ isActive: newStatus })
2717
+ });
2718
+ if (!resp) return;
2719
+ if (resp.ok) {
2720
+ const data = await resp.json();
2721
+ printSuccess(data.message);
2722
+ } else {
2723
+ const err = await resp.json();
2724
+ printError(err.error);
2725
+ }
2726
+ } catch {
2727
+ printError("\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2728
+ }
2729
+ }
2730
+ async function showStats(config3) {
2731
+ try {
2732
+ const resp = await adminFetch(config3, "/api/admin/stats");
2733
+ if (!resp) return;
2734
+ const data = await resp.json();
2735
+ console.log(theme.brandBold("\n \u{1F4C8} \u5E73\u53F0\u7EDF\u8BA1"));
2736
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2737
+ console.log(` \u7528\u6237\u603B\u6570 ${data.users.total}`);
2738
+ console.log(` \u6D3B\u8DC3\u7528\u6237 ${data.users.active}`);
2739
+ console.log(` \u603B\u8BF7\u6C42\u6570 ${data.usage.totalRequests}`);
2740
+ console.log(` \u603BToken\u7528\u91CF ${fmtTokens2(Number(data.usage.totalTokens))}`);
2741
+ console.log();
2742
+ } catch {
2743
+ printError("\u83B7\u53D6\u7EDF\u8BA1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
2744
+ }
2745
+ }
2746
+ async function fetchUserList(config3) {
2747
+ try {
2748
+ const resp = await adminFetch(config3, "/api/admin/users?limit=100");
2749
+ if (!resp) return null;
2750
+ const data = await resp.json();
2751
+ if (!data.users || data.users.length === 0) {
2752
+ printInfo("\u6682\u65E0\u7528\u6237");
2753
+ return null;
2754
+ }
2755
+ return data.users;
2756
+ } catch {
2757
+ printError("\u83B7\u53D6\u7528\u6237\u5217\u8868\u5931\u8D25");
2758
+ return null;
2759
+ }
2760
+ }
2761
+ function fmtTokens2(n) {
2762
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2763
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
2764
+ return String(n);
2765
+ }
2766
+
2767
+ // src/index.ts
2768
+ var program = new Command();
2769
+ var config2 = getConfig();
2770
+ setLocale(config2.locale);
2771
+ program.name("sxai").description(t("description")).version("0.1.0");
2772
+ program.command("chat", { isDefault: true }).description("\u542F\u52A8\u4EA4\u4E92\u5F0FAI\u5BF9\u8BDD\uFF08\u9ED8\u8BA4\u547D\u4EE4\uFF09").option("-m, --model <model>", "\u6307\u5B9AAI\u6A21\u578B", config2.model).action(async (options) => {
2773
+ if (options.model && options.model !== config2.model) {
2774
+ setConfig("model", options.model);
2775
+ }
2776
+ await chatCommand();
2777
+ });
2778
+ program.command("register").description("\u6CE8\u518C\u65B0\u8D26\u6237").action(async () => {
2779
+ await registerCommand();
2780
+ });
2781
+ program.command("login").description("\u767B\u5F55\u8D26\u6237").action(async () => {
2782
+ await loginCommand();
2783
+ });
2784
+ program.command("logout").description("\u9000\u51FA\u767B\u5F55").action(() => {
2785
+ setConfig("authToken", void 0);
2786
+ setConfig("userEmail", void 0);
2787
+ printSuccess("\u5DF2\u9000\u51FA\u767B\u5F55");
2788
+ });
2789
+ program.command("account").alias("me").description("\u67E5\u770B\u8D26\u6237\u4FE1\u606F\u3001\u989D\u5EA6\u548C\u7528\u91CF").action(async () => {
2790
+ await accountCommand();
2791
+ });
2792
+ program.command("admin").description("\u7BA1\u7406\u5458\u63A7\u5236\u53F0\uFF08\u7BA1\u7406\u7528\u6237\u3001\u8BBE\u7F6E\u989D\u5EA6\uFF09").action(async () => {
2793
+ await adminCommand();
2794
+ });
2795
+ program.command("config").description("\u914D\u7F6E\u6C88\u7FD4\u7684AI\u52A9\u624B").action(async () => {
2796
+ await configCommand();
2797
+ });
2798
+ program.command("set-key <provider> <key>").description("\u8BBE\u7F6EAPI\u5BC6\u94A5 (provider: openai, anthropic, deepseek, kimi)").action((provider, key) => {
2799
+ const keyMap = {
2800
+ openai: "openaiApiKey",
2801
+ anthropic: "anthropicApiKey",
2802
+ deepseek: "deepseekApiKey",
2803
+ kimi: "kimiApiKey"
2804
+ };
2805
+ const configKey = keyMap[provider];
2806
+ if (!configKey) {
2807
+ console.error(`\u672A\u77E5\u7684\u63D0\u4F9B\u5546: ${provider}\u3002\u652F\u6301: openai, anthropic, deepseek, kimi`);
2808
+ process.exit(1);
2809
+ }
2810
+ setConfig(configKey, key);
2811
+ printSuccess(`${provider} API Key \u5DF2\u4FDD\u5B58\u3002`);
2812
+ });
2813
+ program.parse();