formagent-sdk 0.1.3 → 0.2.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 +1044 -48
  2. package/dist/index.js +1266 -52
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -5,7 +5,7 @@ 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
10
 
11
11
  // src/utils/id.ts
@@ -76,6 +76,113 @@ class TypedEventEmitter {
76
76
  }
77
77
  }
78
78
 
79
+ // src/utils/truncation.ts
80
+ import * as fs from "node:fs/promises";
81
+ import * as path from "node:path";
82
+ import * as os from "node:os";
83
+ var TRUNCATION_DEFAULTS = {
84
+ MAX_LINES: 2000,
85
+ MAX_BYTES: 50 * 1024,
86
+ RETENTION_MS: 7 * 24 * 60 * 60 * 1000
87
+ };
88
+ function getTempDir(config) {
89
+ return config?.tempDir ?? path.join(os.tmpdir(), "formagent-sdk-output");
90
+ }
91
+ function generateOutputFilename() {
92
+ const timestamp = Date.now();
93
+ const random = Math.random().toString(36).substring(2, 8);
94
+ return `tool_${timestamp}_${random}.txt`;
95
+ }
96
+ async function ensureTempDir(dir) {
97
+ try {
98
+ await fs.mkdir(dir, { recursive: true });
99
+ } catch {}
100
+ }
101
+ async function truncateOutput(text, config = {}) {
102
+ const maxLines = config.maxLines ?? TRUNCATION_DEFAULTS.MAX_LINES;
103
+ const maxBytes = config.maxBytes ?? TRUNCATION_DEFAULTS.MAX_BYTES;
104
+ const direction = config.direction ?? "head";
105
+ const saveToFile = config.saveToFile ?? true;
106
+ const lines = text.split(`
107
+ `);
108
+ const totalBytes = Buffer.byteLength(text, "utf-8");
109
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
110
+ return {
111
+ content: text,
112
+ truncated: false,
113
+ originalBytes: totalBytes,
114
+ originalLines: lines.length
115
+ };
116
+ }
117
+ const out = [];
118
+ let bytes = 0;
119
+ let hitBytes = false;
120
+ if (direction === "head") {
121
+ for (let i = 0;i < lines.length && i < maxLines; i++) {
122
+ const lineBytes = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0);
123
+ if (bytes + lineBytes > maxBytes) {
124
+ hitBytes = true;
125
+ break;
126
+ }
127
+ out.push(lines[i]);
128
+ bytes += lineBytes;
129
+ }
130
+ } else {
131
+ for (let i = lines.length - 1;i >= 0 && out.length < maxLines; i--) {
132
+ const lineBytes = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0);
133
+ if (bytes + lineBytes > maxBytes) {
134
+ hitBytes = true;
135
+ break;
136
+ }
137
+ out.unshift(lines[i]);
138
+ bytes += lineBytes;
139
+ }
140
+ }
141
+ const removed = hitBytes ? totalBytes - bytes : lines.length - out.length;
142
+ const unit = hitBytes ? "bytes" : "lines";
143
+ const preview = out.join(`
144
+ `);
145
+ let outputPath;
146
+ if (saveToFile) {
147
+ const dir = getTempDir(config);
148
+ await ensureTempDir(dir);
149
+ outputPath = path.join(dir, generateOutputFilename());
150
+ await fs.writeFile(outputPath, text, "utf-8");
151
+ }
152
+ const hint = outputPath ? `Full output saved to: ${outputPath}
153
+ Use Read tool with offset/limit to view specific sections, or Grep to search the content.` : "Output was truncated. Consider using more specific queries.";
154
+ const message = direction === "head" ? `${preview}
155
+
156
+ ...${removed} ${unit} truncated...
157
+
158
+ ${hint}` : `...${removed} ${unit} truncated...
159
+
160
+ ${hint}
161
+
162
+ ${preview}`;
163
+ return {
164
+ content: message,
165
+ truncated: true,
166
+ outputPath,
167
+ originalBytes: totalBytes,
168
+ originalLines: lines.length,
169
+ truncatedBytes: bytes,
170
+ truncatedLines: out.length
171
+ };
172
+ }
173
+ async function truncateToolOutput(output, config) {
174
+ const result = await truncateOutput(output, config);
175
+ return result.content;
176
+ }
177
+ function needsTruncation(text, config = {}) {
178
+ const maxLines = config.maxLines ?? TRUNCATION_DEFAULTS.MAX_LINES;
179
+ const maxBytes = config.maxBytes ?? TRUNCATION_DEFAULTS.MAX_BYTES;
180
+ const lines = text.split(`
181
+ `);
182
+ const bytes = Buffer.byteLength(text, "utf-8");
183
+ return lines.length > maxLines || bytes > maxBytes;
184
+ }
185
+
79
186
  // src/hooks/manager.ts
