@theokit/sdk-tools 0.20.0 → 0.21.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/CHANGELOG.md +15 -0
- package/dist/index.cjs +141 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +114 -146
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -14,9 +14,15 @@ import { InteractiveProvider } from '@theokit/sdk/interactive';
|
|
|
14
14
|
* security-checked) before ANY write. A parse error, context mismatch, or path violation anywhere ⇒ typed
|
|
15
15
|
* error and ZERO writes (stronger than Codex, which writes file-by-file and can leave partial writes).
|
|
16
16
|
*
|
|
17
|
-
* Return shape (always a JSON string):
|
|
17
|
+
* Return shape (always a JSON string — never throws on a bad patch):
|
|
18
18
|
* - `{ ok: true, files_patched: string[] }`
|
|
19
|
-
* - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' |
|
|
19
|
+
* - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' |
|
|
20
|
+
* 'patch_failed' | 'duplicate_target' | 'file_exists' | 'io_error' }`
|
|
21
|
+
*
|
|
22
|
+
* Security/robustness (M18 review): forbidden secrets (`.env`/`.git`/`node_modules`/`.theo`) are blocked
|
|
23
|
+
* at ANY path depth and against absolute-path spelling; a file touched by two hunks is rejected
|
|
24
|
+
* (`duplicate_target`); Add over an existing file is rejected (`file_exists`); Delete of a missing file
|
|
25
|
+
* is `not_found`; any unexpected fs error maps to `io_error` (the handler never throws).
|
|
20
26
|
*/
|
|
21
27
|
|
|
22
28
|
interface CreateApplyPatchToolOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,15 @@ import { InteractiveProvider } from '@theokit/sdk/interactive';
|
|
|
14
14
|
* security-checked) before ANY write. A parse error, context mismatch, or path violation anywhere ⇒ typed
|
|
15
15
|
* error and ZERO writes (stronger than Codex, which writes file-by-file and can leave partial writes).
|
|
16
16
|
*
|
|
17
|
-
* Return shape (always a JSON string):
|
|
17
|
+
* Return shape (always a JSON string — never throws on a bad patch):
|
|
18
18
|
* - `{ ok: true, files_patched: string[] }`
|
|
19
|
-
* - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' |
|
|
19
|
+
* - `{ ok: false, error: 'parse_error' | 'path_traversal' | 'forbidden_path' | 'not_found' |
|
|
20
|
+
* 'patch_failed' | 'duplicate_target' | 'file_exists' | 'io_error' }`
|
|
21
|
+
*
|
|
22
|
+
* Security/robustness (M18 review): forbidden secrets (`.env`/`.git`/`node_modules`/`.theo`) are blocked
|
|
23
|
+
* at ANY path depth and against absolute-path spelling; a file touched by two hunks is rejected
|
|
24
|
+
* (`duplicate_target`); Add over an existing file is rejected (`file_exists`); Delete of a missing file
|
|
25
|
+
* is `not_found`; any unexpected fs error maps to `io_error` (the handler never throws).
|
|
20
26
|
*/
|
|
21
27
|
|
|
22
28
|
interface CreateApplyPatchToolOptions {
|
package/dist/index.js
CHANGED
|
@@ -1,106 +1,18 @@
|
|
|
1
1
|
import { rm, mkdir, writeFile, readFile, readdir, open, stat, copyFile } from 'fs/promises';
|
|
2
|
-
import { dirname,
|
|
2
|
+
import { dirname, relative, join, isAbsolute } from 'path';
|
|
3
3
|
import { Tool, ConfigurationError } from '@theokit/sdk';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import {
|
|
6
|
-
import { safeFilenameForId, safePathJoin as safePathJoin$1 } from '@theokit/sdk/path-safety';
|
|
5
|
+
import { safePathJoin, assertNoSymlinkEscape, PathTraversalError, ForbiddenPathError, isForbiddenPath, safeFilenameForId } from '@theokit/sdk/path-safety';
|
|
7
6
|
import { replaceFileAtomic } from '@theokit/sdk/persistence';
|
|
8
7
|
import { resolveFilesystem, FileNotFoundError, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError } from '@theokit/sdk/filesystem';
|
|
9
8
|
import { spawn } from 'child_process';
|
|
9
|
+
import { existsSync, statSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
|
|
10
10
|
import { resolveSandbox } from '@theokit/sdk/sandbox';
|
|
11
11
|
import { resolveInteractive, InteractiveUnavailableError, NoSuchSessionError } from '@theokit/sdk/interactive';
|
|
12
12
|
import { lookup } from 'dns/promises';
|
|
13
13
|
import { isIP } from 'net';
|
|
14
14
|
|
|
15
15
|
// src/apply-patch.ts
|
|
16
|
-
var PathTraversalError = class extends ConfigurationError {
|
|
17
|
-
name = "PathTraversalError";
|
|
18
|
-
constructor(input, resolvedPath) {
|
|
19
|
-
super(`Path traversal attempt: ${input} \u2192 ${resolvedPath}`, {
|
|
20
|
-
code: "path_traversal"
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
var ForbiddenPathError = class extends ConfigurationError {
|
|
25
|
-
name = "ForbiddenPathError";
|
|
26
|
-
constructor(path) {
|
|
27
|
-
super(
|
|
28
|
-
`Path '${path}' is in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files)`,
|
|
29
|
-
{
|
|
30
|
-
code: "forbidden_path"
|
|
31
|
-
}
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
function safePathJoin(base, ...parts) {
|
|
36
|
-
if (base === "") {
|
|
37
|
-
throw new Error("safePathJoin: base must be non-empty");
|
|
38
|
-
}
|
|
39
|
-
const baseResolved = resolve(base);
|
|
40
|
-
const target = resolve(base, ...parts);
|
|
41
|
-
if (target !== baseResolved && !target.startsWith(baseResolved + sep)) {
|
|
42
|
-
throw new PathTraversalError(parts.join("/"), target);
|
|
43
|
-
}
|
|
44
|
-
return target;
|
|
45
|
-
}
|
|
46
|
-
function assertNoSymlinkEscape(path, base) {
|
|
47
|
-
let baseResolved;
|
|
48
|
-
try {
|
|
49
|
-
baseResolved = realpathSync(base);
|
|
50
|
-
} catch {
|
|
51
|
-
baseResolved = resolve(base);
|
|
52
|
-
}
|
|
53
|
-
const resolved = realpathOfDeepestExisting(path);
|
|
54
|
-
if (resolved === void 0) return;
|
|
55
|
-
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
|
|
56
|
-
throw new PathTraversalError(`symlink ${path}`, resolved);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function realpathOfDeepestExisting(path) {
|
|
60
|
-
try {
|
|
61
|
-
return realpathSync(path);
|
|
62
|
-
} catch {
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const stat2 = lstatSync(path);
|
|
66
|
-
if (stat2.isSymbolicLink()) {
|
|
67
|
-
const target = readlinkSync(path);
|
|
68
|
-
const parentReal = realpathOfDeepestExisting(dirname(path));
|
|
69
|
-
const parentBase = parentReal ?? dirname(path);
|
|
70
|
-
return resolve(parentBase, target);
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
}
|
|
74
|
-
let cursor = dirname(path);
|
|
75
|
-
let suffix = path.slice(cursor.length);
|
|
76
|
-
while (cursor !== dirname(cursor)) {
|
|
77
|
-
try {
|
|
78
|
-
const real = realpathSync(cursor);
|
|
79
|
-
return resolve(real, `.${suffix}`);
|
|
80
|
-
} catch {
|
|
81
|
-
suffix = path.slice(dirname(cursor).length);
|
|
82
|
-
cursor = dirname(cursor);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return void 0;
|
|
86
|
-
}
|
|
87
|
-
var LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
|
|
88
|
-
function isForbiddenPath(input) {
|
|
89
|
-
const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
90
|
-
if (normalized.length === 0) return false;
|
|
91
|
-
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
92
|
-
if (segments.length === 0) return false;
|
|
93
|
-
const first = segments[0];
|
|
94
|
-
if (first === ".env.example") return false;
|
|
95
|
-
if (first === ".env") return true;
|
|
96
|
-
if (/^\.env\./.test(first)) return true;
|
|
97
|
-
if (first === ".git") return true;
|
|
98
|
-
if (first === "node_modules") return true;
|
|
99
|
-
if (first === ".theo") return true;
|
|
100
|
-
const basename = segments[segments.length - 1];
|
|
101
|
-
if (LOCK_FILES.has(basename)) return true;
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
16
|
|
|
105
17
|
// src/internal/v4a-patch.ts
|
|
106
18
|
var BEGIN = "*** Begin Patch";
|
|
@@ -265,7 +177,14 @@ function matchesAt(lines, pattern, at, eq) {
|
|
|
265
177
|
function seekSequence(lines, pattern, searchStart, eof) {
|
|
266
178
|
if (pattern.length === 0) return searchStart;
|
|
267
179
|
if (pattern.length > lines.length) return null;
|
|
268
|
-
const
|
|
180
|
+
const starts = eof ? [Math.max(searchStart, lines.length - pattern.length), searchStart] : [searchStart];
|
|
181
|
+
for (const start of starts) {
|
|
182
|
+
const hit = searchFrom(lines, pattern, start);
|
|
183
|
+
if (hit !== null) return hit;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function searchFrom(lines, pattern, start) {
|
|
269
188
|
for (const eq of LADDER) {
|
|
270
189
|
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
271
190
|
if (matchesAt(lines, pattern, i, eq)) return i;
|
|
@@ -322,12 +241,28 @@ async function applyV4APatch(projectRoot, patch) {
|
|
|
322
241
|
const detail = err instanceof V4APatchError ? err.message : String(err);
|
|
323
242
|
return JSON.stringify({ ok: false, error: "parse_error", detail });
|
|
324
243
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
244
|
+
try {
|
|
245
|
+
const plan = await buildPlan(projectRoot, hunks);
|
|
246
|
+
if ("error" in plan) return plan.error;
|
|
247
|
+
await executePlan(plan.ops);
|
|
248
|
+
return JSON.stringify({ ok: true, files_patched: plan.patched });
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return JSON.stringify({
|
|
251
|
+
ok: false,
|
|
252
|
+
error: "io_error",
|
|
253
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function hunkTargets(hunk) {
|
|
258
|
+
if (hunk.kind === "update" && hunk.movePath) return [hunk.path, hunk.movePath];
|
|
259
|
+
return [hunk.path];
|
|
329
260
|
}
|
|
330
261
|
async function buildPlan(projectRoot, hunks) {
|
|
262
|
+
const dup = firstDuplicateTarget(hunks);
|
|
263
|
+
if (dup !== null) {
|
|
264
|
+
return { error: JSON.stringify({ ok: false, error: "duplicate_target", path: dup }) };
|
|
265
|
+
}
|
|
331
266
|
const ops = [];
|
|
332
267
|
const patched = [];
|
|
333
268
|
for (const hunk of hunks) {
|
|
@@ -338,6 +273,16 @@ async function buildPlan(projectRoot, hunks) {
|
|
|
338
273
|
}
|
|
339
274
|
return { ops, patched };
|
|
340
275
|
}
|
|
276
|
+
function firstDuplicateTarget(hunks) {
|
|
277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const hunk of hunks) {
|
|
279
|
+
for (const t of hunkTargets(hunk)) {
|
|
280
|
+
if (seen.has(t)) return t;
|
|
281
|
+
seen.add(t);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
341
286
|
async function executePlan(ops) {
|
|
342
287
|
for (const op of ops) {
|
|
343
288
|
if ("rm" in op) {
|
|
@@ -348,11 +293,29 @@ async function executePlan(ops) {
|
|
|
348
293
|
}
|
|
349
294
|
}
|
|
350
295
|
}
|
|
296
|
+
async function pathExists(abs) {
|
|
297
|
+
try {
|
|
298
|
+
await stat(abs);
|
|
299
|
+
return true;
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
351
304
|
async function planHunk(projectRoot, hunk) {
|
|
352
305
|
const scope = v4aScope(projectRoot, hunk.path);
|
|
353
306
|
if ("error" in scope) return scope;
|
|
354
|
-
if (hunk.kind === "add")
|
|
355
|
-
|
|
307
|
+
if (hunk.kind === "add") {
|
|
308
|
+
if (await pathExists(scope.abs)) {
|
|
309
|
+
return { error: JSON.stringify({ ok: false, error: "file_exists", path: hunk.path }) };
|
|
310
|
+
}
|
|
311
|
+
return { ops: [{ write: { abs: scope.abs, content: hunk.content } }] };
|
|
312
|
+
}
|
|
313
|
+
if (hunk.kind === "delete") {
|
|
314
|
+
if (!await pathExists(scope.abs)) {
|
|
315
|
+
return { error: JSON.stringify({ ok: false, error: "not_found", path: hunk.path }) };
|
|
316
|
+
}
|
|
317
|
+
return { ops: [{ rm: scope.abs }] };
|
|
318
|
+
}
|
|
356
319
|
return planUpdate(projectRoot, hunk, scope.abs);
|
|
357
320
|
}
|
|
358
321
|
async function planUpdate(projectRoot, hunk, abs) {
|
|
@@ -386,27 +349,32 @@ async function planUpdate(projectRoot, hunk, abs) {
|
|
|
386
349
|
if ("error" in dest) return dest;
|
|
387
350
|
return { ops: [{ write: { abs: dest.abs, content: updated } }, { rm: abs }] };
|
|
388
351
|
}
|
|
352
|
+
var FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
|
|
353
|
+
function isForbiddenRel(rel) {
|
|
354
|
+
return rel.split(/[/\\]/).filter(Boolean).some((s) => s !== ".env.example" && (FORBIDDEN_SEGMENTS.has(s) || /^\.env\./.test(s)));
|
|
355
|
+
}
|
|
389
356
|
function v4aScope(projectRoot, file) {
|
|
390
|
-
|
|
391
|
-
return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
|
|
392
|
-
}
|
|
357
|
+
let abs;
|
|
393
358
|
try {
|
|
394
|
-
|
|
359
|
+
abs = safePathJoin(projectRoot, file);
|
|
395
360
|
assertNoSymlinkEscape(abs, projectRoot);
|
|
396
|
-
return { abs };
|
|
397
361
|
} catch (err) {
|
|
398
362
|
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
399
363
|
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: file }) };
|
|
400
364
|
}
|
|
401
365
|
throw err;
|
|
402
366
|
}
|
|
367
|
+
if (isForbiddenPath(file) || isForbiddenRel(relative(projectRoot, abs))) {
|
|
368
|
+
return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
|
|
369
|
+
}
|
|
370
|
+
return { abs };
|
|
403
371
|
}
|
|
404
372
|
function createSessionArtifactStore(options) {
|
|
405
373
|
const { dir } = options;
|
|
406
374
|
const idStrategy = options.idStrategy ?? ((id) => safeFilenameForId(id));
|
|
407
375
|
const extension = options.extension ?? ".md";
|
|
408
376
|
function path(id) {
|
|
409
|
-
return safePathJoin
|
|
377
|
+
return safePathJoin(dir, `${idStrategy(id)}${extension}`);
|
|
410
378
|
}
|
|
411
379
|
async function write(id, content) {
|
|
412
380
|
const target = path(id);
|
|
@@ -742,25 +710,25 @@ function createSettleGate(timer) {
|
|
|
742
710
|
}
|
|
743
711
|
};
|
|
744
712
|
}
|
|
745
|
-
function armTimeoutKill(child, timeoutMs, onTimeout,
|
|
713
|
+
function armTimeoutKill(child, timeoutMs, onTimeout, resolve) {
|
|
746
714
|
const timer = setTimeout(() => {
|
|
747
715
|
gate.fire(() => {
|
|
748
716
|
try {
|
|
749
717
|
process.kill(-(child.pid ?? 0), "SIGKILL");
|
|
750
718
|
} catch {
|
|
751
719
|
}
|
|
752
|
-
|
|
720
|
+
resolve(onTimeout());
|
|
753
721
|
});
|
|
754
722
|
}, timeoutMs);
|
|
755
723
|
const gate = createSettleGate(timer);
|
|
756
724
|
return gate;
|
|
757
725
|
}
|
|
758
|
-
function attachChildSettlers(child, gate, onClose, onError,
|
|
726
|
+
function attachChildSettlers(child, gate, onClose, onError, resolve) {
|
|
759
727
|
child.on("close", (code) => {
|
|
760
|
-
gate.fire(() =>
|
|
728
|
+
gate.fire(() => resolve(onClose(code)));
|
|
761
729
|
});
|
|
762
730
|
child.on("error", (err) => {
|
|
763
|
-
gate.fire(() =>
|
|
731
|
+
gate.fire(() => resolve(onError(err)));
|
|
764
732
|
});
|
|
765
733
|
}
|
|
766
734
|
|
|
@@ -827,7 +795,7 @@ function formatGitResult(result, timeoutMs) {
|
|
|
827
795
|
return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
|
|
828
796
|
}
|
|
829
797
|
function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
830
|
-
return new Promise((
|
|
798
|
+
return new Promise((resolve) => {
|
|
831
799
|
const child = spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
832
800
|
const stdoutChunks = [];
|
|
833
801
|
const stderrChunks = [];
|
|
@@ -837,7 +805,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
837
805
|
child,
|
|
838
806
|
timeoutMs,
|
|
839
807
|
() => ({ kind: "timeout" }),
|
|
840
|
-
|
|
808
|
+
resolve
|
|
841
809
|
);
|
|
842
810
|
child.stdout.on("data", (chunk) => {
|
|
843
811
|
if (gate.settled()) return;
|
|
@@ -867,7 +835,7 @@ function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
|
867
835
|
return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
|
|
868
836
|
},
|
|
869
837
|
(err) => ({ kind: "error", stderr: err.message }),
|
|
870
|
-
|
|
838
|
+
resolve
|
|
871
839
|
);
|
|
872
840
|
});
|
|
873
841
|
}
|
|
@@ -1546,29 +1514,29 @@ function createListDirTool(opts) {
|
|
|
1546
1514
|
path: z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
|
|
1547
1515
|
}),
|
|
1548
1516
|
handler: async ({ path }, ctx) => {
|
|
1549
|
-
const
|
|
1550
|
-
if (
|
|
1517
|
+
const relative3 = path === "" || path === "." ? "." : path;
|
|
1518
|
+
if (relative3 !== "." && isForbiddenPath(relative3)) {
|
|
1551
1519
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
1552
1520
|
}
|
|
1553
1521
|
if (filesystem) {
|
|
1554
1522
|
const backend = await resolveFilesystem(filesystem, ctx ?? {});
|
|
1555
|
-
return listViaBackend(backend,
|
|
1523
|
+
return listViaBackend(backend, relative3, path, max);
|
|
1556
1524
|
}
|
|
1557
|
-
return listViaLocalFs(projectRoot,
|
|
1525
|
+
return listViaLocalFs(projectRoot, relative3, path, max);
|
|
1558
1526
|
}
|
|
1559
1527
|
});
|
|
1560
1528
|
}
|
|
1561
|
-
async function listViaLocalFs(projectRoot,
|
|
1562
|
-
const boundary = resolveDirBoundary(
|
|
1529
|
+
async function listViaLocalFs(projectRoot, relative3, originalPath, max) {
|
|
1530
|
+
const boundary = resolveDirBoundary(relative3, projectRoot, originalPath);
|
|
1563
1531
|
if ("error" in boundary) return boundary.error;
|
|
1564
1532
|
const readResult = await readDirSafe(boundary.absolutePath, originalPath);
|
|
1565
1533
|
if ("error" in readResult) return readResult.error;
|
|
1566
1534
|
return formatListing(readResult.dirents, max);
|
|
1567
1535
|
}
|
|
1568
|
-
async function listViaBackend(backend,
|
|
1536
|
+
async function listViaBackend(backend, relative3, originalPath, max) {
|
|
1569
1537
|
let names;
|
|
1570
1538
|
try {
|
|
1571
|
-
names = await backend.list(
|
|
1539
|
+
names = await backend.list(relative3);
|
|
1572
1540
|
} catch (err) {
|
|
1573
1541
|
if (err instanceof FileNotFoundError) {
|
|
1574
1542
|
return JSON.stringify({ ok: false, error: "not_found", path: originalPath });
|
|
@@ -1582,7 +1550,7 @@ async function listViaBackend(backend, relative2, originalPath, max) {
|
|
|
1582
1550
|
const windowed = names.slice(0, max);
|
|
1583
1551
|
const entries = await Promise.all(
|
|
1584
1552
|
windowed.map(async (name) => {
|
|
1585
|
-
const child =
|
|
1553
|
+
const child = relative3 === "." ? name : `${relative3}/${name}`;
|
|
1586
1554
|
let type = "file";
|
|
1587
1555
|
try {
|
|
1588
1556
|
type = (await backend.stat(child)).isDirectory ? "directory" : "file";
|
|
@@ -1593,9 +1561,9 @@ async function listViaBackend(backend, relative2, originalPath, max) {
|
|
|
1593
1561
|
);
|
|
1594
1562
|
return JSON.stringify({ ok: true, entries, truncated: totalCount > max, totalCount });
|
|
1595
1563
|
}
|
|
1596
|
-
function resolveDirBoundary(
|
|
1564
|
+
function resolveDirBoundary(relative3, projectRoot, originalPath) {
|
|
1597
1565
|
try {
|
|
1598
|
-
const absolutePath =
|
|
1566
|
+
const absolutePath = relative3 === "." ? projectRoot : safePathJoin(projectRoot, relative3);
|
|
1599
1567
|
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
1600
1568
|
return { absolutePath };
|
|
1601
1569
|
} catch (err) {
|
|
@@ -1817,22 +1785,22 @@ function createReadFileTool(opts) {
|
|
|
1817
1785
|
}
|
|
1818
1786
|
async function readViaBackend(backend, path, view, onRead) {
|
|
1819
1787
|
try {
|
|
1820
|
-
const
|
|
1821
|
-
if (
|
|
1788
|
+
const stat3 = await backend.stat(path);
|
|
1789
|
+
if (stat3.size > MAX_FILE_SIZE) {
|
|
1822
1790
|
return JSON.stringify({
|
|
1823
1791
|
ok: false,
|
|
1824
1792
|
error: "too_large",
|
|
1825
1793
|
path,
|
|
1826
|
-
size:
|
|
1794
|
+
size: stat3.size,
|
|
1827
1795
|
limit: MAX_FILE_SIZE
|
|
1828
1796
|
});
|
|
1829
1797
|
}
|
|
1830
1798
|
const raw = await backend.readFile(path);
|
|
1831
1799
|
if (raw.includes("\0")) {
|
|
1832
|
-
return JSON.stringify({ ok: false, error: "binary_file", path, size:
|
|
1800
|
+
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat3.size });
|
|
1833
1801
|
}
|
|
1834
|
-
onRead?.(
|
|
1835
|
-
return JSON.stringify({ ok: true, content: renderView(raw, view), size:
|
|
1802
|
+
onRead?.(stat3.mtimeMs);
|
|
1803
|
+
return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat3.size });
|
|
1836
1804
|
} catch (err) {
|
|
1837
1805
|
if (err instanceof FileNotFoundError) {
|
|
1838
1806
|
return JSON.stringify({ ok: false, error: "not_found", path });
|
|
@@ -1871,22 +1839,22 @@ async function openHandleSafe(absolutePath, path) {
|
|
|
1871
1839
|
}
|
|
1872
1840
|
}
|
|
1873
1841
|
async function readContent(handle, path, view, onRead) {
|
|
1874
|
-
const
|
|
1875
|
-
if (
|
|
1842
|
+
const stat3 = await handle.stat();
|
|
1843
|
+
if (stat3.size > MAX_FILE_SIZE) {
|
|
1876
1844
|
return JSON.stringify({
|
|
1877
1845
|
ok: false,
|
|
1878
1846
|
error: "too_large",
|
|
1879
1847
|
path,
|
|
1880
|
-
size:
|
|
1848
|
+
size: stat3.size,
|
|
1881
1849
|
limit: MAX_FILE_SIZE
|
|
1882
1850
|
});
|
|
1883
1851
|
}
|
|
1884
|
-
if (await isBinaryProbe(handle, Number(
|
|
1885
|
-
return JSON.stringify({ ok: false, error: "binary_file", path, size:
|
|
1852
|
+
if (await isBinaryProbe(handle, Number(stat3.size))) {
|
|
1853
|
+
return JSON.stringify({ ok: false, error: "binary_file", path, size: stat3.size });
|
|
1886
1854
|
}
|
|
1887
1855
|
const raw = await handle.readFile({ encoding: "utf-8" });
|
|
1888
|
-
onRead?.(
|
|
1889
|
-
return JSON.stringify({ ok: true, content: renderView(raw, view), size:
|
|
1856
|
+
onRead?.(stat3.mtimeMs);
|
|
1857
|
+
return JSON.stringify({ ok: true, content: renderView(raw, view), size: stat3.size });
|
|
1890
1858
|
}
|
|
1891
1859
|
async function isBinaryProbe(handle, size) {
|
|
1892
1860
|
const probeLen = Math.min(BINARY_PROBE_BYTES, size);
|
|
@@ -2020,7 +1988,7 @@ function appendCapped(chunks, chunk, current, cap) {
|
|
|
2020
1988
|
return current + chunk.length;
|
|
2021
1989
|
}
|
|
2022
1990
|
function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
|
|
2023
|
-
return new Promise((
|
|
1991
|
+
return new Promise((resolve) => {
|
|
2024
1992
|
const child = spawn(command, args, {
|
|
2025
1993
|
cwd,
|
|
2026
1994
|
detached: true,
|
|
@@ -2033,7 +2001,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
|
|
|
2033
2001
|
child,
|
|
2034
2002
|
timeoutMs,
|
|
2035
2003
|
() => ({ kind: "timeout" }),
|
|
2036
|
-
|
|
2004
|
+
resolve
|
|
2037
2005
|
);
|
|
2038
2006
|
child.stdout.on("data", (chunk) => {
|
|
2039
2007
|
if (gate.settled()) return;
|
|
@@ -2052,7 +2020,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
|
|
|
2052
2020
|
exitCode: code ?? 0
|
|
2053
2021
|
}),
|
|
2054
2022
|
(err) => ({ kind: "spawn_error", message: err.message }),
|
|
2055
|
-
|
|
2023
|
+
resolve
|
|
2056
2024
|
);
|
|
2057
2025
|
});
|
|
2058
2026
|
}
|
|
@@ -2303,7 +2271,7 @@ function createShellTool(opts) {
|
|
|
2303
2271
|
});
|
|
2304
2272
|
}
|
|
2305
2273
|
function runShell(cwd, command, timeoutMs) {
|
|
2306
|
-
return new Promise((
|
|
2274
|
+
return new Promise((resolve) => {
|
|
2307
2275
|
const child = spawn("/bin/sh", ["-c", command], {
|
|
2308
2276
|
cwd,
|
|
2309
2277
|
detached: true,
|
|
@@ -2317,7 +2285,7 @@ function runShell(cwd, command, timeoutMs) {
|
|
|
2317
2285
|
child,
|
|
2318
2286
|
timeoutMs,
|
|
2319
2287
|
() => ({ kind: "timeout" }),
|
|
2320
|
-
(result) =>
|
|
2288
|
+
(result) => resolve(formatResult(result, timeoutMs))
|
|
2321
2289
|
);
|
|
2322
2290
|
child.stdout.on("data", (chunk) => {
|
|
2323
2291
|
if (gate.settled()) return;
|
|
@@ -2355,7 +2323,7 @@ function runShell(cwd, command, timeoutMs) {
|
|
|
2355
2323
|
exitCode: code
|
|
2356
2324
|
}),
|
|
2357
2325
|
(err) => ({ kind: "error", message: err.message }),
|
|
2358
|
-
(result) =>
|
|
2326
|
+
(result) => resolve(formatResult(result, timeoutMs))
|
|
2359
2327
|
);
|
|
2360
2328
|
});
|
|
2361
2329
|
}
|
|
@@ -2802,12 +2770,12 @@ async function writeViaBackend(backend, path, content, guard) {
|
|
|
2802
2770
|
if (rbw) return rbw;
|
|
2803
2771
|
expectedMtime = current ?? void 0;
|
|
2804
2772
|
}
|
|
2805
|
-
const
|
|
2773
|
+
const stat3 = await backend.writeFile(
|
|
2806
2774
|
path,
|
|
2807
2775
|
content,
|
|
2808
2776
|
expectedMtime !== void 0 ? { expectedMtime } : void 0
|
|
2809
2777
|
);
|
|
2810
|
-
return JSON.stringify({ ok: true, path, bytes:
|
|
2778
|
+
return JSON.stringify({ ok: true, path, bytes: stat3.size });
|
|
2811
2779
|
} catch (err) {
|
|
2812
2780
|
return backendErrorToJson(err, path);
|
|
2813
2781
|
}
|
|
@@ -2835,8 +2803,8 @@ async function isBinaryFile(absolutePath) {
|
|
|
2835
2803
|
return false;
|
|
2836
2804
|
}
|
|
2837
2805
|
try {
|
|
2838
|
-
const
|
|
2839
|
-
const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(
|
|
2806
|
+
const stat3 = await handle.stat();
|
|
2807
|
+
const probeLen = Math.min(BINARY_PROBE_BYTES3, Number(stat3.size));
|
|
2840
2808
|
if (probeLen <= 0) return false;
|
|
2841
2809
|
const probe = Buffer.alloc(probeLen);
|
|
2842
2810
|
const { bytesRead } = await handle.read(probe, 0, probeLen, 0);
|