min-agent 0.1.0 → 0.1.1

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 (82) hide show
  1. package/README.md +6 -0
  2. package/bin/min-agent.js +2 -2
  3. package/dist/agent.d.ts +23 -0
  4. package/dist/agent.js +566 -0
  5. package/dist/assistant-stream.d.ts +23 -0
  6. package/dist/assistant-stream.js +114 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +471 -0
  9. package/dist/compaction.d.ts +14 -0
  10. package/dist/compaction.js +99 -0
  11. package/dist/config.d.ts +17 -0
  12. package/dist/config.js +142 -0
  13. package/dist/confirm.d.ts +6 -0
  14. package/dist/confirm.js +37 -0
  15. package/dist/instructions.d.ts +1 -0
  16. package/dist/instructions.js +115 -0
  17. package/dist/markdown.d.ts +15 -0
  18. package/dist/markdown.js +130 -0
  19. package/dist/mcp.d.ts +61 -0
  20. package/dist/mcp.js +237 -0
  21. package/dist/memory.d.ts +31 -0
  22. package/dist/memory.js +131 -0
  23. package/dist/output.d.ts +6 -0
  24. package/dist/output.js +52 -0
  25. package/dist/plugins.d.ts +2 -0
  26. package/dist/plugins.js +66 -0
  27. package/dist/provider.d.ts +2 -0
  28. package/dist/provider.js +41 -0
  29. package/dist/serve.d.ts +9 -0
  30. package/dist/serve.js +351 -0
  31. package/dist/sessions.d.ts +17 -0
  32. package/dist/sessions.js +74 -0
  33. package/dist/skills.d.ts +12 -0
  34. package/dist/skills.js +127 -0
  35. package/dist/tool-output.d.ts +31 -0
  36. package/dist/tool-output.js +119 -0
  37. package/dist/tools/bash.d.ts +6 -0
  38. package/dist/tools/bash.js +93 -0
  39. package/dist/tools/edit.d.ts +7 -0
  40. package/dist/tools/edit.js +51 -0
  41. package/dist/tools/glob.d.ts +6 -0
  42. package/dist/tools/glob.js +36 -0
  43. package/dist/tools/grep.d.ts +7 -0
  44. package/dist/tools/grep.js +35 -0
  45. package/dist/tools/index.d.ts +37 -0
  46. package/dist/tools/index.js +20 -0
  47. package/dist/tools/read.d.ts +7 -0
  48. package/dist/tools/read.js +36 -0
  49. package/dist/tools/web_fetch.d.ts +6 -0
  50. package/dist/tools/web_fetch.js +83 -0
  51. package/dist/tools/web_search.d.ts +6 -0
  52. package/dist/tools/web_search.js +40 -0
  53. package/dist/tools/write.d.ts +6 -0
  54. package/dist/tools/write.js +32 -0
  55. package/package.json +4 -5
  56. package/src/agent.ts +0 -609
  57. package/src/assistant-stream.ts +0 -128
  58. package/src/cli.ts +0 -494
  59. package/src/compaction.ts +0 -119
  60. package/src/config.ts +0 -172
  61. package/src/confirm.ts +0 -42
  62. package/src/instructions.ts +0 -123
  63. package/src/markdown.ts +0 -140
  64. package/src/mcp.ts +0 -300
  65. package/src/memory.ts +0 -164
  66. package/src/output.ts +0 -58
  67. package/src/plugins.ts +0 -94
  68. package/src/provider.ts +0 -50
  69. package/src/serve.ts +0 -400
  70. package/src/sessions.ts +0 -94
  71. package/src/skills.ts +0 -146
  72. package/src/tool-output.ts +0 -146
  73. package/src/tools/bash.ts +0 -108
  74. package/src/tools/edit.ts +0 -65
  75. package/src/tools/glob.ts +0 -37
  76. package/src/tools/grep.ts +0 -37
  77. package/src/tools/index.ts +0 -21
  78. package/src/tools/read.ts +0 -38
  79. package/src/tools/web_fetch.ts +0 -87
  80. package/src/tools/web_search.ts +0 -42
  81. package/src/tools/write.ts +0 -36
  82. package/tsconfig.json +0 -15
