formagent-sdk 0.1.3 → 0.3.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 (3) hide show
  1. package/dist/cli/index.js +3118 -746
  2. package/dist/index.js +2445 -106
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -5,8 +5,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  // src/cli/cli.ts
6
6
  import * as readline from "node:readline";
7
7
  import { homedir as homedir4 } from "node:os";
8
- import { join as join7 } from "node:path";
8
+ import { join as join8 } from "node:path";
9
9
  import { existsSync as existsSync10 } from "node:fs";
10
+ import { readFileSync as readFileSync2 } from "node:fs";
10
11
 
11
12
  // src/utils/id.ts
12
13
  var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -76,6 +77,113 @@ class TypedEventEmitter {
76
77
  }
77
78
  }
78
79
 
80
+ // src/utils/truncation.ts
81
+ import * as fs from "node:fs/promises";
82
+ import * as path from "node:path";
83
+ import * as os from "node:os";
84
+ var TRUNCATION_DEFAULTS = {
85
+ MAX_LINES: 2000,
86
+ MAX_BYTES: 50 * 1024,
87
+ RETENTION_MS: 7 * 24 * 60 * 60 * 1000
88
+ };
89
+ function getTempDir(config) {
90
+ return config?.tempDir ?? path.join(os.tmpdir(), "formagent-sdk-output");
91
+ }
92
+ function generateOutputFilename() {
93
+ const timestamp = Date.now();
94
+ const random = Math.random().toString(36).substring(2, 8);
95
+ return `tool_${timestamp}_${random}.txt`;
96
+ }
97
+ async function ensureTempDir(dir) {
98
+ try {
99
+ await fs.mkdir(dir, { recursive: true });
100
+ } catch {}
101
+ }
102
+ async function truncateOutput(text, config = {}) {
103
+ const maxLines = config.maxLines ?? TRUNCATION_DEFAULTS.MAX_LINES;
104
+ const maxBytes = config.maxBytes ?? TRUNCATION_DEFAULTS.MAX_BYTES;
105
+ const direction = config.direction ?? "head";
106
+ const saveToFile = config.saveToFile ?? true;
107
+ const lines = text.split(`
108
+ `);
109
+ const totalBytes = Buffer.byteLength(text, "utf-8");
110
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
111
+ return {
112
+ content: text,
113
+ truncated: false,
114
+ originalBytes: totalBytes,
115
+ originalLines: lines.length
116
+ };
117
+ }
118
+ const out = [];
119
+ let bytes = 0;
120
+ let hitBytes = false;
121
+ if (direction === "head") {
122
+ for (let i = 0;i < lines.length && i < maxLines; i++) {
123
+ const lineBytes = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0);
124
+ if (bytes + lineBytes > maxBytes) {
125
+ hitBytes = true;
126
+ break;
127
+ }
128
+ out.push(lines[i]);
129
+ bytes += lineBytes;
130
+ }
131
+ } else {
132
+ for (let i = lines.length - 1;i >= 0 && out.length < maxLines; i--) {
133
+ const lineBytes = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0);
134
+ if (bytes + lineBytes > maxBytes) {
135
+ hitBytes = true;
136
+ break;
137
+ }
138
+ out.unshift(lines[i]);
139
+ bytes += lineBytes;
140
+ }
141
+ }
142
+ const removed = hitBytes ? totalBytes - bytes : lines.length - out.length;
143
+ const unit = hitBytes ? "bytes" : "lines";
144
+ const preview = out.join(`
145
+ `);
146
+ let outputPath;
147
+ if (saveToFile) {
148
+ const dir = getTempDir(config);
149
+ await ensureTempDir(dir);
150
+ outputPath = path.join(dir, generateOutputFilename());
151
+ await fs.writeFile(outputPath, text, "utf-8");
152
+ }
153
+ const hint = outputPath ? `Full output saved to: ${outputPath}
154
+ Use Read tool with offset/limit to view specific sections, or Grep to search the content.` : "Output was truncated. Consider using more specific queries.";
155
+ const message = direction === "head" ? `${preview}
156
+
157
+ ...${removed} ${unit} truncated...
158
+
159
+ ${hint}` : `...${removed} ${unit} truncated...
160
+
161
+ ${hint}
162
+
163
+ ${preview}`;
164
+ return {
165
+ content: message,
166
+ truncated: true,
167
+ outputPath,
168
+ originalBytes: totalBytes,
169
+ originalLines: lines.length,
170
+ truncatedBytes: bytes,
171
+ truncatedLines: out.length
172
+ };
173
+ }
174
+ async function truncateToolOutput(output, config) {
175
+ const result = await truncateOutput(output, config);
176
+ return result.content;
177
+ }
178
+ function needsTruncation(text, config = {}) {
179
+ const maxLines = config.maxLines ?? TRUNCATION_DEFAULTS.MAX_LINES;
180
+ const maxBytes = config.maxBytes ?? TRUNCATION_DEFAULTS.MAX_BYTES;
181
+ const lines = text.split(`
182
+ `);
183
+ const bytes = Buffer.byteLength(text, "utf-8");
184
+ return lines.length > maxLines || bytes > maxBytes;
185
+ }
186
+
79
187
  // src/hooks/manager.ts
