@rong/agentscript 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 (77) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/INSTALL.md +92 -0
  3. package/LICENSE +21 -0
  4. package/README.md +246 -0
  5. package/dist/ast/constants.js +1 -0
  6. package/dist/ast/format.js +41 -0
  7. package/dist/ast/types.js +1 -0
  8. package/dist/bin/agentscript.js +234 -0
  9. package/dist/bin/input.js +19 -0
  10. package/dist/bin/repl.js +290 -0
  11. package/dist/index.js +26 -0
  12. package/dist/parser/errors.js +8 -0
  13. package/dist/parser/parser.js +661 -0
  14. package/dist/parser/tokenizer.js +246 -0
  15. package/dist/providers/llm/anthropic.js +36 -0
  16. package/dist/providers/llm/index.js +3 -0
  17. package/dist/providers/llm/ollama.js +19 -0
  18. package/dist/providers/llm/openai.js +31 -0
  19. package/dist/providers/llm/protocol.js +45 -0
  20. package/dist/providers/llm/shared.js +147 -0
  21. package/dist/providers/llm/types.js +1 -0
  22. package/dist/providers/llm/uri.js +24 -0
  23. package/dist/providers/memory/file.js +44 -0
  24. package/dist/providers/memory/host.js +66 -0
  25. package/dist/providers/memory/index.js +1 -0
  26. package/dist/providers/memory/shared.js +56 -0
  27. package/dist/providers/memory/sqlite.js +98 -0
  28. package/dist/providers/mock/index.js +32 -0
  29. package/dist/providers/tools/env.js +11 -0
  30. package/dist/providers/tools/file.js +99 -0
  31. package/dist/providers/tools/host.js +34 -0
  32. package/dist/providers/tools/http.js +40 -0
  33. package/dist/providers/tools/index.js +2 -0
  34. package/dist/providers/tools/scheme.js +16 -0
  35. package/dist/providers/tools/shared.js +92 -0
  36. package/dist/providers/tools/shell.js +80 -0
  37. package/dist/runtime/context.js +160 -0
  38. package/dist/runtime/errors.js +14 -0
  39. package/dist/runtime/evaluator.js +276 -0
  40. package/dist/runtime/generate.js +175 -0
  41. package/dist/runtime/guards.js +39 -0
  42. package/dist/runtime/input.js +38 -0
  43. package/dist/runtime/interpreter.js +314 -0
  44. package/dist/runtime/json.js +59 -0
  45. package/dist/runtime/loader.js +146 -0
  46. package/dist/runtime/scope.js +47 -0
  47. package/dist/runtime/shape.js +132 -0
  48. package/dist/runtime/trace.js +54 -0
  49. package/dist/runtime/truth.js +13 -0
  50. package/dist/runtime/types.js +1 -0
  51. package/dist/runtime/uri.js +10 -0
  52. package/dist/semantic/analyzer.js +519 -0
  53. package/dist/semantic/diagnostics.js +16 -0
  54. package/dist/utils/assert.js +3 -0
  55. package/docs/cn/context-engineering.md +389 -0
  56. package/docs/cn/language.md +478 -0
  57. package/docs/design-history/v0-design.md +365 -0
  58. package/docs/design-history/v0-implement.md +274 -0
  59. package/docs/design-history/v1-design.md +323 -0
  60. package/docs/design-history/v1-implement.md +267 -0
  61. package/docs/design-history/v2-design.md +387 -0
  62. package/docs/design-history/v2-implement.md +399 -0
  63. package/docs/en/context-engineering.md +332 -0
  64. package/docs/en/language.md +478 -0
  65. package/examples/changelog.as +29 -0
  66. package/examples/extract.as +29 -0
  67. package/examples/review.as +38 -0
  68. package/examples/summarize.as +28 -0
  69. package/examples/translate.as +33 -0
  70. package/package.json +59 -0
  71. package/tutorials/cli.as +22 -0
  72. package/tutorials/helloworld.as +14 -0
  73. package/tutorials/memory.as +19 -0
  74. package/tutorials/plan-execute.as +155 -0
  75. package/tutorials/react.as +98 -0
  76. package/tutorials/repl.as +31 -0
  77. package/tutorials/self-improve.as +60 -0
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { RuntimeError } from "../../runtime/errors.js";
3
+ import { isObject } from "../../runtime/guards.js";
4
+ import { runtimeValuesEqual, sanitizeForJson } from "../../runtime/json.js";
5
+ export function readLimit(value) {
6
+ if (value === undefined || value === null)
7
+ return 10;
8
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
9
+ throw new RuntimeError("memory.query limit must be a positive integer");
10
+ }
11
+ return value;
12
+ }
13
+ export function createMemoryEnvelope(record) {
14
+ const now = new Date().toISOString();
15
+ return {
16
+ id: randomUUID(),
17
+ created_at: now,
18
+ updated_at: now,
19
+ record,
20
+ };
21
+ }
22
+ export function matchesQuery(record, query) {
23
+ if (typeof query.kind === "string" && record.kind !== query.kind)
24
+ return false;
25
+ if (typeof query.text === "string" && !matchesText(record, query.text))
26
+ return false;
27
+ if (query.where !== undefined) {
28
+ if (!isObject(query.where)) {
29
+ throw new RuntimeError("memory.query where must be an object");
30
+ }
31
+ for (const [key, expected] of Object.entries(query.where)) {
32
+ if (!(key in record) || !runtimeValuesEqual(record[key], expected))
33
+ return false;
34
+ }
35
+ }
36
+ return true;
37
+ }
38
+ export function isMemoryEnvelope(value) {
39
+ if (typeof value !== "object" || value === null || Array.isArray(value))
40
+ return false;
41
+ const item = value;
42
+ return (typeof item.id === "string" &&
43
+ typeof item.created_at === "string" &&
44
+ typeof item.updated_at === "string" &&
45
+ typeof item.record === "object" &&
46
+ item.record !== null &&
47
+ !Array.isArray(item.record));
48
+ }
49
+ function matchesText(record, text) {
50
+ const needle = text.toLowerCase();
51
+ const candidates = [
52
+ typeof record.text === "string" ? record.text : "",
53
+ JSON.stringify(sanitizeForJson(record)),
54
+ ];
55
+ return candidates.some((candidate) => candidate.toLowerCase().includes(needle));
56
+ }
@@ -0,0 +1,98 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { RuntimeError } from "../../runtime/errors.js";
5
+ import { isObject } from "../../runtime/guards.js";
6
+ import { createMemoryEnvelope, matchesQuery, readLimit } from "./shared.js";
7
+ export class SqliteMemoryBackend {
8
+ add(request, target) {
9
+ mkdirSync(dirname(target.path), { recursive: true });
10
+ const db = openSqlite(target.path);
11
+ try {
12
+ const envelope = createMemoryEnvelope(request.record);
13
+ db.prepare(`
14
+ INSERT INTO memory_records (namespace, id, created_at, updated_at, kind, text, record_json)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?)
16
+ `).run(target.namespace, envelope.id, envelope.created_at, envelope.updated_at, typeof envelope.record.kind === "string" ? envelope.record.kind : null, typeof envelope.record.text === "string" ? envelope.record.text : null, JSON.stringify(envelope.record));
17
+ return envelope;
18
+ }
19
+ finally {
20
+ db.close();
21
+ }
22
+ }
23
+ query(request, target) {
24
+ const query = request.query;
25
+ const limit = readLimit(query.limit);
26
+ if (!existsSync(target.path))
27
+ return [];
28
+ const db = openSqlite(target.path);
29
+ try {
30
+ const conditions = ["namespace = ?"];
31
+ const params = [target.namespace];
32
+ if (typeof query.kind === "string") {
33
+ conditions.push("kind = ?");
34
+ params.push(query.kind);
35
+ }
36
+ if (typeof query.text === "string") {
37
+ conditions.push("(text LIKE ? ESCAPE '\\' OR record_json LIKE ? ESCAPE '\\')");
38
+ const pattern = `%${escapeLike(query.text)}%`;
39
+ params.push(pattern, pattern);
40
+ }
41
+ const needPostFilter = query.where !== undefined;
42
+ const sql = `
43
+ SELECT id, created_at, updated_at, record_json
44
+ FROM memory_records
45
+ WHERE ${conditions.join(" AND ")}
46
+ ORDER BY created_at DESC
47
+ ${needPostFilter ? "" : "LIMIT ?"}
48
+ `;
49
+ const sqlParams = needPostFilter ? params : [...params, limit];
50
+ const rows = db.prepare(sql).all(...sqlParams);
51
+ const records = rows
52
+ .map((row) => rowToEnvelope(row))
53
+ .filter((item) => matchesQuery(item.record, query));
54
+ return records.slice(0, limit);
55
+ }
56
+ finally {
57
+ db.close();
58
+ }
59
+ }
60
+ }
61
+ function openSqlite(path) {
62
+ const db = new DatabaseSync(path);
63
+ db.exec(`
64
+ CREATE TABLE IF NOT EXISTS memory_records (
65
+ namespace TEXT NOT NULL,
66
+ id TEXT NOT NULL,
67
+ created_at TEXT NOT NULL,
68
+ updated_at TEXT NOT NULL,
69
+ kind TEXT,
70
+ text TEXT,
71
+ record_json TEXT NOT NULL,
72
+ PRIMARY KEY (namespace, id)
73
+ )
74
+ `);
75
+ return db;
76
+ }
77
+ function rowToEnvelope(row) {
78
+ let record;
79
+ try {
80
+ record = JSON.parse(row.record_json);
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ throw new RuntimeError(`Invalid sqlite memory record '${row.id}': ${message}`);
85
+ }
86
+ if (!isObject(record)) {
87
+ throw new RuntimeError(`Invalid sqlite memory record '${row.id}': record_json must be an object`);
88
+ }
89
+ return {
90
+ id: row.id,
91
+ created_at: row.created_at,
92
+ updated_at: row.updated_at,
93
+ record,
94
+ };
95
+ }
96
+ function escapeLike(value) {
97
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
98
+ }
@@ -0,0 +1,32 @@
1
+ import { sanitizeForJson } from "../../runtime/json.js";
2
+ import { buildValueFromShape } from "../../runtime/shape.js";
3
+ export class MockLlmProvider {
4
+ async generate(request) {
5
+ return buildValueFromShape(request.returnShape);
6
+ }
7
+ }
8
+ export class MockToolProvider {
9
+ async call(request) {
10
+ return {
11
+ summary: `${request.toolName}.${request.method}`,
12
+ source: request.uri,
13
+ args: sanitizeForJson(request.args)
14
+ };
15
+ }
16
+ }
17
+ export class MockMemoryProvider {
18
+ records = [];
19
+ async add(request) {
20
+ const record = {
21
+ id: String(this.records.length + 1),
22
+ created_at: "mock",
23
+ updated_at: "mock",
24
+ record: request.record
25
+ };
26
+ this.records.push(record);
27
+ return record;
28
+ }
29
+ async query(_request) {
30
+ return [...this.records].reverse();
31
+ }
32
+ }
@@ -0,0 +1,11 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { expectObject, readRequiredString } from "./shared.js";
3
+ export class EnvToolProvider {
4
+ async call(request) {
5
+ if (request.method !== "get") {
6
+ throw new RuntimeError(`Unsupported env method '${request.method}'. Supported: get`);
7
+ }
8
+ const args = expectObject(request.args[0], "Env.get");
9
+ return process.env[readRequiredString(args.name, "Env.get.name")] ?? null;
10
+ }
11
+ }
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { RuntimeError } from "../../runtime/errors.js";
4
+ import { isObject } from "../../runtime/guards.js";
5
+ import { expectObject, readRequiredString } from "./shared.js";
6
+ const FILE_EFFECT_ACTIONS = new Set(["write", "patch"]);
7
+ export class FileToolProvider {
8
+ workspace;
9
+ constructor(workspace) {
10
+ this.workspace = workspace;
11
+ }
12
+ async call(request) {
13
+ if (request.method === "undo") {
14
+ return this.undoFileEffects(request.args[0]);
15
+ }
16
+ const args = expectObject(request.args[0], `File.${request.method}`);
17
+ switch (request.method) {
18
+ case "read":
19
+ return readFileSync(this.workspace.resolveWorkspacePath(readRequiredString(args.path, "File.read.path")), "utf8");
20
+ case "list": {
21
+ const path = this.workspace.resolveWorkspacePath(readRequiredString(args.path, "File.list.path"));
22
+ return readdirSync(path).map((entry) => entry);
23
+ }
24
+ case "write":
25
+ return this.writeFile(request.toolName, args);
26
+ case "patch":
27
+ return this.patchFile(request.toolName, args);
28
+ default:
29
+ throw new RuntimeError(`Unsupported file method '${request.method}'. Supported: read, list, write, patch, undo`);
30
+ }
31
+ }
32
+ writeFile(toolName, args) {
33
+ const path = this.workspace.resolveWorkspacePath(readRequiredString(args.path, "File.write.path"));
34
+ const relPath = this.workspace.workspaceRelativePath(path);
35
+ const content = readRequiredString(args.content, "File.write.content");
36
+ const existed = existsSync(path);
37
+ const previous = existed ? readFileSync(path, "utf8") : null;
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ writeFileSync(path, content);
40
+ return fileEffectResult({
41
+ id: `file-write:${relPath}`,
42
+ tool: toolName,
43
+ action: "write",
44
+ path: relPath,
45
+ undoable: true,
46
+ existed,
47
+ previous,
48
+ });
49
+ }
50
+ patchFile(toolName, args) {
51
+ const path = this.workspace.resolveWorkspacePath(readRequiredString(args.path, "File.patch.path"));
52
+ const relPath = this.workspace.workspaceRelativePath(path);
53
+ const search = readRequiredString(args.search, "File.patch.search");
54
+ const replace = readRequiredString(args.replace, "File.patch.replace");
55
+ const previous = readFileSync(path, "utf8");
56
+ if (search.length === 0 || !previous.includes(search)) {
57
+ return { ok: false, error: "patch search text not found", effects: [] };
58
+ }
59
+ writeFileSync(path, previous.replace(search, replace));
60
+ return fileEffectResult({
61
+ id: `file-patch:${relPath}`,
62
+ tool: toolName,
63
+ action: "patch",
64
+ path: relPath,
65
+ undoable: true,
66
+ previous,
67
+ });
68
+ }
69
+ undoFileEffects(value) {
70
+ if (!Array.isArray(value)) {
71
+ throw new RuntimeError("File.undo expects a list of effects");
72
+ }
73
+ for (const effect of value) {
74
+ if (!isFileEffect(effect))
75
+ continue;
76
+ const path = this.workspace.resolveWorkspacePath(effect.path);
77
+ if (typeof effect.previous === "string") {
78
+ writeFileSync(path, effect.previous);
79
+ }
80
+ else if (effect.existed === false && existsSync(path)) {
81
+ rmSync(path);
82
+ }
83
+ }
84
+ return { ok: true };
85
+ }
86
+ }
87
+ function fileEffectResult(effect) {
88
+ return {
89
+ ok: true,
90
+ effects: [effect],
91
+ };
92
+ }
93
+ function isFileEffect(value) {
94
+ return (isObject(value) &&
95
+ typeof value.action === "string" &&
96
+ FILE_EFFECT_ACTIONS.has(value.action) &&
97
+ typeof value.path === "string" &&
98
+ typeof value.undoable === "boolean");
99
+ }
@@ -0,0 +1,34 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { uriScheme } from "../../runtime/uri.js";
3
+ import { EnvToolProvider } from "./env.js";
4
+ import { FileToolProvider } from "./file.js";
5
+ import { HttpToolProvider } from "./http.js";
6
+ import { SchemeToolProvider } from "./scheme.js";
7
+ import { ShellToolProvider } from "./shell.js";
8
+ import { Workspace } from "./shared.js";
9
+ export class HostToolProvider {
10
+ providers;
11
+ constructor(workspaceRoot = process.cwd()) {
12
+ const workspace = new Workspace(workspaceRoot);
13
+ const http = new HttpToolProvider();
14
+ this.providers = {
15
+ env: new EnvToolProvider(),
16
+ file: new FileToolProvider(workspace),
17
+ http,
18
+ https: http,
19
+ sh: new ShellToolProvider(workspace),
20
+ };
21
+ }
22
+ async call(request) {
23
+ const scheme = uriScheme(request.uri);
24
+ const provider = this.providers[scheme];
25
+ if (!provider) {
26
+ throw new RuntimeError(`Unsupported host tool scheme '${scheme}'`);
27
+ }
28
+ return provider.call(request);
29
+ }
30
+ }
31
+ export function createDefaultToolProvider(workspaceRoot = process.cwd()) {
32
+ const host = new HostToolProvider(workspaceRoot);
33
+ return new SchemeToolProvider({ env: host, file: host, http: host, https: host, sh: host });
34
+ }
@@ -0,0 +1,40 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { isObject } from "../../runtime/guards.js";
3
+ import { sanitizeForJson } from "../../runtime/json.js";
4
+ import { expectObject, parseJsonOrNull, readPositiveInteger, readRequiredString } from "./shared.js";
5
+ const DEFAULT_HTTP_TIMEOUT_MS = 10_000;
6
+ export class HttpToolProvider {
7
+ async call(request) {
8
+ const args = expectObject(request.args[0], `Http.${request.method}`);
9
+ const url = resolveHttpUrl(request.uri, readRequiredString(args.url, "Http.url"));
10
+ const timeout = readPositiveInteger(args.timeout, DEFAULT_HTTP_TIMEOUT_MS);
11
+ const controller = new AbortController();
12
+ const init = {
13
+ method: request.method.toUpperCase(),
14
+ signal: controller.signal,
15
+ };
16
+ if (isObject(args.headers)) {
17
+ init.headers = sanitizeForJson(args.headers);
18
+ }
19
+ if ("body" in args) {
20
+ init.body = typeof args.body === "string" ? args.body : JSON.stringify(sanitizeForJson(args.body));
21
+ }
22
+ const timer = setTimeout(() => controller.abort(), timeout);
23
+ try {
24
+ const response = await fetch(url, init);
25
+ const text = await response.text();
26
+ return { ok: response.ok, status: response.status, body: text, json: parseJsonOrNull(text) };
27
+ }
28
+ finally {
29
+ clearTimeout(timer);
30
+ }
31
+ }
32
+ }
33
+ function resolveHttpUrl(importUri, requestedUrl) {
34
+ const base = new URL(importUri);
35
+ const target = new URL(requestedUrl, base);
36
+ if (target.origin !== base.origin) {
37
+ throw new RuntimeError(`HTTP tool URL origin '${target.origin}' does not match import origin '${base.origin}'`);
38
+ }
39
+ return target.toString();
40
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./host.js";
2
+ export * from "./scheme.js";
@@ -0,0 +1,16 @@
1
+ import { RuntimeError } from "../../runtime/errors.js";
2
+ import { uriScheme } from "../../runtime/uri.js";
3
+ export class SchemeToolProvider {
4
+ providers;
5
+ constructor(providers) {
6
+ this.providers = providers;
7
+ }
8
+ async call(request) {
9
+ const scheme = uriScheme(request.uri);
10
+ const provider = this.providers[scheme];
11
+ if (!provider) {
12
+ throw new RuntimeError(`Unsupported tool URI scheme '${scheme}'`);
13
+ }
14
+ return provider.call(request);
15
+ }
16
+ }
@@ -0,0 +1,92 @@
1
+ import { existsSync, lstatSync, realpathSync, readdirSync, statSync } from "node:fs";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { RuntimeError } from "../../runtime/errors.js";
4
+ import { isObject } from "../../runtime/guards.js";
5
+ import { sanitizeForJson } from "../../runtime/json.js";
6
+ export const DEFAULT_MAX_RESULTS = 100;
7
+ export class Workspace {
8
+ workspaceRoot;
9
+ constructor(workspaceRoot = process.cwd()) {
10
+ this.workspaceRoot = realpathSync(resolve(workspaceRoot));
11
+ }
12
+ resolveWorkspacePath(path) {
13
+ const resolved = resolve(this.workspaceRoot, path);
14
+ if (resolved !== this.workspaceRoot && !resolved.startsWith(`${this.workspaceRoot}${sep}`)) {
15
+ throw new RuntimeError(`Path escapes workspace: ${path}`);
16
+ }
17
+ if (existsSync(resolved)) {
18
+ const real = realpathSync(resolved);
19
+ if (real !== this.workspaceRoot && !real.startsWith(`${this.workspaceRoot}${sep}`)) {
20
+ throw new RuntimeError(`Path escapes workspace: ${path}`);
21
+ }
22
+ }
23
+ return resolved;
24
+ }
25
+ workspaceRelativePath(path) {
26
+ return relative(this.workspaceRoot, path);
27
+ }
28
+ visitWorkspaceTree(root, visitor) {
29
+ const visit = (path) => {
30
+ const linkStat = lstatSync(path);
31
+ if (linkStat.isSymbolicLink())
32
+ return true;
33
+ const stat = statSync(path);
34
+ const relativePath = relative(this.workspaceRoot, path) || ".";
35
+ if (visitor(path, relativePath, stat) === false)
36
+ return false;
37
+ if (stat.isDirectory()) {
38
+ for (const entry of readdirSync(path)) {
39
+ if (visit(resolve(path, entry)) === false)
40
+ return false;
41
+ }
42
+ }
43
+ return true;
44
+ };
45
+ if (existsSync(root))
46
+ visit(root);
47
+ }
48
+ }
49
+ export function expectObject(value, call) {
50
+ if (!isObject(value ?? null)) {
51
+ throw new RuntimeError(`${call} expects one json object argument`);
52
+ }
53
+ return value;
54
+ }
55
+ export function readRequiredString(value, name) {
56
+ if (typeof value !== "string" || value.length === 0) {
57
+ throw new RuntimeError(`${name} is required`);
58
+ }
59
+ return value;
60
+ }
61
+ export function readOptionalString(value) {
62
+ if (value === undefined)
63
+ return undefined;
64
+ if (typeof value !== "string") {
65
+ throw new RuntimeError(`Expected a string, got ${JSON.stringify(sanitizeForJson(value))}`);
66
+ }
67
+ return value;
68
+ }
69
+ export function readPositiveInteger(value, fallback) {
70
+ if (value === undefined)
71
+ return fallback;
72
+ if (typeof value === "number" && Number.isInteger(value) && value > 0)
73
+ return value;
74
+ throw new RuntimeError(`Expected a positive integer, got ${JSON.stringify(sanitizeForJson(value))}`);
75
+ }
76
+ export function globMatcher(pattern) {
77
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
78
+ const regex = new RegExp(`(^|/)${escaped}$`);
79
+ return (value) => regex.test(value);
80
+ }
81
+ export function toolUriTarget(uri) {
82
+ const parsed = new URL(uri);
83
+ return parsed.hostname || parsed.pathname.replace(/^\//, "");
84
+ }
85
+ export function parseJsonOrNull(text) {
86
+ try {
87
+ return JSON.parse(text);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
@@ -0,0 +1,80 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { RuntimeError } from "../../runtime/errors.js";
3
+ import { DEFAULT_MAX_RESULTS, expectObject, globMatcher, readOptionalString, readPositiveInteger, readRequiredString, toolUriTarget, } from "./shared.js";
4
+ const FORBIDDEN_SHELL_TOOLS = new Set(["sh", "bash", "zsh", "fish"]);
5
+ const SUPPORTED_SHELL_COMMANDS = new Set(["find", "grep", "sed"]);
6
+ const MAX_GREP_FILE_BYTES = 1024 * 1024;
7
+ export class ShellToolProvider {
8
+ workspace;
9
+ constructor(workspace) {
10
+ this.workspace = workspace;
11
+ }
12
+ async call(request) {
13
+ const command = toolUriTarget(request.uri);
14
+ if (FORBIDDEN_SHELL_TOOLS.has(command)) {
15
+ throw new RuntimeError(`Forbidden shell tool '${command}'`);
16
+ }
17
+ switch (command) {
18
+ case "find":
19
+ return this.runFind(request.args[0]);
20
+ case "grep":
21
+ return this.runGrep(request.args[0]);
22
+ case "sed":
23
+ return this.runSed(request.args[0]);
24
+ default:
25
+ throw new RuntimeError(`Unsupported shell tool '${command}'. Supported: ${[...SUPPORTED_SHELL_COMMANDS].join(", ")}`);
26
+ }
27
+ }
28
+ runFind(value) {
29
+ const args = expectObject(value, "Find.run");
30
+ const root = this.workspace.resolveWorkspacePath(readRequiredString(args.path, "Find.run.path"));
31
+ const name = readOptionalString(args.name);
32
+ const type = readOptionalString(args.type);
33
+ const max = readPositiveInteger(args.max, DEFAULT_MAX_RESULTS);
34
+ const matcher = name ? globMatcher(name) : undefined;
35
+ const results = [];
36
+ this.workspace.visitWorkspaceTree(root, (_path, relativePath, stat) => {
37
+ const kind = stat.isDirectory() ? "dir" : "file";
38
+ if ((!type || type === kind) && (!matcher || matcher(relativePath))) {
39
+ results.push(relativePath);
40
+ }
41
+ return results.length < max;
42
+ });
43
+ return { ok: true, files: results };
44
+ }
45
+ runGrep(value) {
46
+ const args = expectObject(value, "Grep.run");
47
+ const root = this.workspace.resolveWorkspacePath(readRequiredString(args.path, "Grep.run.path"));
48
+ const pattern = readRequiredString(args.pattern, "Grep.run.pattern");
49
+ const include = readOptionalString(args.include);
50
+ const max = readPositiveInteger(args.max, DEFAULT_MAX_RESULTS);
51
+ const matcher = include ? globMatcher(include) : undefined;
52
+ const matches = [];
53
+ this.workspace.visitWorkspaceTree(root, (path, relativePath, stat) => {
54
+ if (stat.isDirectory())
55
+ return true;
56
+ if (matcher && !matcher(relativePath))
57
+ return true;
58
+ if (stat.size > MAX_GREP_FILE_BYTES)
59
+ return true;
60
+ const lines = readFileSync(path, "utf8").split(/\r?\n/);
61
+ for (const [index, line] of lines.entries()) {
62
+ if (line.includes(pattern)) {
63
+ matches.push({ path: relativePath, line: index + 1, text: line });
64
+ if (matches.length >= max)
65
+ return false;
66
+ }
67
+ }
68
+ return true;
69
+ });
70
+ return { ok: true, matches };
71
+ }
72
+ runSed(value) {
73
+ const args = expectObject(value, "Sed.run");
74
+ const path = this.workspace.resolveWorkspacePath(readRequiredString(args.path, "Sed.run.path"));
75
+ const start = readPositiveInteger(args.start, 1);
76
+ const max = readPositiveInteger(args.max, DEFAULT_MAX_RESULTS);
77
+ const lines = readFileSync(path, "utf8").split(/\r?\n/).slice(start - 1, start - 1 + max);
78
+ return { ok: true, text: lines.join("\n"), start, end: start + lines.length - 1 };
79
+ }
80
+ }