@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
|
@@ -1,16 +1,65 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
1
2
|
import { open, readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { basename, extname, join } from "node:path";
|
|
3
|
+
import { basename, extname, join, relative } from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
3
5
|
import { createIgnore, isBinaryName } from "../ignore.js";
|
|
4
6
|
import { createOutputBuffer } from "../limits.js";
|
|
5
7
|
import { allowedRoots, isInsideAny, realpathOf, sandboxPath } from "./paths.js";
|
|
6
8
|
|
|
7
|
-
export
|
|
9
|
+
export interface ReadManyResult {
|
|
10
|
+
text: string;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
totalBytes: number;
|
|
13
|
+
returnedBytes: number;
|
|
14
|
+
filesRead: number;
|
|
15
|
+
filesSkipped: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readManyResult(cwd: string, paths: string[], maxBytes = readManyMaxBytes()): Promise<ReadManyResult> {
|
|
8
19
|
const blocks: string[] = [];
|
|
20
|
+
let used = 0;
|
|
21
|
+
let totalBytes = 0;
|
|
22
|
+
let filesRead = 0;
|
|
23
|
+
let truncated = false;
|
|
9
24
|
for (const path of paths) {
|
|
25
|
+
if (used >= maxBytes) {
|
|
26
|
+
truncated = true;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
10
29
|
const file = sandboxPath(cwd, path);
|
|
11
|
-
|
|
30
|
+
const result = await readTextFileResult(file, maxBytes - used);
|
|
31
|
+
totalBytes += result.totalBytes;
|
|
32
|
+
const block = `### ${file}\n${result.text}`;
|
|
33
|
+
const blockBytes = Buffer.byteLength(block, "utf8");
|
|
34
|
+
if (used + blockBytes > maxBytes) {
|
|
35
|
+
const allowed = Math.max(0, maxBytes - used);
|
|
36
|
+
const clipped = Buffer.from(block, "utf8").subarray(0, allowed).toString("utf8");
|
|
37
|
+
blocks.push(`${clipped}\n[... read_many budget exhausted at ${maxBytes} bytes ...]`);
|
|
38
|
+
used = maxBytes;
|
|
39
|
+
filesRead += 1;
|
|
40
|
+
truncated = true;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
blocks.push(block);
|
|
44
|
+
used += blockBytes;
|
|
45
|
+
filesRead += 1;
|
|
46
|
+
if (result.truncated)
|
|
47
|
+
truncated = true;
|
|
12
48
|
}
|
|
13
|
-
|
|
49
|
+
if (filesRead < paths.length)
|
|
50
|
+
truncated = true;
|
|
51
|
+
return {
|
|
52
|
+
text: blocks.join("\n\n"),
|
|
53
|
+
truncated,
|
|
54
|
+
totalBytes,
|
|
55
|
+
returnedBytes: used,
|
|
56
|
+
filesRead,
|
|
57
|
+
filesSkipped: Math.max(0, paths.length - filesRead),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function readMany(cwd: string, paths: string[]): Promise<string> {
|
|
62
|
+
return (await readManyResult(cwd, paths)).text;
|
|
14
63
|
}
|
|
15
64
|
|
|
16
65
|
const CODE_EXT = new Set([
|
|
@@ -51,7 +100,7 @@ export async function listFiles(sandbox: string, root: string, maxDepth: number,
|
|
|
51
100
|
let line = `${indent(depth + 1)}${entry.name} (${formatSize(info.size)})`;
|
|
52
101
|
const symbols = await fileSymbols(full, entry.name, info.size);
|
|
53
102
|
if (symbols)
|
|
54
|
-
line += `
|
|
103
|
+
line += ` 鈥?${symbols}`;
|
|
55
104
|
out.line(line);
|
|
56
105
|
if (out.full)
|
|
57
106
|
return;
|
|
@@ -104,67 +153,279 @@ export function formatSize(bytes: number): string {
|
|
|
104
153
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
105
154
|
}
|
|
106
155
|
|
|
107
|
-
export
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
export interface GrepOptions {
|
|
157
|
+
pattern: string;
|
|
158
|
+
flags?: string;
|
|
159
|
+
glob?: string;
|
|
160
|
+
context?: number;
|
|
161
|
+
maxResults?: number;
|
|
162
|
+
maxDepth?: number;
|
|
163
|
+
signal?: AbortSignal;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface GrepResult {
|
|
167
|
+
text: string;
|
|
168
|
+
truncated: boolean;
|
|
169
|
+
matchCount: number;
|
|
170
|
+
filesScanned: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function grepFiles(sandbox: string, root: string, options: GrepOptions): Promise<GrepResult> {
|
|
174
|
+
const re = new RegExp(options.pattern, options.flags ?? "i");
|
|
175
|
+
const maxResults = Math.max(1, options.maxResults ?? 200);
|
|
176
|
+
const context = Math.max(0, options.context ?? 0);
|
|
110
177
|
const maxBytes = grepMaxBytes();
|
|
111
|
-
|
|
178
|
+
const out = createOutputBuffer();
|
|
179
|
+
const globRe = options.glob ? globToRegExp(options.glob) : undefined;
|
|
180
|
+
let matchCount = 0;
|
|
181
|
+
let filesScanned = 0;
|
|
182
|
+
let truncated = false;
|
|
183
|
+
await walkWithin(sandbox, root, options.maxDepth ?? 4, async (file, info) => {
|
|
184
|
+
if (options.signal?.aborted)
|
|
185
|
+
throw Object.assign(new Error("grep aborted"), { code: "ABORTED" });
|
|
112
186
|
if (!info.isFile())
|
|
113
187
|
return;
|
|
114
188
|
if (isBinaryName(basename(file)))
|
|
115
189
|
return;
|
|
190
|
+
if (globRe) {
|
|
191
|
+
const rel = relative(root, file).replace(/\\/g, "/");
|
|
192
|
+
const target = options.glob && options.glob.includes("/") ? rel : basename(file);
|
|
193
|
+
if (!globRe.test(target))
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
116
196
|
if (info.size > maxBytes) {
|
|
117
197
|
out.line(`${file}: skipped (${formatSize(info.size)} exceeds ${formatSize(maxBytes)})`);
|
|
118
198
|
return !out.full;
|
|
119
199
|
}
|
|
200
|
+
filesScanned += 1;
|
|
120
201
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
202
|
+
const result = await readTextFileResult(file, maxBytes);
|
|
203
|
+
if (result.binary)
|
|
204
|
+
return;
|
|
205
|
+
const lines = result.text.split(/\r?\n/);
|
|
206
|
+
const printed = new Set<number>();
|
|
207
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
208
|
+
if (!re.test(lines[index]))
|
|
209
|
+
continue;
|
|
210
|
+
matchCount += 1;
|
|
211
|
+
const start = Math.max(0, index - context);
|
|
212
|
+
const end = Math.min(lines.length - 1, index + context);
|
|
213
|
+
for (let cursor = start; cursor <= end; cursor += 1) {
|
|
214
|
+
if (printed.has(cursor) && cursor !== index)
|
|
215
|
+
continue;
|
|
216
|
+
printed.add(cursor);
|
|
217
|
+
const isMatch = cursor === index;
|
|
218
|
+
out.line(`${file}:${cursor + 1}${isMatch ? ": " : "- "}${lines[cursor]}`);
|
|
219
|
+
if (out.full) {
|
|
220
|
+
truncated = true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (matchCount >= maxResults) {
|
|
225
|
+
truncated = true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
127
229
|
}
|
|
128
|
-
catch {
|
|
230
|
+
catch (error: any) {
|
|
231
|
+
if (error?.code === "ABORTED")
|
|
232
|
+
throw error;
|
|
129
233
|
// Binary or unreadable files are skipped.
|
|
130
234
|
}
|
|
131
235
|
return !out.full;
|
|
132
236
|
});
|
|
133
|
-
|
|
237
|
+
if (out.full)
|
|
238
|
+
truncated = true;
|
|
239
|
+
return { text: out.value(), truncated, matchCount, filesScanned };
|
|
134
240
|
}
|
|
135
241
|
|
|
136
|
-
|
|
242
|
+
function globToRegExp(glob: string): RegExp {
|
|
243
|
+
let source = "^";
|
|
244
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
245
|
+
const char = glob[i];
|
|
246
|
+
if (char === "*") {
|
|
247
|
+
if (glob[i + 1] === "*") {
|
|
248
|
+
source += ".*";
|
|
249
|
+
i += 1;
|
|
250
|
+
if (glob[i + 1] === "/")
|
|
251
|
+
i += 1;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
source += "[^/]*";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (char === "?") {
|
|
258
|
+
source += "[^/]";
|
|
259
|
+
}
|
|
260
|
+
else if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
261
|
+
source += "\\" + char;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
source += char;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return new RegExp(source + "$", "i");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface ReadTextResult {
|
|
271
|
+
text: string;
|
|
272
|
+
truncated: boolean;
|
|
273
|
+
totalLines?: number;
|
|
274
|
+
returnedLines: number;
|
|
275
|
+
totalBytes: number;
|
|
276
|
+
returnedBytes: number;
|
|
277
|
+
binary: boolean;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function readTextFileResult(file: string, limit = readMaxBytes()): Promise<ReadTextResult> {
|
|
137
281
|
const info = await stat(file);
|
|
138
282
|
if (!info.isFile())
|
|
139
|
-
throw new Error(`not a file: ${file}`);
|
|
283
|
+
throw Object.assign(new Error(`not a file: ${file}`), { code: "EISDIR" });
|
|
140
284
|
const length = Math.min(info.size, limit);
|
|
141
285
|
const handle = await open(file, "r");
|
|
142
286
|
try {
|
|
143
287
|
const buffer = Buffer.alloc(length);
|
|
144
288
|
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
145
289
|
const slice = buffer.subarray(0, bytesRead);
|
|
146
|
-
if (slice.subarray(0, Math.min(8192, slice.length)).includes(0))
|
|
147
|
-
return
|
|
290
|
+
if (slice.subarray(0, Math.min(8192, slice.length)).includes(0)) {
|
|
291
|
+
return {
|
|
292
|
+
text: `[binary file omitted: ${file} (${info.size} bytes)]`,
|
|
293
|
+
truncated: false,
|
|
294
|
+
totalLines: undefined,
|
|
295
|
+
returnedLines: 0,
|
|
296
|
+
totalBytes: info.size,
|
|
297
|
+
returnedBytes: 0,
|
|
298
|
+
binary: true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
148
301
|
const text = slice.toString("utf8");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
302
|
+
const truncated = info.size > length;
|
|
303
|
+
const returnedLines = text ? text.split(/\r?\n/).length : 0;
|
|
304
|
+
return {
|
|
305
|
+
text,
|
|
306
|
+
truncated,
|
|
307
|
+
totalLines: truncated ? undefined : returnedLines,
|
|
308
|
+
returnedLines,
|
|
309
|
+
totalBytes: info.size,
|
|
310
|
+
returnedBytes: Buffer.byteLength(text, "utf8"),
|
|
311
|
+
binary: false,
|
|
312
|
+
};
|
|
152
313
|
}
|
|
153
314
|
finally {
|
|
154
315
|
await handle.close();
|
|
155
316
|
}
|
|
156
317
|
}
|
|
157
318
|
|
|
319
|
+
export interface ReadRangeResult {
|
|
320
|
+
text: string;
|
|
321
|
+
truncated: boolean;
|
|
322
|
+
totalLines: number;
|
|
323
|
+
returnedLines: number;
|
|
324
|
+
totalBytes: number;
|
|
325
|
+
returnedBytes: number;
|
|
326
|
+
scannedAll: boolean;
|
|
327
|
+
binary: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function readFileRange(file: string, offset: number, limit: number, options: { maxBytes?: number; scanMaxBytes?: number; signal?: AbortSignal } = {}): Promise<ReadRangeResult> {
|
|
331
|
+
const info = await stat(file);
|
|
332
|
+
if (!info.isFile())
|
|
333
|
+
throw Object.assign(new Error(`not a file: ${file}`), { code: "EISDIR" });
|
|
334
|
+
const probe = await readTextFileResult(file, 8192);
|
|
335
|
+
if (probe.binary) {
|
|
336
|
+
return {
|
|
337
|
+
text: probe.text,
|
|
338
|
+
truncated: false,
|
|
339
|
+
totalLines: 0,
|
|
340
|
+
returnedLines: 0,
|
|
341
|
+
totalBytes: info.size,
|
|
342
|
+
returnedBytes: 0,
|
|
343
|
+
scannedAll: true,
|
|
344
|
+
binary: true,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const start = Math.max(1, Math.floor(offset));
|
|
348
|
+
const count = Math.max(1, Math.floor(limit));
|
|
349
|
+
const maxBytes = options.maxBytes ?? readMaxBytes();
|
|
350
|
+
const scanMaxBytes = options.scanMaxBytes ?? readScanMaxBytes();
|
|
351
|
+
const collected: string[] = [];
|
|
352
|
+
let lineNo = 0;
|
|
353
|
+
let returnedBytes = 0;
|
|
354
|
+
let totalBytes = 0;
|
|
355
|
+
let truncated = false;
|
|
356
|
+
let scannedAll = true;
|
|
357
|
+
let sawRange = false;
|
|
358
|
+
const stream = createReadStream(file, { encoding: "utf8" });
|
|
359
|
+
const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
360
|
+
try {
|
|
361
|
+
for await (const line of lines) {
|
|
362
|
+
if (options.signal?.aborted)
|
|
363
|
+
throw Object.assign(new Error("read aborted"), { code: "ABORTED" });
|
|
364
|
+
lineNo += 1;
|
|
365
|
+
totalBytes += Buffer.byteLength(line, "utf8") + 1;
|
|
366
|
+
if (lineNo >= start && collected.length < count) {
|
|
367
|
+
sawRange = true;
|
|
368
|
+
collected.push(line);
|
|
369
|
+
returnedBytes += Buffer.byteLength(line, "utf8") + 1;
|
|
370
|
+
if (returnedBytes > maxBytes) {
|
|
371
|
+
truncated = true;
|
|
372
|
+
scannedAll = false;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (lineNo > start + count - 1) {
|
|
377
|
+
truncated = true;
|
|
378
|
+
}
|
|
379
|
+
if (totalBytes > scanMaxBytes) {
|
|
380
|
+
truncated = true;
|
|
381
|
+
scannedAll = false;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
lines.close();
|
|
388
|
+
stream.destroy();
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
text: collected.join("\n"),
|
|
392
|
+
truncated,
|
|
393
|
+
totalLines: lineNo,
|
|
394
|
+
returnedLines: sawRange ? collected.length : 0,
|
|
395
|
+
totalBytes: info.size,
|
|
396
|
+
returnedBytes,
|
|
397
|
+
scannedAll,
|
|
398
|
+
binary: false,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function readTextFile(file: string, limit = readMaxBytes()): Promise<string> {
|
|
403
|
+
const result = await readTextFileResult(file, limit);
|
|
404
|
+
return result.truncated
|
|
405
|
+
? `${result.text}\n[... truncated at ${result.returnedBytes} of ${result.totalBytes} bytes; use read_file offset/limit, grep, or a narrower file ...]`
|
|
406
|
+
: result.text;
|
|
407
|
+
}
|
|
408
|
+
|
|
158
409
|
export function readMaxBytes(): number {
|
|
159
410
|
const raw = Number(process.env["FASTCODE_READ_MAX_BYTES"]);
|
|
160
411
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 200_000;
|
|
161
412
|
}
|
|
162
413
|
|
|
163
|
-
function grepMaxBytes(): number {
|
|
414
|
+
export function grepMaxBytes(): number {
|
|
164
415
|
const raw = Number(process.env["FASTCODE_GREP_MAX_BYTES"]);
|
|
165
416
|
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 1_000_000;
|
|
166
417
|
}
|
|
167
418
|
|
|
419
|
+
export function readScanMaxBytes(): number {
|
|
420
|
+
const raw = Number(process.env["FASTCODE_READ_SCAN_MAX_BYTES"]);
|
|
421
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 8_000_000;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function readManyMaxBytes(): number {
|
|
425
|
+
const raw = Number(process.env["FASTCODE_READ_MANY_MAX_BYTES"]);
|
|
426
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 400_000;
|
|
427
|
+
}
|
|
428
|
+
|
|
168
429
|
async function walkWithin(sandbox: string, root: string, maxDepth: number, visit: (file: string, info: any) => Promise<boolean | void>): Promise<void> {
|
|
169
430
|
const roots = allowedRoots(sandbox).map((entry) => realpathOf(entry)).filter(Boolean) as string[];
|
|
170
431
|
const ignore = createIgnore(sandbox);
|
|
@@ -192,6 +453,8 @@ async function walk(node: string, maxDepth: number, visit: (file: string, info:
|
|
|
192
453
|
return;
|
|
193
454
|
state.visited += 1;
|
|
194
455
|
const insideSandbox = !state.roots || state.roots.length === 0 || isInsideAny(real, state.roots);
|
|
456
|
+
if (!insideSandbox)
|
|
457
|
+
return;
|
|
195
458
|
let keepGoing: boolean | void = true;
|
|
196
459
|
try {
|
|
197
460
|
keepGoing = await visit(node, info);
|
|
@@ -201,7 +464,7 @@ async function walk(node: string, maxDepth: number, visit: (file: string, info:
|
|
|
201
464
|
}
|
|
202
465
|
if (keepGoing === false)
|
|
203
466
|
return false;
|
|
204
|
-
if (!info.isDirectory() || depth >= maxDepth
|
|
467
|
+
if (!info.isDirectory() || depth >= maxDepth)
|
|
205
468
|
return;
|
|
206
469
|
let entries;
|
|
207
470
|
try {
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { readFile, writeFile } 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 { invalid, optionalNumber, preparePath, requiredString } from "./validate.js";
|
|
5
|
+
|
|
6
|
+
function countOccurrences(source: string, find: string): number {
|
|
7
|
+
if (!find)
|
|
8
|
+
return 0;
|
|
9
|
+
return source.split(find).length - 1;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
export const replaceInFileTool: Tool = {
|
|
7
13
|
name: "replace_in_file",
|
|
@@ -11,8 +17,13 @@ export const replaceInFileTool: Tool = {
|
|
|
11
17
|
prepare(call, ctx) {
|
|
12
18
|
try {
|
|
13
19
|
requiredString(call, "path");
|
|
14
|
-
requiredString(call, "find");
|
|
20
|
+
const find = requiredString(call, "find");
|
|
15
21
|
requiredString(call, "replace");
|
|
22
|
+
if (!find)
|
|
23
|
+
return invalid("find must not be empty");
|
|
24
|
+
const expected = optionalNumber(call, "expectedMatches");
|
|
25
|
+
if (expected !== undefined && (expected < 1 || !Number.isInteger(expected)))
|
|
26
|
+
return invalid("expectedMatches must be a positive integer");
|
|
16
27
|
}
|
|
17
28
|
catch (err: any) {
|
|
18
29
|
return invalid(err.message);
|
|
@@ -23,10 +34,33 @@ export const replaceInFileTool: Tool = {
|
|
|
23
34
|
const file = resolvePath(ctx.cwd, call);
|
|
24
35
|
const find = requiredString(call, "find");
|
|
25
36
|
const replacement = requiredString(call, "replace");
|
|
37
|
+
const expected = optionalNumber(call, "expectedMatches");
|
|
26
38
|
const source = await readFile(file, "utf8");
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
const count = countOccurrences(source, find);
|
|
40
|
+
if (count === 0) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
output: `text not found in ${file}`,
|
|
44
|
+
code: "NOT_FOUND",
|
|
45
|
+
hint: "Read the file first and pass the exact current text.",
|
|
46
|
+
matches: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (expected !== undefined && count !== expected) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
output: `expected ${expected} match(es) but found ${count} in ${file}; file unchanged`,
|
|
53
|
+
code: "INVALID_ARGS",
|
|
54
|
+
hint: "Update find or expectedMatches so the replacement is explicit.",
|
|
55
|
+
matches: count,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
29
58
|
await writeFile(file, source.split(find).join(replacement), "utf8");
|
|
30
|
-
return {
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
output: `replaced ${count} occurrence(s) in ${file}`,
|
|
62
|
+
matches: count,
|
|
63
|
+
sideEffects: { touched: [file] },
|
|
64
|
+
};
|
|
31
65
|
},
|
|
32
66
|
};
|
package/src/agent/tools/shell.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ShellExecResult } from "../shell-session.js";
|
|
2
2
|
import { executeShellCommand, pollShellSession } from "../shell-session.js";
|
|
3
3
|
import type { Tool } from "./types.js";
|
|
4
|
-
import { invalid, numberOption } from "./validate.js";
|
|
4
|
+
import { invalid, normalizeTimeoutMs, numberOption } from "./validate.js";
|
|
5
5
|
|
|
6
6
|
export const shellTool: Tool = {
|
|
7
7
|
name: "shell",
|
|
@@ -26,7 +26,7 @@ export const shellTool: Tool = {
|
|
|
26
26
|
const result = await executeShellCommand(call.command, ctx.cwd, {
|
|
27
27
|
signal: ctx.signal,
|
|
28
28
|
turn: ctx.turn,
|
|
29
|
-
timeoutMs:
|
|
29
|
+
timeoutMs: normalizeTimeoutMs(call.timeoutMs),
|
|
30
30
|
});
|
|
31
31
|
return formatExec(result);
|
|
32
32
|
},
|
package/src/agent/tools/types.ts
CHANGED
|
@@ -2,12 +2,39 @@ export type StateScope = "call" | "turn" | "session";
|
|
|
2
2
|
|
|
3
3
|
export type RevalidateKey = "trust" | "sandboxRoots" | "schema" | "modelCaps";
|
|
4
4
|
|
|
5
|
+
export type ToolErrorCode =
|
|
6
|
+
| "ENOENT"
|
|
7
|
+
| "EACCES"
|
|
8
|
+
| "EEXIST"
|
|
9
|
+
| "ENOTDIR"
|
|
10
|
+
| "EISDIR"
|
|
11
|
+
| "EXDEV"
|
|
12
|
+
| "INVALID_ARGS"
|
|
13
|
+
| "SANDBOX_DENY"
|
|
14
|
+
| "APPROVAL_DENY"
|
|
15
|
+
| "EGRESS_DENY"
|
|
16
|
+
| "NOT_FOUND"
|
|
17
|
+
| "ABORTED"
|
|
18
|
+
| "UNKNOWN";
|
|
19
|
+
|
|
5
20
|
export interface SideEffects {
|
|
6
21
|
touched?: string[];
|
|
7
22
|
created?: string[];
|
|
8
23
|
removed?: string[];
|
|
9
24
|
}
|
|
10
25
|
|
|
26
|
+
export interface EgressAudit {
|
|
27
|
+
outlet: string;
|
|
28
|
+
target?: string;
|
|
29
|
+
rule: string;
|
|
30
|
+
mode: "audit" | "block";
|
|
31
|
+
wouldBlock: boolean;
|
|
32
|
+
code: "EGRESS_DENY";
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
import type { ApprovalResolver } from "../approval.js";
|
|
37
|
+
|
|
11
38
|
export interface ToolCall {
|
|
12
39
|
tool: string;
|
|
13
40
|
[key: string]: any;
|
|
@@ -16,6 +43,17 @@ export interface ToolCall {
|
|
|
16
43
|
export interface ToolResult {
|
|
17
44
|
ok: boolean;
|
|
18
45
|
output: string;
|
|
46
|
+
code?: ToolErrorCode;
|
|
47
|
+
hint?: string;
|
|
48
|
+
truncated?: boolean;
|
|
49
|
+
totalLines?: number;
|
|
50
|
+
returnedLines?: number;
|
|
51
|
+
totalBytes?: number;
|
|
52
|
+
returnedBytes?: number;
|
|
53
|
+
filesRead?: number;
|
|
54
|
+
filesSkipped?: number;
|
|
55
|
+
matches?: number;
|
|
56
|
+
egress?: EgressAudit;
|
|
19
57
|
sideEffects?: SideEffects;
|
|
20
58
|
}
|
|
21
59
|
|
|
@@ -23,9 +61,13 @@ export interface ToolCtx {
|
|
|
23
61
|
cwd: string;
|
|
24
62
|
signal?: AbortSignal;
|
|
25
63
|
turn?: number;
|
|
64
|
+
requestApproval?: ApprovalResolver;
|
|
65
|
+
catalogHash?: string;
|
|
26
66
|
}
|
|
27
67
|
|
|
28
|
-
export type Prepared =
|
|
68
|
+
export type Prepared =
|
|
69
|
+
| { ok: true }
|
|
70
|
+
| { ok: false; output: string; code?: ToolErrorCode; hint?: string };
|
|
29
71
|
|
|
30
72
|
export interface ApprovalHints {
|
|
31
73
|
mayRequireApproval: boolean;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { debugLog } from "../../lib/debug.js";
|
|
1
2
|
import { sandboxPath } from "./paths.js";
|
|
2
|
-
import type { Prepared, ToolCall } from "./types.js";
|
|
3
|
+
import type { Prepared, ToolCall, ToolErrorCode } from "./types.js";
|
|
3
4
|
|
|
4
|
-
export function invalid(output: string): Prepared {
|
|
5
|
-
return { ok: false, output };
|
|
5
|
+
export function invalid(output: string, code: ToolErrorCode = "INVALID_ARGS", hint?: string): Prepared {
|
|
6
|
+
return { ok: false, output, code, ...(hint ? { hint } : {}) };
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function requiredString(call: ToolCall, key: string): string {
|
|
@@ -24,12 +25,80 @@ export function numberOption(value: any, fallback: number): number {
|
|
|
24
25
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
export function optionalNumber(call: ToolCall, key: string): number | undefined {
|
|
29
|
+
const value = call[key];
|
|
30
|
+
if (value === undefined || value === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
33
|
+
throw new Error(`${key} must be a finite number`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clampInt(value: number | undefined, fallback: number, min: number, max: number): number {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return fallback;
|
|
40
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function booleanOption(value: any, fallback = false): boolean {
|
|
44
|
+
return typeof value === "boolean" ? value : fallback;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const MAX_TIMEOUT_MS = 2147483647;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize a tool `timeoutMs` value.
|
|
51
|
+
*
|
|
52
|
+
* Only `null`/`undefined` mean "not specified". Any other non-number, NaN,
|
|
53
|
+
* Infinity, or negative value is a caller error and must be surfaced instead
|
|
54
|
+
* of silently falling back. Large finite values are clamped to the
|
|
55
|
+
* `setTimeout` ceiling (2^31-1) rather than overflowing to 1ms.
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeTimeoutMs(value: unknown, fallback = 120000): number {
|
|
58
|
+
if (value === undefined || value === null)
|
|
59
|
+
return fallback;
|
|
60
|
+
if (typeof value !== "number") {
|
|
61
|
+
throw new TypeError(`timeoutMs must be a finite number; received ${describeValue(value)}`);
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(value)) {
|
|
64
|
+
throw new TypeError(`timeoutMs must be a finite number; received ${describeValue(value)}`);
|
|
65
|
+
}
|
|
66
|
+
if (value < 0) {
|
|
67
|
+
throw new TypeError(`timeoutMs must not be negative; received ${describeValue(value)}`);
|
|
68
|
+
}
|
|
69
|
+
const floored = Math.floor(value);
|
|
70
|
+
const clamped = Math.min(floored, MAX_TIMEOUT_MS);
|
|
71
|
+
if (clamped !== floored)
|
|
72
|
+
debugLog("timeout", `timeoutMs ${value} clamped to ${clamped}`);
|
|
73
|
+
return clamped;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function describeValue(value: unknown): string {
|
|
77
|
+
if (typeof value === "string")
|
|
78
|
+
return `string ${JSON.stringify(value)}`;
|
|
79
|
+
if (typeof value === "number")
|
|
80
|
+
return String(value);
|
|
81
|
+
if (typeof value === "bigint")
|
|
82
|
+
return `bigint ${value}n`;
|
|
83
|
+
if (typeof value === "object" && value !== null) {
|
|
84
|
+
try {
|
|
85
|
+
return `object ${JSON.stringify(value)}`;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return "object";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return String(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
27
94
|
export function preparePath(cwd: string, raw: string): Prepared {
|
|
28
95
|
try {
|
|
29
96
|
sandboxPath(cwd, raw);
|
|
30
97
|
return { ok: true };
|
|
31
98
|
}
|
|
32
99
|
catch (err: any) {
|
|
33
|
-
|
|
100
|
+
const message = err?.message ?? String(err);
|
|
101
|
+
const code = /outside sandbox/i.test(message) ? "SANDBOX_DENY" : "INVALID_ARGS";
|
|
102
|
+
return invalid(message, code, /outside sandbox/i.test(message) ? "Use a path inside the project or an allowed root." : undefined);
|
|
34
103
|
}
|
|
35
104
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname } from "node:path";
|
|
3
4
|
import { resolvePath } from "./paths.js";
|
|
@@ -21,8 +22,9 @@ export const writeFileTool: 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 writeFile(file, requiredString(call, "content"), "utf8");
|
|
26
|
-
return { ok: true, output: `wrote ${file}`, sideEffects: { touched: [file] } };
|
|
28
|
+
return { ok: true, output: `wrote ${file}`, sideEffects: existed ? { touched: [file] } : { created: [file] } };
|
|
27
29
|
},
|
|
28
30
|
};
|