@datename/fastcode-termux 0.1.6-beta → 0.1.8-beta.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.
- package/LICENSE +4 -0
- package/README.md +50 -17
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +33 -0
- package/dist/agent/approval.js +40 -5
- package/dist/agent/compaction.d.ts +2 -0
- package/dist/agent/compaction.js +60 -17
- package/dist/agent/guards.d.ts +16 -16
- package/dist/agent/guards.js +978 -24
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +20 -0
- package/dist/agent/process-tree.js +81 -10
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +165 -38
- package/dist/agent/registry.d.ts +3 -1
- package/dist/agent/registry.js +168 -11
- package/dist/agent/runner.d.ts +21 -0
- package/dist/agent/runner.js +44 -5
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +14 -0
- package/dist/agent/shell-session.js +251 -40
- package/dist/agent/tool-schemas.js +26 -7
- package/dist/agent/tools/append_file.js +3 -1
- package/dist/agent/tools/apply_patch.js +44 -4
- package/dist/agent/tools/copy_file.js +26 -3
- package/dist/agent/tools/delete_file.js +22 -3
- package/dist/agent/tools/grep.js +35 -4
- package/dist/agent/tools/make_dir.js +3 -1
- package/dist/agent/tools/move_path.d.ts +7 -0
- package/dist/agent/tools/move_path.js +31 -5
- package/dist/agent/tools/read_file.js +63 -3
- package/dist/agent/tools/read_many.js +21 -3
- package/dist/agent/tools/readers.d.ts +53 -1
- package/dist/agent/tools/readers.js +236 -26
- package/dist/agent/tools/replace_in_file.js +38 -5
- package/dist/agent/tools/shell.js +2 -2
- package/dist/agent/tools/types.d.ts +26 -0
- package/dist/agent/tools/validate.d.ts +15 -2
- package/dist/agent/tools/validate.js +66 -3
- package/dist/agent/tools/write_file.js +3 -1
- package/dist/app.d.ts +2 -1
- package/dist/app.js +526 -85
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +397 -337
- package/dist/commands/about.js +1 -1
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +12 -0
- package/dist/commands/compact.js +14 -2
- package/dist/commands/context.js +5 -4
- package/dist/commands/index.js +2 -0
- package/dist/commands/jobs.js +3 -2
- package/dist/commands/kill.js +3 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +15 -15
- package/dist/commands/model.d.ts +1 -13
- package/dist/commands/model.js +6 -38
- package/dist/commands/provider.d.ts +1 -15
- package/dist/commands/provider.js +4 -22
- package/dist/commands/skills.js +34 -4
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/config/store.d.ts +21 -0
- package/dist/config/store.js +47 -9
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +41 -2
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +19 -0
- package/dist/lib/diagnostics.js +32 -6
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/openaiCompatible.d.ts +24 -2
- package/dist/providers/openaiCompatible.js +20 -6
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/session/log.d.ts +14 -2
- package/dist/session/log.js +56 -27
- package/dist/session/projection.d.ts +34 -1
- package/dist/session/projection.js +315 -46
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +15 -0
- package/dist/ui/StatusBar.js +1 -1
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +58 -11
- package/src/agent/compaction.ts +62 -17
- package/src/agent/guards.ts +1049 -25
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +105 -23
- package/src/agent/protocol.ts +187 -39
- package/src/agent/registry.ts +186 -14
- package/src/agent/runner.ts +50 -7
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +265 -41
- package/src/agent/tool-schemas.ts +26 -7
- package/src/agent/tools/append_file.ts +3 -1
- package/src/agent/tools/apply_patch.ts +45 -4
- package/src/agent/tools/copy_file.ts +27 -3
- package/src/agent/tools/delete_file.ts +22 -3
- package/src/agent/tools/grep.ts +36 -4
- package/src/agent/tools/make_dir.ts +3 -1
- package/src/agent/tools/move_path.ts +38 -5
- package/src/agent/tools/read_file.ts +64 -3
- package/src/agent/tools/read_many.ts +21 -3
- package/src/agent/tools/readers.ts +289 -26
- package/src/agent/tools/replace_in_file.ts +39 -5
- package/src/agent/tools/shell.ts +2 -2
- package/src/agent/tools/types.ts +43 -1
- package/src/agent/tools/validate.ts +73 -4
- package/src/agent/tools/write_file.ts +3 -1
- package/src/app.ts +533 -82
- package/src/{cli.ts → cli-main.ts} +34 -6
- package/src/commands/about.ts +1 -1
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +14 -2
- package/src/commands/context.ts +5 -4
- package/src/commands/index.ts +2 -0
- package/src/commands/jobs.ts +3 -2
- package/src/commands/kill.ts +3 -1
- package/src/commands/login.ts +15 -15
- package/src/commands/model.ts +6 -38
- package/src/commands/provider.ts +4 -22
- package/src/commands/skills.ts +34 -4
- package/src/commands/threads.ts +28 -0
- package/src/config/store.ts +47 -9
- package/src/config/trust.ts +45 -2
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +32 -6
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/openaiCompatible.ts +47 -10
- package/src/providers/spec.ts +42 -0
- package/src/session/log.ts +70 -27
- package/src/session/projection.ts +360 -47
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +16 -1
- package/src/ui/StatusBar.ts +1 -1
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
|
@@ -4,15 +4,22 @@ const CWD_NOTE = "Paths are relative to the session working directory unless abs
|
|
|
4
4
|
|
|
5
5
|
export function toolSchemas(): ToolSchema[] {
|
|
6
6
|
return [
|
|
7
|
-
schema("shell", "Run a shell command. Long commands return a session id for polling.", {
|
|
7
|
+
schema("shell", "Run a shell command. Long commands return a session id for polling. Killing a session kills its process group; setsid descendants may escape.", {
|
|
8
8
|
command: { type: "string" },
|
|
9
9
|
timeoutMs: { type: "number" },
|
|
10
10
|
session_id: { type: "string", description: "Poll an existing background session." },
|
|
11
11
|
waitMs: { type: "number", description: "Milliseconds to wait for new session output." },
|
|
12
12
|
}),
|
|
13
13
|
schema("pwd", "Print the current working directory.", {}),
|
|
14
|
-
schema("read_file",
|
|
15
|
-
|
|
14
|
+
schema("read_file", "Read a file. Use offset/limit (1-based lines) for a targeted range in large files.", {
|
|
15
|
+
path: { type: "string" },
|
|
16
|
+
offset: { type: "number", minimum: 1, description: "First line to return (1-based)." },
|
|
17
|
+
limit: { type: "number", minimum: 1, maximum: 2000, description: "Maximum lines to return (default 2000)." },
|
|
18
|
+
}, ["path"]),
|
|
19
|
+
schema("read_many", "Read several files under a shared byte budget.", {
|
|
20
|
+
paths: { type: "array", items: { type: "string" } },
|
|
21
|
+
maxBytes: { type: "number", minimum: 1, description: "Total output budget across all files." },
|
|
22
|
+
}, ["paths"]),
|
|
16
23
|
schema("write_file", "Replace a file with full contents.", {
|
|
17
24
|
path: { type: "string" },
|
|
18
25
|
content: { type: "string" },
|
|
@@ -24,8 +31,15 @@ export function toolSchemas(): ToolSchema[] {
|
|
|
24
31
|
schema("apply_patch", "Apply a multi-file patch (*** Begin Patch ... *** End Patch).", {
|
|
25
32
|
patch: { type: "string" },
|
|
26
33
|
}, ["patch"]),
|
|
27
|
-
schema("copy_file", CWD_NOTE, {
|
|
28
|
-
|
|
34
|
+
schema("copy_file", CWD_NOTE, {
|
|
35
|
+
from: { type: "string" },
|
|
36
|
+
to: { type: "string" },
|
|
37
|
+
overwrite: { type: "boolean", description: "Replace an existing target explicitly (default false)." },
|
|
38
|
+
}, ["from", "to"]),
|
|
39
|
+
schema("delete_file", "Delete a file, or a directory with recursive:true.", {
|
|
40
|
+
path: { type: "string" },
|
|
41
|
+
recursive: { type: "boolean", description: "Required for directories (default false)." },
|
|
42
|
+
}, ["path"]),
|
|
29
43
|
schema("list_files", "List files under a directory.", {
|
|
30
44
|
path: { type: "string" },
|
|
31
45
|
maxDepth: { type: "number" },
|
|
@@ -39,12 +53,17 @@ export function toolSchemas(): ToolSchema[] {
|
|
|
39
53
|
schema("grep", "Search file contents with a regular expression.", {
|
|
40
54
|
pattern: { type: "string" },
|
|
41
55
|
path: { type: "string" },
|
|
42
|
-
maxDepth: { type: "number" },
|
|
56
|
+
maxDepth: { type: "number", minimum: 1, maximum: 20 },
|
|
57
|
+
caseSensitive: { type: "boolean", description: "Match case exactly (default false)." },
|
|
58
|
+
glob: { type: "string", description: "Only search files matching this glob, e.g. *.ts." },
|
|
59
|
+
context: { type: "number", minimum: 0, maximum: 10, description: "Context lines before/after each match." },
|
|
60
|
+
maxResults: { type: "number", minimum: 1, maximum: 5000, description: "Stop after this many matching lines." },
|
|
43
61
|
}, ["pattern"]),
|
|
44
|
-
schema("replace_in_file", "Replace
|
|
62
|
+
schema("replace_in_file", "Replace exact text occurrences; expectedMatches makes the count explicit.", {
|
|
45
63
|
path: { type: "string" },
|
|
46
64
|
find: { type: "string" },
|
|
47
65
|
replace: { type: "string" },
|
|
66
|
+
expectedMatches: { type: "number", minimum: 1, description: "Fail without writing unless this exact count is found." },
|
|
48
67
|
}, ["path", "find", "replace"]),
|
|
49
68
|
schema("stat_path", "Show file metadata.", { path: { type: "string" } }, ["path"]),
|
|
50
69
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
3
|
import { dirname } from "node:path";
|
|
3
4
|
import { resolvePath } from "./paths.js";
|
|
@@ -21,8 +22,9 @@ export const appendFileTool: Tool = {
|
|
|
21
22
|
},
|
|
22
23
|
async execute(call, ctx) {
|
|
23
24
|
const file = resolvePath(ctx.cwd, call);
|
|
25
|
+
const existed = existsSync(file);
|
|
24
26
|
await mkdir(dirname(file), { recursive: true });
|
|
25
27
|
await appendFile(file, requiredString(call, "content"), "utf8");
|
|
26
|
-
return { ok: true, output: `appended ${file}`, sideEffects: { touched: [file] } };
|
|
28
|
+
return { ok: true, output: `appended ${file}`, sideEffects: existed ? { touched: [file] } : { created: [file] } };
|
|
27
29
|
},
|
|
28
30
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { parsePatch } from "../../patch/parse.js";
|
|
2
2
|
import { planPatch, reviewPatchPlan } from "../../patch/plan.js";
|
|
3
3
|
import { applyPatchPlan } from "../../patch/apply.js";
|
|
4
|
+
import { DEFAULT_ALLOW_OPTIONS, approvalRequestId, type ApprovalOption, type ApprovalRequest } from "../approval.js";
|
|
5
|
+
import { approvalKey, isPreApproved, rememberApproval } from "../approval-store.js";
|
|
4
6
|
import type { Tool } from "./types.js";
|
|
5
7
|
import { invalid, requiredString } from "./validate.js";
|
|
6
8
|
|
|
@@ -38,10 +40,29 @@ export const applyPatchTool: Tool = {
|
|
|
38
40
|
if (review.kind === "deny")
|
|
39
41
|
return { ok: false, output: `patch denied: ${review.reason}` };
|
|
40
42
|
if (review.kind === "pending") {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const key = approvalKey("apply_patch", call, ctx.cwd);
|
|
44
|
+
const catalogHash = ctx.catalogHash ?? "";
|
|
45
|
+
if (!isPreApproved(key, catalogHash)) {
|
|
46
|
+
if (!ctx.requestApproval) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
output: `patch needs approval (${review.reason}), but no approval resolver is registered; nothing was written.`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const request = patchApprovalRequest(call, review, planned.plan);
|
|
53
|
+
let decision;
|
|
54
|
+
try {
|
|
55
|
+
decision = await ctx.requestApproval(request);
|
|
56
|
+
}
|
|
57
|
+
catch (err: any) {
|
|
58
|
+
return { ok: false, output: `patch approval failed (fail closed): ${err?.message ?? String(err)}` };
|
|
59
|
+
}
|
|
60
|
+
if (!decision || decision.kind !== "allow") {
|
|
61
|
+
const reason = decision?.kind === "deny" ? decision.reason : decision?.kind === "feedback" ? decision.text : "no allow decision";
|
|
62
|
+
return { ok: false, output: `patch denied: ${reason ?? "no reason"}; nothing was written.` };
|
|
63
|
+
}
|
|
64
|
+
rememberApproval(key, catalogHash, decision.scope);
|
|
65
|
+
}
|
|
45
66
|
}
|
|
46
67
|
const applied = applyPatchPlan(planned.plan);
|
|
47
68
|
const warnings = applied.warnings.length > 0 ? "\n\nwarnings:\n" + applied.warnings.map((line) => "- " + line).join("\n") : "";
|
|
@@ -52,3 +73,23 @@ export const applyPatchTool: Tool = {
|
|
|
52
73
|
};
|
|
53
74
|
},
|
|
54
75
|
};
|
|
76
|
+
|
|
77
|
+
function patchApprovalRequest(call: any, review: { reason: string; changedLines: number }, plan: any): ApprovalRequest {
|
|
78
|
+
const options = DEFAULT_ALLOW_OPTIONS.map((option) => ({ ...option })) as ApprovalOption[];
|
|
79
|
+
const denyIndex = options.findIndex((option) => option.kind === "deny");
|
|
80
|
+
const files = Array.isArray(plan?.files) ? plan.files : [];
|
|
81
|
+
const diff = files.map((file: any) => `${file.relPath ?? file.path}: ${file.changedLines ?? "?"} line(s)`).join("\n");
|
|
82
|
+
return {
|
|
83
|
+
id: approvalRequestId(),
|
|
84
|
+
tool: "apply_patch",
|
|
85
|
+
args: call,
|
|
86
|
+
reason: review.reason.includes("sensitive") ? "sensitive_path" : "large_diff",
|
|
87
|
+
options,
|
|
88
|
+
preview: diff ? { kind: "diff", text: diff } : undefined,
|
|
89
|
+
tier: 2,
|
|
90
|
+
prompt: `Apply patch? ${review.reason}`,
|
|
91
|
+
source: "system",
|
|
92
|
+
defaultIndex: denyIndex >= 0 ? denyIndex : 0,
|
|
93
|
+
timeoutMs: 120_000,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { copyFile, mkdir } from "node:fs/promises";
|
|
1
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { sandboxPath } from "./paths.js";
|
|
4
4
|
import type { Tool } from "./types.js";
|
|
5
|
-
import { invalid, preparePath, requiredString } from "./validate.js";
|
|
5
|
+
import { booleanOption, invalid, preparePath, requiredString } from "./validate.js";
|
|
6
|
+
|
|
7
|
+
async function exists(path: string): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
await stat(path);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
|
|
7
17
|
export const copyFileTool: Tool = {
|
|
8
18
|
name: "copy_file",
|
|
@@ -25,8 +35,22 @@ export const copyFileTool: Tool = {
|
|
|
25
35
|
async execute(call, ctx) {
|
|
26
36
|
const from = sandboxPath(ctx.cwd, requiredString(call, "from"));
|
|
27
37
|
const to = sandboxPath(ctx.cwd, requiredString(call, "to"));
|
|
38
|
+
const overwrite = booleanOption(call.overwrite);
|
|
39
|
+
const existed = await exists(to);
|
|
40
|
+
if (existed && !overwrite) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
output: `copy target exists: ${to}`,
|
|
44
|
+
code: "EEXIST",
|
|
45
|
+
hint: "Pass overwrite:true to replace the target explicitly.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
28
48
|
await mkdir(dirname(to), { recursive: true });
|
|
29
49
|
await copyFile(from, to);
|
|
30
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
output: `copied ${from} to ${to}`,
|
|
53
|
+
sideEffects: existed ? { touched: [to] } : { created: [to] },
|
|
54
|
+
};
|
|
31
55
|
},
|
|
32
56
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
1
|
+
import { lstat, rm } from "node:fs/promises";
|
|
2
2
|
import { resolvePath } from "./paths.js";
|
|
3
3
|
import type { Tool } from "./types.js";
|
|
4
|
-
import { invalid, preparePath, requiredString } from "./validate.js";
|
|
4
|
+
import { booleanOption, invalid, preparePath, requiredString } from "./validate.js";
|
|
5
5
|
|
|
6
6
|
export const deleteFileTool: Tool = {
|
|
7
7
|
name: "delete_file",
|
|
8
8
|
requiresTrust: true,
|
|
9
9
|
stateScope: "turn",
|
|
10
10
|
revalidate: ["trust", "sandboxRoots"],
|
|
11
|
+
approvalHints: { mayRequireApproval: true },
|
|
11
12
|
prepare(call, ctx) {
|
|
12
13
|
let path: string;
|
|
13
14
|
try {
|
|
@@ -20,7 +21,25 @@ export const deleteFileTool: Tool = {
|
|
|
20
21
|
},
|
|
21
22
|
async execute(call, ctx) {
|
|
22
23
|
const file = resolvePath(ctx.cwd, call);
|
|
23
|
-
|
|
24
|
+
const recursive = booleanOption(call.recursive);
|
|
25
|
+
let info;
|
|
26
|
+
try {
|
|
27
|
+
info = await lstat(file);
|
|
28
|
+
}
|
|
29
|
+
catch (err: any) {
|
|
30
|
+
if (err?.code === "ENOENT")
|
|
31
|
+
return { ok: false, output: `path does not exist: ${file}`, code: "ENOENT", hint: "Check the path before deleting." };
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
if (info.isDirectory() && !recursive) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
output: `refusing to delete a directory without recursive:true: ${file}`,
|
|
38
|
+
code: "EISDIR",
|
|
39
|
+
hint: "Pass recursive:true to delete a directory tree.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
await rm(file, { recursive, force: false });
|
|
24
43
|
return { ok: true, output: `deleted ${file}`, sideEffects: { removed: [file] } };
|
|
25
44
|
},
|
|
26
45
|
};
|
package/src/agent/tools/grep.ts
CHANGED
|
@@ -1,25 +1,57 @@
|
|
|
1
1
|
import { grepFiles } from "./readers.js";
|
|
2
2
|
import { resolvePath } from "./paths.js";
|
|
3
3
|
import type { Tool } from "./types.js";
|
|
4
|
-
import { invalid,
|
|
4
|
+
import { booleanOption, clampInt, invalid, optionalNumber, preparePath, requiredString } from "./validate.js";
|
|
5
|
+
|
|
6
|
+
const MAX_PATTERN_LENGTH = 1000;
|
|
5
7
|
|
|
6
8
|
export const grepTool: Tool = {
|
|
7
9
|
name: "grep",
|
|
8
10
|
stateScope: "call",
|
|
9
11
|
revalidate: ["sandboxRoots"],
|
|
10
12
|
prepare(call, ctx) {
|
|
13
|
+
let pattern: string;
|
|
11
14
|
try {
|
|
12
|
-
requiredString(call, "pattern");
|
|
15
|
+
pattern = requiredString(call, "pattern");
|
|
16
|
+
if (pattern.length > MAX_PATTERN_LENGTH)
|
|
17
|
+
return invalid(`pattern is too long (max ${MAX_PATTERN_LENGTH} characters)`);
|
|
18
|
+
const flags = booleanOption(call.caseSensitive) ? "" : "i";
|
|
19
|
+
new RegExp(pattern, flags);
|
|
20
|
+
const context = optionalNumber(call, "context");
|
|
21
|
+
const maxResults = optionalNumber(call, "maxResults");
|
|
22
|
+
const maxDepth = optionalNumber(call, "maxDepth");
|
|
23
|
+
if (context !== undefined && (context < 0 || context > 10))
|
|
24
|
+
return invalid("context must be between 0 and 10");
|
|
25
|
+
if (maxResults !== undefined && (maxResults < 1 || maxResults > 5000))
|
|
26
|
+
return invalid("maxResults must be between 1 and 5000");
|
|
27
|
+
if (maxDepth !== undefined && (maxDepth < 1 || maxDepth > 20))
|
|
28
|
+
return invalid("maxDepth must be between 1 and 20");
|
|
29
|
+
if (call.glob !== undefined && typeof call.glob !== "string")
|
|
30
|
+
return invalid("glob must be a string");
|
|
13
31
|
}
|
|
14
32
|
catch (err: any) {
|
|
15
|
-
return invalid(err
|
|
33
|
+
return invalid(`invalid regular expression: ${err?.message ?? String(err)}`, "INVALID_ARGS", "pattern must be a valid regular expression");
|
|
16
34
|
}
|
|
17
35
|
return preparePath(ctx.cwd, typeof call.path === "string" ? call.path : ".");
|
|
18
36
|
},
|
|
19
37
|
async execute(call, ctx) {
|
|
38
|
+
const pattern = requiredString(call, "pattern");
|
|
39
|
+
const result = await grepFiles(ctx.cwd, resolvePath(ctx.cwd, call, "."), {
|
|
40
|
+
pattern,
|
|
41
|
+
flags: booleanOption(call.caseSensitive) ? "" : "i",
|
|
42
|
+
glob: typeof call.glob === "string" ? call.glob : undefined,
|
|
43
|
+
context: clampInt(optionalNumber(call, "context"), 0, 0, 10),
|
|
44
|
+
maxResults: clampInt(optionalNumber(call, "maxResults"), 200, 1, 5000),
|
|
45
|
+
maxDepth: clampInt(optionalNumber(call, "maxDepth"), 4, 1, 20),
|
|
46
|
+
signal: ctx.signal,
|
|
47
|
+
});
|
|
20
48
|
return {
|
|
21
49
|
ok: true,
|
|
22
|
-
output:
|
|
50
|
+
output: result.text,
|
|
51
|
+
truncated: result.truncated,
|
|
52
|
+
returnedLines: result.matchCount,
|
|
53
|
+
matches: result.matchCount,
|
|
54
|
+
hint: result.truncated ? "grep results were truncated by maxResults or the output cap." : undefined,
|
|
23
55
|
};
|
|
24
56
|
},
|
|
25
57
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { mkdir } from "node:fs/promises";
|
|
2
3
|
import { resolvePath } from "./paths.js";
|
|
3
4
|
import type { Tool } from "./types.js";
|
|
@@ -20,7 +21,8 @@ export const makeDirTool: Tool = {
|
|
|
20
21
|
},
|
|
21
22
|
async execute(call, ctx) {
|
|
22
23
|
const dir = resolvePath(ctx.cwd, call);
|
|
24
|
+
const existed = existsSync(dir);
|
|
23
25
|
await mkdir(dir, { recursive: true });
|
|
24
|
-
return { ok: true, output: `created ${dir}`, sideEffects: { created: [dir] } };
|
|
26
|
+
return { ok: true, output: `created ${dir}`, sideEffects: existed ? undefined : { created: [dir] } };
|
|
25
27
|
},
|
|
26
28
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, rename } from "node:fs/promises";
|
|
1
|
+
import { cp, mkdir, rename, rm } from "node:fs/promises";
|
|
2
2
|
import { lstatSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { sandboxPath } from "./paths.js";
|
|
@@ -15,6 +15,33 @@ function exists(path: string): boolean {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface MoveOps {
|
|
19
|
+
rename: typeof rename;
|
|
20
|
+
cp: typeof cp;
|
|
21
|
+
rm: typeof rm;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function moveWithFallback(from: string, to: string, ops: MoveOps = { rename, cp, rm }): Promise<"rename" | "copy"> {
|
|
25
|
+
try {
|
|
26
|
+
await ops.rename(from, to);
|
|
27
|
+
return "rename";
|
|
28
|
+
}
|
|
29
|
+
catch (err: any) {
|
|
30
|
+
if (err?.code !== "EXDEV")
|
|
31
|
+
throw err;
|
|
32
|
+
await ops.cp(from, to, { recursive: true, force: false, errorOnExist: true });
|
|
33
|
+
try {
|
|
34
|
+
await ops.rm(from, { recursive: true, force: false });
|
|
35
|
+
}
|
|
36
|
+
catch (cleanup: any) {
|
|
37
|
+
throw Object.assign(new Error(`copied ${from} to ${to}, but could not remove the source: ${cleanup?.message ?? String(cleanup)}`), {
|
|
38
|
+
code: "UNKNOWN",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return "copy";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
export const movePathTool: Tool = {
|
|
19
46
|
name: "move_path",
|
|
20
47
|
requiresTrust: true,
|
|
@@ -31,7 +58,7 @@ export const movePathTool: Tool = {
|
|
|
31
58
|
if (!checkedTo.ok)
|
|
32
59
|
return checkedTo;
|
|
33
60
|
if (exists(to))
|
|
34
|
-
return invalid(`move target exists: ${to}
|
|
61
|
+
return invalid(`move target exists: ${to}`, "EEXIST", "Choose a target that does not exist.");
|
|
35
62
|
return { ok: true };
|
|
36
63
|
}
|
|
37
64
|
catch (err: any) {
|
|
@@ -42,9 +69,15 @@ export const movePathTool: Tool = {
|
|
|
42
69
|
const from = sandboxPath(ctx.cwd, requiredString(call, "from"));
|
|
43
70
|
const to = sandboxPath(ctx.cwd, requiredString(call, "to"));
|
|
44
71
|
if (exists(to))
|
|
45
|
-
return { ok: false, output: `move target exists: ${to}
|
|
72
|
+
return { ok: false, output: `move target exists: ${to}`, code: "EEXIST", hint: "Choose a target that does not exist." };
|
|
46
73
|
await mkdir(dirname(to), { recursive: true });
|
|
47
|
-
await
|
|
48
|
-
return {
|
|
74
|
+
const method = await moveWithFallback(from, to);
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
output: method === "copy"
|
|
78
|
+
? `moved ${from} to ${to} (cross-device copy + remove)`
|
|
79
|
+
: `moved ${from} to ${to}`,
|
|
80
|
+
sideEffects: { removed: [from], touched: [to] },
|
|
81
|
+
};
|
|
49
82
|
},
|
|
50
83
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileRange, readTextFileResult } from "./readers.js";
|
|
2
2
|
import { resolvePath } from "./paths.js";
|
|
3
3
|
import type { Tool } from "./types.js";
|
|
4
|
-
import { invalid, preparePath, requiredString } from "./validate.js";
|
|
4
|
+
import { clampInt, invalid, optionalNumber, preparePath, requiredString } from "./validate.js";
|
|
5
|
+
|
|
6
|
+
const MAX_READ_LINES = 2000;
|
|
5
7
|
|
|
6
8
|
export const readFileTool: Tool = {
|
|
7
9
|
name: "read_file",
|
|
@@ -11,6 +13,12 @@ export const readFileTool: Tool = {
|
|
|
11
13
|
let path: string;
|
|
12
14
|
try {
|
|
13
15
|
path = requiredString(call, "path");
|
|
16
|
+
const offset = optionalNumber(call, "offset");
|
|
17
|
+
const limit = optionalNumber(call, "limit");
|
|
18
|
+
if (offset !== undefined && offset < 1)
|
|
19
|
+
return invalid("offset must be >= 1");
|
|
20
|
+
if (limit !== undefined && limit < 1)
|
|
21
|
+
return invalid("limit must be >= 1");
|
|
14
22
|
}
|
|
15
23
|
catch (err: any) {
|
|
16
24
|
return invalid(err.message);
|
|
@@ -18,6 +26,59 @@ export const readFileTool: Tool = {
|
|
|
18
26
|
return preparePath(ctx.cwd, path);
|
|
19
27
|
},
|
|
20
28
|
async execute(call, ctx) {
|
|
21
|
-
|
|
29
|
+
const file = resolvePath(ctx.cwd, call);
|
|
30
|
+
const offset = optionalNumber(call, "offset");
|
|
31
|
+
const limit = optionalNumber(call, "limit");
|
|
32
|
+
if (offset === undefined && limit === undefined) {
|
|
33
|
+
const result = await readTextFileResult(file);
|
|
34
|
+
const output = result.truncated
|
|
35
|
+
? `${result.text}\n[... truncated at ${result.returnedBytes} of ${result.totalBytes} bytes; use offset/limit, grep, or a narrower file ...]`
|
|
36
|
+
: result.text;
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
output,
|
|
40
|
+
truncated: result.truncated,
|
|
41
|
+
totalLines: result.totalLines,
|
|
42
|
+
returnedLines: result.returnedLines,
|
|
43
|
+
totalBytes: result.totalBytes,
|
|
44
|
+
returnedBytes: result.returnedBytes,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const start = clampInt(offset, 1, 1, 1_000_000);
|
|
48
|
+
const count = clampInt(limit, MAX_READ_LINES, 1, MAX_READ_LINES);
|
|
49
|
+
const result = await readFileRange(file, start, count, { signal: ctx.signal });
|
|
50
|
+
if (result.binary) {
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
output: result.text,
|
|
54
|
+
truncated: false,
|
|
55
|
+
totalLines: 0,
|
|
56
|
+
returnedLines: 0,
|
|
57
|
+
totalBytes: result.totalBytes,
|
|
58
|
+
returnedBytes: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (result.returnedLines === 0 && start > result.totalLines) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
output: `offset ${start} is beyond the end of ${file} (${result.totalLines} lines)`,
|
|
65
|
+
code: "NOT_FOUND",
|
|
66
|
+
hint: "Use a smaller offset or read without offset.",
|
|
67
|
+
totalLines: result.totalLines,
|
|
68
|
+
returnedLines: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const end = start + result.returnedLines - 1;
|
|
72
|
+
const header = `[lines ${start}-${end} of ${result.totalLines}${result.scannedAll ? "" : "+"}]\n`;
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
output: header + result.text,
|
|
76
|
+
truncated: result.truncated,
|
|
77
|
+
hint: result.scannedAll ? undefined : "totalLines is a lower bound; the file scan hit FASTCODE_READ_SCAN_MAX_BYTES.",
|
|
78
|
+
totalLines: result.totalLines,
|
|
79
|
+
returnedLines: result.returnedLines,
|
|
80
|
+
totalBytes: result.totalBytes,
|
|
81
|
+
returnedBytes: result.returnedBytes,
|
|
82
|
+
};
|
|
22
83
|
},
|
|
23
84
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readManyResult } from "./readers.js";
|
|
2
2
|
import type { Tool } from "./types.js";
|
|
3
|
-
import { invalid, preparePath, stringArray } from "./validate.js";
|
|
3
|
+
import { invalid, optionalNumber, preparePath, stringArray } from "./validate.js";
|
|
4
4
|
|
|
5
5
|
export const readManyTool: Tool = {
|
|
6
6
|
name: "read_many",
|
|
@@ -10,6 +10,11 @@ export const readManyTool: Tool = {
|
|
|
10
10
|
let paths: string[];
|
|
11
11
|
try {
|
|
12
12
|
paths = stringArray(call, "paths");
|
|
13
|
+
if (paths.length === 0)
|
|
14
|
+
return invalid("paths must contain at least one path");
|
|
15
|
+
const maxBytes = optionalNumber(call, "maxBytes");
|
|
16
|
+
if (maxBytes !== undefined && maxBytes < 1)
|
|
17
|
+
return invalid("maxBytes must be >= 1");
|
|
13
18
|
}
|
|
14
19
|
catch (err: any) {
|
|
15
20
|
return invalid(err.message);
|
|
@@ -22,6 +27,19 @@ export const readManyTool: Tool = {
|
|
|
22
27
|
return { ok: true };
|
|
23
28
|
},
|
|
24
29
|
async execute(call, ctx) {
|
|
25
|
-
|
|
30
|
+
const paths = stringArray(call, "paths");
|
|
31
|
+
const requested = optionalNumber(call, "maxBytes");
|
|
32
|
+
const maxBytes = requested === undefined ? undefined : Math.max(1, Math.floor(requested));
|
|
33
|
+
const result = await readManyResult(ctx.cwd, paths, maxBytes);
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
output: result.text,
|
|
37
|
+
truncated: result.truncated,
|
|
38
|
+
totalBytes: result.totalBytes,
|
|
39
|
+
returnedBytes: result.returnedBytes,
|
|
40
|
+
filesRead: result.filesRead,
|
|
41
|
+
filesSkipped: result.filesSkipped,
|
|
42
|
+
hint: result.filesSkipped > 0 ? `${result.filesSkipped} file(s) skipped by the read_many byte budget.` : undefined,
|
|
43
|
+
};
|
|
26
44
|
},
|
|
27
45
|
};
|