@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.
Files changed (165) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +50 -17
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +33 -0
  10. package/dist/agent/approval.js +40 -5
  11. package/dist/agent/compaction.d.ts +2 -0
  12. package/dist/agent/compaction.js +60 -17
  13. package/dist/agent/guards.d.ts +16 -16
  14. package/dist/agent/guards.js +978 -24
  15. package/dist/agent/policy/defaults.d.ts +16 -0
  16. package/dist/agent/policy/defaults.js +17 -0
  17. package/dist/agent/policy/egress.d.ts +32 -0
  18. package/dist/agent/policy/egress.js +280 -0
  19. package/dist/agent/process-tree.d.ts +20 -0
  20. package/dist/agent/process-tree.js +81 -10
  21. package/dist/agent/protocol.d.ts +20 -1
  22. package/dist/agent/protocol.js +165 -38
  23. package/dist/agent/registry.d.ts +3 -1
  24. package/dist/agent/registry.js +168 -11
  25. package/dist/agent/runner.d.ts +21 -0
  26. package/dist/agent/runner.js +44 -5
  27. package/dist/agent/shell-ast.d.ts +11 -0
  28. package/dist/agent/shell-ast.js +204 -0
  29. package/dist/agent/shell-session.d.ts +14 -0
  30. package/dist/agent/shell-session.js +251 -40
  31. package/dist/agent/tool-schemas.js +26 -7
  32. package/dist/agent/tools/append_file.js +3 -1
  33. package/dist/agent/tools/apply_patch.js +44 -4
  34. package/dist/agent/tools/copy_file.js +26 -3
  35. package/dist/agent/tools/delete_file.js +22 -3
  36. package/dist/agent/tools/grep.js +35 -4
  37. package/dist/agent/tools/make_dir.js +3 -1
  38. package/dist/agent/tools/move_path.d.ts +7 -0
  39. package/dist/agent/tools/move_path.js +31 -5
  40. package/dist/agent/tools/read_file.js +63 -3
  41. package/dist/agent/tools/read_many.js +21 -3
  42. package/dist/agent/tools/readers.d.ts +53 -1
  43. package/dist/agent/tools/readers.js +236 -26
  44. package/dist/agent/tools/replace_in_file.js +38 -5
  45. package/dist/agent/tools/shell.js +2 -2
  46. package/dist/agent/tools/types.d.ts +26 -0
  47. package/dist/agent/tools/validate.d.ts +15 -2
  48. package/dist/agent/tools/validate.js +66 -3
  49. package/dist/agent/tools/write_file.js +3 -1
  50. package/dist/app.d.ts +2 -1
  51. package/dist/app.js +526 -85
  52. package/dist/cli-main.js +363 -0
  53. package/dist/cli.js +397 -337
  54. package/dist/commands/about.js +1 -1
  55. package/dist/commands/clear.d.ts +1 -0
  56. package/dist/commands/clear.js +1 -1
  57. package/dist/commands/compact.d.ts +12 -0
  58. package/dist/commands/compact.js +14 -2
  59. package/dist/commands/context.js +5 -4
  60. package/dist/commands/index.js +2 -0
  61. package/dist/commands/jobs.js +3 -2
  62. package/dist/commands/kill.js +3 -1
  63. package/dist/commands/login.d.ts +2 -2
  64. package/dist/commands/login.js +15 -15
  65. package/dist/commands/model.d.ts +1 -13
  66. package/dist/commands/model.js +6 -38
  67. package/dist/commands/provider.d.ts +1 -15
  68. package/dist/commands/provider.js +4 -22
  69. package/dist/commands/skills.js +34 -4
  70. package/dist/commands/threads.d.ts +9 -0
  71. package/dist/commands/threads.js +26 -0
  72. package/dist/config/store.d.ts +21 -0
  73. package/dist/config/store.js +47 -9
  74. package/dist/config/trust.d.ts +9 -0
  75. package/dist/config/trust.js +41 -2
  76. package/dist/headless.d.ts +7 -0
  77. package/dist/headless.js +213 -0
  78. package/dist/index.js +19 -3
  79. package/dist/lib/debug.d.ts +2 -0
  80. package/dist/lib/debug.js +22 -0
  81. package/dist/lib/diagnostics.d.ts +19 -0
  82. package/dist/lib/diagnostics.js +32 -6
  83. package/dist/lib/highlight-subset.d.ts +21 -0
  84. package/dist/lib/highlight-subset.js +88 -0
  85. package/dist/lib/lifecycle.d.ts +5 -0
  86. package/dist/lib/lifecycle.js +62 -0
  87. package/dist/lib/markdown.js +22 -4
  88. package/dist/providers/deepseek.js +1 -1
  89. package/dist/providers/openaiCompatible.d.ts +24 -2
  90. package/dist/providers/openaiCompatible.js +20 -6
  91. package/dist/providers/spec.d.ts +12 -0
  92. package/dist/providers/spec.js +29 -0
  93. package/dist/session/log.d.ts +14 -2
  94. package/dist/session/log.js +56 -27
  95. package/dist/session/projection.d.ts +34 -1
  96. package/dist/session/projection.js +315 -46
  97. package/dist/skills/manager.d.ts +15 -0
  98. package/dist/skills/manager.js +135 -18
  99. package/dist/types.d.ts +15 -0
  100. package/dist/ui/StatusBar.js +1 -1
  101. package/package.json +3 -2
  102. package/src/agent/approval-json.ts +103 -0
  103. package/src/agent/approval-native.ts +168 -0
  104. package/src/agent/approval-store.ts +38 -0
  105. package/src/agent/approval.ts +58 -11
  106. package/src/agent/compaction.ts +62 -17
  107. package/src/agent/guards.ts +1049 -25
  108. package/src/agent/policy/defaults.ts +34 -0
  109. package/src/agent/policy/egress.ts +335 -0
  110. package/src/agent/process-tree.ts +105 -23
  111. package/src/agent/protocol.ts +187 -39
  112. package/src/agent/registry.ts +186 -14
  113. package/src/agent/runner.ts +50 -7
  114. package/src/agent/shell-ast.ts +227 -0
  115. package/src/agent/shell-session.ts +265 -41
  116. package/src/agent/tool-schemas.ts +26 -7
  117. package/src/agent/tools/append_file.ts +3 -1
  118. package/src/agent/tools/apply_patch.ts +45 -4
  119. package/src/agent/tools/copy_file.ts +27 -3
  120. package/src/agent/tools/delete_file.ts +22 -3
  121. package/src/agent/tools/grep.ts +36 -4
  122. package/src/agent/tools/make_dir.ts +3 -1
  123. package/src/agent/tools/move_path.ts +38 -5
  124. package/src/agent/tools/read_file.ts +64 -3
  125. package/src/agent/tools/read_many.ts +21 -3
  126. package/src/agent/tools/readers.ts +289 -26
  127. package/src/agent/tools/replace_in_file.ts +39 -5
  128. package/src/agent/tools/shell.ts +2 -2
  129. package/src/agent/tools/types.ts +43 -1
  130. package/src/agent/tools/validate.ts +73 -4
  131. package/src/agent/tools/write_file.ts +3 -1
  132. package/src/app.ts +533 -82
  133. package/src/{cli.ts → cli-main.ts} +34 -6
  134. package/src/commands/about.ts +1 -1
  135. package/src/commands/clear.ts +1 -1
  136. package/src/commands/compact.ts +14 -2
  137. package/src/commands/context.ts +5 -4
  138. package/src/commands/index.ts +2 -0
  139. package/src/commands/jobs.ts +3 -2
  140. package/src/commands/kill.ts +3 -1
  141. package/src/commands/login.ts +15 -15
  142. package/src/commands/model.ts +6 -38
  143. package/src/commands/provider.ts +4 -22
  144. package/src/commands/skills.ts +34 -4
  145. package/src/commands/threads.ts +28 -0
  146. package/src/config/store.ts +47 -9
  147. package/src/config/trust.ts +45 -2
  148. package/src/headless.ts +225 -0
  149. package/src/index.ts +19 -3
  150. package/src/lib/debug.ts +24 -0
  151. package/src/lib/diagnostics.ts +32 -6
  152. package/src/lib/highlight-subset.ts +93 -0
  153. package/src/lib/lifecycle.ts +69 -0
  154. package/src/lib/markdown.ts +22 -4
  155. package/src/providers/deepseek.ts +1 -1
  156. package/src/providers/openaiCompatible.ts +47 -10
  157. package/src/providers/spec.ts +42 -0
  158. package/src/session/log.ts +70 -27
  159. package/src/session/projection.ts +360 -47
  160. package/src/skills/manager.ts +140 -18
  161. package/src/types/highlight-shims.d.ts +14 -0
  162. package/src/types.ts +16 -1
  163. package/src/ui/StatusBar.ts +1 -1
  164. package/tsconfig.json +1 -0
  165. /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 readMany(cwd, paths) {
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
- blocks.push(`### ${file}\n${await readTextFile(file)}`);
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
- return blocks.join("\n\n");
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 += ` — ${symbols}`;
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, pattern, maxDepth) {
101
- const re = new RegExp(pattern, "i");
102
- const out = createOutputBuffer();
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
- await walkWithin(sandbox, root, maxDepth, async (file, info) => {
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 text = await readTextFile(file, maxBytes);
115
- const lines = text.split(/\r?\n/);
116
- lines.forEach((line, index) => {
117
- if (re.test(line))
118
- out.line(file + ":" + (index + 1) + ": " + line);
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
- return out.value();
203
+ if (out.full)
204
+ truncated = true;
205
+ return { text: out.value(), truncated, matchCount, filesScanned };
127
206
  }
128
- export async function readTextFile(file, limit = readMaxBytes()) {
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 `[binary file omitted: ${file} (${info.size} bytes)]`;
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
- return info.size > length
142
- ? `${text}\n[... truncated at ${length} of ${info.size} bytes; use grep or read a narrower file ...]`
143
- : text;
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 || !insideSandbox)
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
- if (!source.includes(find))
26
- return { ok: false, output: `text not found in ${file}` };
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 { ok: true, output: `replaced text in ${file}`, sideEffects: { touched: [file] } };
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: numberOption(call.timeoutMs, 120000),
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
- return invalid(err?.message ?? String(err));
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
@@ -1,4 +1,5 @@
1
- export declare const App: ({ initialPrompt, resume }: {
1
+ export declare const App: ({ initialPrompt, resume, ui }: {
2
2
  initialPrompt: any;
3
3
  resume: any;
4
+ ui?: string;
4
5
  }) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;