80
188
  class HookTimeoutError extends Error {
81
189
  constructor(hookName, timeout) {
@@ -312,7 +420,7 @@ class HooksManager {
312
420
  // src/skills/loader.ts
313
421
  import { existsSync, readdirSync, statSync } from "node:fs";
314
422
  import { readFile } from "node:fs/promises";
315
- import { join, dirname, basename } from "node:path";
423
+ import { join as join2, dirname, basename } from "node:path";
316
424
  import { homedir } from "node:os";
317
425
 
318
426
  // src/utils/frontmatter.ts
@@ -427,13 +535,13 @@ class SkillLoader {
427
535
  } = options;
428
536
  const allDirs = [...directories];
429
537
  if (includeUserSkills) {
430
- const userSkillsDir = join(homedir(), USER_SKILLS_DIR);
538
+ const userSkillsDir = join2(homedir(), USER_SKILLS_DIR);
431
539
  if (existsSync(userSkillsDir)) {
432
540
  allDirs.push(userSkillsDir);
433
541
  }
434
542
  }
435
543
  if (includeProjectSkills && this.projectRoot) {
436
- const projectSkillsDir = join(this.projectRoot, PROJECT_SKILLS_DIR);
544
+ const projectSkillsDir = join2(this.projectRoot, PROJECT_SKILLS_DIR);
437
545
  if (existsSync(projectSkillsDir)) {
438
546
  allDirs.push(projectSkillsDir);
439
547
  }
@@ -449,8 +557,8 @@ class SkillLoader {
449
557
  if (this.skills.has(skillId)) {
450
558
  return this.skills.get(skillId);
451
559
  }
452
- for (const path of this.discoveredPaths) {
453
- const skill = await this.parseSkillFile(path);
560
+ for (const path2 of this.discoveredPaths) {
561
+ const skill = await this.parseSkillFile(path2);
454
562
  if (skill && skill.id === skillId) {
455
563
  this.skills.set(skill.id, skill);
456
564
  return skill;
@@ -550,10 +658,10 @@ class SkillLoader {
550
658
  try {
551
659
  const entries = readdirSync(dir);
552
660
  for (const entry of entries) {
553
- const fullPath = join(dir, entry);
661
+ const fullPath = join2(dir, entry);
554
662
  const stat = statSync(fullPath);
555
663
  if (stat.isDirectory()) {
556
- const skillFile = join(fullPath, SKILL_FILE_NAME);
664
+ const skillFile = join2(fullPath, SKILL_FILE_NAME);
557
665
  if (existsSync(skillFile)) {
558
666
  if (!this.discoveredPaths.has(skillFile)) {
559
667
  const skill = await this.parseSkillFile(skillFile);
@@ -633,9 +741,9 @@ var defaultSkillLoader = new SkillLoader;
633
741
 
634
742
  // src/tools/skill.ts
635
743
  import { existsSync as existsSync2 } from "node:fs";
636
- import { join as join2 } from "node:path";
744
+ import { join as join3 } from "node:path";
637
745
  import { homedir as homedir2 } from "node:os";
638
- var DEFAULT_USER_SKILLS_PATH = join2(homedir2(), ".claude/skills");
746
+ var DEFAULT_USER_SKILLS_PATH = join3(homedir2(), ".claude/skills");
639
747
  var skillToolSchema = {
640
748
  type: "object",
641
749
  properties: {
@@ -662,7 +770,7 @@ function createSkillTool(config = {}) {
662
770
  const paths = [];
663
771
  if (config.settingSources && config.settingSources.length > 0) {
664
772
  for (const source of config.settingSources) {
665
- const resolvedPath = source.startsWith("~") ? join2(homedir2(), source.slice(1)) : source;
773
+ const resolvedPath = source.startsWith("~") ? join3(homedir2(), source.slice(1)) : source;
666
774
  if (existsSync2(resolvedPath)) {
667
775
  paths.push(resolvedPath);
668
776
  }
@@ -1113,7 +1221,7 @@ var defaultSystemPromptBuilder = new SystemPromptBuilderImpl;
1113
1221
  // src/prompt/claude-md.ts
1114
1222
  import { existsSync as existsSync3 } from "node:fs";
1115
1223
  import { readFile as readFile2 } from "node:fs/promises";
1116
- import { join as join3, dirname as dirname2 } from "node:path";
1224
+ import { join as join4, dirname as dirname2 } from "node:path";
1117
1225
  import { homedir as homedir3 } from "node:os";
1118
1226
  var CLAUDE_MD_FILENAME = "CLAUDE.md";
1119
1227
  var USER_CLAUDE_DIR = ".claude";
@@ -1127,7 +1235,7 @@ class ClaudeMdLoaderImpl {
1127
1235
  return this.loadFile(filePath, "project");
1128
1236
  }
1129
1237
  async loadUserClaudeMd() {
1130
- const filePath = join3(homedir3(), USER_CLAUDE_DIR, CLAUDE_MD_FILENAME);
1238
+ const filePath = join4(homedir3(), USER_CLAUDE_DIR, CLAUDE_MD_FILENAME);
1131
1239
  if (!existsSync3(filePath)) {
1132
1240
  return;
1133
1241
  }
@@ -1148,9 +1256,9 @@ class ClaudeMdLoaderImpl {
1148
1256
  }
1149
1257
  }
1150
1258
  if (config.additionalPaths) {
1151
- for (const path of config.additionalPaths) {
1152
- if (existsSync3(path)) {
1153
- const content = await this.loadFile(path, "project");
1259
+ for (const path2 of config.additionalPaths) {
1260
+ if (existsSync3(path2)) {
1261
+ const content = await this.loadFile(path2, "project");
1154
1262
  if (content) {
1155
1263
  contents.push(content);
1156
1264
  }
@@ -1177,11 +1285,11 @@ class ClaudeMdLoaderImpl {
1177
1285
  async findProjectClaudeMd(startDir) {
1178
1286
  let currentDir = startDir;
1179
1287
  while (currentDir !== "/") {
1180
- const claudeMdPath = join3(currentDir, CLAUDE_MD_FILENAME);
1288
+ const claudeMdPath = join4(currentDir, CLAUDE_MD_FILENAME);
1181
1289
  if (existsSync3(claudeMdPath)) {
1182
1290
  return claudeMdPath;
1183
1291
  }
1184
- const gitPath = join3(currentDir, ".git");
1292
+ const gitPath = join4(currentDir, ".git");
1185
1293
  if (existsSync3(gitPath)) {
1186
1294
  break;
1187
1295
  }
@@ -1247,6 +1355,7 @@ class SessionImpl {
1247
1355
  _state;
1248
1356
  provider;
1249
1357
  tools;
1358
+ toolNameLookup = new Map;
1250
1359
  emitter;
1251
1360
  pendingMessage = null;
1252
1361
  isReceiving = false;
@@ -1254,6 +1363,7 @@ class SessionImpl {
1254
1363
  closed = false;
1255
1364
  hooksManager = null;
1256
1365
  maxTurns;
1366
+ enableToolRepair = true;
1257
1367
  constructor(id, config, provider, state) {
1258
1368
  this.id = id;
1259
1369
  this.config = config;
@@ -1276,6 +1386,7 @@ class SessionImpl {
1276
1386
  if (config.tools) {
1277
1387
  for (const tool of config.tools) {
1278
1388
  this.tools.set(tool.name, tool);
1389
+ this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
1279
1390
  }
1280
1391
  }
1281
1392
  if (config.settingSources && config.settingSources.length > 0) {
@@ -1284,6 +1395,7 @@ class SessionImpl {
1284
1395
  cwd: config.cwd
1285
1396
  });
1286
1397
  this.tools.set(skillTool2.name, skillTool2);
1398
+ this.toolNameLookup.set(skillTool2.name.toLowerCase(), skillTool2.name);
1287
1399
  }
1288
1400
  this.applyAllowedToolsFilter();
1289
1401
  if (config.hooks) {
@@ -1567,8 +1679,18 @@ class SessionImpl {
1567
1679
  async executeToolCall(block, abortSignal) {
1568
1680
  let toolInput = block.input;
1569
1681
  let systemMessage;
1682
+ let tool = this.tools.get(block.name);
1683
+ let effectiveToolName = block.name;
1684
+ if (!tool && this.enableToolRepair) {
1685
+ const lowerName = block.name.toLowerCase();
1686
+ const originalName = this.toolNameLookup.get(lowerName);
1687
+ if (originalName) {
1688
+ tool = this.tools.get(originalName);
1689
+ effectiveToolName = originalName;
1690
+ }
1691
+ }
1570
1692
  if (this.hooksManager) {
1571
- const preResult = await this.hooksManager.runPreToolUse(block.name, block.input, block.id, abortSignal);
1693
+ const preResult = await this.hooksManager.runPreToolUse(effectiveToolName, block.input, block.id, abortSignal);
1572
1694
  if (!preResult.continue) {
1573
1695
  return {
1574
1696
  type: "tool_result",
@@ -1582,7 +1704,7 @@ class SessionImpl {
1582
1704
  return {
1583
1705
  type: "tool_result",
1584
1706
  tool_use_id: block.id,
1585
- content: preResult.reason ?? `Tool "${block.name}" was denied by hook`,
1707
+ content: preResult.reason ?? `Tool "${effectiveToolName}" was denied by hook`,
1586
1708
  is_error: true,
1587
1709
  _hookSystemMessage: preResult.systemMessage
1588
1710
  };
@@ -1592,12 +1714,13 @@ class SessionImpl {
1592
1714
  }
1593
1715
  systemMessage = preResult.systemMessage;
1594
1716
  }
1595
- const tool = this.tools.get(block.name);
1596
1717
  if (!tool) {
1718
+ const availableTools = Array.from(this.tools.keys()).slice(0, 10);
1719
+ const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
1597
1720
  return {
1598
1721
  type: "tool_result",
1599
1722
  tool_use_id: block.id,
1600
- content: `Error: Tool "${block.name}" not found`,
1723
+ content: `Error: Tool "${block.name}" not found. Available tools: ${availableTools.join(", ")}${suffix}`,
1601
1724
  is_error: true,
1602
1725
  _hookSystemMessage: systemMessage
1603
1726
  };
@@ -1610,7 +1733,10 @@ class SessionImpl {
1610
1733
  let toolResponse;
1611
1734
  try {
1612
1735
  const toolResult = await tool.execute(toolInput, context);
1613
- const content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
1736
+ let content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
1737
+ if (needsTruncation(content)) {
1738
+ content = await truncateToolOutput(content);
1739
+ }
1614
1740
  toolResponse = toolResult;
1615
1741
  result = {
1616
1742
  type: "tool_result",
@@ -1628,7 +1754,7 @@ class SessionImpl {
1628
1754
  };
1629
1755
  }
1630
1756
  if (this.hooksManager) {
1631
- const postResult = await this.hooksManager.runPostToolUse(block.name, toolInput, toolResponse, block.id, abortSignal);
1757
+ const postResult = await this.hooksManager.runPostToolUse(effectiveToolName, toolInput, toolResponse, block.id, abortSignal);
1632
1758
  if (postResult.systemMessage) {
1633
1759
  systemMessage = postResult.systemMessage;
1634
1760
  }
@@ -2270,7 +2396,7 @@ class OpenAIProvider {
2270
2396
  }
2271
2397
  this.config = {
2272
2398
  apiKey,
2273
- baseUrl: config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
2399
+ baseUrl: this.normalizeBaseUrl(config.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"),
2274
2400
  organization: config.organization,
2275
2401
  defaultMaxTokens: config.defaultMaxTokens ?? 4096
2276
2402
  };
@@ -2279,8 +2405,23 @@ class OpenAIProvider {
2279
2405
  return this.supportedModels.some((pattern) => pattern.test(model));
2280
2406
  }
2281
2407
  async complete(request) {
2408
+ if (this.usesResponsesApi(request.config.model)) {
2409
+ const openaiRequest2 = this.buildResponsesRequest(request, false);
2410
+ const response2 = await fetch(`${this.config.baseUrl}/responses`, {
2411
+ method: "POST",
2412
+ headers: this.getHeaders(),
2413
+ body: JSON.stringify(openaiRequest2),
2414
+ signal: request.abortSignal
2415
+ });
2416
+ if (!response2.ok) {
2417
+ const error = await response2.text();
2418
+ throw new Error(`OpenAI API error: ${response2.status} ${error}`);
2419
+ }
2420
+ const data2 = await response2.json();
2421
+ return this.convertResponsesResponse(data2);
2422
+ }
2282
2423
  const openaiRequest = this.buildRequest(request, false);
2283
- const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
2424
+ let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
2284
2425
  method: "POST",
2285
2426
  headers: this.getHeaders(),
2286
2427
  body: JSON.stringify(openaiRequest),
@@ -2288,14 +2429,43 @@ class OpenAIProvider {
2288
2429
  });
2289
2430
  if (!response.ok) {
2290
2431
  const error = await response.text();
2432
+ if (this.shouldFallbackToResponses(response.status, error)) {
2433
+ const fallbackRequest = this.buildResponsesRequest(request, false);
2434
+ response = await fetch(`${this.config.baseUrl}/responses`, {
2435
+ method: "POST",
2436
+ headers: this.getHeaders(),
2437
+ body: JSON.stringify(fallbackRequest),
2438
+ signal: request.abortSignal
2439
+ });
2440
+ if (!response.ok) {
2441
+ const fallbackError = await response.text();
2442
+ throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
2443
+ }
2444
+ const data2 = await response.json();
2445
+ return this.convertResponsesResponse(data2);
2446
+ }
2291
2447
  throw new Error(`OpenAI API error: ${response.status} ${error}`);
2292
2448
  }
2293
2449
  const data = await response.json();
2294
2450
  return this.convertResponse(data);
2295
2451
  }
2296
2452
  async stream(request, options) {
2453
+ if (this.usesResponsesApi(request.config.model)) {
2454
+ const openaiRequest2 = this.buildResponsesRequest(request, true);
2455
+ const response2 = await fetch(`${this.config.baseUrl}/responses`, {
2456
+ method: "POST",
2457
+ headers: this.getHeaders(),
2458
+ body: JSON.stringify(openaiRequest2),
2459
+ signal: request.abortSignal
2460
+ });
2461
+ if (!response2.ok) {
2462
+ const error = await response2.text();
2463
+ throw new Error(`OpenAI API error: ${response2.status} ${error}`);
2464
+ }
2465
+ return this.createResponsesStreamIterator(response2.body, options);
2466
+ }
2297
2467
  const openaiRequest = this.buildRequest(request, true);
2298
- const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
2468
+ let response = await fetch(`${this.config.baseUrl}/chat/completions`, {
2299
2469
  method: "POST",
2300
2470
  headers: this.getHeaders(),
2301
2471
  body: JSON.stringify(openaiRequest),
@@ -2303,6 +2473,20 @@ class OpenAIProvider {
2303
2473
  });
2304
2474
  if (!response.ok) {
2305
2475
  const error = await response.text();
2476
+ if (this.shouldFallbackToResponses(response.status, error)) {
2477
+ const fallbackRequest = this.buildResponsesRequest(request, true);
2478
+ response = await fetch(`${this.config.baseUrl}/responses`, {
2479
+ method: "POST",
2480
+ headers: this.getHeaders(),
2481
+ body: JSON.stringify(fallbackRequest),
2482
+ signal: request.abortSignal
2483
+ });
2484
+ if (!response.ok) {
2485
+ const fallbackError = await response.text();
2486
+ throw new Error(`OpenAI API error: ${response.status} ${fallbackError}`);
2487
+ }
2488
+ return this.createResponsesStreamIterator(response.body, options);
2489
+ }
2306
2490
  throw new Error(`OpenAI API error: ${response.status} ${error}`);
2307
2491
  }
2308
2492
  return this.createStreamIterator(response.body, options);
@@ -2310,10 +2494,10 @@ class OpenAIProvider {
2310
2494
  buildRequest(request, stream) {
2311
2495
  const messages = this.convertMessages(request.messages, request.systemPrompt);
2312
2496
  const tools = request.tools ? this.convertTools(request.tools) : undefined;
2313
- return {
2497
+ const maxTokens = request.config.maxTokens ?? this.config.defaultMaxTokens;
2498
+ const openaiRequest = {
2314
2499
  model: request.config.model,
2315
2500
  messages,
2316
- max_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
2317
2501
  temperature: request.config.temperature,
2318
2502
  top_p: request.config.topP,
2319
2503
  stop: request.config.stopSequences,
@@ -2321,6 +2505,162 @@ class OpenAIProvider {
2321
2505
  stream_options: stream ? { include_usage: true } : undefined,
2322
2506
  tools
2323
2507
  };
2508
+ if (this.usesMaxCompletionTokens(request.config.model)) {
2509
+ openaiRequest.max_completion_tokens = maxTokens;
2510
+ } else {
2511
+ openaiRequest.max_tokens = maxTokens;
2512
+ }
2513
+ return openaiRequest;
2514
+ }
2515
+ buildResponsesRequest(request, stream) {
2516
+ const input = this.convertResponsesInput(request.messages, request.systemPrompt);
2517
+ const tools = request.tools ? this.convertResponsesTools(request.tools) : undefined;
2518
+ return {
2519
+ model: request.config.model,
2520
+ input,
2521
+ max_output_tokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
2522
+ temperature: request.config.temperature,
2523
+ top_p: request.config.topP,
2524
+ stop: request.config.stopSequences,
2525
+ stream,
2526
+ tools
2527
+ };
2528
+ }
2529
+ usesMaxCompletionTokens(model) {
2530
+ return /^gpt-5/.test(model) || /^o1/.test(model);
2531
+ }
2532
+ usesResponsesApi(model) {
2533
+ return /^gpt-5/.test(model) || /^o1/.test(model);
2534
+ }
2535
+ shouldFallbackToResponses(status, errorText) {
2536
+ if (status !== 404) {
2537
+ return false;
2538
+ }
2539
+ const normalized = errorText.toLowerCase();
2540
+ return normalized.includes("/chat/completions") && normalized.includes("not found");
2541
+ }
2542
+ convertResponsesInput(messages, systemPrompt) {
2543
+ const input = [];
2544
+ if (systemPrompt) {
2545
+ input.push({ role: "system", content: systemPrompt });
2546
+ }
2547
+ for (const msg of messages) {
2548
+ if (msg.role === "system") {
2549
+ input.push({
2550
+ role: "system",
2551
+ content: typeof msg.content === "string" ? msg.content : ""
2552
+ });
2553
+ continue;
2554
+ }
2555
+ if (typeof msg.content === "string") {
2556
+ if (msg.role === "user") {
2557
+ input.push({
2558
+ role: "user",
2559
+ content: [{ type: "input_text", text: msg.content }]
2560
+ });
2561
+ } else {
2562
+ input.push({
2563
+ role: "assistant",
2564
+ content: [{ type: "output_text", text: msg.content }]
2565
+ });
2566
+ }
2567
+ continue;
2568
+ }
2569
+ const userContent = [];
2570
+ const assistantContent = [];
2571
+ for (const block of msg.content) {
2572
+ if (block.type === "text") {
2573
+ if (msg.role === "user") {
2574
+ userContent.push({ type: "input_text", text: block.text });
2575
+ } else if (msg.role === "assistant") {
2576
+ assistantContent.push({ type: "output_text", text: block.text });
2577
+ }
2578
+ } else if (block.type === "image" && msg.role === "user") {
2579
+ if (block.source.type === "base64") {
2580
+ userContent.push({
2581
+ type: "input_image",
2582
+ image_url: `data:${block.source.media_type};base64,${block.source.data}`
2583
+ });
2584
+ } else if (block.source.type === "url") {
2585
+ userContent.push({
2586
+ type: "input_image",
2587
+ image_url: block.source.url
2588
+ });
2589
+ }
2590
+ } else if (block.type === "tool_use") {
2591
+ input.push({
2592
+ type: "function_call",
2593
+ call_id: block.id,
2594
+ name: block.name,
2595
+ arguments: JSON.stringify(block.input)
2596
+ });
2597
+ } else if (block.type === "tool_result") {
2598
+ input.push({
2599
+ type: "function_call_output",
2600
+ call_id: block.tool_use_id,
2601
+ output: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
2602
+ });
2603
+ }
2604
+ }
2605
+ if (msg.role === "user" && userContent.length > 0) {
2606
+ input.push({ role: "user", content: userContent });
2607
+ } else if (msg.role === "assistant" && assistantContent.length > 0) {
2608
+ input.push({ role: "assistant", content: assistantContent });
2609
+ }
2610
+ }
2611
+ return input;
2612
+ }
2613
+ convertResponsesResponse(data) {
2614
+ const content = [];
2615
+ for (const item of data.output ?? []) {
2616
+ if (item.type === "message" && item.content) {
2617
+ for (const part of item.content) {
2618
+ if (part.type === "output_text") {
2619
+ content.push({ type: "text", text: part.text });
2620
+ }
2621
+ }
2622
+ } else if (item.type === "function_call" && item.call_id && item.name) {
2623
+ content.push({
2624
+ type: "tool_use",
2625
+ id: item.call_id,
2626
+ name: item.name,
2627
+ input: item.arguments ? JSON.parse(item.arguments) : {}
2628
+ });
2629
+ }
2630
+ }
2631
+ return {
2632
+ id: data.id,
2633
+ model: data.model,
2634
+ content,
2635
+ stopReason: "end_turn",
2636
+ stopSequence: null,
2637
+ usage: {
2638
+ input_tokens: data.usage?.input_tokens ?? 0,
2639
+ output_tokens: data.usage?.output_tokens ?? 0
2640
+ }
2641
+ };
2642
+ }
2643
+ normalizeBaseUrl(baseUrl) {
2644
+ const trimmed = baseUrl.replace(/\/+$/, "");
2645
+ try {
2646
+ const url = new URL(trimmed);
2647
+ const path2 = url.pathname.replace(/\/+$/, "");
2648
+ if (path2 === "" || path2 === "/") {
2649
+ url.pathname = "/v1";
2650
+ return url.toString().replace(/\/+$/, "");
2651
+ }
2652
+ if (path2.endsWith("/openai")) {
2653
+ url.pathname = `${path2}/v1`;
2654
+ return url.toString().replace(/\/+$/, "");
2655
+ }
2656
+ if (!/\/v\d/.test(path2)) {
2657
+ url.pathname = `${path2}/v1`;
2658
+ return url.toString().replace(/\/+$/, "");
2659
+ }
2660
+ return url.toString().replace(/\/+$/, "");
2661
+ } catch {
2662
+ return trimmed;
2663
+ }
2324
2664
  }
2325
2665
  convertMessages(messages, systemPrompt) {
2326
2666
  const result = [];
@@ -2403,6 +2743,14 @@ class OpenAIProvider {
2403
2743
  }
2404
2744
  }));
2405
2745
  }
2746
+ convertResponsesTools(tools) {
2747
+ return tools.map((tool) => ({
2748
+ type: "function",
2749
+ name: tool.name,
2750
+ description: tool.description,
2751
+ parameters: tool.inputSchema
2752
+ }));
2753
+ }
2406
2754
  convertResponse(data) {
2407
2755
  const choice = data.choices[0];
2408
2756
  const content = [];
@@ -2615,6 +2963,221 @@ class OpenAIProvider {
2615
2963
  }
2616
2964
  };
2617
2965
  }
2966
+ createResponsesStreamIterator(body, options) {
2967
+ const self = this;
2968
+ return {
2969
+ async* [Symbol.asyncIterator]() {
2970
+ const reader = body.getReader();
2971
+ const decoder = new TextDecoder;
2972
+ let buffer = "";
2973
+ let emittedMessageStart = false;
2974
+ let textBlockStarted = false;
2975
+ let finished = false;
2976
+ const toolCalls = new Map;
2977
+ let nextToolBlockIndex = 1;
2978
+ const ensureMessageStart = (id, model) => {
2979
+ if (emittedMessageStart)
2980
+ return;
2981
+ emittedMessageStart = true;
2982
+ const startEvent = {
2983
+ type: "message_start",
2984
+ message: {
2985
+ id: id ?? "",
2986
+ type: "message",
2987
+ role: "assistant",
2988
+ content: [],
2989
+ model: model ?? "",
2990
+ stop_reason: null,
2991
+ stop_sequence: null,
2992
+ usage: { input_tokens: 0, output_tokens: 0 }
2993
+ }
2994
+ };
2995
+ options?.onEvent?.(startEvent);
2996
+ return startEvent;
2997
+ };
2998
+ try {
2999
+ while (true) {
3000
+ const { done, value } = await reader.read();
3001
+ if (done)
3002
+ break;
3003
+ buffer += decoder.decode(value, { stream: true });
3004
+ const lines = buffer.split(`
3005
+ `);
3006
+ buffer = lines.pop() || "";
3007
+ for (const line of lines) {
3008
+ if (!line.startsWith("data: "))
3009
+ continue;
3010
+ const data = line.slice(6).trim();
3011
+ if (!data)
3012
+ continue;
3013
+ if (data === "[DONE]") {
3014
+ if (!finished) {
3015
+ const stopEvent = { type: "message_stop" };
3016
+ options?.onEvent?.(stopEvent);
3017
+ yield stopEvent;
3018
+ }
3019
+ finished = true;
3020
+ continue;
3021
+ }
3022
+ let payload;
3023
+ try {
3024
+ payload = JSON.parse(data);
3025
+ } catch {
3026
+ continue;
3027
+ }
3028
+ const type = payload?.type;
3029
+ if (type === "response.created") {
3030
+ const startEvent = ensureMessageStart(payload.response?.id, payload.response?.model);
3031
+ if (startEvent)
3032
+ yield startEvent;
3033
+ continue;
3034
+ }
3035
+ if (!emittedMessageStart) {
3036
+ const startEvent = ensureMessageStart(payload?.response?.id, payload?.response?.model);
3037
+ if (startEvent)
3038
+ yield startEvent;
3039
+ }
3040
+ if (type === "response.output_text.delta") {
3041
+ if (!textBlockStarted) {
3042
+ textBlockStarted = true;
3043
+ const startText = {
3044
+ type: "content_block_start",
3045
+ index: 0,
3046
+ content_block: { type: "text", text: "" }
3047
+ };
3048
+ options?.onEvent?.(startText);
3049
+ yield startText;
3050
+ }
3051
+ const textDelta = payload.delta ?? "";
3052
+ if (textDelta) {
3053
+ const textEvent = {
3054
+ type: "content_block_delta",
3055
+ index: 0,
3056
+ delta: { type: "text_delta", text: textDelta }
3057
+ };
3058
+ options?.onText?.(textDelta);
3059
+ options?.onEvent?.(textEvent);
3060
+ yield textEvent;
3061
+ }
3062
+ } else if (type === "response.output_item.added") {
3063
+ const item = payload.item;
3064
+ if (item?.type === "function_call") {
3065
+ const blockIndex = nextToolBlockIndex++;
3066
+ const callId = item.call_id ?? item.id ?? "";
3067
+ toolCalls.set(item.id, {
3068
+ callId,
3069
+ name: item.name ?? "",
3070
+ arguments: item.arguments ?? "",
3071
+ blockIndex,
3072
+ done: false
3073
+ });
3074
+ const startEvent = {
3075
+ type: "content_block_start",
3076
+ index: blockIndex,
3077
+ content_block: {
3078
+ type: "tool_use",
3079
+ id: callId,
3080
+ name: item.name ?? "",
3081
+ input: {}
3082
+ }
3083
+ };
3084
+ options?.onEvent?.(startEvent);
3085
+ yield startEvent;
3086
+ if (item.arguments) {
3087
+ const deltaEvent = {
3088
+ type: "content_block_delta",
3089
+ index: blockIndex,
3090
+ delta: {
3091
+ type: "input_json_delta",
3092
+ partial_json: item.arguments
3093
+ }
3094
+ };
3095
+ options?.onEvent?.(deltaEvent);
3096
+ yield deltaEvent;
3097
+ }
3098
+ }
3099
+ } else if (type === "response.function_call_arguments.delta") {
3100
+ const entry = toolCalls.get(payload.item_id);
3101
+ if (entry && payload.delta) {
3102
+ entry.arguments += payload.delta;
3103
+ const deltaEvent = {
3104
+ type: "content_block_delta",
3105
+ index: entry.blockIndex,
3106
+ delta: { type: "input_json_delta", partial_json: payload.delta }
3107
+ };
3108
+ options?.onEvent?.(deltaEvent);
3109
+ yield deltaEvent;
3110
+ }
3111
+ } else if (type === "response.output_item.done") {
3112
+ const item = payload.item;
3113
+ if (item?.type === "function_call") {
3114
+ const entry = toolCalls.get(item.id);
3115
+ if (entry && !entry.done) {
3116
+ entry.done = true;
3117
+ const stopEvent = {
3118
+ type: "content_block_stop",
3119
+ index: entry.blockIndex
3120
+ };
3121
+ options?.onEvent?.(stopEvent);
3122
+ yield stopEvent;
3123
+ try {
3124
+ const input = entry.arguments ? JSON.parse(entry.arguments) : {};
3125
+ options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
3126
+ } catch {
3127
+ options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
3128
+ }
3129
+ }
3130
+ }
3131
+ } else if (type === "response.completed" || type === "response.incomplete") {
3132
+ finished = true;
3133
+ if (textBlockStarted) {
3134
+ const stopText = { type: "content_block_stop", index: 0 };
3135
+ options?.onEvent?.(stopText);
3136
+ yield stopText;
3137
+ }
3138
+ for (const entry of toolCalls.values()) {
3139
+ if (entry.done)
3140
+ continue;
3141
+ entry.done = true;
3142
+ const stopEvent2 = {
3143
+ type: "content_block_stop",
3144
+ index: entry.blockIndex
3145
+ };
3146
+ options?.onEvent?.(stopEvent2);
3147
+ yield stopEvent2;
3148
+ try {
3149
+ const input = entry.arguments ? JSON.parse(entry.arguments) : {};
3150
+ options?.onToolUse?.({ id: entry.callId, name: entry.name, input });
3151
+ } catch {
3152
+ options?.onToolUse?.({ id: entry.callId, name: entry.name, input: {} });
3153
+ }
3154
+ }
3155
+ const finishReason = payload.response?.incomplete_details?.reason;
3156
+ const messageDelta = {
3157
+ type: "message_delta",
3158
+ delta: {
3159
+ stop_reason: self.convertResponsesStopReason(finishReason),
3160
+ stop_sequence: null
3161
+ },
3162
+ usage: {
3163
+ output_tokens: payload.response?.usage?.output_tokens ?? 0,
3164
+ input_tokens: payload.response?.usage?.input_tokens ?? 0
3165
+ }
3166
+ };
3167
+ options?.onEvent?.(messageDelta);
3168
+ yield messageDelta;
3169
+ const stopEvent = { type: "message_stop" };
3170
+ options?.onEvent?.(stopEvent);
3171
+ yield stopEvent;
3172
+ }
3173
+ }
3174
+ }
3175
+ } finally {
3176
+ reader.releaseLock();
3177
+ }
3178
+ }
3179
+ };
3180
+ }
2618
3181
  getHeaders() {
2619
3182
  const headers = {
2620
3183
  "Content-Type": "application/json",
@@ -2625,71 +3188,675 @@ class OpenAIProvider {
2625
3188
  }
2626
3189
  return headers;
2627
3190
  }
2628
- }
2629
-
2630
- // src/api.ts
2631
- var globalManager = null;
2632
- var defaultProvider = null;
2633
- var defaultStorage = null;
2634
- function getGlobalManager() {
2635
- if (!globalManager) {
2636
- if (!defaultProvider) {
2637
- if (process.env.ANTHROPIC_API_KEY) {
2638
- defaultProvider = new AnthropicProvider;
2639
- } else if (process.env.OPENAI_API_KEY) {
2640
- defaultProvider = new OpenAIProvider({
2641
- apiKey: process.env.OPENAI_API_KEY,
2642
- baseUrl: process.env.OPENAI_BASE_URL
2643
- });
2644
- } else {
2645
- throw new Error("No default provider set. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
2646
- }
2647
- }
2648
- globalManager = new SessionManagerImpl({
2649
- provider: defaultProvider,
2650
- storage: defaultStorage ?? new MemorySessionStorage
2651
- });
2652
- }
2653
- return globalManager;
2654
- }
2655
- async function createSession(options) {
2656
- if (options?.provider || options?.sessionStorage) {
2657
- let provider = options?.provider ?? defaultProvider;
2658
- if (!provider) {
2659
- if (process.env.ANTHROPIC_API_KEY) {
2660
- provider = new AnthropicProvider;
2661
- } else if (process.env.OPENAI_API_KEY) {
2662
- provider = new OpenAIProvider({
2663
- apiKey: process.env.OPENAI_API_KEY,
2664
- baseUrl: process.env.OPENAI_BASE_URL
2665
- });
2666
- } else {
2667
- throw new Error("No provider available. Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
2668
- }
3191
+ convertResponsesStopReason(reason) {
3192
+ if (reason === "max_output_tokens") {
3193
+ return "max_tokens";
2669
3194
  }
2670
- const customManager = new SessionManagerImpl({
2671
- provider,
2672
- storage: options?.sessionStorage ?? defaultStorage ?? new MemorySessionStorage
2673
- });
2674
- return customManager.create(options);
3195
+ return "end_turn";
2675
3196
  }
2676
- const manager = getGlobalManager();
2677
- return manager.create(options);
2678
3197
  }
2679
3198
 
2680
- // src/tools/registry.ts
2681
- class ToolRegistry {
2682
- tools = new Map;
2683
- register(tool) {
2684
- this.tools.set(tool.id, tool);
2685
- }
2686
- unregister(toolId) {
2687
- this.tools.delete(toolId);
3199
+ // src/llm/gemini.ts
3200
+ class GeminiProvider {
3201
+ id = "gemini";
3202
+ name = "Gemini";
3203
+ supportedModels = [/^gemini-/, /^models\/gemini-/];
3204
+ config;
3205
+ constructor(config = {}) {
3206
+ const apiKey = config.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
3207
+ if (!apiKey) {
3208
+ throw new Error("Gemini API key is required. Set GEMINI_API_KEY/GOOGLE_API_KEY or pass apiKey in config.");
3209
+ }
3210
+ this.config = {
3211
+ apiKey,
3212
+ baseUrl: config.baseUrl ?? process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta",
3213
+ defaultMaxTokens: config.defaultMaxTokens ?? 4096
3214
+ };
2688
3215
  }
2689
- get(toolId) {
2690
- return this.tools.get(toolId);
3216
+ supportsModel(model) {
3217
+ return this.supportedModels.some((pattern) => pattern.test(model));
2691
3218
  }
2692
- getAll() {
3219
+ async complete(request) {
3220
+ const geminiRequest = this.buildRequest(request);
3221
+ const url = this.buildUrl(this.getModelPath(request.config.model) + ":generateContent");
3222
+ const response = await fetch(url, {
3223
+ method: "POST",
3224
+ headers: this.getHeaders(),
3225
+ body: JSON.stringify(geminiRequest),
3226
+ signal: request.abortSignal
3227
+ });
3228
+ if (!response.ok) {
3229
+ const error = await response.text();
3230
+ throw new Error(`Gemini API error: ${response.status} ${error}`);
3231
+ }
3232
+ const data = await response.json();
3233
+ return this.convertResponse(data, request.config.model);
3234
+ }
3235
+ async stream(request, options) {
3236
+ const geminiRequest = this.buildRequest(request);
3237
+ const url = this.buildUrl(this.getModelPath(request.config.model) + ":streamGenerateContent", {
3238
+ alt: "sse"
3239
+ });
3240
+ const response = await fetch(url, {
3241
+ method: "POST",
3242
+ headers: this.getHeaders(),
3243
+ body: JSON.stringify(geminiRequest),
3244
+ signal: request.abortSignal
3245
+ });
3246
+ if (!response.ok) {
3247
+ const error = await response.text();
3248
+ throw new Error(`Gemini API error: ${response.status} ${error}`);
3249
+ }
3250
+ const contentType = response.headers.get("content-type") ?? "";
3251
+ if (!contentType.includes("text/event-stream")) {
3252
+ const data = await response.json();
3253
+ return this.createResponseIterator(data, options, request.config.model);
3254
+ }
3255
+ return this.createStreamIterator(response.body, options, request.config.model);
3256
+ }
3257
+ buildRequest(request) {
3258
+ const { contents, systemInstruction } = this.convertMessages(request.messages, request.systemPrompt);
3259
+ const tools = request.tools ? this.convertTools(request.tools) : undefined;
3260
+ return {
3261
+ contents,
3262
+ systemInstruction,
3263
+ generationConfig: {
3264
+ temperature: request.config.temperature,
3265
+ topP: request.config.topP,
3266
+ maxOutputTokens: request.config.maxTokens ?? this.config.defaultMaxTokens,
3267
+ stopSequences: request.config.stopSequences
3268
+ },
3269
+ tools,
3270
+ toolConfig: tools ? { functionCallingConfig: { mode: "AUTO" } } : undefined
3271
+ };
3272
+ }
3273
+ convertMessages(messages, systemPrompt) {
3274
+ const contents = [];
3275
+ const systemTexts = [];
3276
+ const toolNameById = new Map;
3277
+ for (const msg of messages) {
3278
+ if (typeof msg.content !== "string") {
3279
+ for (const block of msg.content) {
3280
+ if (block.type === "tool_use") {
3281
+ toolNameById.set(block.id, block.name);
3282
+ }
3283
+ }
3284
+ }
3285
+ }
3286
+ if (systemPrompt) {
3287
+ systemTexts.push(systemPrompt);
3288
+ }
3289
+ for (const msg of messages) {
3290
+ if (msg.role === "system") {
3291
+ if (typeof msg.content === "string") {
3292
+ systemTexts.push(msg.content);
3293
+ } else {
3294
+ for (const block of msg.content) {
3295
+ if (block.type === "text") {
3296
+ systemTexts.push(block.text);
3297
+ }
3298
+ }
3299
+ }
3300
+ continue;
3301
+ }
3302
+ if (typeof msg.content === "string") {
3303
+ contents.push({
3304
+ role: msg.role === "assistant" ? "model" : "user",
3305
+ parts: [{ text: msg.content }]
3306
+ });
3307
+ continue;
3308
+ }
3309
+ const parts = [];
3310
+ const toolResponses = [];
3311
+ for (const block of msg.content) {
3312
+ if (block.type === "text") {
3313
+ parts.push({ text: block.text });
3314
+ } else if (block.type === "image") {
3315
+ if (block.source.type === "base64") {
3316
+ parts.push({
3317
+ inlineData: {
3318
+ mimeType: block.source.media_type ?? "image/jpeg",
3319
+ data: block.source.data ?? ""
3320
+ }
3321
+ });
3322
+ } else if (block.source.type === "url") {
3323
+ parts.push({
3324
+ fileData: {
3325
+ mimeType: block.source.media_type ?? "image/jpeg",
3326
+ fileUri: block.source.url ?? ""
3327
+ }
3328
+ });
3329
+ }
3330
+ } else if (block.type === "tool_result") {
3331
+ const toolName = toolNameById.get(block.tool_use_id) ?? "tool";
3332
+ const output = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
3333
+ toolResponses.push({
3334
+ functionResponse: {
3335
+ name: toolName,
3336
+ response: { output }
3337
+ }
3338
+ });
3339
+ }
3340
+ }
3341
+ if (parts.length > 0) {
3342
+ contents.push({
3343
+ role: msg.role === "assistant" ? "model" : "user",
3344
+ parts
3345
+ });
3346
+ }
3347
+ if (toolResponses.length > 0) {
3348
+ contents.push({
3349
+ role: "user",
3350
+ parts: toolResponses
3351
+ });
3352
+ }
3353
+ }
3354
+ const systemInstruction = systemTexts.length > 0 ? { parts: [{ text: systemTexts.join(`
3355
+
3356
+ `) }] } : undefined;
3357
+ return { contents, systemInstruction };
3358
+ }
3359
+ convertTools(tools) {
3360
+ return [
3361
+ {
3362
+ functionDeclarations: tools.map((tool) => ({
3363
+ name: tool.name,
3364
+ description: tool.description,
3365
+ parameters: this.sanitizeSchema(tool.inputSchema)
3366
+ }))
3367
+ }
3368
+ ];
3369
+ }
3370
+ sanitizeSchema(schema) {
3371
+ const visited = new WeakMap;
3372
+ const scrub = (value) => {
3373
+ if (value === null || typeof value !== "object") {
3374
+ return value;
3375
+ }
3376
+ if (Array.isArray(value)) {
3377
+ return value.map((item) => scrub(item));
3378
+ }
3379
+ const existing = visited.get(value);
3380
+ if (existing) {
3381
+ return existing;
3382
+ }
3383
+ const result = {};
3384
+ visited.set(value, result);
3385
+ for (const [key, inner] of Object.entries(value)) {
3386
+ if (key === "additionalProperties") {
3387
+ continue;
3388
+ }
3389
+ result[key] = scrub(inner);
3390
+ }
3391
+ return result;
3392
+ };
3393
+ return scrub(schema);
3394
+ }
3395
+ convertResponse(data, model) {
3396
+ const candidate = data.candidates?.[0];
3397
+ const content = [];
3398
+ const parts = candidate?.content?.parts ?? [];
3399
+ let toolIndex = 0;
3400
+ for (const part of parts) {
3401
+ if ("text" in part && part.text) {
3402
+ content.push({ type: "text", text: part.text });
3403
+ } else if ("functionCall" in part && part.functionCall) {
3404
+ const callId = `${part.functionCall.name}_${toolIndex++}`;
3405
+ content.push({
3406
+ type: "tool_use",
3407
+ id: callId,
3408
+ name: part.functionCall.name,
3409
+ input: part.functionCall.args ?? {}
3410
+ });
3411
+ }
3412
+ }
3413
+ return {
3414
+ id: "",
3415
+ model: data.model ?? model,
3416
+ content,
3417
+ stopReason: this.convertStopReason(candidate?.finishReason),
3418
+ stopSequence: null,
3419
+ usage: this.convertUsage(data.usageMetadata)
3420
+ };
3421
+ }
3422
+ convertUsage(usage) {
3423
+ return {
3424
+ input_tokens: usage?.promptTokenCount ?? 0,
3425
+ output_tokens: usage?.candidatesTokenCount ?? 0
3426
+ };
3427
+ }
3428
+ convertStopReason(reason) {
3429
+ switch (reason) {
3430
+ case "MAX_TOKENS":
3431
+ return "max_tokens";
3432
+ case "STOP":
3433
+ return "end_turn";
3434
+ default:
3435
+ return "end_turn";
3436
+ }
3437
+ }
3438
+ createStreamIterator(body, options, model) {
3439
+ const self = this;
3440
+ return {
3441
+ async* [Symbol.asyncIterator]() {
3442
+ const reader = body.getReader();
3443
+ const decoder = new TextDecoder;
3444
+ let buffer = "";
3445
+ let emittedMessageStart = false;
3446
+ let textBlockStarted = false;
3447
+ let finished = false;
3448
+ let toolIndex = 0;
3449
+ let emittedAny = false;
3450
+ const emitMessageStart = (modelId) => {
3451
+ if (emittedMessageStart)
3452
+ return;
3453
+ emittedMessageStart = true;
3454
+ const startEvent = {
3455
+ type: "message_start",
3456
+ message: {
3457
+ id: "",
3458
+ type: "message",
3459
+ role: "assistant",
3460
+ content: [],
3461
+ model: modelId,
3462
+ stop_reason: null,
3463
+ stop_sequence: null,
3464
+ usage: { input_tokens: 0, output_tokens: 0 }
3465
+ }
3466
+ };
3467
+ options?.onEvent?.(startEvent);
3468
+ return startEvent;
3469
+ };
3470
+ try {
3471
+ while (true) {
3472
+ const { done, value } = await reader.read();
3473
+ if (done)
3474
+ break;
3475
+ buffer += decoder.decode(value, { stream: true });
3476
+ const lines = buffer.split(`
3477
+ `);
3478
+ buffer = lines.pop() || "";
3479
+ for (const line of lines) {
3480
+ const trimmed = line.trim();
3481
+ if (!trimmed)
3482
+ continue;
3483
+ let jsonText = trimmed;
3484
+ if (trimmed.startsWith("data:")) {
3485
+ jsonText = trimmed.slice(5).trim();
3486
+ }
3487
+ let payload;
3488
+ try {
3489
+ payload = JSON.parse(jsonText);
3490
+ } catch {
3491
+ continue;
3492
+ }
3493
+ const startEvent = emitMessageStart(payload.model ?? model);
3494
+ if (startEvent) {
3495
+ yield startEvent;
3496
+ emittedAny = true;
3497
+ }
3498
+ const candidate = payload.candidates?.[0];
3499
+ const parts = candidate?.content?.parts ?? [];
3500
+ for (const part of parts) {
3501
+ if ("text" in part && part.text) {
3502
+ if (!textBlockStarted) {
3503
+ textBlockStarted = true;
3504
+ const startText = {
3505
+ type: "content_block_start",
3506
+ index: 0,
3507
+ content_block: { type: "text", text: "" }
3508
+ };
3509
+ options?.onEvent?.(startText);
3510
+ yield startText;
3511
+ emittedAny = true;
3512
+ }
3513
+ const textEvent = {
3514
+ type: "content_block_delta",
3515
+ index: 0,
3516
+ delta: { type: "text_delta", text: part.text }
3517
+ };
3518
+ options?.onText?.(part.text);
3519
+ options?.onEvent?.(textEvent);
3520
+ yield textEvent;
3521
+ emittedAny = true;
3522
+ } else if ("functionCall" in part && part.functionCall) {
3523
+ const callId = `${part.functionCall.name}_${toolIndex}`;
3524
+ const blockIndex = 1 + toolIndex;
3525
+ toolIndex += 1;
3526
+ const startTool = {
3527
+ type: "content_block_start",
3528
+ index: blockIndex,
3529
+ content_block: {
3530
+ type: "tool_use",
3531
+ id: callId,
3532
+ name: part.functionCall.name,
3533
+ input: {}
3534
+ }
3535
+ };
3536
+ options?.onEvent?.(startTool);
3537
+ yield startTool;
3538
+ emittedAny = true;
3539
+ const args = JSON.stringify(part.functionCall.args ?? {});
3540
+ if (args) {
3541
+ const deltaEvent = {
3542
+ type: "content_block_delta",
3543
+ index: blockIndex,
3544
+ delta: { type: "input_json_delta", partial_json: args }
3545
+ };
3546
+ options?.onEvent?.(deltaEvent);
3547
+ yield deltaEvent;
3548
+ emittedAny = true;
3549
+ }
3550
+ const stopTool = {
3551
+ type: "content_block_stop",
3552
+ index: blockIndex
3553
+ };
3554
+ options?.onEvent?.(stopTool);
3555
+ yield stopTool;
3556
+ emittedAny = true;
3557
+ options?.onToolUse?.({
3558
+ id: callId,
3559
+ name: part.functionCall.name,
3560
+ input: part.functionCall.args ?? {}
3561
+ });
3562
+ emittedAny = true;
3563
+ }
3564
+ }
3565
+ if (candidate?.finishReason && !finished) {
3566
+ finished = true;
3567
+ if (textBlockStarted) {
3568
+ const stopText = { type: "content_block_stop", index: 0 };
3569
+ options?.onEvent?.(stopText);
3570
+ yield stopText;
3571
+ }
3572
+ const messageDelta = {
3573
+ type: "message_delta",
3574
+ delta: {
3575
+ stop_reason: self.convertStopReason(candidate.finishReason),
3576
+ stop_sequence: null
3577
+ },
3578
+ usage: self.convertUsage(payload.usageMetadata)
3579
+ };
3580
+ options?.onEvent?.(messageDelta);
3581
+ yield messageDelta;
3582
+ emittedAny = true;
3583
+ const stopEvent = { type: "message_stop" };
3584
+ options?.onEvent?.(stopEvent);
3585
+ yield stopEvent;
3586
+ emittedAny = true;
3587
+ }
3588
+ }
3589
+ }
3590
+ } finally {
3591
+ reader.releaseLock();
3592
+ }
3593
+ if (!emittedAny) {
3594
+ const trimmed = buffer.trim();
3595
+ if (trimmed) {
3596
+ try {
3597
+ const parsed = JSON.parse(trimmed);
3598
+ const responses = Array.isArray(parsed) ? parsed : [parsed];
3599
+ for (const payload of responses) {
3600
+ const startEvent = emitMessageStart(payload.model ?? model);
3601
+ if (startEvent) {
3602
+ yield startEvent;
3603
+ }
3604
+ const candidate = payload.candidates?.[0];
3605
+ const parts = candidate?.content?.parts ?? [];
3606
+ for (const part of parts) {
3607
+ if ("text" in part && part.text) {
3608
+ if (!textBlockStarted) {
3609
+ textBlockStarted = true;
3610
+ const startText = {
3611
+ type: "content_block_start",
3612
+ index: 0,
3613
+ content_block: { type: "text", text: "" }
3614
+ };
3615
+ options?.onEvent?.(startText);
3616
+ yield startText;
3617
+ }
3618
+ const textEvent = {
3619
+ type: "content_block_delta",
3620
+ index: 0,
3621
+ delta: { type: "text_delta", text: part.text }
3622
+ };
3623
+ options?.onText?.(part.text);
3624
+ options?.onEvent?.(textEvent);
3625
+ yield textEvent;
3626
+ }
3627
+ }
3628
+ if (textBlockStarted) {
3629
+ const stopText = { type: "content_block_stop", index: 0 };
3630
+ options?.onEvent?.(stopText);
3631
+ yield stopText;
3632
+ }
3633
+ const messageDelta = {
3634
+ type: "message_delta",
3635
+ delta: {
3636
+ stop_reason: self.convertStopReason(candidate?.finishReason),
3637
+ stop_sequence: null
3638
+ },
3639
+ usage: self.convertUsage(payload.usageMetadata)
3640
+ };
3641
+ options?.onEvent?.(messageDelta);
3642
+ yield messageDelta;
3643
+ const stopEvent = { type: "message_stop" };
3644
+ options?.onEvent?.(stopEvent);
3645
+ yield stopEvent;
3646
+ }
3647
+ return;
3648
+ } catch {}
3649
+ }
3650
+ }
3651
+ if (!finished) {
3652
+ if (textBlockStarted) {
3653
+ const stopText = { type: "content_block_stop", index: 0 };
3654
+ options?.onEvent?.(stopText);
3655
+ yield stopText;
3656
+ }
3657
+ const stopEvent = { type: "message_stop" };
3658
+ options?.onEvent?.(stopEvent);
3659
+ yield stopEvent;
3660
+ }
3661
+ }
3662
+ };
3663
+ }
3664
+ createResponseIterator(data, options, model) {
3665
+ const responses = Array.isArray(data) ? data : [data];
3666
+ const self = this;
3667
+ return {
3668
+ async* [Symbol.asyncIterator]() {
3669
+ for (const payload of responses) {
3670
+ const candidate = payload.candidates?.[0];
3671
+ const parts = candidate?.content?.parts ?? [];
3672
+ let textIndex = 0;
3673
+ let toolIndex = 0;
3674
+ const startEvent = {
3675
+ type: "message_start",
3676
+ message: {
3677
+ id: "",
3678
+ type: "message",
3679
+ role: "assistant",
3680
+ content: [],
3681
+ model: payload.model ?? model,
3682
+ stop_reason: null,
3683
+ stop_sequence: null,
3684
+ usage: { input_tokens: 0, output_tokens: 0 }
3685
+ }
3686
+ };
3687
+ options?.onEvent?.(startEvent);
3688
+ yield startEvent;
3689
+ for (const part of parts) {
3690
+ if ("text" in part && part.text) {
3691
+ const startText = {
3692
+ type: "content_block_start",
3693
+ index: textIndex,
3694
+ content_block: { type: "text", text: "" }
3695
+ };
3696
+ options?.onEvent?.(startText);
3697
+ yield startText;
3698
+ const textEvent = {
3699
+ type: "content_block_delta",
3700
+ index: textIndex,
3701
+ delta: { type: "text_delta", text: part.text }
3702
+ };
3703
+ options?.onText?.(part.text);
3704
+ options?.onEvent?.(textEvent);
3705
+ yield textEvent;
3706
+ const stopText = { type: "content_block_stop", index: textIndex };
3707
+ options?.onEvent?.(stopText);
3708
+ yield stopText;
3709
+ textIndex += 1;
3710
+ } else if ("functionCall" in part && part.functionCall) {
3711
+ const callId = `${part.functionCall.name}_${toolIndex}`;
3712
+ const blockIndex = textIndex + toolIndex + 1;
3713
+ toolIndex += 1;
3714
+ const startTool = {
3715
+ type: "content_block_start",
3716
+ index: blockIndex,
3717
+ content_block: {
3718
+ type: "tool_use",
3719
+ id: callId,
3720
+ name: part.functionCall.name,
3721
+ input: {}
3722
+ }
3723
+ };
3724
+ options?.onEvent?.(startTool);
3725
+ yield startTool;
3726
+ const args = JSON.stringify(part.functionCall.args ?? {});
3727
+ if (args) {
3728
+ const deltaEvent = {
3729
+ type: "content_block_delta",
3730
+ index: blockIndex,
3731
+ delta: { type: "input_json_delta", partial_json: args }
3732
+ };
3733
+ options?.onEvent?.(deltaEvent);
3734
+ yield deltaEvent;
3735
+ }
3736
+ const stopTool = { type: "content_block_stop", index: blockIndex };
3737
+ options?.onEvent?.(stopTool);
3738
+ yield stopTool;
3739
+ options?.onToolUse?.({
3740
+ id: callId,
3741
+ name: part.functionCall.name,
3742
+ input: part.functionCall.args ?? {}
3743
+ });
3744
+ }
3745
+ }
3746
+ const messageDelta = {
3747
+ type: "message_delta",
3748
+ delta: {
3749
+ stop_reason: self.convertStopReason(candidate?.finishReason),
3750
+ stop_sequence: null
3751
+ },
3752
+ usage: self.convertUsage(payload.usageMetadata)
3753
+ };
3754
+ options?.onEvent?.(messageDelta);
3755
+ yield messageDelta;
3756
+ const stopEvent = { type: "message_stop" };
3757
+ options?.onEvent?.(stopEvent);
3758
+ yield stopEvent;
3759
+ }
3760
+ }
3761
+ };
3762
+ }
3763
+ getHeaders() {
3764
+ return {
3765
+ "Content-Type": "application/json",
3766
+ "x-goog-api-key": this.config.apiKey
3767
+ };
3768
+ }
3769
+ buildUrl(path2, params) {
3770
+ const base = this.config.baseUrl.replace(/\/+$/, "");
3771
+ const url = new URL(`${base}/${path2.replace(/^\/+/, "")}`);
3772
+ if (!url.searchParams.has("key")) {
3773
+ url.searchParams.set("key", this.config.apiKey);
3774
+ }
3775
+ if (params) {
3776
+ for (const [key, value] of Object.entries(params)) {
3777
+ url.searchParams.set(key, value);
3778
+ }
3779
+ }
3780
+ return url.toString();
3781
+ }
3782
+ getModelPath(model) {
3783
+ return model.startsWith("models/") ? model : `models/${model}`;
3784
+ }
3785
+ }
3786
+
3787
+ // src/api.ts
3788
+ var globalManager = null;
3789
+ var defaultProvider = null;
3790
+ var defaultStorage = null;
3791
+ function getGlobalManager() {
3792
+ if (!globalManager) {
3793
+ if (!defaultProvider) {
3794
+ if (process.env.ANTHROPIC_API_KEY) {
3795
+ defaultProvider = new AnthropicProvider;
3796
+ } else if (process.env.OPENAI_API_KEY) {
3797
+ defaultProvider = new OpenAIProvider({
3798
+ apiKey: process.env.OPENAI_API_KEY,
3799
+ baseUrl: process.env.OPENAI_BASE_URL
3800
+ });
3801
+ } else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
3802
+ defaultProvider = new GeminiProvider({
3803
+ apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
3804
+ baseUrl: process.env.GEMINI_BASE_URL
3805
+ });
3806
+ } else {
3807
+ throw new Error("No default provider set. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
3808
+ }
3809
+ }
3810
+ globalManager = new SessionManagerImpl({
3811
+ provider: defaultProvider,
3812
+ storage: defaultStorage ?? new MemorySessionStorage
3813
+ });
3814
+ }
3815
+ return globalManager;
3816
+ }
3817
+ async function createSession(options) {
3818
+ if (options?.provider || options?.sessionStorage) {
3819
+ let provider = options?.provider ?? defaultProvider;
3820
+ if (!provider) {
3821
+ if (process.env.ANTHROPIC_API_KEY) {
3822
+ provider = new AnthropicProvider;
3823
+ } else if (process.env.OPENAI_API_KEY) {
3824
+ provider = new OpenAIProvider({
3825
+ apiKey: process.env.OPENAI_API_KEY,
3826
+ baseUrl: process.env.OPENAI_BASE_URL
3827
+ });
3828
+ } else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
3829
+ provider = new GeminiProvider({
3830
+ apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
3831
+ baseUrl: process.env.GEMINI_BASE_URL
3832
+ });
3833
+ } else {
3834
+ throw new Error("No provider available. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable, " + "call setDefaultProvider(), or pass a provider in the options.");
3835
+ }
3836
+ }
3837
+ const customManager = new SessionManagerImpl({
3838
+ provider,
3839
+ storage: options?.sessionStorage ?? defaultStorage ?? new MemorySessionStorage
3840
+ });
3841
+ return customManager.create(options);
3842
+ }
3843
+ const manager = getGlobalManager();
3844
+ return manager.create(options);
3845
+ }
3846
+
3847
+ // src/tools/registry.ts
3848
+ class ToolRegistry {
3849
+ tools = new Map;
3850
+ register(tool) {
3851
+ this.tools.set(tool.id, tool);
3852
+ }
3853
+ unregister(toolId) {
3854
+ this.tools.delete(toolId);
3855
+ }
3856
+ get(toolId) {
3857
+ return this.tools.get(toolId);
3858
+ }
3859
+ getAll() {
2693
3860
  return Array.from(this.tools.values());
2694
3861
  }
2695
3862
  clear() {
@@ -2828,24 +3995,36 @@ var defaultMCPServerManager = new MCPServerManager;
2828
3995
  // src/tools/manager.ts
2829
3996
  class ToolManager {
2830
3997
  tools = new Map;
3998
+ toolNameLookup = new Map;
2831
3999
  mcpServers = new Map;
2832
4000
  allowedTools;
2833
4001
  onToolEvent;
4002
+ enableToolRepair;
2834
4003
  constructor(options = {}) {
2835
4004
  this.allowedTools = options.allowedTools;
2836
4005
  this.onToolEvent = options.onToolEvent;
4006
+ this.enableToolRepair = options.enableToolRepair ?? true;
2837
4007
  }
2838
4008
  register(tool) {
2839
4009
  this.tools.set(tool.name, tool);
4010
+ this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
2840
4011
  }
2841
4012
  unregister(name) {
2842
4013
  this.tools.delete(name);
4014
+ this.toolNameLookup.delete(name.toLowerCase());
2843
4015
  }
2844
4016
  get(name) {
2845
4017
  const tool = this.tools.get(name);
2846
4018
  if (tool) {
2847
4019
  return tool;
2848
4020
  }
4021
+ if (this.enableToolRepair) {
4022
+ const lowerName = name.toLowerCase();
4023
+ const originalName = this.toolNameLookup.get(lowerName);
4024
+ if (originalName) {
4025
+ return this.tools.get(originalName);
4026
+ }
4027
+ }
2849
4028
  if (isMCPTool(name)) {
2850
4029
  const parsed = parseMCPToolName(name);
2851
4030
  if (parsed) {
@@ -2857,6 +4036,34 @@ class ToolManager {
2857
4036
  }
2858
4037
  return;
2859
4038
  }
4039
+ repairToolName(name) {
4040
+ if (this.tools.has(name)) {
4041
+ return { repaired: false, originalName: name };
4042
+ }
4043
+ const lowerName = name.toLowerCase();
4044
+ const originalName = this.toolNameLookup.get(lowerName);
4045
+ if (originalName && originalName !== name) {
4046
+ return {
4047
+ repaired: true,
4048
+ originalName: name,
4049
+ repairedName: originalName
4050
+ };
4051
+ }
4052
+ if (isMCPTool(name)) {
4053
+ const parsed = parseMCPToolName(name);
4054
+ if (parsed) {
4055
+ const wrapper = this.mcpServers.get(parsed.serverName);
4056
+ if (wrapper) {
4057
+ return { repaired: false, originalName: name };
4058
+ }
4059
+ }
4060
+ }
4061
+ return {
4062
+ repaired: false,
4063
+ originalName: name,
4064
+ error: `Tool "${name}" not found. Available tools: ${Array.from(this.tools.keys()).join(", ")}`
4065
+ };
4066
+ }
2860
4067
  getAll() {
2861
4068
  const allTools = [];
2862
4069
  for (const tool of this.tools.values()) {
@@ -2874,6 +4081,7 @@ class ToolManager {
2874
4081
  }
2875
4082
  clear() {
2876
4083
  this.tools.clear();
4084
+ this.toolNameLookup.clear();
2877
4085
  }
2878
4086
  async registerMCPServer(server) {
2879
4087
  const wrapper = new MCPServerWrapper(server);
@@ -2881,6 +4089,7 @@ class ToolManager {
2881
4089
  const tools = await wrapper.getTools();
2882
4090
  for (const tool of tools) {
2883
4091
  this.tools.set(tool.name, tool);
4092
+ this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
2884
4093
  }
2885
4094
  return tools.length;
2886
4095
  }
@@ -2892,6 +4101,7 @@ class ToolManager {
2892
4101
  const parsed = parseMCPToolName(name);
2893
4102
  if (parsed?.serverName === serverName) {
2894
4103
  this.tools.delete(name);
4104
+ this.toolNameLookup.delete(name.toLowerCase());
2895
4105
  }
2896
4106
  }
2897
4107
  }
@@ -2903,9 +4113,23 @@ class ToolManager {
2903
4113
  return Array.from(this.mcpServers.keys());
2904
4114
  }
2905
4115
  async execute(name, input, context) {
2906
- const tool = this.get(name);
4116
+ let effectiveName = name;
4117
+ let tool = this.tools.get(name);
4118
+ if (!tool && this.enableToolRepair) {
4119
+ const repairResult = this.repairToolName(name);
4120
+ if (repairResult.repaired && repairResult.repairedName) {
4121
+ effectiveName = repairResult.repairedName;
4122
+ tool = this.tools.get(effectiveName);
4123
+ console.debug(`[ToolManager] Repaired tool name: "${name}" -> "${effectiveName}"`);
4124
+ }
4125
+ }
4126
+ if (!tool) {
4127
+ tool = this.get(name);
4128
+ }
2907
4129
  if (!tool) {
2908
- throw new Error(`Tool not found: ${name}`);
4130
+ const availableTools = Array.from(this.tools.keys()).slice(0, 10);
4131
+ const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
4132
+ throw new Error(`Tool not found: "${name}". Available tools: ${availableTools.join(", ")}${suffix}`);
2909
4133
  }
2910
4134
  if (!this.isToolAllowed(tool)) {
2911
4135
  throw new Error(`Tool not allowed: ${name}`);
@@ -2914,7 +4138,7 @@ class ToolManager {
2914
4138
  this.emitEvent({
2915
4139
  type: "tool_start",
2916
4140
  toolId,
2917
- toolName: name,
4141
+ toolName: effectiveName,
2918
4142
  input
2919
4143
  });
2920
4144
  try {
@@ -3220,869 +4444,1618 @@ var BINARY_EXTENSIONS = new Set([
3220
4444
  function createReadTool(options = {}) {
3221
4445
  const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
3222
4446
  return {
3223
- name: "Read",
3224
- description: `Read file contents. Supports text files with optional line range. Returns line numbers in output. For images/PDFs, returns metadata only.`,
4447
+ name: "Read",
4448
+ description: `Read file contents. Supports text files with optional line range. Returns line numbers in output. For images/PDFs, returns metadata only.`,
4449
+ inputSchema: {
4450
+ type: "object",
4451
+ properties: {
4452
+ file_path: {
4453
+ type: "string",
4454
+ description: "Absolute path to the file to read"
4455
+ },
4456
+ offset: {
4457
+ type: "number",
4458
+ description: "Line number to start reading from (1-indexed)"
4459
+ },
4460
+ limit: {
4461
+ type: "number",
4462
+ description: "Number of lines to read (default: 2000)"
4463
+ }
4464
+ },
4465
+ required: ["file_path"]
4466
+ },
4467
+ execute: async (rawInput, _context) => {
4468
+ const input = rawInput;
4469
+ const { file_path, offset = 1, limit = DEFAULT_LINE_LIMIT } = input;
4470
+ const access = checkPathAccess(file_path, options, "file");
4471
+ if (!access.ok) {
4472
+ return { content: access.error, isError: true };
4473
+ }
4474
+ if (!existsSync4(access.resolved)) {
4475
+ return {
4476
+ content: `File not found: ${access.resolved}`,
4477
+ isError: true
4478
+ };
4479
+ }
4480
+ const fileStat = await stat(access.resolved);
4481
+ if (fileStat.size > maxFileSize) {
4482
+ return {
4483
+ content: `File too large: ${fileStat.size} bytes (max: ${maxFileSize} bytes)`,
4484
+ isError: true
4485
+ };
4486
+ }
4487
+ const ext = extname(access.resolved).toLowerCase();
4488
+ if (BINARY_EXTENSIONS.has(ext)) {
4489
+ return {
4490
+ content: `Binary file: ${access.resolved}
4491
+ Size: ${fileStat.size} bytes
4492
+ Type: ${ext}`
4493
+ };
4494
+ }
4495
+ try {
4496
+ const content = await readFile3(access.resolved, "utf-8");
4497
+ const lines = content.split(`
4498
+ `);
4499
+ const startLine = Math.max(1, offset);
4500
+ const endLine = Math.min(lines.length, startLine + limit - 1);
4501
+ const outputLines = [];
4502
+ for (let i = startLine - 1;i < endLine; i++) {
4503
+ const lineNum = i + 1;
4504
+ let line = lines[i];
4505
+ if (line.length > MAX_LINE_LENGTH) {
4506
+ line = line.slice(0, MAX_LINE_LENGTH) + "...";
4507
+ }
4508
+ const padding = String(endLine).length;
4509
+ outputLines.push(`${String(lineNum).padStart(padding)}→${line}`);
4510
+ }
4511
+ let header = "";
4512
+ if (startLine > 1 || endLine < lines.length) {
4513
+ header = `[Lines ${startLine}-${endLine} of ${lines.length}]
4514
+
4515
+ `;
4516
+ }
4517
+ return {
4518
+ content: header + outputLines.join(`
4519
+ `)
4520
+ };
4521
+ } catch (error) {
4522
+ return {
4523
+ content: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
4524
+ isError: true
4525
+ };
4526
+ }
4527
+ }
4528
+ };
4529
+ }
4530
+ var ReadTool = createReadTool();
4531
+ // src/tools/builtin/write.ts
4532
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
4533
+ import { existsSync as existsSync5 } from "node:fs";
4534
+ import { dirname as dirname3 } from "node:path";
4535
+ function createWriteTool(options = {}) {
4536
+ return {
4537
+ name: "Write",
4538
+ description: `Write content to a file. Creates parent directories if needed. Overwrites existing files. Use Edit tool for modifying existing files.`,
4539
+ inputSchema: {
4540
+ type: "object",
4541
+ properties: {
4542
+ file_path: {
4543
+ type: "string",
4544
+ description: "Absolute path to the file to write"
4545
+ },
4546
+ content: {
4547
+ type: "string",
4548
+ description: "Content to write to the file"
4549
+ }
4550
+ },
4551
+ required: ["file_path", "content"]
4552
+ },
4553
+ execute: async (rawInput, _context) => {
4554
+ const input = rawInput;
4555
+ const { file_path, content } = input;
4556
+ const access = checkPathAccess(file_path, options, "file");
4557
+ if (!access.ok) {
4558
+ return { content: access.error, isError: true };
4559
+ }
4560
+ try {
4561
+ const dir = dirname3(access.resolved);
4562
+ if (!existsSync5(dir)) {
4563
+ await mkdir2(dir, { recursive: true });
4564
+ }
4565
+ await writeFile2(access.resolved, content, "utf-8");
4566
+ const lines = content.split(`
4567
+ `).length;
4568
+ const bytes = Buffer.byteLength(content, "utf-8");
4569
+ return {
4570
+ content: `Successfully wrote ${bytes} bytes (${lines} lines) to ${access.resolved}`
4571
+ };
4572
+ } catch (error) {
4573
+ return {
4574
+ content: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
4575
+ isError: true
4576
+ };
4577
+ }
4578
+ }
4579
+ };
4580
+ }
4581
+ var WriteTool = createWriteTool();
4582
+ // src/tools/builtin/edit.ts
4583
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
4584
+ import { existsSync as existsSync6 } from "node:fs";
4585
+ function createEditTool(options = {}) {
4586
+ return {
4587
+ name: "Edit",
4588
+ description: `Edit a file by replacing text. Finds old_string and replaces with new_string. The old_string must be unique in the file unless replace_all is true.`,
3225
4589
  inputSchema: {
3226
4590
  type: "object",
3227
4591
  properties: {
3228
4592
  file_path: {
3229
4593
  type: "string",
3230
- description: "Absolute path to the file to read"
4594
+ description: "Absolute path to the file to edit"
3231
4595
  },
3232
- offset: {
3233
- type: "number",
3234
- description: "Line number to start reading from (1-indexed)"
4596
+ old_string: {
4597
+ type: "string",
4598
+ description: "Text to find and replace"
3235
4599
  },
3236
- limit: {
3237
- type: "number",
3238
- description: "Number of lines to read (default: 2000)"
4600
+ new_string: {
4601
+ type: "string",
4602
+ description: "Replacement text"
4603
+ },
4604
+ replace_all: {
4605
+ type: "boolean",
4606
+ description: "Replace all occurrences (default: false)",
4607
+ default: false
3239
4608
  }
3240
4609
  },
3241
- required: ["file_path"]
4610
+ required: ["file_path", "old_string", "new_string"]
3242
4611
  },
3243
4612
  execute: async (rawInput, _context) => {
3244
4613
  const input = rawInput;
3245
- const { file_path, offset = 1, limit = DEFAULT_LINE_LIMIT } = input;
4614
+ const { file_path, old_string, new_string, replace_all = false } = input;
3246
4615
  const access = checkPathAccess(file_path, options, "file");
3247
4616
  if (!access.ok) {
3248
4617
  return { content: access.error, isError: true };
3249
4618
  }
3250
- if (!existsSync4(access.resolved)) {
4619
+ if (!existsSync6(access.resolved)) {
3251
4620
  return {
3252
4621
  content: `File not found: ${access.resolved}`,
3253
4622
  isError: true
3254
4623
  };
3255
4624
  }
3256
- const fileStat = await stat(access.resolved);
3257
- if (fileStat.size > maxFileSize) {
4625
+ if (old_string === new_string) {
3258
4626
  return {
3259
- content: `File too large: ${fileStat.size} bytes (max: ${maxFileSize} bytes)`,
4627
+ content: `old_string and new_string are identical. No changes needed.`,
3260
4628
  isError: true
3261
4629
  };
3262
4630
  }
3263
- const ext = extname(access.resolved).toLowerCase();
3264
- if (BINARY_EXTENSIONS.has(ext)) {
3265
- return {
3266
- content: `Binary file: ${access.resolved}
3267
- Size: ${fileStat.size} bytes
3268
- Type: ${ext}`
3269
- };
3270
- }
3271
4631
  try {
3272
- const content = await readFile3(access.resolved, "utf-8");
3273
- const lines = content.split(`
3274
- `);
3275
- const startLine = Math.max(1, offset);
3276
- const endLine = Math.min(lines.length, startLine + limit - 1);
3277
- const outputLines = [];
3278
- for (let i = startLine - 1;i < endLine; i++) {
3279
- const lineNum = i + 1;
3280
- let line = lines[i];
3281
- if (line.length > MAX_LINE_LENGTH) {
3282
- line = line.slice(0, MAX_LINE_LENGTH) + "...";
3283
- }
3284
- const padding = String(endLine).length;
3285
- outputLines.push(`${String(lineNum).padStart(padding)}→${line}`);
4632
+ const content = await readFile4(access.resolved, "utf-8");
4633
+ const occurrences = content.split(old_string).length - 1;
4634
+ if (occurrences === 0) {
4635
+ return {
4636
+ content: `Text not found in file: "${old_string.slice(0, 100)}${old_string.length > 100 ? "..." : ""}"`,
4637
+ isError: true
4638
+ };
3286
4639
  }
3287
- let header = "";
3288
- if (startLine > 1 || endLine < lines.length) {
3289
- header = `[Lines ${startLine}-${endLine} of ${lines.length}]
3290
-
3291
- `;
4640
+ if (!replace_all && occurrences > 1) {
4641
+ return {
4642
+ content: `Found ${occurrences} occurrences of the text. Use replace_all: true to replace all, or provide a more unique string.`,
4643
+ isError: true
4644
+ };
4645
+ }
4646
+ let newContent;
4647
+ let replacedCount;
4648
+ if (replace_all) {
4649
+ newContent = content.split(old_string).join(new_string);
4650
+ replacedCount = occurrences;
4651
+ } else {
4652
+ newContent = content.replace(old_string, new_string);
4653
+ replacedCount = 1;
3292
4654
  }
4655
+ await writeFile3(access.resolved, newContent, "utf-8");
3293
4656
  return {
3294
- content: header + outputLines.join(`
3295
- `)
4657
+ content: `Successfully replaced ${replacedCount} occurrence${replacedCount > 1 ? "s" : ""} in ${access.resolved}`
3296
4658
  };
3297
4659
  } catch (error) {
3298
4660
  return {
3299
- content: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
4661
+ content: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
3300
4662
  isError: true
3301
4663
  };
3302
4664
  }
3303
4665
  }
3304
4666
  };
3305
4667
  }
3306
- var ReadTool = createReadTool();
3307
- // src/tools/builtin/write.ts
3308
- import { writeFile, mkdir } from "node:fs/promises";
3309
- import { existsSync as existsSync5 } from "node:fs";
3310
- import { dirname as dirname3 } from "node:path";
3311
- function createWriteTool(options = {}) {
4668
+ var EditTool = createEditTool();
4669
+ // src/tools/builtin/glob.ts
4670
+ import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
4671
+ import { existsSync as existsSync7 } from "node:fs";
4672
+ import { join as join5, relative } from "node:path";
4673
+ var MAX_RESULTS = 1000;
4674
+ function matchGlob(pattern, path2) {
4675
+ const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*").replace(/\./g, "\\.");
4676
+ const regex = new RegExp(`^${regexPattern}$`);
4677
+ return regex.test(path2);
4678
+ }
4679
+ async function scanDir(dir, pattern, results, baseDir, maxDepth = 10, currentDepth = 0) {
4680
+ if (currentDepth > maxDepth || results.length >= MAX_RESULTS) {
4681
+ return;
4682
+ }
4683
+ if (!existsSync7(dir)) {
4684
+ return;
4685
+ }
4686
+ try {
4687
+ const entries = await readdir2(dir, { withFileTypes: true });
4688
+ for (const entry of entries) {
4689
+ if (results.length >= MAX_RESULTS)
4690
+ break;
4691
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
4692
+ continue;
4693
+ }
4694
+ const fullPath = join5(dir, entry.name);
4695
+ const relativePath = relative(baseDir, fullPath);
4696
+ if (entry.isDirectory()) {
4697
+ if (matchGlob(pattern, relativePath) || matchGlob(pattern, relativePath + "/")) {
4698
+ results.push(fullPath);
4699
+ }
4700
+ await scanDir(fullPath, pattern, results, baseDir, maxDepth, currentDepth + 1);
4701
+ } else if (entry.isFile()) {
4702
+ if (matchGlob(pattern, relativePath)) {
4703
+ results.push(fullPath);
4704
+ }
4705
+ }
4706
+ }
4707
+ } catch {}
4708
+ }
4709
+ function createGlobTool(options = {}) {
4710
+ const defaultCwd = options.cwd ?? process.cwd();
3312
4711
  return {
3313
- name: "Write",
3314
- description: `Write content to a file. Creates parent directories if needed. Overwrites existing files. Use Edit tool for modifying existing files.`,
4712
+ name: "Glob",
4713
+ description: `Find files matching a glob pattern. Supports ** for recursive matching, * for single directory, ? for single character. Returns file paths sorted by modification time.`,
3315
4714
  inputSchema: {
3316
4715
  type: "object",
3317
4716
  properties: {
3318
- file_path: {
4717
+ pattern: {
3319
4718
  type: "string",
3320
- description: "Absolute path to the file to write"
4719
+ description: 'Glob pattern to match (e.g., "**/*.ts", "src/**/*.js")'
3321
4720
  },
3322
- content: {
4721
+ path: {
3323
4722
  type: "string",
3324
- description: "Content to write to the file"
4723
+ description: "Directory to search in (default: current directory)"
3325
4724
  }
3326
4725
  },
3327
- required: ["file_path", "content"]
4726
+ required: ["pattern"]
3328
4727
  },
3329
4728
  execute: async (rawInput, _context) => {
3330
4729
  const input = rawInput;
3331
- const { file_path, content } = input;
3332
- const access = checkPathAccess(file_path, options, "file");
4730
+ const { pattern, path: path2 = defaultCwd } = input;
4731
+ const access = checkDirAccess(path2, options);
3333
4732
  if (!access.ok) {
3334
4733
  return { content: access.error, isError: true };
3335
4734
  }
3336
- try {
3337
- const dir = dirname3(access.resolved);
3338
- if (!existsSync5(dir)) {
3339
- await mkdir(dir, { recursive: true });
4735
+ if (!existsSync7(access.resolved)) {
4736
+ return {
4737
+ content: `Directory not found: ${access.resolved}`,
4738
+ isError: true
4739
+ };
4740
+ }
4741
+ try {
4742
+ const results = [];
4743
+ await scanDir(access.resolved, pattern, results, access.resolved);
4744
+ if (results.length === 0) {
4745
+ return {
4746
+ content: `No files found matching pattern: ${pattern}`
4747
+ };
4748
+ }
4749
+ const filesWithStats = await Promise.all(results.map(async (file) => {
4750
+ try {
4751
+ const stats = await stat2(file);
4752
+ return { file, mtime: stats.mtime.getTime() };
4753
+ } catch {
4754
+ return { file, mtime: 0 };
4755
+ }
4756
+ }));
4757
+ filesWithStats.sort((a, b) => b.mtime - a.mtime);
4758
+ const output = filesWithStats.map((f) => f.file).join(`
4759
+ `);
4760
+ const truncated = results.length >= MAX_RESULTS ? `
4761
+
4762
+ (Results truncated at ${MAX_RESULTS} files)` : "";
4763
+ return {
4764
+ content: `Found ${results.length} files:
4765
+
4766
+ ${output}${truncated}`
4767
+ };
4768
+ } catch (error) {
4769
+ return {
4770
+ content: `Failed to search: ${error instanceof Error ? error.message : String(error)}`,
4771
+ isError: true
4772
+ };
4773
+ }
4774
+ }
4775
+ };
4776
+ }
4777
+ var GlobTool = createGlobTool();
4778
+ // src/tools/builtin/grep.ts
4779
+ import { readFile as readFile5, readdir as readdir3, stat as stat3 } from "node:fs/promises";
4780
+ import { existsSync as existsSync8 } from "node:fs";
4781
+ import { join as join6, extname as extname2 } from "node:path";
4782
+ var MAX_RESULTS2 = 500;
4783
+ var MAX_LINE_LENGTH2 = 500;
4784
+ var BINARY_EXTENSIONS2 = new Set([
4785
+ ".png",
4786
+ ".jpg",
4787
+ ".jpeg",
4788
+ ".gif",
4789
+ ".bmp",
4790
+ ".ico",
4791
+ ".webp",
4792
+ ".mp3",
4793
+ ".mp4",
4794
+ ".wav",
4795
+ ".avi",
4796
+ ".mov",
4797
+ ".mkv",
4798
+ ".zip",
4799
+ ".tar",
4800
+ ".gz",
4801
+ ".rar",
4802
+ ".7z",
4803
+ ".exe",
4804
+ ".dll",
4805
+ ".so",
4806
+ ".dylib",
4807
+ ".pdf",
4808
+ ".doc",
4809
+ ".docx",
4810
+ ".xls",
4811
+ ".xlsx",
4812
+ ".woff",
4813
+ ".woff2",
4814
+ ".ttf",
4815
+ ".otf",
4816
+ ".eot",
4817
+ ".lock"
4818
+ ]);
4819
+ function matchGlob2(pattern, filename) {
4820
+ const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\./g, "\\.");
4821
+ return new RegExp(`^${regexPattern}$`).test(filename);
4822
+ }
4823
+ async function searchFile(filePath, regex, before, after) {
4824
+ const matches = [];
4825
+ try {
4826
+ const content = await readFile5(filePath, "utf-8");
4827
+ const lines = content.split(`
4828
+ `);
4829
+ for (let i = 0;i < lines.length; i++) {
4830
+ if (regex.test(lines[i])) {
4831
+ const match = {
4832
+ file: filePath,
4833
+ line: i + 1,
4834
+ content: lines[i].slice(0, MAX_LINE_LENGTH2)
4835
+ };
4836
+ if (before > 0) {
4837
+ match.before = lines.slice(Math.max(0, i - before), i).map((l) => l.slice(0, MAX_LINE_LENGTH2));
4838
+ }
4839
+ if (after > 0) {
4840
+ match.after = lines.slice(i + 1, i + 1 + after).map((l) => l.slice(0, MAX_LINE_LENGTH2));
4841
+ }
4842
+ matches.push(match);
4843
+ if (matches.length >= MAX_RESULTS2) {
4844
+ break;
4845
+ }
4846
+ }
4847
+ }
4848
+ } catch {}
4849
+ return matches;
4850
+ }
4851
+ async function searchDir(dir, regex, glob, before, after, results, maxDepth = 10, currentDepth = 0) {
4852
+ if (currentDepth > maxDepth || results.length >= MAX_RESULTS2) {
4853
+ return;
4854
+ }
4855
+ try {
4856
+ const entries = await readdir3(dir, { withFileTypes: true });
4857
+ for (const entry of entries) {
4858
+ if (results.length >= MAX_RESULTS2)
4859
+ break;
4860
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
4861
+ continue;
4862
+ }
4863
+ const fullPath = join6(dir, entry.name);
4864
+ if (entry.isDirectory()) {
4865
+ await searchDir(fullPath, regex, glob, before, after, results, maxDepth, currentDepth + 1);
4866
+ } else if (entry.isFile()) {
4867
+ const ext = extname2(entry.name).toLowerCase();
4868
+ if (BINARY_EXTENSIONS2.has(ext)) {
4869
+ continue;
3340
4870
  }
3341
- await writeFile(access.resolved, content, "utf-8");
3342
- const lines = content.split(`
3343
- `).length;
3344
- const bytes = Buffer.byteLength(content, "utf-8");
3345
- return {
3346
- content: `Successfully wrote ${bytes} bytes (${lines} lines) to ${access.resolved}`
3347
- };
3348
- } catch (error) {
3349
- return {
3350
- content: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
3351
- isError: true
3352
- };
4871
+ if (glob && !matchGlob2(glob, entry.name)) {
4872
+ continue;
4873
+ }
4874
+ const matches = await searchFile(fullPath, regex, before, after);
4875
+ results.push(...matches);
3353
4876
  }
3354
4877
  }
3355
- };
4878
+ } catch {}
3356
4879
  }
3357
- var WriteTool = createWriteTool();
3358
- // src/tools/builtin/edit.ts
3359
- import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
3360
- import { existsSync as existsSync6 } from "node:fs";
3361
- function createEditTool(options = {}) {
4880
+ function createGrepTool(options = {}) {
4881
+ const defaultCwd = options.cwd ?? process.cwd();
3362
4882
  return {
3363
- name: "Edit",
3364
- description: `Edit a file by replacing text. Finds old_string and replaces with new_string. The old_string must be unique in the file unless replace_all is true.`,
4883
+ name: "Grep",
4884
+ description: `Search file contents using regular expressions. Supports context lines before/after matches. Returns matching lines with file paths and line numbers.`,
3365
4885
  inputSchema: {
3366
4886
  type: "object",
3367
4887
  properties: {
3368
- file_path: {
4888
+ pattern: {
3369
4889
  type: "string",
3370
- description: "Absolute path to the file to edit"
4890
+ description: "Regular expression pattern to search for"
3371
4891
  },
3372
- old_string: {
4892
+ path: {
3373
4893
  type: "string",
3374
- description: "Text to find and replace"
4894
+ description: "File or directory to search in"
3375
4895
  },
3376
- new_string: {
4896
+ glob: {
3377
4897
  type: "string",
3378
- description: "Replacement text"
4898
+ description: 'Glob pattern to filter files (e.g., "*.ts")'
3379
4899
  },
3380
- replace_all: {
4900
+ before: {
4901
+ type: "number",
4902
+ description: "Number of lines to show before each match"
4903
+ },
4904
+ after: {
4905
+ type: "number",
4906
+ description: "Number of lines to show after each match"
4907
+ },
4908
+ ignoreCase: {
3381
4909
  type: "boolean",
3382
- description: "Replace all occurrences (default: false)",
3383
- default: false
4910
+ description: "Case insensitive search"
3384
4911
  }
3385
4912
  },
3386
- required: ["file_path", "old_string", "new_string"]
4913
+ required: ["pattern"]
3387
4914
  },
3388
4915
  execute: async (rawInput, _context) => {
3389
4916
  const input = rawInput;
3390
- const { file_path, old_string, new_string, replace_all = false } = input;
3391
- const access = checkPathAccess(file_path, options, "file");
4917
+ const {
4918
+ pattern,
4919
+ path: path2 = defaultCwd,
4920
+ glob,
4921
+ before = 0,
4922
+ after = 0,
4923
+ ignoreCase = false
4924
+ } = input;
4925
+ const access = checkPathAccess(path2, options, "dir");
3392
4926
  if (!access.ok) {
3393
4927
  return { content: access.error, isError: true };
3394
4928
  }
3395
- if (!existsSync6(access.resolved)) {
3396
- return {
3397
- content: `File not found: ${access.resolved}`,
3398
- isError: true
3399
- };
3400
- }
3401
- if (old_string === new_string) {
4929
+ if (!existsSync8(access.resolved)) {
3402
4930
  return {
3403
- content: `old_string and new_string are identical. No changes needed.`,
4931
+ content: `Path not found: ${access.resolved}`,
3404
4932
  isError: true
3405
4933
  };
3406
4934
  }
3407
4935
  try {
3408
- const content = await readFile4(access.resolved, "utf-8");
3409
- const occurrences = content.split(old_string).length - 1;
3410
- if (occurrences === 0) {
3411
- return {
3412
- content: `Text not found in file: "${old_string.slice(0, 100)}${old_string.length > 100 ? "..." : ""}"`,
3413
- isError: true
3414
- };
4936
+ const flags = ignoreCase ? "gi" : "g";
4937
+ const regex = new RegExp(pattern, flags);
4938
+ const results = [];
4939
+ const pathStat = await stat3(access.resolved);
4940
+ if (pathStat.isFile()) {
4941
+ const matches = await searchFile(access.resolved, regex, before, after);
4942
+ results.push(...matches);
4943
+ } else if (pathStat.isDirectory()) {
4944
+ await searchDir(access.resolved, regex, glob, before, after, results);
3415
4945
  }
3416
- if (!replace_all && occurrences > 1) {
4946
+ if (results.length === 0) {
3417
4947
  return {
3418
- content: `Found ${occurrences} occurrences of the text. Use replace_all: true to replace all, or provide a more unique string.`,
3419
- isError: true
4948
+ content: `No matches found for pattern: ${pattern}`
3420
4949
  };
3421
4950
  }
3422
- let newContent;
3423
- let replacedCount;
3424
- if (replace_all) {
3425
- newContent = content.split(old_string).join(new_string);
3426
- replacedCount = occurrences;
3427
- } else {
3428
- newContent = content.replace(old_string, new_string);
3429
- replacedCount = 1;
4951
+ const output = [];
4952
+ for (const match of results) {
4953
+ if (match.before?.length) {
4954
+ for (let i = 0;i < match.before.length; i++) {
4955
+ const lineNum = match.line - match.before.length + i;
4956
+ output.push(`${match.file}:${lineNum}- ${match.before[i]}`);
4957
+ }
4958
+ }
4959
+ output.push(`${match.file}:${match.line}: ${match.content}`);
4960
+ if (match.after?.length) {
4961
+ for (let i = 0;i < match.after.length; i++) {
4962
+ const lineNum = match.line + i + 1;
4963
+ output.push(`${match.file}:${lineNum}+ ${match.after[i]}`);
4964
+ }
4965
+ }
4966
+ if (match.before?.length || match.after?.length) {
4967
+ output.push("--");
4968
+ }
3430
4969
  }
3431
- await writeFile2(access.resolved, newContent, "utf-8");
4970
+ const truncated = results.length >= MAX_RESULTS2 ? `
4971
+
4972
+ (Results truncated at ${MAX_RESULTS2} matches)` : "";
3432
4973
  return {
3433
- content: `Successfully replaced ${replacedCount} occurrence${replacedCount > 1 ? "s" : ""} in ${access.resolved}`
4974
+ content: `Found ${results.length} matches:
4975
+
4976
+ ${output.join(`
4977
+ `)}${truncated}`
3434
4978
  };
3435
4979
  } catch (error) {
3436
4980
  return {
3437
- content: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
4981
+ content: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
3438
4982
  isError: true
3439
4983
  };
3440
4984
  }
3441
4985
  }
3442
4986
  };
3443
4987
  }
3444
- var EditTool = createEditTool();
3445
- // src/tools/builtin/glob.ts
3446
- import { readdir, stat as stat2 } from "node:fs/promises";
3447
- import { existsSync as existsSync7 } from "node:fs";
3448
- import { join as join4, relative } from "node:path";
3449
- var MAX_RESULTS = 1000;
3450
- function matchGlob(pattern, path) {
3451
- const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*").replace(/\./g, "\\.");
3452
- const regex = new RegExp(`^${regexPattern}$`);
3453
- return regex.test(path);
4988
+ var GrepTool = createGrepTool();
4989
+ // src/tools/builtin/webfetch.ts
4990
+ import { lookup } from "node:dns/promises";
4991
+ var MAX_CONTENT_LENGTH = 1e5;
4992
+ var FETCH_TIMEOUT = 30000;
4993
+ function isIpV4(host) {
4994
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
3454
4995
  }
3455
- async function scanDir(dir, pattern, results, baseDir, maxDepth = 10, currentDepth = 0) {
3456
- if (currentDepth > maxDepth || results.length >= MAX_RESULTS) {
3457
- return;
4996
+ function isIpV6(host) {
4997
+ return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
4998
+ }
4999
+ function isPrivateIpv4(ip) {
5000
+ const parts = ip.split(".").map((p) => Number(p));
5001
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
5002
+ return true;
5003
+ const [a, b] = parts;
5004
+ if (a === 10)
5005
+ return true;
5006
+ if (a === 127)
5007
+ return true;
5008
+ if (a === 0)
5009
+ return true;
5010
+ if (a === 169 && b === 254)
5011
+ return true;
5012
+ if (a === 172 && b >= 16 && b <= 31)
5013
+ return true;
5014
+ if (a === 192 && b === 168)
5015
+ return true;
5016
+ if (a === 100 && b >= 64 && b <= 127)
5017
+ return true;
5018
+ if (a >= 224)
5019
+ return true;
5020
+ return false;
5021
+ }
5022
+ function isPrivateIpv6(ip) {
5023
+ const normalized = ip.toLowerCase();
5024
+ if (normalized === "::" || normalized === "::1")
5025
+ return true;
5026
+ if (normalized.startsWith("fe80:"))
5027
+ return true;
5028
+ if (normalized.startsWith("fc") || normalized.startsWith("fd"))
5029
+ return true;
5030
+ return false;
5031
+ }
5032
+ async function denyPrivateNetworkTargets(url, options) {
5033
+ if (options.allowPrivateNetwork)
5034
+ return null;
5035
+ const hostname = url.hostname.toLowerCase();
5036
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
5037
+ return `Blocked by policy (allowPrivateNetwork=false): hostname "${hostname}" is local-only`;
3458
5038
  }
3459
- if (!existsSync7(dir)) {
3460
- return;
5039
+ if (isIpV4(hostname) && isPrivateIpv4(hostname)) {
5040
+ return `Blocked by policy (allowPrivateNetwork=false): private IPv4 target "${hostname}"`;
5041
+ }
5042
+ if (isIpV6(hostname) && isPrivateIpv6(hostname)) {
5043
+ return `Blocked by policy (allowPrivateNetwork=false): private IPv6 target "${hostname}"`;
3461
5044
  }
5045
+ const resolveHostnames = options.resolveHostnames ?? true;
5046
+ if (!resolveHostnames)
5047
+ return null;
3462
5048
  try {
3463
- const entries = await readdir(dir, { withFileTypes: true });
3464
- for (const entry of entries) {
3465
- if (results.length >= MAX_RESULTS)
3466
- break;
3467
- if (entry.name.startsWith(".") || entry.name === "node_modules") {
3468
- continue;
5049
+ const addrs = await lookup(hostname, { all: true, verbatim: true });
5050
+ for (const addr of addrs) {
5051
+ if (addr.family === 4 && isPrivateIpv4(addr.address)) {
5052
+ return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv4 "${addr.address}"`;
3469
5053
  }
3470
- const fullPath = join4(dir, entry.name);
3471
- const relativePath = relative(baseDir, fullPath);
3472
- if (entry.isDirectory()) {
3473
- if (matchGlob(pattern, relativePath) || matchGlob(pattern, relativePath + "/")) {
3474
- results.push(fullPath);
3475
- }
3476
- await scanDir(fullPath, pattern, results, baseDir, maxDepth, currentDepth + 1);
3477
- } else if (entry.isFile()) {
3478
- if (matchGlob(pattern, relativePath)) {
3479
- results.push(fullPath);
3480
- }
5054
+ if (addr.family === 6 && isPrivateIpv6(addr.address)) {
5055
+ return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv6 "${addr.address}"`;
3481
5056
  }
3482
5057
  }
3483
- } catch {}
5058
+ } catch (e) {
5059
+ return `DNS resolution failed for "${hostname}" (allowPrivateNetwork=false): ${e instanceof Error ? e.message : String(e)}`;
5060
+ }
5061
+ return null;
5062
+ }
5063
+ function htmlToMarkdown(html) {
5064
+ let md = html;
5065
+ md = md.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
5066
+ md = md.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
5067
+ md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, `
5068
+ # $1
5069
+ `);
5070
+ md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, `
5071
+ ## $1
5072
+ `);
5073
+ md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, `
5074
+ ### $1
5075
+ `);
5076
+ md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, `
5077
+ #### $1
5078
+ `);
5079
+ md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, `
5080
+ ##### $1
5081
+ `);
5082
+ md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, `
5083
+ ###### $1
5084
+ `);
5085
+ md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, `
5086
+ $1
5087
+ `);
5088
+ md = md.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
5089
+ md = md.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
5090
+ md = md.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
5091
+ md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
5092
+ md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "\n```\n$1\n```\n");
5093
+ md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, `- $1
5094
+ `);
5095
+ md = md.replace(/<\/?[ou]l[^>]*>/gi, `
5096
+ `);
5097
+ md = md.replace(/<br\s*\/?>/gi, `
5098
+ `);
5099
+ md = md.replace(/<[^>]+>/g, "");
5100
+ md = md.replace(/&nbsp;/g, " ");
5101
+ md = md.replace(/&amp;/g, "&");
5102
+ md = md.replace(/&lt;/g, "<");
5103
+ md = md.replace(/&gt;/g, ">");
5104
+ md = md.replace(/&quot;/g, '"');
5105
+ md = md.replace(/\n{3,}/g, `
5106
+
5107
+ `);
5108
+ md = md.trim();
5109
+ return md;
3484
5110
  }
3485
- function createGlobTool(options = {}) {
3486
- const defaultCwd = options.cwd ?? process.cwd();
5111
+ function createWebFetchTool(options = {}) {
3487
5112
  return {
3488
- name: "Glob",
3489
- description: `Find files matching a glob pattern. Supports ** for recursive matching, * for single directory, ? for single character. Returns file paths sorted by modification time.`,
5113
+ name: "WebFetch",
5114
+ description: `Fetch content from a URL. Converts HTML to markdown for readability. Use for retrieving web pages, documentation, or API responses.`,
3490
5115
  inputSchema: {
3491
5116
  type: "object",
3492
5117
  properties: {
3493
- pattern: {
5118
+ url: {
3494
5119
  type: "string",
3495
- description: 'Glob pattern to match (e.g., "**/*.ts", "src/**/*.js")'
5120
+ description: "URL to fetch"
3496
5121
  },
3497
- path: {
5122
+ prompt: {
3498
5123
  type: "string",
3499
- description: "Directory to search in (default: current directory)"
5124
+ description: "Optional prompt to describe what information to extract"
3500
5125
  }
3501
5126
  },
3502
- required: ["pattern"]
5127
+ required: ["url"]
3503
5128
  },
3504
5129
  execute: async (rawInput, _context) => {
3505
5130
  const input = rawInput;
3506
- const { pattern, path = defaultCwd } = input;
3507
- const access = checkDirAccess(path, options);
3508
- if (!access.ok) {
3509
- return { content: access.error, isError: true };
3510
- }
3511
- if (!existsSync7(access.resolved)) {
5131
+ const { url, prompt } = input;
5132
+ let parsedUrl;
5133
+ try {
5134
+ parsedUrl = new URL(url);
5135
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
5136
+ return {
5137
+ content: `Invalid URL protocol: ${parsedUrl.protocol}. Only http/https are allowed.`,
5138
+ isError: true
5139
+ };
5140
+ }
5141
+ if (parsedUrl.protocol === "http:")
5142
+ parsedUrl.protocol = "https:";
5143
+ } catch {
3512
5144
  return {
3513
- content: `Directory not found: ${access.resolved}`,
5145
+ content: `Invalid URL: ${url}`,
3514
5146
  isError: true
3515
5147
  };
3516
5148
  }
5149
+ const ssrfDeny = await denyPrivateNetworkTargets(parsedUrl, options);
5150
+ if (ssrfDeny) {
5151
+ return { content: ssrfDeny, isError: true };
5152
+ }
3517
5153
  try {
3518
- const results = [];
3519
- await scanDir(access.resolved, pattern, results, access.resolved);
3520
- if (results.length === 0) {
5154
+ const controller = new AbortController;
5155
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
5156
+ const response = await fetch(parsedUrl.toString(), {
5157
+ signal: controller.signal,
5158
+ headers: {
5159
+ "User-Agent": "Mozilla/5.0 (compatible; OpenCode-Agent/1.0)",
5160
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
5161
+ }
5162
+ });
5163
+ clearTimeout(timeout);
5164
+ if (!response.ok) {
3521
5165
  return {
3522
- content: `No files found matching pattern: ${pattern}`
5166
+ content: `HTTP error: ${response.status} ${response.statusText}`,
5167
+ isError: true
3523
5168
  };
3524
5169
  }
3525
- const filesWithStats = await Promise.all(results.map(async (file) => {
3526
- try {
3527
- const stats = await stat2(file);
3528
- return { file, mtime: stats.mtime.getTime() };
3529
- } catch {
3530
- return { file, mtime: 0 };
3531
- }
3532
- }));
3533
- filesWithStats.sort((a, b) => b.mtime - a.mtime);
3534
- const output = filesWithStats.map((f) => f.file).join(`
3535
- `);
3536
- const truncated = results.length >= MAX_RESULTS ? `
3537
-
3538
- (Results truncated at ${MAX_RESULTS} files)` : "";
3539
- return {
3540
- content: `Found ${results.length} files:
5170
+ const finalUrl = new URL(response.url);
5171
+ if (finalUrl.host !== parsedUrl.host) {
5172
+ return {
5173
+ content: `Redirected to different host: ${response.url}
3541
5174
 
3542
- ${output}${truncated}`
3543
- };
3544
- } catch (error) {
3545
- return {
3546
- content: `Failed to search: ${error instanceof Error ? error.message : String(error)}`,
3547
- isError: true
3548
- };
3549
- }
3550
- }
3551
- };
3552
- }
3553
- var GlobTool = createGlobTool();
3554
- // src/tools/builtin/grep.ts
3555
- import { readFile as readFile5, readdir as readdir2, stat as stat3 } from "node:fs/promises";
3556
- import { existsSync as existsSync8 } from "node:fs";
3557
- import { join as join5, extname as extname2 } from "node:path";
3558
- var MAX_RESULTS2 = 500;
3559
- var MAX_LINE_LENGTH2 = 500;
3560
- var BINARY_EXTENSIONS2 = new Set([
3561
- ".png",
3562
- ".jpg",
3563
- ".jpeg",
3564
- ".gif",
3565
- ".bmp",
3566
- ".ico",
3567
- ".webp",
3568
- ".mp3",
3569
- ".mp4",
3570
- ".wav",
3571
- ".avi",
3572
- ".mov",
3573
- ".mkv",
3574
- ".zip",
3575
- ".tar",
3576
- ".gz",
3577
- ".rar",
3578
- ".7z",
3579
- ".exe",
3580
- ".dll",
3581
- ".so",
3582
- ".dylib",
3583
- ".pdf",
3584
- ".doc",
3585
- ".docx",
3586
- ".xls",
3587
- ".xlsx",
3588
- ".woff",
3589
- ".woff2",
3590
- ".ttf",
3591
- ".otf",
3592
- ".eot",
3593
- ".lock"
3594
- ]);
3595
- function matchGlob2(pattern, filename) {
3596
- const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\./g, "\\.");
3597
- return new RegExp(`^${regexPattern}$`).test(filename);
3598
- }
3599
- async function searchFile(filePath, regex, before, after) {
3600
- const matches = [];
3601
- try {
3602
- const content = await readFile5(filePath, "utf-8");
3603
- const lines = content.split(`
3604
- `);
3605
- for (let i = 0;i < lines.length; i++) {
3606
- if (regex.test(lines[i])) {
3607
- const match = {
3608
- file: filePath,
3609
- line: i + 1,
3610
- content: lines[i].slice(0, MAX_LINE_LENGTH2)
3611
- };
3612
- if (before > 0) {
3613
- match.before = lines.slice(Math.max(0, i - before), i).map((l) => l.slice(0, MAX_LINE_LENGTH2));
5175
+ Please fetch the new URL if you want to continue.`
5176
+ };
3614
5177
  }
3615
- if (after > 0) {
3616
- match.after = lines.slice(i + 1, i + 1 + after).map((l) => l.slice(0, MAX_LINE_LENGTH2));
5178
+ const ssrfDenyAfter = await denyPrivateNetworkTargets(finalUrl, options);
5179
+ if (ssrfDenyAfter) {
5180
+ return { content: `Redirect target denied: ${ssrfDenyAfter}`, isError: true };
3617
5181
  }
3618
- matches.push(match);
3619
- if (matches.length >= MAX_RESULTS2) {
3620
- break;
5182
+ const contentType = response.headers.get("content-type") || "";
5183
+ let content = await response.text();
5184
+ if (content.length > MAX_CONTENT_LENGTH) {
5185
+ content = content.slice(0, MAX_CONTENT_LENGTH) + `
5186
+
5187
+ ... (content truncated)`;
3621
5188
  }
3622
- }
3623
- }
3624
- } catch {}
3625
- return matches;
3626
- }
3627
- async function searchDir(dir, regex, glob, before, after, results, maxDepth = 10, currentDepth = 0) {
3628
- if (currentDepth > maxDepth || results.length >= MAX_RESULTS2) {
3629
- return;
3630
- }
3631
- try {
3632
- const entries = await readdir2(dir, { withFileTypes: true });
3633
- for (const entry of entries) {
3634
- if (results.length >= MAX_RESULTS2)
3635
- break;
3636
- if (entry.name.startsWith(".") || entry.name === "node_modules") {
3637
- continue;
3638
- }
3639
- const fullPath = join5(dir, entry.name);
3640
- if (entry.isDirectory()) {
3641
- await searchDir(fullPath, regex, glob, before, after, results, maxDepth, currentDepth + 1);
3642
- } else if (entry.isFile()) {
3643
- const ext = extname2(entry.name).toLowerCase();
3644
- if (BINARY_EXTENSIONS2.has(ext)) {
3645
- continue;
5189
+ if (contentType.includes("text/html")) {
5190
+ content = htmlToMarkdown(content);
3646
5191
  }
3647
- if (glob && !matchGlob2(glob, entry.name)) {
3648
- continue;
5192
+ let output = `URL: ${response.url}
5193
+ `;
5194
+ output += `Content-Type: ${contentType}
5195
+
5196
+ `;
5197
+ if (prompt) {
5198
+ output += `Requested: ${prompt}
5199
+
5200
+ `;
3649
5201
  }
3650
- const matches = await searchFile(fullPath, regex, before, after);
3651
- results.push(...matches);
5202
+ output += content;
5203
+ return {
5204
+ content: output
5205
+ };
5206
+ } catch (error) {
5207
+ if (error instanceof Error && error.name === "AbortError") {
5208
+ return {
5209
+ content: `Request timed out after ${FETCH_TIMEOUT}ms`,
5210
+ isError: true
5211
+ };
5212
+ }
5213
+ return {
5214
+ content: `Failed to fetch: ${error instanceof Error ? error.message : String(error)}`,
5215
+ isError: true
5216
+ };
3652
5217
  }
3653
5218
  }
3654
- } catch {}
5219
+ };
3655
5220
  }
3656
- function createGrepTool(options = {}) {
3657
- const defaultCwd = options.cwd ?? process.cwd();
5221
+ var WebFetchTool = createWebFetchTool();
5222
+ // src/tools/builtin/websearch.ts
5223
+ var API_CONFIG = {
5224
+ BASE_URL: "https://mcp.exa.ai",
5225
+ ENDPOINTS: {
5226
+ SEARCH: "/mcp"
5227
+ },
5228
+ DEFAULT_NUM_RESULTS: 8,
5229
+ DEFAULT_CONTEXT_MAX_CHARS: 1e4,
5230
+ TIMEOUT_MS: 30000
5231
+ };
5232
+ var WEBSEARCH_DESCRIPTION = `Search the web for up-to-date information using Exa AI.
5233
+
5234
+ Use this tool when you need to:
5235
+ - Find current information, news, or recent events
5236
+ - Look up documentation or technical resources
5237
+ - Research topics beyond your knowledge cutoff
5238
+ - Verify facts or get multiple perspectives
5239
+
5240
+ The search returns curated, AI-optimized content with context.
5241
+
5242
+ Parameters:
5243
+ - query: The search query (required)
5244
+ - numResults: Number of results to return (default: 8, max: 20)
5245
+ - livecrawl: 'fallback' (default) or 'preferred' for fresh content
5246
+ - type: 'auto' (default), 'fast', or 'deep'
5247
+ - contextMaxCharacters: Max characters per result (default: 10000)
5248
+
5249
+ Best practices:
5250
+ - Use specific, detailed queries for better results
5251
+ - For technical topics, include relevant terms and context
5252
+ - Use 'deep' type for comprehensive research
5253
+ - Use 'fast' type for quick fact-checking`;
5254
+ function createWebSearchTool(options = {}) {
3658
5255
  return {
3659
- name: "Grep",
3660
- description: `Search file contents using regular expressions. Supports context lines before/after matches. Returns matching lines with file paths and line numbers.`,
5256
+ name: "WebSearch",
5257
+ description: WEBSEARCH_DESCRIPTION,
3661
5258
  inputSchema: {
3662
5259
  type: "object",
3663
5260
  properties: {
3664
- pattern: {
5261
+ query: {
3665
5262
  type: "string",
3666
- description: "Regular expression pattern to search for"
5263
+ description: "Search query to find relevant information"
3667
5264
  },
3668
- path: {
3669
- type: "string",
3670
- description: "File or directory to search in"
5265
+ numResults: {
5266
+ type: "number",
5267
+ description: "Number of search results to return (default: 8, max: 20)"
3671
5268
  },
3672
- glob: {
5269
+ livecrawl: {
3673
5270
  type: "string",
3674
- description: 'Glob pattern to filter files (e.g., "*.ts")'
5271
+ enum: ["fallback", "preferred"],
5272
+ description: "Live crawl mode - 'fallback': use live crawling as backup, 'preferred': prioritize live crawling"
3675
5273
  },
3676
- before: {
3677
- type: "number",
3678
- description: "Number of lines to show before each match"
5274
+ type: {
5275
+ type: "string",
5276
+ enum: ["auto", "fast", "deep"],
5277
+ description: "Search type - 'auto': balanced, 'fast': quick results, 'deep': comprehensive"
3679
5278
  },
3680
- after: {
5279
+ contextMaxCharacters: {
3681
5280
  type: "number",
3682
- description: "Number of lines to show after each match"
3683
- },
3684
- ignoreCase: {
3685
- type: "boolean",
3686
- description: "Case insensitive search"
5281
+ description: "Maximum characters for context per result (default: 10000)"
3687
5282
  }
3688
5283
  },
3689
- required: ["pattern"]
5284
+ required: ["query"]
3690
5285
  },
3691
- execute: async (rawInput, _context) => {
5286
+ execute: async (rawInput, context) => {
3692
5287
  const input = rawInput;
3693
- const {
3694
- pattern,
3695
- path = defaultCwd,
3696
- glob,
3697
- before = 0,
3698
- after = 0,
3699
- ignoreCase = false
3700
- } = input;
3701
- const access = checkPathAccess(path, options, "dir");
3702
- if (!access.ok) {
3703
- return { content: access.error, isError: true };
3704
- }
3705
- if (!existsSync8(access.resolved)) {
5288
+ const { query, numResults, livecrawl, type, contextMaxCharacters } = input;
5289
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
3706
5290
  return {
3707
- content: `Path not found: ${access.resolved}`,
5291
+ content: "Error: A non-empty search query is required.",
3708
5292
  isError: true
3709
5293
  };
3710
5294
  }
3711
- try {
3712
- const flags = ignoreCase ? "gi" : "g";
3713
- const regex = new RegExp(pattern, flags);
3714
- const results = [];
3715
- const pathStat = await stat3(access.resolved);
3716
- if (pathStat.isFile()) {
3717
- const matches = await searchFile(access.resolved, regex, before, after);
3718
- results.push(...matches);
3719
- } else if (pathStat.isDirectory()) {
3720
- await searchDir(access.resolved, regex, glob, before, after, results);
5295
+ const clampedNumResults = Math.min(Math.max(numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 1), 20);
5296
+ const searchRequest = {
5297
+ jsonrpc: "2.0",
5298
+ id: 1,
5299
+ method: "tools/call",
5300
+ params: {
5301
+ name: "web_search_exa",
5302
+ arguments: {
5303
+ query: query.trim(),
5304
+ numResults: clampedNumResults,
5305
+ livecrawl: livecrawl || "fallback",
5306
+ type: type || "auto",
5307
+ contextMaxCharacters: contextMaxCharacters || API_CONFIG.DEFAULT_CONTEXT_MAX_CHARS
5308
+ }
3721
5309
  }
3722
- if (results.length === 0) {
5310
+ };
5311
+ const controller = new AbortController;
5312
+ const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT_MS);
5313
+ try {
5314
+ const signal = context.abortSignal ? AbortSignal.any([controller.signal, context.abortSignal]) : controller.signal;
5315
+ const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
5316
+ method: "POST",
5317
+ headers: {
5318
+ Accept: "application/json, text/event-stream",
5319
+ "Content-Type": "application/json",
5320
+ "User-Agent": "FormAgent-SDK/1.0"
5321
+ },
5322
+ body: JSON.stringify(searchRequest),
5323
+ signal
5324
+ });
5325
+ clearTimeout(timeoutId);
5326
+ if (!response.ok) {
5327
+ const errorText = await response.text();
3723
5328
  return {
3724
- content: `No matches found for pattern: ${pattern}`
5329
+ content: `Search error (${response.status}): ${errorText}`,
5330
+ isError: true
3725
5331
  };
3726
5332
  }
3727
- const output = [];
3728
- for (const match of results) {
3729
- if (match.before?.length) {
3730
- for (let i = 0;i < match.before.length; i++) {
3731
- const lineNum = match.line - match.before.length + i;
3732
- output.push(`${match.file}:${lineNum}- ${match.before[i]}`);
3733
- }
3734
- }
3735
- output.push(`${match.file}:${match.line}: ${match.content}`);
3736
- if (match.after?.length) {
3737
- for (let i = 0;i < match.after.length; i++) {
3738
- const lineNum = match.line + i + 1;
3739
- output.push(`${match.file}:${lineNum}+ ${match.after[i]}`);
5333
+ const responseText = await response.text();
5334
+ const lines = responseText.split(`
5335
+ `);
5336
+ for (const line of lines) {
5337
+ if (line.startsWith("data: ")) {
5338
+ try {
5339
+ const data = JSON.parse(line.substring(6));
5340
+ if (data.error) {
5341
+ return {
5342
+ content: `Search API error: ${data.error.message}`,
5343
+ isError: true
5344
+ };
5345
+ }
5346
+ if (data.result?.content && data.result.content.length > 0) {
5347
+ const resultText = data.result.content[0].text;
5348
+ let output = `## Web Search Results
5349
+
5350
+ `;
5351
+ output += `**Query:** ${query}
5352
+ `;
5353
+ output += `**Results:** ${clampedNumResults} requested
5354
+
5355
+ `;
5356
+ output += `---
5357
+
5358
+ `;
5359
+ output += resultText;
5360
+ return {
5361
+ content: output
5362
+ };
5363
+ }
5364
+ } catch (parseError) {
5365
+ continue;
3740
5366
  }
3741
5367
  }
3742
- if (match.before?.length || match.after?.length) {
3743
- output.push("--");
3744
- }
3745
5368
  }
3746
- const truncated = results.length >= MAX_RESULTS2 ? `
3747
-
3748
- (Results truncated at ${MAX_RESULTS2} matches)` : "";
5369
+ try {
5370
+ const data = JSON.parse(responseText);
5371
+ if (data.result?.content && data.result.content.length > 0) {
5372
+ return {
5373
+ content: data.result.content[0].text
5374
+ };
5375
+ }
5376
+ } catch {}
3749
5377
  return {
3750
- content: `Found ${results.length} matches:
3751
-
3752
- ${output.join(`
3753
- `)}${truncated}`
5378
+ content: `No search results found for query: "${query}". Please try a different or more specific query.`
3754
5379
  };
3755
5380
  } catch (error) {
5381
+ clearTimeout(timeoutId);
5382
+ if (error instanceof Error) {
5383
+ if (error.name === "AbortError") {
5384
+ return {
5385
+ content: `Search request timed out after ${API_CONFIG.TIMEOUT_MS}ms. Please try again or use a simpler query.`,
5386
+ isError: true
5387
+ };
5388
+ }
5389
+ return {
5390
+ content: `Search failed: ${error.message}`,
5391
+ isError: true
5392
+ };
5393
+ }
3756
5394
  return {
3757
- content: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
5395
+ content: `Search failed: ${String(error)}`,
3758
5396
  isError: true
3759
5397
  };
3760
5398
  }
3761
5399
  }
3762
5400
  };
3763
5401
  }
3764
- var GrepTool = createGrepTool();
3765
- // src/tools/builtin/webfetch.ts
3766
- import { lookup } from "node:dns/promises";
3767
- var MAX_CONTENT_LENGTH = 1e5;
3768
- var FETCH_TIMEOUT = 30000;
3769
- function isIpV4(host) {
3770
- return /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
3771
- }
3772
- function isIpV6(host) {
3773
- return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
5402
+ var WebSearchTool = createWebSearchTool();
5403
+ // src/tools/builtin/todo.ts
5404
+ var globalTodos = [];
5405
+ var onTodoChange = null;
5406
+ function setTodoChangeCallback(callback) {
5407
+ onTodoChange = callback;
3774
5408
  }
3775
- function isPrivateIpv4(ip) {
3776
- const parts = ip.split(".").map((p) => Number(p));
3777
- if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
3778
- return true;
3779
- const [a, b] = parts;
3780
- if (a === 10)
3781
- return true;
3782
- if (a === 127)
3783
- return true;
3784
- if (a === 0)
3785
- return true;
3786
- if (a === 169 && b === 254)
3787
- return true;
3788
- if (a === 172 && b >= 16 && b <= 31)
3789
- return true;
3790
- if (a === 192 && b === 168)
3791
- return true;
3792
- if (a === 100 && b >= 64 && b <= 127)
3793
- return true;
3794
- if (a >= 224)
3795
- return true;
3796
- return false;
5409
+ function getTodos() {
5410
+ return [...globalTodos];
3797
5411
  }
3798
- function isPrivateIpv6(ip) {
3799
- const normalized = ip.toLowerCase();
3800
- if (normalized === "::" || normalized === "::1")
3801
- return true;
3802
- if (normalized.startsWith("fe80:"))
3803
- return true;
3804
- if (normalized.startsWith("fc") || normalized.startsWith("fd"))
3805
- return true;
3806
- return false;
5412
+ function clearTodos() {
5413
+ globalTodos = [];
5414
+ onTodoChange?.(globalTodos);
3807
5415
  }
3808
- async function denyPrivateNetworkTargets(url, options) {
3809
- if (options.allowPrivateNetwork)
3810
- return null;
3811
- const hostname = url.hostname.toLowerCase();
3812
- if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
3813
- return `Blocked by policy (allowPrivateNetwork=false): hostname "${hostname}" is local-only`;
3814
- }
3815
- if (isIpV4(hostname) && isPrivateIpv4(hostname)) {
3816
- return `Blocked by policy (allowPrivateNetwork=false): private IPv4 target "${hostname}"`;
3817
- }
3818
- if (isIpV6(hostname) && isPrivateIpv6(hostname)) {
3819
- return `Blocked by policy (allowPrivateNetwork=false): private IPv6 target "${hostname}"`;
3820
- }
3821
- const resolveHostnames = options.resolveHostnames ?? true;
3822
- if (!resolveHostnames)
3823
- return null;
3824
- try {
3825
- const addrs = await lookup(hostname, { all: true, verbatim: true });
3826
- for (const addr of addrs) {
3827
- if (addr.family === 4 && isPrivateIpv4(addr.address)) {
3828
- return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv4 "${addr.address}"`;
5416
+ function createTodoWriteTool(options = {}) {
5417
+ return {
5418
+ name: "TodoWrite",
5419
+ description: `Manage a task list to track progress on complex tasks. Use to plan work, track completed items, and show progress to the user.`,
5420
+ inputSchema: {
5421
+ type: "object",
5422
+ properties: {
5423
+ todos: {
5424
+ type: "array",
5425
+ description: "Updated todo list",
5426
+ items: {
5427
+ type: "object",
5428
+ properties: {
5429
+ content: {
5430
+ type: "string",
5431
+ description: "Task description (imperative form, e.g., 'Run tests')"
5432
+ },
5433
+ status: {
5434
+ type: "string",
5435
+ enum: ["pending", "in_progress", "completed"],
5436
+ description: "Task status"
5437
+ },
5438
+ activeForm: {
5439
+ type: "string",
5440
+ description: "Present continuous form (e.g., 'Running tests')"
5441
+ }
5442
+ },
5443
+ required: ["content", "status", "activeForm"]
5444
+ }
5445
+ }
5446
+ },
5447
+ required: ["todos"]
5448
+ },
5449
+ execute: async (rawInput, _context) => {
5450
+ const input = rawInput;
5451
+ const { todos } = input;
5452
+ for (const todo of todos) {
5453
+ if (!todo.content || !todo.status || !todo.activeForm) {
5454
+ return {
5455
+ content: "Invalid todo item: missing required fields (content, status, activeForm)",
5456
+ isError: true
5457
+ };
5458
+ }
5459
+ if (!["pending", "in_progress", "completed"].includes(todo.status)) {
5460
+ return {
5461
+ content: `Invalid status: ${todo.status}. Must be pending, in_progress, or completed.`,
5462
+ isError: true
5463
+ };
5464
+ }
3829
5465
  }
3830
- if (addr.family === 6 && isPrivateIpv6(addr.address)) {
3831
- return `Blocked by policy (allowPrivateNetwork=false): "${hostname}" resolves to private IPv6 "${addr.address}"`;
5466
+ globalTodos = todos;
5467
+ onTodoChange?.(globalTodos);
5468
+ const completed = todos.filter((t) => t.status === "completed").length;
5469
+ const inProgress = todos.filter((t) => t.status === "in_progress").length;
5470
+ const pending = todos.filter((t) => t.status === "pending").length;
5471
+ const lines = [
5472
+ `Todo list updated (${completed}/${todos.length} completed)`,
5473
+ ""
5474
+ ];
5475
+ for (const todo of todos) {
5476
+ const icon = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "→" : "○";
5477
+ lines.push(`${icon} ${todo.content}`);
5478
+ }
5479
+ if (inProgress > 0) {
5480
+ lines.push("");
5481
+ lines.push(`Currently: ${todos.find((t) => t.status === "in_progress")?.activeForm}`);
3832
5482
  }
5483
+ return {
5484
+ content: lines.join(`
5485
+ `)
5486
+ };
3833
5487
  }
3834
- } catch (e) {
3835
- return `DNS resolution failed for "${hostname}" (allowPrivateNetwork=false): ${e instanceof Error ? e.message : String(e)}`;
3836
- }
3837
- return null;
5488
+ };
3838
5489
  }
3839
- function htmlToMarkdown(html) {
3840
- let md = html;
3841
- md = md.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
3842
- md = md.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
3843
- md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, `
3844
- # $1
3845
- `);
3846
- md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, `
3847
- ## $1
3848
- `);
3849
- md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, `
3850
- ### $1
3851
- `);
3852
- md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, `
3853
- #### $1
3854
- `);
3855
- md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, `
3856
- ##### $1
3857
- `);
3858
- md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, `
3859
- ###### $1
3860
- `);
3861
- md = md.replace(/<p[^>]*>(.*?)<\/p>/gi, `
3862
- $1
3863
- `);
3864
- md = md.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, "[$2]($1)");
3865
- md = md.replace(/<(strong|b)[^>]*>(.*?)<\/\1>/gi, "**$2**");
3866
- md = md.replace(/<(em|i)[^>]*>(.*?)<\/\1>/gi, "*$2*");
3867
- md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
3868
- md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "\n```\n$1\n```\n");
3869
- md = md.replace(/<li[^>]*>(.*?)<\/li>/gi, `- $1
3870
- `);
3871
- md = md.replace(/<\/?[ou]l[^>]*>/gi, `
3872
- `);
3873
- md = md.replace(/<br\s*\/?>/gi, `
3874
- `);
3875
- md = md.replace(/<[^>]+>/g, "");
3876
- md = md.replace(/&nbsp;/g, " ");
3877
- md = md.replace(/&amp;/g, "&");
3878
- md = md.replace(/&lt;/g, "<");
3879
- md = md.replace(/&gt;/g, ">");
3880
- md = md.replace(/&quot;/g, '"');
3881
- md = md.replace(/\n{3,}/g, `
5490
+ var TodoWriteTool = createTodoWriteTool();
5491
+ // src/tools/builtin/askuser.ts
5492
+ var globalAskUserHandler = null;
5493
+ var ASKUSER_DESCRIPTION = `Ask the user questions to gather information, clarify requirements, or get decisions.
3882
5494
 
3883
- `);
3884
- md = md.trim();
3885
- return md;
3886
- }
3887
- function createWebFetchTool(options = {}) {
5495
+ Use this tool when you need to:
5496
+ - Gather user preferences or requirements
5497
+ - Clarify ambiguous instructions
5498
+ - Get decisions on implementation choices
5499
+ - Offer choices about what direction to take
5500
+
5501
+ Parameters:
5502
+ - questions: Array of questions (1-4 questions per call)
5503
+ - question: The question text (required)
5504
+ - header: Short label for display (optional)
5505
+ - options: Array of choices (optional)
5506
+ - label: Display text
5507
+ - description: Explanation of the option
5508
+ - multiSelect: Allow multiple selections (default: false)
5509
+ - defaultValue: Default if no response
5510
+
5511
+ Best practices:
5512
+ - Keep questions clear and concise
5513
+ - Provide options when there are known choices
5514
+ - Use multiSelect for non-mutually-exclusive options
5515
+ - Limit to 4 questions per call to avoid overwhelming the user`;
5516
+ function createAskUserTool(options = {}) {
3888
5517
  return {
3889
- name: "WebFetch",
3890
- description: `Fetch content from a URL. Converts HTML to markdown for readability. Use for retrieving web pages, documentation, or API responses.`,
5518
+ name: "AskUser",
5519
+ description: ASKUSER_DESCRIPTION,
3891
5520
  inputSchema: {
3892
5521
  type: "object",
3893
5522
  properties: {
3894
- url: {
3895
- type: "string",
3896
- description: "URL to fetch"
3897
- },
3898
- prompt: {
3899
- type: "string",
3900
- description: "Optional prompt to describe what information to extract"
5523
+ questions: {
5524
+ type: "array",
5525
+ description: "Questions to ask the user (1-4 questions)",
5526
+ items: {
5527
+ type: "object",
5528
+ properties: {
5529
+ question: {
5530
+ type: "string",
5531
+ description: "The question text to display"
5532
+ },
5533
+ header: {
5534
+ type: "string",
5535
+ description: "Short label for the question (max 12 chars)"
5536
+ },
5537
+ options: {
5538
+ type: "array",
5539
+ description: "Available choices (2-4 options)",
5540
+ items: {
5541
+ type: "object",
5542
+ properties: {
5543
+ label: {
5544
+ type: "string",
5545
+ description: "Display text for this option"
5546
+ },
5547
+ description: {
5548
+ type: "string",
5549
+ description: "Explanation of what this option means"
5550
+ }
5551
+ },
5552
+ required: ["label"]
5553
+ }
5554
+ },
5555
+ multiSelect: {
5556
+ type: "boolean",
5557
+ description: "Allow multiple selections (default: false)"
5558
+ },
5559
+ defaultValue: {
5560
+ type: "string",
5561
+ description: "Default value if user doesn't respond"
5562
+ }
5563
+ },
5564
+ required: ["question"]
5565
+ },
5566
+ minItems: 1,
5567
+ maxItems: 4
3901
5568
  }
3902
5569
  },
3903
- required: ["url"]
5570
+ required: ["questions"]
3904
5571
  },
3905
- execute: async (rawInput, _context) => {
5572
+ execute: async (rawInput, context) => {
3906
5573
  const input = rawInput;
3907
- const { url, prompt } = input;
3908
- let parsedUrl;
3909
- try {
3910
- parsedUrl = new URL(url);
3911
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
3912
- return {
3913
- content: `Invalid URL protocol: ${parsedUrl.protocol}. Only http/https are allowed.`,
3914
- isError: true
3915
- };
3916
- }
3917
- if (parsedUrl.protocol === "http:")
3918
- parsedUrl.protocol = "https:";
3919
- } catch {
5574
+ const { questions } = input;
5575
+ if (!questions || !Array.isArray(questions) || questions.length === 0) {
3920
5576
  return {
3921
- content: `Invalid URL: ${url}`,
5577
+ content: "Error: At least one question is required.",
3922
5578
  isError: true
3923
5579
  };
3924
5580
  }
3925
- const ssrfDeny = await denyPrivateNetworkTargets(parsedUrl, options);
3926
- if (ssrfDeny) {
3927
- return { content: ssrfDeny, isError: true };
5581
+ if (questions.length > 4) {
5582
+ return {
5583
+ content: "Error: Maximum 4 questions per call. Please split into multiple calls.",
5584
+ isError: true
5585
+ };
5586
+ }
5587
+ if (!globalAskUserHandler) {
5588
+ return {
5589
+ content: `Error: AskUser handler not configured. The SDK user must call setAskUserHandler() to enable user interaction.
5590
+
5591
+ Questions that would have been asked:
5592
+ ${questions.map((q, i) => `${i + 1}. ${q.question}${q.options ? ` [Options: ${q.options.map((o) => o.label).join(", ")}]` : ""}`).join(`
5593
+ `)}
5594
+
5595
+ To enable this tool, the SDK user should implement an AskUser handler.`,
5596
+ isError: true
5597
+ };
3928
5598
  }
3929
5599
  try {
3930
- const controller = new AbortController;
3931
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
3932
- const response = await fetch(parsedUrl.toString(), {
3933
- signal: controller.signal,
3934
- headers: {
3935
- "User-Agent": "Mozilla/5.0 (compatible; OpenCode-Agent/1.0)",
3936
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
5600
+ const answers = await globalAskUserHandler(questions, context);
5601
+ const formatAnswer = (answer) => {
5602
+ if (answer === undefined || answer === null)
5603
+ return "(no answer)";
5604
+ if (Array.isArray(answer))
5605
+ return answer.join(", ") || "(no answer)";
5606
+ return answer || "(no answer)";
5607
+ };
5608
+ const formattedAnswers = questions.map((q, i) => `Q: "${q.question}"
5609
+ A: ${formatAnswer(answers[i])}`).join(`
5610
+
5611
+ `);
5612
+ return {
5613
+ content: `User has answered your questions:
5614
+
5615
+ ${formattedAnswers}
5616
+
5617
+ You can now continue with the user's answers in mind.`,
5618
+ metadata: {
5619
+ questions: questions.map((q) => q.question),
5620
+ answers
3937
5621
  }
3938
- });
3939
- clearTimeout(timeout);
3940
- if (!response.ok) {
3941
- return {
3942
- content: `HTTP error: ${response.status} ${response.statusText}`,
3943
- isError: true
3944
- };
3945
- }
3946
- const finalUrl = new URL(response.url);
3947
- if (finalUrl.host !== parsedUrl.host) {
3948
- return {
3949
- content: `Redirected to different host: ${response.url}
5622
+ };
5623
+ } catch (error) {
5624
+ return {
5625
+ content: `Error getting user response: ${error instanceof Error ? error.message : String(error)}`,
5626
+ isError: true
5627
+ };
5628
+ }
5629
+ }
5630
+ };
5631
+ }
5632
+ var AskUserTool = createAskUserTool();
5633
+ // src/tools/builtin/batch.ts
5634
+ var globalToolResolver = null;
5635
+ var DISALLOWED_TOOLS = new Set(["Batch", "batch", "AskUser", "askuser"]);
5636
+ var BATCH_DESCRIPTION = `Execute multiple tool calls in parallel for improved performance.
3950
5637
 
3951
- Please fetch the new URL if you want to continue.`
3952
- };
3953
- }
3954
- const ssrfDenyAfter = await denyPrivateNetworkTargets(finalUrl, options);
3955
- if (ssrfDenyAfter) {
3956
- return { content: `Redirect target denied: ${ssrfDenyAfter}`, isError: true };
3957
- }
3958
- const contentType = response.headers.get("content-type") || "";
3959
- let content = await response.text();
3960
- if (content.length > MAX_CONTENT_LENGTH) {
3961
- content = content.slice(0, MAX_CONTENT_LENGTH) + `
5638
+ Use this tool when you need to:
5639
+ - Read multiple files at once
5640
+ - Perform several independent operations
5641
+ - Speed up tasks that don't have dependencies
3962
5642
 
3963
- ... (content truncated)`;
3964
- }
3965
- if (contentType.includes("text/html")) {
3966
- content = htmlToMarkdown(content);
3967
- }
3968
- let output = `URL: ${response.url}
3969
- `;
3970
- output += `Content-Type: ${contentType}
5643
+ Parameters:
5644
+ - tool_calls: Array of tool calls (1-10 calls per batch)
5645
+ - tool: Name of the tool to execute
5646
+ - parameters: Parameters for that tool
3971
5647
 
3972
- `;
3973
- if (prompt) {
3974
- output += `Requested: ${prompt}
5648
+ Limitations:
5649
+ - Maximum 10 tool calls per batch
5650
+ - Cannot nest Batch calls (no batch within batch)
5651
+ - Cannot batch AskUser (requires sequential interaction)
5652
+ - All calls execute in parallel - don't batch dependent operations
3975
5653
 
3976
- `;
5654
+ Best practices:
5655
+ - Group independent operations (e.g., reading multiple unrelated files)
5656
+ - Don't batch operations that depend on each other's results
5657
+ - Use for bulk file reads, multiple greps, or parallel web fetches
5658
+
5659
+ Example:
5660
+ {
5661
+ "tool_calls": [
5662
+ { "tool": "Read", "parameters": { "file_path": "/path/to/file1.ts" } },
5663
+ { "tool": "Read", "parameters": { "file_path": "/path/to/file2.ts" } },
5664
+ { "tool": "Grep", "parameters": { "pattern": "TODO", "path": "./src" } }
5665
+ ]
5666
+ }`;
5667
+ function createBatchTool(options = {}, toolMap) {
5668
+ return {
5669
+ name: "Batch",
5670
+ description: BATCH_DESCRIPTION,
5671
+ inputSchema: {
5672
+ type: "object",
5673
+ properties: {
5674
+ tool_calls: {
5675
+ type: "array",
5676
+ description: "Array of tool calls to execute in parallel",
5677
+ items: {
5678
+ type: "object",
5679
+ properties: {
5680
+ tool: {
5681
+ type: "string",
5682
+ description: "Name of the tool to execute"
5683
+ },
5684
+ parameters: {
5685
+ type: "object",
5686
+ description: "Parameters for the tool",
5687
+ additionalProperties: true
5688
+ }
5689
+ },
5690
+ required: ["tool", "parameters"]
5691
+ },
5692
+ minItems: 1,
5693
+ maxItems: 10
3977
5694
  }
3978
- output += content;
5695
+ },
5696
+ required: ["tool_calls"]
5697
+ },
5698
+ execute: async (rawInput, context) => {
5699
+ const input = rawInput;
5700
+ const { tool_calls } = input;
5701
+ if (!tool_calls || !Array.isArray(tool_calls) || tool_calls.length === 0) {
3979
5702
  return {
3980
- content: output
5703
+ content: "Error: At least one tool call is required.",
5704
+ isError: true
3981
5705
  };
3982
- } catch (error) {
3983
- if (error instanceof Error && error.name === "AbortError") {
5706
+ }
5707
+ const callsToExecute = tool_calls.slice(0, 10);
5708
+ const discardedCalls = tool_calls.slice(10);
5709
+ const executeCall = async (call) => {
5710
+ const startTime = Date.now();
5711
+ try {
5712
+ if (DISALLOWED_TOOLS.has(call.tool)) {
5713
+ return {
5714
+ success: false,
5715
+ tool: call.tool,
5716
+ error: `Tool '${call.tool}' cannot be used in batch. Disallowed: ${Array.from(DISALLOWED_TOOLS).join(", ")}`,
5717
+ duration: Date.now() - startTime
5718
+ };
5719
+ }
5720
+ let toolExecute;
5721
+ if (toolMap) {
5722
+ const tool = toolMap.get(call.tool);
5723
+ if (tool) {
5724
+ toolExecute = tool.execute;
5725
+ }
5726
+ }
5727
+ if (!toolExecute && globalToolResolver) {
5728
+ toolExecute = globalToolResolver(call.tool);
5729
+ }
5730
+ if (!toolExecute) {
5731
+ const availableTools = toolMap ? Array.from(toolMap.keys()).filter((n) => !DISALLOWED_TOOLS.has(n)) : [];
5732
+ return {
5733
+ success: false,
5734
+ tool: call.tool,
5735
+ error: `Tool '${call.tool}' not found.${availableTools.length > 0 ? ` Available: ${availableTools.slice(0, 10).join(", ")}` : ""}`,
5736
+ duration: Date.now() - startTime
5737
+ };
5738
+ }
5739
+ const result = await toolExecute(call.parameters, context);
3984
5740
  return {
3985
- content: `Request timed out after ${FETCH_TIMEOUT}ms`,
3986
- isError: true
5741
+ success: !result.isError,
5742
+ tool: call.tool,
5743
+ result,
5744
+ duration: Date.now() - startTime
5745
+ };
5746
+ } catch (error) {
5747
+ return {
5748
+ success: false,
5749
+ tool: call.tool,
5750
+ error: error instanceof Error ? error.message : String(error),
5751
+ duration: Date.now() - startTime
3987
5752
  };
3988
5753
  }
3989
- return {
3990
- content: `Failed to fetch: ${error instanceof Error ? error.message : String(error)}`,
3991
- isError: true
3992
- };
5754
+ };
5755
+ const results = await Promise.all(callsToExecute.map(executeCall));
5756
+ for (const call of discardedCalls) {
5757
+ results.push({
5758
+ success: false,
5759
+ tool: call.tool,
5760
+ error: "Exceeded maximum of 10 tool calls per batch",
5761
+ duration: 0
5762
+ });
5763
+ }
5764
+ const successCount = results.filter((r) => r.success).length;
5765
+ const failCount = results.length - successCount;
5766
+ const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
5767
+ const formatResult = (r, index) => {
5768
+ if (r.success && r.result) {
5769
+ const content = typeof r.result.content === "string" ? r.result.content.slice(0, 500) + (r.result.content.length > 500 ? "..." : "") : JSON.stringify(r.result.content).slice(0, 500);
5770
+ return `[${index + 1}] ${r.tool}: SUCCESS (${r.duration}ms)
5771
+ ${content}`;
5772
+ } else {
5773
+ return `[${index + 1}] ${r.tool}: FAILED (${r.duration}ms)
5774
+ Error: ${r.error}`;
5775
+ }
5776
+ };
5777
+ const resultsOutput = results.map(formatResult).join(`
5778
+
5779
+ ---
5780
+
5781
+ `);
5782
+ const summary = failCount > 0 ? `Batch execution: ${successCount}/${results.length} succeeded, ${failCount} failed (${totalDuration}ms total)` : `Batch execution: All ${successCount} tools succeeded (${totalDuration}ms total)`;
5783
+ return {
5784
+ content: `${summary}
5785
+
5786
+ ${resultsOutput}`,
5787
+ metadata: {
5788
+ totalCalls: results.length,
5789
+ successful: successCount,
5790
+ failed: failCount,
5791
+ totalDuration,
5792
+ details: results.map((r) => ({
5793
+ tool: r.tool,
5794
+ success: r.success,
5795
+ duration: r.duration,
5796
+ error: r.error
5797
+ }))
5798
+ }
5799
+ };
5800
+ }
5801
+ };
5802
+ }
5803
+ var BatchTool = createBatchTool();
5804
+ // src/tools/builtin/httprequest.ts
5805
+ import { lookup as lookup2 } from "node:dns/promises";
5806
+ var DEFAULT_TIMEOUT2 = 30000;
5807
+ var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
5808
+ function isPrivateIp(ip) {
5809
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
5810
+ const parts = ip.split(".").map(Number);
5811
+ const [a, b] = parts;
5812
+ if (a === 10)
5813
+ return true;
5814
+ if (a === 127)
5815
+ return true;
5816
+ if (a === 0)
5817
+ return true;
5818
+ if (a === 169 && b === 254)
5819
+ return true;
5820
+ if (a === 172 && b >= 16 && b <= 31)
5821
+ return true;
5822
+ if (a === 192 && b === 168)
5823
+ return true;
5824
+ if (a === 100 && b >= 64 && b <= 127)
5825
+ return true;
5826
+ if (a >= 224)
5827
+ return true;
5828
+ }
5829
+ const normalized = ip.toLowerCase();
5830
+ if (normalized === "::" || normalized === "::1")
5831
+ return true;
5832
+ if (normalized.startsWith("fe80:"))
5833
+ return true;
5834
+ if (normalized.startsWith("fc") || normalized.startsWith("fd"))
5835
+ return true;
5836
+ return false;
5837
+ }
5838
+ async function validateUrl(url, options) {
5839
+ let parsedUrl;
5840
+ try {
5841
+ parsedUrl = new URL(url);
5842
+ } catch {
5843
+ return { valid: false, error: `Invalid URL: ${url}` };
5844
+ }
5845
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
5846
+ return { valid: false, error: `Invalid protocol: ${parsedUrl.protocol}. Only http/https allowed.` };
5847
+ }
5848
+ if (!options.allowPrivateNetwork) {
5849
+ const hostname = parsedUrl.hostname.toLowerCase();
5850
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
5851
+ return { valid: false, error: `Blocked: localhost/local hostnames not allowed` };
5852
+ }
5853
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && isPrivateIp(hostname)) {
5854
+ return { valid: false, error: `Blocked: private IP address ${hostname}` };
5855
+ }
5856
+ const resolveHostnames = options.resolveHostnames ?? true;
5857
+ if (resolveHostnames) {
5858
+ try {
5859
+ const addrs = await lookup2(hostname, { all: true, verbatim: true });
5860
+ for (const addr of addrs) {
5861
+ if (isPrivateIp(addr.address)) {
5862
+ return { valid: false, error: `Blocked: ${hostname} resolves to private IP ${addr.address}` };
5863
+ }
5864
+ }
5865
+ } catch (e) {
5866
+ return { valid: false, error: `DNS resolution failed for ${hostname}` };
3993
5867
  }
3994
5868
  }
3995
- };
3996
- }
3997
- var WebFetchTool = createWebFetchTool();
3998
- // src/tools/builtin/todo.ts
3999
- var globalTodos = [];
4000
- var onTodoChange = null;
4001
- function setTodoChangeCallback(callback) {
4002
- onTodoChange = callback;
4003
- }
4004
- function getTodos() {
4005
- return [...globalTodos];
4006
- }
4007
- function clearTodos() {
4008
- globalTodos = [];
4009
- onTodoChange?.(globalTodos);
5869
+ }
5870
+ return { valid: true, parsedUrl };
4010
5871
  }
4011
- function createTodoWriteTool(options = {}) {
5872
+ var HTTPREQUEST_DESCRIPTION = `Make HTTP requests to APIs and web services.
5873
+
5874
+ Use this tool when you need to:
5875
+ - Call REST APIs (GET, POST, PUT, DELETE, etc.)
5876
+ - Send data to web services
5877
+ - Fetch JSON data from APIs
5878
+ - Interact with webhooks
5879
+
5880
+ Parameters:
5881
+ - method: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
5882
+ - url: Full URL to request (required)
5883
+ - headers: Request headers as key-value pairs (optional)
5884
+ - body: Request body for POST/PUT/PATCH (optional, auto-serialized to JSON)
5885
+ - timeout: Request timeout in ms (default: 30000, max: 120000)
5886
+ - responseType: Expected response type - 'json', 'text', or 'binary' (default: auto-detect)
5887
+
5888
+ Security:
5889
+ - Private/local network access is blocked by default
5890
+ - Maximum response size: 5MB
5891
+
5892
+ Best practices:
5893
+ - Include appropriate Content-Type header for POST/PUT requests
5894
+ - Handle authentication via headers (Authorization, API keys, etc.)
5895
+ - Use responseType: 'json' when expecting JSON response
5896
+
5897
+ Example:
5898
+ {
5899
+ "method": "POST",
5900
+ "url": "https://api.example.com/data",
5901
+ "headers": {
5902
+ "Authorization": "Bearer token123",
5903
+ "Content-Type": "application/json"
5904
+ },
5905
+ "body": { "name": "test", "value": 42 }
5906
+ }`;
5907
+ function createHttpRequestTool(options = {}) {
4012
5908
  return {
4013
- name: "TodoWrite",
4014
- description: `Manage a task list to track progress on complex tasks. Use to plan work, track completed items, and show progress to the user.`,
5909
+ name: "HttpRequest",
5910
+ description: HTTPREQUEST_DESCRIPTION,
4015
5911
  inputSchema: {
4016
5912
  type: "object",
4017
5913
  properties: {
4018
- todos: {
4019
- type: "array",
4020
- description: "Updated todo list",
4021
- items: {
4022
- type: "object",
4023
- properties: {
4024
- content: {
4025
- type: "string",
4026
- description: "Task description (imperative form, e.g., 'Run tests')"
4027
- },
4028
- status: {
4029
- type: "string",
4030
- enum: ["pending", "in_progress", "completed"],
4031
- description: "Task status"
4032
- },
4033
- activeForm: {
4034
- type: "string",
4035
- description: "Present continuous form (e.g., 'Running tests')"
4036
- }
4037
- },
4038
- required: ["content", "status", "activeForm"]
4039
- }
5914
+ method: {
5915
+ type: "string",
5916
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
5917
+ description: "HTTP method"
5918
+ },
5919
+ url: {
5920
+ type: "string",
5921
+ description: "URL to request"
5922
+ },
5923
+ headers: {
5924
+ type: "object",
5925
+ description: "Request headers",
5926
+ additionalProperties: { type: "string" }
5927
+ },
5928
+ body: {
5929
+ description: "Request body (auto-serialized to JSON if object)"
5930
+ },
5931
+ timeout: {
5932
+ type: "number",
5933
+ description: "Request timeout in milliseconds (default: 30000, max: 120000)"
5934
+ },
5935
+ responseType: {
5936
+ type: "string",
5937
+ enum: ["json", "text", "binary"],
5938
+ description: "Expected response type (default: auto-detect)"
4040
5939
  }
4041
5940
  },
4042
- required: ["todos"]
5941
+ required: ["method", "url"]
4043
5942
  },
4044
- execute: async (rawInput, _context) => {
5943
+ execute: async (rawInput, context) => {
4045
5944
  const input = rawInput;
4046
- const { todos } = input;
4047
- for (const todo of todos) {
4048
- if (!todo.content || !todo.status || !todo.activeForm) {
5945
+ const { method, url, headers = {}, body, timeout, responseType } = input;
5946
+ const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
5947
+ if (!validMethods.includes(method)) {
5948
+ return {
5949
+ content: `Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(", ")}`,
5950
+ isError: true
5951
+ };
5952
+ }
5953
+ const urlValidation = await validateUrl(url, options);
5954
+ if (!urlValidation.valid) {
5955
+ return {
5956
+ content: urlValidation.error,
5957
+ isError: true
5958
+ };
5959
+ }
5960
+ const requestTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT2, 120000);
5961
+ const controller = new AbortController;
5962
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
5963
+ const signal = context.abortSignal ? AbortSignal.any([controller.signal, context.abortSignal]) : controller.signal;
5964
+ const requestHeaders = {
5965
+ "User-Agent": "FormAgent-SDK/1.0",
5966
+ ...headers
5967
+ };
5968
+ let requestBody;
5969
+ if (body !== undefined && ["POST", "PUT", "PATCH"].includes(method)) {
5970
+ if (typeof body === "string") {
5971
+ requestBody = body;
5972
+ } else {
5973
+ requestBody = JSON.stringify(body);
5974
+ if (!requestHeaders["Content-Type"] && !requestHeaders["content-type"]) {
5975
+ requestHeaders["Content-Type"] = "application/json";
5976
+ }
5977
+ }
5978
+ }
5979
+ try {
5980
+ const response = await fetch(urlValidation.parsedUrl.toString(), {
5981
+ method,
5982
+ headers: requestHeaders,
5983
+ body: requestBody,
5984
+ signal
5985
+ });
5986
+ clearTimeout(timeoutId);
5987
+ const contentLength = response.headers.get("content-length");
5988
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
4049
5989
  return {
4050
- content: "Invalid todo item: missing required fields (content, status, activeForm)",
5990
+ content: `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
4051
5991
  isError: true
4052
5992
  };
4053
5993
  }
4054
- if (!["pending", "in_progress", "completed"].includes(todo.status)) {
5994
+ const contentType = response.headers.get("content-type") || "";
5995
+ let responseBody;
5996
+ let responseText;
5997
+ const effectiveResponseType = responseType || (contentType.includes("application/json") ? "json" : "text");
5998
+ if (effectiveResponseType === "json") {
5999
+ try {
6000
+ responseBody = await response.json();
6001
+ responseText = JSON.stringify(responseBody, null, 2);
6002
+ } catch {
6003
+ responseText = await response.text();
6004
+ responseBody = responseText;
6005
+ }
6006
+ } else if (effectiveResponseType === "binary") {
6007
+ const buffer = await response.arrayBuffer();
6008
+ responseText = `[Binary data: ${buffer.byteLength} bytes]`;
6009
+ responseBody = { type: "binary", size: buffer.byteLength };
6010
+ } else {
6011
+ responseText = await response.text();
6012
+ responseBody = responseText;
6013
+ }
6014
+ if (responseText.length > 1e5) {
6015
+ responseText = responseText.slice(0, 1e5) + `
6016
+
6017
+ ... (truncated)`;
6018
+ }
6019
+ const statusEmoji = response.ok ? "✓" : "✗";
6020
+ const output = `${statusEmoji} ${method} ${url}
6021
+ Status: ${response.status} ${response.statusText}
6022
+ Content-Type: ${contentType}
6023
+
6024
+ Response:
6025
+ ${responseText}`;
6026
+ return {
6027
+ content: output,
6028
+ isError: !response.ok,
6029
+ metadata: {
6030
+ status: response.status,
6031
+ statusText: response.statusText,
6032
+ headers: Object.fromEntries(response.headers.entries()),
6033
+ body: responseBody
6034
+ }
6035
+ };
6036
+ } catch (error) {
6037
+ clearTimeout(timeoutId);
6038
+ if (error instanceof Error) {
6039
+ if (error.name === "AbortError") {
6040
+ return {
6041
+ content: `Request timed out after ${requestTimeout}ms`,
6042
+ isError: true
6043
+ };
6044
+ }
4055
6045
  return {
4056
- content: `Invalid status: ${todo.status}. Must be pending, in_progress, or completed.`,
6046
+ content: `Request failed: ${error.message}`,
4057
6047
  isError: true
4058
6048
  };
4059
6049
  }
6050
+ return {
6051
+ content: `Request failed: ${String(error)}`,
6052
+ isError: true
6053
+ };
4060
6054
  }
4061
- globalTodos = todos;
4062
- onTodoChange?.(globalTodos);
4063
- const completed = todos.filter((t) => t.status === "completed").length;
4064
- const inProgress = todos.filter((t) => t.status === "in_progress").length;
4065
- const pending = todos.filter((t) => t.status === "pending").length;
4066
- const lines = [
4067
- `Todo list updated (${completed}/${todos.length} completed)`,
4068
- ""
4069
- ];
4070
- for (const todo of todos) {
4071
- const icon = todo.status === "completed" ? "✓" : todo.status === "in_progress" ? "→" : "○";
4072
- lines.push(`${icon} ${todo.content}`);
4073
- }
4074
- if (inProgress > 0) {
4075
- lines.push("");
4076
- lines.push(`Currently: ${todos.find((t) => t.status === "in_progress")?.activeForm}`);
4077
- }
4078
- return {
4079
- content: lines.join(`
4080
- `)
4081
- };
4082
6055
  }
4083
6056
  };
4084
6057
  }
4085
- var TodoWriteTool = createTodoWriteTool();
6058
+ var HttpRequestTool = createHttpRequestTool();
4086
6059
  // src/tools/builtin/index.ts
4087
6060
  var builtinTools = [
4088
6061
  BashTool,
@@ -4092,14 +6065,18 @@ var builtinTools = [
4092
6065
  GlobTool,
4093
6066
  GrepTool,
4094
6067
  WebFetchTool,
4095
- TodoWriteTool
6068
+ WebSearchTool,
6069
+ HttpRequestTool,
6070
+ TodoWriteTool,
6071
+ AskUserTool,
6072
+ BatchTool
4096
6073
  ];
4097
6074
  // src/utils/env.ts
4098
6075
  import { existsSync as existsSync9, readFileSync } from "node:fs";
4099
- import { join as join6 } from "node:path";
6076
+ import { join as join7 } from "node:path";
4100
6077
  function loadEnvOverride(cwd) {
4101
6078
  const dir = cwd || process.cwd();
4102
- const envPath = join6(dir, ".env");
6079
+ const envPath = join7(dir, ".env");
4103
6080
  if (!existsSync9(envPath)) {
4104
6081
  return;
4105
6082
  }
@@ -4128,8 +6105,18 @@ function loadEnvOverride(cwd) {
4128
6105
 
4129
6106
  // src/cli/cli.ts
4130
6107
  loadEnvOverride();
4131
- var VERSION = "0.1.0";
4132
- var SKILLS_PATH = join7(homedir4(), ".claude");
6108
+ function getCliVersion() {
6109
+ try {
6110
+ const pkgUrl = new URL("../../package.json", import.meta.url);
6111
+ const raw = readFileSync2(pkgUrl, "utf-8");
6112
+ const parsed = JSON.parse(raw);
6113
+ return parsed.version ?? "0.0.0";
6114
+ } catch {
6115
+ return "0.0.0";
6116
+ }
6117
+ }
6118
+ var VERSION = getCliVersion();
6119
+ var SKILLS_PATH = join8(homedir4(), ".claude");
4133
6120
  var colors = {
4134
6121
  reset: "\x1B[0m",
4135
6122
  bold: "\x1B[1m",
@@ -4157,8 +6144,10 @@ var session = null;
4157
6144
  var totalInputTokens = 0;
4158
6145
  var totalOutputTokens = 0;
4159
6146
  var messageCount = 0;
6147
+ var currentProviderId = null;
6148
+ var currentModelOverride = null;
4160
6149
  function isGitRepo(dir) {
4161
- return existsSync10(join7(dir, ".git"));
6150
+ return existsSync10(join8(dir, ".git"));
4162
6151
  }
4163
6152
  function getOsVersion() {
4164
6153
  try {
@@ -4204,15 +6193,20 @@ ${c.bold("Interactive Commands:")}
4204
6193
  ${c.cyan("/clear")} Clear conversation history
4205
6194
  ${c.cyan("/tools")} List available tools
4206
6195
  ${c.cyan("/skills")} List available skills
6196
+ ${c.cyan("/models")} Show or switch provider/model
4207
6197
  ${c.cyan("/todos")} Show current todo list
4208
6198
  ${c.cyan("/usage")} Show token usage statistics
6199
+ ${c.cyan("/debug")} Show debug info (prompt, model, env)
4209
6200
  ${c.cyan("/exit")} Exit the CLI
4210
6201
 
4211
6202
  ${c.bold("Environment:")}
4212
6203
  ${c.cyan("ANTHROPIC_API_KEY")} Anthropic API key (for Claude models)
4213
6204
  ${c.cyan("ANTHROPIC_MODEL")} Optional. Claude model (default: claude-sonnet-4-20250514)
6205
+ ${c.cyan("GEMINI_API_KEY")} Gemini API key (for Gemini models)
6206
+ ${c.cyan("GEMINI_MODEL")} Optional. Gemini model (default: gemini-1.5-pro)
6207
+ ${c.cyan("GEMINI_BASE_URL")} Optional. Custom Gemini API base URL
4214
6208
  ${c.cyan("OPENAI_API_KEY")} OpenAI API key (for GPT models)
4215
- ${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-4o)
6209
+ ${c.cyan("OPENAI_MODEL")} Optional. OpenAI model (default: gpt-5.2)
4216
6210
  ${c.cyan("OPENAI_BASE_URL")} Optional. Custom OpenAI-compatible API URL
4217
6211
 
4218
6212
  ${c.bold("Examples:")}
@@ -4230,7 +6224,7 @@ function printVersion() {
4230
6224
  console.log(`formagent-sdk v${VERSION}`);
4231
6225
  }
4232
6226
  function printBanner() {
4233
- const model = getDefaultModel();
6227
+ const model = getActiveModel();
4234
6228
  console.log();
4235
6229
  console.log(c.cyan("╔═══════════════════════════════════════════════════════════╗"));
4236
6230
  console.log(c.cyan("║") + c.bold(" FormAgent CLI v" + VERSION + " ") + c.cyan("║"));
@@ -4238,6 +6232,7 @@ function printBanner() {
4238
6232
  console.log(c.cyan("╚═══════════════════════════════════════════════════════════╝"));
4239
6233
  console.log();
4240
6234
  console.log(c.dim(" Model: ") + c.green(model));
6235
+ console.log(c.dim(" Provider: ") + c.green(getActiveProviderId() ?? "auto"));
4241
6236
  console.log(c.dim(" Type your message and press Enter to chat."));
4242
6237
  console.log(c.dim(" Use /help for commands, /exit to quit."));
4243
6238
  console.log();
@@ -4250,8 +6245,10 @@ function printInteractiveHelp() {
4250
6245
  console.log(` ${c.cyan("/clear")} Clear conversation history`);
4251
6246
  console.log(` ${c.cyan("/tools")} List available tools`);
4252
6247
  console.log(` ${c.cyan("/skills")} List available skills`);
6248
+ console.log(` ${c.cyan("/models")} Show or switch provider/model`);
4253
6249
  console.log(` ${c.cyan("/todos")} Show current todo list`);
4254
6250
  console.log(` ${c.cyan("/usage")} Show token usage statistics`);
6251
+ console.log(` ${c.cyan("/debug")} Show debug info (prompt, model, env)`);
4255
6252
  console.log(` ${c.cyan("/exit")} Exit the CLI`);
4256
6253
  console.log();
4257
6254
  }
@@ -4322,6 +6319,298 @@ function printUsage() {
4322
6319
  console.log(` ${c.cyan("Est. cost:")} $${(inputCost + outputCost).toFixed(4)}`);
4323
6320
  console.log();
4324
6321
  }
6322
+ async function resetSessionForModelChange() {
6323
+ if (session) {
6324
+ await session.close();
6325
+ session = null;
6326
+ }
6327
+ totalInputTokens = 0;
6328
+ totalOutputTokens = 0;
6329
+ messageCount = 0;
6330
+ }
6331
+ function printModelsHelp() {
6332
+ const provider = getActiveProviderId() ?? "auto";
6333
+ const model = getActiveModel();
6334
+ console.log();
6335
+ console.log(c.bold("Model Selection:"));
6336
+ console.log();
6337
+ console.log(` ${c.cyan("Current provider:")} ${provider}`);
6338
+ console.log(` ${c.cyan("Current model:")} ${model}`);
6339
+ console.log();
6340
+ console.log(c.bold("Usage:"));
6341
+ console.log(` ${c.cyan("/models")}`);
6342
+ console.log(c.dim(" List models for the active provider"));
6343
+ console.log(` ${c.cyan("/models")} openai gpt-5-mini`);
6344
+ console.log(` ${c.cyan("/models")} anthropic claude-sonnet-4-20250514`);
6345
+ console.log(` ${c.cyan("/models")} gemini gemini-1.5-pro`);
6346
+ console.log(` ${c.cyan("/models")} gpt-5.2`);
6347
+ console.log(` ${c.cyan("/models")} reset`);
6348
+ console.log();
6349
+ }
6350
+ async function handleModelsCommand(args) {
6351
+ if (args.length === 0) {
6352
+ await listModelsSummary();
6353
+ return;
6354
+ }
6355
+ if (args[0].toLowerCase() === "reset") {
6356
+ currentProviderId = null;
6357
+ currentModelOverride = null;
6358
+ await resetSessionForModelChange();
6359
+ console.log(c.green(`
6360
+ ✓ Model selection reset to environment defaults.
6361
+ `));
6362
+ return;
6363
+ }
6364
+ if (args.length === 1) {
6365
+ const provider2 = parseProvider(args[0]);
6366
+ if (provider2) {
6367
+ currentProviderId = provider2;
6368
+ currentModelOverride = null;
6369
+ await resetSessionForModelChange();
6370
+ console.log(c.green(`
6371
+ ✓ Provider set to ${provider2}. Model: ${getActiveModel()}.
6372
+ `));
6373
+ return;
6374
+ }
6375
+ currentModelOverride = args[0];
6376
+ currentProviderId = inferProviderFromModel(args[0]) ?? currentProviderId;
6377
+ await resetSessionForModelChange();
6378
+ console.log(c.green(`
6379
+ ✓ Model set to ${currentModelOverride} (provider: ${getActiveProviderId() ?? "auto"}).
6380
+ `));
6381
+ return;
6382
+ }
6383
+ const provider = parseProvider(args[0]);
6384
+ if (!provider) {
6385
+ console.log(c.yellow(`
6386
+ Unknown provider: ${args[0]}. Use "openai", "anthropic", or "gemini".
6387
+ `));
6388
+ return;
6389
+ }
6390
+ const model = args.slice(1).join(" ");
6391
+ if (!model) {
6392
+ console.log(c.yellow(`
6393
+ Missing model name. Example: /models openai gpt-5-mini
6394
+ `));
6395
+ return;
6396
+ }
6397
+ currentProviderId = provider;
6398
+ currentModelOverride = model;
6399
+ await resetSessionForModelChange();
6400
+ console.log(c.green(`
6401
+ ✓ Provider set to ${provider}, model set to ${model}.
6402
+ `));
6403
+ }
6404
+ function normalizeOpenAIBaseUrl(baseUrl) {
6405
+ const trimmed = baseUrl.replace(/\/+$/, "");
6406
+ if (trimmed.endsWith("/v1")) {
6407
+ return trimmed;
6408
+ }
6409
+ return `${trimmed}/v1`;
6410
+ }
6411
+ function getOpenAIApiType(baseUrl) {
6412
+ const normalized = baseUrl.toLowerCase();
6413
+ return normalized.includes("api.openai.com") ? "openai" : "openai-compatible";
6414
+ }
6415
+ function isGoogleGeminiBaseUrl(baseUrl) {
6416
+ const normalized = baseUrl.toLowerCase();
6417
+ return normalized.includes("generativelanguage.googleapis.com") || normalized.includes("/v1beta");
6418
+ }
6419
+ async function listAnthropicModels() {
6420
+ const baseUrlRaw = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/+$/, "");
6421
+ const apiKey = process.env.ANTHROPIC_API_KEY;
6422
+ const baseUrl = baseUrlRaw.endsWith("/v1") ? baseUrlRaw : `${baseUrlRaw}/v1`;
6423
+ console.log(c.bold("Anthropic Models:"));
6424
+ console.log(c.dim(" API Type: anthropic (official)"));
6425
+ console.log(c.dim(` Base URL: ${baseUrl}`));
6426
+ if (!apiKey) {
6427
+ console.log(c.red(" ✗ ANTHROPIC_API_KEY not set"));
6428
+ console.log();
6429
+ return;
6430
+ }
6431
+ const res = await fetch(`${baseUrl}/models`, {
6432
+ headers: {
6433
+ "x-api-key": apiKey,
6434
+ "anthropic-version": "2023-06-01"
6435
+ }
6436
+ });
6437
+ if (!res.ok) {
6438
+ console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
6439
+ console.log(c.dim(` URL: ${baseUrl}/models`));
6440
+ console.log();
6441
+ return;
6442
+ }
6443
+ const payload = await res.json();
6444
+ const items = payload.data ?? [];
6445
+ for (const item of items) {
6446
+ const name = item.display_name ? ` (${item.display_name})` : "";
6447
+ console.log(` ${c.green("●")} ${item.id}${name}`);
6448
+ }
6449
+ console.log();
6450
+ }
6451
+ async function listOpenAIModels() {
6452
+ const baseUrl = normalizeOpenAIBaseUrl(process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1");
6453
+ const apiFlavor = getOpenAIApiType(baseUrl);
6454
+ const apiKey = process.env.OPENAI_API_KEY;
6455
+ console.log(c.bold("OpenAI Models:"));
6456
+ console.log(c.dim(` API Type: ${apiFlavor}`));
6457
+ console.log(c.dim(` Base URL: ${baseUrl}`));
6458
+ if (!apiKey) {
6459
+ console.log(c.red(" ✗ OPENAI_API_KEY not set"));
6460
+ console.log();
6461
+ return;
6462
+ }
6463
+ const res = await fetch(`${baseUrl}/models`, {
6464
+ headers: { Authorization: `Bearer ${apiKey}` }
6465
+ });
6466
+ if (!res.ok) {
6467
+ console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
6468
+ console.log(c.dim(` URL: ${baseUrl}/models`));
6469
+ console.log();
6470
+ return;
6471
+ }
6472
+ const payload = await res.json();
6473
+ const items = payload.data ?? [];
6474
+ for (const item of items) {
6475
+ const owner = item.owned_by ? ` (${item.owned_by})` : "";
6476
+ console.log(` ${c.green("●")} ${item.id}${owner}`);
6477
+ }
6478
+ console.log();
6479
+ }
6480
+ async function listGeminiModels() {
6481
+ const baseUrlRaw = (process.env.GEMINI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "");
6482
+ const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
6483
+ console.log(c.bold("Gemini Models:"));
6484
+ console.log(c.dim(` Base URL: ${baseUrlRaw}`));
6485
+ if (!apiKey) {
6486
+ console.log(c.red(" ✗ GEMINI_API_KEY not set"));
6487
+ console.log();
6488
+ return;
6489
+ }
6490
+ if (isGoogleGeminiBaseUrl(baseUrlRaw)) {
6491
+ console.log(c.dim(" API Type: gemini"));
6492
+ const url = `${baseUrlRaw}/models`;
6493
+ const res2 = await fetch(url, {
6494
+ headers: { "x-goog-api-key": apiKey }
6495
+ });
6496
+ if (!res2.ok) {
6497
+ console.log(c.red(` ✗ Failed to fetch models (${res2.status})`));
6498
+ console.log(c.dim(` URL: ${url}`));
6499
+ console.log();
6500
+ return;
6501
+ }
6502
+ const payload2 = await res2.json();
6503
+ const items2 = payload2.models ?? [];
6504
+ for (const item of items2) {
6505
+ console.log(` ${c.green("●")} ${item.name}`);
6506
+ }
6507
+ console.log();
6508
+ return;
6509
+ }
6510
+ const openaiBase = normalizeOpenAIBaseUrl(baseUrlRaw);
6511
+ console.log(c.dim(" API Type: openai-compatible"));
6512
+ console.log(c.dim(" Auth: Bearer (GEMINI_API_KEY)"));
6513
+ const res = await fetch(`${openaiBase}/models`, {
6514
+ headers: { Authorization: `Bearer ${apiKey}` }
6515
+ });
6516
+ if (!res.ok) {
6517
+ console.log(c.red(` ✗ Failed to fetch models (${res.status})`));
6518
+ console.log(c.dim(` URL: ${openaiBase}/models`));
6519
+ console.log();
6520
+ return;
6521
+ }
6522
+ const payload = await res.json();
6523
+ const items = payload.data ?? [];
6524
+ for (const item of items) {
6525
+ const owner = item.owned_by ? ` (${item.owned_by})` : "";
6526
+ console.log(` ${c.green("●")} ${item.id}${owner}`);
6527
+ }
6528
+ console.log();
6529
+ }
6530
+ async function listModelsSummary() {
6531
+ const provider = getActiveProviderId();
6532
+ const apiType = provider ?? "auto";
6533
+ console.log();
6534
+ console.log(c.bold("Available Models:"));
6535
+ console.log(c.dim(` Active Provider: ${apiType}`));
6536
+ console.log();
6537
+ printModelsHelp();
6538
+ try {
6539
+ await listOpenAIModels();
6540
+ } catch (error) {
6541
+ console.log(c.red(` ✗ OpenAI: ${error instanceof Error ? error.message : String(error)}`));
6542
+ console.log();
6543
+ }
6544
+ try {
6545
+ await listGeminiModels();
6546
+ } catch (error) {
6547
+ console.log(c.red(` ✗ Gemini: ${error instanceof Error ? error.message : String(error)}`));
6548
+ console.log();
6549
+ }
6550
+ await listAnthropicModels();
6551
+ }
6552
+ function printDebug() {
6553
+ const model = getActiveModel();
6554
+ const tools = getAllTools();
6555
+ const systemPrompt = buildSystemPrompt();
6556
+ const cwd = process.cwd();
6557
+ console.log();
6558
+ console.log(c.bold("═══════════════════════════════════════════════════════════"));
6559
+ console.log(c.bold(" DEBUG INFORMATION "));
6560
+ console.log(c.bold("═══════════════════════════════════════════════════════════"));
6561
+ console.log();
6562
+ console.log(c.bold("Model:"));
6563
+ console.log(` ${c.cyan("Current:")} ${model}`);
6564
+ console.log(` ${c.cyan("Provider:")} ${getActiveProviderId() ?? "auto"}`);
6565
+ console.log(` ${c.cyan("Override:")} ${currentModelOverride ?? c.dim("(not set)")}`);
6566
+ console.log(` ${c.cyan("ANTHROPIC_MODEL:")} ${process.env.ANTHROPIC_MODEL || c.dim("(not set)")}`);
6567
+ console.log(` ${c.cyan("GEMINI_MODEL:")} ${process.env.GEMINI_MODEL || c.dim("(not set)")}`);
6568
+ console.log(` ${c.cyan("GEMINI_BASE_URL:")} ${process.env.GEMINI_BASE_URL || c.dim("(not set)")}`);
6569
+ console.log(` ${c.cyan("OPENAI_MODEL:")} ${process.env.OPENAI_MODEL || c.dim("(not set)")}`);
6570
+ console.log(` ${c.cyan("OPENAI_BASE_URL:")} ${process.env.OPENAI_BASE_URL || c.dim("(not set)")}`);
6571
+ console.log();
6572
+ console.log(c.bold("API Keys:"));
6573
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
6574
+ const geminiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
6575
+ const openaiKey = process.env.OPENAI_API_KEY;
6576
+ console.log(` ${c.cyan("ANTHROPIC_API_KEY:")} ${anthropicKey ? c.green("✓ set") + c.dim(` (${anthropicKey.slice(0, 8)}...${anthropicKey.slice(-4)})`) : c.red("✗ not set")}`);
6577
+ console.log(` ${c.cyan("GEMINI_API_KEY:")} ${geminiKey ? c.green("✓ set") + c.dim(` (${geminiKey.slice(0, 8)}...${geminiKey.slice(-4)})`) : c.red("✗ not set")}`);
6578
+ console.log(` ${c.cyan("OPENAI_API_KEY:")} ${openaiKey ? c.green("✓ set") + c.dim(` (${openaiKey.slice(0, 8)}...${openaiKey.slice(-4)})`) : c.red("✗ not set")}`);
6579
+ console.log();
6580
+ console.log(c.bold("Environment:"));
6581
+ console.log(` ${c.cyan("Working dir:")} ${cwd}`);
6582
+ console.log(` ${c.cyan("Git repo:")} ${isGitRepo(cwd) ? c.green("Yes") : "No"}`);
6583
+ console.log(` ${c.cyan("Platform:")} ${process.platform}`);
6584
+ console.log(` ${c.cyan("OS Version:")} ${getOsVersion()}`);
6585
+ console.log(` ${c.cyan("Shell:")} ${process.env.SHELL || c.dim("(not set)")}`);
6586
+ console.log(` ${c.cyan("Skills path:")} ${SKILLS_PATH}`);
6587
+ console.log();
6588
+ console.log(c.bold("Tools:") + c.dim(` (${tools.length} total)`));
6589
+ const toolNames = tools.map((t) => t.name);
6590
+ console.log(` ${toolNames.join(", ")}`);
6591
+ console.log();
6592
+ console.log(c.bold("Session State:"));
6593
+ console.log(` ${c.cyan("Active:")} ${session ? c.green("Yes") : "No"}`);
6594
+ console.log(` ${c.cyan("Messages:")} ${messageCount}`);
6595
+ console.log(` ${c.cyan("Input tokens:")} ${totalInputTokens.toLocaleString()}`);
6596
+ console.log(` ${c.cyan("Output tokens:")} ${totalOutputTokens.toLocaleString()}`);
6597
+ console.log();
6598
+ console.log(c.bold("System Prompt:") + c.dim(` (${systemPrompt.length} chars)`));
6599
+ console.log(c.dim("─".repeat(60)));
6600
+ const promptLines = systemPrompt.split(`
6601
+ `);
6602
+ const maxLines = 50;
6603
+ for (let i = 0;i < Math.min(promptLines.length, maxLines); i++) {
6604
+ const lineNum = String(i + 1).padStart(3, " ");
6605
+ const line = promptLines[i].slice(0, 75);
6606
+ console.log(`${c.dim(lineNum + "│")} ${line}${promptLines[i].length > 75 ? c.dim("...") : ""}`);
6607
+ }
6608
+ if (promptLines.length > maxLines) {
6609
+ console.log(c.dim(` ... (${promptLines.length - maxLines} more lines)`));
6610
+ }
6611
+ console.log(c.dim("─".repeat(60)));
6612
+ console.log();
6613
+ }
4325
6614
  function formatToolInput(name, input) {
4326
6615
  switch (name) {
4327
6616
  case "Bash":
@@ -4349,19 +6638,94 @@ function formatToolInput(name, input) {
4349
6638
  return JSON.stringify(input).slice(0, 50);
4350
6639
  }
4351
6640
  }
4352
- function getDefaultModel() {
6641
+ function getDefaultProviderFromEnv() {
4353
6642
  if (process.env.ANTHROPIC_API_KEY) {
4354
- return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
6643
+ return "anthropic";
4355
6644
  }
4356
6645
  if (process.env.OPENAI_API_KEY) {
4357
- return process.env.OPENAI_MODEL || "gpt-4o";
6646
+ return "openai";
6647
+ }
6648
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
6649
+ return "gemini";
6650
+ }
6651
+ return null;
6652
+ }
6653
+ function inferProviderFromModel(model) {
6654
+ const normalized = model.toLowerCase();
6655
+ if (normalized.startsWith("claude")) {
6656
+ return "anthropic";
6657
+ }
6658
+ if (normalized.startsWith("gpt") || normalized.startsWith("o1") || normalized.startsWith("chatgpt")) {
6659
+ return "openai";
6660
+ }
6661
+ if (normalized.startsWith("gemini") || normalized.startsWith("models/gemini")) {
6662
+ return "gemini";
6663
+ }
6664
+ return null;
6665
+ }
6666
+ function getDefaultModelForProvider(providerId) {
6667
+ if (providerId === "anthropic") {
6668
+ return process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514";
6669
+ }
6670
+ if (providerId === "gemini") {
6671
+ return process.env.GEMINI_MODEL || "gemini-1.5-pro";
6672
+ }
6673
+ return process.env.OPENAI_MODEL || "gpt-5.2";
6674
+ }
6675
+ function getActiveProviderId() {
6676
+ if (currentProviderId) {
6677
+ return currentProviderId;
6678
+ }
6679
+ if (currentModelOverride) {
6680
+ return inferProviderFromModel(currentModelOverride);
6681
+ }
6682
+ return getDefaultProviderFromEnv();
6683
+ }
6684
+ function getActiveModel() {
6685
+ if (currentModelOverride) {
6686
+ return currentModelOverride;
6687
+ }
6688
+ const provider = getActiveProviderId();
6689
+ if (provider) {
6690
+ return getDefaultModelForProvider(provider);
4358
6691
  }
4359
6692
  return "claude-sonnet-4-20250514";
4360
6693
  }
6694
+ function parseProvider(arg) {
6695
+ const normalized = arg.toLowerCase();
6696
+ if (normalized === "anthropic" || normalized === "claude") {
6697
+ return "anthropic";
6698
+ }
6699
+ if (normalized === "openai" || normalized === "gpt") {
6700
+ return "openai";
6701
+ }
6702
+ if (normalized === "gemini" || normalized === "google") {
6703
+ return "gemini";
6704
+ }
6705
+ return null;
6706
+ }
6707
+ function createProvider(providerId) {
6708
+ if (providerId === "anthropic") {
6709
+ return new AnthropicProvider;
6710
+ }
6711
+ if (providerId === "gemini") {
6712
+ return new GeminiProvider({
6713
+ apiKey: process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY,
6714
+ baseUrl: process.env.GEMINI_BASE_URL
6715
+ });
6716
+ }
6717
+ return new OpenAIProvider({
6718
+ apiKey: process.env.OPENAI_API_KEY,
6719
+ baseUrl: process.env.OPENAI_BASE_URL
6720
+ });
6721
+ }
4361
6722
  async function getSession() {
4362
6723
  if (!session) {
6724
+ const providerId = getActiveProviderId();
6725
+ const provider = providerId ? createProvider(providerId) : undefined;
4363
6726
  session = await createSession({
4364
- model: getDefaultModel(),
6727
+ model: getActiveModel(),
6728
+ provider,
4365
6729
  tools: getAllTools(),
4366
6730
  systemPrompt: buildSystemPrompt()
4367
6731
  });
@@ -4419,7 +6783,9 @@ async function handleInput(input) {
4419
6783
  return true;
4420
6784
  }
4421
6785
  if (trimmed.startsWith("/")) {
4422
- const cmd = trimmed.toLowerCase();
6786
+ const parts = trimmed.split(/\s+/);
6787
+ const cmd = parts[0].toLowerCase();
6788
+ const args = parts.slice(1);
4423
6789
  switch (cmd) {
4424
6790
  case "/help":
4425
6791
  printInteractiveHelp();
@@ -4443,12 +6809,18 @@ async function handleInput(input) {
4443
6809
  case "/skills":
4444
6810
  await printSkills();
4445
6811
  return true;
6812
+ case "/models":
6813
+ await handleModelsCommand(args);
6814
+ return true;
4446
6815
  case "/todos":
4447
6816
  printTodos();
4448
6817
  return true;
4449
6818
  case "/usage":
4450
6819
  printUsage();
4451
6820
  return true;
6821
+ case "/debug":
6822
+ printDebug();
6823
+ return true;
4452
6824
  case "/exit":
4453
6825
  case "/quit":
4454
6826
  case "/q":
@@ -4472,9 +6844,9 @@ Error: ${error instanceof Error ? error.message : String(error)}
4472
6844
  return true;
4473
6845
  }
4474
6846
  async function runQuickQuery(query) {
4475
- if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
6847
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
4476
6848
  console.error(c.red("Error: No API key found"));
4477
- console.error(c.dim("Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
6849
+ console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
4478
6850
  process.exit(1);
4479
6851
  }
4480
6852
  try {
@@ -4488,9 +6860,9 @@ async function runQuickQuery(query) {
4488
6860
  }
4489
6861
  }
4490
6862
  async function runInteractive() {
4491
- if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
6863
+ if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) {
4492
6864
  console.error(c.red("Error: No API key found"));
4493
- console.error(c.dim("Set ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable"));
6865
+ console.error(c.dim("Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY environment variable"));
4494
6866
  process.exit(1);
4495
6867
  }
4496
6868
  setTodoChangeCallback(() => {});