@@ -0,0 +1,119 @@
1
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
2
+ import path from "path";
3
+ import { randomBytes } from "crypto";
4
+ import { getConfigDir } from "./config.js";
5
+ /** Default limits (aligned with common agent tooling practice). */
6
+ export const TOOL_OUTPUT_MAX_LINES = 2000;
7
+ export const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
8
+ const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
9
+ function toolOutputDir() {
10
+ return path.join(getConfigDir(), "tool-output");
11
+ }
12
+ function cleanupOldToolOutputs(dir) {
13
+ if (!existsSync(dir))
14
+ return;
15
+ const now = Date.now();
16
+ try {
17
+ for (const f of readdirSync(dir)) {
18
+ if (!f.startsWith("tool-") || !f.endsWith(".txt"))
19
+ continue;
20
+ const p = path.join(dir, f);
21
+ try {
22
+ if (now - statSync(p).mtimeMs > RETENTION_MS)
23
+ unlinkSync(p);
24
+ }
25
+ catch {
26
+ /* ignore */
27
+ }
28
+ }
29
+ }
30
+ catch {
31
+ /* ignore */
32
+ }
33
+ }
34
+ /** Write full text to ~/.min-agent/tool-output/ and return absolute path. */
35
+ export function writeFullToolOutput(fullText) {
36
+ const dir = toolOutputDir();
37
+ mkdirSync(dir, { recursive: true });
38
+ cleanupOldToolOutputs(dir);
39
+ const name = `tool-${Date.now()}-${randomBytes(4).toString("hex")}.txt`;
40
+ const filePath = path.join(dir, name);
41
+ writeFileSync(filePath, fullText, "utf-8");
42
+ return filePath;
43
+ }
44
+ const hint = (filePath) => `The tool output was truncated. Full output saved to: ${filePath}\nUse the read tool with startLine/endLine, or grep, to inspect further.`;
45
+ /** Keep end of text within line/byte limits (good for shell logs). */
46
+ export function tailPreview(text, maxLines, maxBytes) {
47
+ const lines = text.split("\n");
48
+ const totalBytes = Buffer.byteLength(text, "utf-8");
49
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
50
+ return { text, cut: false };
51
+ }
52
+ const out = [];
53
+ let bytes = 0;
54
+ for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
55
+ const size = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0);
56
+ if (bytes + size > maxBytes) {
57
+ if (out.length === 0) {
58
+ const buf = Buffer.from(lines[i], "utf-8");
59
+ let start = buf.length - maxBytes;
60
+ if (start < 0)
61
+ start = 0;
62
+ while (start < buf.length && (buf[start] & 0xc0) === 0x80)
63
+ start++;
64
+ out.unshift(buf.subarray(start).toString("utf-8"));
65
+ }
66
+ break;
67
+ }
68
+ out.unshift(lines[i]);
69
+ bytes += size;
70
+ }
71
+ return { text: out.join("\n"), cut: true };
72
+ }
73
+ /** Keep start of text within line/byte limits (good for files / HTTP bodies). */
74
+ export function headPreview(text, maxLines, maxBytes) {
75
+ const lines = text.split("\n");
76
+ const totalBytes = Buffer.byteLength(text, "utf-8");
77
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
78
+ return { text, cut: false };
79
+ }
80
+ const out = [];
81
+ let bytes = 0;
82
+ for (let i = 0; i < lines.length && out.length < maxLines; i++) {
83
+ const size = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0);
84
+ if (bytes + size > maxBytes) {
85
+ if (out.length === 0) {
86
+ const buf = Buffer.from(lines[i], "utf-8");
87
+ let end = Math.min(maxBytes, buf.length);
88
+ while (end > 0 && (buf[end - 1] & 0xc0) === 0x80)
89
+ end--;
90
+ out.push(buf.subarray(0, end).toString("utf-8"));
91
+ }
92
+ break;
93
+ }
94
+ out.push(lines[i]);
95
+ bytes += size;
96
+ }
97
+ return { text: out.join("\n"), cut: true };
98
+ }
99
+ /**
100
+ * If text exceeds limits, write full text to disk and return a preview + path hint.
101
+ * Otherwise returns the original string.
102
+ */
103
+ export function truncateToolOutput(text, options = {}) {
104
+ const maxLines = options.maxLines ?? TOOL_OUTPUT_MAX_LINES;
105
+ const maxBytes = options.maxBytes ?? TOOL_OUTPUT_MAX_BYTES;
106
+ const direction = options.direction ?? "head";
107
+ const lines = text.split("\n");
108
+ const totalBytes = Buffer.byteLength(text, "utf-8");
109
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
110
+ return { content: text, truncated: false };
111
+ }
112
+ const filePath = writeFullToolOutput(text);
113
+ const preview = direction === "tail" ? tailPreview(text, maxLines, maxBytes).text : headPreview(text, maxLines, maxBytes).text;
114
+ const header = "...output truncated...\n\n";
115
+ const content = direction === "tail"
116
+ ? `${header}${hint(filePath)}\n\n${preview}`
117
+ : `${preview}\n\n${header}${hint(filePath)}`;
118
+ return { content, truncated: true, outputPath: filePath };
119
+ }
@@ -0,0 +1,6 @@
1
+ type BashInput = {
2
+ command: string;
3
+ timeout?: number;
4
+ };
5
+ export declare const bashTool: import("ai").Tool<BashInput, string>;
6
+ export {};
@@ -0,0 +1,93 @@
1
+ import { spawn } from "child_process";
2
+ import { tool, jsonSchema } from "ai";
3
+ import { confirm, isDangerousCommand, isAutoApprove } from "../confirm.js";
4
+ import { truncateToolOutput } from "../tool-output.js";
5
+ /** Hard cap for in-memory collection before killing the process (avoid OOM on huge stdout). */
6
+ const COLLECT_HARD_CAP_BYTES = 16 * 1024 * 1024;
7
+ function runCommand(command, cwd, timeoutMs) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn(command, {
10
+ shell: true,
11
+ cwd,
12
+ env: process.env,
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ });
15
+ const outChunks = [];
16
+ const errChunks = [];
17
+ let total = 0;
18
+ let killedCap = false;
19
+ let killedTimeout = false;
20
+ const timer = setTimeout(() => {
21
+ killedTimeout = true;
22
+ child.kill("SIGTERM");
23
+ setTimeout(() => child.kill("SIGKILL"), 2000).unref();
24
+ }, timeoutMs);
25
+ const push = (buf, arr) => {
26
+ total += buf.length;
27
+ if (total > COLLECT_HARD_CAP_BYTES && !killedCap) {
28
+ killedCap = true;
29
+ child.kill("SIGKILL");
30
+ return;
31
+ }
32
+ arr.push(buf);
33
+ };
34
+ child.stdout?.on("data", (b) => push(b, outChunks));
35
+ child.stderr?.on("data", (b) => push(b, errChunks));
36
+ child.on("error", (err) => {
37
+ clearTimeout(timer);
38
+ reject(err);
39
+ });
40
+ child.on("close", (code) => {
41
+ clearTimeout(timer);
42
+ const stdout = Buffer.concat(outChunks).toString("utf-8");
43
+ const stderr = Buffer.concat(errChunks).toString("utf-8");
44
+ let combined = stdout.replace(/\s+$/, "");
45
+ if (stderr)
46
+ combined += (combined ? "\n" : "") + stderr.replace(/\s+$/, "");
47
+ if (killedCap) {
48
+ combined +=
49
+ `\n\n[bash] Output collection stopped: exceeded ${COLLECT_HARD_CAP_BYTES} bytes in-memory cap (process was killed). Prefer redirecting to a file (e.g. > out.txt) then read with startLine/endLine.`;
50
+ }
51
+ else if (killedTimeout) {
52
+ combined += `\n\n[bash] Command exceeded timeout ${timeoutMs} ms (process terminated).`;
53
+ }
54
+ resolve({
55
+ code,
56
+ output: combined || "(no output)",
57
+ killedByTimeout: killedTimeout,
58
+ killedByCap: killedCap,
59
+ });
60
+ });
61
+ });
62
+ }
63
+ export const bashTool = tool({
64
+ description: "Run a shell command. Use this for system operations, running builds, tests, git commands, etc. The command runs in the current working directory. Very long stdout/stderr is truncated: full output may be saved under ~/.min-agent/tool-output/ with a preview returned.",
65
+ inputSchema: jsonSchema({
66
+ type: "object",
67
+ properties: {
68
+ command: { type: "string", description: "The shell command to execute" },
69
+ timeout: { type: "number", description: "Timeout in milliseconds (default: 30000)" },
70
+ },
71
+ required: ["command"],
72
+ }),
73
+ execute: async ({ command, timeout }) => {
74
+ if (!isAutoApprove() && isDangerousCommand(command)) {
75
+ const approved = await confirm(`Execute dangerous command: ${command}`);
76
+ if (!approved)
77
+ return "Command rejected by user.";
78
+ }
79
+ const timeoutMs = timeout ?? 30000;
80
+ try {
81
+ const { code, output, killedByTimeout, killedByCap } = await runCommand(command, process.cwd(), timeoutMs);
82
+ let body = output;
83
+ if (code !== 0 && code !== null && !killedByTimeout && !killedByCap) {
84
+ body = `Exit code ${code}\n${body}`;
85
+ }
86
+ const { content } = truncateToolOutput(body, { direction: "tail" });
87
+ return content.trim() || "(no output)";
88
+ }
89
+ catch (err) {
90
+ return `Error: ${err.message ?? String(err)}`;
91
+ }
92
+ },
93
+ });
@@ -0,0 +1,7 @@
1
+ type EditInput = {
2
+ filePath: string;
3
+ oldText: string;
4
+ newText: string;
5
+ };
6
+ export declare const editTool: import("ai").Tool<EditInput, string>;
7
+ export {};
@@ -0,0 +1,51 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { readFileSync, writeFileSync, existsSync } from "fs";
3
+ import path from "path";
4
+ import { confirm, isAutoApprove } from "../confirm.js";
5
+ export const editTool = tool({
6
+ description: "Edit a file by replacing a specific text block with new content. The oldText must match exactly (including whitespace and indentation). Use this for precise edits instead of rewriting entire files.",
7
+ inputSchema: jsonSchema({
8
+ type: "object",
9
+ properties: {
10
+ filePath: { type: "string", description: "Path to the file to edit (relative to cwd or absolute)" },
11
+ oldText: { type: "string", description: "The exact text to find and replace (must match exactly)" },
12
+ newText: { type: "string", description: "The new text to replace it with" },
13
+ },
14
+ required: ["filePath", "oldText", "newText"],
15
+ }),
16
+ execute: async ({ filePath, oldText, newText }) => {
17
+ const resolved = path.resolve(process.cwd(), filePath);
18
+ if (!existsSync(resolved)) {
19
+ return `Error: File not found: ${filePath}`;
20
+ }
21
+ const content = readFileSync(resolved, "utf-8");
22
+ const occurrences = content.split(oldText).length - 1;
23
+ if (occurrences === 0) {
24
+ // Try to help: show nearby content
25
+ const lines = content.split("\n");
26
+ const searchLines = oldText.split("\n");
27
+ const firstLine = searchLines[0].trim();
28
+ const nearbyIdx = lines.findIndex((l) => l.includes(firstLine));
29
+ if (nearbyIdx >= 0) {
30
+ const context = lines.slice(Math.max(0, nearbyIdx - 1), nearbyIdx + 3).join("\n");
31
+ return `Error: oldText not found exactly. Found similar content near line ${nearbyIdx + 1}:\n${context}\n\nMake sure whitespace and indentation match exactly.`;
32
+ }
33
+ return `Error: oldText not found in ${filePath}. Make sure the text matches exactly including whitespace.`;
34
+ }
35
+ if (occurrences > 1) {
36
+ return `Error: oldText found ${occurrences} times in ${filePath}. Please provide more context to make the match unique.`;
37
+ }
38
+ // Confirm edit
39
+ if (!isAutoApprove()) {
40
+ const preview = oldText.length > 80 ? oldText.slice(0, 80) + "..." : oldText;
41
+ const approved = await confirm(`Edit ${filePath}: replace "${preview}"`);
42
+ if (!approved)
43
+ return "Edit rejected by user.";
44
+ }
45
+ const updated = content.replace(oldText, newText);
46
+ writeFileSync(resolved, updated, "utf-8");
47
+ const oldLines = oldText.split("\n").length;
48
+ const newLines = newText.split("\n").length;
49
+ return `Edited ${filePath}: replaced ${oldLines} line(s) with ${newLines} line(s)`;
50
+ },
51
+ });
@@ -0,0 +1,6 @@
1
+ type GlobInput = {
2
+ pattern: string;
3
+ cwd?: string;
4
+ };
5
+ export declare const globTool: import("ai").Tool<GlobInput, string>;
6
+ export {};
@@ -0,0 +1,36 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { globSync } from "glob";
3
+ import { truncateToolOutput } from "../tool-output.js";
4
+ export const globTool = tool({
5
+ description: "Find files matching a glob pattern. Returns a list of file paths. Use this to discover project structure and find files.",
6
+ inputSchema: jsonSchema({
7
+ type: "object",
8
+ properties: {
9
+ pattern: { type: "string", description: "Glob pattern to match (e.g. 'src/**/*.ts', '*.json')" },
10
+ cwd: { type: "string", description: "Directory to search in (defaults to working directory)" },
11
+ },
12
+ required: ["pattern"],
13
+ }),
14
+ execute: async ({ pattern, cwd }) => {
15
+ try {
16
+ const matches = globSync(pattern, {
17
+ cwd: cwd ?? process.cwd(),
18
+ ignore: ["**/node_modules/**", "**/.git/**"],
19
+ nodir: true,
20
+ });
21
+ if (matches.length === 0)
22
+ return "No files found matching pattern";
23
+ let text;
24
+ if (matches.length > 100) {
25
+ text = matches.slice(0, 100).join("\n") + `\n\n... (${matches.length - 100} more files)`;
26
+ }
27
+ else {
28
+ text = matches.join("\n");
29
+ }
30
+ return truncateToolOutput(text, { direction: "head" }).content;
31
+ }
32
+ catch (err) {
33
+ return `Error: ${err.message}`;
34
+ }
35
+ },
36
+ });
@@ -0,0 +1,7 @@
1
+ type GrepInput = {
2
+ pattern: string;
3
+ path?: string;
4
+ include?: string;
5
+ };
6
+ export declare const grepTool: import("ai").Tool<GrepInput, string>;
7
+ export {};
@@ -0,0 +1,35 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { execSync } from "child_process";
3
+ import { truncateToolOutput } from "../tool-output.js";
4
+ export const grepTool = tool({
5
+ description: "Search for a pattern in files using grep. Returns matching lines with file paths and line numbers. Use this to find code references, usages, and definitions.",
6
+ inputSchema: jsonSchema({
7
+ type: "object",
8
+ properties: {
9
+ pattern: { type: "string", description: "The regex pattern to search for" },
10
+ path: { type: "string", description: "File or directory path to search in (defaults to current directory)" },
11
+ include: { type: "string", description: "File pattern to include (e.g. '*.ts')" },
12
+ },
13
+ required: ["pattern"],
14
+ }),
15
+ execute: async ({ pattern, path: searchPath, include }) => {
16
+ const target = searchPath ?? ".";
17
+ const includeFlag = include ? `--include='${include}'` : "";
18
+ const cmd = `grep -rn ${includeFlag} --color=never -E '${pattern.replace(/'/g, "'\\''")}' '${target}' 2>/dev/null | head -50`;
19
+ try {
20
+ const output = execSync(cmd, {
21
+ encoding: "utf-8",
22
+ cwd: process.cwd(),
23
+ timeout: 10000,
24
+ maxBuffer: 512 * 1024,
25
+ });
26
+ const text = output.trim() || "No matches found";
27
+ return truncateToolOutput(text, { direction: "head" }).content;
28
+ }
29
+ catch (err) {
30
+ if (err.status === 1)
31
+ return "No matches found";
32
+ return `Error: ${err.message}`;
33
+ }
34
+ },
35
+ });
@@ -0,0 +1,37 @@
1
+ export declare function createTools(): {
2
+ bash: import("ai").Tool<{
3
+ command: string;
4
+ timeout?: number;
5
+ }, string>;
6
+ read: import("ai").Tool<{
7
+ filePath: string;
8
+ startLine?: number;
9
+ endLine?: number;
10
+ }, string>;
11
+ write: import("ai").Tool<{
12
+ filePath: string;
13
+ content: string;
14
+ }, string>;
15
+ edit: import("ai").Tool<{
16
+ filePath: string;
17
+ oldText: string;
18
+ newText: string;
19
+ }, string>;
20
+ glob: import("ai").Tool<{
21
+ pattern: string;
22
+ cwd?: string;
23
+ }, string>;
24
+ grep: import("ai").Tool<{
25
+ pattern: string;
26
+ path?: string;
27
+ include?: string;
28
+ }, string>;
29
+ web_search: import("ai").Tool<{
30
+ query: string;
31
+ categories?: string;
32
+ }, string>;
33
+ web_fetch: import("ai").Tool<{
34
+ url: string;
35
+ method?: string;
36
+ }, string>;
37
+ };
@@ -0,0 +1,20 @@
1
+ import { bashTool } from "./bash.js";
2
+ import { readTool } from "./read.js";
3
+ import { writeTool } from "./write.js";
4
+ import { editTool } from "./edit.js";
5
+ import { globTool } from "./glob.js";
6
+ import { grepTool } from "./grep.js";
7
+ import { webSearchTool } from "./web_search.js";
8
+ import { webFetchTool } from "./web_fetch.js";
9
+ export function createTools() {
10
+ return {
11
+ bash: bashTool,
12
+ read: readTool,
13
+ write: writeTool,
14
+ edit: editTool,
15
+ glob: globTool,
16
+ grep: grepTool,
17
+ web_search: webSearchTool,
18
+ web_fetch: webFetchTool,
19
+ };
20
+ }
@@ -0,0 +1,7 @@
1
+ type ReadInput = {
2
+ filePath: string;
3
+ startLine?: number;
4
+ endLine?: number;
5
+ };
6
+ export declare const readTool: import("ai").Tool<ReadInput, string>;
7
+ export {};
@@ -0,0 +1,36 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { readFileSync, statSync } from "fs";
3
+ import path from "path";
4
+ import { truncateToolOutput } from "../tool-output.js";
5
+ export const readTool = tool({
6
+ description: "Read the contents of a file. Returns the file content as text. Use this to understand code, check configurations, etc.",
7
+ inputSchema: jsonSchema({
8
+ type: "object",
9
+ properties: {
10
+ filePath: { type: "string", description: "Path to the file to read (relative to cwd or absolute)" },
11
+ startLine: { type: "number", description: "Start line number (1-indexed)" },
12
+ endLine: { type: "number", description: "End line number (1-indexed, inclusive)" },
13
+ },
14
+ required: ["filePath"],
15
+ }),
16
+ execute: async ({ filePath, startLine, endLine }) => {
17
+ const resolved = path.resolve(process.cwd(), filePath);
18
+ try {
19
+ const stat = statSync(resolved);
20
+ if (stat.isDirectory())
21
+ return `Error: ${filePath} is a directory, not a file`;
22
+ const content = readFileSync(resolved, "utf-8");
23
+ if (startLine || endLine) {
24
+ const lines = content.split("\n");
25
+ const start = (startLine ?? 1) - 1;
26
+ const end = endLine ?? lines.length;
27
+ const slice = lines.slice(start, end).join("\n");
28
+ return truncateToolOutput(slice, { direction: "head" }).content;
29
+ }
30
+ return truncateToolOutput(content, { direction: "head" }).content;
31
+ }
32
+ catch (err) {
33
+ return `Error reading file: ${err.message}`;
34
+ }
35
+ },
36
+ });
@@ -0,0 +1,6 @@
1
+ type WebFetchInput = {
2
+ url: string;
3
+ method?: string;
4
+ };
5
+ export declare const webFetchTool: import("ai").Tool<WebFetchInput, string>;
6
+ export {};
@@ -0,0 +1,83 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { truncateToolOutput } from "../tool-output.js";
3
+ const FIRECRAWL_BASE = "https://fireclawl.xc.lonae.com";
4
+ export const webFetchTool = tool({
5
+ description: "Fetch content from a URL. Use this to access web pages, APIs, or any HTTP resource. Returns the response body as text (or markdown for HTML pages). Supports JavaScript-rendered SPA pages via fallback.",
6
+ inputSchema: jsonSchema({
7
+ type: "object",
8
+ properties: {
9
+ url: { type: "string", description: "The URL to fetch" },
10
+ method: { type: "string", description: "HTTP method (default: GET)" },
11
+ },
12
+ required: ["url"],
13
+ }),
14
+ execute: async ({ url, method }) => {
15
+ // First try: direct fetch
16
+ const directResult = await directFetch(url, method);
17
+ // If we got meaningful content, return it
18
+ if (directResult && hasContent(directResult)) {
19
+ return directResult;
20
+ }
21
+ // Fallback: use Firecrawl for SPA/dynamic pages
22
+ const firecrawlResult = await firecrawlFetch(url);
23
+ if (firecrawlResult)
24
+ return firecrawlResult;
25
+ // Return whatever we got from direct fetch
26
+ return directResult || "Failed to fetch content from URL";
27
+ },
28
+ });
29
+ async function directFetch(url, method) {
30
+ try {
31
+ const response = await fetch(url, {
32
+ method: method ?? "GET",
33
+ headers: {
34
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
35
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
36
+ },
37
+ signal: AbortSignal.timeout(15000),
38
+ });
39
+ if (!response.ok)
40
+ return `HTTP ${response.status}`;
41
+ const text = await response.text();
42
+ return truncateToolOutput(text, { direction: "head" }).content;
43
+ }
44
+ catch (err) {
45
+ return null;
46
+ }
47
+ }
48
+ async function firecrawlFetch(url) {
49
+ try {
50
+ const response = await fetch(`${FIRECRAWL_BASE}/v1/scrape`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({
54
+ url,
55
+ formats: ["markdown"],
56
+ onlyMainContent: true,
57
+ waitFor: 3000,
58
+ timeout: 30000,
59
+ }),
60
+ signal: AbortSignal.timeout(35000),
61
+ });
62
+ if (!response.ok)
63
+ return null;
64
+ const data = await response.json();
65
+ const markdown = data?.data?.markdown || data?.data?.content;
66
+ if (!markdown)
67
+ return null;
68
+ return truncateToolOutput(markdown, { direction: "head" }).content;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function hasContent(html) {
75
+ // Check if the response has meaningful content (not just an empty SPA shell)
76
+ if (html.length < 200)
77
+ return false;
78
+ // SPA shells typically have very little text content outside of script tags
79
+ const withoutScripts = html.replace(/<script[\s\S]*?<\/script>/gi, "");
80
+ const textContent = withoutScripts.replace(/<[^>]*>/g, "").trim();
81
+ // If after removing scripts and tags there's less than 100 chars, it's likely an empty shell
82
+ return textContent.length > 100;
83
+ }
@@ -0,0 +1,6 @@
1
+ type WebSearchInput = {
2
+ query: string;
3
+ categories?: string;
4
+ };
5
+ export declare const webSearchTool: import("ai").Tool<WebSearchInput, string>;
6
+ export {};
@@ -0,0 +1,40 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { truncateToolOutput } from "../tool-output.js";
3
+ const SEARXNG_BASE = "https://searxng.xc.lonae.com";
4
+ export const webSearchTool = tool({
5
+ description: "Search the web for information. Returns search results with titles, URLs, and snippets. Use this when you need current information, news, documentation, or answers that require up-to-date knowledge.",
6
+ inputSchema: jsonSchema({
7
+ type: "object",
8
+ properties: {
9
+ query: { type: "string", description: "The search query" },
10
+ categories: { type: "string", description: "Search categories: general, news, images, science, it (default: general)" },
11
+ },
12
+ required: ["query"],
13
+ }),
14
+ execute: async ({ query, categories }) => {
15
+ try {
16
+ const params = new URLSearchParams({
17
+ q: query,
18
+ format: "json",
19
+ categories: categories ?? "general",
20
+ });
21
+ const response = await fetch(`${SEARXNG_BASE}/search?${params}`, {
22
+ headers: { "Accept": "application/json" },
23
+ signal: AbortSignal.timeout(15000),
24
+ });
25
+ if (!response.ok)
26
+ return `Search error: HTTP ${response.status}`;
27
+ const data = await response.json();
28
+ const results = (data.results ?? []).slice(0, 10);
29
+ if (results.length === 0)
30
+ return "No search results found. Try a different query.";
31
+ const text = results
32
+ .map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.content ?? ""}`)
33
+ .join("\n\n");
34
+ return truncateToolOutput(text, { direction: "head" }).content;
35
+ }
36
+ catch (err) {
37
+ return `Search error: ${err.message}`;
38
+ }
39
+ },
40
+ });
@@ -0,0 +1,6 @@
1
+ type WriteInput = {
2
+ filePath: string;
3
+ content: string;
4
+ };
5
+ export declare const writeTool: import("ai").Tool<WriteInput, string>;
6
+ export {};
@@ -0,0 +1,32 @@
1
+ import { tool, jsonSchema } from "ai";
2
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import path from "path";
4
+ import { confirm, isAutoApprove } from "../confirm.js";
5
+ export const writeTool = tool({
6
+ description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Creates parent directories as needed.",
7
+ inputSchema: jsonSchema({
8
+ type: "object",
9
+ properties: {
10
+ filePath: { type: "string", description: "Path to the file to write (relative to cwd or absolute)" },
11
+ content: { type: "string", description: "The content to write to the file" },
12
+ },
13
+ required: ["filePath", "content"],
14
+ }),
15
+ execute: async ({ filePath, content }) => {
16
+ const resolved = path.resolve(process.cwd(), filePath);
17
+ // Confirm overwriting existing files
18
+ if (!isAutoApprove() && existsSync(resolved)) {
19
+ const approved = await confirm(`Overwrite existing file: ${filePath}`);
20
+ if (!approved)
21
+ return "Write rejected by user.";
22
+ }
23
+ try {
24
+ mkdirSync(path.dirname(resolved), { recursive: true });
25
+ writeFileSync(resolved, content, "utf-8");
26
+ return `Written ${content.length} bytes to ${filePath}`;
27
+ }
28
+ catch (err) {
29
+ return `Error writing file: ${err.message}`;
30
+ }
31
+ },
32
+ });