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