@saber2pr/ai-agent 0.0.54 → 0.0.55

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.
@@ -0,0 +1,18 @@
1
+ import { AgentGraphLLMResponse, AgentGraphModel, StreamChunkCallback } from '../model/AgentGraphModel';
2
+ import { BaseMessage } from '@langchain/core/messages';
3
+ import { CreateAgentOptions } from '../agent/createAgent';
4
+ export declare class LLMModel extends AgentGraphModel {
5
+ private chatId;
6
+ private options;
7
+ constructor(options: CreateAgentOptions);
8
+ resetChat(): void;
9
+ callApi(prompt: string): Promise<AgentGraphLLMResponse>;
10
+ /**
11
+ * 流式调用 API:发送 stream: true,自动适配多种响应格式(SSE / NDJSON / 普通 JSON)。
12
+ */
13
+ /**
14
+ * 流式调用 API:发送 stream: true
15
+ * 适配 SSE 格式,解析内容增量、思考过程以及最终的 Token 统计
16
+ */
17
+ callApiStream(prompt: string, lastMsg: BaseMessage, onChunk: StreamChunkCallback): Promise<AgentGraphLLMResponse>;
18
+ }
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LLMModel = void 0;
4
+ const AgentGraphModel_1 = require("../model/AgentGraphModel");
5
+ class LLMModel extends AgentGraphModel_1.AgentGraphModel {
6
+ chatId;
7
+ options;
8
+ constructor(options) {
9
+ super();
10
+ this.chatId = '';
11
+ this.options = options;
12
+ }
13
+ resetChat() {
14
+ this.chatId = '';
15
+ }
16
+ async callApi(prompt) {
17
+ const response = await fetch(this.options.apiUrl, {
18
+ method: 'POST',
19
+ body: JSON.stringify({
20
+ query: prompt,
21
+ chatId: this.chatId,
22
+ stream: false,
23
+ }),
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ Authorization: `Bearer ${this.options.apiKey}`,
27
+ },
28
+ });
29
+ if (!response.ok) {
30
+ throw new Error(`LLM API 响应异常: ${response.status} ${response.statusText}`);
31
+ }
32
+ const data = await response.json();
33
+ this.chatId = data.chat_id || data.chatId;
34
+ return {
35
+ text: data.text || '',
36
+ reasoning: data.reason || data.thought || '', // 适配后端可能的思考字段名
37
+ token: data.token, // ✅ 确保这里取到了 API 返回的 token
38
+ duration: data.duration, // ✅ 确保这里取到了 API 返回的 duration
39
+ };
40
+ }
41
+ /**
42
+ * 流式调用 API:发送 stream: true,自动适配多种响应格式(SSE / NDJSON / 普通 JSON)。
43
+ */
44
+ /**
45
+ * 流式调用 API:发送 stream: true
46
+ * 适配 SSE 格式,解析内容增量、思考过程以及最终的 Token 统计
47
+ */
48
+ async callApiStream(prompt, lastMsg, onChunk) {
49
+ const files = lastMsg?.additional_kwargs?.files || [];
50
+ const response = await fetch(this.options.apiUrl, {
51
+ method: 'POST',
52
+ body: JSON.stringify({
53
+ query: prompt,
54
+ chatId: this.chatId,
55
+ stream: true,
56
+ files
57
+ }),
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ Authorization: `Bearer ${this.options.apiKey}`,
61
+ },
62
+ });
63
+ if (!response.ok) {
64
+ throw new Error(`LLM API 响应异常: ${response.status} ${response.statusText}`);
65
+ }
66
+ const contentType = response.headers.get('content-type') || '';
67
+ // ✅ 情况1: 兜底处理非流式响应
68
+ if (contentType.includes('application/json')) {
69
+ const data = await response.json();
70
+ this.chatId = data.chat_id || data.chatId;
71
+ const text = data.text || data.content || '';
72
+ if (text)
73
+ onChunk(text);
74
+ return {
75
+ text,
76
+ reasoning: data.reason || data.thought || data.reasoning_content || '',
77
+ token: data.total_tokens || data.token || 0,
78
+ duration: data.duration || 0,
79
+ };
80
+ }
81
+ // ✅ 情况2: 流式响应处理
82
+ const reader = response.body?.getReader();
83
+ if (!reader) {
84
+ throw new Error('响应体不支持流式读取');
85
+ }
86
+ const decoder = new TextDecoder();
87
+ let fullText = '';
88
+ let reasoning = '';
89
+ let token = 0;
90
+ let duration = 0;
91
+ let buffer = '';
92
+ let currentEvent = ''; // 记录当前的 SSE 事件类型
93
+ /**
94
+ * 内部解析函数:处理每一行 SSE 数据
95
+ */
96
+ const processLine = (line) => {
97
+ const trimmed = line.trim();
98
+ if (!trimmed)
99
+ return;
100
+ // 1. 识别事件类型 (如 event: answer)
101
+ if (trimmed.startsWith('event:')) {
102
+ currentEvent = trimmed.slice(6).trim();
103
+ return;
104
+ }
105
+ // 2. 提取 Data 字符串
106
+ let dataStr = trimmed;
107
+ if (trimmed.startsWith('data:')) {
108
+ dataStr = trimmed.slice(5).trim();
109
+ }
110
+ // 跳过结束标志
111
+ if (dataStr === '[DONE]')
112
+ return;
113
+ try {
114
+ const data = JSON.parse(dataStr);
115
+ // A. 提取对话文本 (仅在 answer 事件中)
116
+ if (currentEvent === 'answer' || data.event === 'answer') {
117
+ const chunkText = data.delta?.content || data.text || '';
118
+ if (chunkText) {
119
+ onChunk(chunkText);
120
+ fullText += chunkText;
121
+ }
122
+ // 提取流式思考内容 (如果有)
123
+ const rChunk = data.delta?.reasoning_content || '';
124
+ if (rChunk)
125
+ reasoning += rChunk;
126
+ }
127
+ // B. 提取统计信息 (在 done 或 flowNodeStatus 事件中)
128
+ // 根据你的日志,统计数据可能在 data.data 下或根部
129
+ const nestedData = data.data || data;
130
+ // 关键:优先匹配 total_tokens
131
+ const foundToken = nestedData.total_tokens || nestedData.totalTokens || nestedData.token || 0;
132
+ const foundDuration = nestedData.duration || 0;
133
+ if (foundToken > 0)
134
+ token = foundToken;
135
+ if (foundDuration > 0)
136
+ duration = foundDuration;
137
+ // C. 更新会话 ID
138
+ if (data.chat_id || data.chatId) {
139
+ this.chatId = data.chat_id || data.chatId;
140
+ }
141
+ // D. 提取非流式的完整思考内容
142
+ const fullReasoning = nestedData.reasoning_content || nestedData.reason || nestedData.thought;
143
+ if (fullReasoning && currentEvent !== 'answer') {
144
+ reasoning = fullReasoning;
145
+ }
146
+ }
147
+ catch (e) {
148
+ // 非 JSON 格式行,且当前处于回答状态时,作为纯文本 fallback
149
+ if (currentEvent === 'answer') {
150
+ onChunk(trimmed);
151
+ fullText += trimmed;
152
+ }
153
+ }
154
+ };
155
+ // 主循环:读取流
156
+ while (true) {
157
+ const { done, value } = await reader.read();
158
+ if (done)
159
+ break;
160
+ buffer += decoder.decode(value, { stream: true });
161
+ const lines = buffer.split('\n');
162
+ buffer = lines.pop() || ''; // 留下不完整的一行在下个循环处理
163
+ for (const line of lines) {
164
+ processLine(line);
165
+ }
166
+ }
167
+ // 处理流结束后的残留 buffer
168
+ if (buffer.trim()) {
169
+ processLine(buffer);
170
+ }
171
+ return {
172
+ text: fullText,
173
+ reasoning,
174
+ token,
175
+ duration
176
+ };
177
+ }
178
+ }
179
+ exports.LLMModel = LLMModel;
@@ -0,0 +1,13 @@
1
+ import McpGraphAgent from '../core/agent-graph';
2
+ import { GraphAgentOptions } from '../types/type';
3
+ import { LLMModel } from '../adapters/llm';
4
+ export interface CreateAgentOptions {
5
+ apiKey: string;
6
+ apiUrl: string;
7
+ targetDir?: string;
8
+ /** 是否启用流式输出,默认 false */
9
+ stream?: boolean;
10
+ config?: GraphAgentOptions;
11
+ }
12
+ export declare const createAgent: (options: CreateAgentOptions) => McpGraphAgent<LLMModel>;
13
+ export type Agent = ReturnType<typeof createAgent>;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createAgent = void 0;
7
+ const agent_graph_1 = __importDefault(require("../core/agent-graph"));
8
+ const llm_1 = require("../adapters/llm");
9
+ const createAgent = (options) => {
10
+ const agent = new agent_graph_1.default({
11
+ alwaysSystem: false,
12
+ apiModel: new llm_1.LLMModel(options),
13
+ targetDir: options.targetDir,
14
+ stream: options.stream, // ✅ 将 stream 选项透传到 McpGraphAgent
15
+ ...(options?.config || {}),
16
+ });
17
+ return agent;
18
+ };
19
+ exports.createAgent = createAgent;
@@ -4,8 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- const agent_graph_1 = __importDefault(require("./core/agent-graph"));
8
- const agent = new agent_graph_1.default({
9
- stream: true,
10
- });
7
+ const agent_1 = __importDefault(require("./core/agent"));
8
+ const agent = new agent_1.default();
11
9
  agent.start();