80
187
  class HookTimeoutError extends Error {
81
188
  constructor(hookName, timeout) {
@@ -312,7 +419,7 @@ class HooksManager {
312
419
  // src/skills/loader.ts
313
420
  import { existsSync, readdirSync, statSync } from "node:fs";
314
421
  import { readFile } from "node:fs/promises";
315
- import { join, dirname, basename } from "node:path";
422
+ import { join as join2, dirname, basename } from "node:path";
316
423
  import { homedir } from "node:os";
317
424
 
318
425
  // src/utils/frontmatter.ts
@@ -427,13 +534,13 @@ class SkillLoader {
427
534
  } = options;
428
535
  const allDirs = [...directories];
429
536
  if (includeUserSkills) {
430
- const userSkillsDir = join(homedir(), USER_SKILLS_DIR);
537
+ const userSkillsDir = join2(homedir(), USER_SKILLS_DIR);
431
538
  if (existsSync(userSkillsDir)) {
432
539
  allDirs.push(userSkillsDir);
433
540
  }
434
541
  }
435
542
  if (includeProjectSkills && this.projectRoot) {
436
- const projectSkillsDir = join(this.projectRoot, PROJECT_SKILLS_DIR);
543
+ const projectSkillsDir = join2(this.projectRoot, PROJECT_SKILLS_DIR);
437
544
  if (existsSync(projectSkillsDir)) {
438
545
  allDirs.push(projectSkillsDir);
439
546
  }
@@ -449,8 +556,8 @@ class SkillLoader {
449
556
  if (this.skills.has(skillId)) {
450
557
  return this.skills.get(skillId);
451
558
  }
452
- for (const path of this.discoveredPaths) {
453
- const skill = await this.parseSkillFile(path);
559
+ for (const path2 of this.discoveredPaths) {
560
+ const skill = await this.parseSkillFile(path2);
454
561
  if (skill && skill.id === skillId) {
455
562
  this.skills.set(skill.id, skill);
456
563
  return skill;
@@ -550,10 +657,10 @@ class SkillLoader {
550
657
  try {
551
658
  const entries = readdirSync(dir);
552
659
  for (const entry of entries) {
553
- const fullPath = join(dir, entry);
660
+ const fullPath = join2(dir, entry);
554
661
  const stat = statSync(fullPath);
555
662
  if (stat.isDirectory()) {
556
- const skillFile = join(fullPath, SKILL_FILE_NAME);
663
+ const skillFile = join2(fullPath, SKILL_FILE_NAME);
557
664
  if (existsSync(skillFile)) {
558
665
  if (!this.discoveredPaths.has(skillFile)) {
559
666
  const skill = await this.parseSkillFile(skillFile);
@@ -633,9 +740,9 @@ var defaultSkillLoader = new SkillLoader;
633
740
 
634
741
  // src/tools/skill.ts
635
742
  import { existsSync as existsSync2 } from "node:fs";
636
- import { join as join2 } from "node:path";
743
+ import { join as join3 } from "node:path";
637
744
  import { homedir as homedir2 } from "node:os";
638
- var DEFAULT_USER_SKILLS_PATH = join2(homedir2(), ".claude/skills");
745
+ var DEFAULT_USER_SKILLS_PATH = join3(homedir2(), ".claude/skills");
639
746
  var skillToolSchema = {
640
747
  type: "object",
641
748
  properties: {
@@ -662,7 +769,7 @@ function createSkillTool(config = {}) {
662
769
  const paths = [];
663
770
  if (config.settingSources && config.settingSources.length > 0) {
664
771
  for (const source of config.settingSources) {
665
- const resolvedPath = source.startsWith("~") ? join2(homedir2(), source.slice(1)) : source;
772
+ const resolvedPath = source.startsWith("~") ? join3(homedir2(), source.slice(1)) : source;
666
773
  if (existsSync2(resolvedPath)) {
667
774
  paths.push(resolvedPath);
668
775
  }
@@ -1113,7 +1220,7 @@ var defaultSystemPromptBuilder = new SystemPromptBuilderImpl;
1113
1220
  // src/prompt/claude-md.ts
1114
1221
  import { existsSync as existsSync3 } from "node:fs";
1115
1222
  import { readFile as readFile2 } from "node:fs/promises";
1116
- import { join as join3, dirname as dirname2 } from "node:path";
1223
+ import { join as join4, dirname as dirname2 } from "node:path";
1117
1224
  import { homedir as homedir3 } from "node:os";
1118
1225
  var CLAUDE_MD_FILENAME = "CLAUDE.md";
1119
1226
  var USER_CLAUDE_DIR = ".claude";
@@ -1127,7 +1234,7 @@ class ClaudeMdLoaderImpl {
1127
1234
  return this.loadFile(filePath, "project");
1128
1235
  }
1129
1236
  async loadUserClaudeMd() {
1130
- const filePath = join3(homedir3(), USER_CLAUDE_DIR, CLAUDE_MD_FILENAME);
1237
+ const filePath = join4(homedir3(), USER_CLAUDE_DIR, CLAUDE_MD_FILENAME);
1131
1238
  if (!existsSync3(filePath)) {
1132
1239
  return;
1133
1240
  }
@@ -1148,9 +1255,9 @@ class ClaudeMdLoaderImpl {
1148
1255
  }
1149
1256
  }
1150
1257
  if (config.additionalPaths) {
1151
- for (const path of config.additionalPaths) {
1152
- if (existsSync3(path)) {
1153
- const content = await this.loadFile(path, "project");
1258
+ for (const path2 of config.additionalPaths) {
1259
+ if (existsSync3(path2)) {
1260
+ const content = await this.loadFile(path2, "project");
1154
1261
  if (content) {
1155
1262
  contents.push(content);
1156
1263
  }
@@ -1177,11 +1284,11 @@ class ClaudeMdLoaderImpl {
1177
1284
  async findProjectClaudeMd(startDir) {
1178
1285
  let currentDir = startDir;
1179
1286
  while (currentDir !== "/") {
1180
- const claudeMdPath = join3(currentDir, CLAUDE_MD_FILENAME);
1287
+ const claudeMdPath = join4(currentDir, CLAUDE_MD_FILENAME);
1181
1288
  if (existsSync3(claudeMdPath)) {
1182
1289
  return claudeMdPath;
1183
1290
  }
1184
- const gitPath = join3(currentDir, ".git");
1291
+ const gitPath = join4(currentDir, ".git");
1185
1292
  if (existsSync3(gitPath)) {
1186
1293
  break;
1187
1294
  }
@@ -1247,6 +1354,7 @@ class SessionImpl {
1247
1354
  _state;
1248
1355
  provider;
1249
1356
  tools;
1357
+ toolNameLookup = new Map;
1250
1358
  emitter;
1251
1359
  pendingMessage = null;
1252
1360
  isReceiving = false;
@@ -1254,6 +1362,7 @@ class SessionImpl {
1254
1362
  closed = false;
1255
1363
  hooksManager = null;
1256
1364
  maxTurns;
1365
+ enableToolRepair = true;
1257
1366
  constructor(id, config, provider, state) {
1258
1367
  this.id = id;
1259
1368
  this.config = config;
@@ -1276,6 +1385,7 @@ class SessionImpl {
1276
1385
  if (config.tools) {
1277
1386
  for (const tool of config.tools) {
1278
1387
  this.tools.set(tool.name, tool);
1388
+ this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
1279
1389
  }
1280
1390
  }
1281
1391
  if (config.settingSources && config.settingSources.length > 0) {
@@ -1284,6 +1394,7 @@ class SessionImpl {
1284
1394
  cwd: config.cwd
1285
1395
  });
1286
1396
  this.tools.set(skillTool2.name, skillTool2);
1397
+ this.toolNameLookup.set(skillTool2.name.toLowerCase(), skillTool2.name);
1287
1398
  }
1288
1399
  this.applyAllowedToolsFilter();
1289
1400
  if (config.hooks) {
@@ -1592,12 +1703,23 @@ class SessionImpl {
1592
1703
  }
1593
1704
  systemMessage = preResult.systemMessage;
1594
1705
  }
1595
- const tool = this.tools.get(block.name);
1706
+ let tool = this.tools.get(block.name);
1707
+ let effectiveToolName = block.name;
1708
+ if (!tool && this.enableToolRepair) {
1709
+ const lowerName = block.name.toLowerCase();
1710
+ const originalName = this.toolNameLookup.get(lowerName);
1711
+ if (originalName) {
1712
+ tool = this.tools.get(originalName);
1713
+ effectiveToolName = originalName;
1714
+ }
1715
+ }
1596
1716
  if (!tool) {
1717
+ const availableTools = Array.from(this.tools.keys()).slice(0, 10);
1718
+ const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
1597
1719
  return {
1598
1720
  type: "tool_result",
1599
1721
  tool_use_id: block.id,
1600
- content: `Error: Tool "${block.name}" not found`,
1722
+ content: `Error: Tool "${block.name}" not found. Available tools: ${availableTools.join(", ")}${suffix}`,
1601
1723
  is_error: true,
1602
1724
  _hookSystemMessage: systemMessage
1603
1725
  };
@@ -1610,7 +1732,10 @@ class SessionImpl {
1610
1732
  let toolResponse;
1611
1733
  try {
1612
1734
  const toolResult = await tool.execute(toolInput, context);
1613
- const content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
1735
+ let content = typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content);
1736
+ if (needsTruncation(content)) {
1737
+ content = await truncateToolOutput(content);
1738
+ }
1614
1739
  toolResponse = toolResult;
1615
1740
  result = {
1616
1741
  type: "tool_result",
@@ -2828,24 +2953,36 @@ var defaultMCPServerManager = new MCPServerManager;
2828
2953
  // src/tools/manager.ts
2829
2954
  class ToolManager {
2830
2955
  tools = new Map;
2956
+ toolNameLookup = new Map;
2831
2957
  mcpServers = new Map;
2832
2958
  allowedTools;
2833
2959
  onToolEvent;
2960
+ enableToolRepair;
2834
2961
  constructor(options = {}) {
2835
2962
  this.allowedTools = options.allowedTools;
2836
2963
  this.onToolEvent = options.onToolEvent;
2964
+ this.enableToolRepair = options.enableToolRepair ?? true;
2837
2965
  }
2838
2966
  register(tool) {
2839
2967
  this.tools.set(tool.name, tool);
2968
+ this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
2840
2969
  }
2841
2970
  unregister(name) {
2842
2971
  this.tools.delete(name);
2972
+ this.toolNameLookup.delete(name.toLowerCase());
2843
2973
  }
2844
2974
  get(name) {
2845
2975
  const tool = this.tools.get(name);
2846
2976
  if (tool) {
2847
2977
  return tool;
2848
2978
  }
2979
+ if (this.enableToolRepair) {
2980
+ const lowerName = name.toLowerCase();
2981
+ const originalName = this.toolNameLookup.get(lowerName);
2982
+ if (originalName) {
2983
+ return this.tools.get(originalName);
2984
+ }
2985
+ }
2849
2986
  if (isMCPTool(name)) {
2850
2987
  const parsed = parseMCPToolName(name);
2851
2988
  if (parsed) {
@@ -2857,6 +2994,34 @@ class ToolManager {
2857
2994
  }
2858
2995
  return;
2859
2996
  }
2997
+ repairToolName(name) {
2998
+ if (this.tools.has(name)) {
2999
+ return { repaired: false, originalName: name };
3000
+ }
3001
+ const lowerName = name.toLowerCase();
3002
+ const originalName = this.toolNameLookup.get(lowerName);
3003
+ if (originalName && originalName !== name) {
3004
+ return {
3005
+ repaired: true,
3006
+ originalName: name,
3007
+ repairedName: originalName
3008
+ };
3009
+ }
3010
+ if (isMCPTool(name)) {
3011
+ const parsed = parseMCPToolName(name);
3012
+ if (parsed) {
3013
+ const wrapper = this.mcpServers.get(parsed.serverName);
3014
+ if (wrapper) {
3015
+ return { repaired: false, originalName: name };
3016
+ }
3017
+ }
3018
+ }
3019
+ return {
3020
+ repaired: false,
3021
+ originalName: name,
3022
+ error: `Tool "${name}" not found. Available tools: ${Array.from(this.tools.keys()).join(", ")}`
3023
+ };
3024
+ }
2860
3025
  getAll() {
2861
3026
  const allTools = [];
2862
3027
  for (const tool of this.tools.values()) {
@@ -2874,6 +3039,7 @@ class ToolManager {
2874
3039
  }
2875
3040
  clear() {
2876
3041
  this.tools.clear();
3042
+ this.toolNameLookup.clear();
2877
3043
  }
2878
3044
  async registerMCPServer(server) {
2879
3045
  const wrapper = new MCPServerWrapper(server);
@@ -2881,6 +3047,7 @@ class ToolManager {
2881
3047
  const tools = await wrapper.getTools();
2882
3048
  for (const tool of tools) {
2883
3049
  this.tools.set(tool.name, tool);
3050
+ this.toolNameLookup.set(tool.name.toLowerCase(), tool.name);
2884
3051
  }
2885
3052
  return tools.length;
2886
3053
  }
@@ -2892,6 +3059,7 @@ class ToolManager {
2892
3059
  const parsed = parseMCPToolName(name);
2893
3060
  if (parsed?.serverName === serverName) {
2894
3061
  this.tools.delete(name);
3062
+ this.toolNameLookup.delete(name.toLowerCase());
2895
3063
  }
2896
3064
  }
2897
3065
  }
@@ -2903,9 +3071,23 @@ class ToolManager {
2903
3071
  return Array.from(this.mcpServers.keys());
2904
3072
  }
2905
3073
  async execute(name, input, context) {
2906
- const tool = this.get(name);
3074
+ let effectiveName = name;
3075
+ let tool = this.tools.get(name);
3076
+ if (!tool && this.enableToolRepair) {
3077
+ const repairResult = this.repairToolName(name);
3078
+ if (repairResult.repaired && repairResult.repairedName) {
3079
+ effectiveName = repairResult.repairedName;
3080
+ tool = this.tools.get(effectiveName);
3081
+ console.debug(`[ToolManager] Repaired tool name: "${name}" -> "${effectiveName}"`);
3082
+ }
3083
+ }
2907
3084
  if (!tool) {
2908
- throw new Error(`Tool not found: ${name}`);
3085
+ tool = this.get(name);
3086
+ }
3087
+ if (!tool) {
3088
+ const availableTools = Array.from(this.tools.keys()).slice(0, 10);
3089
+ const suffix = this.tools.size > 10 ? ` (and ${this.tools.size - 10} more)` : "";
3090
+ throw new Error(`Tool not found: "${name}". Available tools: ${availableTools.join(", ")}${suffix}`);
2909
3091
  }
2910
3092
  if (!this.isToolAllowed(tool)) {
2911
3093
  throw new Error(`Tool not allowed: ${name}`);
@@ -2914,7 +3096,7 @@ class ToolManager {
2914
3096
  this.emitEvent({
2915
3097
  type: "tool_start",
2916
3098
  toolId,
2917
- toolName: name,
3099
+ toolName: effectiveName,
2918
3100
  input
2919
3101
  });
2920
3102
  try {
@@ -3305,7 +3487,7 @@ Type: ${ext}`
3305
3487
  }
3306
3488
  var ReadTool = createReadTool();
3307
3489
  // src/tools/builtin/write.ts
3308
- import { writeFile, mkdir } from "node:fs/promises";
3490
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
3309
3491
  import { existsSync as existsSync5 } from "node:fs";
3310
3492
  import { dirname as dirname3 } from "node:path";
3311
3493
  function createWriteTool(options = {}) {
@@ -3336,9 +3518,9 @@ function createWriteTool(options = {}) {
3336
3518
  try {
3337
3519
  const dir = dirname3(access.resolved);
3338
3520
  if (!existsSync5(dir)) {
3339
- await mkdir(dir, { recursive: true });
3521
+ await mkdir2(dir, { recursive: true });
3340
3522
  }
3341
- await writeFile(access.resolved, content, "utf-8");
3523
+ await writeFile2(access.resolved, content, "utf-8");
3342
3524
  const lines = content.split(`
3343
3525
  `).length;
3344
3526
  const bytes = Buffer.byteLength(content, "utf-8");
@@ -3356,7 +3538,7 @@ function createWriteTool(options = {}) {
3356
3538
  }
3357
3539
  var WriteTool = createWriteTool();
3358
3540
  // src/tools/builtin/edit.ts
3359
- import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
3541
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3360
3542
  import { existsSync as existsSync6 } from "node:fs";
3361
3543
  function createEditTool(options = {}) {
3362
3544
  return {
@@ -3428,7 +3610,7 @@ function createEditTool(options = {}) {
3428
3610
  newContent = content.replace(old_string, new_string);
3429
3611
  replacedCount = 1;
3430
3612
  }
3431
- await writeFile2(access.resolved, newContent, "utf-8");
3613
+ await writeFile3(access.resolved, newContent, "utf-8");
3432
3614
  return {
3433
3615
  content: `Successfully replaced ${replacedCount} occurrence${replacedCount > 1 ? "s" : ""} in ${access.resolved}`
3434
3616
  };
@@ -3443,14 +3625,14 @@ function createEditTool(options = {}) {
3443
3625
  }
3444
3626
  var EditTool = createEditTool();
3445
3627
  // src/tools/builtin/glob.ts
3446
- import { readdir, stat as stat2 } from "node:fs/promises";
3628
+ import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
3447
3629
  import { existsSync as existsSync7 } from "node:fs";
3448
- import { join as join4, relative } from "node:path";
3630
+ import { join as join5, relative } from "node:path";
3449
3631
  var MAX_RESULTS = 1000;
3450
- function matchGlob(pattern, path) {
3632
+ function matchGlob(pattern, path2) {
3451
3633
  const regexPattern = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*").replace(/\./g, "\\.");
3452
3634
  const regex = new RegExp(`^${regexPattern}$`);
3453
- return regex.test(path);
3635
+ return regex.test(path2);
3454
3636
  }
3455
3637
  async function scanDir(dir, pattern, results, baseDir, maxDepth = 10, currentDepth = 0) {
3456
3638
  if (currentDepth > maxDepth || results.length >= MAX_RESULTS) {
@@ -3460,14 +3642,14 @@ async function scanDir(dir, pattern, results, baseDir, maxDepth = 10, currentDep
3460
3642
  return;
3461
3643
  }
3462
3644
  try {
3463
- const entries = await readdir(dir, { withFileTypes: true });
3645
+ const entries = await readdir2(dir, { withFileTypes: true });
3464
3646
  for (const entry of entries) {
3465
3647
  if (results.length >= MAX_RESULTS)
3466
3648
  break;
3467
3649
  if (entry.name.startsWith(".") || entry.name === "node_modules") {
3468
3650
  continue;
3469
3651
  }
3470
- const fullPath = join4(dir, entry.name);
3652
+ const fullPath = join5(dir, entry.name);
3471
3653
  const relativePath = relative(baseDir, fullPath);
3472
3654
  if (entry.isDirectory()) {
3473
3655
  if (matchGlob(pattern, relativePath) || matchGlob(pattern, relativePath + "/")) {
@@ -3503,8 +3685,8 @@ function createGlobTool(options = {}) {
3503
3685
  },
3504
3686
  execute: async (rawInput, _context) => {
3505
3687
  const input = rawInput;
3506
- const { pattern, path = defaultCwd } = input;
3507
- const access = checkDirAccess(path, options);
3688
+ const { pattern, path: path2 = defaultCwd } = input;
3689
+ const access = checkDirAccess(path2, options);
3508
3690
  if (!access.ok) {
3509
3691
  return { content: access.error, isError: true };
3510
3692
  }
@@ -3552,9 +3734,9 @@ ${output}${truncated}`
3552
3734
  }
3553
3735
  var GlobTool = createGlobTool();
3554
3736
  // src/tools/builtin/grep.ts
3555
- import { readFile as readFile5, readdir as readdir2, stat as stat3 } from "node:fs/promises";
3737
+ import { readFile as readFile5, readdir as readdir3, stat as stat3 } from "node:fs/promises";
3556
3738
  import { existsSync as existsSync8 } from "node:fs";
3557
- import { join as join5, extname as extname2 } from "node:path";
3739
+ import { join as join6, extname as extname2 } from "node:path";
3558
3740
  var MAX_RESULTS2 = 500;
3559
3741
  var MAX_LINE_LENGTH2 = 500;
3560
3742
  var BINARY_EXTENSIONS2 = new Set([
@@ -3629,14 +3811,14 @@ async function searchDir(dir, regex, glob, before, after, results, maxDepth = 10
3629
3811
  return;
3630
3812
  }
3631
3813
  try {
3632
- const entries = await readdir2(dir, { withFileTypes: true });
3814
+ const entries = await readdir3(dir, { withFileTypes: true });
3633
3815
  for (const entry of entries) {
3634
3816
  if (results.length >= MAX_RESULTS2)
3635
3817
  break;
3636
3818
  if (entry.name.startsWith(".") || entry.name === "node_modules") {
3637
3819
  continue;
3638
3820
  }
3639
- const fullPath = join5(dir, entry.name);
3821
+ const fullPath = join6(dir, entry.name);
3640
3822
  if (entry.isDirectory()) {
3641
3823
  await searchDir(fullPath, regex, glob, before, after, results, maxDepth, currentDepth + 1);
3642
3824
  } else if (entry.isFile()) {
@@ -3692,13 +3874,13 @@ function createGrepTool(options = {}) {
3692
3874
  const input = rawInput;
3693
3875
  const {
3694
3876
  pattern,
3695
- path = defaultCwd,
3877
+ path: path2 = defaultCwd,
3696
3878
  glob,
3697
3879
  before = 0,
3698
3880
  after = 0,
3699
3881
  ignoreCase = false
3700
3882
  } = input;
3701
- const access = checkPathAccess(path, options, "dir");
3883
+ const access = checkPathAccess(path2, options, "dir");
3702
3884
  if (!access.ok) {
3703
3885
  return { content: access.error, isError: true };
3704
3886
  }
@@ -3995,6 +4177,187 @@ Please fetch the new URL if you want to continue.`
3995
4177
  };
3996
4178
  }
3997
4179
  var WebFetchTool = createWebFetchTool();
4180
+ // src/tools/builtin/websearch.ts
4181
+ var API_CONFIG = {
4182
+ BASE_URL: "https://mcp.exa.ai",
4183
+ ENDPOINTS: {
4184
+ SEARCH: "/mcp"
4185
+ },
4186
+ DEFAULT_NUM_RESULTS: 8,
4187
+ DEFAULT_CONTEXT_MAX_CHARS: 1e4,
4188
+ TIMEOUT_MS: 30000
4189
+ };
4190
+ var WEBSEARCH_DESCRIPTION = `Search the web for up-to-date information using Exa AI.
4191
+
4192
+ Use this tool when you need to:
4193
+ - Find current information, news, or recent events
4194
+ - Look up documentation or technical resources
4195
+ - Research topics beyond your knowledge cutoff
4196
+ - Verify facts or get multiple perspectives
4197
+
4198
+ The search returns curated, AI-optimized content with context.
4199
+
4200
+ Parameters:
4201
+ - query: The search query (required)
4202
+ - numResults: Number of results to return (default: 8, max: 20)
4203
+ - livecrawl: 'fallback' (default) or 'preferred' for fresh content
4204
+ - type: 'auto' (default), 'fast', or 'deep'
4205
+ - contextMaxCharacters: Max characters per result (default: 10000)
4206
+
4207
+ Best practices:
4208
+ - Use specific, detailed queries for better results
4209
+ - For technical topics, include relevant terms and context
4210
+ - Use 'deep' type for comprehensive research
4211
+ - Use 'fast' type for quick fact-checking`;
4212
+ function createWebSearchTool(options = {}) {
4213
+ return {
4214
+ name: "WebSearch",
4215
+ description: WEBSEARCH_DESCRIPTION,
4216
+ inputSchema: {
4217
+ type: "object",
4218
+ properties: {
4219
+ query: {
4220
+ type: "string",
4221
+ description: "Search query to find relevant information"
4222
+ },
4223
+ numResults: {
4224
+ type: "number",
4225
+ description: "Number of search results to return (default: 8, max: 20)"
4226
+ },
4227
+ livecrawl: {
4228
+ type: "string",
4229
+ enum: ["fallback", "preferred"],
4230
+ description: "Live crawl mode - 'fallback': use live crawling as backup, 'preferred': prioritize live crawling"
4231
+ },
4232
+ type: {
4233
+ type: "string",
4234
+ enum: ["auto", "fast", "deep"],
4235
+ description: "Search type - 'auto': balanced, 'fast': quick results, 'deep': comprehensive"
4236
+ },
4237
+ contextMaxCharacters: {
4238
+ type: "number",
4239
+ description: "Maximum characters for context per result (default: 10000)"
4240
+ }
4241
+ },
4242
+ required: ["query"]
4243
+ },
4244
+ execute: async (rawInput, context) => {
4245
+ const input = rawInput;
4246
+ const { query, numResults, livecrawl, type, contextMaxCharacters } = input;
4247
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
4248
+ return {
4249
+ content: "Error: A non-empty search query is required.",
4250
+ isError: true
4251
+ };
4252
+ }
4253
+ const clampedNumResults = Math.min(Math.max(numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 1), 20);
4254
+ const searchRequest = {
4255
+ jsonrpc: "2.0",
4256
+ id: 1,
4257
+ method: "tools/call",
4258
+ params: {
4259
+ name: "web_search_exa",
4260
+ arguments: {
4261
+ query: query.trim(),
4262
+ numResults: clampedNumResults,
4263
+ livecrawl: livecrawl || "fallback",
4264
+ type: type || "auto",
4265
+ contextMaxCharacters: contextMaxCharacters || API_CONFIG.DEFAULT_CONTEXT_MAX_CHARS
4266
+ }
4267
+ }
4268
+ };
4269
+ const controller = new AbortController;
4270
+ const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT_MS);
4271
+ try {
4272
+ const signal = context.abortSignal ? AbortSignal.any([controller.signal, context.abortSignal]) : controller.signal;
4273
+ const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`, {
4274
+ method: "POST",
4275
+ headers: {
4276
+ Accept: "application/json, text/event-stream",
4277
+ "Content-Type": "application/json",
4278
+ "User-Agent": "FormAgent-SDK/1.0"
4279
+ },
4280
+ body: JSON.stringify(searchRequest),
4281
+ signal
4282
+ });
4283
+ clearTimeout(timeoutId);
4284
+ if (!response.ok) {
4285
+ const errorText = await response.text();
4286
+ return {
4287
+ content: `Search error (${response.status}): ${errorText}`,
4288
+ isError: true
4289
+ };
4290
+ }
4291
+ const responseText = await response.text();
4292
+ const lines = responseText.split(`
4293
+ `);
4294
+ for (const line of lines) {
4295
+ if (line.startsWith("data: ")) {
4296
+ try {
4297
+ const data = JSON.parse(line.substring(6));
4298
+ if (data.error) {
4299
+ return {
4300
+ content: `Search API error: ${data.error.message}`,
4301
+ isError: true
4302
+ };
4303
+ }
4304
+ if (data.result?.content && data.result.content.length > 0) {
4305
+ const resultText = data.result.content[0].text;
4306
+ let output = `## Web Search Results
4307
+
4308
+ `;
4309
+ output += `**Query:** ${query}
4310
+ `;
4311
+ output += `**Results:** ${clampedNumResults} requested
4312
+
4313
+ `;
4314
+ output += `---
4315
+
4316
+ `;
4317
+ output += resultText;
4318
+ return {
4319
+ content: output
4320
+ };
4321
+ }
4322
+ } catch (parseError) {
4323
+ continue;
4324
+ }
4325
+ }
4326
+ }
4327
+ try {
4328
+ const data = JSON.parse(responseText);
4329
+ if (data.result?.content && data.result.content.length > 0) {
4330
+ return {
4331
+ content: data.result.content[0].text
4332
+ };
4333
+ }
4334
+ } catch {}
4335
+ return {
4336
+ content: `No search results found for query: "${query}". Please try a different or more specific query.`
4337
+ };
4338
+ } catch (error) {
4339
+ clearTimeout(timeoutId);
4340
+ if (error instanceof Error) {
4341
+ if (error.name === "AbortError") {
4342
+ return {
4343
+ content: `Search request timed out after ${API_CONFIG.TIMEOUT_MS}ms. Please try again or use a simpler query.`,
4344
+ isError: true
4345
+ };
4346
+ }
4347
+ return {
4348
+ content: `Search failed: ${error.message}`,
4349
+ isError: true
4350
+ };
4351
+ }
4352
+ return {
4353
+ content: `Search failed: ${String(error)}`,
4354
+ isError: true
4355
+ };
4356
+ }
4357
+ }
4358
+ };
4359
+ }
4360
+ var WebSearchTool = createWebSearchTool();
3998
4361
  // src/tools/builtin/todo.ts
3999
4362
  var globalTodos = [];
4000
4363
  var onTodoChange = null;
@@ -4083,6 +4446,574 @@ function createTodoWriteTool(options = {}) {
4083
4446
  };
4084
4447
  }
4085
4448
  var TodoWriteTool = createTodoWriteTool();
4449
+ // src/tools/builtin/askuser.ts
4450
+ var globalAskUserHandler = null;
4451
+ var ASKUSER_DESCRIPTION = `Ask the user questions to gather information, clarify requirements, or get decisions.
4452
+
4453
+ Use this tool when you need to:
4454
+ - Gather user preferences or requirements
4455
+ - Clarify ambiguous instructions
4456
+ - Get decisions on implementation choices
4457
+ - Offer choices about what direction to take
4458
+
4459
+ Parameters:
4460
+ - questions: Array of questions (1-4 questions per call)
4461
+ - question: The question text (required)
4462
+ - header: Short label for display (optional)
4463
+ - options: Array of choices (optional)
4464
+ - label: Display text
4465
+ - description: Explanation of the option
4466
+ - multiSelect: Allow multiple selections (default: false)
4467
+ - defaultValue: Default if no response
4468
+
4469
+ Best practices:
4470
+ - Keep questions clear and concise
4471
+ - Provide options when there are known choices
4472
+ - Use multiSelect for non-mutually-exclusive options
4473
+ - Limit to 4 questions per call to avoid overwhelming the user`;
4474
+ function createAskUserTool(options = {}) {
4475
+ return {
4476
+ name: "AskUser",
4477
+ description: ASKUSER_DESCRIPTION,
4478
+ inputSchema: {
4479
+ type: "object",
4480
+ properties: {
4481
+ questions: {
4482
+ type: "array",
4483
+ description: "Questions to ask the user (1-4 questions)",
4484
+ items: {
4485
+ type: "object",
4486
+ properties: {
4487
+ question: {
4488
+ type: "string",
4489
+ description: "The question text to display"
4490
+ },
4491
+ header: {
4492
+ type: "string",
4493
+ description: "Short label for the question (max 12 chars)"
4494
+ },
4495
+ options: {
4496
+ type: "array",
4497
+ description: "Available choices (2-4 options)",
4498
+ items: {
4499
+ type: "object",
4500
+ properties: {
4501
+ label: {
4502
+ type: "string",
4503
+ description: "Display text for this option"
4504
+ },
4505
+ description: {
4506
+ type: "string",
4507
+ description: "Explanation of what this option means"
4508
+ }
4509
+ },
4510
+ required: ["label"]
4511
+ }
4512
+ },
4513
+ multiSelect: {
4514
+ type: "boolean",
4515
+ description: "Allow multiple selections (default: false)"
4516
+ },
4517
+ defaultValue: {
4518
+ type: "string",
4519
+ description: "Default value if user doesn't respond"
4520
+ }
4521
+ },
4522
+ required: ["question"]
4523
+ },
4524
+ minItems: 1,
4525
+ maxItems: 4
4526
+ }
4527
+ },
4528
+ required: ["questions"]
4529
+ },
4530
+ execute: async (rawInput, context) => {
4531
+ const input = rawInput;
4532
+ const { questions } = input;
4533
+ if (!questions || !Array.isArray(questions) || questions.length === 0) {
4534
+ return {
4535
+ content: "Error: At least one question is required.",
4536
+ isError: true
4537
+ };
4538
+ }
4539
+ if (questions.length > 4) {
4540
+ return {
4541
+ content: "Error: Maximum 4 questions per call. Please split into multiple calls.",
4542
+ isError: true
4543
+ };
4544
+ }
4545
+ if (!globalAskUserHandler) {
4546
+ return {
4547
+ content: `Error: AskUser handler not configured. The SDK user must call setAskUserHandler() to enable user interaction.
4548
+
4549
+ Questions that would have been asked:
4550
+ ${questions.map((q, i) => `${i + 1}. ${q.question}${q.options ? ` [Options: ${q.options.map((o) => o.label).join(", ")}]` : ""}`).join(`
4551
+ `)}
4552
+
4553
+ To enable this tool, the SDK user should implement an AskUser handler.`,
4554
+ isError: true
4555
+ };
4556
+ }
4557
+ try {
4558
+ const answers = await globalAskUserHandler(questions, context);
4559
+ const formatAnswer = (answer) => {
4560
+ if (answer === undefined || answer === null)
4561
+ return "(no answer)";
4562
+ if (Array.isArray(answer))
4563
+ return answer.join(", ") || "(no answer)";
4564
+ return answer || "(no answer)";
4565
+ };
4566
+ const formattedAnswers = questions.map((q, i) => `Q: "${q.question}"
4567
+ A: ${formatAnswer(answers[i])}`).join(`
4568
+
4569
+ `);
4570
+ return {
4571
+ content: `User has answered your questions:
4572
+
4573
+ ${formattedAnswers}
4574
+
4575
+ You can now continue with the user's answers in mind.`,
4576
+ metadata: {
4577
+ questions: questions.map((q) => q.question),
4578
+ answers
4579
+ }
4580
+ };
4581
+ } catch (error) {
4582
+ return {
4583
+ content: `Error getting user response: ${error instanceof Error ? error.message : String(error)}`,
4584
+ isError: true
4585
+ };
4586
+ }
4587
+ }
4588
+ };
4589
+ }
4590
+ var AskUserTool = createAskUserTool();
4591
+ // src/tools/builtin/batch.ts
4592
+ var globalToolResolver = null;
4593
+ var DISALLOWED_TOOLS = new Set(["Batch", "batch", "AskUser", "askuser"]);
4594
+ var BATCH_DESCRIPTION = `Execute multiple tool calls in parallel for improved performance.
4595
+
4596
+ Use this tool when you need to:
4597
+ - Read multiple files at once
4598
+ - Perform several independent operations
4599
+ - Speed up tasks that don't have dependencies
4600
+
4601
+ Parameters:
4602
+ - tool_calls: Array of tool calls (1-10 calls per batch)
4603
+ - tool: Name of the tool to execute
4604
+ - parameters: Parameters for that tool
4605
+
4606
+ Limitations:
4607
+ - Maximum 10 tool calls per batch
4608
+ - Cannot nest Batch calls (no batch within batch)
4609
+ - Cannot batch AskUser (requires sequential interaction)
4610
+ - All calls execute in parallel - don't batch dependent operations
4611
+
4612
+ Best practices:
4613
+ - Group independent operations (e.g., reading multiple unrelated files)
4614
+ - Don't batch operations that depend on each other's results
4615
+ - Use for bulk file reads, multiple greps, or parallel web fetches
4616
+
4617
+ Example:
4618
+ {
4619
+ "tool_calls": [
4620
+ { "tool": "Read", "parameters": { "file_path": "/path/to/file1.ts" } },
4621
+ { "tool": "Read", "parameters": { "file_path": "/path/to/file2.ts" } },
4622
+ { "tool": "Grep", "parameters": { "pattern": "TODO", "path": "./src" } }
4623
+ ]
4624
+ }`;
4625
+ function createBatchTool(options = {}, toolMap) {
4626
+ return {
4627
+ name: "Batch",
4628
+ description: BATCH_DESCRIPTION,
4629
+ inputSchema: {
4630
+ type: "object",
4631
+ properties: {
4632
+ tool_calls: {
4633
+ type: "array",
4634
+ description: "Array of tool calls to execute in parallel",
4635
+ items: {
4636
+ type: "object",
4637
+ properties: {
4638
+ tool: {
4639
+ type: "string",
4640
+ description: "Name of the tool to execute"
4641
+ },
4642
+ parameters: {
4643
+ type: "object",
4644
+ description: "Parameters for the tool",
4645
+ additionalProperties: true
4646
+ }
4647
+ },
4648
+ required: ["tool", "parameters"]
4649
+ },
4650
+ minItems: 1,
4651
+ maxItems: 10
4652
+ }
4653
+ },
4654
+ required: ["tool_calls"]
4655
+ },
4656
+ execute: async (rawInput, context) => {
4657
+ const input = rawInput;
4658
+ const { tool_calls } = input;
4659
+ if (!tool_calls || !Array.isArray(tool_calls) || tool_calls.length === 0) {
4660
+ return {
4661
+ content: "Error: At least one tool call is required.",
4662
+ isError: true
4663
+ };
4664
+ }
4665
+ const callsToExecute = tool_calls.slice(0, 10);
4666
+ const discardedCalls = tool_calls.slice(10);
4667
+ const executeCall = async (call) => {
4668
+ const startTime = Date.now();
4669
+ try {
4670
+ if (DISALLOWED_TOOLS.has(call.tool)) {
4671
+ return {
4672
+ success: false,
4673
+ tool: call.tool,
4674
+ error: `Tool '${call.tool}' cannot be used in batch. Disallowed: ${Array.from(DISALLOWED_TOOLS).join(", ")}`,
4675
+ duration: Date.now() - startTime
4676
+ };
4677
+ }
4678
+ let toolExecute;
4679
+ if (toolMap) {
4680
+ const tool = toolMap.get(call.tool);
4681
+ if (tool) {
4682
+ toolExecute = tool.execute;
4683
+ }
4684
+ }
4685
+ if (!toolExecute && globalToolResolver) {
4686
+ toolExecute = globalToolResolver(call.tool);
4687
+ }
4688
+ if (!toolExecute) {
4689
+ const availableTools = toolMap ? Array.from(toolMap.keys()).filter((n) => !DISALLOWED_TOOLS.has(n)) : [];
4690
+ return {
4691
+ success: false,
4692
+ tool: call.tool,
4693
+ error: `Tool '${call.tool}' not found.${availableTools.length > 0 ? ` Available: ${availableTools.slice(0, 10).join(", ")}` : ""}`,
4694
+ duration: Date.now() - startTime
4695
+ };
4696
+ }
4697
+ const result = await toolExecute(call.parameters, context);
4698
+ return {
4699
+ success: !result.isError,
4700
+ tool: call.tool,
4701
+ result,
4702
+ duration: Date.now() - startTime
4703
+ };
4704
+ } catch (error) {
4705
+ return {
4706
+ success: false,
4707
+ tool: call.tool,
4708
+ error: error instanceof Error ? error.message : String(error),
4709
+ duration: Date.now() - startTime
4710
+ };
4711
+ }
4712
+ };
4713
+ const results = await Promise.all(callsToExecute.map(executeCall));
4714
+ for (const call of discardedCalls) {
4715
+ results.push({
4716
+ success: false,
4717
+ tool: call.tool,
4718
+ error: "Exceeded maximum of 10 tool calls per batch",
4719
+ duration: 0
4720
+ });
4721
+ }
4722
+ const successCount = results.filter((r) => r.success).length;
4723
+ const failCount = results.length - successCount;
4724
+ const totalDuration = results.reduce((sum, r) => sum + (r.duration || 0), 0);
4725
+ const formatResult = (r, index) => {
4726
+ if (r.success && r.result) {
4727
+ 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);
4728
+ return `[${index + 1}] ${r.tool}: SUCCESS (${r.duration}ms)
4729
+ ${content}`;
4730
+ } else {
4731
+ return `[${index + 1}] ${r.tool}: FAILED (${r.duration}ms)
4732
+ Error: ${r.error}`;
4733
+ }
4734
+ };
4735
+ const resultsOutput = results.map(formatResult).join(`
4736
+
4737
+ ---
4738
+
4739
+ `);
4740
+ const summary = failCount > 0 ? `Batch execution: ${successCount}/${results.length} succeeded, ${failCount} failed (${totalDuration}ms total)` : `Batch execution: All ${successCount} tools succeeded (${totalDuration}ms total)`;
4741
+ return {
4742
+ content: `${summary}
4743
+
4744
+ ${resultsOutput}`,
4745
+ metadata: {
4746
+ totalCalls: results.length,
4747
+ successful: successCount,
4748
+ failed: failCount,
4749
+ totalDuration,
4750
+ details: results.map((r) => ({
4751
+ tool: r.tool,
4752
+ success: r.success,
4753
+ duration: r.duration,
4754
+ error: r.error
4755
+ }))
4756
+ }
4757
+ };
4758
+ }
4759
+ };
4760
+ }
4761
+ var BatchTool = createBatchTool();
4762
+ // src/tools/builtin/httprequest.ts
4763
+ import { lookup as lookup2 } from "node:dns/promises";
4764
+ var DEFAULT_TIMEOUT2 = 30000;
4765
+ var MAX_RESPONSE_SIZE = 5 * 1024 * 1024;
4766
+ function isPrivateIp(ip) {
4767
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
4768
+ const parts = ip.split(".").map(Number);
4769
+ const [a, b] = parts;
4770
+ if (a === 10)
4771
+ return true;
4772
+ if (a === 127)
4773
+ return true;
4774
+ if (a === 0)
4775
+ return true;
4776
+ if (a === 169 && b === 254)
4777
+ return true;
4778
+ if (a === 172 && b >= 16 && b <= 31)
4779
+ return true;
4780
+ if (a === 192 && b === 168)
4781
+ return true;
4782
+ if (a === 100 && b >= 64 && b <= 127)
4783
+ return true;
4784
+ if (a >= 224)
4785
+ return true;
4786
+ }
4787
+ const normalized = ip.toLowerCase();
4788
+ if (normalized === "::" || normalized === "::1")
4789
+ return true;
4790
+ if (normalized.startsWith("fe80:"))
4791
+ return true;
4792
+ if (normalized.startsWith("fc") || normalized.startsWith("fd"))
4793
+ return true;
4794
+ return false;
4795
+ }
4796
+ async function validateUrl(url, options) {
4797
+ let parsedUrl;
4798
+ try {
4799
+ parsedUrl = new URL(url);
4800
+ } catch {
4801
+ return { valid: false, error: `Invalid URL: ${url}` };
4802
+ }
4803
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
4804
+ return { valid: false, error: `Invalid protocol: ${parsedUrl.protocol}. Only http/https allowed.` };
4805
+ }
4806
+ if (!options.allowPrivateNetwork) {
4807
+ const hostname = parsedUrl.hostname.toLowerCase();
4808
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
4809
+ return { valid: false, error: `Blocked: localhost/local hostnames not allowed` };
4810
+ }
4811
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) && isPrivateIp(hostname)) {
4812
+ return { valid: false, error: `Blocked: private IP address ${hostname}` };
4813
+ }
4814
+ const resolveHostnames = options.resolveHostnames ?? true;
4815
+ if (resolveHostnames) {
4816
+ try {
4817
+ const addrs = await lookup2(hostname, { all: true, verbatim: true });
4818
+ for (const addr of addrs) {
4819
+ if (isPrivateIp(addr.address)) {
4820
+ return { valid: false, error: `Blocked: ${hostname} resolves to private IP ${addr.address}` };
4821
+ }
4822
+ }
4823
+ } catch (e) {
4824
+ return { valid: false, error: `DNS resolution failed for ${hostname}` };
4825
+ }
4826
+ }
4827
+ }
4828
+ return { valid: true, parsedUrl };
4829
+ }
4830
+ var HTTPREQUEST_DESCRIPTION = `Make HTTP requests to APIs and web services.
4831
+
4832
+ Use this tool when you need to:
4833
+ - Call REST APIs (GET, POST, PUT, DELETE, etc.)
4834
+ - Send data to web services
4835
+ - Fetch JSON data from APIs
4836
+ - Interact with webhooks
4837
+
4838
+ Parameters:
4839
+ - method: HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
4840
+ - url: Full URL to request (required)
4841
+ - headers: Request headers as key-value pairs (optional)
4842
+ - body: Request body for POST/PUT/PATCH (optional, auto-serialized to JSON)
4843
+ - timeout: Request timeout in ms (default: 30000, max: 120000)
4844
+ - responseType: Expected response type - 'json', 'text', or 'binary' (default: auto-detect)
4845
+
4846
+ Security:
4847
+ - Private/local network access is blocked by default
4848
+ - Maximum response size: 5MB
4849
+
4850
+ Best practices:
4851
+ - Include appropriate Content-Type header for POST/PUT requests
4852
+ - Handle authentication via headers (Authorization, API keys, etc.)
4853
+ - Use responseType: 'json' when expecting JSON response
4854
+
4855
+ Example:
4856
+ {
4857
+ "method": "POST",
4858
+ "url": "https://api.example.com/data",
4859
+ "headers": {
4860
+ "Authorization": "Bearer token123",
4861
+ "Content-Type": "application/json"
4862
+ },
4863
+ "body": { "name": "test", "value": 42 }
4864
+ }`;
4865
+ function createHttpRequestTool(options = {}) {
4866
+ return {
4867
+ name: "HttpRequest",
4868
+ description: HTTPREQUEST_DESCRIPTION,
4869
+ inputSchema: {
4870
+ type: "object",
4871
+ properties: {
4872
+ method: {
4873
+ type: "string",
4874
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
4875
+ description: "HTTP method"
4876
+ },
4877
+ url: {
4878
+ type: "string",
4879
+ description: "URL to request"
4880
+ },
4881
+ headers: {
4882
+ type: "object",
4883
+ description: "Request headers",
4884
+ additionalProperties: { type: "string" }
4885
+ },
4886
+ body: {
4887
+ description: "Request body (auto-serialized to JSON if object)"
4888
+ },
4889
+ timeout: {
4890
+ type: "number",
4891
+ description: "Request timeout in milliseconds (default: 30000, max: 120000)"
4892
+ },
4893
+ responseType: {
4894
+ type: "string",
4895
+ enum: ["json", "text", "binary"],
4896
+ description: "Expected response type (default: auto-detect)"
4897
+ }
4898
+ },
4899
+ required: ["method", "url"]
4900
+ },
4901
+ execute: async (rawInput, context) => {
4902
+ const input = rawInput;
4903
+ const { method, url, headers = {}, body, timeout, responseType } = input;
4904
+ const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
4905
+ if (!validMethods.includes(method)) {
4906
+ return {
4907
+ content: `Invalid HTTP method: ${method}. Must be one of: ${validMethods.join(", ")}`,
4908
+ isError: true
4909
+ };
4910
+ }
4911
+ const urlValidation = await validateUrl(url, options);
4912
+ if (!urlValidation.valid) {
4913
+ return {
4914
+ content: urlValidation.error,
4915
+ isError: true
4916
+ };
4917
+ }
4918
+ const requestTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT2, 120000);
4919
+ const controller = new AbortController;
4920
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
4921
+ const signal = context.abortSignal ? AbortSignal.any([controller.signal, context.abortSignal]) : controller.signal;
4922
+ const requestHeaders = {
4923
+ "User-Agent": "FormAgent-SDK/1.0",
4924
+ ...headers
4925
+ };
4926
+ let requestBody;
4927
+ if (body !== undefined && ["POST", "PUT", "PATCH"].includes(method)) {
4928
+ if (typeof body === "string") {
4929
+ requestBody = body;
4930
+ } else {
4931
+ requestBody = JSON.stringify(body);
4932
+ if (!requestHeaders["Content-Type"] && !requestHeaders["content-type"]) {
4933
+ requestHeaders["Content-Type"] = "application/json";
4934
+ }
4935
+ }
4936
+ }
4937
+ try {
4938
+ const response = await fetch(urlValidation.parsedUrl.toString(), {
4939
+ method,
4940
+ headers: requestHeaders,
4941
+ body: requestBody,
4942
+ signal
4943
+ });
4944
+ clearTimeout(timeoutId);
4945
+ const contentLength = response.headers.get("content-length");
4946
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
4947
+ return {
4948
+ content: `Response too large: ${contentLength} bytes (max: ${MAX_RESPONSE_SIZE})`,
4949
+ isError: true
4950
+ };
4951
+ }
4952
+ const contentType = response.headers.get("content-type") || "";
4953
+ let responseBody;
4954
+ let responseText;
4955
+ const effectiveResponseType = responseType || (contentType.includes("application/json") ? "json" : "text");
4956
+ if (effectiveResponseType === "json") {
4957
+ try {
4958
+ responseBody = await response.json();
4959
+ responseText = JSON.stringify(responseBody, null, 2);
4960
+ } catch {
4961
+ responseText = await response.text();
4962
+ responseBody = responseText;
4963
+ }
4964
+ } else if (effectiveResponseType === "binary") {
4965
+ const buffer = await response.arrayBuffer();
4966
+ responseText = `[Binary data: ${buffer.byteLength} bytes]`;
4967
+ responseBody = { type: "binary", size: buffer.byteLength };
4968
+ } else {
4969
+ responseText = await response.text();
4970
+ responseBody = responseText;
4971
+ }
4972
+ if (responseText.length > 1e5) {
4973
+ responseText = responseText.slice(0, 1e5) + `
4974
+
4975
+ ... (truncated)`;
4976
+ }
4977
+ const statusEmoji = response.ok ? "✓" : "✗";
4978
+ const output = `${statusEmoji} ${method} ${url}
4979
+ Status: ${response.status} ${response.statusText}
4980
+ Content-Type: ${contentType}
4981
+
4982
+ Response:
4983
+ ${responseText}`;
4984
+ return {
4985
+ content: output,
4986
+ isError: !response.ok,
4987
+ metadata: {
4988
+ status: response.status,
4989
+ statusText: response.statusText,
4990
+ headers: Object.fromEntries(response.headers.entries()),
4991
+ body: responseBody
4992
+ }
4993
+ };
4994
+ } catch (error) {
4995
+ clearTimeout(timeoutId);
4996
+ if (error instanceof Error) {
4997
+ if (error.name === "AbortError") {
4998
+ return {
4999
+ content: `Request timed out after ${requestTimeout}ms`,
5000
+ isError: true
5001
+ };
5002
+ }
5003
+ return {
5004
+ content: `Request failed: ${error.message}`,
5005
+ isError: true
5006
+ };
5007
+ }
5008
+ return {
5009
+ content: `Request failed: ${String(error)}`,
5010
+ isError: true
5011
+ };
5012
+ }
5013
+ }
5014
+ };
5015
+ }
5016
+ var HttpRequestTool = createHttpRequestTool();
4086
5017
  // src/tools/builtin/index.ts
4087
5018
  var builtinTools = [
4088
5019
  BashTool,
@@ -4092,14 +5023,18 @@ var builtinTools = [
4092
5023
  GlobTool,
4093
5024
  GrepTool,
4094
5025
  WebFetchTool,
4095
- TodoWriteTool
5026
+ WebSearchTool,
5027
+ HttpRequestTool,
5028
+ TodoWriteTool,
5029
+ AskUserTool,
5030
+ BatchTool
4096
5031
  ];
4097
5032
  // src/utils/env.ts
4098
5033
  import { existsSync as existsSync9, readFileSync } from "node:fs";
4099
- import { join as join6 } from "node:path";
5034
+ import { join as join7 } from "node:path";
4100
5035
  function loadEnvOverride(cwd) {
4101
5036
  const dir = cwd || process.cwd();
4102
- const envPath = join6(dir, ".env");
5037
+ const envPath = join7(dir, ".env");
4103
5038
  if (!existsSync9(envPath)) {
4104
5039
  return;
4105
5040
  }
@@ -4129,7 +5064,7 @@ function loadEnvOverride(cwd) {
4129
5064
  // src/cli/cli.ts
4130
5065
  loadEnvOverride();
4131
5066
  var VERSION = "0.1.0";
4132
- var SKILLS_PATH = join7(homedir4(), ".claude");
5067
+ var SKILLS_PATH = join8(homedir4(), ".claude");
4133
5068
  var colors = {
4134
5069
  reset: "\x1B[0m",
4135
5070
  bold: "\x1B[1m",
@@ -4158,7 +5093,7 @@ var totalInputTokens = 0;
4158
5093
  var totalOutputTokens = 0;
4159
5094
  var messageCount = 0;
4160
5095
  function isGitRepo(dir) {
4161
- return existsSync10(join7(dir, ".git"));
5096
+ return existsSync10(join8(dir, ".git"));
4162
5097
  }
4163
5098
  function getOsVersion() {
4164
5099
  try {
@@ -4206,6 +5141,7 @@ ${c.bold("Interactive Commands:")}
4206
5141
  ${c.cyan("/skills")} List available skills
4207
5142
  ${c.cyan("/todos")} Show current todo list
4208
5143
  ${c.cyan("/usage")} Show token usage statistics
5144
+ ${c.cyan("/debug")} Show debug info (prompt, model, env)
4209
5145
  ${c.cyan("/exit")} Exit the CLI
4210
5146
 
4211
5147
  ${c.bold("Environment:")}
@@ -4252,6 +5188,7 @@ function printInteractiveHelp() {
4252
5188
  console.log(` ${c.cyan("/skills")} List available skills`);
4253
5189
  console.log(` ${c.cyan("/todos")} Show current todo list`);
4254
5190
  console.log(` ${c.cyan("/usage")} Show token usage statistics`);
5191
+ console.log(` ${c.cyan("/debug")} Show debug info (prompt, model, env)`);
4255
5192
  console.log(` ${c.cyan("/exit")} Exit the CLI`);
4256
5193
  console.log();
4257
5194
  }
@@ -4322,6 +5259,62 @@ function printUsage() {
4322
5259
  console.log(` ${c.cyan("Est. cost:")} $${(inputCost + outputCost).toFixed(4)}`);
4323
5260
  console.log();
4324
5261
  }
5262
+ function printDebug() {
5263
+ const model = getDefaultModel();
5264
+ const tools = getAllTools();
5265
+ const systemPrompt = buildSystemPrompt();
5266
+ const cwd = process.cwd();
5267
+ console.log();
5268
+ console.log(c.bold("═══════════════════════════════════════════════════════════"));
5269
+ console.log(c.bold(" DEBUG INFORMATION "));
5270
+ console.log(c.bold("═══════════════════════════════════════════════════════════"));
5271
+ console.log();
5272
+ console.log(c.bold("Model:"));
5273
+ console.log(` ${c.cyan("Current:")} ${model}`);
5274
+ console.log(` ${c.cyan("ANTHROPIC_MODEL:")} ${process.env.ANTHROPIC_MODEL || c.dim("(not set)")}`);
5275
+ console.log(` ${c.cyan("OPENAI_MODEL:")} ${process.env.OPENAI_MODEL || c.dim("(not set)")}`);
5276
+ console.log(` ${c.cyan("OPENAI_BASE_URL:")} ${process.env.OPENAI_BASE_URL || c.dim("(not set)")}`);
5277
+ console.log();
5278
+ console.log(c.bold("API Keys:"));
5279
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
5280
+ const openaiKey = process.env.OPENAI_API_KEY;
5281
+ console.log(` ${c.cyan("ANTHROPIC_API_KEY:")} ${anthropicKey ? c.green("✓ set") + c.dim(` (${anthropicKey.slice(0, 8)}...${anthropicKey.slice(-4)})`) : c.red("✗ not set")}`);
5282
+ console.log(` ${c.cyan("OPENAI_API_KEY:")} ${openaiKey ? c.green("✓ set") + c.dim(` (${openaiKey.slice(0, 8)}...${openaiKey.slice(-4)})`) : c.red("✗ not set")}`);
5283
+ console.log();
5284
+ console.log(c.bold("Environment:"));
5285
+ console.log(` ${c.cyan("Working dir:")} ${cwd}`);
5286
+ console.log(` ${c.cyan("Git repo:")} ${isGitRepo(cwd) ? c.green("Yes") : "No"}`);
5287
+ console.log(` ${c.cyan("Platform:")} ${process.platform}`);
5288
+ console.log(` ${c.cyan("OS Version:")} ${getOsVersion()}`);
5289
+ console.log(` ${c.cyan("Shell:")} ${process.env.SHELL || c.dim("(not set)")}`);
5290
+ console.log(` ${c.cyan("Skills path:")} ${SKILLS_PATH}`);
5291
+ console.log();
5292
+ console.log(c.bold("Tools:") + c.dim(` (${tools.length} total)`));
5293
+ const toolNames = tools.map((t) => t.name);
5294
+ console.log(` ${toolNames.join(", ")}`);
5295
+ console.log();
5296
+ console.log(c.bold("Session State:"));
5297
+ console.log(` ${c.cyan("Active:")} ${session ? c.green("Yes") : "No"}`);
5298
+ console.log(` ${c.cyan("Messages:")} ${messageCount}`);
5299
+ console.log(` ${c.cyan("Input tokens:")} ${totalInputTokens.toLocaleString()}`);
5300
+ console.log(` ${c.cyan("Output tokens:")} ${totalOutputTokens.toLocaleString()}`);
5301
+ console.log();
5302
+ console.log(c.bold("System Prompt:") + c.dim(` (${systemPrompt.length} chars)`));
5303
+ console.log(c.dim("─".repeat(60)));
5304
+ const promptLines = systemPrompt.split(`
5305
+ `);
5306
+ const maxLines = 50;
5307
+ for (let i = 0;i < Math.min(promptLines.length, maxLines); i++) {
5308
+ const lineNum = String(i + 1).padStart(3, " ");
5309
+ const line = promptLines[i].slice(0, 75);
5310
+ console.log(`${c.dim(lineNum + "│")} ${line}${promptLines[i].length > 75 ? c.dim("...") : ""}`);
5311
+ }
5312
+ if (promptLines.length > maxLines) {
5313
+ console.log(c.dim(` ... (${promptLines.length - maxLines} more lines)`));
5314
+ }
5315
+ console.log(c.dim("─".repeat(60)));
5316
+ console.log();
5317
+ }
4325
5318
  function formatToolInput(name, input) {
4326
5319
  switch (name) {
4327
5320
  case "Bash":
@@ -4449,6 +5442,9 @@ async function handleInput(input) {
4449
5442
  case "/usage":
4450
5443
  printUsage();
4451
5444
  return true;
5445
+ case "/debug":
5446
+ printDebug();
5447
+ return true;
4452
5448
  case "/exit":
4453
5449
  case "/quit":
4454
5450
  case "/q":