@jiangyuan1209/yuan-claw 0.1.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 (45) hide show
  1. package/.env.example +28 -0
  2. package/README.md +441 -0
  3. package/dist/agent/build-system-prompt.js +46 -0
  4. package/dist/agent/message-types.js +1 -0
  5. package/dist/agent/message-utils.js +239 -0
  6. package/dist/agent/parse-agent-response.js +48 -0
  7. package/dist/agent/prompts.js +27 -0
  8. package/dist/agent/protocol.js +1 -0
  9. package/dist/agent/read-approval.js +45 -0
  10. package/dist/agent/read-confirmation.js +17 -0
  11. package/dist/agent/run-local-agent-loop.js +272 -0
  12. package/dist/cli/help.js +24 -0
  13. package/dist/cli/main.js +108 -0
  14. package/dist/cli/parse-args.js +75 -0
  15. package/dist/cli/repl.js +188 -0
  16. package/dist/config/config-path.js +9 -0
  17. package/dist/config/init-user-config.js +39 -0
  18. package/dist/config/load-config.js +46 -0
  19. package/dist/config.js +29 -0
  20. package/dist/events/event-bus.js +62 -0
  21. package/dist/lib/initGlobalProxy.js +52 -0
  22. package/dist/memory/session-store.js +43 -0
  23. package/dist/memory/types.js +1 -0
  24. package/dist/model/client.js +7 -0
  25. package/dist/model/providers/openai-compatible.js +42 -0
  26. package/dist/scripts/test-brave.js +16 -0
  27. package/dist/security/network-policy.js +19 -0
  28. package/dist/security/path-guards.js +18 -0
  29. package/dist/security/shell-policy.js +40 -0
  30. package/dist/shared/utils.js +1 -0
  31. package/dist/tools/file/grep-text.js +85 -0
  32. package/dist/tools/file/list-files.js +52 -0
  33. package/dist/tools/file/read-file.js +39 -0
  34. package/dist/tools/file/write-file.js +37 -0
  35. package/dist/tools/git/git-diff.js +54 -0
  36. package/dist/tools/git/git-status.js +44 -0
  37. package/dist/tools/registry.js +41 -0
  38. package/dist/tools/shell/shell-exec.js +41 -0
  39. package/dist/tools/types.js +1 -0
  40. package/dist/tools/web/extract-readable-text.js +54 -0
  41. package/dist/tools/web/http-fetch.js +55 -0
  42. package/dist/tools/web/index.js +4 -0
  43. package/dist/tools/web/search-providers/brave.js +31 -0
  44. package/dist/tools/web/web-search.js +33 -0
  45. package/package.json +26 -0
