@saber2pr/ai-agent 0.0.3 → 0.0.5

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 (3) hide show
  1. package/lib/agent.d.ts +11 -13
  2. package/lib/agent.js +269 -126
  3. package/package.json +3 -1
package/lib/agent.d.ts CHANGED
@@ -4,28 +4,26 @@ export default class McpAgent {
4
4
  private clients;
5
5
  private allTools;
6
6
  private messages;
7
+ private engine;
8
+ private encoder;
7
9
  constructor();
8
10
  /**
9
- * 1. API Configuration Management
10
- * Checks for existing config or prompts user for input.
11
+ * 计算当前消息列表的总 Token 消耗
12
+ * 兼容多模态内容 (Content Parts) 工具调用 (Tool Calls)
11
13
  */
12
- private ensureApiConfig;
14
+ private calculateTokens;
13
15
  /**
14
- * 2. Load MCP server configs from common IDE paths
16
+ * 核心功能:内置代码分析工具
17
+ * 这里的逻辑直接调用 PromptEngine,不走网络请求,效率极高
15
18
  */
19
+ private registerBuiltinTools;
20
+ private ensureApiConfig;
16
21
  private loadMcpConfigs;
17
- /**
18
- * 3. Initialization
19
- * Validates API credentials and connects to MCP servers.
20
- */
21
22
  init(): Promise<void>;
22
- /**
23
- * 4. Core Chat Logic
24
- * Handles user input and recursive tool calls.
25
- */
26
23
  private processChat;
27
24
  /**
28
- * 5. Start the Interactive Shell
25
+ * 简易 Loading 动画辅助函数
29
26
  */
27
+ private showLoading;
30
28
  start(): Promise<void>;
31
29
  }
package/lib/agent.js CHANGED
@@ -36,124 +36,252 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ const openai_1 = __importDefault(require("openai"));
39
40
  const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
40
41
  const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
41
- const openai_1 = __importDefault(require("openai"));
42
42
  const fs_1 = __importDefault(require("fs"));
43
43
  const path_1 = __importDefault(require("path"));
44
44
  const os_1 = __importDefault(require("os"));
45
45
  const readline = __importStar(require("readline"));
46
+ const ts_context_mcp_1 = require("@saber2pr/ts-context-mcp"); // 引入我们的核心引擎
47
+ const js_tiktoken_1 = require("js-tiktoken");
46
48
  const CONFIG_FILE = path_1.default.join(os_1.default.homedir(), ".saber2pr-agent.json");