package/lib/cli.js CHANGED
@@ -1,9 +1,104 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
- const agent_1 = __importDefault(require("./core/agent"));
8
- const agent = new agent_1.default();
9
- agent.start();
4
+ const fs_1 = require("fs");
5
+ const config_1 = require("./config/config");
6
+ const readline_1 = require("readline");
7
+ const createAgent_1 = require("./agent/createAgent");
8
+ // 读取配置文件
9
+ function loadConfig() {
10
+ if (!(0, fs_1.existsSync)(config_1.CONFIG_FILE)) {
11
+ return null;
12
+ }
13
+ try {
14
+ const content = (0, fs_1.readFileSync)(config_1.CONFIG_FILE, 'utf-8');
15
+ const config = JSON.parse(content);
16
+ // 验证配置完整性
17
+ if (!config.apiKey || !config.apiUrl) {
18
+ return null;
19
+ }
20
+ return config;
21
+ }
22
+ catch (error) {
23
+ console.error(`读取配置文件失败: ${config_1.CONFIG_FILE}`, error);
24
+ return null;
25
+ }
26
+ }
27
+ // 保存配置文件
28
+ function saveConfig(config) {
29
+ try {
30
+ (0, fs_1.writeFileSync)(config_1.CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
31
+ console.log(`配置已保存到: ${config_1.CONFIG_FILE}`);
32
+ }
33
+ catch (error) {
34
+ console.error(`保存配置文件失败: ${config_1.CONFIG_FILE}`, error);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ // CLI 交互式输入配置
39
+ async function promptConfig() {
40
+ const rl = (0, readline_1.createInterface)({
41
+ input: process.stdin,
42
+ output: process.stdout,
43
+ });
44
+ const question = (query) => {
45
+ return new Promise(resolve => {
46
+ rl.question(query, resolve);
47
+ });
48
+ };
49
+ try {
50
+ console.log('\n欢迎使用 AI Agent CLI!');
51
+ console.log('首次使用需要配置 API 信息。\n');
52
+ const apiUrlInput = await question(`请输入 API URL: `);
53
+ if (!apiUrlInput.trim()) {
54
+ console.error('API URL 不能为空!');
55
+ rl.close();
56
+ process.exit(1);
57
+ }
58
+ const apiKey = await question('请输入 API Key: ');
59
+ if (!apiKey.trim()) {
60
+ console.error('API Key 不能为空!');
61
+ rl.close();
62
+ process.exit(1);
63
+ }
64
+ const apiUrl = apiUrlInput.trim();
65
+ rl.close();
66
+ const config = {
67
+ apiKey: apiKey.trim(),
68
+ apiUrl: apiUrl.trim(),
69
+ };
70
+ return config;
71
+ }
72
+ catch (error) {
73
+ rl.close();
74
+ console.error('输入配置时出错:', error);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ // 主函数
79
+ async function main() {
80
+ let config = loadConfig();
81
+ // 如果配置不存在或不完整,提示用户输入
82
+ if (!config) {
83
+ console.log(`未找到配置文件或配置不完整。${config_1.CONFIG_FILE}`);
84
+ config = await promptConfig();
85
+ saveConfig(config);
86
+ }
87
+ // 创建并启动 agent(默认开启流式输出)
88
+ const agent = (0, createAgent_1.createAgent)({
89
+ apiKey: config.apiKey,
90
+ apiUrl: config.apiUrl,
91
+ stream: true,
92
+ targetDir: process.cwd()
93
+ });
94
+ const model = await agent.ensureInitialized();
95
+ if (process.env.AI_AGENT_CLI_NO_MCP) {
96
+ model.setMcpEnabled(false);
97
+ }
98
+ console.log('欢迎使用 AI Agent CLI!请输入问题,按 Ctrl+C 退出。\n当前目录:', process.cwd());
99
+ await agent.start();
100
+ }
101
+ main().catch(error => {
102
+ console.error('启动失败:', error);
103
+ process.exit(1);
104
+ });
@@ -32,7 +32,7 @@ const AgentState = langgraph_1.Annotation.Root({
32
32
  // ✅ Token 累加器
33
33
  tokenUsage: (0, langgraph_1.Annotation)({
34
34
  reducer: (x, y) => ({
35
- total: ((x === null || x === void 0 ? void 0 : x.total) || 0) + ((y === null || y === void 0 ? void 0 : y.total) || 0),
35
+ total: (x?.total || 0) + (y?.total || 0),
36
36
  }),
37
37
  default: () => ({ total: 0 }),
38
38
  }),
@@ -43,12 +43,22 @@ const AgentState = langgraph_1.Annotation.Root({
43
43
  }),
44
44
  });
45
45
  class McpGraphAgent {
46
+ model;
47
+ toolNode;
48
+ targetDir;
49
+ options;
50
+ checkpointer = new langgraph_1.MemorySaver();
51
+ langchainTools = [];
52
+ stopLoadingFunc = null;
53
+ verbose;
54
+ alwaysSystem;
55
+ recursionLimit;
56
+ apiConfig;
57
+ maxTokens;
58
+ mcpClients = [];
59
+ streamEnabled;
60
+ streamOutputCallback = null;
46
61
  constructor(options = {}) {
47
- this.checkpointer = new langgraph_1.MemorySaver();
48
- this.langchainTools = [];
49
- this.stopLoadingFunc = null;
50
- this.mcpClients = [];
51
- this.streamOutputCallback = null;
52
62
  this.options = options;
53
63
  this.verbose = options.verbose || false;
54
64
  this.alwaysSystem = options.alwaysSystem || true;
@@ -342,7 +352,6 @@ class McpGraphAgent {
342
352
  ask();
343
353
  }
344
354
  renderOutput(output, isStreaming = false) {
345
- var _a, _b, _c;
346
355
  this.stopLoading(); // 停止加载动画
347
356
  const agentNode = output.agent;
348
357
  // ✅ 打印工具执行结果(tools 节点的输出)
@@ -350,9 +359,9 @@ class McpGraphAgent {
350
359
  if (toolsNode && toolsNode.messages) {
351
360
  const toolMessages = Array.isArray(toolsNode.messages) ? toolsNode.messages : [];
352
361
  // 获取最近的 AI 消息以匹配 tool_call_id
353
- const lastAiMsg = (_a = agentNode === null || agentNode === void 0 ? void 0 : agentNode.messages) === null || _a === void 0 ? void 0 : _a[agentNode.messages.length - 1];
362
+ const lastAiMsg = agentNode?.messages?.[agentNode.messages.length - 1];
354
363
  const toolCallMap = new Map();
355
- if (lastAiMsg === null || lastAiMsg === void 0 ? void 0 : lastAiMsg.tool_calls) {
364
+ if (lastAiMsg?.tool_calls) {
356
365
  lastAiMsg.tool_calls.forEach((tc) => {
357
366
  if (tc.id)
358
367
  toolCallMap.set(tc.id, tc.name);
@@ -374,7 +383,7 @@ class McpGraphAgent {
374
383
  const msg = agentNode.messages[agentNode.messages.length - 1];
375
384
  // 1. 打印思考过程(如果有)
376
385
  // 流式模式下思考内容可能已经随流输出,此处仅在非流式模式或有独立 reasoning 字段时打印
377
- const reasoning = (_b = msg.additional_kwargs) === null || _b === void 0 ? void 0 : _b.reasoning;
386
+ const reasoning = msg.additional_kwargs?.reasoning;
378
387
  if (reasoning && !isStreaming) {
379
388
  console.log(`\n🧠 [思考]: ${reasoning}`);
380
389
  }
@@ -391,7 +400,7 @@ class McpGraphAgent {
391
400
  process.stdout.write(`📊 \x1b[2m[实时统计] 消耗: ${token} tokens | 耗时: ${duration}ms\x1b[0m\n`);
392
401
  }
393
402
  // 4. 打印工具调用情况
394
- if ((_c = msg.tool_calls) === null || _c === void 0 ? void 0 : _c.length) {
403
+ if (msg.tool_calls?.length) {
395
404
  msg.tool_calls.forEach(call => {
396
405
  console.log(`🛠️ [调用工具]: ${call.name} 📦 参数: ${JSON.stringify(call.args)}`);
397
406
  });
@@ -406,19 +415,19 @@ class McpGraphAgent {
406
415
  .join('\n')}`
407
416
  : '';
408
417
  // 1. 构建当前的系统提示词模板
409
- const systemPromptTemplate = `你是一个代码专家。工作目录:${this.targetDir}
418
+ const systemPromptTemplate = `You are an expert software engineer. Working directory: ${this.targetDir}.
410
419
 
411
- # 🧠 思考要求 (Mandatory Thinking Process)
412
- 在进行任何输出或调用工具之前,你**必须**先进行深度的逻辑思考。请将你的思考过程包裹在 <think> 标签内。
420
+ # 🧠 Mandatory Thinking Process
421
+ Before providing any output or calling a tool, you **MUST** conduct a deep logical analysis. Wrap your thought process within <think> tags.
413
422
 
414
- # 🛠️ 工具调用规范
415
- 1. Arguments 必须是纯粹的 JSON 对象,严禁将其作为字符串放入引号中。
416
- 2. 严禁对 JSON 内容进行二次转义。
417
- 3. **禁止空操作**:如果你认为任务已完成或不需要调用工具,请不要输出任何 Action 结构。严禁使用 "None""null" 或空字符串作为工具名称。
423
+ # 🛠️ Tool Call Specifications
424
+ 1. **Pure JSON Arguments**: Arguments must be a valid JSON object. NEVER wrap the entire JSON object in a string or quotes.
425
+ 2. **No Double Escaping**: Do not double-escape characters within the JSON.
426
+ 3. **No Idle Operations**: If the task is complete or no tool is needed, DO NOT output any "Action" structure. Never use "None", "null", or empty strings as a tool name.
418
427
 
419
- # 🎯 核心指令
420
- 1. **任务终结判定**:当你已经读取了用户要求的文件、回答了问题或完成了代码编写时,必须立即提供最终回复。
421
- 2. **回复格式**:任务完成时,请以 "Final Answer:" 开头进行总结,此时不再调用任何工具。
428
+ # 🎯 Core Instructions
429
+ 1. **Termination Criterion**: Once you have read the requested files, answered the questions, or completed the code implementation, you must provide the final response immediately.
430
+ 2. **Response Format**: Upon task completion, start your summary with "Final Answer:". No further tool calls should be made after this point.
422
431
 
423
432
  {extraPrompt}`;
424
433
  // 2. 核心逻辑:处理消息上下文
@@ -574,12 +583,11 @@ class McpGraphAgent {
574
583
  }
575
584
  }
576
585
  getRecentToolCalls(messages, limit = 5) {
577
- var _a;
578
586
  const toolCalls = [];
579
587
  // 从后往前遍历消息,收集最近的工具调用
580
588
  for (let i = messages.length - 1; i >= 0 && toolCalls.length < limit; i--) {
581
589
  const msg = messages[i];
582
- if ((_a = msg.tool_calls) === null || _a === void 0 ? void 0 : _a.length) {
590
+ if (msg.tool_calls?.length) {
583
591
  for (const tc of msg.tool_calls) {
584
592
  toolCalls.push({ name: tc.name, args: tc.args });
585
593
  if (toolCalls.length >= limit)
@@ -590,8 +598,7 @@ class McpGraphAgent {
590
598
  return toolCalls;
591
599
  }
592
600
  printFinalSummary(state) {
593
- var _a;
594
- const totalTokens = ((_a = state.tokenUsage) === null || _a === void 0 ? void 0 : _a.total) || 0;
601
+ const totalTokens = state.tokenUsage?.total || 0;
595
602
  const totalMs = state.totalDuration || 0;
596
603
  if (totalTokens > 0 || totalMs > 0) {
597
604
  console.log('\n' + '═'.repeat(50));
package/lib/core/agent.js CHANGED
@@ -49,16 +49,21 @@ const config_1 = require("../config/config");
49
49
  const builtin_1 = require("../tools/builtin");
50
50
  const jsonSchemaToZod_1 = require("../utils/jsonSchemaToZod");
51
51
  class McpAgent {
52
+ openai;
53
+ modelName = '';
54
+ allTools = [];
55
+ messages = [];
56
+ encoder;
57
+ extraTools = [];
58
+ maxTokens;
59
+ apiConfig;
60
+ targetDir;
61
+ mcpClients = [];
52
62
  constructor(options) {
53
- this.modelName = '';
54
- this.allTools = [];
55
- this.messages = [];
56
- this.extraTools = [];
57
- this.mcpClients = [];
58
- this.targetDir = (options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd();
59
- this.extraTools = (options === null || options === void 0 ? void 0 : options.tools) || []; // 接收外部传入的工具
60
- this.maxTokens = (options === null || options === void 0 ? void 0 : options.maxTokens) || 100000; // 默认 100k
61
- this.apiConfig = options === null || options === void 0 ? void 0 : options.apiConfig;
63
+ this.targetDir = options?.targetDir || process.cwd();
64
+ this.extraTools = options?.tools || []; // 接收外部传入的工具
65
+ this.maxTokens = options?.maxTokens || 100000; // 默认 100k
66
+ this.apiConfig = options?.apiConfig;
62
67
  let baseSystemPrompt = `你是一个专业的代码架构师。
63
68
  你的目标是理解并分析用户项目,请务必遵循以下工作流:
64
69
 
@@ -74,7 +79,7 @@ class McpAgent {
74
79
  - 优先查看 Skeleton(骨架),只有需要修复逻辑时才读取完整 Text(全文)。
75
80
  - 始终以中文回答思考过程。`;
76
81
  // 2. 拼接额外指令
77
- if (options === null || options === void 0 ? void 0 : options.extraSystemPrompt) {
82
+ if (options?.extraSystemPrompt) {
78
83
  const extra = typeof options.extraSystemPrompt === 'string'
79
84
  ? options.extraSystemPrompt
80
85
  : JSON.stringify(options.extraSystemPrompt, null, 2);
@@ -152,7 +157,7 @@ class McpAgent {
152
157
  }),
153
158
  ...this.extraTools,
154
159
  ];
155
- if (allTools === null || allTools === void 0 ? void 0 : allTools.length) {
160
+ if (allTools?.length) {
156
161
  this.allTools.push(...allTools);
157
162
  }
158
163
  }
@@ -251,7 +256,6 @@ class McpAgent {
251
256
  });
252
257
  }
253
258
  async processChat(userInput) {
254
- var _a;
255
259
  this.messages.push({ role: 'user', content: userInput });
256
260
  while (true) {
257
261
  // --- 新增:发送请求前先检查并裁剪 ---
@@ -282,7 +286,7 @@ class McpAgent {
282
286
  const { message } = response.choices[0];
283
287
  this.messages.push(message);
284
288
  // 计算本次 AI 回复生成的 Token
285
- const completionTokens = ((_a = response.usage) === null || _a === void 0 ? void 0 : _a.completion_tokens) ||
289
+ const completionTokens = response.usage?.completion_tokens ||
286
290
  (message.content ? this.encoder.encode(message.content).length : 0);
287
291
  console.log(`✨ AI 回复消耗: ${completionTokens} tokens`);
288
292
  if (!message.tool_calls) {
@@ -301,10 +305,10 @@ class McpAgent {
301
305
  console.log(`\n 🛠️ 执行工具: \x1b[36m${call.function.name}\x1b[0m`);
302
306
  console.log(` 📦 传入参数: \x1b[2m${JSON.stringify(args)}\x1b[0m`);
303
307
  let result;
304
- if (tool === null || tool === void 0 ? void 0 : tool._handler) {
308
+ if (tool?._handler) {
305
309
  result = await tool._handler(args);
306
310
  }
307
- else if ((tool === null || tool === void 0 ? void 0 : tool._client) && tool._originalName) {
311
+ else if (tool?._client && tool._originalName) {
308
312
  const mcpRes = await tool._client.callTool({
309
313
  name: tool._originalName,
310
314
  arguments: args,
package/lib/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export { createTool } from './utils/createTool';
4
4
  export * from './model/AgentGraphModel';
5
5
  export { default as McpGraphAgent } from './core/agent-graph';
6
6
  export * from './types/type';
7
+ export { createAgent } from './agent/createAgent';
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.McpGraphAgent = exports.createTool = exports.default = void 0;
20
+ exports.createAgent = exports.McpGraphAgent = exports.createTool = exports.default = void 0;
21
21
  __exportStar(require("./core/agent"), exports);
22
22
  var agent_1 = require("./core/agent");
23
23
  Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(agent_1).default; } });
@@ -27,3 +27,5 @@ __exportStar(require("./model/AgentGraphModel"), exports);
27
27
  var agent_graph_1 = require("./core/agent-graph");
28
28
  Object.defineProperty(exports, "McpGraphAgent", { enumerable: true, get: function () { return __importDefault(agent_graph_1).default; } });
29
29
  __exportStar(require("./types/type"), exports);
30
+ var createAgent_1 = require("./agent/createAgent");
31
+ Object.defineProperty(exports, "createAgent", { enumerable: true, get: function () { return createAgent_1.createAgent; } });
@@ -4,9 +4,12 @@ exports.AgentGraphModel = void 0;
4
4
  const chat_models_1 = require("@langchain/core/language_models/chat_models");
5
5
  const messages_1 = require("@langchain/core/messages");
6
6
  const function_calling_1 = require("@langchain/core/utils/function_calling");
7
- const cleanToolDefinition_1 = require("../utils/cleanToolDefinition");
7
+ const generateToolMarkdown_1 = require("../utils/generateToolMarkdown");
8
8
  const kit_1 = require("../utils/kit");
9
9
  class AgentGraphModel extends chat_models_1.BaseChatModel {
10
+ boundTools;
11
+ mcpEnabled = true;
12
+ mcpTools = [];
10
13
  setMcpTools(tools) {
11
14
  this.mcpTools = tools;
12
15
  }
@@ -21,12 +24,10 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
21
24
  }
22
25
  isMcpTool(tool) {
23
26
  const mcpTools = (0, kit_1.getArray)(this.mcpTools);
24
- return mcpTools.some(t => { var _a, _b; return ((_a = t === null || t === void 0 ? void 0 : t.function) === null || _a === void 0 ? void 0 : _a.name) === ((_b = tool === null || tool === void 0 ? void 0 : tool.function) === null || _b === void 0 ? void 0 : _b.name); });
27
+ return mcpTools.some(t => t?.function?.name === tool?.function?.name);
25
28
  }
26
29
  constructor(fields) {
27
30
  super(fields || {});
28
- this.mcpEnabled = true;
29
- this.mcpTools = [];
30
31
  }
31
32
  bindTools(tools) {
32
33
  this.boundTools = tools.map(t => (0, function_calling_1.convertToOpenAITool)(t));
@@ -175,7 +176,7 @@ class AgentGraphModel extends chat_models_1.BaseChatModel {
175
176
  };
176
177
  const tools = this.getMcpEnabled() ? (0, kit_1.getArray)(this.boundTools) : (0, kit_1.getArray)(this.boundTools).filter(tool => !this.isMcpTool(tool));
177
178
  const toolsContext = tools.length
178
- ? `\n[Tools]\n${JSON.stringify(tools.map(cleanToolDefinition_1.cleanToolDefinition), null, 2)}`
179
+ ? `${(0, generateToolMarkdown_1.generateToolMarkdown)(tools)}`
179
180
  : '';
180
181
  return `
181
182
  ${format(systemMsg)}
@@ -6,7 +6,7 @@ const ts_lsp_1 = require("./ts-lsp");
6
6
  function createDefaultBuiltinTools(context) {
7
7
  const { options } = context;
8
8
  return [
9
- ...(0, ts_lsp_1.getTsLspTools)((options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd()),
10
- ...(0, filesystem_1.getFilesystemTools)((options === null || options === void 0 ? void 0 : options.targetDir) || process.cwd()),
9
+ ...(0, ts_lsp_1.getTsLspTools)(options?.targetDir || process.cwd()),
10
+ ...(0, filesystem_1.getFilesystemTools)(options?.targetDir || process.cwd()),
11
11
  ];
12
12
  }
@@ -1,11 +1,45 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.getFilesystemTools = void 0;
7
40
  const promises_1 = __importDefault(require("fs/promises"));
8
- const minimatch_1 = require("minimatch");
41
+ const minimatchLib = __importStar(require("minimatch"));
42
+ const minimatch = typeof minimatchLib === 'function' ? minimatchLib : minimatchLib.minimatch;
9
43
  const path_1 = __importDefault(require("path"));
10
44
  const zod_1 = require("zod");
11
45
  const createTool_1 = require("../../utils/createTool");
@@ -50,7 +84,7 @@ const ListDirectoryWithSizesArgsSchema = zod_1.z.object({
50
84
  const DirectoryTreeArgsSchema = zod_1.z.object({
51
85
  path: zod_1.z.string(),
52
86
  excludePatterns: zod_1.z.array(zod_1.z.string()).optional().default([]),
53
- depth: zod_1.z.number().optional().default(2).describe('递归深度,默认 2 层。增加深度会消耗更多 Token'),
87
+ depth: zod_1.z.number().optional().default(2).describe('Recursive depth, default 2 layers. Increasing depth will consume more tokens'),
54
88
  });
55
89
  const MoveFileArgsSchema = zod_1.z.object({
56
90
  source: zod_1.z.string(),
@@ -65,19 +99,19 @@ const GetFileInfoArgsSchema = zod_1.z.object({
65
99
  path: zod_1.z.string(),
66
100
  });
67
101
  const GrepSearchArgsSchema = zod_1.z.object({
68
- path: zod_1.z.string().describe('搜索的起始目录路径'),
69
- query: zod_1.z.string().describe('要搜索的文本关键字'),
70
- includePattern: zod_1.z.string().optional().default('**/*').describe('匹配模式,例如 "**/*.ts"'),
71
- maxFiles: zod_1.z.number().optional().default(100).describe('最大扫描文件数,防止大型项目超时'),
102
+ path: zod_1.z.string().describe('The starting directory path to search'),
103
+ query: zod_1.z.string().describe('The text keyword to search'),
104
+ includePattern: zod_1.z.string().optional().default('**/*').describe('The matching pattern, for example "**/*.ts"'),
105
+ maxFiles: zod_1.z.number().optional().default(100).describe('The maximum number of files to scan, to prevent large projects from timing out'),
72
106
  });
73
107
  const PatchEditArgsSchema = zod_1.z.object({
74
- path: zod_1.z.string().describe('文件路径'),
108
+ path: zod_1.z.string().describe('The file path'),
75
109
  patches: zod_1.z.array(zod_1.z.object({
76
- startLine: zod_1.z.number().describe('起始行号(包含)'),
77
- endLine: zod_1.z.number().describe('结束行号(包含)'),
78
- replacement: zod_1.z.string().describe('要插入的新代码内容'),
79
- originalSnippet: zod_1.z.string().optional().describe('可选:该行范围内的原始代码片段,用于二次校验防止行号偏移'),
80
- })).describe('补丁列表。注意:若有多个补丁,建议从文件尾部向头部执行,或确保行号不重叠'),
110
+ startLine: zod_1.z.number().describe('The start line number (inclusive)'),
111
+ endLine: zod_1.z.number().describe('The end line number (inclusive)'),
112
+ replacement: zod_1.z.string().describe('The new code content to insert'),
113
+ originalSnippet: zod_1.z.string().optional().describe('Optional: The original code snippet within the line range, used to verify and prevent line number offset'),
114
+ })).describe('The patch list. Note: If there are multiple patches, it is recommended to execute from the end of the file to the beginning, or ensure that the line numbers do not overlap'),
81
115
  });
82
116
  const getFilesystemTools = (targetDir) => {
83
117
  (0, lib_1.setAllowedDirectories)([targetDir]);
@@ -101,15 +135,15 @@ const getFilesystemTools = (targetDir) => {
101
135
  };
102
136
  const readTextFileTool = (0, createTool_1.createTool)({
103
137
  name: 'read_text_file',
104
- description: '读取文件全文。若超过100行则禁止使用,必须改用 read_file_range。支持 head/tail 参数。',
138
+ description: 'Read the full content of the file. If it exceeds 100 lines, it is forbidden to use, must use read_file_range. Supports head/tail parameters.',
105
139
  parameters: ReadTextFileArgsSchema,
106
140
  handler: readTextFileHandler,
107
141
  });
108
142
  const readMultipleFilesTool = (0, createTool_1.createTool)({
109
143
  name: 'read_multiple_files',
110
- description: '同时读取多个文件的内容。当你需要对比多个文件或分析跨文件关联时使用。' +
111
- '注意:为了防止 Token 溢出,本工具一次最多读取 10 个文件,且每个文件仅展示前 6000 字符。' +
112
- '若需查看完整大文件或特定逻辑,请改用 read_file_range',
144
+ description: 'Read the content of multiple files at the same time. Use when you need to compare multiple files or analyze cross-file relationships.' +
145
+ 'Note: To prevent token overflow, this tool can read up to 10 files at a time, and each file only displays the first 6000 characters.' +
146
+ 'If you need to view the complete large file or specific logic, please use read_file_range instead.',
113
147
  parameters: ReadMultipleFilesArgsSchema,
114
148
  handler: async (args) => {
115
149
  // 保护 1:文件数量限制 (防止 AI 一次传入几十个文件)
@@ -124,25 +158,25 @@ const getFilesystemTools = (targetDir) => {
124
158
  const validPath = await (0, lib_1.validatePath)(targetDir, filePath);
125
159
  const content = await (0, lib_1.readFileContent)(validPath);
126
160
  if (content.length > MAX_CHARS_PER_FILE) {
127
- return `${filePath} (内容已截断):\n${content.substring(0, MAX_CHARS_PER_FILE)}\n\n[... 内容过长,仅展示前 ${MAX_CHARS_PER_FILE} 字符。若需查看后续内容,请使用 read_file_range 指定行号读取 ...]`;
161
+ return `${filePath} (Content truncated):\n${content.substring(0, MAX_CHARS_PER_FILE)}\n\n[... Content too long, only showing the first ${MAX_CHARS_PER_FILE} characters. If you need to view the subsequent content, please use read_file_range to specify the line number to read ...]`;
128
162
  }
129
163
  return `${filePath}:\n${content}\n`;
130
164
  }
131
165
  catch (error) {
132
166
  const errorMessage = error instanceof Error ? error.message : String(error);
133
- return `${filePath}: 读取失败 - ${errorMessage}`;
167
+ return `${filePath}: Read failed - ${errorMessage}`;
134
168
  }
135
169
  }));
136
170
  let text = results.join('\n---\n');
137
171
  if (isTruncatedByCount) {
138
- text += `\n\n⚠️ 注意:一次请求最多处理 ${MAX_FILES} 个文件。剩余 ${args.paths.length - MAX_FILES} 个文件未读取,请分批请求。`;
172
+ text += `\n\n⚠️ Note: The maximum number of files processed in one request is ${MAX_FILES}. ${args.paths.length - MAX_FILES} files remain unread, please request in batches.`;
139
173
  }
140
174
  return text;
141
175
  },
142
176
  });
143
177
  const writeFileTool = (0, createTool_1.createTool)({
144
178
  name: 'write_file',
145
- description: '仅用于创建新文件。严禁用于修改现有源代码。',
179
+ description: 'Only used to create new files. It is forbidden to use it to modify existing source code.',
146
180
  parameters: WriteFileArgsSchema,
147
181
  handler: async (args) => {
148
182
  const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
@@ -153,7 +187,7 @@ const getFilesystemTools = (targetDir) => {
153
187
  });
154
188
  const createDirectoryTool = (0, createTool_1.createTool)({
155
189
  name: 'create_directory',
156
- description: '递归创建目录。支持多级嵌套。若目录已存在则静默成功。仅限允许目录。',
190
+ description: 'Recursively create a directory. Supports nested directories. If the directory already exists, it will be successful silently. Only allowed directories.',
157
191
  parameters: CreateDirectoryArgsSchema,
158
192
  handler: async (args) => {
159
193
  const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
@@ -164,7 +198,7 @@ const getFilesystemTools = (targetDir) => {
164
198
  });
165
199
  const listDirectoryWithSizesTool = (0, createTool_1.createTool)({
166
200
  name: 'list_directory',
167
- description: '列出目录内容。返回带 [FILE]/[DIR] 前缀的条目、大小及汇总。支持按名称或大小排序。',
201
+ description: 'List the contents of the directory. Return entries with [FILE]/[DIR] prefix, size, and summary. Supports sorting by name or size.',
168
202
  parameters: ListDirectoryWithSizesArgsSchema,
169
203
  handler: async (args) => {
170
204
  const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
@@ -215,8 +249,8 @@ const getFilesystemTools = (targetDir) => {
215
249
  });
216
250
  const directoryTreeTool = (0, createTool_1.createTool)({
217
251
  name: 'directory_tree',
218
- description: '获取目录的递归树状 JSON 结构。' +
219
- '默认仅展示 2 层深度以节省 Token。如果需要看更深层级,请调大 depth 参数。',
252
+ description: 'Get the recursive tree JSON structure of the directory.' +
253
+ 'Default only shows 2 layers of depth to save tokens. If you need to see deeper levels, please increase the depth parameter.',
220
254
  parameters: DirectoryTreeArgsSchema,
221
255
  handler: async (args) => {
222
256
  // 在 directory_tree 的 handler 内部
@@ -230,7 +264,7 @@ const getFilesystemTools = (targetDir) => {
230
264
  const result = [];
231
265
  for (const entry of entries) {
232
266
  const relativePath = path_1.default.relative(rootPath, path_1.default.join(currentPath, entry.name));
233
- const shouldExclude = excludePatterns.some(pattern => (0, minimatch_1.minimatch)(relativePath, pattern, { dot: true }));
267
+ const shouldExclude = excludePatterns.some(pattern => minimatch(relativePath, pattern, { dot: true }));
234
268
  if (shouldExclude)
235
269
  continue;
236
270
  const entryData = {
@@ -251,7 +285,7 @@ const getFilesystemTools = (targetDir) => {
251
285
  });
252
286
  const moveFileTool = (0, createTool_1.createTool)({
253
287
  name: 'move_file',
254
- description: '移动或重命名文件/目录。目标路径若已存在则失败。源与目标均须在允许目录内。',
288
+ description: 'Move or rename a file/directory. If the target path already exists, it will fail. The source and target must be within the allowed directories.',
255
289
  parameters: MoveFileArgsSchema,
256
290
  handler: async (args) => {
257
291
  const validSourcePath = await (0, lib_1.validatePath)(targetDir, args.source);
@@ -263,7 +297,7 @@ const getFilesystemTools = (targetDir) => {
263
297
  });
264
298
  const searchFilesTool = (0, createTool_1.createTool)({
265
299
  name: 'search_files',
266
- description: '在指定路径下搜索匹配模式的文件名。返回匹配的相对路径列表。',
300
+ description: 'Search for file names matching a pattern in the specified path. Return a list of matching relative paths.',
267
301
  parameters: SearchFilesArgsSchema,
268
302
  handler: async (args) => {
269
303
  const combinedExcludes = [...DEFAULT_IGNORE, ...(args.excludePatterns || [])];
@@ -277,7 +311,7 @@ const getFilesystemTools = (targetDir) => {
277
311
  });
278
312
  const getFileInfoTool = (0, createTool_1.createTool)({
279
313
  name: 'get_file_info',
280
- description: '查看文件元数据(大小、行数、修改时间)。读取大文件前务必先调用此工具。',
314
+ description: 'View file metadata (size, line count, modification time). Must call this tool before reading large files.',
281
315
  parameters: GetFileInfoArgsSchema,
282
316
  handler: async (args) => {
283
317
  const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
@@ -299,9 +333,9 @@ const getFilesystemTools = (targetDir) => {
299
333
  });
300
334
  const grepSearchTool = (0, createTool_1.createTool)({
301
335
  name: 'grep_search',
302
- description: '在指定目录的文件内容中搜索关键字。' +
303
- '该工具会返回包含关键字的文件路径及匹配行的预览。' +
304
- '请尽量通过 includePattern 缩小搜索范围。',
336
+ description: 'Search for keywords in the content of files in the specified directory.' +
337
+ 'This tool will return the file paths and preview of matching lines containing the keywords.' +
338
+ 'Please narrow down the search scope as much as possible using includePattern.',
305
339
  parameters: GrepSearchArgsSchema,
306
340
  handler: async (args) => {
307
341
  const startPath = await (0, lib_1.validatePath)(targetDir, args.path);
@@ -333,21 +367,21 @@ const getFilesystemTools = (targetDir) => {
333
367
  }));
334
368
  }
335
369
  let response = matches.length > 0
336
- ? `找到关键词 "${args.query}" 的位置如下:\n${matches.join('\n')}`
337
- : `未找到包含 "${args.query}" 的内容。`;
370
+ ? `The location of the keyword "${args.query}" is as follows:\n${matches.join('\n')}`
371
+ : `The content containing "${args.query}" was not found.`;
338
372
  if (allFiles.length > args.maxFiles) {
339
- response += `\n\n注意:搜索已达到限制,仅扫描了前 ${args.maxFiles} 个文件。若未找到结果,请提供更精确的 path includePattern。`;
373
+ response += `\n\nNote: The search has reached the limit, only scanned the first ${args.maxFiles} files. If no results are found, please provide a more precise path or includePattern.`;
340
374
  }
341
375
  return response;
342
376
  },
343
377
  });
344
378
  const readFileRangeTool = (0, createTool_1.createTool)({
345
379
  name: 'read_file_range',
346
- description: '精准读取指定行范围(包含行号前缀)。修改代码前或根据报错定位时必用。',
380
+ description: 'Precisely read the specified line range (includes line number prefix). Must use this tool before modifying code or locating errors based on error messages.',
347
381
  parameters: zod_1.z.object({
348
- path: zod_1.z.string().describe('相对于目标目录的文件路径'),
349
- startLine: zod_1.z.number().describe('起始行号(从 1 开始计)'),
350
- endLine: zod_1.z.number().describe('结束行号'),
382
+ path: zod_1.z.string().describe('The file path relative to the target directory'),
383
+ startLine: zod_1.z.number().describe('The starting line number (starts from 1)'),
384
+ endLine: zod_1.z.number().describe('The ending line number'),
351
385
  }),
352
386
  handler: async (args) => {
353
387
  // 1. 验证路径安全(沿用你代码中的 validatePath 逻辑)
@@ -360,26 +394,26 @@ const getFilesystemTools = (targetDir) => {
360
394
  const start = Math.max(1, args.startLine);
361
395
  const end = Math.min(totalLines, args.endLine);
362
396
  if (start > totalLines) {
363
- return `错误:文件仅有 ${totalLines} 行,起始行号 ${start} 超出范围。`;
397
+ return `Error: The file only has ${totalLines} lines, the starting line number ${start} is out of range.`;
364
398
  }
365
399
  if (start > end) {
366
- return `错误:起始行号 ${start} 不能大于结束行号 ${end}。`;
400
+ return `Error: The starting line number ${start} cannot be greater than the ending line number ${end}.`;
367
401
  }
368
402
  // 3. 截取并添加行号索引(核心:增强 AI 的位置感)
369
403
  const selectedLines = lines.slice(start - 1, end);
370
404
  const formattedContent = selectedLines
371
405
  .map((line, index) => `${start + index}| ${line}`)
372
406
  .join('\n');
373
- return `[文件: ${args.path} | ${start} ${end} / ${totalLines} ]\n${formattedContent}`;
407
+ return `[File: ${args.path} | Lines ${start} to ${end} / Total ${totalLines} lines]\n${formattedContent}`;
374
408
  }
375
409
  catch (error) {
376
- return `读取文件范围失败: ${error.message}`;
410
+ return `Failed to read the file range: ${error.message}`;
377
411
  }
378
412
  },
379
413
  });
380
414
  const editFileTool = (0, createTool_1.createTool)({
381
415
  name: 'edit_file',
382
- description: '基于行号范围替换代码。修改逻辑的唯一工具。调用前须通过 read_file_range 获取最新行号。支持删除(空内容)或单行替换。',
416
+ description: 'Replace code based on line number range. The only tool for modifying logic. Must call read_file_range to get the latest line number before calling. Supports deletion (empty content) or single line replacement.',
383
417
  parameters: PatchEditArgsSchema,
384
418
  handler: async (args) => {
385
419
  const validPath = await (0, lib_1.validatePath)(targetDir, args.path);
@@ -391,14 +425,14 @@ const getFilesystemTools = (targetDir) => {
391
425
  for (const patch of sortedPatches) {
392
426
  // 校验行号合法性
393
427
  if (patch.startLine < 1 || patch.endLine > lines.length || patch.startLine > patch.endLine) {
394
- return `错误:行号范围 ${patch.startLine}-${patch.endLine} 超出文件实际范围 (1-${lines.length})`;
428
+ return `Error: The line number range ${patch.startLine}-${patch.endLine} is out of the actual range of the file (1-${lines.length})`;
395
429
  }
396
430
  // 可选:二次校验(防止 AI 记忆了错误的行号)
397
431
  if (patch.originalSnippet) {
398
432
  const currentText = lines.slice(patch.startLine - 1, patch.endLine).join('\n');
399
433
  // 模糊对比,如果差异太大则报错
400
434
  if (!currentText.includes(patch.originalSnippet.trim()) && currentText.trim().length > 0) {
401
- return `警告:第 ${patch.startLine} 行的内容已发生变动,与你预想的代码不符。请重新读取文件获取最新行号。`;
435
+ return `Warning: The content of the ${patch.startLine} line has changed, which does not match your expected code. Please read the file again to get the latest line number.`;
402
436
  }
403
437
  }
404
438
  // 执行替换:splice(开始索引, 删除数量, 替换内容)
@@ -406,10 +440,10 @@ const getFilesystemTools = (targetDir) => {
406
440
  lines.splice(patch.startLine - 1, (patch.endLine - patch.startLine) + 1, patch.replacement);
407
441
  }
408
442
  await promises_1.default.writeFile(validPath, lines.join('\n'), 'utf-8');
409
- return `成功通过行号更新了 ${args.path} ${args.patches.length} 处代码。`;
443
+ return `Successfully updated ${args.path} with ${args.patches.length} code changes.`;
410
444
  }
411
445
  catch (error) {
412
- return `Patch 失败: ${error.message}`;
446
+ return `Patch failed: ${error.message}`;
413
447
  }
414
448
  },
415
449
  });
@@ -142,7 +142,6 @@ async function writeFileContent(filePath, content) {
142
142
  }
143
143
  }
144
144
  async function applyFileEdits(filePath, edits, dryRun = false) {
145
- var _a;
146
145
  // Read file content and normalize line endings
147
146
  const content = normalizeLineEndings(await promises_1.default.readFile(filePath, 'utf-8'));
148
147
  // Apply edits sequentially
@@ -168,14 +167,13 @@ async function applyFileEdits(filePath, edits, dryRun = false) {
168
167
  });
169
168
  if (isMatch) {
170
169
  // Preserve original indentation of first line
171
- const originalIndent = ((_a = contentLines[i].match(/^\s*/)) === null || _a === void 0 ? void 0 : _a[0]) || '';
170
+ const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
172
171
  const newLines = normalizedNew.split('\n').map((line, j) => {
173
- var _a, _b, _c;
174
172
  if (j === 0)
175
173
  return originalIndent + line.trimStart();
176
174
  // For subsequent lines, try to preserve relative indentation
177
- const oldIndent = ((_b = (_a = oldLines[j]) === null || _a === void 0 ? void 0 : _a.match(/^\s*/)) === null || _b === void 0 ? void 0 : _b[0]) || '';
178
- const newIndent = ((_c = line.match(/^\s*/)) === null || _c === void 0 ? void 0 : _c[0]) || '';
175
+ const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
176
+ const newIndent = line.match(/^\s*/)?.[0] || '';
179
177
  if (oldIndent && newIndent) {
180
178
  const relativeIndent = newIndent.length - oldIndent.length;
181
179
  return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
@@ -9,18 +9,19 @@ const getTsLspTools = (targetDir) => {
9
9
  return [
10
10
  (0, createTool_1.createTool)({
11
11
  name: 'get_method_body',
12
- description: '【仅限TS/JS】通过方法名提取代码块。比行号读取更抗干扰,参考逻辑时首选。',
12
+ description: '[Only for TS/JS] Extract code blocks by method name. More resistant to interference than line number reading, preferred when referencing logic.',
13
13
  parameters: zod_1.z.object({
14
- filePath: zod_1.z.string().describe('文件相对路径'),
15
- methodName: zod_1.z.string().describe('方法名或函数名'),
14
+ filePath: zod_1.z.string().describe('The relative file path'),
15
+ methodName: zod_1.z.string().describe('The method name or function name'),
16
16
  }),
17
17
  handler: async ({ filePath, methodName }) => {
18
- return engine.getMethodImplementation(filePath, methodName);
18
+ const res = engine.getMethodImplementation(filePath, methodName);
19
+ return typeof res === 'string' ? res : JSON.stringify(res);
19
20
  },
20
21
  }),
21
22
  (0, createTool_1.createTool)({
22
23
  name: 'get_repo_map',
23
- description: '获取项目全局文件结构及导出清单,用于快速定位代码',
24
+ description: 'Get the global file structure and export list of the project, used for quick code location',
24
25
  parameters: zod_1.z.object({}),
25
26
  handler: async () => {
26
27
  engine.refresh();
@@ -29,26 +30,26 @@ const getTsLspTools = (targetDir) => {
29
30
  }),
30
31
  (0, createTool_1.createTool)({
31
32
  name: 'analyze_deps',
32
- description: '分析指定文件的依赖关系,支持 tsconfig 路径别名解析',
33
- parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('文件相对路径') }),
33
+ description: 'Analyze the dependencies of the specified file, support tsconfig path alias parsing',
34
+ parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('The relative file path') }),
34
35
  handler: async ({ filePath }) => engine.getDeps(filePath),
35
36
  }),
36
37
  (0, createTool_1.createTool)({
37
38
  name: 'read_skeleton',
38
- description: '提取文件的结构定义(接口、类、方法签名),忽略具体实现以节省 Token',
39
- parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('文件相对路径') }),
39
+ description: 'Extract the structure definition of the file (interface, class, method signature), ignoring the specific implementation to save tokens',
40
+ parameters: zod_1.z.object({ filePath: zod_1.z.string().describe('The relative file path') }),
40
41
  handler: async (args) => {
41
- const pathArg = args === null || args === void 0 ? void 0 : args.filePath;
42
+ const pathArg = args?.filePath;
42
43
  if (typeof pathArg !== 'string' || pathArg.trim() === '') {
43
- return `Error: 参数 'filePath' 无效。收到的是: ${JSON.stringify(pathArg)}`;
44
+ return `Error: The parameter 'filePath' is invalid. Received: ${JSON.stringify(pathArg)}`;
44
45
  }
45
46
  try {
46
47
  engine.refresh();
47
48
  const result = engine.getSkeleton(pathArg);
48
- return result || `// Warning: 文件 ${pathArg} 存在但未找到任何可提取的结构。`;
49
+ return result || `// Warning: The file ${pathArg} exists but no structure can be extracted.`;
49
50
  }
50
51
  catch (error) {
51
- return `Error: 解析文件 ${pathArg} 时发生内部错误: ${error.message}`;
52
+ return `Error: An internal error occurred while parsing the file ${pathArg}: ${error.message}`;
52
53
  }
53
54
  },
54
55
  }),
@@ -5,7 +5,7 @@ const tools_1 = require("@langchain/core/tools");
5
5
  function convertToLangChainTool(info) {
6
6
  return new tools_1.DynamicStructuredTool({
7
7
  name: info.function.name,
8
- description: info.function.description || "",
8
+ description: info.function.description || '',
9
9
  schema: info.function.parameters,
10
10
  func: async (args) => {
11
11
  if (info._handler)
@@ -17,7 +17,7 @@ function convertToLangChainTool(info) {
17
17
  });
18
18
  return JSON.stringify(result);
19
19
  }
20
- return "Error: No tool execution handler found.";
20
+ return 'Error: No tool execution handler found.';
21
21
  },
22
22
  });
23
23
  }
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createTool = createTool;
4
4
  function createTool(options) {
5
5
  return {
6
- type: "function",
6
+ type: 'function',
7
7
  function: {
8
8
  name: options.name,
9
9
  description: options.description,
@@ -11,13 +11,13 @@ function formatSchema(schema) {
11
11
  const requiredKeys = res.required || [];
12
12
  const lines = [];
13
13
  for (const key in res.properties) {
14
- lines.push(` - ${key}: ${res.properties[key].type}${requiredKeys.includes(key) ? " (required)" : ""}`);
14
+ lines.push(` - ${key}: ${res.properties[key].type}${requiredKeys.includes(key) ? ' (required)' : ''}`);
15
15
  }
16
16
  if (lines.length > 0) {
17
- return lines.join("\n");
17
+ return lines.join('\n');
18
18
  }
19
- return "No parameters";
19
+ return 'No parameters';
20
20
  }
21
21
  const keys = Object.keys(schema.shape);
22
- return keys.join(", ");
22
+ return keys.join(', ');
23
23
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 将工具定义从 Zod Schema 转换为极简 Markdown 格式
3
+ * 目的:显著节省 System Prompt 的 Token,同时保持 LLM 理解力
4
+ */
5
+ export declare function generateToolMarkdown(tools: any[]): string;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateToolMarkdown = generateToolMarkdown;
4
+ const cleanToolDefinition_1 = require("./cleanToolDefinition");
5
+ const kit_1 = require("./kit");
6
+ /**
7
+ * 将工具定义从 Zod Schema 转换为极简 Markdown 格式
8
+ * 目的:显著节省 System Prompt 的 Token,同时保持 LLM 理解力
9
+ */
10
+ function generateToolMarkdown(tools) {
11
+ let markdown = "## Tool Definitions\n\n";
12
+ (0, kit_1.getArray)(tools).forEach((tool) => {
13
+ const { name, description, parameters } = (0, cleanToolDefinition_1.cleanToolDefinition)(tool);
14
+ markdown += `- **${name}**: ${description}\n`;
15
+ // 提取 Zod 参数
16
+ const shape = parameters.properties || {};
17
+ const requiredFields = parameters.required || [];
18
+ Object.entries(shape).forEach(([paramName, schema]) => {
19
+ const isRequired = requiredFields.includes(paramName);
20
+ const type = schema.type.replace('Zod', '').toLowerCase();
21
+ const desc = schema.description || schema.title || '';
22
+ markdown += ` - \`${paramName}\` (${type}${isRequired ? ', required' : ''})${desc ? `: ${desc}` : ''}\n`;
23
+ });
24
+ markdown += "\n";
25
+ });
26
+ return markdown;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber2pr/ai-agent",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "AI Assistant CLI.",
5
5
  "author": "saber2pr",
6
6
  "license": "ISC",
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "bin": {
11
11
  "sagent": "./lib/cli.js",
12
- "sagent-graph": "./lib/cli-graph.js"
12
+ "sagent-openai": "./lib/cli-openai.js"
13
13
  },
14
14
  "publishConfig": {
15
15
  "access": "public",
@@ -42,7 +42,7 @@
42
42
  "@langchain/core": "0.3.40"
43
43
  },
44
44
  "devDependencies": {
45
- "@types/node": "^16.3.3",
45
+ "@types/node": "^25.2.3",
46
46
  "prettier": "^2.4.1"
47
47
  }
48
48
  }
File without changes