jinzd-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 +4439 -0
  2. package/package.json +81 -0
package/dist/index.js ADDED
@@ -0,0 +1,4439 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { program } from "commander";
5
+
6
+ // src/config/config-manager.ts
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+
11
+ // src/config/schema.ts
12
+ import { z } from "zod";
13
+ var CustomModelSchema = z.object({
14
+ id: z.string(),
15
+ displayName: z.string().optional(),
16
+ contextWindow: z.number().optional()
17
+ });
18
+ var CustomProviderConfigSchema = z.object({
19
+ id: z.string(),
20
+ // 唯一 ID,不能与内置 provider 重名
21
+ displayName: z.string(),
22
+ // 显示名称
23
+ apiKey: z.string().optional(),
24
+ // 可选:直接在此写 key(也可通过 apiKeys 字段或环境变量提供)
25
+ baseUrl: z.string(),
26
+ // OpenAI 兼容 API 的 base URL(必填)
27
+ defaultModel: z.string(),
28
+ // 默认使用的模型 ID
29
+ models: z.array(CustomModelSchema).default([]),
30
+ timeout: z.number().optional()
31
+ // 请求超时(ms),覆盖全局默认值
32
+ });
33
+ var ModelParamsSchema = z.object({
34
+ temperature: z.number().min(0).max(2).optional(),
35
+ maxTokens: z.number().int().positive().optional(),
36
+ timeout: z.number().int().positive().optional(),
37
+ /** 是否启用深度思考(thinking)模式,GLM-5 等模型专用 */
38
+ thinking: z.boolean().optional()
39
+ });
40
+ var ConfigSchema = z.object({
41
+ version: z.string().default("1.0.0"),
42
+ defaultProvider: z.string().default("claude"),
43
+ // 每个 provider 的默认模型(key 为 provider ID)
44
+ defaultModels: z.record(z.string()).default({}),
45
+ // API Keys:放宽为任意 record,支持自定义 provider ID
46
+ apiKeys: z.record(z.string()).default({}),
47
+ customBaseUrls: z.record(z.string()).default({}),
48
+ // Per-provider timeout in ms (e.g. { deepseek: 60000 })
49
+ timeouts: z.record(z.number()).default({}),
50
+ // 自定义 Provider 列表(OpenAI 兼容接口,无需改代码)
51
+ customProviders: z.array(CustomProviderConfigSchema).default([]),
52
+ // 按模型 ID 存储的推理参数(key 为模型 ID,如 "deepseek-chat")
53
+ modelParams: z.record(ModelParamsSchema).default({}),
54
+ ui: z.object({
55
+ streaming: z.boolean().default(true),
56
+ markdownRendering: z.boolean().default(true),
57
+ showTokenCount: z.boolean().default(false)
58
+ }).default({}),
59
+ session: z.object({
60
+ autoSave: z.boolean().default(true),
61
+ maxHistoryDays: z.number().default(30),
62
+ systemPrompt: z.string().optional()
63
+ }).default({}),
64
+ // 项目上下文文件配置
65
+ // 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md 机制
66
+ // 默认按顺序查找:AICLI.md → CLAUDE.md → .aicli/context.md
67
+ // 设为 false 可禁用此功能
68
+ contextFile: z.union([z.string(), z.literal(false)]).default("auto")
69
+ });
70
+
71
+ // src/config/env-loader.ts
72
+ var ENV_KEY_MAP = {
73
+ claude: "AICLI_API_KEY_CLAUDE",
74
+ gemini: "AICLI_API_KEY_GEMINI",
75
+ deepseek: "AICLI_API_KEY_DEEPSEEK",
76
+ zhipu: "AICLI_API_KEY_ZHIPU",
77
+ kimi: "AICLI_API_KEY_KIMI"
78
+ };
79
+ var EnvLoader = class {
80
+ /**
81
+ * 读取指定 provider 的 API Key 环境变量。
82
+ * 优先级:固定映射(如 AICLI_API_KEY_CLAUDE)> 动态格式(AICLI_API_KEY_<ID大写>)
83
+ * 自定义 provider 示例:id="siliconflow" → 读取 AICLI_API_KEY_SILICONFLOW
84
+ */
85
+ static getApiKey(providerId) {
86
+ const fixedEnvVar = ENV_KEY_MAP[providerId];
87
+ if (fixedEnvVar) {
88
+ const val = process.env[fixedEnvVar];
89
+ if (val) return val;
90
+ }
91
+ const dynamicEnvVar = `AICLI_API_KEY_${providerId.toUpperCase().replace(/-/g, "_")}`;
92
+ return process.env[dynamicEnvVar] || void 0;
93
+ }
94
+ static getDefaultProvider() {
95
+ return process.env["AICLI_PROVIDER"] || void 0;
96
+ }
97
+ static isStreamingDisabled() {
98
+ return process.env["AICLI_NO_STREAM"] === "1";
99
+ }
100
+ };
101
+
102
+ // src/core/constants.ts
103
+ var VERSION = "0.1.0";
104
+ var CONFIG_DIR_NAME = ".aicli";
105
+ var CONFIG_FILE_NAME = "config.json";
106
+ var HISTORY_DIR_NAME = "history";
107
+ var PLUGINS_DIR_NAME = "plugins";
108
+ var AUTHOR = "\u664B\u6B63\u4E1C";
109
+ var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
110
+ var DESCRIPTION = "\u8DE8\u5E73\u53F0 REPL \u98CE\u683C AI \u5BF9\u8BDD\u5DE5\u5177\uFF0C\u652F\u6301\u591A Provider \u4E0E Agentic \u5DE5\u5177\u8C03\u7528";
111
+
112
+ // src/core/errors.ts
113
+ var AiCliError = class extends Error {
114
+ constructor(message) {
115
+ super(message);
116
+ this.name = "AiCliError";
117
+ }
118
+ };
119
+ var ProviderError = class extends AiCliError {
120
+ constructor(providerId, message, cause) {
121
+ super(`[${providerId}] ${message}`);
122
+ this.providerId = providerId;
123
+ this.cause = cause;
124
+ this.name = "ProviderError";
125
+ }
126
+ };
127
+ var AuthError = class extends ProviderError {
128
+ constructor(providerId) {
129
+ super(providerId, "Invalid or missing API key. Run: ai-cli config");
130
+ this.name = "AuthError";
131
+ }
132
+ };
133
+ var RateLimitError = class extends ProviderError {
134
+ constructor(providerId) {
135
+ super(providerId, "Rate limit exceeded. Please wait before trying again.");
136
+ this.name = "RateLimitError";
137
+ }
138
+ };
139
+ var ConfigError = class extends AiCliError {
140
+ constructor(message) {
141
+ super(message);
142
+ this.name = "ConfigError";
143
+ }
144
+ };
145
+ var ProviderNotFoundError = class extends AiCliError {
146
+ constructor(providerId) {
147
+ super(
148
+ `Provider '${providerId}' is not configured. Run: ai-cli config`
149
+ );
150
+ this.name = "ProviderNotFoundError";
151
+ }
152
+ };
153
+
154
+ // src/config/config-manager.ts
155
+ var ConfigManager = class {
156
+ configDir;
157
+ configPath;
158
+ config;
159
+ constructor(configDir) {
160
+ this.configDir = configDir ?? join(homedir(), CONFIG_DIR_NAME);
161
+ this.configPath = join(this.configDir, CONFIG_FILE_NAME);
162
+ this.config = this.load();
163
+ }
164
+ load() {
165
+ if (!existsSync(this.configPath)) {
166
+ return ConfigSchema.parse({});
167
+ }
168
+ try {
169
+ const raw = JSON.parse(readFileSync(this.configPath, "utf-8"));
170
+ return ConfigSchema.parse(raw);
171
+ } catch (err) {
172
+ throw new ConfigError(
173
+ `Config file at ${this.configPath} is invalid. Delete it and run 'ai-cli config' to recreate.
174
+ ${err}`
175
+ );
176
+ }
177
+ }
178
+ save() {
179
+ mkdirSync(this.configDir, { recursive: true });
180
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
181
+ }
182
+ getApiKey(providerId) {
183
+ const envKey = EnvLoader.getApiKey(providerId);
184
+ if (envKey) return envKey;
185
+ return this.config.apiKeys[providerId];
186
+ }
187
+ setApiKey(providerId, key) {
188
+ this.config.apiKeys[providerId] = key;
189
+ this.save();
190
+ }
191
+ get(key) {
192
+ return this.config[key];
193
+ }
194
+ set(key, value) {
195
+ this.config[key] = value;
196
+ this.save();
197
+ }
198
+ isFirstRun() {
199
+ return !existsSync(this.configPath);
200
+ }
201
+ getConfigDir() {
202
+ return this.configDir;
203
+ }
204
+ getHistoryDir() {
205
+ return join(this.configDir, HISTORY_DIR_NAME);
206
+ }
207
+ getPluginsDir() {
208
+ return join(this.configDir, PLUGINS_DIR_NAME);
209
+ }
210
+ getDefaultProvider() {
211
+ return EnvLoader.getDefaultProvider() ?? this.config.defaultProvider;
212
+ }
213
+ };
214
+
215
+ // src/providers/claude.ts
216
+ import Anthropic from "@anthropic-ai/sdk";
217
+
218
+ // src/providers/base.ts
219
+ var BaseProvider = class {
220
+ /**
221
+ * 将 Message[] 转换为 OpenAI API 格式的消息数组。
222
+ * content 为 string 时直接传递;为 MessageContentPart[] 时保留数组格式(vision 请求)。
223
+ */
224
+ normalizeMessages(messages) {
225
+ return messages.map((m) => ({ role: m.role, content: m.content }));
226
+ }
227
+ };
228
+
229
+ // src/providers/claude.ts
230
+ var ClaudeProvider = class extends BaseProvider {
231
+ client;
232
+ info = {
233
+ id: "claude",
234
+ displayName: "Claude (Anthropic)",
235
+ defaultModel: "claude-sonnet-4-5-20250929",
236
+ apiKeyEnvVar: "AICLI_API_KEY_CLAUDE",
237
+ requiresApiKey: true,
238
+ models: [
239
+ {
240
+ id: "claude-opus-4-6",
241
+ displayName: "Claude Opus 4.6",
242
+ contextWindow: 2e5,
243
+ supportsStreaming: true
244
+ },
245
+ {
246
+ id: "claude-sonnet-4-5-20250929",
247
+ displayName: "Claude Sonnet 4.5",
248
+ contextWindow: 2e5,
249
+ supportsStreaming: true
250
+ },
251
+ {
252
+ id: "claude-haiku-4-5-20251001",
253
+ displayName: "Claude Haiku 4.5",
254
+ contextWindow: 2e5,
255
+ supportsStreaming: true
256
+ }
257
+ ]
258
+ };
259
+ async initialize(apiKey, options) {
260
+ this.client = new Anthropic({
261
+ apiKey,
262
+ baseURL: options?.baseUrl
263
+ });
264
+ }
265
+ async chat(request) {
266
+ try {
267
+ const messages = request.messages.filter((m) => m.role !== "system").map((m) => ({
268
+ role: m.role,
269
+ content: m.content
270
+ }));
271
+ const response = await this.client.messages.create({
272
+ model: request.model,
273
+ messages,
274
+ system: request.systemPrompt,
275
+ max_tokens: request.maxTokens ?? 8192,
276
+ temperature: request.temperature
277
+ });
278
+ const content = response.content[0].type === "text" ? response.content[0].text : "";
279
+ return {
280
+ content,
281
+ model: response.model,
282
+ usage: {
283
+ inputTokens: response.usage.input_tokens,
284
+ outputTokens: response.usage.output_tokens
285
+ }
286
+ };
287
+ } catch (err) {
288
+ throw this.wrapError(err);
289
+ }
290
+ }
291
+ async *chatStream(request) {
292
+ try {
293
+ const messages = request.messages.filter((m) => m.role !== "system").map((m) => ({
294
+ role: m.role,
295
+ content: m.content
296
+ }));
297
+ const stream = this.client.messages.stream({
298
+ model: request.model,
299
+ messages,
300
+ system: request.systemPrompt,
301
+ max_tokens: request.maxTokens ?? 8192
302
+ });
303
+ for await (const event of stream) {
304
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
305
+ yield { delta: event.delta.text, done: false };
306
+ }
307
+ if (event.type === "message_stop") {
308
+ yield { delta: "", done: true };
309
+ }
310
+ }
311
+ } catch (err) {
312
+ throw this.wrapError(err);
313
+ }
314
+ }
315
+ async chatWithTools(request, tools) {
316
+ try {
317
+ const anthropicTools = tools.map((t) => ({
318
+ name: t.name,
319
+ description: t.description,
320
+ input_schema: {
321
+ type: "object",
322
+ properties: Object.fromEntries(
323
+ Object.entries(t.parameters).map(([key, schema]) => [
324
+ key,
325
+ {
326
+ type: schema.type,
327
+ description: schema.description,
328
+ ...schema.enum ? { enum: schema.enum } : {}
329
+ }
330
+ ])
331
+ ),
332
+ required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
333
+ }
334
+ }));
335
+ const baseMessages = request.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
336
+ const extraMessages = request._extraMessages ?? [];
337
+ const allMessages = [...baseMessages, ...extraMessages];
338
+ const response = await this.client.messages.create({
339
+ model: request.model,
340
+ messages: allMessages,
341
+ tools: anthropicTools,
342
+ system: request.systemPrompt,
343
+ max_tokens: request.maxTokens ?? 8192,
344
+ temperature: request.temperature
345
+ });
346
+ const usage = {
347
+ inputTokens: response.usage.input_tokens,
348
+ outputTokens: response.usage.output_tokens
349
+ };
350
+ const toolUseBlocks = response.content.filter(
351
+ (b) => b.type === "tool_use"
352
+ );
353
+ if (toolUseBlocks.length > 0) {
354
+ const toolCalls = toolUseBlocks.map((b) => ({
355
+ id: b.id,
356
+ name: b.name,
357
+ arguments: b.input
358
+ }));
359
+ return { toolCalls, usage };
360
+ }
361
+ const textBlock = response.content.find((b) => b.type === "text");
362
+ return { content: textBlock?.text ?? "", usage };
363
+ } catch (err) {
364
+ throw this.wrapError(err);
365
+ }
366
+ }
367
+ buildToolResultMessages(assistantToolCalls, results) {
368
+ const assistantContent = assistantToolCalls.map((tc) => ({
369
+ type: "tool_use",
370
+ id: tc.id,
371
+ name: tc.name,
372
+ input: tc.arguments
373
+ }));
374
+ const userContent = results.map((r) => ({
375
+ type: "tool_result",
376
+ tool_use_id: r.callId,
377
+ content: r.content,
378
+ is_error: r.isError
379
+ }));
380
+ return [
381
+ { role: "assistant", content: assistantContent },
382
+ { role: "user", content: userContent }
383
+ ];
384
+ }
385
+ async validateApiKey(apiKey) {
386
+ try {
387
+ const testClient = new Anthropic({ apiKey });
388
+ await testClient.models.list();
389
+ return true;
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+ async listModels() {
395
+ return this.info.models;
396
+ }
397
+ wrapError(err) {
398
+ if (err instanceof Anthropic.AuthenticationError) {
399
+ return new AuthError("claude");
400
+ }
401
+ if (err instanceof Anthropic.RateLimitError) {
402
+ return new RateLimitError("claude");
403
+ }
404
+ if (err instanceof Error) {
405
+ return new ProviderError("claude", err.message, err);
406
+ }
407
+ return new ProviderError("claude", String(err));
408
+ }
409
+ };
410
+
411
+ // src/providers/gemini.ts
412
+ import { GoogleGenerativeAI } from "@google/generative-ai";
413
+
414
+ // src/core/types.ts
415
+ function getContentText(content) {
416
+ if (typeof content === "string") return content;
417
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("");
418
+ }
419
+
420
+ // src/providers/gemini.ts
421
+ var GeminiProvider = class extends BaseProvider {
422
+ client;
423
+ info = {
424
+ id: "gemini",
425
+ displayName: "Gemini (Google)",
426
+ defaultModel: "gemini-2.0-flash",
427
+ apiKeyEnvVar: "AICLI_API_KEY_GEMINI",
428
+ requiresApiKey: true,
429
+ models: [
430
+ {
431
+ id: "gemini-2.0-flash",
432
+ displayName: "Gemini 2.0 Flash",
433
+ contextWindow: 1e6,
434
+ supportsStreaming: true
435
+ },
436
+ {
437
+ id: "gemini-2.0-flash-lite",
438
+ displayName: "Gemini 2.0 Flash Lite",
439
+ contextWindow: 1e6,
440
+ supportsStreaming: true
441
+ },
442
+ {
443
+ id: "gemini-1.5-pro",
444
+ displayName: "Gemini 1.5 Pro",
445
+ contextWindow: 2e6,
446
+ supportsStreaming: true
447
+ }
448
+ ]
449
+ };
450
+ async initialize(apiKey) {
451
+ this.client = new GoogleGenerativeAI(apiKey);
452
+ }
453
+ toGeminiHistory(messages) {
454
+ return messages.filter((m) => m.role !== "system").map((m) => ({
455
+ role: m.role === "assistant" ? "model" : "user",
456
+ parts: [{ text: getContentText(m.content) }]
457
+ }));
458
+ }
459
+ async chat(request) {
460
+ try {
461
+ const genModel = this.client.getGenerativeModel({
462
+ model: request.model,
463
+ systemInstruction: request.systemPrompt
464
+ });
465
+ const history = this.toGeminiHistory(request.messages.slice(0, -1));
466
+ const lastMessage = getContentText(request.messages[request.messages.length - 1].content);
467
+ const chat = genModel.startChat({ history });
468
+ const result = await chat.sendMessage(lastMessage);
469
+ return {
470
+ content: result.response.text(),
471
+ model: request.model
472
+ };
473
+ } catch (err) {
474
+ throw this.wrapError(err);
475
+ }
476
+ }
477
+ async *chatStream(request) {
478
+ try {
479
+ const genModel = this.client.getGenerativeModel({
480
+ model: request.model,
481
+ systemInstruction: request.systemPrompt
482
+ });
483
+ const history = this.toGeminiHistory(request.messages.slice(0, -1));
484
+ const lastMessage = getContentText(request.messages[request.messages.length - 1].content);
485
+ const chat = genModel.startChat({ history });
486
+ const result = await chat.sendMessageStream(lastMessage);
487
+ for await (const chunk of result.stream) {
488
+ yield { delta: chunk.text(), done: false };
489
+ }
490
+ yield { delta: "", done: true };
491
+ } catch (err) {
492
+ throw this.wrapError(err);
493
+ }
494
+ }
495
+ async chatWithTools(request, tools) {
496
+ try {
497
+ const geminiTools = [{
498
+ functionDeclarations: tools.map((t) => ({
499
+ name: t.name,
500
+ description: t.description,
501
+ parameters: {
502
+ type: "object",
503
+ properties: Object.fromEntries(
504
+ Object.entries(t.parameters).map(([key, schema]) => [
505
+ key,
506
+ {
507
+ type: schema.type,
508
+ description: schema.description,
509
+ ...schema.enum ? { enum: schema.enum } : {}
510
+ }
511
+ ])
512
+ ),
513
+ required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
514
+ }
515
+ }))
516
+ }];
517
+ const baseHistory = this.toGeminiHistory(request.messages.slice(0, -1));
518
+ const lastMessage = getContentText(
519
+ request.messages[request.messages.length - 1].content
520
+ );
521
+ const extraHistory = request._extraMessages ?? [];
522
+ const fullHistory = [...baseHistory, ...extraHistory];
523
+ const genModel = this.client.getGenerativeModel({
524
+ model: request.model,
525
+ systemInstruction: request.systemPrompt
526
+ });
527
+ const chat = genModel.startChat({ history: fullHistory, tools: geminiTools });
528
+ const result = await chat.sendMessage(lastMessage);
529
+ const response = result.response;
530
+ const parts = response.candidates?.[0]?.content?.parts ?? [];
531
+ const fnCalls = parts.filter((p) => !!p.functionCall).map((p) => p.functionCall);
532
+ if (fnCalls.length > 0) {
533
+ const toolCalls = fnCalls.map((fc, i) => ({
534
+ id: `gemini-fn-${Date.now()}-${i}`,
535
+ name: fc.name,
536
+ arguments: fc.args
537
+ }));
538
+ return { toolCalls };
539
+ }
540
+ return { content: response.text() };
541
+ } catch (err) {
542
+ throw this.wrapError(err);
543
+ }
544
+ }
545
+ buildToolResultMessages(assistantToolCalls, results) {
546
+ const modelParts = assistantToolCalls.map((tc) => ({
547
+ functionCall: { name: tc.name, args: tc.arguments }
548
+ }));
549
+ const fnParts = results.map((r, i) => ({
550
+ functionResponse: {
551
+ name: assistantToolCalls[i].name,
552
+ response: { result: r.content, isError: r.isError }
553
+ }
554
+ }));
555
+ return [
556
+ { role: "model", parts: modelParts },
557
+ { role: "function", parts: fnParts }
558
+ ];
559
+ }
560
+ async validateApiKey(apiKey) {
561
+ try {
562
+ const testClient = new GoogleGenerativeAI(apiKey);
563
+ const model = testClient.getGenerativeModel({ model: "gemini-2.0-flash" });
564
+ await model.generateContent("hi");
565
+ return true;
566
+ } catch {
567
+ return false;
568
+ }
569
+ }
570
+ async listModels() {
571
+ return this.info.models;
572
+ }
573
+ wrapError(err) {
574
+ if (err instanceof Error) {
575
+ if (err.message.includes("API key") || err.message.includes("PERMISSION_DENIED")) {
576
+ return new AuthError("gemini");
577
+ }
578
+ return new ProviderError("gemini", err.message, err);
579
+ }
580
+ return new ProviderError("gemini", String(err));
581
+ }
582
+ };
583
+
584
+ // src/providers/openai-compatible.ts
585
+ import OpenAI from "openai";
586
+ var OpenAICompatibleProvider = class extends BaseProvider {
587
+ client;
588
+ defaultTimeout = 6e4;
589
+ // ms
590
+ async initialize(apiKey, options) {
591
+ if (options?.timeout !== void 0) {
592
+ this.defaultTimeout = options.timeout;
593
+ }
594
+ this.client = new OpenAI({
595
+ apiKey,
596
+ baseURL: options?.baseUrl ?? this.defaultBaseUrl,
597
+ timeout: this.defaultTimeout
598
+ });
599
+ }
600
+ /** 将 systemPrompt + messages 合并为 OpenAI messages 数组(system 消息放首位)。 */
601
+ buildMessages(request) {
602
+ const msgs = this.normalizeMessages(request.messages);
603
+ if (request.systemPrompt) {
604
+ return [{ role: "system", content: request.systemPrompt }, ...msgs];
605
+ }
606
+ return msgs;
607
+ }
608
+ async chat(request) {
609
+ try {
610
+ const response = await this.client.chat.completions.create({
611
+ model: request.model,
612
+ messages: this.buildMessages(request),
613
+ temperature: request.temperature,
614
+ max_tokens: request.maxTokens,
615
+ stream: false,
616
+ ...request.thinking ? { thinking: { type: "enabled" } } : {}
617
+ }, {
618
+ timeout: request.timeout ?? this.defaultTimeout
619
+ });
620
+ return {
621
+ content: response.choices[0].message.content ?? "",
622
+ model: response.model,
623
+ usage: response.usage ? {
624
+ inputTokens: response.usage.prompt_tokens,
625
+ outputTokens: response.usage.completion_tokens
626
+ } : void 0
627
+ };
628
+ } catch (err) {
629
+ throw this.wrapError(err);
630
+ }
631
+ }
632
+ async *chatStream(request) {
633
+ try {
634
+ const stream = await this.client.chat.completions.create({
635
+ model: request.model,
636
+ messages: this.buildMessages(request),
637
+ temperature: request.temperature,
638
+ max_tokens: request.maxTokens,
639
+ stream: true,
640
+ // 请求末尾 usage chunk,供 token 统计使用
641
+ stream_options: { include_usage: true },
642
+ ...request.thinking ? { thinking: { type: "enabled" } } : {}
643
+ }, {
644
+ timeout: request.timeout ?? this.defaultTimeout
645
+ });
646
+ for await (const chunk of stream) {
647
+ const choice = chunk.choices[0];
648
+ const done = choice?.finish_reason != null && choice.finish_reason !== "";
649
+ if (!choice && chunk.usage) {
650
+ yield {
651
+ delta: "",
652
+ done: true,
653
+ usage: {
654
+ inputTokens: chunk.usage.prompt_tokens,
655
+ outputTokens: chunk.usage.completion_tokens
656
+ }
657
+ };
658
+ continue;
659
+ }
660
+ const delta = choice?.delta?.content ?? "";
661
+ yield { delta, done };
662
+ }
663
+ } catch (err) {
664
+ throw this.wrapError(err);
665
+ }
666
+ }
667
+ /**
668
+ * 请求 AI 并获取工具调用列表(不执行,只解析)。
669
+ * 返回 { toolCalls, usage? } 时说明 AI 想要调用工具,
670
+ * 返回 { content, usage? } 时说明 AI 给出了最终回答。
671
+ */
672
+ async chatWithTools(request, tools) {
673
+ try {
674
+ const openaiTools = tools.map((t) => ({
675
+ type: "function",
676
+ function: {
677
+ name: t.name,
678
+ description: t.description,
679
+ parameters: {
680
+ type: "object",
681
+ properties: Object.fromEntries(
682
+ Object.entries(t.parameters).map(([key, schema]) => [
683
+ key,
684
+ {
685
+ type: schema.type,
686
+ description: schema.description,
687
+ ...schema.enum ? { enum: schema.enum } : {}
688
+ }
689
+ ])
690
+ ),
691
+ required: Object.entries(t.parameters).filter(([, s]) => s.required).map(([k]) => k)
692
+ }
693
+ }
694
+ }));
695
+ const baseMessages = this.buildMessages(request);
696
+ const extraMessages = request._extraMessages ?? [];
697
+ const allMessages = [...baseMessages, ...extraMessages];
698
+ const response = await this.client.chat.completions.create({
699
+ model: request.model,
700
+ messages: allMessages,
701
+ tools: openaiTools,
702
+ tool_choice: "auto",
703
+ temperature: request.temperature,
704
+ max_tokens: request.maxTokens,
705
+ stream: false,
706
+ ...request.thinking ? { thinking: { type: "enabled" } } : {}
707
+ }, {
708
+ timeout: request.timeout ?? this.defaultTimeout
709
+ });
710
+ const message = response.choices[0].message;
711
+ const usage = response.usage ? {
712
+ inputTokens: response.usage.prompt_tokens,
713
+ outputTokens: response.usage.completion_tokens
714
+ } : void 0;
715
+ if (message.tool_calls && message.tool_calls.length > 0) {
716
+ const toolCalls = message.tool_calls.map((tc) => {
717
+ const rawArgs = tc.function.arguments || "{}";
718
+ let parsedArgs;
719
+ try {
720
+ parsedArgs = JSON.parse(rawArgs);
721
+ } catch {
722
+ const truncated = rawArgs.trimEnd();
723
+ const lastComma = truncated.lastIndexOf(",");
724
+ const fixed = lastComma > 0 ? truncated.slice(0, lastComma) + "}" : truncated.slice(0, truncated.indexOf("{") + 1) + "}";
725
+ try {
726
+ parsedArgs = JSON.parse(fixed);
727
+ } catch {
728
+ parsedArgs = {};
729
+ }
730
+ }
731
+ return {
732
+ id: tc.id,
733
+ name: tc.function.name,
734
+ arguments: parsedArgs
735
+ };
736
+ });
737
+ return { toolCalls, usage };
738
+ }
739
+ return { content: message.content ?? "", usage };
740
+ } catch (err) {
741
+ throw this.wrapError(err);
742
+ }
743
+ }
744
+ /**
745
+ * 将工具结果作为 tool_call 消息追加,供下一轮使用
746
+ */
747
+ buildToolResultMessages(assistantToolCalls, results) {
748
+ const assistantMsg = {
749
+ role: "assistant",
750
+ content: null,
751
+ tool_calls: assistantToolCalls.map((tc) => ({
752
+ id: tc.id,
753
+ type: "function",
754
+ function: {
755
+ name: tc.name,
756
+ arguments: JSON.stringify(tc.arguments)
757
+ }
758
+ }))
759
+ };
760
+ const resultMsgs = results.map((r) => ({
761
+ role: "tool",
762
+ tool_call_id: r.callId,
763
+ content: r.content
764
+ }));
765
+ return [assistantMsg, ...resultMsgs];
766
+ }
767
+ async validateApiKey(apiKey) {
768
+ try {
769
+ const testClient = new OpenAI({ apiKey, baseURL: this.defaultBaseUrl });
770
+ await testClient.models.list();
771
+ return true;
772
+ } catch {
773
+ return false;
774
+ }
775
+ }
776
+ async listModels() {
777
+ return this.info.models;
778
+ }
779
+ wrapError(err) {
780
+ if (err instanceof OpenAI.AuthenticationError) {
781
+ return new AuthError(this.info.id);
782
+ }
783
+ if (err instanceof OpenAI.RateLimitError) {
784
+ return new RateLimitError(this.info.id);
785
+ }
786
+ if (err instanceof Error) {
787
+ return new ProviderError(this.info.id, err.message, err);
788
+ }
789
+ return new ProviderError(this.info.id, String(err));
790
+ }
791
+ };
792
+
793
+ // src/providers/deepseek.ts
794
+ var DeepSeekProvider = class extends OpenAICompatibleProvider {
795
+ defaultBaseUrl = "https://api.deepseek.com/v1";
796
+ info = {
797
+ id: "deepseek",
798
+ displayName: "DeepSeek",
799
+ defaultModel: "deepseek-chat",
800
+ apiKeyEnvVar: "AICLI_API_KEY_DEEPSEEK",
801
+ requiresApiKey: true,
802
+ baseUrl: this.defaultBaseUrl,
803
+ models: [
804
+ {
805
+ id: "deepseek-chat",
806
+ displayName: "DeepSeek Chat (V3)",
807
+ contextWindow: 65536,
808
+ supportsStreaming: true
809
+ },
810
+ {
811
+ id: "deepseek-reasoner",
812
+ displayName: "DeepSeek Reasoner (R1)",
813
+ contextWindow: 65536,
814
+ supportsStreaming: true
815
+ }
816
+ ]
817
+ };
818
+ };
819
+
820
+ // src/providers/zhipu.ts
821
+ var ZhipuProvider = class extends OpenAICompatibleProvider {
822
+ defaultBaseUrl = "https://open.bigmodel.cn/api/paas/v4";
823
+ info = {
824
+ id: "zhipu",
825
+ displayName: "\u667A\u8C31\u6E05\u8A00 (GLM)",
826
+ defaultModel: "glm-5",
827
+ apiKeyEnvVar: "AICLI_API_KEY_ZHIPU",
828
+ requiresApiKey: true,
829
+ baseUrl: this.defaultBaseUrl,
830
+ models: [
831
+ // GLM-5 旗舰系列(最新,支持深度思考)
832
+ {
833
+ id: "glm-5",
834
+ displayName: "GLM-5\uFF08\u65D7\u8230\uFF0C\u6DF1\u5EA6\u601D\u8003\uFF09",
835
+ contextWindow: 131072,
836
+ supportsStreaming: true,
837
+ supportsThinking: true
838
+ },
839
+ // GLM-4.6 系列(视觉 + 思考)
840
+ {
841
+ id: "glm-4.6v",
842
+ displayName: "GLM-4.6V\uFF08\u89C6\u89C9+\u601D\u8003\uFF09",
843
+ contextWindow: 131072,
844
+ supportsStreaming: true,
845
+ supportsThinking: true
846
+ },
847
+ // GLM-Z1 推理系列(支持深度思考)
848
+ {
849
+ id: "glm-z1",
850
+ displayName: "GLM-Z1\uFF08\u63A8\u7406\u65D7\u8230\uFF09",
851
+ contextWindow: 131072,
852
+ supportsStreaming: true,
853
+ supportsThinking: true
854
+ },
855
+ {
856
+ id: "glm-z1-air",
857
+ displayName: "GLM-Z1 Air\uFF08\u8F7B\u91CF\u63A8\u7406\uFF09",
858
+ contextWindow: 131072,
859
+ supportsStreaming: true,
860
+ supportsThinking: true
861
+ },
862
+ {
863
+ id: "glm-z1-flash",
864
+ displayName: "GLM-Z1 Flash\uFF08\u514D\u8D39\u63A8\u7406\uFF09",
865
+ contextWindow: 128e3,
866
+ supportsStreaming: true,
867
+ supportsThinking: true
868
+ },
869
+ // GLM-4 系列(稳定)
870
+ {
871
+ id: "glm-4-plus",
872
+ displayName: "GLM-4 Plus",
873
+ contextWindow: 128e3,
874
+ supportsStreaming: true
875
+ },
876
+ {
877
+ id: "glm-4-air",
878
+ displayName: "GLM-4 Air",
879
+ contextWindow: 128e3,
880
+ supportsStreaming: true
881
+ },
882
+ {
883
+ id: "glm-4-flash",
884
+ displayName: "GLM-4 Flash\uFF08\u514D\u8D39\uFF09",
885
+ contextWindow: 128e3,
886
+ supportsStreaming: true
887
+ }
888
+ ]
889
+ };
890
+ };
891
+
892
+ // src/providers/kimi.ts
893
+ var KimiProvider = class extends OpenAICompatibleProvider {
894
+ defaultBaseUrl = "https://api.moonshot.ai/v1";
895
+ info = {
896
+ id: "kimi",
897
+ displayName: "Kimi (Moonshot AI)",
898
+ defaultModel: "kimi-k2-0711-preview",
899
+ apiKeyEnvVar: "AICLI_API_KEY_KIMI",
900
+ requiresApiKey: true,
901
+ baseUrl: this.defaultBaseUrl,
902
+ models: [
903
+ // Kimi K2 系列(新一代,推荐)
904
+ {
905
+ id: "kimi-k2-0711-preview",
906
+ displayName: "Kimi K2 Preview",
907
+ contextWindow: 131072,
908
+ supportsStreaming: true
909
+ },
910
+ {
911
+ id: "kimi-k2-turbo-preview",
912
+ displayName: "Kimi K2 Turbo",
913
+ contextWindow: 131072,
914
+ supportsStreaming: true
915
+ },
916
+ // moonshot-v1 系列(老一代,稳定)
917
+ {
918
+ id: "moonshot-v1-8k",
919
+ displayName: "Moonshot V1 8K",
920
+ contextWindow: 8192,
921
+ supportsStreaming: true
922
+ },
923
+ {
924
+ id: "moonshot-v1-32k",
925
+ displayName: "Moonshot V1 32K",
926
+ contextWindow: 32768,
927
+ supportsStreaming: true
928
+ },
929
+ {
930
+ id: "moonshot-v1-128k",
931
+ displayName: "Moonshot V1 128K",
932
+ contextWindow: 131072,
933
+ supportsStreaming: true
934
+ },
935
+ // moonshot-v1 Vision 系列(支持图片输入)
936
+ {
937
+ id: "moonshot-v1-8k-vision-preview",
938
+ displayName: "Moonshot V1 8K Vision",
939
+ contextWindow: 8192,
940
+ supportsStreaming: true
941
+ },
942
+ {
943
+ id: "moonshot-v1-32k-vision-preview",
944
+ displayName: "Moonshot V1 32K Vision",
945
+ contextWindow: 32768,
946
+ supportsStreaming: true
947
+ },
948
+ {
949
+ id: "moonshot-v1-128k-vision-preview",
950
+ displayName: "Moonshot V1 128K Vision",
951
+ contextWindow: 131072,
952
+ supportsStreaming: true
953
+ }
954
+ ]
955
+ };
956
+ };
957
+
958
+ // src/providers/custom.ts
959
+ var CustomProvider = class extends OpenAICompatibleProvider {
960
+ defaultBaseUrl;
961
+ info;
962
+ constructor(cfg) {
963
+ super();
964
+ this.defaultBaseUrl = cfg.baseUrl;
965
+ this.info = {
966
+ id: cfg.id,
967
+ displayName: cfg.displayName,
968
+ defaultModel: cfg.defaultModel,
969
+ // 环境变量格式:AICLI_API_KEY_<ID大写>,如 AICLI_API_KEY_SILICONFLOW
970
+ apiKeyEnvVar: `AICLI_API_KEY_${cfg.id.toUpperCase()}`,
971
+ // 本地服务(如 Ollama)不需要 API Key,设为 false
972
+ requiresApiKey: false,
973
+ baseUrl: cfg.baseUrl,
974
+ models: cfg.models.length > 0 ? cfg.models.map((m) => ({
975
+ id: m.id,
976
+ displayName: m.displayName ?? m.id,
977
+ contextWindow: m.contextWindow ?? 4096,
978
+ supportsStreaming: true
979
+ })) : [
980
+ // 如果未配置 models 列表,以 defaultModel 作为唯一条目
981
+ {
982
+ id: cfg.defaultModel,
983
+ displayName: cfg.defaultModel,
984
+ contextWindow: 4096,
985
+ supportsStreaming: true
986
+ }
987
+ ]
988
+ };
989
+ }
990
+ };
991
+
992
+ // src/providers/registry.ts
993
+ var BUILT_IN_PROVIDERS = [
994
+ ClaudeProvider,
995
+ GeminiProvider,
996
+ DeepSeekProvider,
997
+ ZhipuProvider,
998
+ KimiProvider
999
+ ];
1000
+ var ProviderRegistry = class {
1001
+ providers = /* @__PURE__ */ new Map();
1002
+ // 记录自定义 provider 的状态(用于 listAll 展示)
1003
+ customConfigs = [];
1004
+ async initialize(getApiKey, getOptions, customProviderConfigs) {
1005
+ for (const ProviderClass of BUILT_IN_PROVIDERS) {
1006
+ const provider = new ProviderClass();
1007
+ const apiKey = getApiKey(provider.info.id);
1008
+ if (apiKey) {
1009
+ const options = getOptions?.(provider.info.id) ?? {};
1010
+ await provider.initialize(apiKey, options);
1011
+ this.providers.set(provider.info.id, provider);
1012
+ }
1013
+ }
1014
+ this.customConfigs = customProviderConfigs ?? [];
1015
+ for (const cfg of this.customConfigs) {
1016
+ const provider = new CustomProvider(cfg);
1017
+ const apiKey = getApiKey(cfg.id) ?? cfg.apiKey ?? "no-key";
1018
+ const extraOptions = getOptions?.(cfg.id) ?? {};
1019
+ await provider.initialize(apiKey, {
1020
+ baseUrl: cfg.baseUrl,
1021
+ timeout: cfg.timeout,
1022
+ ...extraOptions
1023
+ });
1024
+ this.providers.set(cfg.id, provider);
1025
+ }
1026
+ }
1027
+ get(id) {
1028
+ const provider = this.providers.get(id);
1029
+ if (!provider) {
1030
+ throw new ProviderNotFoundError(id);
1031
+ }
1032
+ return provider;
1033
+ }
1034
+ has(id) {
1035
+ return this.providers.has(id);
1036
+ }
1037
+ listAvailable() {
1038
+ return [...this.providers.values()];
1039
+ }
1040
+ listAll() {
1041
+ const builtIn = BUILT_IN_PROVIDERS.map((P) => {
1042
+ const p = new P();
1043
+ return {
1044
+ id: p.info.id,
1045
+ displayName: p.info.displayName,
1046
+ configured: this.providers.has(p.info.id),
1047
+ defaultModel: p.info.defaultModel,
1048
+ isCustom: false
1049
+ };
1050
+ });
1051
+ const custom = this.customConfigs.map((cfg) => ({
1052
+ id: cfg.id,
1053
+ displayName: cfg.displayName,
1054
+ configured: this.providers.has(cfg.id),
1055
+ defaultModel: cfg.defaultModel,
1056
+ isCustom: true
1057
+ }));
1058
+ return [...builtIn, ...custom];
1059
+ }
1060
+ getFirstAvailable() {
1061
+ return this.providers.values().next().value;
1062
+ }
1063
+ };
1064
+
1065
+ // src/session/session-manager.ts
1066
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync } from "fs";
1067
+ import { join as join2 } from "path";
1068
+ import { v4 as uuidv4 } from "uuid";
1069
+
1070
+ // src/session/session.ts
1071
+ var Session = class _Session {
1072
+ id;
1073
+ provider;
1074
+ model;
1075
+ created;
1076
+ updated;
1077
+ messages = [];
1078
+ title;
1079
+ constructor(id, provider, model) {
1080
+ this.id = id;
1081
+ this.provider = provider;
1082
+ this.model = model;
1083
+ this.created = /* @__PURE__ */ new Date();
1084
+ this.updated = /* @__PURE__ */ new Date();
1085
+ }
1086
+ addMessage(message) {
1087
+ this.messages.push(message);
1088
+ this.updated = /* @__PURE__ */ new Date();
1089
+ if (!this.title && message.role === "user") {
1090
+ this.title = getContentText(message.content).slice(0, 50).replace(/\n/g, " ");
1091
+ }
1092
+ }
1093
+ clear() {
1094
+ this.messages = [];
1095
+ this.title = void 0;
1096
+ this.updated = /* @__PURE__ */ new Date();
1097
+ }
1098
+ getMeta() {
1099
+ return {
1100
+ id: this.id,
1101
+ provider: this.provider,
1102
+ model: this.model,
1103
+ messageCount: this.messages.length,
1104
+ created: this.created,
1105
+ updated: this.updated,
1106
+ title: this.title
1107
+ };
1108
+ }
1109
+ toJSON() {
1110
+ return {
1111
+ id: this.id,
1112
+ provider: this.provider,
1113
+ model: this.model,
1114
+ created: this.created.toISOString(),
1115
+ updated: this.updated.toISOString(),
1116
+ title: this.title,
1117
+ messages: this.messages.map((m) => ({
1118
+ ...m,
1119
+ timestamp: m.timestamp.toISOString()
1120
+ }))
1121
+ };
1122
+ }
1123
+ static fromJSON(data) {
1124
+ const session = new _Session(data.id, data.provider, data.model);
1125
+ session.title = data.title;
1126
+ session.created = new Date(data.created);
1127
+ session.updated = new Date(data.updated);
1128
+ session.messages = data.messages.map((m) => ({
1129
+ ...m,
1130
+ timestamp: new Date(m.timestamp)
1131
+ }));
1132
+ return session;
1133
+ }
1134
+ };
1135
+
1136
+ // src/session/session-manager.ts
1137
+ var SessionManager = class {
1138
+ constructor(config) {
1139
+ this.config = config;
1140
+ this.historyDir = config.getHistoryDir();
1141
+ }
1142
+ _current = null;
1143
+ historyDir;
1144
+ get current() {
1145
+ return this._current;
1146
+ }
1147
+ createSession(provider, model) {
1148
+ const session = new Session(uuidv4(), provider, model);
1149
+ this._current = session;
1150
+ return session;
1151
+ }
1152
+ async save() {
1153
+ if (!this._current) return;
1154
+ mkdirSync2(this.historyDir, { recursive: true });
1155
+ const filePath = join2(this.historyDir, `${this._current.id}.json`);
1156
+ writeFileSync2(filePath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
1157
+ }
1158
+ loadSession(id) {
1159
+ const filePath = join2(this.historyDir, `${id}.json`);
1160
+ if (!existsSync2(filePath)) {
1161
+ throw new Error(`Session ${id} not found`);
1162
+ }
1163
+ const data = JSON.parse(readFileSync2(filePath, "utf-8"));
1164
+ const session = Session.fromJSON(data);
1165
+ this._current = session;
1166
+ return session;
1167
+ }
1168
+ listSessions() {
1169
+ if (!existsSync2(this.historyDir)) return [];
1170
+ const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json"));
1171
+ const metas = [];
1172
+ for (const file of files) {
1173
+ try {
1174
+ const data = JSON.parse(
1175
+ readFileSync2(join2(this.historyDir, file), "utf-8")
1176
+ );
1177
+ metas.push({
1178
+ id: data.id,
1179
+ provider: data.provider,
1180
+ model: data.model,
1181
+ messageCount: data.messages?.length ?? 0,
1182
+ created: new Date(data.created),
1183
+ updated: new Date(data.updated),
1184
+ title: data.title
1185
+ });
1186
+ } catch {
1187
+ }
1188
+ }
1189
+ return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
1190
+ }
1191
+ };
1192
+
1193
+ // src/repl/repl.ts
1194
+ import * as readline from "readline";
1195
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
1196
+ import { join as join7, resolve as resolve3, extname as extname2 } from "path";
1197
+ import chalk5 from "chalk";
1198
+
1199
+ // src/repl/renderer.ts
1200
+ import chalk from "chalk";
1201
+ import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1202
+ import { dirname } from "path";
1203
+ function fmtContextWindow(tokens) {
1204
+ if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
1205
+ if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
1206
+ return `${tokens}`;
1207
+ }
1208
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1209
+ function createSpinner(text) {
1210
+ if (!process.stdout.isTTY) {
1211
+ return {
1212
+ stop() {
1213
+ },
1214
+ start(_t) {
1215
+ }
1216
+ };
1217
+ }
1218
+ let frameIdx = 0;
1219
+ let currentText = text;
1220
+ let timer = null;
1221
+ const render = () => {
1222
+ const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
1223
+ frameIdx++;
1224
+ process.stdout.write(`\r${chalk.cyan(frame)} ${currentText} `);
1225
+ };
1226
+ const clear = () => {
1227
+ process.stdout.write("\r\x1B[2K");
1228
+ };
1229
+ timer = setInterval(render, 80);
1230
+ render();
1231
+ return {
1232
+ stop() {
1233
+ if (timer) {
1234
+ clearInterval(timer);
1235
+ timer = null;
1236
+ }
1237
+ clear();
1238
+ },
1239
+ start(t) {
1240
+ currentText = t;
1241
+ if (!timer) {
1242
+ frameIdx = 0;
1243
+ timer = setInterval(render, 80);
1244
+ render();
1245
+ }
1246
+ }
1247
+ };
1248
+ }
1249
+ var Renderer = class {
1250
+ printWelcome(provider, model, contextWindow) {
1251
+ const ctxStr = contextWindow ? chalk.dim(` (ctx: ${fmtContextWindow(contextWindow)})`) : "";
1252
+ console.log();
1253
+ console.log(chalk.bold.cyan(" \u{1F916} ai-cli") + chalk.gray(` v${VERSION}`) + chalk.dim(" \u2014 " + AUTHOR + " <" + AUTHOR_EMAIL + ">"));
1254
+ console.log(chalk.dim(" " + "\u2500".repeat(55)));
1255
+ console.log(chalk.gray(` Provider : `) + chalk.green(provider));
1256
+ console.log(chalk.gray(` Model : `) + chalk.white(model) + ctxStr);
1257
+ console.log(chalk.gray(" Commands : ") + chalk.dim("/help \xB7 /about \xB7 Ctrl+C to exit"));
1258
+ console.log();
1259
+ }
1260
+ printAbout() {
1261
+ console.log();
1262
+ console.log(chalk.bold.cyan(" \u{1F916} ai-cli") + chalk.gray(` v${VERSION}`));
1263
+ console.log(chalk.dim(" " + "\u2500".repeat(50)));
1264
+ console.log(chalk.gray(" \u7B80\u4ECB ") + chalk.white(DESCRIPTION));
1265
+ console.log(chalk.dim(" " + "\u2500".repeat(50)));
1266
+ console.log(chalk.gray(" \u4F5C\u8005 ") + chalk.yellow(AUTHOR));
1267
+ console.log(chalk.gray(" \u90AE\u7BB1 ") + chalk.cyan(AUTHOR_EMAIL));
1268
+ console.log(chalk.dim(" " + "\u2500".repeat(50)));
1269
+ console.log(chalk.gray(" \u652F\u6301\u7684 Provider\uFF1A"));
1270
+ console.log(chalk.dim(" DeepSeek \xB7 Kimi (Moonshot) \xB7 Claude (Anthropic)"));
1271
+ console.log(chalk.dim(" Gemini (Google) \xB7 \u667A\u8C31\u6E05\u8A00 \xB7 \u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9"));
1272
+ console.log(chalk.dim(" " + "\u2500".repeat(50)));
1273
+ console.log(chalk.gray(" \u5DE5\u5177\u80FD\u529B\uFF1A"));
1274
+ console.log(chalk.dim(" bash \xB7 read_file \xB7 write_file \xB7 edit_file"));
1275
+ console.log(chalk.dim(" grep_files \xB7 list_dir \xB7 run_interactive"));
1276
+ console.log();
1277
+ }
1278
+ printPrompt(provider, _model) {
1279
+ process.stdout.write(chalk.green(`[${provider}]`) + chalk.white(" > "));
1280
+ }
1281
+ async renderStream(stream, options) {
1282
+ process.stdout.write(chalk.cyan("Assistant: "));
1283
+ let fileStream = null;
1284
+ if (options?.saveToFile) {
1285
+ mkdirSync3(dirname(options.saveToFile), { recursive: true });
1286
+ fileStream = createWriteStream(options.saveToFile, { encoding: "utf-8" });
1287
+ }
1288
+ let fullContent = "";
1289
+ let usage;
1290
+ let inThinking = false;
1291
+ let thinkingShown = false;
1292
+ let buf = "";
1293
+ const flushBuf = () => {
1294
+ if (!buf) return;
1295
+ let out = buf;
1296
+ while (!inThinking) {
1297
+ const openIdx = out.indexOf("<think>");
1298
+ if (openIdx === -1) break;
1299
+ if (openIdx > 0) process.stdout.write(out.slice(0, openIdx));
1300
+ inThinking = true;
1301
+ if (!thinkingShown) {
1302
+ process.stdout.write(chalk.dim("\n\u{1F4AD} Thinking...\n"));
1303
+ thinkingShown = true;
1304
+ }
1305
+ out = out.slice(openIdx + "<think>".length);
1306
+ }
1307
+ if (inThinking) {
1308
+ const closeIdx = out.indexOf("</think>");
1309
+ if (closeIdx !== -1) {
1310
+ inThinking = false;
1311
+ out = out.slice(closeIdx + "</think>".length);
1312
+ buf = out;
1313
+ flushBuf();
1314
+ return;
1315
+ } else {
1316
+ buf = "";
1317
+ return;
1318
+ }
1319
+ }
1320
+ if (out) process.stdout.write(out);
1321
+ buf = "";
1322
+ };
1323
+ for await (const chunk of stream) {
1324
+ if (chunk.usage) {
1325
+ usage = chunk.usage;
1326
+ }
1327
+ if (chunk.done) {
1328
+ flushBuf();
1329
+ break;
1330
+ }
1331
+ if (!chunk.delta) continue;
1332
+ fullContent += chunk.delta;
1333
+ buf += chunk.delta;
1334
+ if (fileStream) fileStream.write(chunk.delta);
1335
+ flushBuf();
1336
+ }
1337
+ process.stdout.write("\n\n");
1338
+ if (fileStream) {
1339
+ await new Promise((resolve4, reject) => {
1340
+ fileStream.end((err) => err ? reject(err) : resolve4());
1341
+ });
1342
+ const kb = (Buffer.byteLength(fullContent, "utf-8") / 1024).toFixed(1);
1343
+ process.stdout.write(chalk.green(` \u2705 \u5DF2\u4FDD\u5B58: ${options.saveToFile} (${kb} KB)
1344
+
1345
+ `));
1346
+ }
1347
+ return { content: fullContent, usage };
1348
+ }
1349
+ renderResponse(content) {
1350
+ process.stdout.write(chalk.cyan("Assistant: "));
1351
+ let displayed = content;
1352
+ if (content.includes("<think>")) {
1353
+ let hasThinking = false;
1354
+ displayed = content.replace(/<think>[\s\S]*?<\/think>/g, () => {
1355
+ hasThinking = true;
1356
+ return "";
1357
+ }).trimStart();
1358
+ if (hasThinking) {
1359
+ process.stdout.write(chalk.dim("\n\u{1F4AD} Thinking...\n"));
1360
+ }
1361
+ }
1362
+ process.stdout.write(displayed);
1363
+ process.stdout.write("\n\n");
1364
+ }
1365
+ renderError(err) {
1366
+ const message = err instanceof Error ? err.message : String(err);
1367
+ console.error(chalk.red(`
1368
+ Error: ${message}
1369
+ `));
1370
+ }
1371
+ /**
1372
+ * 在 AI 回复后显示 token 用量。
1373
+ * @param usage 本次调用的 input/output tokens
1374
+ * @param sessionTotal 本次会话累计 tokens(可选)
1375
+ */
1376
+ renderUsage(usage, sessionTotal) {
1377
+ const total = usage.inputTokens + usage.outputTokens;
1378
+ let line = chalk.dim("\u{1F4CA} ") + chalk.dim(`in ${usage.inputTokens.toLocaleString()}`) + chalk.dim(" + ") + chalk.dim(`out ${usage.outputTokens.toLocaleString()}`) + chalk.dim(` = ${total.toLocaleString()} tokens`);
1379
+ if (sessionTotal) {
1380
+ const sessionSum = sessionTotal.inputTokens + sessionTotal.outputTokens;
1381
+ line += chalk.dim(` \u2502 session total: ${sessionSum.toLocaleString()}`);
1382
+ }
1383
+ process.stdout.write(line + "\n\n");
1384
+ }
1385
+ printInfo(message) {
1386
+ console.log(chalk.yellow(` ${message}`));
1387
+ }
1388
+ printSuccess(message) {
1389
+ console.log(chalk.green(` ${message}`));
1390
+ }
1391
+ showSpinner(text) {
1392
+ return createSpinner(text);
1393
+ }
1394
+ printTable(headers, rows) {
1395
+ const colWidths = headers.map((h, i) => {
1396
+ const maxRow = Math.max(...rows.map((r) => String(r[i] ?? "").length));
1397
+ return Math.max(h.length, maxRow);
1398
+ });
1399
+ const separator = colWidths.map((w) => "-".repeat(w + 2)).join("+");
1400
+ const headerLine = headers.map((h, i) => ` ${h.padEnd(colWidths[i])} `).join("|");
1401
+ console.log(chalk.gray(separator));
1402
+ console.log(chalk.bold(headerLine));
1403
+ console.log(chalk.gray(separator));
1404
+ for (const row of rows) {
1405
+ const line = row.map((cell, i) => {
1406
+ const val = String(cell ?? "");
1407
+ const colored = cell === true ? chalk.green(val) : cell === false ? chalk.red(val) : val;
1408
+ return ` ${colored.padEnd(colWidths[i])} `;
1409
+ }).join("|");
1410
+ console.log(line);
1411
+ }
1412
+ console.log(chalk.gray(separator));
1413
+ }
1414
+ };
1415
+
1416
+ // src/repl/commands/index.ts
1417
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
1418
+ import { resolve, dirname as dirname2 } from "path";
1419
+
1420
+ // src/tools/undo-stack.ts
1421
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync3 } from "fs";
1422
+ var MAX_UNDO_DEPTH = 20;
1423
+ var UndoStack = class {
1424
+ stack = [];
1425
+ /**
1426
+ * 在执行文件写入操作之前调用,保存当前状态。
1427
+ * @param filePath 将要被写入的文件路径
1428
+ * @param description 操作描述,如 "write_file: src/index.ts"
1429
+ */
1430
+ push(filePath, description) {
1431
+ let previousContent = null;
1432
+ if (existsSync3(filePath)) {
1433
+ try {
1434
+ previousContent = readFileSync3(filePath, "utf-8");
1435
+ } catch {
1436
+ return;
1437
+ }
1438
+ }
1439
+ this.stack.push({
1440
+ filePath,
1441
+ previousContent,
1442
+ description,
1443
+ timestamp: /* @__PURE__ */ new Date()
1444
+ });
1445
+ if (this.stack.length > MAX_UNDO_DEPTH) {
1446
+ this.stack.shift();
1447
+ }
1448
+ }
1449
+ /**
1450
+ * 弹出并执行最近一次撤销操作。
1451
+ * @returns 撤销结果描述,或 null(栈为空时)
1452
+ */
1453
+ undo() {
1454
+ const entry = this.stack.pop();
1455
+ if (!entry) return null;
1456
+ try {
1457
+ if (entry.previousContent === null) {
1458
+ if (existsSync3(entry.filePath)) {
1459
+ unlinkSync(entry.filePath);
1460
+ }
1461
+ return { entry, result: `Deleted newly created file: ${entry.filePath}` };
1462
+ } else {
1463
+ writeFileSync3(entry.filePath, entry.previousContent, "utf-8");
1464
+ const lines = entry.previousContent.split("\n").length;
1465
+ return {
1466
+ entry,
1467
+ result: `Restored ${entry.filePath} to previous state (${lines} lines)`
1468
+ };
1469
+ }
1470
+ } catch (err) {
1471
+ const msg = err instanceof Error ? err.message : String(err);
1472
+ return { entry, result: `Undo failed: ${msg}` };
1473
+ }
1474
+ }
1475
+ /** 查看栈顶条目(不弹出),用于 /undo 显示预览 */
1476
+ peek() {
1477
+ return this.stack[this.stack.length - 1] ?? null;
1478
+ }
1479
+ /** 当前栈深度 */
1480
+ get depth() {
1481
+ return this.stack.length;
1482
+ }
1483
+ /** 清空撤销栈(新会话时调用) */
1484
+ clear() {
1485
+ this.stack = [];
1486
+ }
1487
+ };
1488
+ var undoStack = new UndoStack();
1489
+
1490
+ // src/repl/commands/index.ts
1491
+ function fmtCtx(tokens) {
1492
+ if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
1493
+ if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
1494
+ return `${tokens}`;
1495
+ }
1496
+ var CommandRegistry = class {
1497
+ commands = /* @__PURE__ */ new Map();
1498
+ register(command) {
1499
+ this.commands.set(command.name, command);
1500
+ }
1501
+ get(name) {
1502
+ return this.commands.get(name);
1503
+ }
1504
+ listAll() {
1505
+ return [...this.commands.values()];
1506
+ }
1507
+ };
1508
+ function createDefaultCommands() {
1509
+ return [
1510
+ {
1511
+ name: "help",
1512
+ description: "List all available commands",
1513
+ usage: "/help",
1514
+ execute(_args, ctx) {
1515
+ const commands = ctx.providers ? [
1516
+ " /help - Show this help",
1517
+ " /about - About ai-cli & author info",
1518
+ " /provider <name> - Switch AI provider",
1519
+ " /model <name> - Switch model",
1520
+ " /clear - Clear conversation history",
1521
+ " /session new - Start a new session",
1522
+ " /session list - List saved sessions",
1523
+ " /session load <id> - Resume a session",
1524
+ " /system <prompt> - Set system prompt",
1525
+ " /context - Show or reload project context file",
1526
+ " /status - Show current status",
1527
+ " /undo - Undo the last file write/edit operation",
1528
+ " /export [md|json] [file] - Export session to file (default: auto-named .md)",
1529
+ " /tools - List all AI tools available",
1530
+ " /exit - Exit"
1531
+ ] : [];
1532
+ console.log("\nAvailable commands:");
1533
+ commands.forEach((c) => console.log(c));
1534
+ console.log();
1535
+ }
1536
+ },
1537
+ {
1538
+ name: "about",
1539
+ description: "Show information about ai-cli and its author",
1540
+ usage: "/about",
1541
+ execute(_args, ctx) {
1542
+ ctx.renderer.printAbout();
1543
+ }
1544
+ },
1545
+ {
1546
+ name: "provider",
1547
+ description: "Switch AI provider (interactive selector when no arg)",
1548
+ usage: "/provider [name]",
1549
+ async execute(args, ctx) {
1550
+ const id = args[0];
1551
+ if (!id) {
1552
+ const all = ctx.providers.listAll();
1553
+ const currentProvider = ctx.getCurrentProvider();
1554
+ const items = all.map((p) => ({
1555
+ value: p.id,
1556
+ label: p.displayName,
1557
+ hint: p.configured ? "\u2713 configured" : "\u2717 not configured"
1558
+ }));
1559
+ const initialIdx = Math.max(0, items.findIndex((i) => i.value === currentProvider));
1560
+ const chosen = await ctx.select("Select provider:", items, initialIdx);
1561
+ if (!chosen) return;
1562
+ if (!ctx.providers.has(chosen)) {
1563
+ ctx.renderer.renderError(`Provider '${chosen}' is not configured. Run: ai-cli config`);
1564
+ return;
1565
+ }
1566
+ ctx.setProvider(chosen);
1567
+ ctx.sessions.createSession(chosen, ctx.getCurrentModel());
1568
+ ctx.renderer.printSuccess(`Switched to provider: ${chosen}`);
1569
+ return;
1570
+ }
1571
+ if (!ctx.providers.has(id)) {
1572
+ ctx.renderer.renderError(
1573
+ `Provider '${id}' is not configured. Run: ai-cli config`
1574
+ );
1575
+ return;
1576
+ }
1577
+ ctx.setProvider(id);
1578
+ ctx.sessions.createSession(id, ctx.getCurrentModel());
1579
+ ctx.renderer.printSuccess(`Switched to provider: ${id}`);
1580
+ }
1581
+ },
1582
+ {
1583
+ name: "model",
1584
+ description: "Switch model (interactive selector when no arg)",
1585
+ usage: "/model [name]",
1586
+ async execute(args, ctx) {
1587
+ const name = args[0];
1588
+ if (!name) {
1589
+ const provider = ctx.providers.get(ctx.getCurrentProvider());
1590
+ const currentModel = ctx.getCurrentModel();
1591
+ const items = provider.info.models.map((m) => ({
1592
+ value: m.id,
1593
+ label: m.id,
1594
+ hint: [
1595
+ m.displayName,
1596
+ `ctx:${fmtCtx(m.contextWindow)}`,
1597
+ m.supportsThinking ? "\u{1F9E0}" : "",
1598
+ m.id === currentModel ? "(current)" : ""
1599
+ ].filter(Boolean).join(" ")
1600
+ }));
1601
+ const initialIdx = Math.max(0, items.findIndex((i) => i.value === currentModel));
1602
+ const chosen = await ctx.select("Select model:", items, initialIdx);
1603
+ if (!chosen) return;
1604
+ ctx.setProvider(ctx.getCurrentProvider(), chosen);
1605
+ ctx.sessions.createSession(ctx.getCurrentProvider(), chosen);
1606
+ ctx.renderer.printSuccess(`Switched to model: ${chosen}`);
1607
+ return;
1608
+ }
1609
+ ctx.setProvider(ctx.getCurrentProvider(), name);
1610
+ ctx.sessions.createSession(ctx.getCurrentProvider(), name);
1611
+ ctx.renderer.printSuccess(`Switched to model: ${name}`);
1612
+ }
1613
+ },
1614
+ {
1615
+ name: "clear",
1616
+ description: "Clear conversation history",
1617
+ usage: "/clear",
1618
+ execute(_args, ctx) {
1619
+ ctx.sessions.current?.clear();
1620
+ ctx.renderer.printInfo("Conversation cleared.");
1621
+ }
1622
+ },
1623
+ {
1624
+ name: "session",
1625
+ description: "Session management",
1626
+ usage: "/session new|list|load <id>",
1627
+ execute(args, ctx) {
1628
+ const sub = args[0];
1629
+ if (sub === "new") {
1630
+ const provider = ctx.getCurrentProvider();
1631
+ const model = ctx.getCurrentModel();
1632
+ ctx.sessions.createSession(provider, model);
1633
+ ctx.renderer.printSuccess("New session started.");
1634
+ } else if (sub === "list") {
1635
+ const sessions = ctx.sessions.listSessions();
1636
+ if (sessions.length === 0) {
1637
+ ctx.renderer.printInfo("No saved sessions.");
1638
+ return;
1639
+ }
1640
+ console.log();
1641
+ ctx.renderer.printTable(
1642
+ ["ID (short)", "Provider", "Model", "Messages", "Date", "Title"],
1643
+ sessions.map((s) => [
1644
+ s.id.slice(0, 8),
1645
+ s.provider,
1646
+ s.model,
1647
+ s.messageCount,
1648
+ s.updated.toLocaleDateString(),
1649
+ s.title ?? "(untitled)"
1650
+ ])
1651
+ );
1652
+ } else if (sub === "load") {
1653
+ const id = args[1];
1654
+ if (!id) {
1655
+ ctx.renderer.renderError("Usage: /session load <id>");
1656
+ return;
1657
+ }
1658
+ const sessions = ctx.sessions.listSessions();
1659
+ const match = sessions.find((s) => s.id.startsWith(id));
1660
+ if (!match) {
1661
+ ctx.renderer.renderError(`Session '${id}' not found.`);
1662
+ return;
1663
+ }
1664
+ ctx.sessions.loadSession(match.id);
1665
+ ctx.setProvider(match.provider, match.model);
1666
+ ctx.renderer.printSuccess(
1667
+ `Loaded session ${match.id.slice(0, 8)}: ${match.title ?? "(untitled)"}`
1668
+ );
1669
+ } else {
1670
+ ctx.renderer.renderError("Usage: /session new|list|load <id>");
1671
+ }
1672
+ }
1673
+ },
1674
+ {
1675
+ name: "system",
1676
+ description: "Set system prompt",
1677
+ usage: "/system <prompt>",
1678
+ execute(args, ctx) {
1679
+ const prompt = args.join(" ");
1680
+ if (!prompt) {
1681
+ const current = ctx.config.get("session").systemPrompt;
1682
+ ctx.renderer.printInfo(
1683
+ `Current system prompt: ${current ?? "(none)"}`
1684
+ );
1685
+ return;
1686
+ }
1687
+ const session = ctx.config.get("session");
1688
+ ctx.config.set("session", { ...session, systemPrompt: prompt });
1689
+ ctx.renderer.printSuccess("System prompt updated.");
1690
+ }
1691
+ },
1692
+ {
1693
+ name: "context",
1694
+ description: "Show or reload project context file (AICLI.md / CLAUDE.md)",
1695
+ usage: "/context [reload]",
1696
+ execute(args, ctx) {
1697
+ const sub = args[0];
1698
+ if (sub === "reload") {
1699
+ ctx.reloadContext();
1700
+ const filePath2 = ctx.getContextFilePath();
1701
+ if (filePath2) {
1702
+ ctx.renderer.printSuccess(`Context reloaded from: ${filePath2}`);
1703
+ } else {
1704
+ ctx.renderer.printInfo("No context file found. Place AICLI.md or CLAUDE.md in your project root.");
1705
+ }
1706
+ return;
1707
+ }
1708
+ const filePath = ctx.getContextFilePath();
1709
+ const prompt = ctx.getActiveSystemPrompt();
1710
+ console.log();
1711
+ if (filePath) {
1712
+ console.log(` Context file : ${filePath}`);
1713
+ console.log(` Total chars : ${prompt?.length ?? 0}`);
1714
+ console.log(` Preview : ${(prompt ?? "").slice(0, 120).replace(/\n/g, " ")}...`);
1715
+ } else {
1716
+ console.log(" No project context file loaded.");
1717
+ console.log(" Create AICLI.md (or CLAUDE.md) in your project root to auto-load context.");
1718
+ console.log(' Or set "contextFile": "your-file.md" in ~/.aicli/config.json');
1719
+ }
1720
+ console.log();
1721
+ }
1722
+ },
1723
+ {
1724
+ name: "status",
1725
+ description: "Show current status",
1726
+ usage: "/status",
1727
+ execute(_args, ctx) {
1728
+ const session = ctx.sessions.current;
1729
+ const filePath = ctx.getContextFilePath();
1730
+ const tokenUsage = ctx.getSessionTokenUsage();
1731
+ const gitBranch = ctx.getGitBranch();
1732
+ console.log();
1733
+ const provider = ctx.providers.get(ctx.getCurrentProvider());
1734
+ const currentModel = ctx.getCurrentModel();
1735
+ const modelInfo = provider?.info.models.find((m) => m.id === currentModel);
1736
+ const ctxStr = modelInfo ? `${fmtCtx(modelInfo.contextWindow)} tokens` : "unknown";
1737
+ console.log(` Provider : ${ctx.getCurrentProvider()}`);
1738
+ console.log(` Model : ${currentModel}`);
1739
+ console.log(` Ctx Win : ${ctxStr}`);
1740
+ console.log(` Messages : ${session?.messages.length ?? 0}`);
1741
+ console.log(` Session : ${session?.id.slice(0, 8) ?? "none"}`);
1742
+ console.log(` Context : ${filePath ?? "(none)"}`);
1743
+ if (gitBranch) {
1744
+ console.log(` Git : ${gitBranch}`);
1745
+ }
1746
+ const sys = ctx.config.get("session").systemPrompt;
1747
+ if (sys) {
1748
+ console.log(` System : ${sys.slice(0, 60)}...`);
1749
+ }
1750
+ const totalTokens = tokenUsage.inputTokens + tokenUsage.outputTokens;
1751
+ if (totalTokens > 0) {
1752
+ console.log(
1753
+ ` Tokens : in ${tokenUsage.inputTokens.toLocaleString()} + out ${tokenUsage.outputTokens.toLocaleString()} = ${totalTokens.toLocaleString()} (session total)`
1754
+ );
1755
+ }
1756
+ console.log();
1757
+ }
1758
+ },
1759
+ {
1760
+ name: "export",
1761
+ description: "Export current session to file or stdout",
1762
+ usage: "/export [md|json] [filename]",
1763
+ execute(args, ctx) {
1764
+ let format = "md";
1765
+ let outFile = null;
1766
+ for (const arg of args) {
1767
+ if (arg === "md" || arg === "json") {
1768
+ format = arg;
1769
+ } else if (arg) {
1770
+ outFile = arg;
1771
+ }
1772
+ }
1773
+ const session = ctx.sessions.current;
1774
+ if (!session || session.messages.length === 0) {
1775
+ ctx.renderer.printInfo("No messages to export.");
1776
+ return;
1777
+ }
1778
+ let output;
1779
+ if (format === "json") {
1780
+ output = JSON.stringify(session.toJSON(), null, 2);
1781
+ } else {
1782
+ const dateStr = session.created.toISOString().replace("T", " ").slice(0, 19);
1783
+ let md = `# Conversation Export
1784
+
1785
+ `;
1786
+ md += `| Field | Value |
1787
+ |----------|-------|
1788
+ `;
1789
+ md += `| Provider | ${session.provider} |
1790
+ `;
1791
+ md += `| Model | ${session.model} |
1792
+ `;
1793
+ md += `| Date | ${dateStr} |
1794
+ `;
1795
+ md += `| Session | ${session.id.slice(0, 8)} |
1796
+
1797
+ ---
1798
+
1799
+ `;
1800
+ for (const msg of session.messages) {
1801
+ const role = msg.role === "user" ? "## \u{1F464} You" : "## \u{1F916} Assistant";
1802
+ const text = getContentText(msg.content);
1803
+ md += `${role}
1804
+
1805
+ ${text}
1806
+
1807
+ ---
1808
+
1809
+ `;
1810
+ }
1811
+ output = md;
1812
+ }
1813
+ if (outFile) {
1814
+ const absPath = resolve(process.cwd(), outFile);
1815
+ try {
1816
+ mkdirSync4(dirname2(absPath), { recursive: true });
1817
+ writeFileSync4(absPath, output, "utf-8");
1818
+ ctx.renderer.printSuccess(`Exported ${session.messages.length} messages \u2192 ${absPath}`);
1819
+ } catch (err) {
1820
+ ctx.renderer.renderError(`Failed to write file: ${err instanceof Error ? err.message : String(err)}`);
1821
+ }
1822
+ } else {
1823
+ const shortId = session.id.slice(0, 8);
1824
+ const dateTag = session.created.toISOString().slice(0, 10);
1825
+ const ext = format === "json" ? "json" : "md";
1826
+ const autoName = `session-${dateTag}-${shortId}.${ext}`;
1827
+ const absPath = resolve(process.cwd(), autoName);
1828
+ try {
1829
+ writeFileSync4(absPath, output, "utf-8");
1830
+ ctx.renderer.printSuccess(`Exported ${session.messages.length} messages \u2192 ${absPath}`);
1831
+ } catch (err) {
1832
+ ctx.renderer.renderError(`Failed to write file: ${err instanceof Error ? err.message : String(err)}`);
1833
+ }
1834
+ }
1835
+ }
1836
+ },
1837
+ {
1838
+ name: "tools",
1839
+ description: "List all AI tools available in agentic mode",
1840
+ usage: "/tools",
1841
+ execute(_args, ctx) {
1842
+ const tools = ctx.tools.listAll();
1843
+ console.log("\nAvailable AI tools:");
1844
+ for (const t of tools) {
1845
+ console.log(
1846
+ ` ${t.definition.name.padEnd(20)} ${t.definition.description}`
1847
+ );
1848
+ }
1849
+ console.log();
1850
+ }
1851
+ },
1852
+ {
1853
+ name: "undo",
1854
+ description: "Undo the last file write or edit operation",
1855
+ usage: "/undo",
1856
+ execute(_args, ctx) {
1857
+ const top = undoStack.peek();
1858
+ if (!top) {
1859
+ ctx.renderer.printInfo("Nothing to undo.");
1860
+ return;
1861
+ }
1862
+ const timeStr = top.timestamp.toLocaleTimeString();
1863
+ ctx.renderer.printInfo(
1864
+ `Undoing: ${top.description} (${timeStr})`
1865
+ );
1866
+ const undoResult = undoStack.undo();
1867
+ if (undoResult) {
1868
+ ctx.renderer.printSuccess(undoResult.result);
1869
+ } else {
1870
+ ctx.renderer.printInfo("Nothing to undo.");
1871
+ }
1872
+ }
1873
+ },
1874
+ {
1875
+ name: "exit",
1876
+ description: "Exit the REPL",
1877
+ usage: "/exit",
1878
+ execute(_args, ctx) {
1879
+ ctx.exit();
1880
+ }
1881
+ }
1882
+ ];
1883
+ }
1884
+
1885
+ // src/repl/select-list.ts
1886
+ import chalk2 from "chalk";
1887
+ var CLEAR_LINE = "\r\x1B[2K";
1888
+ var MOVE_UP = (n) => n > 0 ? `\x1B[${n}A` : "";
1889
+ var IGNORE_ENTER_MS = 80;
1890
+ function selectFromList(prompt, items, initialIndex = 0) {
1891
+ if (items.length === 0) return Promise.resolve(null);
1892
+ const PAGE = 12;
1893
+ return new Promise((resolve4) => {
1894
+ let selected = Math.max(0, Math.min(initialIndex, items.length - 1));
1895
+ let windowStart = Math.max(0, selected - Math.floor(PAGE / 2));
1896
+ let lastRenderedLines = 0;
1897
+ let done = false;
1898
+ const startTime = Date.now();
1899
+ let escBuf = "";
1900
+ let escTimer = null;
1901
+ const flushEsc = () => {
1902
+ if (escTimer) {
1903
+ clearTimeout(escTimer);
1904
+ escTimer = null;
1905
+ }
1906
+ const seq = escBuf;
1907
+ escBuf = "";
1908
+ handleSequence(seq);
1909
+ };
1910
+ const renderLine = (item, active, absIdx) => {
1911
+ const cursor = active ? chalk2.cyan("\u276F ") : " ";
1912
+ const label = active ? chalk2.cyan(item.label) : item.label;
1913
+ const hint = item.hint ? chalk2.dim(" " + item.hint) : "";
1914
+ const num = chalk2.dim(String(absIdx + 1).padStart(2) + " ");
1915
+ return cursor + num + label + hint;
1916
+ };
1917
+ const render = () => {
1918
+ if (selected < windowStart) windowStart = selected;
1919
+ if (selected >= windowStart + PAGE) windowStart = selected - PAGE + 1;
1920
+ const windowEnd = Math.min(windowStart + PAGE, items.length);
1921
+ const visible = items.slice(windowStart, windowEnd);
1922
+ const lines = [];
1923
+ lines.push(chalk2.bold(prompt));
1924
+ if (windowStart > 0)
1925
+ lines.push(chalk2.dim(` \u2191 ${windowStart} more above`));
1926
+ visible.forEach(
1927
+ (item, i) => lines.push(renderLine(item, windowStart + i === selected, windowStart + i))
1928
+ );
1929
+ const below = items.length - windowEnd;
1930
+ if (below > 0)
1931
+ lines.push(chalk2.dim(` \u2193 ${below} more below`));
1932
+ lines.push(chalk2.dim(" \u2191\u2193 move \xB7 Enter select \xB7 Esc cancel"));
1933
+ if (lastRenderedLines > 0) {
1934
+ process.stdout.write(MOVE_UP(lastRenderedLines) + CLEAR_LINE);
1935
+ for (let i = 1; i < lastRenderedLines; i++)
1936
+ process.stdout.write("\x1B[1B" + CLEAR_LINE);
1937
+ process.stdout.write(MOVE_UP(lastRenderedLines - 1));
1938
+ }
1939
+ process.stdout.write(lines.join("\n") + "\n");
1940
+ lastRenderedLines = lines.length;
1941
+ };
1942
+ let savedDataListeners = [];
1943
+ const cleanup = (result) => {
1944
+ if (done) return;
1945
+ done = true;
1946
+ if (escTimer) {
1947
+ clearTimeout(escTimer);
1948
+ escTimer = null;
1949
+ }
1950
+ process.stdin.removeListener("data", onData);
1951
+ try {
1952
+ process.stdin.setRawMode(false);
1953
+ } catch {
1954
+ }
1955
+ for (const listener of savedDataListeners) {
1956
+ process.stdin.on("data", listener);
1957
+ }
1958
+ if (lastRenderedLines > 0) {
1959
+ process.stdout.write(MOVE_UP(lastRenderedLines) + CLEAR_LINE);
1960
+ for (let i = 1; i < lastRenderedLines; i++)
1961
+ process.stdout.write("\x1B[1B" + CLEAR_LINE);
1962
+ process.stdout.write(MOVE_UP(lastRenderedLines - 1));
1963
+ }
1964
+ if (result !== null) {
1965
+ process.stdout.write(chalk2.dim(` \u2714 ${result}
1966
+ `));
1967
+ }
1968
+ resolve4(result);
1969
+ };
1970
+ const handleSequence = (seq) => {
1971
+ if (seq === "\x1B") {
1972
+ cleanup(null);
1973
+ return;
1974
+ }
1975
+ if (seq === "\x1B[A" || seq === "\x1BOA") {
1976
+ selected = (selected - 1 + items.length) % items.length;
1977
+ render();
1978
+ return;
1979
+ }
1980
+ if (seq === "\x1B[B" || seq === "\x1BOB") {
1981
+ selected = (selected + 1) % items.length;
1982
+ render();
1983
+ return;
1984
+ }
1985
+ };
1986
+ const onData = (buf) => {
1987
+ for (let i = 0; i < buf.length; ) {
1988
+ const byte = buf[i];
1989
+ if (escBuf.length > 0 || byte === 27) {
1990
+ escBuf += String.fromCharCode(byte);
1991
+ i++;
1992
+ if (escTimer) clearTimeout(escTimer);
1993
+ if (escBuf === "\x1B") {
1994
+ escTimer = setTimeout(flushEsc, 30);
1995
+ } else if (escBuf.length >= 3 || escBuf.length === 2 && escBuf[1] !== "[" && escBuf[1] !== "O") {
1996
+ flushEsc();
1997
+ } else {
1998
+ escTimer = setTimeout(flushEsc, 30);
1999
+ }
2000
+ continue;
2001
+ }
2002
+ i++;
2003
+ const ch = String.fromCharCode(byte);
2004
+ const isEnter = byte === 13 || byte === 10;
2005
+ if (isEnter) {
2006
+ if (Date.now() - startTime < IGNORE_ENTER_MS) continue;
2007
+ cleanup(items[selected].value);
2008
+ return;
2009
+ }
2010
+ if (byte === 3 || ch === "q") {
2011
+ cleanup(null);
2012
+ return;
2013
+ }
2014
+ if (ch >= "1" && ch <= "9") {
2015
+ const n = parseInt(ch, 10) - 1;
2016
+ if (n < items.length) {
2017
+ selected = n;
2018
+ cleanup(items[selected].value);
2019
+ return;
2020
+ }
2021
+ }
2022
+ if (ch === "k") {
2023
+ selected = (selected - 1 + items.length) % items.length;
2024
+ render();
2025
+ } else if (ch === "j") {
2026
+ selected = (selected + 1) % items.length;
2027
+ render();
2028
+ }
2029
+ }
2030
+ };
2031
+ if (!process.stdin.isTTY) {
2032
+ resolve4(items[0]?.value ?? null);
2033
+ return;
2034
+ }
2035
+ try {
2036
+ process.stdin.setRawMode(true);
2037
+ } catch {
2038
+ resolve4(items[0]?.value ?? null);
2039
+ return;
2040
+ }
2041
+ savedDataListeners = process.stdin.rawListeners("data");
2042
+ process.stdin.removeAllListeners("data");
2043
+ process.stdin.resume();
2044
+ process.stdin.on("data", onData);
2045
+ render();
2046
+ });
2047
+ }
2048
+
2049
+ // src/tools/builtin/bash.ts
2050
+ import { execSync } from "child_process";
2051
+ import { existsSync as existsSync4 } from "fs";
2052
+ import { platform } from "os";
2053
+ import { resolve as resolve2 } from "path";
2054
+ var IS_WINDOWS = platform() === "win32";
2055
+ var SHELL = IS_WINDOWS ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
2056
+ var persistentCwd = process.cwd();
2057
+ var bashTool = {
2058
+ definition: {
2059
+ name: "bash",
2060
+ description: IS_WINDOWS ? `\u5728 PowerShell \u4E2D\u6267\u884C\u547D\u4EE4\u3002\u652F\u6301 mkdir\u3001ls\u3001cat\u3001python \u7B49\u3002
2061
+ \u91CD\u8981\u89C4\u5219\uFF1A
2062
+ 1. \u6BCF\u6B21 bash \u8C03\u7528\u662F\u72EC\u7ACB\u5B50\u8FDB\u7A0B\uFF0Ccd \u547D\u4EE4\u4E0D\u4F1A\u6301\u4E45\u751F\u6548\u3002\u5982\u9700\u5728\u6307\u5B9A\u76EE\u5F55\u6267\u884C\uFF0C\u8BF7\u4F7F\u7528 cwd \u53C2\u6570\uFF0C\u6216\u5C06\u547D\u4EE4\u5408\u5E76\uFF1A\u5982 "cd mydir; ls" \u6216 "mkdir mydir; cd mydir; New-Item file.txt"\u3002
2063
+ 2. \u5982\u679C\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08\u8FD4\u56DE\u9519\u8BEF\u6216\u975E\u96F6\u9000\u51FA\u7801\uFF09\uFF0C\u8BF7\u7ACB\u5373\u505C\u6B62\uFF0C\u5C06\u9519\u8BEF\u4FE1\u606F\u544A\u77E5\u7528\u6237\uFF0C\u4E0D\u8981\u53CD\u590D\u5C1D\u8BD5\u76F8\u540C\u6216\u7C7B\u4F3C\u7684\u547D\u4EE4\u3002
2064
+ 3. \u591A\u6761\u547D\u4EE4\u53EF\u7528\u5206\u53F7\u5408\u5E76\u4E3A\u4E00\u6B21\u8C03\u7528\u4EE5\u51CF\u5C11\u8F6E\u6B21\u3002
2065
+ 4. \u5220\u9664\u76EE\u5F55\u8BF7\u4F7F\u7528 Remove-Item -Recurse\uFF08\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u4F18\u5316\u4E3A\u66F4\u53EF\u9760\u7684\u65B9\u5F0F\uFF09\u3002` : `\u5728 ${SHELL} \u4E2D\u6267\u884C\u547D\u4EE4\u3002
2066
+ \u91CD\u8981\u89C4\u5219\uFF1A
2067
+ 1. \u6BCF\u6B21 bash \u8C03\u7528\u662F\u72EC\u7ACB\u5B50\u8FDB\u7A0B\uFF0Ccd \u547D\u4EE4\u4E0D\u4F1A\u6301\u4E45\u751F\u6548\u3002\u5982\u9700\u5728\u6307\u5B9A\u76EE\u5F55\u6267\u884C\uFF0C\u8BF7\u4F7F\u7528 cwd \u53C2\u6570\uFF0C\u6216\u5C06\u547D\u4EE4\u5408\u5E76\uFF1A\u5982 "cd mydir && ls" \u6216 "mkdir -p mydir && touch mydir/file.txt"\u3002
2068
+ 2. \u5982\u679C\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08\u8FD4\u56DE\u9519\u8BEF\u6216\u975E\u96F6\u9000\u51FA\u7801\uFF09\uFF0C\u8BF7\u7ACB\u5373\u505C\u6B62\uFF0C\u5C06\u9519\u8BEF\u4FE1\u606F\u544A\u77E5\u7528\u6237\uFF0C\u4E0D\u8981\u53CD\u590D\u5C1D\u8BD5\u76F8\u540C\u6216\u7C7B\u4F3C\u7684\u547D\u4EE4\u3002
2069
+ 3. \u591A\u6761\u547D\u4EE4\u53EF\u7528 && \u5408\u5E76\u4E3A\u4E00\u6B21\u8C03\u7528\u4EE5\u51CF\u5C11\u8F6E\u6B21\u3002`,
2070
+ parameters: {
2071
+ command: {
2072
+ type: "string",
2073
+ description: IS_WINDOWS ? `\u8981\u6267\u884C\u7684 PowerShell \u547D\u4EE4\u3002\u53EF\u7528\u5206\u53F7\u5408\u5E76\u591A\u6761\u547D\u4EE4\uFF0C\u5982\uFF1A"mkdir mydir; Set-Content mydir/file.txt '\u5185\u5BB9'"` : `\u8981\u6267\u884C\u7684 ${SHELL} \u547D\u4EE4\u3002\u53EF\u7528 && \u5408\u5E76\u591A\u6761\u547D\u4EE4\uFF0C\u5982\uFF1A"mkdir -p mydir && echo '\u5185\u5BB9' > mydir/file.txt"`,
2074
+ required: true
2075
+ },
2076
+ cwd: {
2077
+ type: "string",
2078
+ description: "\u547D\u4EE4\u6267\u884C\u7684\u5DE5\u4F5C\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u8DEF\u5F84\uFF09\u3002\u8BBE\u7F6E\u540E\u8BE5\u6B21\u547D\u4EE4\u5728\u6B64\u76EE\u5F55\u4E0B\u6267\u884C\uFF0C\u5E76\u81EA\u52A8\u8BB0\u4F4F\u8BE5\u76EE\u5F55\u4F9B\u540E\u7EED\u547D\u4EE4\u4F7F\u7528\u3002",
2079
+ required: false
2080
+ },
2081
+ timeout: {
2082
+ type: "number",
2083
+ description: "\u8D85\u65F6\u6BEB\u79D2\u6570\uFF0C\u9ED8\u8BA4 30000",
2084
+ required: false
2085
+ }
2086
+ },
2087
+ dangerous: false
2088
+ },
2089
+ async execute(args) {
2090
+ const command = String(args["command"] ?? "");
2091
+ const timeout = Number(args["timeout"] ?? 3e4);
2092
+ const cwdArg = args["cwd"] ? String(args["cwd"]) : void 0;
2093
+ if (!command.trim()) {
2094
+ throw new Error("command is required");
2095
+ }
2096
+ let effectiveCwd = persistentCwd;
2097
+ if (cwdArg) {
2098
+ const resolved = resolve2(persistentCwd, cwdArg);
2099
+ if (existsSync4(resolved)) {
2100
+ effectiveCwd = resolved;
2101
+ persistentCwd = resolved;
2102
+ } else {
2103
+ effectiveCwd = resolved;
2104
+ }
2105
+ }
2106
+ let actualCommand;
2107
+ if (IS_WINDOWS) {
2108
+ const fixedCommand = fixWindowsDeleteCommand(command);
2109
+ actualCommand = `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; $OutputEncoding = [System.Text.Encoding]::UTF8; ${fixedCommand}`;
2110
+ } else {
2111
+ actualCommand = command;
2112
+ }
2113
+ try {
2114
+ const output = execSync(actualCommand, {
2115
+ timeout,
2116
+ encoding: IS_WINDOWS ? "buffer" : "utf-8",
2117
+ stdio: ["pipe", "pipe", "pipe"],
2118
+ cwd: effectiveCwd,
2119
+ shell: SHELL,
2120
+ env: {
2121
+ ...process.env,
2122
+ PYTHONUTF8: "1",
2123
+ PYTHONIOENCODING: "utf-8"
2124
+ }
2125
+ });
2126
+ updateCwdFromCommand(command, effectiveCwd);
2127
+ const result = IS_WINDOWS && Buffer.isBuffer(output) ? output.toString("utf-8") : output;
2128
+ return result || "(command completed with no output)";
2129
+ } catch (err) {
2130
+ if (err && typeof err === "object" && "status" in err) {
2131
+ const execErr = err;
2132
+ const stderr = IS_WINDOWS && Buffer.isBuffer(execErr.stderr) ? execErr.stderr.toString("utf-8").trim() : execErr.stderr?.toString().trim() ?? "";
2133
+ const stdout = IS_WINDOWS && Buffer.isBuffer(execErr.stdout) ? execErr.stdout.toString("utf-8").trim() : execErr.stdout?.toString().trim() ?? "";
2134
+ const combined = [stdout, stderr].filter(Boolean).join("\n");
2135
+ throw new Error(
2136
+ `Exit code ${execErr.status}:
2137
+ ${combined || (execErr.message ?? "Unknown error")}
2138
+
2139
+ [\u547D\u4EE4\u5931\u8D25\u3002\u8BF7\u5C06\u4E0A\u8FF0\u9519\u8BEF\u4FE1\u606F\u544A\u77E5\u7528\u6237\uFF0C\u4E0D\u8981\u7EE7\u7EED\u5C1D\u8BD5\u53D8\u4F53\u547D\u4EE4\u3002]`
2140
+ );
2141
+ }
2142
+ throw err;
2143
+ }
2144
+ }
2145
+ };
2146
+ function fixWindowsDeleteCommand(command) {
2147
+ return command.replace(
2148
+ /Remove-Item\b([^;\n]*)/gi,
2149
+ (match, args) => {
2150
+ if (!/recurse/i.test(args)) return match;
2151
+ let pathValue = "";
2152
+ const pathMatch = args.match(/-Path\s+(['"]?)([^'";\s]+)\1/i) ?? args.match(/(?:^|\s)(['"]?)([^'";\s-][^'";\s]*)\1/);
2153
+ if (pathMatch) {
2154
+ pathValue = pathMatch[2] ?? "";
2155
+ }
2156
+ if (!pathValue) return match;
2157
+ return `cmd /c rmdir /s /q "${pathValue}"`;
2158
+ }
2159
+ );
2160
+ }
2161
+ function updateCwdFromCommand(command, baseCwd) {
2162
+ const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+([^\s;&|]+)/g)];
2163
+ if (cdMatches.length === 0) return;
2164
+ const lastMatch = cdMatches[cdMatches.length - 1];
2165
+ const target = lastMatch?.[1];
2166
+ if (!target || target.startsWith("$") || target === "~") return;
2167
+ try {
2168
+ const newDir = resolve2(baseCwd, target);
2169
+ if (existsSync4(newDir)) {
2170
+ persistentCwd = newDir;
2171
+ }
2172
+ } catch {
2173
+ }
2174
+ }
2175
+
2176
+ // src/tools/builtin/read-file.ts
2177
+ import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2178
+ var readFileTool = {
2179
+ definition: {
2180
+ name: "read_file",
2181
+ description: "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\u3002\u652F\u6301\u6587\u672C\u6587\u4EF6\uFF0C\u8FD4\u56DE\u6587\u4EF6\u7684\u5B8C\u6574\u5185\u5BB9\u3002",
2182
+ parameters: {
2183
+ path: {
2184
+ type: "string",
2185
+ description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u7684\u76F8\u5BF9\u8DEF\u5F84\uFF09",
2186
+ required: true
2187
+ },
2188
+ encoding: {
2189
+ type: "string",
2190
+ description: "\u7F16\u7801\u683C\u5F0F\uFF0C\u9ED8\u8BA4 utf-8",
2191
+ enum: ["utf-8", "utf8", "ascii", "base64"],
2192
+ required: false
2193
+ }
2194
+ },
2195
+ dangerous: false
2196
+ },
2197
+ async execute(args) {
2198
+ const filePath = String(args["path"] ?? "");
2199
+ const encoding = args["encoding"] ?? "utf-8";
2200
+ if (!filePath) throw new Error("path is required");
2201
+ if (!existsSync5(filePath)) throw new Error(`File not found: ${filePath}`);
2202
+ const content = readFileSync4(filePath, encoding);
2203
+ const lines = content.split("\n").length;
2204
+ return `[File: ${filePath} | ${lines} lines]
2205
+
2206
+ ${content}`;
2207
+ }
2208
+ };
2209
+
2210
+ // src/tools/builtin/write-file.ts
2211
+ import { writeFileSync as writeFileSync5, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
2212
+ import { dirname as dirname3 } from "path";
2213
+ var writeFileTool = {
2214
+ definition: {
2215
+ name: "write_file",
2216
+ description: `\u5C06\u5185\u5BB9\u5199\u5165\u6587\u4EF6\u3002\u5982\u679C\u6587\u4EF6\u4E0D\u5B58\u5728\u5219\u521B\u5EFA\uFF0C\u5B58\u5728\u5219\u8986\u76D6\uFF08append=false\uFF09\u6216\u8FFD\u52A0\uFF08append=true\uFF09\u3002\u4F1A\u81EA\u52A8\u521B\u5EFA\u6240\u9700\u7684\u7236\u76EE\u5F55\u3002
2217
+ \u91CD\u8981\u63D0\u793A\uFF1A\u5F53\u5185\u5BB9\u8F83\u957F\uFF08\u8D85\u8FC7500\u884C\u62163000\u5B57\u7B26\uFF09\u65F6\uFF0C\u5FC5\u987B\u5206\u591A\u6B21\u8C03\u7528\u5E76\u4F7F\u7528 append=true \u8FFD\u52A0\u5199\u5165\uFF0C\u6BCF\u6B21\u5199\u5165\u4E0D\u8D85\u8FC7300\u884C\uFF0C\u4EE5\u907F\u514D\u5185\u5BB9\u622A\u65AD\u3002\u7B2C\u4E00\u6B21\u8C03\u7528\u4F7F\u7528 append=false\uFF08\u8986\u76D6\uFF09\uFF0C\u540E\u7EED\u8C03\u7528\u4F7F\u7528 append=true\uFF08\u8FFD\u52A0\uFF09\u3002`,
2218
+ parameters: {
2219
+ path: {
2220
+ type: "string",
2221
+ description: "\u6587\u4EF6\u8DEF\u5F84",
2222
+ required: true
2223
+ },
2224
+ content: {
2225
+ type: "string",
2226
+ description: "\u8981\u5199\u5165\u7684\u5185\u5BB9",
2227
+ required: true
2228
+ },
2229
+ append: {
2230
+ type: "string",
2231
+ description: "\u662F\u5426\u8FFD\u52A0\u5199\u5165\u3002true=\u8FFD\u52A0\u5230\u6587\u4EF6\u672B\u5C3E\uFF0Cfalse=\u8986\u76D6\uFF08\u9ED8\u8BA4\uFF09\u3002\u957F\u6587\u4EF6\u5FC5\u987B\u5206\u6BB5\u8FFD\u52A0\u5199\u5165\u3002",
2232
+ required: false
2233
+ },
2234
+ encoding: {
2235
+ type: "string",
2236
+ description: "\u7F16\u7801\u683C\u5F0F\uFF0C\u9ED8\u8BA4 utf-8",
2237
+ required: false
2238
+ }
2239
+ },
2240
+ dangerous: false
2241
+ // executor 会将 write_file 标记为 'write' 级别
2242
+ },
2243
+ async execute(args) {
2244
+ const filePath = String(args["path"] ?? "");
2245
+ const content = String(args["content"] ?? "");
2246
+ const encoding = args["encoding"] ?? "utf-8";
2247
+ const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
2248
+ if (!filePath) throw new Error("path is required");
2249
+ if (!appendMode) {
2250
+ undoStack.push(filePath, `write_file: ${filePath}`);
2251
+ }
2252
+ mkdirSync5(dirname3(filePath), { recursive: true });
2253
+ if (appendMode) {
2254
+ appendFileSync(filePath, content, encoding);
2255
+ } else {
2256
+ writeFileSync5(filePath, content, encoding);
2257
+ }
2258
+ const lines = content.split("\n").length;
2259
+ const mode = appendMode ? "appended" : "written";
2260
+ return `File ${mode}: ${filePath} (${lines} lines, ${content.length} bytes)`;
2261
+ }
2262
+ };
2263
+
2264
+ // src/tools/builtin/edit-file.ts
2265
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync6 } from "fs";
2266
+ var editFileTool = {
2267
+ definition: {
2268
+ name: "edit_file",
2269
+ description: `\u7CBE\u51C6\u7F16\u8F91\u6587\u4EF6\u4E2D\u7684\u5185\u5BB9\uFF0C\u652F\u6301\u4E09\u79CD\u6A21\u5F0F\uFF1A
2270
+ 1. \u5B57\u7B26\u4E32\u66FF\u6362\uFF08\u6700\u5E38\u7528\uFF09\uFF1A\u63D0\u4F9B old_str \u548C new_str\uFF0C\u5C06\u6587\u4EF6\u4E2D\u7CBE\u786E\u5339\u914D\u7684 old_str \u66FF\u6362\u4E3A new_str\u3002old_str \u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\u51FA\u73B0\u3002
2271
+ 2. \u884C\u63D2\u5165\uFF1A\u63D0\u4F9B insert_after_line\uFF08\u884C\u53F7\uFF0C\u4ECE1\u5F00\u59CB\uFF09\u548C insert_content\uFF0C\u5728\u8BE5\u884C\u540E\u63D2\u5165\u65B0\u5185\u5BB9\u3002
2272
+ 3. \u884C\u5220\u9664\uFF1A\u63D0\u4F9B delete_from_line \u548C delete_to_line\uFF08\u542B\uFF09\uFF0C\u5220\u9664\u8BE5\u8303\u56F4\u5185\u7684\u884C\u3002
2273
+ \u6CE8\u610F\uFF1A\u8DEF\u5F84\u53EF\u4EE5\u662F\u7EDD\u5BF9\u8DEF\u5F84\u6216\u76F8\u5BF9\u4E8E\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u7684\u8DEF\u5F84\u3002`,
2274
+ parameters: {
2275
+ path: {
2276
+ type: "string",
2277
+ description: "\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84",
2278
+ required: true
2279
+ },
2280
+ old_str: {
2281
+ type: "string",
2282
+ description: "\u3010\u66FF\u6362\u6A21\u5F0F\u3011\u8981\u88AB\u66FF\u6362\u7684\u539F\u59CB\u5B57\u7B26\u4E32\uFF0C\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\u51FA\u73B0\uFF08\u5305\u542B\u8DB3\u591F\u4E0A\u4E0B\u6587\u4EE5\u786E\u4FDD\u552F\u4E00\u6027\uFF09",
2283
+ required: false
2284
+ },
2285
+ new_str: {
2286
+ type: "string",
2287
+ description: "\u3010\u66FF\u6362\u6A21\u5F0F\u3011\u66FF\u6362\u540E\u7684\u65B0\u5B57\u7B26\u4E32\uFF0C\u53EF\u4EE5\u4E3A\u7A7A\u5B57\u7B26\u4E32\uFF08\u8868\u793A\u5220\u9664 old_str\uFF09",
2288
+ required: false
2289
+ },
2290
+ insert_after_line: {
2291
+ type: "number",
2292
+ description: "\u3010\u63D2\u5165\u6A21\u5F0F\u3011\u5728\u7B2C\u51E0\u884C\u4E4B\u540E\u63D2\u5165\uFF0C\u4ECE 1 \u5F00\u59CB\u8BA1\u6570\uFF0C0 \u8868\u793A\u5728\u6587\u4EF6\u5F00\u5934\u63D2\u5165",
2293
+ required: false
2294
+ },
2295
+ insert_content: {
2296
+ type: "string",
2297
+ description: "\u3010\u63D2\u5165\u6A21\u5F0F\u3011\u8981\u63D2\u5165\u7684\u5185\u5BB9\uFF08\u4E0D\u9700\u8981\u624B\u52A8\u52A0\u6362\u884C\u7B26\uFF09",
2298
+ required: false
2299
+ },
2300
+ delete_from_line: {
2301
+ type: "number",
2302
+ description: "\u3010\u5220\u9664\u6A21\u5F0F\u3011\u4ECE\u7B2C\u51E0\u884C\u5F00\u59CB\u5220\u9664\uFF0C\u4ECE 1 \u5F00\u59CB\u8BA1\u6570",
2303
+ required: false
2304
+ },
2305
+ delete_to_line: {
2306
+ type: "number",
2307
+ description: "\u3010\u5220\u9664\u6A21\u5F0F\u3011\u5220\u9664\u5230\u7B2C\u51E0\u884C\uFF08\u542B\uFF09\uFF0C\u4ECE 1 \u5F00\u59CB\u8BA1\u6570",
2308
+ required: false
2309
+ },
2310
+ encoding: {
2311
+ type: "string",
2312
+ description: "\u6587\u4EF6\u7F16\u7801\uFF0C\u9ED8\u8BA4 utf-8",
2313
+ required: false
2314
+ }
2315
+ },
2316
+ dangerous: false
2317
+ // executor 中 edit_file 按 write 级别处理
2318
+ },
2319
+ async execute(args) {
2320
+ const filePath = String(args["path"] ?? "");
2321
+ const encoding = args["encoding"] ?? "utf-8";
2322
+ if (!filePath) throw new Error("path is required");
2323
+ if (!existsSync6(filePath)) throw new Error(`File not found: ${filePath}`);
2324
+ const original = readFileSync5(filePath, encoding);
2325
+ if (args["old_str"] !== void 0) {
2326
+ const oldStr = String(args["old_str"]);
2327
+ const newStr = String(args["new_str"] ?? "");
2328
+ if (oldStr === "") throw new Error("old_str cannot be empty");
2329
+ const firstIndex = original.indexOf(oldStr);
2330
+ if (firstIndex === -1) {
2331
+ const lines = original.split("\n");
2332
+ return `ERROR: old_str not found in file.
2333
+ File has ${lines.length} lines. Please read the file first and use exact text including whitespace/indentation.`;
2334
+ }
2335
+ const secondIndex = original.indexOf(oldStr, firstIndex + 1);
2336
+ if (secondIndex !== -1) {
2337
+ return `ERROR: old_str appears multiple times in file (at least at positions ${firstIndex} and ${secondIndex}). Please include more surrounding context to make it unique.`;
2338
+ }
2339
+ undoStack.push(filePath, `edit_file (replace): ${filePath}`);
2340
+ const updated = original.slice(0, firstIndex) + newStr + original.slice(firstIndex + oldStr.length);
2341
+ writeFileSync6(filePath, updated, encoding);
2342
+ const oldLines = oldStr.split("\n").length;
2343
+ const newLines = newStr.split("\n").length;
2344
+ const linesBefore = original.slice(0, firstIndex).split("\n").length;
2345
+ return `Successfully edited ${filePath}
2346
+ Location: around line ${linesBefore}
2347
+ Replaced: ${oldLines} line(s) \u2192 ${newLines} line(s)
2348
+ Old: ${truncatePreview(oldStr)}
2349
+ New: ${truncatePreview(newStr)}`;
2350
+ }
2351
+ if (args["insert_after_line"] !== void 0) {
2352
+ const afterLine = Number(args["insert_after_line"]);
2353
+ const content = String(args["insert_content"] ?? "");
2354
+ const lines = original.split("\n");
2355
+ if (afterLine < 0 || afterLine > lines.length) {
2356
+ throw new Error(`insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
2357
+ }
2358
+ undoStack.push(filePath, `edit_file (insert): ${filePath}`);
2359
+ lines.splice(afterLine, 0, content);
2360
+ writeFileSync6(filePath, lines.join("\n"), encoding);
2361
+ return `Successfully inserted ${content.split("\n").length} line(s) after line ${afterLine} in ${filePath}`;
2362
+ }
2363
+ if (args["delete_from_line"] !== void 0) {
2364
+ const fromLine = Number(args["delete_from_line"]);
2365
+ const toLine = Number(args["delete_to_line"] ?? args["delete_from_line"]);
2366
+ const lines = original.split("\n");
2367
+ if (fromLine < 1 || toLine < fromLine || toLine > lines.length) {
2368
+ throw new Error(
2369
+ `Invalid line range: ${fromLine}-${toLine} (file has ${lines.length} lines, lines are 1-indexed)`
2370
+ );
2371
+ }
2372
+ undoStack.push(filePath, `edit_file (delete): ${filePath}`);
2373
+ const deleted = lines.splice(fromLine - 1, toLine - fromLine + 1);
2374
+ writeFileSync6(filePath, lines.join("\n"), encoding);
2375
+ return `Successfully deleted lines ${fromLine}-${toLine} (${deleted.length} lines) from ${filePath}`;
2376
+ }
2377
+ throw new Error(
2378
+ "No operation specified. Provide either: (old_str + new_str) for replace, (insert_after_line + insert_content) for insert, or (delete_from_line + delete_to_line) for delete."
2379
+ );
2380
+ }
2381
+ };
2382
+ function truncatePreview(str, maxLen = 80) {
2383
+ const oneLine = str.replace(/\n/g, "\u21B5");
2384
+ if (oneLine.length <= maxLen) return JSON.stringify(str);
2385
+ return JSON.stringify(oneLine.slice(0, maxLen)) + "...";
2386
+ }
2387
+
2388
+ // src/tools/builtin/list-dir.ts
2389
+ import { readdirSync as readdirSync2, statSync, existsSync as existsSync7 } from "fs";
2390
+ import { join as join3 } from "path";
2391
+ var listDirTool = {
2392
+ definition: {
2393
+ name: "list_dir",
2394
+ description: "\u5217\u51FA\u76EE\u5F55\u5185\u5BB9\uFF0C\u663E\u793A\u6587\u4EF6\u540D\u3001\u7C7B\u578B\u548C\u5927\u5C0F\u3002",
2395
+ parameters: {
2396
+ path: {
2397
+ type: "string",
2398
+ description: "\u76EE\u5F55\u8DEF\u5F84\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55",
2399
+ required: false
2400
+ },
2401
+ recursive: {
2402
+ type: "boolean",
2403
+ description: "\u662F\u5426\u9012\u5F52\u5217\u51FA\u5B50\u76EE\u5F55\uFF0C\u9ED8\u8BA4 false",
2404
+ required: false
2405
+ }
2406
+ },
2407
+ dangerous: false
2408
+ },
2409
+ async execute(args) {
2410
+ const dirPath = String(args["path"] ?? process.cwd());
2411
+ const recursive = Boolean(args["recursive"] ?? false);
2412
+ if (!existsSync7(dirPath)) throw new Error(`Directory not found: ${dirPath}`);
2413
+ const lines = [`Directory: ${dirPath}
2414
+ `];
2415
+ listRecursive(dirPath, "", recursive, lines);
2416
+ return lines.join("\n");
2417
+ }
2418
+ };
2419
+ function listRecursive(basePath, indent, recursive, lines) {
2420
+ let entries;
2421
+ try {
2422
+ entries = readdirSync2(basePath, { withFileTypes: true });
2423
+ } catch {
2424
+ lines.push(`${indent}(permission denied)`);
2425
+ return;
2426
+ }
2427
+ const sorted = entries.sort((a, b) => {
2428
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
2429
+ return a.name.localeCompare(b.name);
2430
+ });
2431
+ for (const entry of sorted) {
2432
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
2433
+ if (entry.isDirectory()) {
2434
+ lines.push(`${indent}\u{1F4C1} ${entry.name}/ (skipped)`);
2435
+ }
2436
+ continue;
2437
+ }
2438
+ if (entry.isDirectory()) {
2439
+ lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
2440
+ if (recursive) {
2441
+ listRecursive(join3(basePath, entry.name), indent + " ", true, lines);
2442
+ }
2443
+ } else {
2444
+ try {
2445
+ const stat = statSync(join3(basePath, entry.name));
2446
+ const size = formatSize(stat.size);
2447
+ lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
2448
+ } catch {
2449
+ lines.push(`${indent}\u{1F4C4} ${entry.name}`);
2450
+ }
2451
+ }
2452
+ }
2453
+ }
2454
+ function formatSize(bytes) {
2455
+ if (bytes < 1024) return `${bytes}B`;
2456
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
2457
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
2458
+ }
2459
+
2460
+ // src/tools/builtin/grep-files.ts
2461
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2, existsSync as existsSync8 } from "fs";
2462
+ import { join as join4, relative } from "path";
2463
+ var grepFilesTool = {
2464
+ definition: {
2465
+ name: "grep_files",
2466
+ description: `\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u5339\u914D\u7684\u6587\u672C\u6216\u4EE3\u7801\uFF0C\u8FD4\u56DE\u5339\u914D\u884C\u53CA\u884C\u53F7\u3002\u9002\u7528\u4E8E\uFF1A
2467
+ - \u67E5\u627E\u51FD\u6570/\u7C7B\u5B9A\u4E49\u4F4D\u7F6E\uFF08\u5982\u641C\u7D22 "function handleChat" \u6216 "class ProviderRegistry"\uFF09
2468
+ - \u67E5\u627E\u7279\u5B9A\u53D8\u91CF\u6216\u5B57\u7B26\u4E32\u7684\u6240\u6709\u4F7F\u7528\u4F4D\u7F6E
2469
+ - \u5728\u4FEE\u6539\u4EE3\u7801\u524D\u5148\u786E\u8BA4\u5F71\u54CD\u8303\u56F4
2470
+ \u652F\u6301\u6B63\u5219\u8868\u8FBE\u5F0F\u3002\u81EA\u52A8\u8DF3\u8FC7 node_modules\u3001dist\u3001.git \u7B49\u76EE\u5F55\u3002`,
2471
+ parameters: {
2472
+ pattern: {
2473
+ type: "string",
2474
+ description: '\u641C\u7D22\u6A21\u5F0F\uFF0C\u652F\u6301\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08\u5982 "function\\s+\\w+" \u6216\u5B57\u9762\u91CF "import React"\uFF09',
2475
+ required: true
2476
+ },
2477
+ path: {
2478
+ type: "string",
2479
+ description: "\u641C\u7D22\u6839\u76EE\u5F55\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55",
2480
+ required: false
2481
+ },
2482
+ file_pattern: {
2483
+ type: "string",
2484
+ description: '\u6587\u4EF6\u540D\u8FC7\u6EE4\uFF0C\u652F\u6301\u7B80\u5355\u901A\u914D\u7B26\uFF08\u5982 "*.ts"\u3001"*.py"\u3001"*.json"\uFF09\uFF0C\u9ED8\u8BA4\u641C\u7D22\u6240\u6709\u6587\u672C\u6587\u4EF6',
2485
+ required: false
2486
+ },
2487
+ ignore_case: {
2488
+ type: "boolean",
2489
+ description: "\u662F\u5426\u5FFD\u7565\u5927\u5C0F\u5199\uFF0C\u9ED8\u8BA4 false",
2490
+ required: false
2491
+ },
2492
+ context_lines: {
2493
+ type: "number",
2494
+ description: "\u6BCF\u4E2A\u5339\u914D\u884C\u524D\u540E\u663E\u793A\u7684\u4E0A\u4E0B\u6587\u884C\u6570\uFF0C\u9ED8\u8BA4 0\uFF08\u4EC5\u663E\u793A\u5339\u914D\u884C\uFF09",
2495
+ required: false
2496
+ },
2497
+ max_results: {
2498
+ type: "number",
2499
+ description: "\u6700\u5927\u8FD4\u56DE\u7ED3\u679C\u6570\uFF0C\u9ED8\u8BA4 50",
2500
+ required: false
2501
+ }
2502
+ },
2503
+ dangerous: false
2504
+ },
2505
+ async execute(args) {
2506
+ const pattern = String(args["pattern"] ?? "");
2507
+ const rootPath = String(args["path"] ?? process.cwd());
2508
+ const filePattern = args["file_pattern"] ? String(args["file_pattern"]) : void 0;
2509
+ const ignoreCase = Boolean(args["ignore_case"] ?? false);
2510
+ const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
2511
+ const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
2512
+ if (!pattern) throw new Error("pattern is required");
2513
+ if (!existsSync8(rootPath)) throw new Error(`Path not found: ${rootPath}`);
2514
+ let regex;
2515
+ try {
2516
+ regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
2517
+ } catch {
2518
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2519
+ regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
2520
+ }
2521
+ const results = [];
2522
+ const stat = statSync2(rootPath);
2523
+ if (stat.isFile()) {
2524
+ searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
2525
+ } else {
2526
+ collectFiles(rootPath, filePattern, results, regex, contextLines, maxResults, rootPath);
2527
+ }
2528
+ if (results.length === 0) {
2529
+ return `No matches found for pattern: ${pattern}
2530
+ Searched in: ${rootPath}${filePattern ? `
2531
+ File filter: ${filePattern}` : ""}`;
2532
+ }
2533
+ const lines = [
2534
+ `Found ${results.length} match(es) for: ${pattern}`,
2535
+ `Searched in: ${rootPath}`,
2536
+ ""
2537
+ ];
2538
+ let currentFile = "";
2539
+ for (const r of results) {
2540
+ if (r.file !== currentFile) {
2541
+ if (currentFile) lines.push("");
2542
+ lines.push(`\u2500\u2500 ${r.file} \u2500\u2500`);
2543
+ currentFile = r.file;
2544
+ }
2545
+ if (r.contextBefore) {
2546
+ for (const [ln, text] of r.contextBefore) {
2547
+ lines.push(` ${String(ln).padStart(4)}\u2502 ${text}`);
2548
+ }
2549
+ }
2550
+ lines.push(`\u25B6 ${String(r.lineNumber).padStart(4)}\u2502 ${r.lineText}`);
2551
+ if (r.contextAfter) {
2552
+ for (const [ln, text] of r.contextAfter) {
2553
+ lines.push(` ${String(ln).padStart(4)}\u2502 ${text}`);
2554
+ }
2555
+ }
2556
+ }
2557
+ if (results.length >= maxResults) {
2558
+ lines.push("");
2559
+ lines.push(`(Results truncated at ${maxResults}. Use max_results or narrow your search.)`);
2560
+ }
2561
+ return lines.join("\n");
2562
+ }
2563
+ };
2564
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", ".next", "__pycache__", ".cache", "coverage", ".nyc_output"]);
2565
+ var BINARY_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".mp3", ".mp4", ".wav", ".zip", ".tar", ".gz", ".rar", ".7z", ".exe", ".dll", ".so", ".dylib", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]);
2566
+ function matchesFilePattern(filename, pattern) {
2567
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2568
+ return new RegExp(`^${escaped}$`, "i").test(filename);
2569
+ }
2570
+ function isBinary(filename) {
2571
+ const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase();
2572
+ return BINARY_EXTS.has(ext);
2573
+ }
2574
+ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxResults, rootPath) {
2575
+ if (results.length >= maxResults) return;
2576
+ let entries;
2577
+ try {
2578
+ entries = readdirSync3(dirPath, { withFileTypes: true });
2579
+ } catch {
2580
+ return;
2581
+ }
2582
+ for (const entry of entries) {
2583
+ if (results.length >= maxResults) return;
2584
+ if (entry.isDirectory()) {
2585
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
2586
+ collectFiles(join4(dirPath, entry.name), filePattern, results, regex, contextLines, maxResults, rootPath);
2587
+ } else if (entry.isFile()) {
2588
+ if (isBinary(entry.name)) continue;
2589
+ if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
2590
+ const fullPath = join4(dirPath, entry.name);
2591
+ const relPath = relative(rootPath, fullPath);
2592
+ searchInFile(fullPath, relPath, regex, contextLines, maxResults, results);
2593
+ }
2594
+ }
2595
+ }
2596
+ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, results) {
2597
+ let content;
2598
+ try {
2599
+ content = readFileSync6(fullPath, "utf-8");
2600
+ } catch {
2601
+ return;
2602
+ }
2603
+ if (content.length > 1e6) return;
2604
+ const lines = content.split("\n");
2605
+ regex.lastIndex = 0;
2606
+ for (let i = 0; i < lines.length; i++) {
2607
+ if (results.length >= maxResults) return;
2608
+ regex.lastIndex = 0;
2609
+ if (regex.test(lines[i])) {
2610
+ const result = {
2611
+ file: displayPath,
2612
+ lineNumber: i + 1,
2613
+ lineText: lines[i].trimEnd()
2614
+ };
2615
+ if (contextLines > 0) {
2616
+ result.contextBefore = [];
2617
+ result.contextAfter = [];
2618
+ for (let c = Math.max(0, i - contextLines); c < i; c++) {
2619
+ result.contextBefore.push([c + 1, lines[c].trimEnd()]);
2620
+ }
2621
+ for (let c = i + 1; c <= Math.min(lines.length - 1, i + contextLines); c++) {
2622
+ result.contextAfter.push([c + 1, lines[c].trimEnd()]);
2623
+ }
2624
+ }
2625
+ results.push(result);
2626
+ }
2627
+ }
2628
+ }
2629
+
2630
+ // src/tools/builtin/glob-files.ts
2631
+ import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync9 } from "fs";
2632
+ import { join as join5, relative as relative2, basename } from "path";
2633
+ var globFilesTool = {
2634
+ definition: {
2635
+ name: "glob_files",
2636
+ description: `\u6309\u6587\u4EF6\u540D\u6216\u8DEF\u5F84\u6A21\u5F0F\u67E5\u627E\u6587\u4EF6\uFF0C\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u8DEF\u5F84\u5217\u8868\u3002\u9002\u7528\u4E8E\uFF1A
2637
+ - \u67E5\u627E\u6240\u6709\u67D0\u7C7B\u578B\u6587\u4EF6\uFF08\u5982 "**/*.ts" \u627E\u6240\u6709 TypeScript \u6587\u4EF6\uFF09
2638
+ - \u67E5\u627E\u7279\u5B9A\u540D\u79F0\u7684\u6587\u4EF6\uFF08\u5982 "**/index.ts" \u6216 "package.json"\uFF09
2639
+ - \u67E5\u627E\u67D0\u76EE\u5F55\u4E0B\u7684\u6587\u4EF6\uFF08\u5982 "src/components/**"\uFF09
2640
+ \u7ED3\u679C\u6309\u6700\u8FD1\u4FEE\u6539\u65F6\u95F4\u6392\u5E8F\u3002\u81EA\u52A8\u8DF3\u8FC7 node_modules\u3001dist\u3001.git \u7B49\u76EE\u5F55\u3002`,
2641
+ parameters: {
2642
+ pattern: {
2643
+ type: "string",
2644
+ description: 'glob \u6A21\u5F0F\uFF0C\u5982 "**/*.ts"\u3001"src/**/*.tsx"\u3001"**/package.json"\u3001"*.md"',
2645
+ required: true
2646
+ },
2647
+ path: {
2648
+ type: "string",
2649
+ description: "\u641C\u7D22\u6839\u76EE\u5F55\uFF0C\u9ED8\u8BA4\u4E3A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55",
2650
+ required: false
2651
+ },
2652
+ max_results: {
2653
+ type: "number",
2654
+ description: "\u6700\u5927\u8FD4\u56DE\u6587\u4EF6\u6570\uFF0C\u9ED8\u8BA4 100",
2655
+ required: false
2656
+ }
2657
+ },
2658
+ dangerous: false
2659
+ },
2660
+ async execute(args) {
2661
+ const pattern = String(args["pattern"] ?? "");
2662
+ const rootPath = String(args["path"] ?? process.cwd());
2663
+ const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
2664
+ if (!pattern) throw new Error("pattern is required");
2665
+ if (!existsSync9(rootPath)) throw new Error(`Path not found: ${rootPath}`);
2666
+ const regex = globToRegex(pattern);
2667
+ const matches = [];
2668
+ collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
2669
+ if (matches.length === 0) {
2670
+ return `No files matched pattern: ${pattern}
2671
+ Searched in: ${rootPath}`;
2672
+ }
2673
+ matches.sort((a, b) => b.mtime - a.mtime);
2674
+ const lines = [
2675
+ `Found ${matches.length} file(s) matching: ${pattern}`,
2676
+ `Searched in: ${rootPath}`,
2677
+ "",
2678
+ ...matches.map((m) => m.relPath)
2679
+ ];
2680
+ if (matches.length >= maxResults) {
2681
+ lines.push("");
2682
+ lines.push(`(Results truncated at ${maxResults}. Use max_results or narrow your pattern.)`);
2683
+ }
2684
+ return lines.join("\n");
2685
+ }
2686
+ };
2687
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
2688
+ "node_modules",
2689
+ ".git",
2690
+ "dist",
2691
+ "dist-cjs",
2692
+ "release",
2693
+ ".next",
2694
+ "__pycache__",
2695
+ ".cache",
2696
+ "coverage",
2697
+ ".nyc_output",
2698
+ ".turbo",
2699
+ ".vite",
2700
+ "build",
2701
+ "out"
2702
+ ]);
2703
+ function globToRegex(pattern) {
2704
+ const normalized = pattern.replace(/\\/g, "/");
2705
+ let regStr = "";
2706
+ let i = 0;
2707
+ while (i < normalized.length) {
2708
+ const ch = normalized[i];
2709
+ if (ch === "*" && normalized[i + 1] === "*") {
2710
+ regStr += ".*";
2711
+ i += 2;
2712
+ if (normalized[i] === "/") i++;
2713
+ } else if (ch === "*") {
2714
+ regStr += "[^/]*";
2715
+ i++;
2716
+ } else if (ch === "?") {
2717
+ regStr += "[^/]";
2718
+ i++;
2719
+ } else if (".+^${}()|[]\\".includes(ch)) {
2720
+ regStr += "\\" + ch;
2721
+ i++;
2722
+ } else {
2723
+ regStr += ch;
2724
+ i++;
2725
+ }
2726
+ }
2727
+ if (!normalized.includes("/")) {
2728
+ return new RegExp(`(^|/)${regStr}$`, "i");
2729
+ }
2730
+ return new RegExp(`(^|/)${regStr}$`, "i");
2731
+ }
2732
+ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
2733
+ if (results.length >= maxResults) return;
2734
+ let entries;
2735
+ try {
2736
+ entries = readdirSync4(dirPath, { withFileTypes: true });
2737
+ } catch {
2738
+ return;
2739
+ }
2740
+ for (const entry of entries) {
2741
+ if (results.length >= maxResults) break;
2742
+ const fullPath = join5(dirPath, entry.name);
2743
+ if (entry.isDirectory()) {
2744
+ if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
2745
+ collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
2746
+ } else if (entry.isFile()) {
2747
+ const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
2748
+ if (regex.test(relPath) || regex.test(basename(relPath))) {
2749
+ try {
2750
+ const stat = statSync3(fullPath);
2751
+ results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
2752
+ } catch {
2753
+ results.push({ relPath, absPath: fullPath, mtime: 0 });
2754
+ }
2755
+ }
2756
+ }
2757
+ }
2758
+ }
2759
+
2760
+ // src/tools/builtin/run-interactive.ts
2761
+ import { spawn } from "child_process";
2762
+ import { platform as platform2 } from "os";
2763
+ var IS_WINDOWS2 = platform2() === "win32";
2764
+ var runInteractiveTool = {
2765
+ definition: {
2766
+ name: "run_interactive",
2767
+ description: '\u8FD0\u884C\u9700\u8981 stdin \u4EA4\u4E92\u7684\u547D\u4EE4\u884C\u7A0B\u5E8F\uFF08\u5982 Python \u731C\u6570\u6E38\u620F\u3001\u95EE\u7B54\u811A\u672C\u3001\u83DC\u5355\u7A0B\u5E8F\uFF09\u3002\u901A\u8FC7 stdin_lines \u6570\u7EC4\u63D0\u524D\u51C6\u5907\u597D\u6240\u6709\u8F93\u5165\u884C\uFF0C\u7A0B\u5E8F\u4F9D\u6B21\u6D88\u8D39\u5E76\u8FD4\u56DE\u5B8C\u6574\u8F93\u51FA\u3002\u3010\u91CD\u8981\u3011args \u53C2\u6570\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\uFF0C\u5982 ["workspace/guess.py"]\uFF0C\u4E0D\u80FD\u662F\u5B57\u7B26\u4E32\u3002\u3010\u731C\u6570\u7B56\u7565\u3011\u731C1-100\u7684\u6570\u7528\u4E8C\u5206\u6CD5\uFF1A\u4ECE50\u5F00\u59CB\uFF0C\u592A\u5927\u5219\u53D6\u4E2D\u95F4\u504F\u5C0F\u503C\uFF0C\u592A\u5C0F\u5219\u53D6\u4E2D\u95F4\u504F\u5927\u503C\uFF0C\u6700\u591A7\u6B21\u5FC5\u731C\u4E2D\u3002\u793A\u4F8B stdin_lines: ["50","25","37","43","40","41","n"]\uFF08"n"=\u4E0D\u518D\u73A9\u4E00\u6B21\uFF09\u3002Windows Python\u8DEF\u5F84: C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe',
2768
+ parameters: {
2769
+ executable: {
2770
+ type: "string",
2771
+ description: '\u53EF\u6267\u884C\u6587\u4EF6\u7684\u5B8C\u6574\u8DEF\u5F84\u6216\u547D\u4EE4\u540D\uFF0C\u5982 "python"\u3001"C:\\\\Users\\\\Jinzd\\\\anaconda3\\\\envs\\\\python312\\\\python.exe"\u3001"node"',
2772
+ required: true
2773
+ },
2774
+ args: {
2775
+ type: "array",
2776
+ description: '\u4F20\u7ED9\u53EF\u6267\u884C\u6587\u4EF6\u7684\u53C2\u6570\u6570\u7EC4\uFF0C\u5982 ["workspace/guess.py"] \u6216 ["-c", "print(1)"]',
2777
+ items: { type: "string" },
2778
+ required: true
2779
+ },
2780
+ stdin_lines: {
2781
+ type: "array",
2782
+ description: '\u6309\u987A\u5E8F\u63D0\u4F9B\u7ED9\u7A0B\u5E8F stdin \u7684\u8F93\u5165\u884C\u6570\u7EC4\u3002\u4F8B\u5982\u731C\u6570\u6E38\u620F\u4E2D\u4F9D\u6B21\u8F93\u5165 ["50", "25", "37"]\u3002\u6BCF\u884C\u4F1A\u81EA\u52A8\u8FFD\u52A0\u6362\u884C\u7B26\u3002',
2783
+ items: { type: "string" },
2784
+ required: true
2785
+ },
2786
+ timeout: {
2787
+ type: "number",
2788
+ description: "\u6574\u4F53\u8D85\u65F6\u6BEB\u79D2\u6570\uFF0C\u9ED8\u8BA4 20000\uFF0820\u79D2\uFF09",
2789
+ required: false
2790
+ }
2791
+ },
2792
+ dangerous: false
2793
+ },
2794
+ async execute(args) {
2795
+ const executable = String(args["executable"] ?? "").trim();
2796
+ const rawArgs = args["args"];
2797
+ const cmdArgs = Array.isArray(rawArgs) ? rawArgs.map(String) : typeof rawArgs === "string" && rawArgs.trim() ? [rawArgs.trim()] : [];
2798
+ const rawStdin = args["stdin_lines"];
2799
+ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String) : typeof rawStdin === "string" && rawStdin.trim() ? rawStdin.split(",").map((s) => s.trim()).filter(Boolean) : [];
2800
+ const timeout = Number(args["timeout"] ?? 2e4);
2801
+ if (!executable) {
2802
+ throw new Error("executable is required");
2803
+ }
2804
+ const env = {
2805
+ ...process.env,
2806
+ // 强制 Python UTF-8 模式,修复 Windows 中文乱码
2807
+ PYTHONUTF8: "1",
2808
+ PYTHONIOENCODING: "utf-8",
2809
+ PYTHONDONTWRITEBYTECODE: "1"
2810
+ };
2811
+ return new Promise((resolve4) => {
2812
+ const child = spawn(executable, cmdArgs.map(String), {
2813
+ cwd: process.cwd(),
2814
+ env,
2815
+ stdio: ["pipe", "pipe", "pipe"]
2816
+ });
2817
+ let stdout = "";
2818
+ let stderr = "";
2819
+ child.stdout.setEncoding("utf-8");
2820
+ child.stderr.setEncoding("utf-8");
2821
+ child.stdout.on("data", (chunk) => {
2822
+ stdout += chunk;
2823
+ });
2824
+ child.stderr.on("data", (chunk) => {
2825
+ stderr += chunk;
2826
+ });
2827
+ let lineIdx = 0;
2828
+ const writeNextLine = () => {
2829
+ if (lineIdx < stdinLines.length && !child.stdin.destroyed) {
2830
+ setTimeout(() => {
2831
+ child.stdin.write(stdinLines[lineIdx++] + (IS_WINDOWS2 ? "\r\n" : "\n"));
2832
+ writeNextLine();
2833
+ }, 150);
2834
+ } else if (!child.stdin.destroyed) {
2835
+ child.stdin.end();
2836
+ }
2837
+ };
2838
+ setTimeout(writeNextLine, 400);
2839
+ const timer = setTimeout(() => {
2840
+ child.kill();
2841
+ resolve4(`[Timeout after ${timeout}ms]
2842
+ ${buildOutput(stdout, stderr)}`);
2843
+ }, timeout);
2844
+ child.on("close", (code) => {
2845
+ clearTimeout(timer);
2846
+ const output = buildOutput(stdout, stderr);
2847
+ if (code !== 0 && code !== null) {
2848
+ resolve4(`Exit code ${code}:
2849
+ ${output}`);
2850
+ } else {
2851
+ resolve4(output || "(no output)");
2852
+ }
2853
+ });
2854
+ child.on("error", (err) => {
2855
+ clearTimeout(timer);
2856
+ resolve4(
2857
+ `Failed to start process "${executable}": ${err.message}
2858
+ Hint: On Windows, use the full path to the executable, e.g.:
2859
+ C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
2860
+ );
2861
+ });
2862
+ });
2863
+ }
2864
+ };
2865
+ function buildOutput(stdout, stderr) {
2866
+ const parts = [];
2867
+ if (stdout.trim()) parts.push(stdout);
2868
+ if (stderr.trim()) parts.push(`[stderr]
2869
+ ${stderr}`);
2870
+ return parts.join("\n") || "(no output)";
2871
+ }
2872
+
2873
+ // src/tools/builtin/web-fetch.ts
2874
+ function htmlToText(html) {
2875
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<svg[\s\S]*?<\/svg>/gi, "");
2876
+ text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, content) => {
2877
+ const prefix = "#".repeat(Number(lvl));
2878
+ return `
2879
+ ${prefix} ${stripTags(content).trim()}
2880
+ `;
2881
+ });
2882
+ text = text.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_m, code) => {
2883
+ return "\n```\n" + stripTags(code) + "\n```\n";
2884
+ });
2885
+ text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m, item) => {
2886
+ return `
2887
+ - ${stripTags(item).trim()}`;
2888
+ });
2889
+ text = text.replace(/<\/(p|div|section|article|blockquote|tr)>/gi, "\n");
2890
+ text = text.replace(/<br\s*\/?>/gi, "\n");
2891
+ text = text.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_m, href, label) => {
2892
+ const l = stripTags(label).trim();
2893
+ if (href.startsWith("http") && l && l !== href) return `[${l}](${href})`;
2894
+ return l || href;
2895
+ });
2896
+ text = stripTags(text);
2897
+ text = text.replace(/\n{3,}/g, "\n\n");
2898
+ text = text.split("\n").map((l) => l.trimEnd()).join("\n");
2899
+ return text.trim();
2900
+ }
2901
+ function stripTags(html) {
2902
+ return html.replace(/<[^>]+>/g, "");
2903
+ }
2904
+ function extractTitle(html) {
2905
+ const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
2906
+ return m ? stripTags(m[1]).trim() : "";
2907
+ }
2908
+ function extractDescription(html) {
2909
+ const m = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i) ?? html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']description["']/i);
2910
+ return m ? m[1].trim() : "";
2911
+ }
2912
+ var MAX_OUTPUT = 16e3;
2913
+ var webFetchTool = {
2914
+ definition: {
2915
+ name: "web_fetch",
2916
+ description: "Fetch a URL and return its content as plain text / Markdown. Use this to read documentation, API references, READMEs, articles, or any public web page. Follows redirects automatically. Returns the first ~16000 characters of extracted text.",
2917
+ parameters: {
2918
+ url: {
2919
+ type: "string",
2920
+ description: "The full URL to fetch (must start with http:// or https://)",
2921
+ required: true
2922
+ },
2923
+ selector: {
2924
+ type: "string",
2925
+ description: "Optional: keyword to search in the extracted text. If provided, returns only the paragraphs / sections that contain this keyword (case-insensitive).",
2926
+ required: false
2927
+ }
2928
+ }
2929
+ },
2930
+ async execute(args) {
2931
+ const url = String(args["url"] ?? "").trim();
2932
+ const selector = args["selector"] ? String(args["selector"]).trim() : "";
2933
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
2934
+ throw new Error(`Invalid URL: "${url}". URL must start with http:// or https://`);
2935
+ }
2936
+ const controller = new AbortController();
2937
+ const timeoutId = setTimeout(() => controller.abort(), 2e4);
2938
+ let rawHtml;
2939
+ let finalUrl;
2940
+ let contentType;
2941
+ try {
2942
+ const resp = await fetch(url, {
2943
+ signal: controller.signal,
2944
+ headers: {
2945
+ "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
2946
+ Accept: "text/html,application/xhtml+xml,text/plain,*/*",
2947
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
2948
+ },
2949
+ redirect: "follow"
2950
+ });
2951
+ clearTimeout(timeoutId);
2952
+ finalUrl = resp.url;
2953
+ contentType = resp.headers.get("content-type") ?? "";
2954
+ if (!resp.ok) {
2955
+ throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
2956
+ }
2957
+ const buf = await resp.arrayBuffer();
2958
+ rawHtml = new TextDecoder("utf-8", { fatal: false }).decode(buf.slice(0, 2e6));
2959
+ } catch (err) {
2960
+ clearTimeout(timeoutId);
2961
+ if (err.name === "AbortError") {
2962
+ throw new Error(`Request timed out after 20s: ${url}`);
2963
+ }
2964
+ throw err;
2965
+ }
2966
+ let text;
2967
+ if (contentType.includes("text/plain") || contentType.includes("application/json")) {
2968
+ text = rawHtml;
2969
+ } else {
2970
+ const title = extractTitle(rawHtml);
2971
+ const desc = extractDescription(rawHtml);
2972
+ let body = htmlToText(rawHtml);
2973
+ const header = [
2974
+ title ? `# ${title}` : "",
2975
+ desc ? `> ${desc}` : "",
2976
+ `Source: ${finalUrl}`,
2977
+ ""
2978
+ ].filter(Boolean).join("\n");
2979
+ text = header + "\n\n" + body;
2980
+ }
2981
+ if (selector) {
2982
+ const lower = selector.toLowerCase();
2983
+ const paragraphs = text.split("\n\n");
2984
+ const matched = paragraphs.filter((p) => p.toLowerCase().includes(lower));
2985
+ if (matched.length > 0) {
2986
+ text = `[Filtered by keyword: "${selector}"]
2987
+
2988
+ ` + matched.join("\n\n");
2989
+ } else {
2990
+ text = `[No paragraphs contain keyword: "${selector}"]
2991
+
2992
+ ` + text;
2993
+ }
2994
+ }
2995
+ if (text.length > MAX_OUTPUT) {
2996
+ const kept = text.slice(0, MAX_OUTPUT);
2997
+ text = kept + `
2998
+
2999
+ ... [\u5185\u5BB9\u5DF2\u622A\u65AD\uFF1A\u5171 ${text.length} \u5B57\u7B26\uFF0C\u4EC5\u8FD4\u56DE\u524D ${MAX_OUTPUT} \u5B57\u7B26\u3002\u5982\u9700\u67E5\u770B\u66F4\u591A\uFF0C\u8BF7\u4F7F\u7528 selector \u53C2\u6570\u7F29\u5C0F\u8303\u56F4] ...`;
3000
+ }
3001
+ return text;
3002
+ }
3003
+ };
3004
+
3005
+ // src/tools/builtin/save-last-response.ts
3006
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
3007
+ import { dirname as dirname4 } from "path";
3008
+ var lastResponseStore = { content: "" };
3009
+ var saveLastResponseTool = {
3010
+ definition: {
3011
+ name: "save_last_response",
3012
+ description: `\u751F\u6210\u5927\u578B\u6587\u6863\uFF08\u6A21\u8003\u8BD5\u5377\u3001\u62A5\u544A\u3001\u957F\u7BC7\u6587\u7AE0\u7B49\uFF09\u5E76\u4FDD\u5B58\u5230\u6587\u4EF6\u7684\u4E13\u7528\u5DE5\u5177\u3002
3013
+
3014
+ \u3010\u6B63\u786E\u7528\u6CD5\u3011\u5F53\u7528\u6237\u8981\u6C42\u751F\u6210\u5E76\u4FDD\u5B58\u5927\u578B\u6587\u6863\u65F6\uFF0C\u76F4\u63A5\u8C03\u7528\u6B64\u5DE5\u5177\u5E76\u4F20\u5165\u76EE\u6807\u8DEF\u5F84\uFF1A
3015
+ - \u7CFB\u7EDF\u4F1A\u81EA\u52A8\u53D1\u8D77\u6D41\u5F0F\u751F\u6210\u8BF7\u6C42\uFF0C\u5185\u5BB9\u540C\u65F6\u8F93\u51FA\u5230\u7EC8\u7AEF\u548C\u78C1\u76D8\uFF08tee \u6A21\u5F0F\uFF09
3016
+ - \u65E0\u9700\u5148\u8F93\u51FA\u5185\u5BB9\u518D\u8C03\u7528\u2014\u2014\u7CFB\u7EDF\u81EA\u52A8\u5B8C\u6210"\u751F\u6210 + \u4FDD\u5B58"\u4E00\u6B65\u5230\u4F4D
3017
+ - \u53C2\u6570\u53EA\u6709\u6587\u4EF6\u8DEF\u5F84\uFF0C\u4E0D\u901A\u8FC7\u53C2\u6570\u4F20\u9012\u5185\u5BB9\uFF0C\u5F7B\u5E95\u89C4\u907F API arguments \u622A\u65AD\u95EE\u9898
3018
+
3019
+ \u3010\u7981\u6B62\u3011\u4E0D\u8981\u7528 write_file \u4FDD\u5B58\u5927\u5185\u5BB9\u2014\u2014write_file \u7684 content \u53C2\u6570\u5728\u5927\u6587\u6863\u65F6\u4F1A\u88AB API \u622A\u65AD\uFF0C\u5BFC\u81F4\u6587\u4EF6\u4E0D\u5B8C\u6574\u3002
3020
+
3021
+ \u3010\u9002\u7528\u573A\u666F\u3011\u6A21\u8003\u8BD5\u5377\uFF08600-700\u884C\uFF0C15-25KB\uFF09\u3001\u6280\u672F\u62A5\u544A\u3001\u957F\u7BC7\u6587\u7AE0\u7B49\u8D85\u8FC7 2KB \u7684\u5185\u5BB9\u3002`,
3022
+ parameters: {
3023
+ path: {
3024
+ type: "string",
3025
+ description: "\u4FDD\u5B58\u6587\u4EF6\u7684\u8DEF\u5F84\uFF08\u542B\u6587\u4EF6\u540D\uFF09\uFF0C\u5982 exam_papers/20260221-01-\u6A21\u8003-\u8FDB\u9636.md",
3026
+ required: true
3027
+ }
3028
+ },
3029
+ dangerous: false
3030
+ // getDangerLevel 中标记为 write
3031
+ },
3032
+ async execute(args) {
3033
+ const filePath = String(args["path"] ?? "");
3034
+ if (!filePath) throw new Error("path is required");
3035
+ const content = lastResponseStore.content;
3036
+ if (!content) {
3037
+ throw new Error("\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u5185\u5BB9\uFF1AAI \u5C1A\u672A\u4EA7\u751F\u4EFB\u4F55\u56DE\u590D\uFF0C\u6216\u4E0A\u6B21\u56DE\u590D\u4E3A\u7A7A\u3002");
3038
+ }
3039
+ undoStack.push(filePath, `save_last_response: ${filePath}`);
3040
+ mkdirSync6(dirname4(filePath), { recursive: true });
3041
+ writeFileSync7(filePath, content, "utf-8");
3042
+ const lines = content.split("\n").length;
3043
+ return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
3044
+ }
3045
+ };
3046
+
3047
+ // src/tools/registry.ts
3048
+ var ToolRegistry = class {
3049
+ tools = /* @__PURE__ */ new Map();
3050
+ constructor() {
3051
+ this.register(bashTool);
3052
+ this.register(readFileTool);
3053
+ this.register(writeFileTool);
3054
+ this.register(editFileTool);
3055
+ this.register(listDirTool);
3056
+ this.register(grepFilesTool);
3057
+ this.register(globFilesTool);
3058
+ this.register(runInteractiveTool);
3059
+ this.register(webFetchTool);
3060
+ this.register(saveLastResponseTool);
3061
+ }
3062
+ register(tool) {
3063
+ this.tools.set(tool.definition.name, tool);
3064
+ }
3065
+ get(name) {
3066
+ return this.tools.get(name);
3067
+ }
3068
+ /** 返回所有工具的 schema,用于发送给 AI */
3069
+ getDefinitions() {
3070
+ return [...this.tools.values()].map((t) => t.definition);
3071
+ }
3072
+ listAll() {
3073
+ return [...this.tools.values()];
3074
+ }
3075
+ };
3076
+
3077
+ // src/tools/executor.ts
3078
+ import chalk4 from "chalk";
3079
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
3080
+
3081
+ // src/tools/types.ts
3082
+ function getDangerLevel(toolName, args) {
3083
+ if (toolName === "bash") {
3084
+ const cmd = String(args["command"] ?? "");
3085
+ if (/\brm\s+(-\w*f\w*|-\w*r\w*f\w*)\b|\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
3086
+ if (/\bRemove-Item\b|\bri\s+.*-Recurse\b|\brd\s+\/s\b|\brmdir\s+\/s\b/.test(cmd)) return "destructive";
3087
+ if (/\bdel\s+\S/.test(cmd) && !/\bmkdir\b/.test(cmd)) return "destructive";
3088
+ if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
3089
+ return "safe";
3090
+ }
3091
+ if (toolName === "write_file") return "write";
3092
+ if (toolName === "edit_file") return "write";
3093
+ if (toolName === "save_last_response") return "write";
3094
+ if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "run_interactive" || toolName === "web_fetch") return "safe";
3095
+ return "write";
3096
+ }
3097
+
3098
+ // src/tools/diff-utils.ts
3099
+ import chalk3 from "chalk";
3100
+ function renderDiff(oldText, newText, opts = {}) {
3101
+ const contextLines = opts.contextLines ?? 3;
3102
+ const maxLines = opts.maxLines ?? 120;
3103
+ const filePath = opts.filePath ?? "";
3104
+ const oldLines = oldText.split("\n");
3105
+ const newLines = newText.split("\n");
3106
+ const hunks = computeHunks(oldLines, newLines, contextLines);
3107
+ if (hunks.length === 0) {
3108
+ return chalk3.dim(" (no changes)");
3109
+ }
3110
+ const output = [];
3111
+ if (filePath) {
3112
+ output.push(chalk3.bold.white(`--- ${filePath} (before)`));
3113
+ output.push(chalk3.bold.white(`+++ ${filePath} (after)`));
3114
+ }
3115
+ let totalDisplayed = 0;
3116
+ for (const hunk of hunks) {
3117
+ if (totalDisplayed >= maxLines) {
3118
+ output.push(chalk3.dim(` ... (diff truncated, too many changes)`));
3119
+ break;
3120
+ }
3121
+ output.push(
3122
+ chalk3.cyan(
3123
+ `@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@`
3124
+ )
3125
+ );
3126
+ for (const line of hunk.lines) {
3127
+ if (totalDisplayed >= maxLines) break;
3128
+ totalDisplayed++;
3129
+ if (line.type === "context") {
3130
+ output.push(chalk3.dim(` ${line.text}`));
3131
+ } else if (line.type === "remove") {
3132
+ output.push(chalk3.red(`- ${line.text}`));
3133
+ } else {
3134
+ output.push(chalk3.green(`+ ${line.text}`));
3135
+ }
3136
+ }
3137
+ }
3138
+ return output.join("\n");
3139
+ }
3140
+ function computeHunks(oldLines, newLines, contextLines) {
3141
+ const edits = diffLines(oldLines, newLines);
3142
+ if (edits.every((e) => e.type === "context")) return [];
3143
+ const hunks = [];
3144
+ let i = 0;
3145
+ while (i < edits.length) {
3146
+ if (edits[i].type === "context") {
3147
+ i++;
3148
+ continue;
3149
+ }
3150
+ const start = Math.max(0, i - contextLines);
3151
+ let end = i;
3152
+ while (end < edits.length) {
3153
+ if (edits[end].type !== "context") {
3154
+ end++;
3155
+ } else {
3156
+ let hasMoreChange = false;
3157
+ for (let j = end + 1; j < Math.min(edits.length, end + contextLines * 2 + 1); j++) {
3158
+ if (edits[j].type !== "context") {
3159
+ hasMoreChange = true;
3160
+ break;
3161
+ }
3162
+ }
3163
+ if (hasMoreChange) {
3164
+ end++;
3165
+ } else {
3166
+ break;
3167
+ }
3168
+ }
3169
+ }
3170
+ end = Math.min(edits.length, end + contextLines);
3171
+ const hunkEdits = edits.slice(start, end);
3172
+ let oldStart = 0;
3173
+ let newStart = 0;
3174
+ for (let k = 0; k < start; k++) {
3175
+ if (edits[k].type !== "add") oldStart++;
3176
+ if (edits[k].type !== "remove") newStart++;
3177
+ }
3178
+ let oldCount = 0;
3179
+ let newCount = 0;
3180
+ for (const e of hunkEdits) {
3181
+ if (e.type !== "add") oldCount++;
3182
+ if (e.type !== "remove") newCount++;
3183
+ }
3184
+ hunks.push({
3185
+ oldStart,
3186
+ oldCount,
3187
+ newStart,
3188
+ newCount,
3189
+ lines: hunkEdits.map((e) => ({ type: e.type, text: e.text }))
3190
+ });
3191
+ i = end;
3192
+ }
3193
+ return hunks;
3194
+ }
3195
+ function diffLines(oldLines, newLines) {
3196
+ const n = oldLines.length;
3197
+ const m = newLines.length;
3198
+ if (n * m > 25e4) {
3199
+ return simpleDiff(oldLines, newLines);
3200
+ }
3201
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
3202
+ for (let i2 = 1; i2 <= n; i2++) {
3203
+ for (let j2 = 1; j2 <= m; j2++) {
3204
+ if (oldLines[i2 - 1] === newLines[j2 - 1]) {
3205
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
3206
+ } else {
3207
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
3208
+ }
3209
+ }
3210
+ }
3211
+ const result = [];
3212
+ let i = n;
3213
+ let j = m;
3214
+ while (i > 0 || j > 0) {
3215
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
3216
+ result.unshift({ type: "context", text: oldLines[i - 1] });
3217
+ i--;
3218
+ j--;
3219
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
3220
+ result.unshift({ type: "add", text: newLines[j - 1] });
3221
+ j--;
3222
+ } else {
3223
+ result.unshift({ type: "remove", text: oldLines[i - 1] });
3224
+ i--;
3225
+ }
3226
+ }
3227
+ return result;
3228
+ }
3229
+ function simpleDiff(oldLines, newLines) {
3230
+ const result = [];
3231
+ const maxLen = Math.max(oldLines.length, newLines.length);
3232
+ for (let i = 0; i < maxLen; i++) {
3233
+ const o = oldLines[i];
3234
+ const n = newLines[i];
3235
+ if (o !== void 0 && n !== void 0) {
3236
+ if (o === n) {
3237
+ result.push({ type: "context", text: o });
3238
+ } else {
3239
+ result.push({ type: "remove", text: o });
3240
+ result.push({ type: "add", text: n });
3241
+ }
3242
+ } else if (o !== void 0) {
3243
+ result.push({ type: "remove", text: o });
3244
+ } else if (n !== void 0) {
3245
+ result.push({ type: "add", text: n });
3246
+ }
3247
+ }
3248
+ return result;
3249
+ }
3250
+
3251
+ // src/tools/executor.ts
3252
+ var MAX_TOOL_OUTPUT_CHARS = 12e3;
3253
+ function truncateOutput(content, toolName) {
3254
+ if (content.length <= MAX_TOOL_OUTPUT_CHARS) return content;
3255
+ const keepHead = Math.floor(MAX_TOOL_OUTPUT_CHARS * 0.7);
3256
+ const keepTail = Math.floor(MAX_TOOL_OUTPUT_CHARS * 0.2);
3257
+ const omitted = content.length - keepHead - keepTail;
3258
+ const lines = content.split("\n").length;
3259
+ const head = content.slice(0, keepHead);
3260
+ const tail = content.slice(content.length - keepTail);
3261
+ return head + `
3262
+
3263
+ ... [\u8F93\u51FA\u5DF2\u622A\u65AD\uFF1A\u5171 ${content.length} \u5B57\u7B26 / ${lines} \u884C\uFF0C\u7701\u7565\u4E86\u4E2D\u95F4 ${omitted} \u5B57\u7B26\u3002\u5982\u9700\u67E5\u770B\u5B8C\u6574\u5185\u5BB9\uFF0C\u8BF7\u4F7F\u7528 read_file \u5206\u6BB5\u8BFB\u53D6` + (toolName === "bash" ? "\uFF0C\u6216\u7F29\u5C0F\u547D\u4EE4\u8303\u56F4" : "") + `] ...
3264
+
3265
+ ` + tail;
3266
+ }
3267
+ var ToolExecutor = class {
3268
+ constructor(registry) {
3269
+ this.registry = registry;
3270
+ }
3271
+ round = 0;
3272
+ totalRounds = 0;
3273
+ /** readline 接口引用,由 repl.ts 注入,用于 confirm() 读取用户输入 */
3274
+ rl = null;
3275
+ /**
3276
+ * confirm() 进行中标志。
3277
+ * repl.ts 的主循环 line handler 在此为 true 时忽略输入,
3278
+ * 防止用户输入 "y"+Enter 被同时触发 once('line') 和主循环 on('line')。
3279
+ */
3280
+ confirming = false;
3281
+ /** confirm() 的取消回调,由 SIGINT handler 调用 */
3282
+ cancelConfirmFn = null;
3283
+ /**
3284
+ * 由外部(repl.ts SIGINT handler)调用,将当前 confirm() 等待视为用户按 N 取消。
3285
+ * 若当前没有 confirm() 进行中,无操作。
3286
+ */
3287
+ cancelConfirm() {
3288
+ if (this.cancelConfirmFn) {
3289
+ this.cancelConfirmFn();
3290
+ }
3291
+ }
3292
+ setRoundInfo(current, total) {
3293
+ this.round = current;
3294
+ this.totalRounds = total;
3295
+ }
3296
+ /**
3297
+ * 注入 readline 接口,供 confirm() 使用。
3298
+ * 必须在 start() 之前调用,rl 初始化后立即注入。
3299
+ */
3300
+ setReadline(rl) {
3301
+ this.rl = rl;
3302
+ }
3303
+ async execute(call) {
3304
+ const tool = this.registry.get(call.name);
3305
+ if (!tool) {
3306
+ return {
3307
+ callId: call.id,
3308
+ content: `Unknown tool: ${call.name}`,
3309
+ isError: true
3310
+ };
3311
+ }
3312
+ const dangerLevel = getDangerLevel(call.name, call.arguments);
3313
+ if (dangerLevel === "destructive") {
3314
+ const confirmed = await this.confirm(call, dangerLevel);
3315
+ if (!confirmed) {
3316
+ return {
3317
+ callId: call.id,
3318
+ content: "User cancelled the operation.",
3319
+ isError: false
3320
+ };
3321
+ }
3322
+ }
3323
+ this.printToolCall(call);
3324
+ this.printDiffPreview(call);
3325
+ try {
3326
+ const rawContent = await tool.execute(call.arguments);
3327
+ const content = truncateOutput(rawContent, call.name);
3328
+ const wasTruncated = content !== rawContent;
3329
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
3330
+ return { callId: call.id, content, isError: false };
3331
+ } catch (err) {
3332
+ const message = err instanceof Error ? err.message : String(err);
3333
+ this.printToolResult(call.name, message, true, false);
3334
+ return { callId: call.id, content: message, isError: true };
3335
+ }
3336
+ }
3337
+ async executeAll(calls) {
3338
+ const results = [];
3339
+ for (const call of calls) {
3340
+ results.push(await this.execute(call));
3341
+ }
3342
+ return results;
3343
+ }
3344
+ printToolCall(call) {
3345
+ const dangerLevel = getDangerLevel(call.name, call.arguments);
3346
+ console.log();
3347
+ const icon = dangerLevel === "write" ? chalk4.yellow("\u270E Tool: ") : chalk4.bold.cyan("\u2699 Tool: ");
3348
+ const roundBadge = this.totalRounds > 0 ? chalk4.dim(` [${this.round}/${this.totalRounds}]`) : "";
3349
+ console.log(icon + chalk4.white(call.name) + roundBadge);
3350
+ for (const [key, val] of Object.entries(call.arguments)) {
3351
+ let valStr;
3352
+ if (Array.isArray(val)) {
3353
+ const json = JSON.stringify(val);
3354
+ valStr = json.length > 160 ? json.slice(0, 160) + "..." : json;
3355
+ } else if (typeof val === "string" && val.length > 120) {
3356
+ valStr = val.slice(0, 120) + "...";
3357
+ } else {
3358
+ valStr = String(val);
3359
+ }
3360
+ console.log(chalk4.gray(` ${key}: `) + chalk4.white(valStr));
3361
+ }
3362
+ }
3363
+ /**
3364
+ * 对 write_file / edit_file 在执行前展示 diff 预览。
3365
+ * - write_file:比较旧文件内容与新内容
3366
+ * - edit_file (replace):比较旧字符串与新字符串
3367
+ * - edit_file (insert/delete):显示操作摘要,不做 diff(变化明确)
3368
+ */
3369
+ printDiffPreview(call) {
3370
+ if (call.name === "write_file") {
3371
+ const filePath = String(call.arguments["path"] ?? "");
3372
+ const newContent = String(call.arguments["content"] ?? "");
3373
+ if (!filePath) return;
3374
+ if (existsSync10(filePath)) {
3375
+ let oldContent;
3376
+ try {
3377
+ oldContent = readFileSync7(filePath, "utf-8");
3378
+ } catch {
3379
+ return;
3380
+ }
3381
+ if (oldContent === newContent) {
3382
+ console.log(chalk4.dim(" (file content unchanged)"));
3383
+ return;
3384
+ }
3385
+ const diff = renderDiff(oldContent, newContent, { filePath, contextLines: 3 });
3386
+ console.log(chalk4.dim(" \u2500\u2500 diff preview \u2500\u2500"));
3387
+ console.log(diff);
3388
+ console.log();
3389
+ } else {
3390
+ const lines = newContent.split("\n");
3391
+ const preview = lines.slice(0, 20).map((l) => chalk4.green(`+ ${l}`)).join("\n");
3392
+ const more = lines.length > 20 ? chalk4.dim(`
3393
+ ... (+${lines.length - 20} more lines)`) : "";
3394
+ console.log(chalk4.dim(" \u2500\u2500 new file preview \u2500\u2500"));
3395
+ console.log(preview + more);
3396
+ console.log();
3397
+ }
3398
+ } else if (call.name === "edit_file") {
3399
+ const filePath = String(call.arguments["path"] ?? "");
3400
+ if (!filePath || !existsSync10(filePath)) return;
3401
+ const oldStr = call.arguments["old_str"];
3402
+ const newStr = call.arguments["new_str"];
3403
+ if (oldStr !== void 0) {
3404
+ const diff = renderDiff(
3405
+ String(oldStr),
3406
+ String(newStr ?? ""),
3407
+ { filePath, contextLines: 2 }
3408
+ );
3409
+ console.log(chalk4.dim(" \u2500\u2500 diff preview \u2500\u2500"));
3410
+ console.log(diff);
3411
+ console.log();
3412
+ } else if (call.arguments["insert_after_line"] !== void 0) {
3413
+ const line = Number(call.arguments["insert_after_line"]);
3414
+ const insertContent = String(call.arguments["insert_content"] ?? "");
3415
+ const insertLines = insertContent.split("\n");
3416
+ const preview = insertLines.slice(0, 5).map((l) => chalk4.green(`+ ${l}`)).join("\n");
3417
+ const more = insertLines.length > 5 ? chalk4.dim(`
3418
+ ... (+${insertLines.length - 5} more lines)`) : "";
3419
+ console.log(chalk4.dim(` \u2500\u2500 insert after line ${line} \u2500\u2500`));
3420
+ console.log(preview + more);
3421
+ console.log();
3422
+ } else if (call.arguments["delete_from_line"] !== void 0) {
3423
+ const from = Number(call.arguments["delete_from_line"]);
3424
+ const to = Number(call.arguments["delete_to_line"] ?? from);
3425
+ let fileContent;
3426
+ try {
3427
+ fileContent = readFileSync7(filePath, "utf-8");
3428
+ } catch {
3429
+ return;
3430
+ }
3431
+ const fileLines = fileContent.split("\n");
3432
+ const deleted = fileLines.slice(from - 1, to);
3433
+ const preview = deleted.slice(0, 5).map((l) => chalk4.red(`- ${l}`)).join("\n");
3434
+ const more = deleted.length > 5 ? chalk4.dim(`
3435
+ ... (-${deleted.length - 5} more lines)`) : "";
3436
+ console.log(chalk4.dim(` \u2500\u2500 delete lines ${from}\u2013${to} \u2500\u2500`));
3437
+ console.log(preview + more);
3438
+ console.log();
3439
+ }
3440
+ }
3441
+ }
3442
+ printToolResult(name, content, isError, wasTruncated) {
3443
+ if (isError) {
3444
+ console.log(chalk4.red(`\u26A0 ${name} error: `) + chalk4.gray(content.slice(0, 300)));
3445
+ } else {
3446
+ const lines = content.split("\n");
3447
+ const maxLines = name === "run_interactive" ? 40 : 8;
3448
+ const preview = lines.slice(0, maxLines).join("\n");
3449
+ const moreLines = lines.length > maxLines ? chalk4.gray(`
3450
+ ... (${lines.length - maxLines} more lines)`) : "";
3451
+ const truncatedNote = wasTruncated ? chalk4.yellow(`
3452
+ \u26A1 \u8F93\u51FA\u5DF2\u622A\u65AD\u81F3 ${MAX_TOOL_OUTPUT_CHARS} \u5B57\u7B26\u540E\u8FD4\u56DE\u7ED9 AI`) : "";
3453
+ console.log(chalk4.green("\u2713 Result: ") + chalk4.gray(preview) + moreLines + truncatedNote);
3454
+ }
3455
+ console.log();
3456
+ }
3457
+ confirm(call, level) {
3458
+ const color = level === "destructive" ? chalk4.red : chalk4.yellow;
3459
+ const label = level === "destructive" ? "\u26A0 DESTRUCTIVE" : "\u270E Write";
3460
+ console.log();
3461
+ console.log(color(`${label} operation: `) + chalk4.bold(call.name));
3462
+ for (const [key, val] of Object.entries(call.arguments)) {
3463
+ const valStr = typeof val === "string" && val.length > 200 ? val.slice(0, 200) + "..." : String(val);
3464
+ console.log(chalk4.gray(` ${key}: `) + valStr);
3465
+ }
3466
+ if (!this.rl) {
3467
+ process.stdout.write(color("No readline: auto-rejected.\n"));
3468
+ return Promise.resolve(false);
3469
+ }
3470
+ const rl = this.rl;
3471
+ const rlAny = rl;
3472
+ const savedOutput = rlAny.output;
3473
+ rlAny.output = process.stdout;
3474
+ rl.resume();
3475
+ process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
3476
+ this.confirming = true;
3477
+ return new Promise((resolve4) => {
3478
+ const cleanup = (answer) => {
3479
+ rl.removeListener("line", onLine);
3480
+ this.cancelConfirmFn = null;
3481
+ rl.pause();
3482
+ rlAny.output = savedOutput;
3483
+ this.confirming = false;
3484
+ resolve4(answer === "y");
3485
+ };
3486
+ const onLine = (line) => cleanup(line.trim().toLowerCase());
3487
+ this.cancelConfirmFn = () => {
3488
+ process.stdout.write(chalk4.gray("\n(cancelled)\n"));
3489
+ cleanup("n");
3490
+ };
3491
+ rl.once("line", onLine);
3492
+ });
3493
+ }
3494
+ };
3495
+
3496
+ // src/tools/git-context.ts
3497
+ import { execSync as execSync2 } from "child_process";
3498
+ import { existsSync as existsSync11 } from "fs";
3499
+ import { join as join6 } from "path";
3500
+ function runGit(cmd, cwd) {
3501
+ try {
3502
+ return execSync2(`git ${cmd}`, {
3503
+ cwd,
3504
+ encoding: "utf-8",
3505
+ stdio: ["pipe", "pipe", "pipe"],
3506
+ timeout: 5e3
3507
+ }).trim();
3508
+ } catch {
3509
+ return null;
3510
+ }
3511
+ }
3512
+ function getGitContext(cwd = process.cwd()) {
3513
+ if (!existsSync11(join6(cwd, ".git"))) {
3514
+ const result = runGit("rev-parse --git-dir", cwd);
3515
+ if (!result) return null;
3516
+ }
3517
+ const branch = runGit("rev-parse --abbrev-ref HEAD", cwd);
3518
+ if (!branch) return null;
3519
+ const statusOutput = runGit("status --porcelain", cwd) ?? "";
3520
+ const statusLines = statusOutput ? statusOutput.split("\n").filter(Boolean) : [];
3521
+ const stagedFiles = [];
3522
+ const changedFiles = [];
3523
+ for (const line of statusLines) {
3524
+ const xy = line.slice(0, 2);
3525
+ const file = line.slice(3).trim();
3526
+ const indexStatus = xy[0];
3527
+ const workStatus = xy[1];
3528
+ if (indexStatus && indexStatus !== " " && indexStatus !== "?") {
3529
+ stagedFiles.push(`${indexStatus} ${file}`);
3530
+ }
3531
+ if (workStatus && workStatus !== " ") {
3532
+ changedFiles.push(`${workStatus} ${file}`);
3533
+ }
3534
+ }
3535
+ const logOutput = runGit("log --oneline -3", cwd) ?? "";
3536
+ const recentCommits = logOutput ? logOutput.split("\n").filter(Boolean) : [];
3537
+ const unpushedOutput = runGit("log @{u}..HEAD --oneline 2>/dev/null", cwd);
3538
+ const hasUnpushed = unpushedOutput !== null && unpushedOutput.trim().length > 0;
3539
+ return {
3540
+ branch,
3541
+ changedFiles,
3542
+ stagedFiles,
3543
+ recentCommits,
3544
+ hasUnpushed
3545
+ };
3546
+ }
3547
+ function formatGitContextForPrompt(ctx) {
3548
+ const lines = ["# Git Repository Status", ""];
3549
+ lines.push(`- **Branch**: \`${ctx.branch}\``);
3550
+ if (ctx.stagedFiles.length > 0) {
3551
+ lines.push(`- **Staged** (${ctx.stagedFiles.length} files):`);
3552
+ for (const f of ctx.stagedFiles.slice(0, 10)) {
3553
+ lines.push(` - ${f}`);
3554
+ }
3555
+ if (ctx.stagedFiles.length > 10) {
3556
+ lines.push(` - ... and ${ctx.stagedFiles.length - 10} more`);
3557
+ }
3558
+ }
3559
+ if (ctx.changedFiles.length > 0) {
3560
+ lines.push(`- **Modified** (${ctx.changedFiles.length} files):`);
3561
+ for (const f of ctx.changedFiles.slice(0, 10)) {
3562
+ lines.push(` - ${f}`);
3563
+ }
3564
+ if (ctx.changedFiles.length > 10) {
3565
+ lines.push(` - ... and ${ctx.changedFiles.length - 10} more`);
3566
+ }
3567
+ }
3568
+ if (ctx.stagedFiles.length === 0 && ctx.changedFiles.length === 0) {
3569
+ lines.push("- **Working tree**: clean");
3570
+ }
3571
+ if (ctx.recentCommits.length > 0) {
3572
+ lines.push("- **Recent commits**:");
3573
+ for (const c of ctx.recentCommits) {
3574
+ lines.push(` - ${c}`);
3575
+ }
3576
+ }
3577
+ if (ctx.hasUnpushed) {
3578
+ lines.push("- \u26A0\uFE0F Has unpushed commits");
3579
+ }
3580
+ return lines.join("\n");
3581
+ }
3582
+
3583
+ // src/repl/repl.ts
3584
+ var IMAGE_MIME = {
3585
+ ".png": "image/png",
3586
+ ".jpg": "image/jpeg",
3587
+ ".jpeg": "image/jpeg",
3588
+ ".gif": "image/gif",
3589
+ ".webp": "image/webp"
3590
+ };
3591
+ function parseAtReferences(input, cwd) {
3592
+ const atPattern = /@(?:"([^"]+)"|'([^']+)'|(\S+))/g;
3593
+ const refs = [];
3594
+ const imageParts = [];
3595
+ let textBody = input;
3596
+ let match;
3597
+ while ((match = atPattern.exec(input)) !== null) {
3598
+ const rawPath = match[1] ?? match[2] ?? match[3] ?? "";
3599
+ const absPath = resolve3(cwd, rawPath);
3600
+ const ext = extname2(rawPath).toLowerCase();
3601
+ const mime = IMAGE_MIME[ext];
3602
+ if (!existsSync12(absPath)) {
3603
+ refs.push({ path: rawPath, type: "notfound" });
3604
+ continue;
3605
+ }
3606
+ if (mime) {
3607
+ const data = readFileSync8(absPath).toString("base64");
3608
+ imageParts.push({
3609
+ type: "image_url",
3610
+ image_url: { url: `data:${mime};base64,${data}` }
3611
+ });
3612
+ refs.push({ path: rawPath, type: "image" });
3613
+ textBody = textBody.replace(match[0], "").trim();
3614
+ } else {
3615
+ const content = readFileSync8(absPath, "utf-8");
3616
+ const inlined = `
3617
+
3618
+ [File: ${rawPath}]
3619
+ \`\`\`
3620
+ ${content}
3621
+ \`\`\``;
3622
+ textBody = textBody.replace(match[0], inlined);
3623
+ refs.push({ path: rawPath, type: "text" });
3624
+ }
3625
+ }
3626
+ const parts = [];
3627
+ if (textBody.trim()) {
3628
+ parts.push({ type: "text", text: textBody.trim() });
3629
+ }
3630
+ parts.push(...imageParts);
3631
+ return { parts, hasImage: imageParts.length > 0, refs };
3632
+ }
3633
+ var MAX_TOOL_ROUNDS = 20;
3634
+ var CONTEXT_FILE_CANDIDATES = ["AICLI.md", "CLAUDE.md", ".aicli/context.md"];
3635
+ var Repl = class {
3636
+ constructor(providers, sessions, config, events) {
3637
+ this.providers = providers;
3638
+ this.sessions = sessions;
3639
+ this.config = config;
3640
+ this.events = events;
3641
+ this.renderer = new Renderer();
3642
+ this.commands = new CommandRegistry();
3643
+ for (const cmd of createDefaultCommands()) {
3644
+ this.commands.register(cmd);
3645
+ }
3646
+ this.currentProvider = config.getDefaultProvider();
3647
+ if (!providers.has(this.currentProvider)) {
3648
+ const first = providers.getFirstAvailable();
3649
+ if (!first) throw new Error("No providers configured");
3650
+ this.currentProvider = first.info.id;
3651
+ }
3652
+ const defaultModels = config.get("defaultModels");
3653
+ this.currentModel = defaultModels[this.currentProvider] ?? providers.get(this.currentProvider).info.defaultModel;
3654
+ this.rl = readline.createInterface({
3655
+ input: process.stdin,
3656
+ output: process.stdout,
3657
+ terminal: true
3658
+ });
3659
+ this.toolRegistry = new ToolRegistry();
3660
+ this.toolExecutor = new ToolExecutor(this.toolRegistry);
3661
+ this.toolExecutor.setReadline(this.rl);
3662
+ }
3663
+ rl;
3664
+ currentProvider;
3665
+ currentModel;
3666
+ running = false;
3667
+ renderer;
3668
+ commands;
3669
+ toolRegistry;
3670
+ toolExecutor;
3671
+ /** 运行时有效的 system prompt(合并了项目上下文 + 用户配置) */
3672
+ activeSystemPrompt;
3673
+ /** 当前加载的项目上下文文件路径(用于 /context 命令显示状态) */
3674
+ contextFilePath = null;
3675
+ /** 本次会话累计 token 用量 */
3676
+ sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
3677
+ /** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
3678
+ gitBranch = null;
3679
+ /**
3680
+ * 交互式列表选择器进行中标志。
3681
+ * 与 toolExecutor.confirming 类似:主循环 line handler 在此为 true 时忽略 line 事件,
3682
+ * 防止 selectFromList 结束后 stdin.pause()/resume() 释放的残留字节被解析为新命令。
3683
+ */
3684
+ selecting = false;
3685
+ /**
3686
+ * 查找并读取项目上下文文件,返回其内容。
3687
+ * 优先级:config.contextFile(若非 'auto')> AICLI.md > CLAUDE.md > .aicli/context.md
3688
+ * 返回 { content, filePath } 或 null(未找到 / 已禁用)
3689
+ */
3690
+ loadProjectContext() {
3691
+ const setting = this.config.get("contextFile");
3692
+ if (setting === false) return null;
3693
+ const cwd = process.cwd();
3694
+ if (setting !== "auto") {
3695
+ const fullPath = join7(cwd, setting);
3696
+ if (existsSync12(fullPath)) {
3697
+ const content = readFileSync8(fullPath, "utf-8").trim();
3698
+ if (content) return { content, filePath: setting };
3699
+ }
3700
+ return null;
3701
+ }
3702
+ for (const candidate of CONTEXT_FILE_CANDIDATES) {
3703
+ const fullPath = join7(cwd, candidate);
3704
+ if (existsSync12(fullPath)) {
3705
+ const content = readFileSync8(fullPath, "utf-8").trim();
3706
+ if (content) return { content, filePath: candidate };
3707
+ }
3708
+ }
3709
+ return null;
3710
+ }
3711
+ /**
3712
+ * 将项目上下文内容合并到 system prompt。
3713
+ * 合并顺序:用户 systemPrompt → 项目上下文(AICLI.md) → Git 状态
3714
+ */
3715
+ buildSystemPromptWithContext(projectContext, gitContextStr = null) {
3716
+ const userPrompt = this.config.get("session").systemPrompt;
3717
+ const parts = [];
3718
+ if (userPrompt) parts.push(userPrompt);
3719
+ if (projectContext) parts.push(`# Project Context
3720
+
3721
+ ${projectContext}`);
3722
+ if (gitContextStr) parts.push(gitContextStr);
3723
+ return parts.length > 0 ? parts.join("\n\n---\n\n") : void 0;
3724
+ }
3725
+ /** 每次请求时动态生成含当前日期时间的 system prompt。 */
3726
+ buildCurrentSystemPrompt() {
3727
+ const now = /* @__PURE__ */ new Date();
3728
+ const WEEKDAYS = ["\u661F\u671F\u65E5", "\u661F\u671F\u4E00", "\u661F\u671F\u4E8C", "\u661F\u671F\u4E09", "\u661F\u671F\u56DB", "\u661F\u671F\u4E94", "\u661F\u671F\u516D"];
3729
+ const pad = (n) => String(n).padStart(2, "0");
3730
+ const dateStr = `${now.getFullYear()}\u5E74${pad(now.getMonth() + 1)}\u6708${pad(now.getDate())}\u65E5 ${WEEKDAYS[now.getDay()]}`;
3731
+ const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
3732
+ const dateTimeInfo = `\u5F53\u524D\u65E5\u671F\u65F6\u95F4\uFF1A${dateStr} ${timeStr}`;
3733
+ if (this.activeSystemPrompt) {
3734
+ return `${dateTimeInfo}
3735
+
3736
+ ---
3737
+
3738
+ ${this.activeSystemPrompt}`;
3739
+ }
3740
+ return dateTimeInfo;
3741
+ }
3742
+ refreshPrompt() {
3743
+ const promptStr = chalk5.green(`[${this.currentProvider}]`) + chalk5.white(" > ");
3744
+ this.rl.setPrompt(promptStr);
3745
+ }
3746
+ showPrompt() {
3747
+ this.refreshPrompt();
3748
+ const rlDbg = this.rl;
3749
+ rlDbg.prevRows = 0;
3750
+ if (rlDbg.line) {
3751
+ process.stderr.write(`[DBG showPrompt] line=${JSON.stringify(rlDbg.line)}
3752
+ `);
3753
+ }
3754
+ this.rl.prompt();
3755
+ }
3756
+ async start() {
3757
+ this.running = true;
3758
+ this.sessions.createSession(this.currentProvider, this.currentModel);
3759
+ this.events.emit("session.start", { sessionId: this.sessions.current.id });
3760
+ const ctx = this.loadProjectContext();
3761
+ this.contextFilePath = ctx?.filePath ?? null;
3762
+ const gitCtx = getGitContext();
3763
+ this.gitBranch = gitCtx?.branch ?? null;
3764
+ const gitContextStr = gitCtx ? formatGitContextForPrompt(gitCtx) : null;
3765
+ this.activeSystemPrompt = this.buildSystemPromptWithContext(
3766
+ ctx?.content ?? null,
3767
+ gitContextStr
3768
+ );
3769
+ const welcomeProvider = this.providers.get(this.currentProvider);
3770
+ const welcomeModelInfo = welcomeProvider?.info.models.find((m) => m.id === this.currentModel);
3771
+ this.renderer.printWelcome(this.currentProvider, this.currentModel, welcomeModelInfo?.contextWindow);
3772
+ if (ctx) {
3773
+ process.stdout.write(
3774
+ chalk5.dim(` \u{1F4C4} Project context loaded: ${ctx.filePath} (${ctx.content.length} chars)
3775
+ `)
3776
+ );
3777
+ }
3778
+ if (gitCtx) {
3779
+ const statusSummary = gitCtx.stagedFiles.length + gitCtx.changedFiles.length > 0 ? chalk5.yellow(` (${gitCtx.stagedFiles.length} staged, ${gitCtx.changedFiles.length} modified)`) : chalk5.dim(" (clean)");
3780
+ process.stdout.write(
3781
+ chalk5.dim(` \u{1F500} Git branch: `) + chalk5.cyan(gitCtx.branch) + statusSummary + "\n"
3782
+ );
3783
+ }
3784
+ this.rl.on("SIGINT", () => {
3785
+ if (this.toolExecutor.confirming) {
3786
+ this.toolExecutor.cancelConfirm();
3787
+ return;
3788
+ }
3789
+ this.handleExit();
3790
+ });
3791
+ this.showPrompt();
3792
+ await new Promise((resolve4) => {
3793
+ let processing = false;
3794
+ this.rl.on("line", async (line) => {
3795
+ if (this.toolExecutor.confirming) return;
3796
+ if (this.selecting) return;
3797
+ const input = line.trim();
3798
+ if (!input) {
3799
+ this.showPrompt();
3800
+ return;
3801
+ }
3802
+ processing = true;
3803
+ this.rl.pause();
3804
+ const rlAny = this.rl;
3805
+ const savedOutput = rlAny.output;
3806
+ rlAny.output = null;
3807
+ try {
3808
+ if (input.startsWith("/")) {
3809
+ await this.handleCommand(input);
3810
+ } else {
3811
+ await this.handleChat(input);
3812
+ }
3813
+ } catch (err) {
3814
+ this.renderer.renderError(err);
3815
+ } finally {
3816
+ processing = false;
3817
+ }
3818
+ if (this.running) {
3819
+ rlAny.output = savedOutput;
3820
+ const rlInternal = this.rl;
3821
+ rlInternal.line = "";
3822
+ rlInternal.cursor = 0;
3823
+ rlInternal.paused = false;
3824
+ process.stdin.resume();
3825
+ this.showPrompt();
3826
+ } else {
3827
+ resolve4();
3828
+ }
3829
+ });
3830
+ this.rl.on("close", () => {
3831
+ if (!processing) {
3832
+ resolve4();
3833
+ }
3834
+ });
3835
+ });
3836
+ }
3837
+ async handleChat(userInput) {
3838
+ const session = this.sessions.current;
3839
+ const { parts, hasImage, refs } = parseAtReferences(userInput, process.cwd());
3840
+ for (const ref of refs) {
3841
+ if (ref.type === "notfound") {
3842
+ process.stdout.write(chalk5.yellow(` \u26A0 File not found: ${ref.path}
3843
+ `));
3844
+ } else if (ref.type === "image") {
3845
+ process.stdout.write(chalk5.dim(` \u{1F4CE} Image: ${ref.path}
3846
+ `));
3847
+ } else {
3848
+ process.stdout.write(chalk5.dim(` \u{1F4C4} File: ${ref.path}
3849
+ `));
3850
+ }
3851
+ }
3852
+ const messageContent = parts.length > 0 ? parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts : userInput;
3853
+ if (hasImage) {
3854
+ const visionHint = this.getVisionModelHint();
3855
+ if (visionHint) {
3856
+ process.stdout.write(
3857
+ chalk5.yellow(` \u2716 Vision not supported \u2013 ${visionHint}
3858
+ `)
3859
+ );
3860
+ return;
3861
+ }
3862
+ process.stdout.write(
3863
+ chalk5.dim(` \u{1F5BC} Vision request \u2013 sending image to ${this.currentProvider}
3864
+ `)
3865
+ );
3866
+ }
3867
+ session.addMessage({
3868
+ role: "user",
3869
+ content: messageContent,
3870
+ timestamp: /* @__PURE__ */ new Date()
3871
+ });
3872
+ this.events.emit("message.before", { input: userInput });
3873
+ try {
3874
+ const provider = this.providers.get(this.currentProvider);
3875
+ const supportsTools = "chatWithTools" in provider;
3876
+ if (supportsTools) {
3877
+ await this.handleChatWithTools(
3878
+ provider,
3879
+ session.messages
3880
+ );
3881
+ } else {
3882
+ await this.handleChatSimple(provider, session.messages);
3883
+ }
3884
+ if (this.config.get("session").autoSave) {
3885
+ await this.sessions.save();
3886
+ }
3887
+ } catch (err) {
3888
+ this.renderer.renderError(err);
3889
+ }
3890
+ }
3891
+ /**
3892
+ * 当当前模型不支持视觉输入时,返回提示字符串(告知用户切换哪个模型)。
3893
+ * 若当前模型已支持(model ID 含 vision),返回 null(不需要提示)。
3894
+ */
3895
+ getVisionModelHint() {
3896
+ const model = this.currentModel;
3897
+ const provider = this.currentProvider;
3898
+ if (model.includes("vision") || model.includes("vl") || model.endsWith(".6v") || // glm-4.6v
3899
+ model.endsWith("-v")) return null;
3900
+ if (provider === "kimi") {
3901
+ if (model.startsWith("moonshot-v1-")) {
3902
+ const visionModel = model + "-vision-preview";
3903
+ return `model "${model}" does not support images. Use /model ${visionModel}`;
3904
+ }
3905
+ return null;
3906
+ }
3907
+ if (provider === "zhipu") {
3908
+ return `model "${model}" does not support images. Use /model glm-4.6v`;
3909
+ }
3910
+ return `model "${model}" may not support image input`;
3911
+ }
3912
+ /**
3913
+ * 从 config.modelParams 读取当前模型的推理参数(temperature / maxTokens / timeout / thinking)。
3914
+ * 用户可在 config.json 的 modelParams 字段按模型 ID 配置,例如:
3915
+ * "modelParams": { "glm-5": { "temperature": 1.0, "maxTokens": 65536, "thinking": true } }
3916
+ */
3917
+ getModelParams() {
3918
+ const allParams = this.config.get("modelParams");
3919
+ return allParams[this.currentModel] ?? {};
3920
+ }
3921
+ async handleChatSimple(provider, messages) {
3922
+ const session = this.sessions.current;
3923
+ const useStreaming = this.config.get("ui").streaming;
3924
+ const modelParams = this.getModelParams();
3925
+ if (useStreaming) {
3926
+ const stream = provider.chatStream({
3927
+ messages,
3928
+ model: this.currentModel,
3929
+ systemPrompt: this.buildCurrentSystemPrompt(),
3930
+ stream: true,
3931
+ temperature: modelParams.temperature,
3932
+ maxTokens: modelParams.maxTokens,
3933
+ timeout: modelParams.timeout,
3934
+ thinking: modelParams.thinking
3935
+ });
3936
+ const { content, usage } = await this.renderer.renderStream(stream);
3937
+ lastResponseStore.content = content;
3938
+ session.addMessage({ role: "assistant", content, timestamp: /* @__PURE__ */ new Date() });
3939
+ this.events.emit("message.after", { content });
3940
+ if (usage) {
3941
+ this.sessionTokenUsage.inputTokens += usage.inputTokens;
3942
+ this.sessionTokenUsage.outputTokens += usage.outputTokens;
3943
+ this.renderer.renderUsage(usage, this.sessionTokenUsage);
3944
+ }
3945
+ } else {
3946
+ const spinner = this.renderer.showSpinner("Thinking...");
3947
+ try {
3948
+ const response = await provider.chat({
3949
+ messages,
3950
+ model: this.currentModel,
3951
+ systemPrompt: this.buildCurrentSystemPrompt(),
3952
+ stream: false,
3953
+ temperature: modelParams.temperature,
3954
+ maxTokens: modelParams.maxTokens,
3955
+ timeout: modelParams.timeout,
3956
+ thinking: modelParams.thinking
3957
+ });
3958
+ spinner.stop();
3959
+ this.renderer.renderResponse(response.content);
3960
+ lastResponseStore.content = response.content;
3961
+ session.addMessage({ role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() });
3962
+ this.events.emit("message.after", { content: response.content });
3963
+ if (response.usage) {
3964
+ this.sessionTokenUsage.inputTokens += response.usage.inputTokens;
3965
+ this.sessionTokenUsage.outputTokens += response.usage.outputTokens;
3966
+ this.renderer.renderUsage(response.usage, this.sessionTokenUsage);
3967
+ }
3968
+ } finally {
3969
+ spinner.stop();
3970
+ }
3971
+ }
3972
+ }
3973
+ async handleChatWithTools(provider, messages) {
3974
+ const session = this.sessions.current;
3975
+ const toolDefs = this.toolRegistry.getDefinitions();
3976
+ const apiMessages = [...messages];
3977
+ const extraMessages = [];
3978
+ const systemPrompt = this.buildCurrentSystemPrompt();
3979
+ const modelParams = this.getModelParams();
3980
+ const useStreaming = this.config.get("ui").streaming;
3981
+ const spinner = this.renderer.showSpinner("Thinking...");
3982
+ const roundUsage = { inputTokens: 0, outputTokens: 0 };
3983
+ try {
3984
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
3985
+ this.toolExecutor.setRoundInfo(round + 1, MAX_TOOL_ROUNDS);
3986
+ const result = await provider.chatWithTools(
3987
+ {
3988
+ messages: apiMessages,
3989
+ model: this.currentModel,
3990
+ systemPrompt,
3991
+ stream: false,
3992
+ temperature: modelParams.temperature,
3993
+ maxTokens: modelParams.maxTokens,
3994
+ timeout: modelParams.timeout,
3995
+ thinking: modelParams.thinking,
3996
+ ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
3997
+ },
3998
+ toolDefs
3999
+ );
4000
+ if (result.usage) {
4001
+ roundUsage.inputTokens += result.usage.inputTokens;
4002
+ roundUsage.outputTokens += result.usage.outputTokens;
4003
+ }
4004
+ if ("content" in result) {
4005
+ spinner.stop();
4006
+ let finalContent;
4007
+ if (useStreaming) {
4008
+ const stream = provider.chatStream({
4009
+ messages: apiMessages,
4010
+ model: this.currentModel,
4011
+ systemPrompt,
4012
+ stream: true,
4013
+ temperature: modelParams.temperature,
4014
+ maxTokens: modelParams.maxTokens,
4015
+ timeout: modelParams.timeout,
4016
+ thinking: modelParams.thinking,
4017
+ ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
4018
+ });
4019
+ const { content: streamContent, usage: streamUsage } = await this.renderer.renderStream(stream);
4020
+ finalContent = streamContent;
4021
+ if (streamUsage) {
4022
+ roundUsage.inputTokens += streamUsage.inputTokens;
4023
+ roundUsage.outputTokens += streamUsage.outputTokens;
4024
+ }
4025
+ } else {
4026
+ this.renderer.renderResponse(result.content);
4027
+ finalContent = result.content;
4028
+ }
4029
+ lastResponseStore.content = finalContent;
4030
+ session.addMessage({
4031
+ role: "assistant",
4032
+ content: finalContent,
4033
+ timestamp: /* @__PURE__ */ new Date()
4034
+ });
4035
+ this.events.emit("message.after", { content: finalContent });
4036
+ if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
4037
+ this.sessionTokenUsage.inputTokens += roundUsage.inputTokens;
4038
+ this.sessionTokenUsage.outputTokens += roundUsage.outputTokens;
4039
+ this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
4040
+ }
4041
+ return;
4042
+ }
4043
+ spinner.stop();
4044
+ const saveLastResponseCall = result.toolCalls.find((tc) => tc.name === "save_last_response");
4045
+ if (saveLastResponseCall) {
4046
+ const saveToFile = String(saveLastResponseCall.arguments["path"] ?? "");
4047
+ if (!saveToFile) {
4048
+ } else {
4049
+ const genStream = provider.chatStream({
4050
+ messages: apiMessages,
4051
+ model: this.currentModel,
4052
+ systemPrompt,
4053
+ stream: true,
4054
+ temperature: modelParams.temperature,
4055
+ maxTokens: modelParams.maxTokens,
4056
+ timeout: modelParams.timeout,
4057
+ thinking: modelParams.thinking,
4058
+ ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
4059
+ });
4060
+ const { content: genContent, usage: genUsage } = await this.renderer.renderStream(
4061
+ genStream,
4062
+ { saveToFile }
4063
+ );
4064
+ lastResponseStore.content = genContent;
4065
+ if (genUsage) {
4066
+ roundUsage.inputTokens += genUsage.inputTokens;
4067
+ roundUsage.outputTokens += genUsage.outputTokens;
4068
+ }
4069
+ session.addMessage({ role: "assistant", content: genContent, timestamp: /* @__PURE__ */ new Date() });
4070
+ this.events.emit("message.after", { content: genContent });
4071
+ const lines = genContent.split("\n").length;
4072
+ const bytes = Buffer.byteLength(genContent, "utf-8");
4073
+ const syntheticResults = result.toolCalls.map((tc) => ({
4074
+ callId: tc.id,
4075
+ content: tc.name === "save_last_response" ? `File saved: ${saveToFile} (${lines} lines, ${bytes} bytes)` : `[skipped: file already saved by tee streaming]`,
4076
+ isError: false
4077
+ }));
4078
+ const newMsgs2 = provider.buildToolResultMessages(result.toolCalls, syntheticResults);
4079
+ extraMessages.push(...newMsgs2);
4080
+ if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
4081
+ this.sessionTokenUsage.inputTokens += roundUsage.inputTokens;
4082
+ this.sessionTokenUsage.outputTokens += roundUsage.outputTokens;
4083
+ this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
4084
+ }
4085
+ return;
4086
+ }
4087
+ }
4088
+ const toolResults = await this.toolExecutor.executeAll(result.toolCalls);
4089
+ const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults);
4090
+ extraMessages.push(...newMsgs);
4091
+ const nextRound = round + 2;
4092
+ spinner.start(
4093
+ nextRound <= MAX_TOOL_ROUNDS ? `Thinking... (round ${nextRound}/${MAX_TOOL_ROUNDS})` : "Thinking..."
4094
+ );
4095
+ }
4096
+ spinner.stop();
4097
+ this.renderer.renderError(
4098
+ `Reached maximum tool call rounds (${MAX_TOOL_ROUNDS}). Stopping.`
4099
+ );
4100
+ } finally {
4101
+ spinner.stop();
4102
+ }
4103
+ }
4104
+ async handleCommand(input) {
4105
+ const parts = input.slice(1).split(" ");
4106
+ const cmdName = parts[0].toLowerCase();
4107
+ const args = parts.slice(1);
4108
+ const cmd = this.commands.get(cmdName);
4109
+ if (!cmd) {
4110
+ this.renderer.renderError(
4111
+ `Unknown command: /${cmdName}. Type /help for available commands.`
4112
+ );
4113
+ return;
4114
+ }
4115
+ const ctx = {
4116
+ providers: this.providers,
4117
+ sessions: this.sessions,
4118
+ config: this.config,
4119
+ renderer: this.renderer,
4120
+ tools: this.toolRegistry,
4121
+ // 交互式列表选择器:命令执行期间 rl 已 pause + output=null,
4122
+ // selectFromList 直接用 process.stdout.write,完全绕过 readline。
4123
+ // rl.output 保持 null,避免 readline terminal 模式把 line buffer
4124
+ // ("/provider" 等命令字符串) 重绘到 stdout,造成界面污染。
4125
+ select: (prompt, items, initialIndex) => {
4126
+ this.selecting = true;
4127
+ return selectFromList(prompt, items, initialIndex).finally(() => {
4128
+ const rlInternal = this.rl;
4129
+ rlInternal.line = "";
4130
+ rlInternal.cursor = 0;
4131
+ process.stdin.pause();
4132
+ setImmediate(() => {
4133
+ this.selecting = false;
4134
+ });
4135
+ });
4136
+ },
4137
+ setProvider: (id, model) => {
4138
+ this.currentProvider = id;
4139
+ this.currentModel = model ?? this.config.get("defaultModels")[id] ?? this.providers.get(id).info.defaultModel;
4140
+ this.refreshPrompt();
4141
+ this.events.emit("provider.switch", {
4142
+ providerId: id,
4143
+ model: this.currentModel
4144
+ });
4145
+ },
4146
+ getCurrentProvider: () => this.currentProvider,
4147
+ getCurrentModel: () => this.currentModel,
4148
+ getContextFilePath: () => this.contextFilePath,
4149
+ getActiveSystemPrompt: () => this.buildCurrentSystemPrompt(),
4150
+ reloadContext: () => {
4151
+ const projectCtx = this.loadProjectContext();
4152
+ this.contextFilePath = projectCtx?.filePath ?? null;
4153
+ const gitCtx = getGitContext();
4154
+ this.gitBranch = gitCtx?.branch ?? null;
4155
+ const gitContextStr = gitCtx ? formatGitContextForPrompt(gitCtx) : null;
4156
+ this.activeSystemPrompt = this.buildSystemPromptWithContext(
4157
+ projectCtx?.content ?? null,
4158
+ gitContextStr
4159
+ );
4160
+ },
4161
+ getSessionTokenUsage: () => ({ ...this.sessionTokenUsage }),
4162
+ getGitBranch: () => this.gitBranch,
4163
+ exit: () => this.handleExit()
4164
+ };
4165
+ await cmd.execute(args, ctx);
4166
+ }
4167
+ handleExit() {
4168
+ this.running = false;
4169
+ const sessionId = this.sessions.current?.id;
4170
+ if (sessionId) {
4171
+ this.events.emit("session.end", { sessionId });
4172
+ }
4173
+ this.rl.close();
4174
+ console.log(chalk5.gray("\nGoodbye!"));
4175
+ process.exit(0);
4176
+ }
4177
+ };
4178
+
4179
+ // src/repl/setup-wizard.ts
4180
+ import { password, select } from "@inquirer/prompts";
4181
+ import chalk6 from "chalk";
4182
+ var PROVIDERS = [
4183
+ { value: "claude", name: "Claude (Anthropic)" },
4184
+ { value: "gemini", name: "Gemini (Google)" },
4185
+ { value: "deepseek", name: "DeepSeek" },
4186
+ { value: "zhipu", name: "\u667A\u8C31\u6E05\u8A00 (GLM)" },
4187
+ { value: "kimi", name: "Kimi (Moonshot AI)" }
4188
+ ];
4189
+ function maskKey(key) {
4190
+ if (key.length <= 10) return "****";
4191
+ return key.slice(0, 6) + "****" + key.slice(-4);
4192
+ }
4193
+ var SetupWizard = class {
4194
+ constructor(config) {
4195
+ this.config = config;
4196
+ }
4197
+ async runFirstRun() {
4198
+ console.log(chalk6.bold.cyan("\nWelcome to ai-cli!\n"));
4199
+ console.log("Let's set up your first AI provider.\n");
4200
+ try {
4201
+ const providerId = await select({
4202
+ message: "Which AI provider do you want to set up first?",
4203
+ choices: PROVIDERS
4204
+ });
4205
+ await this.setupProvider(providerId);
4206
+ this.config.set("defaultProvider", providerId);
4207
+ this.config.save();
4208
+ console.log(chalk6.green("\nSetup complete! Starting ai-cli...\n"));
4209
+ return true;
4210
+ } catch {
4211
+ return false;
4212
+ }
4213
+ }
4214
+ async runFull() {
4215
+ console.log(chalk6.bold.cyan("\nai-cli Configuration\n"));
4216
+ let running = true;
4217
+ while (running) {
4218
+ const action = await select({
4219
+ message: "What would you like to configure?",
4220
+ choices: [
4221
+ { value: "apikey", name: "Manage API key for a provider" },
4222
+ { value: "default", name: "Change default provider" },
4223
+ { value: "done", name: "Done" }
4224
+ ]
4225
+ });
4226
+ if (action === "apikey") {
4227
+ const choicesWithStatus = PROVIDERS.map((p) => {
4228
+ const existingKey = this.config.getApiKey(p.value);
4229
+ const status = existingKey ? chalk6.green(`[${maskKey(existingKey)}]`) : chalk6.gray("[\u672A\u914D\u7F6E]");
4230
+ return { value: p.value, name: `${p.name} ${status}` };
4231
+ });
4232
+ const providerId = await select({
4233
+ message: "Select provider:",
4234
+ choices: choicesWithStatus
4235
+ });
4236
+ await this.setupProvider(providerId);
4237
+ } else if (action === "default") {
4238
+ const providerId = await select({
4239
+ message: "Select default provider:",
4240
+ choices: PROVIDERS
4241
+ });
4242
+ this.config.set("defaultProvider", providerId);
4243
+ this.config.save();
4244
+ console.log(chalk6.green(`Default provider set to: ${providerId}
4245
+ `));
4246
+ } else {
4247
+ running = false;
4248
+ }
4249
+ }
4250
+ }
4251
+ async setupProvider(providerId) {
4252
+ const provider = PROVIDERS.find((p) => p.value === providerId);
4253
+ const displayName = provider?.name ?? providerId;
4254
+ const existingKey = this.config.getApiKey(providerId);
4255
+ console.log(`
4256
+ Managing ${displayName} API Key`);
4257
+ if (existingKey) {
4258
+ console.log(chalk6.gray(` Current: ${maskKey(existingKey)}`));
4259
+ } else {
4260
+ console.log(chalk6.gray(" Current: (\u672A\u914D\u7F6E)"));
4261
+ }
4262
+ const choices = existingKey ? [
4263
+ { value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep current key)" },
4264
+ { value: "show", name: "\u663E\u793A\u539F\u59CB Key (show full key)" },
4265
+ { value: "change", name: "\u4FEE\u6539 Key (update key)" }
4266
+ ] : [{ value: "change", name: "\u8F93\u5165 Key (enter key)" }, { value: "skip", name: "\u8DF3\u8FC7 (skip)" }];
4267
+ const action = await select({
4268
+ message: "Action:",
4269
+ choices
4270
+ });
4271
+ if (action === "show" && existingKey) {
4272
+ console.log(chalk6.yellow(`
4273
+ \u5B8C\u6574 Key: ${existingKey}
4274
+ `));
4275
+ const updateAfterShow = await select({
4276
+ message: "Would you like to update this key?",
4277
+ choices: [
4278
+ { value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep)" },
4279
+ { value: "change", name: "\u4FEE\u6539 (update)" }
4280
+ ]
4281
+ });
4282
+ if (updateAfterShow !== "change") return;
4283
+ } else if (action === "keep" || action === "skip") {
4284
+ return;
4285
+ }
4286
+ const newKey = await password({
4287
+ message: `Enter ${displayName} API key:`,
4288
+ validate: (val) => val.length > 0 || "API key cannot be empty"
4289
+ });
4290
+ this.config.setApiKey(providerId, newKey);
4291
+ console.log(chalk6.green(`API key saved for ${displayName}: ${maskKey(newKey)}
4292
+ `));
4293
+ }
4294
+ };
4295
+
4296
+ // src/core/event-bus.ts
4297
+ import { EventEmitter } from "events";
4298
+ var EventBus = class extends EventEmitter {
4299
+ emit(event, data) {
4300
+ return super.emit(event, data);
4301
+ }
4302
+ on(event, listener) {
4303
+ return super.on(event, listener);
4304
+ }
4305
+ off(event, listener) {
4306
+ return super.off(event, listener);
4307
+ }
4308
+ once(event, listener) {
4309
+ return super.once(event, listener);
4310
+ }
4311
+ };
4312
+
4313
+ // src/index.ts
4314
+ process.on("uncaughtException", (err) => {
4315
+ process.stderr.write(`
4316
+ [Fatal] Uncaught exception: ${err.message}
4317
+ ${err.stack ?? ""}
4318
+ `);
4319
+ process.exit(1);
4320
+ });
4321
+ process.on("unhandledRejection", (reason) => {
4322
+ const msg = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
4323
+ process.stderr.write(`
4324
+ [Fatal] Unhandled rejection: ${msg}
4325
+ `);
4326
+ process.exit(1);
4327
+ });
4328
+ program.name("ai-cli").description("Cross-platform REPL-style AI CLI with multi-provider support").version(VERSION);
4329
+ program.option("-p, --provider <name>", "AI provider to use (claude/gemini/deepseek/zhipu/kimi or any custom provider)").option("-m, --model <name>", "Model to use").option("--no-stream", "Disable streaming output").action(async (options) => {
4330
+ await startRepl(options);
4331
+ });
4332
+ program.command("config").description("Configure API keys and preferences").action(async () => {
4333
+ const config = new ConfigManager();
4334
+ const wizard = new SetupWizard(config);
4335
+ await wizard.runFull();
4336
+ console.log("Configuration saved.");
4337
+ });
4338
+ program.command("providers").description("List all AI providers and their configuration status").action(async () => {
4339
+ const config = new ConfigManager();
4340
+ const registry = new ProviderRegistry();
4341
+ await registry.initialize(
4342
+ (id) => config.getApiKey(id),
4343
+ (id) => ({
4344
+ baseUrl: config.get("customBaseUrls")[id],
4345
+ timeout: config.get("timeouts")[id]
4346
+ }),
4347
+ config.get("customProviders")
4348
+ );
4349
+ const all = registry.listAll();
4350
+ console.log("\nAvailable providers:\n");
4351
+ console.log(
4352
+ "Status".padEnd(8) + "Type".padEnd(10) + "ID".padEnd(16) + "Display Name".padEnd(26) + "Default Model"
4353
+ );
4354
+ console.log("-".repeat(80));
4355
+ for (const p of all) {
4356
+ const status = p.configured ? "\u2713" : "\u2717";
4357
+ const color = p.configured ? "\x1B[32m" : "\x1B[31m";
4358
+ const tag = p.isCustom ? "\x1B[36mcustom\x1B[0m " : "built-in ";
4359
+ console.log(
4360
+ `${color}${status}\x1B[0m ` + tag + p.id.padEnd(16) + p.displayName.padEnd(26) + p.defaultModel
4361
+ );
4362
+ }
4363
+ console.log();
4364
+ });
4365
+ program.command("sessions").description("List recent conversation sessions").action(async () => {
4366
+ const config = new ConfigManager();
4367
+ const sessions = new SessionManager(config);
4368
+ const list = sessions.listSessions();
4369
+ if (list.length === 0) {
4370
+ console.log("\nNo saved sessions.\n");
4371
+ return;
4372
+ }
4373
+ console.log("\nRecent sessions:\n");
4374
+ console.log(
4375
+ "ID".padEnd(10) + "Provider".padEnd(12) + "Model".padEnd(30) + "Msgs".padEnd(6) + "Date".padEnd(12) + "Title"
4376
+ );
4377
+ console.log("-".repeat(80));
4378
+ for (const s of list) {
4379
+ console.log(
4380
+ s.id.slice(0, 8).padEnd(10) + s.provider.padEnd(12) + s.model.padEnd(30) + String(s.messageCount).padEnd(6) + s.updated.toLocaleDateString().padEnd(12) + (s.title ?? "(untitled)")
4381
+ );
4382
+ }
4383
+ console.log();
4384
+ });
4385
+ program.parse();
4386
+ async function startRepl(options) {
4387
+ const config = new ConfigManager();
4388
+ if (config.isFirstRun()) {
4389
+ const wizard = new SetupWizard(config);
4390
+ const configured = await wizard.runFirstRun();
4391
+ if (!configured) {
4392
+ console.log('\nSetup cancelled. Run "ai-cli config" to configure later.');
4393
+ process.exit(0);
4394
+ }
4395
+ }
4396
+ if (options.provider) {
4397
+ config.set("defaultProvider", options.provider);
4398
+ }
4399
+ if (options.stream === false) {
4400
+ const ui = config.get("ui");
4401
+ config.set("ui", { ...ui, streaming: false });
4402
+ }
4403
+ const events = new EventBus();
4404
+ const providers = new ProviderRegistry();
4405
+ await providers.initialize(
4406
+ (id) => config.getApiKey(id),
4407
+ (id) => ({
4408
+ baseUrl: config.get("customBaseUrls")[id],
4409
+ timeout: config.get("timeouts")[id]
4410
+ }),
4411
+ config.get("customProviders")
4412
+ );
4413
+ if (providers.listAvailable().length === 0) {
4414
+ console.error(
4415
+ "\nNo providers configured. Run: ai-cli config\n"
4416
+ );
4417
+ process.exit(1);
4418
+ }
4419
+ if (options.provider && !providers.has(options.provider)) {
4420
+ console.error(
4421
+ `
4422
+ Provider '${options.provider}' is not configured. Run: ai-cli config
4423
+ `
4424
+ );
4425
+ process.exit(1);
4426
+ }
4427
+ const sessions = new SessionManager(config);
4428
+ const repl = new Repl(providers, sessions, config, events);
4429
+ if (options.model) {
4430
+ const defaultModels = config.get("defaultModels");
4431
+ const provider = options.provider ?? config.getDefaultProvider();
4432
+ config.set("defaultModels", { ...defaultModels, [provider]: options.model });
4433
+ }
4434
+ process.on("SIGTERM", async () => {
4435
+ await sessions.save();
4436
+ process.exit(0);
4437
+ });
4438
+ await repl.start();
4439
+ }