47
- // --- Core Class ---
48
49
  class McpAgent {
49
50
  constructor() {
50
51
  this.modelName = "";
51
52
  this.clients = [];
52
53
  this.allTools = [];
53
54
  this.messages = [];
55
+ this.encoder = (0, js_tiktoken_1.getEncoding)("cl100k_base");
56
+ // 默认以当前工作目录为分析目标
57
+ this.engine = new ts_context_mcp_1.PromptEngine(process.cwd());
54
58
  this.messages.push({
55
59
  role: "system",
56
- content: "You are a powerful local assistant. You can access local tools provided by the user via the MCP protocol. Please answer questions by combining tool outputs and context.",
60
+ content: `你是一个专业的 AI 代码架构师。
61
+ 你可以访问本地文件系统并利用 AST (抽象语法树) 技术分析代码。
62
+ 你的核心目标是提供准确的代码结构、依赖关系和逻辑分析。
63
+ 请先使用 get_repo_map 查看项目整体代码结构。
64
+ 请优先使用 read_skeleton 查看结构,只有在必要时才使用 read_full_code 或 get_method_body。`,
57
65
  });
66
+ // 初始化内置工具
67
+ this.registerBuiltinTools();
58
68
  }
59
69
  /**
60
- * 1. API Configuration Management
61
- * Checks for existing config or prompts user for input.
70
+ * 计算当前消息列表的总 Token 消耗
71
+ * 兼容多模态内容 (Content Parts) 工具调用 (Tool Calls)
62
72
  */
63
- async ensureApiConfig() {
64
- if (fs_1.default.existsSync(CONFIG_FILE)) {
65
- try {
66
- const config = JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, "utf-8"));
67
- if (config.baseURL && config.apiKey && config.model) {
68
- return config;
73
+ calculateTokens() {
74
+ let total = 0;
75
+ for (const msg of this.messages) {
76
+ // 1. 处理消息内容 (Content)
77
+ if (msg.content) {
78
+ if (typeof msg.content === "string") {
79
+ // 普通文本消息
80
+ total += this.encoder.encode(msg.content).length;
81
+ }
82
+ else if (Array.isArray(msg.content)) {
83
+ // 多模态/复合内容消息 (ChatCompletionContentPart[])
84
+ for (const part of msg.content) {
85
+ if (part.type === "text" && "text" in part) {
86
+ total += this.encoder.encode(part.text || "").length;
87
+ }
88
+ // 注意:图片 (image_url) 的 Token 计算通常基于分辨率,tiktoken 无法计算
89
+ }
69
90
  }
70
91
  }
71
- catch (e) {
72
- console.error(`[Error] Failed to read ${CONFIG_FILE}, re-initializing...`);
92
+ // 2. 处理助手角色发出的工具调用请求 (Assistant Tool Calls)
93
+ // 这是为了统计 AI 发出的指令所占用的 Token
94
+ if (msg.role === "assistant" && msg.tool_calls) {
95
+ for (const call of msg.tool_calls) {
96
+ if (call.type === "function") {
97
+ // 统计函数名和参数字符串
98
+ total += this.encoder.encode(call.function.name).length;
99
+ total += this.encoder.encode(call.function.arguments).length;
100
+ }
101
+ }
102
+ }
103
+ // 3. 处理工具返回的结果 (Tool Role)
104
+ // 在 processChat 中,我们确保了工具返回的 result 最终被转为了 string
105
+ if (msg.role === "tool" && typeof msg.content === "string") {
106
+ total += this.encoder.encode(msg.content).length;
73
107
  }
74
108
  }
75
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
76
- const question = (query) => new Promise((resolve) => rl.question(query, resolve));
77
- console.log("\n🔑 API Configuration not found. Please provide the following details:");
78
- const baseURL = await question("? API Base URL: ");
79
- const apiKey = await question("? API Key: ");
80
- const model = await question("? Model Name: ");
81
- if (!baseURL || !apiKey || !model) {
82
- console.error("❌ Error: All fields (Base URL, API Key, Model Name) are required!");
83
- process.exit(1);
109
+ return total;
110
+ }
111
+ /**
112
+ * 核心功能:内置代码分析工具
113
+ * 这里的逻辑直接调用 PromptEngine,不走网络请求,效率极高
114
+ */
115
+ registerBuiltinTools() {
116
+ const builtinTools = [
117
+ {
118
+ type: "function",
119
+ function: {
120
+ name: "get_repo_map",
121
+ description: "获取项目全局文件结构及导出清单,用于快速定位代码",
122
+ parameters: { type: "object", properties: {} },
123
+ },
124
+ _handler: async () => {
125
+ this.engine.refresh();
126
+ return this.engine.getRepoMap();
127
+ },
128
+ },
129
+ {
130
+ type: "function",
131
+ function: {
132
+ name: "analyze_deps",
133
+ description: "分析指定文件的依赖关系,支持 tsconfig 路径别名解析",
134
+ parameters: {
135
+ type: "object",
136
+ properties: {
137
+ filePath: { type: "string", description: "文件相对路径" },
138
+ },
139
+ required: ["filePath"],
140
+ },
141
+ },
142
+ _handler: async ({ filePath }) => this.engine.getDeps(filePath),
143
+ },
144
+ {
145
+ type: "function",
146
+ function: {
147
+ name: "read_skeleton",
148
+ description: "提取文件的结构定义(接口、类、方法签名),忽略具体实现以节省 Token",
149
+ parameters: {
150
+ type: "object",
151
+ properties: {
152
+ filePath: { type: "string", description: "文件相对路径" },
153
+ },
154
+ required: ["filePath"],
155
+ },
156
+ },
157
+ _handler: async (args) => {
158
+ // 1. 严格路径守卫:防止 undefined 或空字符串进入 path 模块
159
+ const pathArg = args === null || args === void 0 ? void 0 : args.filePath;
160
+ if (typeof pathArg !== 'string' || pathArg.trim() === '') {
161
+ return `Error: 参数 'filePath' 无效。收到的是: ${JSON.stringify(pathArg)}`;
162
+ }
163
+ try {
164
+ // 2. 刷新引擎状态,确保分析的是最新的文件内容
165
+ this.engine.refresh();
166
+ // 3. 执行获取
167
+ const result = this.engine.getSkeleton(pathArg);
168
+ // 4. 空值回退:防止 getSkeleton 返回 null 导致后续统计 Token 时崩溃
169
+ return result || `// Warning: 文件 ${pathArg} 存在但未找到任何可提取的结构。`;
170
+ }
171
+ catch (error) {
172
+ // 5. 捕获 AST 级别的 match 错误
173
+ return `Error: 解析文件 ${pathArg} 时发生内部错误: ${error.message}`;
174
+ }
175
+ },
176
+ },
177
+ {
178
+ type: "function",
179
+ function: {
180
+ name: "get_method_body",
181
+ description: "获取指定文件内某个方法或函数的完整实现代码",
182
+ parameters: {
183
+ type: "object",
184
+ properties: {
185
+ filePath: { type: "string", description: "文件路径" },
186
+ methodName: { type: "string", description: "方法名或函数名" },
187
+ },
188
+ required: ["filePath", "methodName"],
189
+ },
190
+ },
191
+ _handler: async ({ filePath, methodName }) => this.engine.getMethodImplementation(filePath, methodName),
192
+ },
193
+ {
194
+ type: "function",
195
+ function: {
196
+ name: "read_full_code",
197
+ description: "读取指定文件的完整源代码内容。当需要分析具体实现逻辑或查找硬编码字符串时使用。",
198
+ parameters: {
199
+ type: "object",
200
+ properties: {
201
+ filePath: { type: "string", description: "文件相对路径" },
202
+ },
203
+ required: ["filePath"],
204
+ },
205
+ },
206
+ // 核心实现:直接利用 fs 读取
207
+ _handler: async ({ filePath }) => {
208
+ try {
209
+ if (typeof filePath !== 'string' || !filePath) {
210
+ return "Error: filePath 不能为空";
211
+ }
212
+ // 拼合绝对路径
213
+ const fullPath = path_1.default.resolve(this.engine.getRootDir(), filePath);
214
+ // 安全检查:防止 AI 尝试读取项目外的敏感文件
215
+ if (!fullPath.startsWith(this.engine.getRootDir())) {
216
+ return "Error: 权限拒绝,禁止访问项目目录外的文件。";
217
+ }
218
+ if (!fs_1.default.existsSync(fullPath)) {
219
+ return `Error: 文件不存在: ${filePath}`;
220
+ }
221
+ return fs_1.default.readFileSync(fullPath, "utf-8");
222
+ }
223
+ catch (err) {
224
+ return `Error: 读取文件失败: ${err.message}`;
225
+ }
226
+ },
227
+ },
228
+ ];
229
+ this.allTools.push(...builtinTools);
230
+ }
231
+ // --- 初始化与环境准备 (API Config & MCP Servers) ---
232
+ async ensureApiConfig() {
233
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
234
+ return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, "utf-8"));
84
235
  }
85
- const config = { baseURL, apiKey, model };
236
+ const rl = readline.createInterface({
237
+ input: process.stdin,
238
+ output: process.stdout,
239
+ });
240
+ const question = (q) => new Promise((res) => rl.question(q, res));
241
+ console.log("\n🔑 配置 API 凭据:");
242
+ const config = {
243
+ baseURL: await question("? API Base URL (如 https://api.openai.com/v1): "),
244
+ apiKey: await question("? API Key: "),
245
+ model: await question("? Model Name (如 gpt-4o): "),
246
+ };
86
247
  fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
87
- console.log(`✅ Configuration saved to ${CONFIG_FILE}\n`);
88
248
  rl.close();
89
249
  return config;
90
250
  }
91
- /**
92
- * 2. Load MCP server configs from common IDE paths
93
- */
94
251
  loadMcpConfigs() {
95
- const combinedConfig = { mcpServers: {} };
96
- const configPaths = [
252
+ const combined = { mcpServers: {} };
253
+ const paths = [
97
254
  path_1.default.join(os_1.default.homedir(), ".cursor", "mcp.json"),
98
255
  path_1.default.join(os_1.default.homedir(), ".vscode", "mcp.json"),
99
256
  ];
100
- for (const p of configPaths) {
257
+ paths.forEach((p) => {
101
258
  if (fs_1.default.existsSync(p)) {
102
- try {
103
- const content = JSON.parse(fs_1.default.readFileSync(p, "utf-8"));
104
- if (content.mcpServers) {
105
- combinedConfig.mcpServers = { ...combinedConfig.mcpServers, ...content.mcpServers };
106
- console.log(`[MCP] Config loaded from: ${p}`);
107
- }
108
- }
109
- catch (e) {
110
- console.error(`[Error] Failed to parse MCP config ${p}:`, e);
111
- }
259
+ const content = JSON.parse(fs_1.default.readFileSync(p, "utf-8"));
260
+ Object.assign(combined.mcpServers, content.mcpServers);
112
261
  }
113
- }
114
- return combinedConfig;
262
+ });
263
+ return combined;
115
264
  }
116
- /**
117
- * 3. Initialization
118
- * Validates API credentials and connects to MCP servers.
119
- */
120
265
  async init() {
121
- // A. Setup & Validate OpenAI
122
266
  const apiConfig = await this.ensureApiConfig();
123
267
  this.openai = new openai_1.default({
124
268
  baseURL: apiConfig.baseURL,
125
269
  apiKey: apiConfig.apiKey,
126
270
  });
127
271
  this.modelName = apiConfig.model;
128
- console.log("🔍 Validating API configuration...");
129
- try {
130
- // Perform a lightweight check to verify URL and Key
131
- await this.openai.models.list();
132
- console.log("✅ API validation successful.");
133
- }
134
- catch (e) {
135
- console.error("\n❌ API Connection Failed!");
136
- console.error(`Reason: ${e.message}`);
137
- console.log(`\nSuggestion: If you made a mistake, please delete or edit: ${CONFIG_FILE}`);
138
- process.exit(1);
139
- }
140
- // B. Setup MCP Clients
272
+ // 链接外部 MCP Server ( Google Search, Filesystem 等)
141
273
  const mcpConfig = this.loadMcpConfigs();
142
- const serverEntries = Object.entries(mcpConfig.mcpServers);
143
- if (serverEntries.length === 0) {
144
- console.warn("⚠️ No MCP server configurations found.");
145
- }
146
- for (const [name, server] of serverEntries) {
274
+ for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
147
275
  try {
148
276
  const transport = new stdio_js_1.StdioClientTransport({
149
277
  command: server.command,
150
278
  args: server.args || [],
151
- env: { ...process.env, ...(server.env || {}) },
279
+ env: { ...process.env, ...server.env },
152
280
  });
153
281
  const client = new index_js_1.Client({ name, version: "1.0.0" }, { capabilities: {} });
154
282
  await client.connect(transport);
155
283
  const { tools } = await client.listTools();
156
- const formatted = tools.map((t) => ({
284
+ this.allTools.push(...tools.map((t) => ({
157
285
  type: "function",
158
286
  function: {
159
287
  name: `${name}__${t.name}`,
@@ -162,93 +290,108 @@ class McpAgent {
162
290
  },
163
291
  _originalName: t.name,
164
292
  _client: client,
165
- }));
166
- this.allTools.push(...formatted);
167
- this.clients.push(client);
168
- console.log(`✅ [${name}] Connected, loaded ${tools.length} tools`);
293
+ })));
294
+ console.log(`✅ [${name}] 加载成功`);
169
295
  }
170
296
  catch (e) {
171
- console.error(`❌ [${name}] Failed to start:`, e);
297
+ console.error(`❌ [${name}] 启动失败`);
172
298
  }
173
299
  }
174
300
  }
175
- /**
176
- * 4. Core Chat Logic
177
- * Handles user input and recursive tool calls.
178
- */
179
301
  async processChat(userInput) {
180
- this.messages.push({ role: "user", content: userInput });
181
- let isThinking = true;
182
- while (isThinking) {
183
- const apiTools = this.allTools.map(({ _originalName, _client, ...rest }) => rest);
184
- const response = await this.openai.chat.completions.create({
185
- model: this.modelName,
186
- messages: this.messages,
187
- tools: apiTools.length > 0 ? apiTools : undefined,
188
- tool_choice: "auto",
189
- });
302
+ var _a;
303
+ this.messages.push({ role: 'user', content: userInput });
304
+ while (true) {
305
+ // 打印当前上下文的累计 Token
306
+ const currentInputTokens = this.calculateTokens();
307
+ console.log(`\n📊 当前上下文累计: ${currentInputTokens} tokens`);
308
+ const stopLoading = this.showLoading("🤖 Agent 正在思考...");
309
+ let response;
310
+ try {
311
+ response = await this.openai.chat.completions.create({
312
+ model: this.modelName,
313
+ messages: this.messages,
314
+ tools: this.allTools.map(({ _handler, _client, _originalName, ...rest }) => rest),
315
+ tool_choice: 'auto'
316
+ });
317
+ }
318
+ finally {
319
+ stopLoading();
320
+ }
190
321
  const message = response.choices[0].message;
191
- // If no more tool calls, exit loop and show final response
192
- if (!message.tool_calls || message.tool_calls.length === 0) {
193
- this.messages.push(message);
322
+ this.messages.push(message);
323
+ // 计算本次 AI 回复生成的 Token
324
+ const completionTokens = ((_a = response.usage) === null || _a === void 0 ? void 0 : _a.completion_tokens) ||
325
+ (message.content ? this.encoder.encode(message.content).length : 0);
326
+ console.log(`✨ AI 回复消耗: ${completionTokens} tokens`);
327
+ if (!message.tool_calls) {
194
328
  console.log(`\n🤖 Agent: ${message.content}`);
195
- isThinking = false;
196
329
  break;
197
330
  }
198
- // Handle tool calls requested by the model
199
- this.messages.push(message);
200
- console.log(`\n⚙️ Model requested ${message.tool_calls.length} tool calls...`);
201
- for (const toolCall of message.tool_calls) {
202
- const toolInfo = this.allTools.find((t) => t.function.name === toolCall.function.name);
203
- if (toolInfo) {
204
- const args = JSON.parse(toolCall.function.arguments);
205
- console.log(` - Executing: ${toolInfo.function.name}`);
206
- try {
207
- const result = await toolInfo._client.callTool({
208
- name: toolInfo._originalName,
209
- arguments: args,
210
- });
211
- this.messages.push({
212
- role: "tool",
213
- tool_call_id: toolCall.id,
214
- content: JSON.stringify(result.content),
215
- });
216
- }
217
- catch (error) {
218
- console.error(` - Execution failed: ${error.message}`);
219
- this.messages.push({
220
- role: "tool",
221
- tool_call_id: toolCall.id,
222
- content: `Error: ${error.message}`,
223
- });
224
- }
331
+ console.log(`\n⚙️ 正在执行 ${message.tool_calls.length} 个操作...`);
332
+ for (const call of message.tool_calls) {
333
+ const tool = this.allTools.find(t => t.function.name === call.function.name);
334
+ const args = JSON.parse(call.function.arguments);
335
+ // 打印文件路径提示
336
+ if (args.filePath) {
337
+ console.log(` 📂 正在查看文件: ${args.filePath}`);
338
+ }
339
+ console.log(` 🛠️ 执行: ${call.function.name}`);
340
+ let result;
341
+ if (tool === null || tool === void 0 ? void 0 : tool._handler) {
342
+ result = await tool._handler(args);
343
+ }
344
+ else if ((tool === null || tool === void 0 ? void 0 : tool._client) && tool._originalName) {
345
+ const mcpRes = await tool._client.callTool({
346
+ name: tool._originalName,
347
+ arguments: args
348
+ });
349
+ result = mcpRes.content;
225
350
  }
351
+ const resultContent = typeof result === "string" ? result : JSON.stringify(result);
352
+ // 打印工具返回结果的 Token 消耗
353
+ const toolResultTokens = this.encoder.encode(resultContent).length;
354
+ console.log(` 📝 工具输出: ${toolResultTokens} tokens`);
355
+ this.messages.push({
356
+ role: 'tool',
357
+ tool_call_id: call.id,
358
+ content: resultContent
359
+ });
360
+ console.log(` ✅ 完成: ${call.function.name}`);
226
361
  }
227
362
  }
228
363
  }
229
364
  /**
230
- * 5. Start the Interactive Shell
365
+ * 简易 Loading 动画辅助函数
231
366
  */
367
+ showLoading(text) {
368
+ const chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
369
+ let i = 0;
370
+ const timer = setInterval(() => {
371
+ process.stdout.write(`\r${chars[i]} ${text}`);
372
+ i = (i + 1) % chars.length;
373
+ }, 80);
374
+ return () => {
375
+ clearInterval(timer);
376
+ process.stdout.write('\r\x1b[K'); // 清除当前行
377
+ };
378
+ }
232
379
  async start() {
233
380
  await this.init();
234
381
  const rl = readline.createInterface({
235
382
  input: process.stdin,
236
383
  output: process.stdout,
237
384
  });
238
- console.log(`\n🚀 Agent Started (Model: ${this.modelName})! Type 'exit' to quit.`);
385
+ console.log(`\n🚀 代码助手已启动 (目标目录: ${this.engine.getRootDir()})`);
239
386
  const chatLoop = () => {
240
- rl.question("\n👤 You: ", async (input) => {
241
- if (input.toLowerCase() === "exit") {
242
- console.log("Goodbye!");
243
- rl.close();
387
+ rl.question("\n👤 你: ", async (input) => {
388
+ if (input.toLowerCase() === "exit")
244
389
  process.exit(0);
245
- }
246
390
  try {
247
391
  await this.processChat(input);
248
392
  }
249
393
  catch (err) {
250
- console.error("\n❌ System Error during chat:", err.message);
251
- console.log("Try checking your API configuration or network connection.");
394
+ console.error("\n❌ 系统错误:", err.message);
252
395
  }
253
396
  chatLoop();
254
397
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saber2pr/ai-agent",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "AI Assistant CLI.",
5
5
  "author": "saber2pr",
6
6
  "license": "ISC",
@@ -22,6 +22,8 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@modelcontextprotocol/sdk": "^1.25.3",
25
+ "@saber2pr/ts-context-mcp": "^0.0.6",
26
+ "js-tiktoken": "^1.0.21",
25
27
  "openai": "^6.16.0"
26
28
  },
27
29
  "devDependencies": {