@vietor/easy-agent 0.2.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.
@@ -1,18 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- const DESCRIPTION = [
3
- "Read a file's full contents as UTF-8 text.",
4
- "path may be relative (to the working directory) or absolute.",
5
- ].join(" ");
6
- export const fileReadTool = {
7
- name: "FileRead",
8
- description: DESCRIPTION,
9
- parameters: {
10
- type: "object",
11
- properties: { path: { type: "string" } },
12
- required: ["path"],
13
- },
14
- async execute(args) {
15
- return readFile(args.path, "utf-8");
16
- },
17
- summaryArg: "path",
18
- };
@@ -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
- };
@@ -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
- "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) {
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);
26
- return files.length ? files.join("\n") : "(no matches)";
27
- },
28
- summaryArg: ["pattern", "path"],
29
- };
@@ -1,36 +0,0 @@
1
- import { resolveCwd, runRgLines } from "../util/ripgrep.js";
2
- const MAX_MATCHES = 200;
3
- const DESCRIPTION = [
4
- "Search file contents under a directory recursively for a regex pattern (RE2 syntax).",
5
- "Skips node_modules and .git.",
6
- "Returns matching lines as path:line: content, capped at 200 matches.",
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
- },
17
- required: ["pattern"],
18
- },
19
- async execute(args) {
20
- const cwd = resolveCwd(args.path);
21
- const rgArgs = [
22
- "--line-number",
23
- "--with-filename",
24
- "--no-heading",
25
- args.pattern,
26
- ".",
27
- ];
28
- const lines = await runRgLines(rgArgs, cwd);
29
- if (!lines.length)
30
- return "(no matches)";
31
- if (lines.length > MAX_MATCHES)
32
- return lines.slice(0, MAX_MATCHES).join("\n") + "\n(truncated)";
33
- return lines.join("\n");
34
- },
35
- summaryArg: ["pattern", "path"],
36
- };
@@ -1,51 +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
- export class ToolRegistry {
9
- tools = new Map();
10
- register(tool) {
11
- this.tools.set(tool.name, tool);
12
- }
13
- schemas() {
14
- return [...this.tools.values()].map((t) => ({
15
- type: "function",
16
- function: {
17
- name: t.name,
18
- description: t.description,
19
- parameters: t.parameters,
20
- },
21
- }));
22
- }
23
- async execute(name, args) {
24
- const tool = this.tools.get(name);
25
- if (!tool)
26
- return { content: `Error: unknown tool ${name}`, isError: true };
27
- try {
28
- const r = await tool.execute(args);
29
- return typeof r === "string" ? { content: r } : r;
30
- }
31
- catch (e) {
32
- return { content: `Error: ${e.message}`, isError: true };
33
- }
34
- }
35
- summarize(name, args) {
36
- const tool = this.tools.get(name);
37
- if (!tool?.summaryArg)
38
- return "";
39
- const keys = Array.isArray(tool.summaryArg) ? tool.summaryArg : [tool.summaryArg];
40
- for (const k of keys) {
41
- const v = args[k];
42
- if (typeof v === "string" && v)
43
- return v;
44
- }
45
- return "";
46
- }
47
- }
48
- export function registerBuiltinTools(tools) {
49
- for (const t of [shellTool, fileReadTool, fileWriteTool, fileEditTool, globTool, grepTool, webFetchTool])
50
- tools.register(t);
51
- }
@@ -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) {
26
- const command = args.command;
27
- const r = await runProcess(shell, [...shellArgs, commandPrefix + command]);
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
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,140 +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) {
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
- });
124
- }
125
- catch (e) {
126
- throw new Error(`failed to fetch ${url}: ${e.message}`);
127
- }
128
- if (!res.ok)
129
- throw new Error(`${res.status} ${res.statusText} for ${url}`);
130
- const body = await res.text();
131
- const contentType = res.headers.get("content-type") || "";
132
- const mime = mimeFrom(contentType);
133
- if (!isTextualMime(mime))
134
- throw new Error(`unsupported content type: ${mime} for ${url}`);
135
- if (!contentType.includes("html"))
136
- return body;
137
- return format === "text" ? htmlToText(body) : htmlToMarkdown(body);
138
- },
139
- summaryArg: "url",
140
- };
@@ -1,35 +0,0 @@
1
- export class LogStore {
2
- entries = [];
3
- listeners = new Set();
4
- getSnapshot = () => this.entries;
5
- subscribe = (listener) => {
6
- this.listeners.add(listener);
7
- return () => {
8
- this.listeners.delete(listener);
9
- };
10
- };
11
- append(entry) {
12
- this.entries = [...this.entries, entry];
13
- this.emit();
14
- }
15
- clear() {
16
- this.entries = [];
17
- this.emit();
18
- }
19
- setToolResult(id, result, isError) {
20
- for (let i = this.entries.length - 1; i >= 0; i--) {
21
- const entry = this.entries[i];
22
- if (entry.kind === "tool" && entry.id === id && entry.result === null) {
23
- const copy = [...this.entries];
24
- copy[i] = { ...entry, result, isError };
25
- this.entries = copy;
26
- this.emit();
27
- return;
28
- }
29
- }
30
- }
31
- emit() {
32
- for (const listener of this.listeners)
33
- listener();
34
- }
35
- }
@@ -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
- }
@@ -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
- }
@@ -1,33 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- const MAX_BUFFER = 10 * 1024 * 1024;
3
- export function runProcess(cmd, args, opts = {}) {
4
- return new Promise((resolve) => {
5
- const child = spawn(cmd, args, {
6
- cwd: opts.cwd,
7
- stdio: ["ignore", "pipe", "pipe"],
8
- });
9
- const outChunks = [];
10
- const errChunks = [];
11
- let size = 0;
12
- let overflow = false;
13
- child.stdout?.on("data", (c) => {
14
- outChunks.push(c);
15
- size += c.length;
16
- if (size > MAX_BUFFER) {
17
- overflow = true;
18
- child.kill();
19
- }
20
- });
21
- child.stderr?.on("data", (c) => {
22
- errChunks.push(c);
23
- });
24
- child.on("error", (error) => resolve({ stdout: "", stderr: "", status: null, error }));
25
- child.on("close", (status) => {
26
- const stdout = Buffer.concat(outChunks).toString("utf-8");
27
- const stderr = Buffer.concat(errChunks).toString("utf-8");
28
- resolve(overflow
29
- ? { stdout, stderr, status, error: new Error("output exceeded maxBuffer") }
30
- : { stdout, stderr, status });
31
- });
32
- });
33
- }
@@ -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) {
9
- const rgArgs = ["--hidden", "--path-separator", "/", "-g", "!.git/**", ...args];
10
- const r = await runProcess(rgPath, rgArgs, { cwd });
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
- }