@vietor/easy-agent 0.3.1 → 0.4.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.
- package/README.md +31 -22
- package/dist/cli.js +0 -0
- package/dist/cmds/builtin.js +8 -0
- package/dist/config.js +1 -1
- package/dist/main.js +14 -40
- package/dist/tui/App.js +25 -12
- package/dist/tui/AppHeader.js +5 -4
- package/dist/tui/LogList.js +9 -0
- package/dist/tui/PromptOrCommandInput.js +1 -4
- package/dist/tui/TodoView.js +19 -0
- package/package.json +18 -21
- package/dist/cmds/registry.js +0 -30
- package/dist/cmds/types.js +0 -1
- package/dist/core/agent.js +0 -134
- package/dist/core/conversation.js +0 -82
- package/dist/core/logstore.js +0 -49
- package/dist/core/session.js +0 -181
- package/dist/llm/client.js +0 -80
- package/dist/llm/types.js +0 -1
- package/dist/mcp/client.js +0 -81
- package/dist/mcp/server.js +0 -124
- package/dist/skills/loader.js +0 -43
- package/dist/skills/types.js +0 -1
- package/dist/tools/ask_user.js +0 -24
- package/dist/tools/file_edit.js +0 -47
- package/dist/tools/file_read.js +0 -43
- package/dist/tools/file_write.js +0 -22
- package/dist/tools/glob.js +0 -29
- package/dist/tools/grep.js +0 -73
- package/dist/tools/registry.js +0 -52
- package/dist/tools/shell.js +0 -34
- package/dist/tools/types.js +0 -1
- package/dist/tools/web_fetch.js +0 -141
- package/dist/util/async.js +0 -58
- package/dist/util/fs.js +0 -17
- package/dist/util/package.js +0 -26
- package/dist/util/process.js +0 -43
- package/dist/util/ripgrep.js +0 -17
package/dist/tools/file_edit.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
const DESCRIPTION = [
|
|
3
|
-
"Replace occurrences of old_string with new_string in a file.",
|
|
4
|
-
"old_string must match exactly (including whitespace and indentation); read the file first and include enough surrounding context to be unique.",
|
|
5
|
-
"By default old_string must appear exactly once; set replace_all to true to replace every occurrence.",
|
|
6
|
-
"When copying old_string from FileRead output, strip the leading line-number prefix (digits and tab) before matching.",
|
|
7
|
-
"For full rewrites prefer FileWrite.",
|
|
8
|
-
].join(" ");
|
|
9
|
-
export const fileEditTool = {
|
|
10
|
-
name: "FileEdit",
|
|
11
|
-
description: DESCRIPTION,
|
|
12
|
-
parameters: {
|
|
13
|
-
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
path: { type: "string" },
|
|
16
|
-
old_string: { type: "string" },
|
|
17
|
-
new_string: { type: "string" },
|
|
18
|
-
replace_all: { type: "boolean", description: "replace all occurrences (default false)" },
|
|
19
|
-
},
|
|
20
|
-
required: ["path", "old_string", "new_string"],
|
|
21
|
-
},
|
|
22
|
-
async execute(args) {
|
|
23
|
-
const path = args.path;
|
|
24
|
-
const oldStr = args.old_string;
|
|
25
|
-
const newStr = args.new_string;
|
|
26
|
-
const all = args.replace_all === true;
|
|
27
|
-
if (!path)
|
|
28
|
-
throw new Error("path is required");
|
|
29
|
-
if (!oldStr)
|
|
30
|
-
throw new Error("old_string is required");
|
|
31
|
-
if (newStr === undefined)
|
|
32
|
-
throw new Error("new_string is required");
|
|
33
|
-
const content = await readFile(path, "utf-8");
|
|
34
|
-
if (!content.includes(oldStr))
|
|
35
|
-
throw new Error(`old_string not found in ${path}`);
|
|
36
|
-
if (all) {
|
|
37
|
-
await writeFile(path, content.split(oldStr).join(newStr), "utf-8");
|
|
38
|
-
return `Edited ${path} (replaced all)`;
|
|
39
|
-
}
|
|
40
|
-
const count = content.split(oldStr).length - 1;
|
|
41
|
-
if (count > 1)
|
|
42
|
-
throw new Error(`old_string appears ${count} times in ${path}, must be unique (or set replace_all)`);
|
|
43
|
-
await writeFile(path, content.replace(oldStr, newStr), "utf-8");
|
|
44
|
-
return `Edited ${path}`;
|
|
45
|
-
},
|
|
46
|
-
summaryArg: "path",
|
|
47
|
-
};
|
package/dist/tools/file_read.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
const DEFAULT_LIMIT = 2000;
|
|
3
|
-
const DESCRIPTION = [
|
|
4
|
-
"Read a file's contents as UTF-8 text, returned with line numbers (cat -n format).",
|
|
5
|
-
"path may be relative (to the working directory) or absolute.",
|
|
6
|
-
"Reads up to 2000 lines by default; use offset and limit to page through larger files.",
|
|
7
|
-
].join(" ");
|
|
8
|
-
export const fileReadTool = {
|
|
9
|
-
name: "FileRead",
|
|
10
|
-
description: DESCRIPTION,
|
|
11
|
-
parameters: {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: {
|
|
14
|
-
path: { type: "string" },
|
|
15
|
-
offset: { type: "number", description: "line number to start reading from (1-indexed)" },
|
|
16
|
-
limit: { type: "number", description: "number of lines to read (default 2000)" },
|
|
17
|
-
},
|
|
18
|
-
required: ["path"],
|
|
19
|
-
},
|
|
20
|
-
async execute(args) {
|
|
21
|
-
const path = args.path;
|
|
22
|
-
const offset = args.offset || 1;
|
|
23
|
-
const limit = args.limit || DEFAULT_LIMIT;
|
|
24
|
-
const content = await readFile(path, "utf-8");
|
|
25
|
-
if (!content)
|
|
26
|
-
return "(empty file)";
|
|
27
|
-
const lines = content.split("\n");
|
|
28
|
-
const start = Math.max(0, offset - 1);
|
|
29
|
-
if (start >= lines.length) {
|
|
30
|
-
return `(offset ${offset} is past end of file; file has ${lines.length} lines)`;
|
|
31
|
-
}
|
|
32
|
-
const end = Math.min(lines.length, start + limit);
|
|
33
|
-
const slice = lines.slice(start, end);
|
|
34
|
-
let out = slice
|
|
35
|
-
.map((line, i) => `${String(start + i + 1).padStart(6, " ")}\t${line}`)
|
|
36
|
-
.join("\n");
|
|
37
|
-
if (end < lines.length) {
|
|
38
|
-
out += `\n(${lines.length - end} more lines; use offset=${end + 1} to continue)`;
|
|
39
|
-
}
|
|
40
|
-
return out;
|
|
41
|
-
},
|
|
42
|
-
summaryArg: "path",
|
|
43
|
-
};
|
package/dist/tools/file_write.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
const DESCRIPTION = [
|
|
4
|
-
"Write content to a file, overwriting it entirely if it exists and creating parent directories as needed.",
|
|
5
|
-
"Use for new files or full rewrites; for targeted changes prefer FileEdit.",
|
|
6
|
-
].join(" ");
|
|
7
|
-
export const fileWriteTool = {
|
|
8
|
-
name: "FileWrite",
|
|
9
|
-
description: DESCRIPTION,
|
|
10
|
-
parameters: {
|
|
11
|
-
type: "object",
|
|
12
|
-
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
13
|
-
required: ["path", "content"],
|
|
14
|
-
},
|
|
15
|
-
async execute(args) {
|
|
16
|
-
const path = args.path;
|
|
17
|
-
await mkdir(dirname(path), { recursive: true });
|
|
18
|
-
await writeFile(path, args.content, "utf-8");
|
|
19
|
-
return `Wrote ${path}`;
|
|
20
|
-
},
|
|
21
|
-
summaryArg: "path",
|
|
22
|
-
};
|
package/dist/tools/glob.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { resolveCwd, runRgLines } from "../util/ripgrep.js";
|
|
2
|
-
const DESCRIPTION = [
|
|
3
|
-
"List files under a directory, optionally filtered by a glob pattern (e.g. **/*.ts); omit pattern to list every file.",
|
|
4
|
-
"Includes hidden files (e.g. .env, .gitignore); skips node_modules and .git.",
|
|
5
|
-
"Returns paths relative to the root, one per line.",
|
|
6
|
-
].join(" ");
|
|
7
|
-
export const globTool = {
|
|
8
|
-
name: "Glob",
|
|
9
|
-
description: DESCRIPTION,
|
|
10
|
-
parameters: {
|
|
11
|
-
type: "object",
|
|
12
|
-
properties: {
|
|
13
|
-
pattern: { type: "string", description: "glob pattern; omit to list all files" },
|
|
14
|
-
path: { type: "string", description: "root directory, defaults to cwd" },
|
|
15
|
-
},
|
|
16
|
-
required: [],
|
|
17
|
-
},
|
|
18
|
-
async execute(args, ctx) {
|
|
19
|
-
const cwd = resolveCwd(args.path);
|
|
20
|
-
const rgArgs = ["--files"];
|
|
21
|
-
const pattern = args.pattern;
|
|
22
|
-
if (pattern)
|
|
23
|
-
rgArgs.push("-g", pattern);
|
|
24
|
-
rgArgs.push(".");
|
|
25
|
-
const files = await runRgLines(rgArgs, cwd, ctx.signal);
|
|
26
|
-
return files.length ? files.join("\n") : "(no matches)";
|
|
27
|
-
},
|
|
28
|
-
summaryArg: ["pattern", "path"],
|
|
29
|
-
};
|
package/dist/tools/grep.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { resolveCwd, runRgLines } from "../util/ripgrep.js";
|
|
2
|
-
const DEFAULT_HEAD_LIMIT = 200;
|
|
3
|
-
const DESCRIPTION = [
|
|
4
|
-
"Search file contents under a directory recursively for a regex pattern (RE2 syntax).",
|
|
5
|
-
"Includes hidden files (e.g. .env, .gitignore); skips node_modules and .git.",
|
|
6
|
-
"By default returns matching lines as path:line:content, capped at 200 lines; use output_mode, head_limit, and context options to control output.",
|
|
7
|
-
].join(" ");
|
|
8
|
-
export const grepTool = {
|
|
9
|
-
name: "Grep",
|
|
10
|
-
description: DESCRIPTION,
|
|
11
|
-
parameters: {
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: {
|
|
14
|
-
pattern: { type: "string" },
|
|
15
|
-
path: { type: "string", description: "root directory, defaults to cwd" },
|
|
16
|
-
glob: { type: "string", description: "glob pattern to filter files (e.g. *.ts)" },
|
|
17
|
-
type: { type: "string", description: "file type to search (e.g. ts, js, py, rust, go)" },
|
|
18
|
-
output_mode: {
|
|
19
|
-
type: "string",
|
|
20
|
-
enum: ["content", "files_with_matches", "count"],
|
|
21
|
-
description: "content (default, matching lines), files_with_matches (file paths only), count (match counts per file)",
|
|
22
|
-
},
|
|
23
|
-
ignore_case: { type: "boolean", description: "case-insensitive match" },
|
|
24
|
-
before: { type: "number", description: "lines to show before each match" },
|
|
25
|
-
after: { type: "number", description: "lines to show after each match" },
|
|
26
|
-
context: { type: "number", description: "lines to show before and after each match" },
|
|
27
|
-
only_matching: { type: "boolean", description: "print only the matched (non-empty) parts" },
|
|
28
|
-
multiline: { type: "boolean", description: "allow patterns to span newlines" },
|
|
29
|
-
head_limit: { type: "number", description: "max output lines (default 200)" },
|
|
30
|
-
},
|
|
31
|
-
required: ["pattern"],
|
|
32
|
-
},
|
|
33
|
-
async execute(args, ctx) {
|
|
34
|
-
const cwd = resolveCwd(args.path);
|
|
35
|
-
const rgArgs = ["--line-number", "--with-filename", "--no-heading"];
|
|
36
|
-
if (args.ignore_case)
|
|
37
|
-
rgArgs.push("-i");
|
|
38
|
-
if (args.only_matching)
|
|
39
|
-
rgArgs.push("-o");
|
|
40
|
-
if (args.multiline)
|
|
41
|
-
rgArgs.push("-U", "--multiline-dotall");
|
|
42
|
-
const context = args.context;
|
|
43
|
-
if (context)
|
|
44
|
-
rgArgs.push("-C", String(context));
|
|
45
|
-
else {
|
|
46
|
-
const before = args.before;
|
|
47
|
-
const after = args.after;
|
|
48
|
-
if (before)
|
|
49
|
-
rgArgs.push("-B", String(before));
|
|
50
|
-
if (after)
|
|
51
|
-
rgArgs.push("-A", String(after));
|
|
52
|
-
}
|
|
53
|
-
if (args.glob)
|
|
54
|
-
rgArgs.push("-g", args.glob);
|
|
55
|
-
if (args.type)
|
|
56
|
-
rgArgs.push("-t", args.type);
|
|
57
|
-
const output_mode = args.output_mode || "content";
|
|
58
|
-
if (output_mode === "files_with_matches")
|
|
59
|
-
rgArgs.push("-l");
|
|
60
|
-
else if (output_mode === "count")
|
|
61
|
-
rgArgs.push("-c");
|
|
62
|
-
rgArgs.push(args.pattern, ".");
|
|
63
|
-
const lines = await runRgLines(rgArgs, cwd, ctx.signal);
|
|
64
|
-
if (!lines.length)
|
|
65
|
-
return "(no matches)";
|
|
66
|
-
const headLimit = args.head_limit || DEFAULT_HEAD_LIMIT;
|
|
67
|
-
if (lines.length > headLimit) {
|
|
68
|
-
return lines.slice(0, headLimit).join("\n") + `\n(${lines.length - headLimit} more matches, truncated)`;
|
|
69
|
-
}
|
|
70
|
-
return lines.join("\n");
|
|
71
|
-
},
|
|
72
|
-
summaryArg: ["pattern", "path"],
|
|
73
|
-
};
|
package/dist/tools/registry.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { shellTool } from "./shell.js";
|
|
2
|
-
import { fileReadTool } from "./file_read.js";
|
|
3
|
-
import { fileWriteTool } from "./file_write.js";
|
|
4
|
-
import { fileEditTool } from "./file_edit.js";
|
|
5
|
-
import { globTool } from "./glob.js";
|
|
6
|
-
import { grepTool } from "./grep.js";
|
|
7
|
-
import { webFetchTool } from "./web_fetch.js";
|
|
8
|
-
import { askUserTool } from "./ask_user.js";
|
|
9
|
-
export class ToolRegistry {
|
|
10
|
-
tools = new Map();
|
|
11
|
-
register(tool) {
|
|
12
|
-
this.tools.set(tool.name, tool);
|
|
13
|
-
}
|
|
14
|
-
schemas() {
|
|
15
|
-
return [...this.tools.values()].map((t) => ({
|
|
16
|
-
type: "function",
|
|
17
|
-
function: {
|
|
18
|
-
name: t.name,
|
|
19
|
-
description: t.description,
|
|
20
|
-
parameters: t.parameters,
|
|
21
|
-
},
|
|
22
|
-
}));
|
|
23
|
-
}
|
|
24
|
-
async execute(name, args, ctx) {
|
|
25
|
-
const tool = this.tools.get(name);
|
|
26
|
-
if (!tool)
|
|
27
|
-
return { content: `Error: unknown tool ${name}`, isError: true };
|
|
28
|
-
try {
|
|
29
|
-
const r = await tool.execute(args, ctx);
|
|
30
|
-
return typeof r === "string" ? { content: r } : r;
|
|
31
|
-
}
|
|
32
|
-
catch (e) {
|
|
33
|
-
return { content: `Error: ${e.message}`, isError: true };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
summarize(name, args) {
|
|
37
|
-
const tool = this.tools.get(name);
|
|
38
|
-
if (!tool?.summaryArg)
|
|
39
|
-
return "";
|
|
40
|
-
const keys = Array.isArray(tool.summaryArg) ? tool.summaryArg : [tool.summaryArg];
|
|
41
|
-
for (const k of keys) {
|
|
42
|
-
const v = args[k];
|
|
43
|
-
if (typeof v === "string" && v)
|
|
44
|
-
return v;
|
|
45
|
-
}
|
|
46
|
-
return "";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export function registerBuiltinTools(tools) {
|
|
50
|
-
for (const t of [shellTool, fileReadTool, fileWriteTool, fileEditTool, globTool, grepTool, webFetchTool, askUserTool])
|
|
51
|
-
tools.register(t);
|
|
52
|
-
}
|
package/dist/tools/shell.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { runProcess } from "../util/process.js";
|
|
2
|
-
const isWindows = process.platform === "win32";
|
|
3
|
-
const shell = isWindows ? "powershell.exe" : "/bin/sh";
|
|
4
|
-
const shellArgs = isWindows ? ["-NoProfile", "-NonInteractive", "-Command"] : ["-c"];
|
|
5
|
-
const commandPrefix = isWindows
|
|
6
|
-
? "[Console]::OutputEncoding=[Text.Encoding]::UTF8; $OutputEncoding=[Text.Encoding]::UTF8; "
|
|
7
|
-
: "";
|
|
8
|
-
const DESCRIPTION = [
|
|
9
|
-
"Execute a shell command and return combined stdout and stderr.",
|
|
10
|
-
isWindows
|
|
11
|
-
? "Runs on Windows PowerShell 5.1 (powershell.exe), NOT pwsh (PowerShell 7+). Chain commands with semicolons; conditional command chaining, null-coalescing, and ternary operators are pwsh-only and unsupported here."
|
|
12
|
-
: `Runs on ${shell} (POSIX sh).`,
|
|
13
|
-
"Runs synchronously with no stdin, so interactive prompts cannot be answered.",
|
|
14
|
-
"Output is capped at ~10MB; for large files prefer Grep or FileRead.",
|
|
15
|
-
"For URL content prefer WebFetch; use Shell for web requests only when WebFetch cannot (non-GET, custom headers, auth, raw bytes, or status codes).",
|
|
16
|
-
].join(" ");
|
|
17
|
-
export const shellTool = {
|
|
18
|
-
name: "Shell",
|
|
19
|
-
description: DESCRIPTION,
|
|
20
|
-
parameters: {
|
|
21
|
-
type: "object",
|
|
22
|
-
properties: { command: { type: "string" } },
|
|
23
|
-
required: ["command"],
|
|
24
|
-
},
|
|
25
|
-
async execute(args, ctx) {
|
|
26
|
-
const command = args.command;
|
|
27
|
-
const r = await runProcess(shell, [...shellArgs, commandPrefix + command], {}, ctx.signal);
|
|
28
|
-
if (r.status === 0 && !r.error) {
|
|
29
|
-
return r.stdout || "(no output)";
|
|
30
|
-
}
|
|
31
|
-
return (r.stdout || "") + (r.stderr || "") + (r.error?.message || "");
|
|
32
|
-
},
|
|
33
|
-
summaryArg: "command",
|
|
34
|
-
};
|
package/dist/tools/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/tools/web_fetch.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { Parser } from "htmlparser2";
|
|
2
|
-
import TurndownService from "turndown";
|
|
3
|
-
const SKIP_TAGS = new Set(["script", "style", "noscript", "template", "head", "title", "meta", "link", "base"]);
|
|
4
|
-
const BLOCK_TAGS = new Set([
|
|
5
|
-
"p",
|
|
6
|
-
"div",
|
|
7
|
-
"ul",
|
|
8
|
-
"ol",
|
|
9
|
-
"li",
|
|
10
|
-
"br",
|
|
11
|
-
"tr",
|
|
12
|
-
"table",
|
|
13
|
-
"blockquote",
|
|
14
|
-
"pre",
|
|
15
|
-
"section",
|
|
16
|
-
"article",
|
|
17
|
-
"header",
|
|
18
|
-
"footer",
|
|
19
|
-
"nav",
|
|
20
|
-
"aside",
|
|
21
|
-
"h1",
|
|
22
|
-
"h2",
|
|
23
|
-
"h3",
|
|
24
|
-
"h4",
|
|
25
|
-
"h5",
|
|
26
|
-
"h6",
|
|
27
|
-
"hr",
|
|
28
|
-
]);
|
|
29
|
-
function normalize(s) {
|
|
30
|
-
return s
|
|
31
|
-
.replace(/\r\n/g, "\n")
|
|
32
|
-
.replace(/\u00a0/g, " ")
|
|
33
|
-
.replace(/[ \t]+\n/g, "\n")
|
|
34
|
-
.split("\n")
|
|
35
|
-
.map((l) => l.replace(/[ \t]{2,}/g, " ").replace(/\s+$/, ""))
|
|
36
|
-
.join("\n")
|
|
37
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
38
|
-
.trim();
|
|
39
|
-
}
|
|
40
|
-
function htmlToText(html) {
|
|
41
|
-
let out = "";
|
|
42
|
-
let skip = 0;
|
|
43
|
-
const parser = new Parser({
|
|
44
|
-
onopentag(name) {
|
|
45
|
-
if (SKIP_TAGS.has(name))
|
|
46
|
-
skip++;
|
|
47
|
-
else if (name === "li")
|
|
48
|
-
out += "\n- ";
|
|
49
|
-
else if (BLOCK_TAGS.has(name))
|
|
50
|
-
out += "\n";
|
|
51
|
-
},
|
|
52
|
-
onclosetag(name) {
|
|
53
|
-
if (SKIP_TAGS.has(name) && skip > 0)
|
|
54
|
-
skip--;
|
|
55
|
-
},
|
|
56
|
-
ontext(text) {
|
|
57
|
-
if (skip === 0)
|
|
58
|
-
out += text;
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
parser.write(html);
|
|
62
|
-
parser.end();
|
|
63
|
-
return normalize(out);
|
|
64
|
-
}
|
|
65
|
-
const turndown = new TurndownService({
|
|
66
|
-
headingStyle: "atx",
|
|
67
|
-
hr: "---",
|
|
68
|
-
bulletListMarker: "-",
|
|
69
|
-
codeBlockStyle: "fenced",
|
|
70
|
-
emDelimiter: "*",
|
|
71
|
-
strongDelimiter: "**",
|
|
72
|
-
linkStyle: "inlined",
|
|
73
|
-
});
|
|
74
|
-
turndown.remove(["script", "style", "title", "meta", "head", "noscript", "template", "link", "base"]);
|
|
75
|
-
function htmlToMarkdown(html) {
|
|
76
|
-
return turndown.turndown(html);
|
|
77
|
-
}
|
|
78
|
-
function mimeFrom(contentType) {
|
|
79
|
-
return contentType.split(";", 1)[0].trim().toLowerCase();
|
|
80
|
-
}
|
|
81
|
-
function isTextualMime(mime) {
|
|
82
|
-
return (!mime ||
|
|
83
|
-
mime.startsWith("text/") ||
|
|
84
|
-
mime === "application/json" ||
|
|
85
|
-
mime.endsWith("+json") ||
|
|
86
|
-
mime === "application/xml" ||
|
|
87
|
-
mime.endsWith("+xml") ||
|
|
88
|
-
mime === "application/javascript" ||
|
|
89
|
-
mime === "application/x-javascript");
|
|
90
|
-
}
|
|
91
|
-
const DESCRIPTION = [
|
|
92
|
-
"Fetch a URL via GET and return its content as markdown (default) or plain text.",
|
|
93
|
-
"Follows redirects.",
|
|
94
|
-
"HTML is converted; other textual types (JSON, XML, plain text) are returned raw; non-textual content (images, binaries) is rejected.",
|
|
95
|
-
].join(" ");
|
|
96
|
-
export const webFetchTool = {
|
|
97
|
-
name: "WebFetch",
|
|
98
|
-
description: DESCRIPTION,
|
|
99
|
-
parameters: {
|
|
100
|
-
type: "object",
|
|
101
|
-
properties: {
|
|
102
|
-
url: {
|
|
103
|
-
type: "string",
|
|
104
|
-
description: "full URL including scheme (http or https)",
|
|
105
|
-
},
|
|
106
|
-
format: {
|
|
107
|
-
type: "string",
|
|
108
|
-
description: "output format: 'markdown' (default) or 'text'",
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
required: ["url"],
|
|
112
|
-
},
|
|
113
|
-
async execute(args, ctx) {
|
|
114
|
-
const url = args.url;
|
|
115
|
-
const format = (args.format || "markdown").toLowerCase();
|
|
116
|
-
let res;
|
|
117
|
-
try {
|
|
118
|
-
res = await fetch(url, {
|
|
119
|
-
headers: {
|
|
120
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
|
121
|
-
},
|
|
122
|
-
redirect: "follow",
|
|
123
|
-
signal: ctx.signal,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
catch (e) {
|
|
127
|
-
throw new Error(`failed to fetch ${url}: ${e.message}`);
|
|
128
|
-
}
|
|
129
|
-
if (!res.ok)
|
|
130
|
-
throw new Error(`${res.status} ${res.statusText} for ${url}`);
|
|
131
|
-
const body = await res.text();
|
|
132
|
-
const contentType = res.headers.get("content-type") || "";
|
|
133
|
-
const mime = mimeFrom(contentType);
|
|
134
|
-
if (!isTextualMime(mime))
|
|
135
|
-
throw new Error(`unsupported content type: ${mime} for ${url}`);
|
|
136
|
-
if (!contentType.includes("html"))
|
|
137
|
-
return body;
|
|
138
|
-
return format === "text" ? htmlToText(body) : htmlToMarkdown(body);
|
|
139
|
-
},
|
|
140
|
-
summaryArg: "url",
|
|
141
|
-
};
|
package/dist/util/async.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export async function withRetry(fn, opts) {
|
|
2
|
-
for (let attempt = 0;; attempt++) {
|
|
3
|
-
try {
|
|
4
|
-
return await fn();
|
|
5
|
-
}
|
|
6
|
-
catch (e) {
|
|
7
|
-
if (attempt < opts.retries && opts.retryable(e)) {
|
|
8
|
-
opts.onRetry?.(attempt + 1, opts.retries);
|
|
9
|
-
await trySleep(opts.backoff(attempt), opts.signal);
|
|
10
|
-
continue;
|
|
11
|
-
}
|
|
12
|
-
throw e;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function trySleep(ms, signal) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
if (signal?.aborted) {
|
|
19
|
-
reject(new Error("aborted"));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const onAbort = () => {
|
|
23
|
-
clearTimeout(timer);
|
|
24
|
-
reject(new Error("aborted"));
|
|
25
|
-
};
|
|
26
|
-
const timer = setTimeout(() => {
|
|
27
|
-
signal?.removeEventListener("abort", onAbort);
|
|
28
|
-
resolve();
|
|
29
|
-
}, ms);
|
|
30
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
export async function withAbort(fn, opts) {
|
|
34
|
-
const onAbort = () => opts.onAbort?.();
|
|
35
|
-
if (opts.signal?.aborted)
|
|
36
|
-
onAbort();
|
|
37
|
-
else
|
|
38
|
-
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
39
|
-
try {
|
|
40
|
-
return await fn(() => !!opts.signal?.aborted);
|
|
41
|
-
}
|
|
42
|
-
finally {
|
|
43
|
-
opts.signal?.removeEventListener("abort", onAbort);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export function withTimeout(p, ms) {
|
|
47
|
-
let timer;
|
|
48
|
-
const timed = new Promise((_, reject) => {
|
|
49
|
-
timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
|
|
50
|
-
});
|
|
51
|
-
return Promise.race([
|
|
52
|
-
p.finally(() => {
|
|
53
|
-
if (timer)
|
|
54
|
-
clearTimeout(timer);
|
|
55
|
-
}),
|
|
56
|
-
timed,
|
|
57
|
-
]);
|
|
58
|
-
}
|
package/dist/util/fs.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
export function tryReadFileText(path) {
|
|
3
|
-
if (existsSync(path)) {
|
|
4
|
-
const content = readFileSync(path, "utf-8").trim();
|
|
5
|
-
if (content)
|
|
6
|
-
return content;
|
|
7
|
-
}
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
export function readFirstFileContent(paths, fn) {
|
|
11
|
-
for (const p of paths) {
|
|
12
|
-
const content = fn(p);
|
|
13
|
-
if (content)
|
|
14
|
-
return content;
|
|
15
|
-
}
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
package/dist/util/package.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
const __dirname = import.meta.dirname;
|
|
4
|
-
const MAX_PARENT_TRAVERSAL = 10;
|
|
5
|
-
let _pkg = null;
|
|
6
|
-
function findPackageJson() {
|
|
7
|
-
let current = __dirname;
|
|
8
|
-
for (let i = 0; i < MAX_PARENT_TRAVERSAL; i++) {
|
|
9
|
-
const pkgPath = join(current, "package.json");
|
|
10
|
-
if (existsSync(pkgPath)) {
|
|
11
|
-
return pkgPath;
|
|
12
|
-
}
|
|
13
|
-
const parent = dirname(current);
|
|
14
|
-
if (parent === current)
|
|
15
|
-
break;
|
|
16
|
-
current = parent;
|
|
17
|
-
}
|
|
18
|
-
throw new Error("Cannot find package.json");
|
|
19
|
-
}
|
|
20
|
-
export function getPackageInfo() {
|
|
21
|
-
if (_pkg === null) {
|
|
22
|
-
const pkgPath = findPackageJson();
|
|
23
|
-
_pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
24
|
-
}
|
|
25
|
-
return _pkg;
|
|
26
|
-
}
|
package/dist/util/process.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
3
|
-
export function runProcess(cmd, args, opts = {}, signal) {
|
|
4
|
-
return new Promise((resolve) => {
|
|
5
|
-
const child = spawn(cmd, args, {
|
|
6
|
-
cwd: opts.cwd,
|
|
7
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8
|
-
});
|
|
9
|
-
const onAbort = () => child.kill();
|
|
10
|
-
if (signal) {
|
|
11
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
12
|
-
if (signal.aborted)
|
|
13
|
-
onAbort();
|
|
14
|
-
}
|
|
15
|
-
const outChunks = [];
|
|
16
|
-
const errChunks = [];
|
|
17
|
-
let size = 0;
|
|
18
|
-
let overflow = false;
|
|
19
|
-
child.stdout?.on("data", (c) => {
|
|
20
|
-
outChunks.push(c);
|
|
21
|
-
size += c.length;
|
|
22
|
-
if (size > MAX_BUFFER) {
|
|
23
|
-
overflow = true;
|
|
24
|
-
child.kill();
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
child.stderr?.on("data", (c) => {
|
|
28
|
-
errChunks.push(c);
|
|
29
|
-
});
|
|
30
|
-
child.on("error", (error) => {
|
|
31
|
-
signal?.removeEventListener("abort", onAbort);
|
|
32
|
-
resolve({ stdout: "", stderr: "", status: null, error });
|
|
33
|
-
});
|
|
34
|
-
child.on("close", (status) => {
|
|
35
|
-
signal?.removeEventListener("abort", onAbort);
|
|
36
|
-
const stdout = Buffer.concat(outChunks).toString("utf-8");
|
|
37
|
-
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
38
|
-
resolve(overflow
|
|
39
|
-
? { stdout, stderr, status, error: new Error("output exceeded maxBuffer") }
|
|
40
|
-
: { stdout, stderr, status });
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
package/dist/util/ripgrep.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, join } from "node:path";
|
|
2
|
-
import { rgPath } from "@vscode/ripgrep";
|
|
3
|
-
import { runProcess } from "./process.js";
|
|
4
|
-
export function resolveCwd(path) {
|
|
5
|
-
const root = path || ".";
|
|
6
|
-
return isAbsolute(root) ? root : join(process.cwd(), root);
|
|
7
|
-
}
|
|
8
|
-
export async function runRgLines(args, cwd, signal) {
|
|
9
|
-
const rgArgs = ["--hidden", "--path-separator", "/", "-g", "!.git/**", "-g", "!node_modules/**", ...args];
|
|
10
|
-
const r = await runProcess(rgPath, rgArgs, { cwd }, signal);
|
|
11
|
-
if (r.error)
|
|
12
|
-
throw r.error;
|
|
13
|
-
if (r.status !== 0 && r.status !== 1) {
|
|
14
|
-
throw new Error((r.stderr || "").trim() || `ripgrep exited with ${r.status}`);
|
|
15
|
-
}
|
|
16
|
-
return r.stdout.split("\n").filter(Boolean).map((f) => f.replace(/^\.\//, ""));
|
|
17
|
-
}
|