@@ -0,0 +1,42 @@
1
+ import OpenAI from "openai";
2
+ export function createOpenAICompatibleClient(options = {}) {
3
+ const config = options.config ?? {};
4
+ const apiKey = config.MODEL_API_KEY ??
5
+ config.OPENAI_API_KEY ??
6
+ process.env.MODEL_API_KEY ??
7
+ process.env.OPENAI_API_KEY;
8
+ const baseURL = config.MODEL_BASE_URL ??
9
+ config.OPENAI_BASE_URL ??
10
+ process.env.MODEL_BASE_URL ??
11
+ process.env.OPENAI_BASE_URL;
12
+ const model = options.model ??
13
+ config.MODEL_NAME ??
14
+ config.OPENAI_MODEL ??
15
+ process.env.MODEL_NAME ??
16
+ process.env.OPENAI_MODEL ??
17
+ "gpt-4o-mini";
18
+ if (!apiKey) {
19
+ throw new Error("Missing MODEL_API_KEY / OPENAI_API_KEY in environment variables or ~/.my-agent/settings.json.");
20
+ }
21
+ const client = new OpenAI({
22
+ apiKey,
23
+ baseURL,
24
+ });
25
+ return {
26
+ async generate(messages) {
27
+ const response = await client.chat.completions.create({
28
+ model,
29
+ messages: messages.map((message) => ({
30
+ role: message.role === "tool" ? "user" : message.role,
31
+ content: message.content,
32
+ })),
33
+ temperature: 0,
34
+ });
35
+ const text = response.choices[0]?.message?.content;
36
+ if (typeof text !== "string" || !text.trim()) {
37
+ throw new Error("Model returned empty content.");
38
+ }
39
+ return text;
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,16 @@
1
+ import { initGlobalProxy } from "../lib/initGlobalProxy.js";
2
+ import { createBraveSearchProvider } from "../tools/web/index.js";
3
+ initGlobalProxy();
4
+ async function main() {
5
+ const apiKey = process.env.BRAVE_API_KEY;
6
+ if (!apiKey) {
7
+ throw new Error("Missing BRAVE_API_KEY");
8
+ }
9
+ const search = createBraveSearchProvider({ apiKey });
10
+ const results = await search("OpenAI", 5);
11
+ console.log(results);
12
+ }
13
+ main().catch((err) => {
14
+ console.error(err);
15
+ process.exit(1);
16
+ });
@@ -0,0 +1,19 @@
1
+ export function validateUrlByPolicy(rawUrl) {
2
+ let url;
3
+ try {
4
+ url = new URL(rawUrl);
5
+ }
6
+ catch {
7
+ throw new Error(`Invalid URL: ${rawUrl}`);
8
+ }
9
+ if (!["http:", "https:"].includes(url.protocol)) {
10
+ throw new Error(`Unsupported URL protocol: ${url.protocol}`);
11
+ }
12
+ const hostname = url.hostname.toLowerCase();
13
+ if (hostname === "localhost" ||
14
+ hostname === "127.0.0.1" ||
15
+ hostname === "::1") {
16
+ throw new Error(`Blocked local address: ${hostname}`);
17
+ }
18
+ return url;
19
+ }
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ export function resolveWorkspaceRoot(workspaceRoot) {
3
+ return path.resolve(workspaceRoot ?? process.cwd());
4
+ }
5
+ export function resolveSafePath(workspaceRoot, targetPath) {
6
+ const root = path.resolve(workspaceRoot);
7
+ const fullPath = path.resolve(root, targetPath);
8
+ const relative = path.relative(root, fullPath);
9
+ if (relative === ".." ||
10
+ relative.startsWith(`..${path.sep}`) ||
11
+ path.isAbsolute(relative)) {
12
+ throw new Error(`Path escapes workspace: ${targetPath}`);
13
+ }
14
+ return fullPath;
15
+ }
16
+ export function toWorkspaceRelativePath(workspaceRoot, fullPath) {
17
+ return path.relative(path.resolve(workspaceRoot), path.resolve(fullPath));
18
+ }
@@ -0,0 +1,40 @@
1
+ const DEFAULT_DENY_PATTERNS = [
2
+ /\brm\s+-rf\s+\//i,
3
+ /\bsudo\b/i,
4
+ /\bshutdown\b/i,
5
+ /\breboot\b/i,
6
+ /\bmkfs\b/i,
7
+ /\bdd\b/i,
8
+ />\s*\/dev\//i,
9
+ ];
10
+ const DEFAULT_ALLOW_PREFIXES = [
11
+ "ls",
12
+ "pwd",
13
+ "cat",
14
+ "echo",
15
+ "find",
16
+ "grep",
17
+ "head",
18
+ "tail",
19
+ "wc",
20
+ "sort",
21
+ "uniq",
22
+ "npm",
23
+ "node",
24
+ "git",
25
+ ];
26
+ export function validateShellCommand(command) {
27
+ const trimmed = command.trim();
28
+ if (!trimmed) {
29
+ throw new Error("Shell command cannot be empty");
30
+ }
31
+ for (const pattern of DEFAULT_DENY_PATTERNS) {
32
+ if (pattern.test(trimmed)) {
33
+ throw new Error(`Shell command denied by policy: ${trimmed}`);
34
+ }
35
+ }
36
+ const firstToken = trimmed.split(/\s+/)[0];
37
+ if (!DEFAULT_ALLOW_PREFIXES.includes(firstToken)) {
38
+ throw new Error(`Shell command not allowed by policy. Command prefix: ${firstToken}`);
39
+ }
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { resolveSafePath } from "../../security/path-guards.js";
5
+ const GrepTextInputSchema = z.object({
6
+ query: z.string().min(1),
7
+ path: z.string().default("."),
8
+ maxResults: z.number().int().positive().max(200).default(50),
9
+ caseSensitive: z.boolean().default(false),
10
+ });
11
+ export function createGrepTextTool(options) {
12
+ return {
13
+ name: "grep_text",
14
+ description: "Search text in files under the workspace",
15
+ riskLevel: "safe",
16
+ inputSchema: GrepTextInputSchema,
17
+ async execute(rawArgs) {
18
+ try {
19
+ const args = GrepTextInputSchema.parse(rawArgs);
20
+ const basePath = resolveSafePath(options.workspaceRoot, args.path);
21
+ const matches = [];
22
+ async function walk(currentPath) {
23
+ const dirents = await fs.readdir(currentPath, { withFileTypes: true });
24
+ for (const dirent of dirents) {
25
+ if (matches.length >= args.maxResults)
26
+ return;
27
+ const full = path.join(currentPath, dirent.name);
28
+ if (dirent.isDirectory()) {
29
+ if (dirent.name === "node_modules" ||
30
+ dirent.name === ".git" ||
31
+ dirent.name === "dist") {
32
+ continue;
33
+ }
34
+ await walk(full);
35
+ continue;
36
+ }
37
+ if (!dirent.isFile())
38
+ continue;
39
+ let content;
40
+ try {
41
+ content = await fs.readFile(full, "utf8");
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ const lines = content.split(/\r?\n/);
47
+ const query = args.caseSensitive
48
+ ? args.query
49
+ : args.query.toLowerCase();
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const lineText = lines[i];
52
+ const target = args.caseSensitive
53
+ ? lineText
54
+ : lineText.toLowerCase();
55
+ if (target.includes(query)) {
56
+ matches.push({
57
+ file: path.relative(options.workspaceRoot, full),
58
+ line: i + 1,
59
+ text: lineText,
60
+ });
61
+ if (matches.length >= args.maxResults)
62
+ return;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ await walk(basePath);
68
+ return {
69
+ success: true,
70
+ output: {
71
+ query: args.query,
72
+ matches,
73
+ truncated: matches.length >= args.maxResults,
74
+ },
75
+ };
76
+ }
77
+ catch (error) {
78
+ return {
79
+ success: false,
80
+ error: error instanceof Error ? error.message : String(error),
81
+ };
82
+ }
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { resolveSafePath } from "../../security/path-guards.js";
5
+ const ListFilesInputSchema = z.object({
6
+ path: z.string().default("."),
7
+ recursive: z.boolean().default(false),
8
+ maxEntries: z.number().int().positive().max(1000).default(200),
9
+ });
10
+ export function createListFilesTool(options) {
11
+ return {
12
+ name: "list_files",
13
+ description: "List files and directories inside the workspace",
14
+ riskLevel: "safe",
15
+ inputSchema: ListFilesInputSchema,
16
+ async execute(rawArgs) {
17
+ try {
18
+ const args = ListFilesInputSchema.parse(rawArgs);
19
+ const basePath = resolveSafePath(options.workspaceRoot, args.path);
20
+ const results = [];
21
+ async function walk(currentPath) {
22
+ const dirents = await fs.readdir(currentPath, { withFileTypes: true });
23
+ for (const dirent of dirents) {
24
+ if (results.length >= args.maxEntries)
25
+ return;
26
+ const full = path.join(currentPath, dirent.name);
27
+ const rel = path.relative(options.workspaceRoot, full) || ".";
28
+ results.push(dirent.isDirectory() ? `${rel}/` : rel);
29
+ if (args.recursive && dirent.isDirectory()) {
30
+ await walk(full);
31
+ }
32
+ }
33
+ }
34
+ await walk(basePath);
35
+ return {
36
+ success: true,
37
+ output: {
38
+ path: args.path,
39
+ entries: results,
40
+ truncated: results.length >= args.maxEntries,
41
+ },
42
+ };
43
+ }
44
+ catch (error) {
45
+ return {
46
+ success: false,
47
+ error: error instanceof Error ? error.message : String(error),
48
+ };
49
+ }
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { resolveSafePath } from "../../security/path-guards.js";
4
+ const ReadFileInputSchema = z.object({
5
+ path: z.string().min(1),
6
+ maxChars: z.number().int().positive().max(50000).default(12000),
7
+ });
8
+ export function createReadFileTool(options) {
9
+ return {
10
+ name: "read_file",
11
+ description: "Read a UTF-8 text file from the workspace",
12
+ riskLevel: "safe",
13
+ inputSchema: ReadFileInputSchema,
14
+ async execute(rawArgs) {
15
+ try {
16
+ const args = ReadFileInputSchema.parse(rawArgs);
17
+ const fullPath = resolveSafePath(options.workspaceRoot, args.path);
18
+ const content = await fs.readFile(fullPath, "utf8");
19
+ const sliced = content.slice(0, args.maxChars);
20
+ return {
21
+ success: true,
22
+ output: {
23
+ path: args.path,
24
+ content: sliced,
25
+ truncated: content.length > sliced.length,
26
+ totalChars: content.length,
27
+ returnedChars: sliced.length,
28
+ },
29
+ };
30
+ }
31
+ catch (error) {
32
+ return {
33
+ success: false,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ };
36
+ }
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { resolveSafePath } from "../../security/path-guards.js";
5
+ const WriteFileInputSchema = z.object({
6
+ path: z.string().min(1),
7
+ content: z.string(),
8
+ });
9
+ export function createWriteFileTool(options) {
10
+ return {
11
+ name: "write_file",
12
+ description: "Write a UTF-8 text file inside the workspace",
13
+ riskLevel: "confirm",
14
+ inputSchema: WriteFileInputSchema,
15
+ async execute(rawArgs) {
16
+ try {
17
+ const args = WriteFileInputSchema.parse(rawArgs);
18
+ const fullPath = resolveSafePath(options.workspaceRoot, args.path);
19
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
20
+ await fs.writeFile(fullPath, args.content, "utf8");
21
+ return {
22
+ success: true,
23
+ output: {
24
+ path: args.path,
25
+ bytes: Buffer.byteLength(args.content, "utf8"),
26
+ },
27
+ };
28
+ }
29
+ catch (error) {
30
+ return {
31
+ success: false,
32
+ error: error instanceof Error ? error.message : String(error),
33
+ };
34
+ }
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,54 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { z } from "zod";
4
+ const execFileAsync = promisify(execFile);
5
+ const GitDiffInputSchema = z.object({
6
+ cached: z.boolean().default(false),
7
+ path: z.string().optional(),
8
+ maxChars: z.number().int().positive().max(50000).default(12000),
9
+ });
10
+ export function createGitDiffTool(options) {
11
+ return {
12
+ name: "git_diff",
13
+ description: "Get git diff in the workspace",
14
+ riskLevel: "safe",
15
+ inputSchema: GitDiffInputSchema,
16
+ async execute(rawArgs) {
17
+ try {
18
+ const args = GitDiffInputSchema.parse(rawArgs);
19
+ const gitArgs = ["diff"];
20
+ if (args.cached) {
21
+ gitArgs.push("--cached");
22
+ }
23
+ if (args.path) {
24
+ gitArgs.push("--", args.path);
25
+ }
26
+ const { stdout, stderr } = await execFileAsync("git", gitArgs, {
27
+ cwd: options.workspaceRoot,
28
+ timeout: 10000,
29
+ maxBuffer: 1024 * 1024,
30
+ });
31
+ const diff = stdout.slice(0, args.maxChars);
32
+ return {
33
+ success: true,
34
+ output: {
35
+ cached: args.cached,
36
+ path: args.path ?? null,
37
+ diff,
38
+ truncated: stdout.length > diff.length,
39
+ totalChars: stdout.length,
40
+ returnedChars: diff.length,
41
+ stderr,
42
+ empty: stdout.trim().length === 0,
43
+ },
44
+ };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ success: false,
49
+ error: error instanceof Error ? error.message : String(error),
50
+ };
51
+ }
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,44 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export function createGitStatusTool(options) {
5
+ return {
6
+ name: "git_status",
7
+ description: "Get git repository status in the workspace",
8
+ riskLevel: "safe",
9
+ async execute(rawArgs) {
10
+ try {
11
+ const { stdout: branchStdout } = await execFileAsync("git", ["branch", "--show-current"], {
12
+ cwd: options.workspaceRoot,
13
+ timeout: 10000,
14
+ maxBuffer: 1024 * 1024,
15
+ });
16
+ const { stdout, stderr } = await execFileAsync("git", ["status", "--short", "--branch"], {
17
+ cwd: options.workspaceRoot,
18
+ timeout: 10000,
19
+ maxBuffer: 1024 * 1024,
20
+ });
21
+ const lines = stdout
22
+ .split(/\r?\n/)
23
+ .map((line) => line.trimEnd())
24
+ .filter(Boolean);
25
+ return {
26
+ success: true,
27
+ output: {
28
+ branch: branchStdout.trim() || null,
29
+ summary: stdout,
30
+ stderr,
31
+ lines,
32
+ dirty: lines.some((line) => !line.startsWith("##")),
33
+ },
34
+ };
35
+ }
36
+ catch (error) {
37
+ return {
38
+ success: false,
39
+ error: error instanceof Error ? error.message : String(error),
40
+ };
41
+ }
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,41 @@
1
+ import { createReadFileTool } from "./file/read-file.js";
2
+ import { createWriteFileTool } from "./file/write-file.js";
3
+ import { createListFilesTool } from "./file/list-files.js";
4
+ import { createGrepTextTool } from "./file/grep-text.js";
5
+ import { createShellExecTool } from "./shell/shell-exec.js";
6
+ import { createGitStatusTool } from "./git/git-status.js";
7
+ import { createGitDiffTool } from "./git/git-diff.js";
8
+ import { createHttpFetchTool, createWebSearchTool, createExtractReadableTextTool, createBraveSearchProvider, } from "./web/index.js";
9
+ export function createToolRegistry(options) {
10
+ const tools = [
11
+ createReadFileTool({ workspaceRoot: options.workspaceRoot }),
12
+ createWriteFileTool({ workspaceRoot: options.workspaceRoot }),
13
+ createListFilesTool({ workspaceRoot: options.workspaceRoot }),
14
+ createGrepTextTool({ workspaceRoot: options.workspaceRoot }),
15
+ createShellExecTool({ workspaceRoot: options.workspaceRoot }),
16
+ createGitStatusTool({ workspaceRoot: options.workspaceRoot }),
17
+ createGitDiffTool({ workspaceRoot: options.workspaceRoot }),
18
+ createHttpFetchTool(),
19
+ createExtractReadableTextTool(),
20
+ ];
21
+ const config = options.config ?? {};
22
+ const braveApiKey = config.BRAVE_SEARCH_API_KEY ??
23
+ config.BRAVE_API_KEY ??
24
+ process.env.BRAVE_SEARCH_API_KEY ??
25
+ process.env.BRAVE_API_KEY;
26
+ if (braveApiKey) {
27
+ tools.push(createWebSearchTool({
28
+ provider: createBraveSearchProvider({
29
+ apiKey: braveApiKey,
30
+ }),
31
+ }));
32
+ console.warn("[tools] web_search enabled via Brave Search API");
33
+ }
34
+ else {
35
+ console.warn("[tools] web_search disabled: set BRAVE_SEARCH_API_KEY or BRAVE_API_KEY");
36
+ }
37
+ return new Map(tools.map((tool) => [tool.name, tool]));
38
+ }
39
+ export function getToolsFromRegistry(registry) {
40
+ return Array.from(registry.values());
41
+ }
@@ -0,0 +1,41 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { z } from "zod";
4
+ import { validateShellCommand } from "../../security/shell-policy.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const ShellExecInputSchema = z.object({
7
+ command: z.string().min(1),
8
+ timeoutMs: z.number().int().positive().max(20000).default(10000),
9
+ });
10
+ export function createShellExecTool(options) {
11
+ return {
12
+ name: "shell_exec",
13
+ description: "Execute a shell command in the workspace with safety policy",
14
+ riskLevel: "dangerous",
15
+ inputSchema: ShellExecInputSchema,
16
+ async execute(rawArgs) {
17
+ try {
18
+ const args = ShellExecInputSchema.parse(rawArgs);
19
+ validateShellCommand(args.command);
20
+ const { stdout, stderr } = await execFileAsync("sh", ["-lc", args.command], {
21
+ cwd: options.workspaceRoot,
22
+ timeout: args.timeoutMs,
23
+ maxBuffer: 1024 * 1024,
24
+ });
25
+ return {
26
+ success: true,
27
+ output: {
28
+ stdout,
29
+ stderr,
30
+ },
31
+ };
32
+ }
33
+ catch (error) {
34
+ return {
35
+ success: false,
36
+ error: error instanceof Error ? error.message : String(error),
37
+ };
38
+ }
39
+ },
40
+ };
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ const ExtractReadableTextInputSchema = z.object({
3
+ content: z.string().min(1),
4
+ maxChars: z.number().int().positive().max(50000).default(12000),
5
+ });
6
+ function stripTags(html) {
7
+ return html
8
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
9
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
10
+ .replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
11
+ .replace(/<\/(p|div|section|article|li|tr|h1|h2|h3|h4|h5|h6)>/gi, "\n")
12
+ .replace(/<br\s*\/?>/gi, "\n")
13
+ .replace(/<[^>]+>/g, " ")
14
+ .replace(/&nbsp;/gi, " ")
15
+ .replace(/&amp;/gi, "&")
16
+ .replace(/&lt;/gi, "<")
17
+ .replace(/&gt;/gi, ">")
18
+ .replace(/&quot;/gi, '"')
19
+ .replace(/&#39;/gi, "'")
20
+ .replace(/\r/g, "")
21
+ .replace(/\n{3,}/g, "\n\n")
22
+ .replace(/[ \t]{2,}/g, " ")
23
+ .trim();
24
+ }
25
+ export function createExtractReadableTextTool() {
26
+ return {
27
+ name: "extract_readable_text",
28
+ description: "Extract readable text from HTML or plain text content",
29
+ riskLevel: "safe",
30
+ inputSchema: ExtractReadableTextInputSchema,
31
+ async execute(rawArgs) {
32
+ try {
33
+ const args = ExtractReadableTextInputSchema.parse(rawArgs);
34
+ const text = stripTags(args.content);
35
+ const sliced = text.slice(0, args.maxChars);
36
+ return {
37
+ success: true,
38
+ output: {
39
+ text: sliced,
40
+ truncated: text.length > sliced.length,
41
+ totalChars: text.length,
42
+ returnedChars: sliced.length,
43
+ },
44
+ };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ success: false,
49
+ error: error instanceof Error ? error.message : String(error),
50
+ };
51
+ }
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ const HttpFetchInputSchema = z.object({
3
+ url: z.string().url(),
4
+ });
5
+ export function createHttpFetchTool(options = {}) {
6
+ const timeoutMs = options.timeoutMs ?? 15000;
7
+ const userAgent = options.userAgent ??
8
+ "Mozilla/5.0 (compatible; XSimpleHttpFetch/1.0)";
9
+ return {
10
+ name: "http_fetch",
11
+ description: "Fetch a URL and return status, headers, final URL, and response text",
12
+ riskLevel: "safe",
13
+ inputSchema: HttpFetchInputSchema,
14
+ async execute(rawArgs) {
15
+ const controller = new AbortController();
16
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
17
+ try {
18
+ const args = HttpFetchInputSchema.parse(rawArgs);
19
+ const response = await fetch(args.url, {
20
+ method: "GET",
21
+ redirect: "follow",
22
+ headers: {
23
+ "User-Agent": userAgent,
24
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7",
25
+ },
26
+ signal: controller.signal,
27
+ });
28
+ const text = await response.text();
29
+ const headers = Object.fromEntries(response.headers.entries());
30
+ return {
31
+ success: true,
32
+ output: {
33
+ url: args.url,
34
+ finalUrl: response.url,
35
+ ok: response.ok,
36
+ status: response.status,
37
+ statusText: response.statusText,
38
+ headers,
39
+ contentType: response.headers.get("content-type") ?? "",
40
+ text,
41
+ },
42
+ };
43
+ }
44
+ catch (error) {
45
+ return {
46
+ success: false,
47
+ error: error instanceof Error ? error.message : String(error),
48
+ };
49
+ }
50
+ finally {
51
+ clearTimeout(timer);
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,4 @@
1
+ export { createHttpFetchTool } from "./http-fetch.js";
2
+ export { createWebSearchTool } from "./web-search.js";
3
+ export { createExtractReadableTextTool } from "./extract-readable-text.js";
4
+ export { createBraveSearchProvider } from "./search-providers/brave.js";