@theokit/sdk-tools 0.20.1 → 0.22.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/dist/index.cjs +178 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -4
- package/dist/index.d.ts +111 -4
- package/dist/index.js +151 -183
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -4,105 +4,17 @@ var promises = require('fs/promises');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var sdk = require('@theokit/sdk');
|
|
6
6
|
var zod = require('zod');
|
|
7
|
-
var fs = require('fs');
|
|
8
7
|
var pathSafety = require('@theokit/sdk/path-safety');
|
|
9
8
|
var persistence = require('@theokit/sdk/persistence');
|
|
10
9
|
var filesystem = require('@theokit/sdk/filesystem');
|
|
11
|
-
var
|
|
10
|
+
var fs = require('fs');
|
|
12
11
|
var sandbox = require('@theokit/sdk/sandbox');
|
|
12
|
+
var child_process = require('child_process');
|
|
13
13
|
var interactive = require('@theokit/sdk/interactive');
|
|
14
14
|
var promises$1 = require('dns/promises');
|
|
15
15
|
var net = require('net');
|
|
16
16
|
|
|
17
17
|
// src/apply-patch.ts
|
|
18
|
-
var PathTraversalError = class extends sdk.ConfigurationError {
|
|
19
|
-
name = "PathTraversalError";
|
|
20
|
-
constructor(input, resolvedPath) {
|
|
21
|
-
super(`Path traversal attempt: ${input} \u2192 ${resolvedPath}`, {
|
|
22
|
-
code: "path_traversal"
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
var ForbiddenPathError = class extends sdk.ConfigurationError {
|
|
27
|
-
name = "ForbiddenPathError";
|
|
28
|
-
constructor(path) {
|
|
29
|
-
super(
|
|
30
|
-
`Path '${path}' is in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files)`,
|
|
31
|
-
{
|
|
32
|
-
code: "forbidden_path"
|
|
33
|
-
}
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
function safePathJoin(base, ...parts) {
|
|
38
|
-
if (base === "") {
|
|
39
|
-
throw new Error("safePathJoin: base must be non-empty");
|
|
40
|
-
}
|
|
41
|
-
const baseResolved = path.resolve(base);
|
|
42
|
-
const target = path.resolve(base, ...parts);
|
|
43
|
-
if (target !== baseResolved && !target.startsWith(baseResolved + path.sep)) {
|
|
44
|
-
throw new PathTraversalError(parts.join("/"), target);
|
|
45
|
-
}
|
|
46
|
-
return target;
|
|
47
|
-
}
|
|
48
|
-
function assertNoSymlinkEscape(path$1, base) {
|
|
49
|
-
let baseResolved;
|
|
50
|
-
try {
|
|
51
|
-
baseResolved = fs.realpathSync(base);
|
|
52
|
-
} catch {
|
|
53
|
-
baseResolved = path.resolve(base);
|
|
54
|
-
}
|
|
55
|
-
const resolved = realpathOfDeepestExisting(path$1);
|
|
56
|
-
if (resolved === void 0) return;
|
|
57
|
-
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + path.sep)) {
|
|
58
|
-
throw new PathTraversalError(`symlink ${path$1}`, resolved);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function realpathOfDeepestExisting(path$1) {
|
|
62
|
-
try {
|
|
63
|
-
return fs.realpathSync(path$1);
|
|
64
|
-
} catch {
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
const stat3 = fs.lstatSync(path$1);
|
|
68
|
-
if (stat3.isSymbolicLink()) {
|
|
69
|
-
const target = fs.readlinkSync(path$1);
|
|
70
|
-
const parentReal = realpathOfDeepestExisting(path.dirname(path$1));
|
|
71
|
-
const parentBase = parentReal ?? path.dirname(path$1);
|
|
72
|
-
return path.resolve(parentBase, target);
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
}
|
|
76
|
-
let cursor = path.dirname(path$1);
|
|
77
|
-
let suffix = path$1.slice(cursor.length);
|
|
78
|
-
while (cursor !== path.dirname(cursor)) {
|
|
79
|
-
try {
|
|
80
|
-
const real = fs.realpathSync(cursor);
|
|
81
|
-
return path.resolve(real, `.${suffix}`);
|
|
82
|
-
} catch {
|
|
83
|
-
suffix = path$1.slice(path.dirname(cursor).length);
|
|
84
|
-
cursor = path.dirname(cursor);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return void 0;
|
|
88
|
-
}
|
|
89
|
-
var LOCK_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
|
|
90
|
-
function isForbiddenPath(input) {
|
|
91
|
-
const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
92
|
-
if (normalized.length === 0) return false;
|
|
93
|
-
const segments = normalized.split("/").filter((s) => s.length > 0);
|
|
94
|
-
if (segments.length === 0) return false;
|
|
95
|
-
const first = segments[0];
|
|
96
|
-
if (first === ".env.example") return false;
|
|
97
|
-
if (first === ".env") return true;
|
|
98
|
-
if (/^\.env\./.test(first)) return true;
|
|
99
|
-
if (first === ".git") return true;
|
|
100
|
-
if (first === "node_modules") return true;
|
|
101
|
-
if (first === ".theo") return true;
|
|
102
|
-
const basename = segments[segments.length - 1];
|
|
103
|
-
if (LOCK_FILES.has(basename)) return true;
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
18
|
|
|
107
19
|
// src/internal/v4a-patch.ts
|
|
108
20
|
var BEGIN = "*** Begin Patch";
|
|
@@ -446,15 +358,15 @@ function isForbiddenRel(rel) {
|
|
|
446
358
|
function v4aScope(projectRoot, file) {
|
|
447
359
|
let abs;
|
|
448
360
|
try {
|
|
449
|
-
abs = safePathJoin(projectRoot, file);
|
|
450
|
-
assertNoSymlinkEscape(abs, projectRoot);
|
|
361
|
+
abs = pathSafety.safePathJoin(projectRoot, file);
|
|
362
|
+
pathSafety.assertNoSymlinkEscape(abs, projectRoot);
|
|
451
363
|
} catch (err) {
|
|
452
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
364
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
453
365
|
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: file }) };
|
|
454
366
|
}
|
|
455
367
|
throw err;
|
|
456
368
|
}
|
|
457
|
-
if (isForbiddenPath(file) || isForbiddenRel(path.relative(projectRoot, abs))) {
|
|
369
|
+
if (pathSafety.isForbiddenPath(file) || isForbiddenRel(path.relative(projectRoot, abs))) {
|
|
458
370
|
return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
|
|
459
371
|
}
|
|
460
372
|
return { abs };
|
|
@@ -665,7 +577,7 @@ async function editViaBackend(filesystem$1, ctx, path, old_string, new_string) {
|
|
|
665
577
|
return JSON.stringify({ ok: true, replacements: 1 });
|
|
666
578
|
}
|
|
667
579
|
async function editViaLocal(projectRoot, path, old_string, new_string) {
|
|
668
|
-
const absolutePath = safePathJoin(projectRoot, path);
|
|
580
|
+
const absolutePath = pathSafety.safePathJoin(projectRoot, path);
|
|
669
581
|
let content;
|
|
670
582
|
try {
|
|
671
583
|
content = await promises.readFile(absolutePath, "utf-8");
|
|
@@ -683,14 +595,14 @@ async function editViaLocal(projectRoot, path, old_string, new_string) {
|
|
|
683
595
|
return JSON.stringify({ ok: true, replacements: 1 });
|
|
684
596
|
}
|
|
685
597
|
function editScopeError(path, projectRoot) {
|
|
686
|
-
if (isForbiddenPath(path)) {
|
|
598
|
+
if (pathSafety.isForbiddenPath(path)) {
|
|
687
599
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
688
600
|
}
|
|
689
601
|
try {
|
|
690
|
-
assertNoSymlinkEscape(safePathJoin(projectRoot, path), projectRoot);
|
|
602
|
+
pathSafety.assertNoSymlinkEscape(pathSafety.safePathJoin(projectRoot, path), projectRoot);
|
|
691
603
|
return null;
|
|
692
604
|
} catch (err) {
|
|
693
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
605
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
694
606
|
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
695
607
|
}
|
|
696
608
|
throw err;
|
|
@@ -699,8 +611,8 @@ function editScopeError(path, projectRoot) {
|
|
|
699
611
|
function createEditFileTool(opts) {
|
|
700
612
|
const { projectRoot, filesystem } = opts;
|
|
701
613
|
return sdk.Tool.create({
|
|
702
|
-
name: "edit_file",
|
|
703
|
-
description: "Make an exact string replacement in a project-relative file. Replaces the FIRST occurrence of old_string with new_string (a whitespace-normalized fallback is attempted if the exact match fails) and writes a .bak backup first. Read the file first so old_string matches the on-disk text exactly; include enough surrounding context to make it unique \u2014 only the first match is replaced, so a too-short old_string can edit the wrong location. old_string must be non-empty and differ from new_string; to change every occurrence, call edit_file repeatedly. Returns { ok, replacements } or { ok: false, error }.",
|
|
614
|
+
name: opts.name ?? "edit_file",
|
|
615
|
+
description: opts.description ?? "Make an exact string replacement in a project-relative file. Replaces the FIRST occurrence of old_string with new_string (a whitespace-normalized fallback is attempted if the exact match fails) and writes a .bak backup first. Read the file first so old_string matches the on-disk text exactly; include enough surrounding context to make it unique \u2014 only the first match is replaced, so a too-short old_string can edit the wrong location. old_string must be non-empty and differ from new_string; to change every occurrence, call edit_file repeatedly. Returns { ok, replacements } or { ok: false, error }.",
|
|
704
616
|
inputSchema: zod.z.object({
|
|
705
617
|
path: zod.z.string().min(1).describe("Project-relative file path."),
|
|
706
618
|
old_string: zod.z.string().min(1).describe("String to find in the file."),
|
|
@@ -772,21 +684,6 @@ function formatError(message, code) {
|
|
|
772
684
|
return `> **Error:** ${prefix}${message}`;
|
|
773
685
|
}
|
|
774
686
|
|
|
775
|
-
// src/path-scope.ts
|
|
776
|
-
function checkPathScope(path, projectRoot) {
|
|
777
|
-
if (path === void 0 || path === "") return null;
|
|
778
|
-
try {
|
|
779
|
-
const abs = safePathJoin(projectRoot, path);
|
|
780
|
-
assertNoSymlinkEscape(abs, projectRoot);
|
|
781
|
-
return null;
|
|
782
|
-
} catch (err) {
|
|
783
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
784
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
785
|
-
}
|
|
786
|
-
throw err;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
687
|
// src/subprocess.ts
|
|
791
688
|
function createSettleGate(timer) {
|
|
792
689
|
let done = false;
|
|
@@ -800,25 +697,104 @@ function createSettleGate(timer) {
|
|
|
800
697
|
}
|
|
801
698
|
};
|
|
802
699
|
}
|
|
803
|
-
function armTimeoutKill(child, timeoutMs, onTimeout,
|
|
700
|
+
function armTimeoutKill(child, timeoutMs, onTimeout, resolve) {
|
|
804
701
|
const timer = setTimeout(() => {
|
|
805
702
|
gate.fire(() => {
|
|
806
703
|
try {
|
|
807
704
|
process.kill(-(child.pid ?? 0), "SIGKILL");
|
|
808
705
|
} catch {
|
|
809
706
|
}
|
|
810
|
-
|
|
707
|
+
resolve(onTimeout());
|
|
811
708
|
});
|
|
812
709
|
}, timeoutMs);
|
|
813
710
|
const gate = createSettleGate(timer);
|
|
814
711
|
return gate;
|
|
815
712
|
}
|
|
816
|
-
function attachChildSettlers(child, gate, onClose, onError,
|
|
713
|
+
function attachChildSettlers(child, gate, onClose, onError, resolve) {
|
|
817
714
|
child.on("close", (code) => {
|
|
818
|
-
gate.fire(() =>
|
|
715
|
+
gate.fire(() => resolve(onClose(code)));
|
|
819
716
|
});
|
|
820
717
|
child.on("error", (err) => {
|
|
821
|
-
gate.fire(() =>
|
|
718
|
+
gate.fire(() => resolve(onError(err)));
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/internal/git-exec.ts
|
|
723
|
+
function formatGitResult(result, timeoutMs) {
|
|
724
|
+
if (result.kind === "timeout") {
|
|
725
|
+
return JSON.stringify({ ok: false, error: "timeout", timeoutMs });
|
|
726
|
+
}
|
|
727
|
+
if (result.kind === "error") {
|
|
728
|
+
return JSON.stringify({ ok: false, error: "git_failed", stderr: result.stderr });
|
|
729
|
+
}
|
|
730
|
+
return JSON.stringify({ ok: true, diff: result.stdout, truncated: result.truncated });
|
|
731
|
+
}
|
|
732
|
+
function runGitProcess(cwd, args, timeoutMs, maxStdoutBytes) {
|
|
733
|
+
return new Promise((resolve) => {
|
|
734
|
+
const child = child_process.spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
735
|
+
const stdoutChunks = [];
|
|
736
|
+
const stderrChunks = [];
|
|
737
|
+
let stdoutBytes = 0;
|
|
738
|
+
let truncated = false;
|
|
739
|
+
const gate = armTimeoutKill(
|
|
740
|
+
child,
|
|
741
|
+
timeoutMs,
|
|
742
|
+
() => ({ kind: "timeout" }),
|
|
743
|
+
resolve
|
|
744
|
+
);
|
|
745
|
+
child.stdout.on("data", (chunk) => {
|
|
746
|
+
if (gate.settled()) return;
|
|
747
|
+
if (stdoutBytes >= maxStdoutBytes) {
|
|
748
|
+
truncated = true;
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const remaining = maxStdoutBytes - stdoutBytes;
|
|
752
|
+
if (chunk.length > remaining) {
|
|
753
|
+
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
754
|
+
stdoutBytes = maxStdoutBytes;
|
|
755
|
+
truncated = true;
|
|
756
|
+
} else {
|
|
757
|
+
stdoutChunks.push(chunk);
|
|
758
|
+
stdoutBytes += chunk.length;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
child.stderr.on("data", (chunk) => {
|
|
762
|
+
stderrChunks.push(chunk);
|
|
763
|
+
});
|
|
764
|
+
attachChildSettlers(
|
|
765
|
+
child,
|
|
766
|
+
gate,
|
|
767
|
+
(code) => {
|
|
768
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
769
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
770
|
+
return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
|
|
771
|
+
},
|
|
772
|
+
(err) => ({ kind: "error", stderr: err.message }),
|
|
773
|
+
resolve
|
|
774
|
+
);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/path-scope.ts
|
|
779
|
+
function checkPathScope(path, projectRoot) {
|
|
780
|
+
if (path === void 0 || path === "") return null;
|
|
781
|
+
try {
|
|
782
|
+
const abs = pathSafety.safePathJoin(projectRoot, path);
|
|
783
|
+
pathSafety.assertNoSymlinkEscape(abs, projectRoot);
|
|
784
|
+
return null;
|
|
785
|
+
} catch (err) {
|
|
786
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
787
|
+
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
788
|
+
}
|
|
789
|
+
throw err;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
var SEGMENTOS_SENSIVEIS = /* @__PURE__ */ new Set([".env", ".git", "node_modules", ".theo"]);
|
|
793
|
+
function ehProibidoEmQualquerProfundidade(path) {
|
|
794
|
+
const segs = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
795
|
+
return segs.some((s) => {
|
|
796
|
+
if (s === ".env.example") return false;
|
|
797
|
+
return SEGMENTOS_SENSIVEIS.has(s) || /^\.env\./.test(s);
|
|
822
798
|
});
|
|
823
799
|
}
|
|
824
800
|
|
|
@@ -875,58 +851,25 @@ function buildDiffArgs(cached, path) {
|
|
|
875
851
|
if (path !== void 0 && path !== "") args.push("--", path);
|
|
876
852
|
return args;
|
|
877
853
|
}
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const child = child_process.spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
890
|
-
const stdoutChunks = [];
|
|
891
|
-
const stderrChunks = [];
|
|
892
|
-
let stdoutBytes = 0;
|
|
893
|
-
let truncated = false;
|
|
894
|
-
const gate = armTimeoutKill(
|
|
895
|
-
child,
|
|
896
|
-
timeoutMs,
|
|
897
|
-
() => ({ kind: "timeout" }),
|
|
898
|
-
resolve2
|
|
899
|
-
);
|
|
900
|
-
child.stdout.on("data", (chunk) => {
|
|
901
|
-
if (gate.settled()) return;
|
|
902
|
-
if (stdoutBytes >= maxStdoutBytes) {
|
|
903
|
-
truncated = true;
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
const remaining = maxStdoutBytes - stdoutBytes;
|
|
907
|
-
if (chunk.length > remaining) {
|
|
908
|
-
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
909
|
-
stdoutBytes = maxStdoutBytes;
|
|
910
|
-
truncated = true;
|
|
911
|
-
} else {
|
|
912
|
-
stdoutChunks.push(chunk);
|
|
913
|
-
stdoutBytes += chunk.length;
|
|
854
|
+
function createGitStatusTool(opts) {
|
|
855
|
+
const { projectRoot, timeoutMs = 3e4, maxStdoutBytes = 5 * 1024 * 1024 } = opts;
|
|
856
|
+
return sdk.Tool.create({
|
|
857
|
+
name: opts.name ?? "git_status",
|
|
858
|
+
description: opts.description ?? "Show the working-tree status in porcelain format: staged, unstaged and untracked paths, one per line with a two-character status code. Use before committing, or to see what changed without reading the full diff. Optional 'path' scopes the report to a subdirectory.",
|
|
859
|
+
inputSchema: zod.z.object({
|
|
860
|
+
path: zod.z.string().optional().describe("Optional project-relative path to scope the status report.")
|
|
861
|
+
}),
|
|
862
|
+
handler: async ({ path: path$1 }) => {
|
|
863
|
+
if (!fs.existsSync(path.join(projectRoot, ".git"))) {
|
|
864
|
+
return JSON.stringify({ ok: false, error: "not_a_repo" });
|
|
914
865
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
(code) => {
|
|
923
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
924
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
925
|
-
return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
|
|
926
|
-
},
|
|
927
|
-
(err) => ({ kind: "error", stderr: err.message }),
|
|
928
|
-
resolve2
|
|
929
|
-
);
|
|
866
|
+
const scopeCheck = checkPathScope(path$1, projectRoot);
|
|
867
|
+
if (scopeCheck !== null) return scopeCheck;
|
|
868
|
+
const args = ["status", "--porcelain"];
|
|
869
|
+
if (path$1 !== void 0 && path$1 !== "") args.push("--", path$1);
|
|
870
|
+
const result = await runGitProcess(projectRoot, args, timeoutMs, maxStdoutBytes);
|
|
871
|
+
return formatGitResult(result, timeoutMs);
|
|
872
|
+
}
|
|
930
873
|
});
|
|
931
874
|
}
|
|
932
875
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
@@ -951,7 +894,7 @@ function createGlobTool(opts) {
|
|
|
951
894
|
await walkDirBackend(backend, searchRel, searchRel, regex, found, 0);
|
|
952
895
|
return JSON.stringify({ ok: true, files: found.sort(), count: found.length });
|
|
953
896
|
}
|
|
954
|
-
const searchRoot = cwd ? safePathJoin(projectRoot, cwd) : projectRoot;
|
|
897
|
+
const searchRoot = cwd ? pathSafety.safePathJoin(projectRoot, cwd) : projectRoot;
|
|
955
898
|
const files = [];
|
|
956
899
|
await walkDir(searchRoot, searchRoot, regex, files);
|
|
957
900
|
const relativePaths = files.map((f) => path.relative(projectRoot, f)).sort();
|
|
@@ -962,10 +905,10 @@ function createGlobTool(opts) {
|
|
|
962
905
|
function globScopeError(cwd, projectRoot) {
|
|
963
906
|
if (!cwd) return null;
|
|
964
907
|
try {
|
|
965
|
-
assertNoSymlinkEscape(safePathJoin(projectRoot, cwd), projectRoot);
|
|
908
|
+
pathSafety.assertNoSymlinkEscape(pathSafety.safePathJoin(projectRoot, cwd), projectRoot);
|
|
966
909
|
return null;
|
|
967
910
|
} catch (err) {
|
|
968
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
911
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
969
912
|
return JSON.stringify({ ok: false, error: "path_traversal", path: cwd });
|
|
970
913
|
}
|
|
971
914
|
throw err;
|
|
@@ -1598,15 +1541,17 @@ var DEFAULT_MAX_ENTRIES = 500;
|
|
|
1598
1541
|
function createListDirTool(opts) {
|
|
1599
1542
|
const { projectRoot, max = DEFAULT_MAX_ENTRIES, filesystem: filesystem$1 } = opts;
|
|
1600
1543
|
return sdk.Tool.create({
|
|
1601
|
-
name: "list_dir",
|
|
1602
|
-
description: `Return the direct entries of a project-relative directory. Refuses paths outside the project root or in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files). Caps at ${String(max)} entries by default; result carries truncated + totalCount.`,
|
|
1544
|
+
name: opts.name ?? "list_dir",
|
|
1545
|
+
description: opts.description ?? `Return the direct entries of a project-relative directory. Refuses paths outside the project root or in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files). Caps at ${String(max)} entries by default; result carries truncated + totalCount.`,
|
|
1603
1546
|
inputSchema: zod.z.object({
|
|
1604
1547
|
path: zod.z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
|
|
1605
1548
|
}),
|
|
1606
1549
|
handler: async ({ path }, ctx) => {
|
|
1607
1550
|
const relative3 = path === "" || path === "." ? "." : path;
|
|
1608
|
-
|
|
1609
|
-
|
|
1551
|
+
const veredito = decidirEscopo(relative3, path, opts.allowAbsolute === true);
|
|
1552
|
+
if (veredito.erro !== void 0) return veredito.erro;
|
|
1553
|
+
if (veredito.raizAbsoluta !== void 0) {
|
|
1554
|
+
return listViaLocalFs(veredito.raizAbsoluta, ".", path, max);
|
|
1610
1555
|
}
|
|
1611
1556
|
if (filesystem$1) {
|
|
1612
1557
|
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
@@ -1616,6 +1561,16 @@ function createListDirTool(opts) {
|
|
|
1616
1561
|
}
|
|
1617
1562
|
});
|
|
1618
1563
|
}
|
|
1564
|
+
function decidirEscopo(relative3, original, allowAbsolute) {
|
|
1565
|
+
const recusa = (error) => ({
|
|
1566
|
+
erro: JSON.stringify({ ok: false, error, path: original })
|
|
1567
|
+
});
|
|
1568
|
+
if (relative3 !== "." && pathSafety.isForbiddenPath(relative3)) return recusa("forbidden_path");
|
|
1569
|
+
if (!path.isAbsolute(relative3)) return {};
|
|
1570
|
+
if (!allowAbsolute) return recusa("path_traversal");
|
|
1571
|
+
if (ehProibidoEmQualquerProfundidade(relative3)) return recusa("forbidden_path");
|
|
1572
|
+
return { raizAbsoluta: relative3 };
|
|
1573
|
+
}
|
|
1619
1574
|
async function listViaLocalFs(projectRoot, relative3, originalPath, max) {
|
|
1620
1575
|
const boundary = resolveDirBoundary(relative3, projectRoot, originalPath);
|
|
1621
1576
|
if ("error" in boundary) return boundary.error;
|
|
@@ -1653,11 +1608,11 @@ async function listViaBackend(backend, relative3, originalPath, max) {
|
|
|
1653
1608
|
}
|
|
1654
1609
|
function resolveDirBoundary(relative3, projectRoot, originalPath) {
|
|
1655
1610
|
try {
|
|
1656
|
-
const absolutePath = relative3 === "." ? projectRoot : safePathJoin(projectRoot, relative3);
|
|
1657
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
1611
|
+
const absolutePath = relative3 === "." ? projectRoot : pathSafety.safePathJoin(projectRoot, relative3);
|
|
1612
|
+
pathSafety.assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
1658
1613
|
return { absolutePath };
|
|
1659
1614
|
} catch (err) {
|
|
1660
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1615
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
1661
1616
|
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: originalPath }) };
|
|
1662
1617
|
}
|
|
1663
1618
|
throw err;
|
|
@@ -1776,6 +1731,11 @@ function createPlanModeTool(options) {
|
|
|
1776
1731
|
}
|
|
1777
1732
|
|
|
1778
1733
|
// src/question.ts
|
|
1734
|
+
function askerDoContexto(context) {
|
|
1735
|
+
if (typeof context !== "object" || context === null) return void 0;
|
|
1736
|
+
const candidato = context.askUser;
|
|
1737
|
+
return typeof candidato === "function" ? candidato : void 0;
|
|
1738
|
+
}
|
|
1779
1739
|
function createQuestionTool(opts) {
|
|
1780
1740
|
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
1781
1741
|
return {
|
|
@@ -1788,12 +1748,20 @@ function createQuestionTool(opts) {
|
|
|
1788
1748
|
},
|
|
1789
1749
|
required: ["question"]
|
|
1790
1750
|
},
|
|
1791
|
-
handler: async (input) => {
|
|
1751
|
+
handler: async (input, ctx) => {
|
|
1752
|
+
const askUser = askerDoContexto(ctx?.context) ?? opts.askUser;
|
|
1753
|
+
if (askUser === void 0) {
|
|
1754
|
+
return JSON.stringify({
|
|
1755
|
+
ok: false,
|
|
1756
|
+
error: "no_asker",
|
|
1757
|
+
message: "No asker available: pass `askUser` to createQuestionTool, or provide `context.askUser` via SendOptions.context."
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1792
1760
|
const timeout = new Promise((_, reject) => {
|
|
1793
1761
|
setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
1794
1762
|
});
|
|
1795
1763
|
try {
|
|
1796
|
-
const answer = await Promise.race([
|
|
1764
|
+
const answer = await Promise.race([askUser(input.question), timeout]);
|
|
1797
1765
|
return JSON.stringify({ ok: true, answer });
|
|
1798
1766
|
} catch (err) {
|
|
1799
1767
|
if (err instanceof Error && err.message === "timeout") {
|
|
@@ -1819,7 +1787,7 @@ function isForbiddenAtAnyDepth(path) {
|
|
|
1819
1787
|
});
|
|
1820
1788
|
}
|
|
1821
1789
|
function forbiddenReadError(path$1, allowAbsolute) {
|
|
1822
|
-
if (isForbiddenPath(path$1)) {
|
|
1790
|
+
if (pathSafety.isForbiddenPath(path$1)) {
|
|
1823
1791
|
return JSON.stringify({ ok: false, error: "forbidden_path", path: path$1 });
|
|
1824
1792
|
}
|
|
1825
1793
|
if (allowAbsolute && path.isAbsolute(path$1) && isForbiddenAtAnyDepth(path$1)) {
|
|
@@ -1906,11 +1874,11 @@ function resolveBoundary(path$1, projectRoot, allowAbsolute) {
|
|
|
1906
1874
|
return { absolutePath: path$1 };
|
|
1907
1875
|
}
|
|
1908
1876
|
try {
|
|
1909
|
-
const absolutePath = safePathJoin(projectRoot, path$1);
|
|
1910
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
1877
|
+
const absolutePath = pathSafety.safePathJoin(projectRoot, path$1);
|
|
1878
|
+
pathSafety.assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
1911
1879
|
return { absolutePath };
|
|
1912
1880
|
} catch (err) {
|
|
1913
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
1881
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
1914
1882
|
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
|
|
1915
1883
|
}
|
|
1916
1884
|
throw err;
|
|
@@ -2032,7 +2000,7 @@ function createRunVitestTool(opts) {
|
|
|
2032
2000
|
});
|
|
2033
2001
|
}
|
|
2034
2002
|
function validateVitestScope(path, projectRoot) {
|
|
2035
|
-
if (path !== void 0 && path !== "" && isForbiddenPath(path)) {
|
|
2003
|
+
if (path !== void 0 && path !== "" && pathSafety.isForbiddenPath(path)) {
|
|
2036
2004
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
2037
2005
|
}
|
|
2038
2006
|
return checkPathScope(path, projectRoot);
|
|
@@ -2078,7 +2046,7 @@ function appendCapped(chunks, chunk, current, cap) {
|
|
|
2078
2046
|
return current + chunk.length;
|
|
2079
2047
|
}
|
|
2080
2048
|
function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
|
|
2081
|
-
return new Promise((
|
|
2049
|
+
return new Promise((resolve) => {
|
|
2082
2050
|
const child = child_process.spawn(command, args, {
|
|
2083
2051
|
cwd,
|
|
2084
2052
|
detached: true,
|
|
@@ -2091,7 +2059,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
|
|
|
2091
2059
|
child,
|
|
2092
2060
|
timeoutMs,
|
|
2093
2061
|
() => ({ kind: "timeout" }),
|
|
2094
|
-
|
|
2062
|
+
resolve
|
|
2095
2063
|
);
|
|
2096
2064
|
child.stdout.on("data", (chunk) => {
|
|
2097
2065
|
if (gate.settled()) return;
|
|
@@ -2110,7 +2078,7 @@ function runProcess(cwd, command, args, timeoutMs, maxStdoutBytes) {
|
|
|
2110
2078
|
exitCode: code ?? 0
|
|
2111
2079
|
}),
|
|
2112
2080
|
(err) => ({ kind: "spawn_error", message: err.message }),
|
|
2113
|
-
|
|
2081
|
+
resolve
|
|
2114
2082
|
);
|
|
2115
2083
|
});
|
|
2116
2084
|
}
|
|
@@ -2131,8 +2099,8 @@ function createSearchTextTool(opts) {
|
|
|
2131
2099
|
const queryKind = regex ? "a JavaScript REGULAR EXPRESSION" : "LITERAL, CASE-SENSITIVE text";
|
|
2132
2100
|
const queryMatch = regex ? "matched as a regex" : "matched as a substring, not a regex";
|
|
2133
2101
|
return sdk.Tool.create({
|
|
2134
|
-
name: "search_text",
|
|
2135
|
-
description: `Search file CONTENTS for ${queryKind} across the project tree (the query is ${queryMatch}). Use search_text when you know the content; use glob_files when you know the filename shape; use read_file when you know the exact path. Skips sensitive dirs (.env/.git/node_modules/.theo), binary files, and files over 1 MB; 'path' scopes the search to a subdirectory. Returns up to ${String(maxMatches)} matches as { file, line, preview } \u2014 cite locations to the user as file:line. Returns { ok, matches } or { ok: false, error }.`,
|
|
2102
|
+
name: opts.name ?? "search_text",
|
|
2103
|
+
description: opts.description ?? `Search file CONTENTS for ${queryKind} across the project tree (the query is ${queryMatch}). Use search_text when you know the content; use glob_files when you know the filename shape; use read_file when you know the exact path. Skips sensitive dirs (.env/.git/node_modules/.theo), binary files, and files over 1 MB; 'path' scopes the search to a subdirectory. Returns up to ${String(maxMatches)} matches as { file, line, preview } \u2014 cite locations to the user as file:line. Returns { ok, matches } or { ok: false, error }.`,
|
|
2136
2104
|
inputSchema: zod.z.object({
|
|
2137
2105
|
query: regex ? zod.z.string().min(1).describe("A JavaScript regular expression, e.g. 'function\\\\s+main'.") : zod.z.string().min(1).describe("Literal text to search for. Case-sensitive."),
|
|
2138
2106
|
path: zod.z.string().optional().describe(
|
|
@@ -2191,11 +2159,11 @@ function resolveSearchScope(path$1, projectRoot, allowAbsolute) {
|
|
|
2191
2159
|
return { scopeAbs: scopeRel };
|
|
2192
2160
|
}
|
|
2193
2161
|
try {
|
|
2194
|
-
const scopeAbs = scopeRel === "." ? projectRoot : safePathJoin(projectRoot, scopeRel);
|
|
2195
|
-
assertNoSymlinkEscape(scopeAbs, projectRoot);
|
|
2162
|
+
const scopeAbs = scopeRel === "." ? projectRoot : pathSafety.safePathJoin(projectRoot, scopeRel);
|
|
2163
|
+
pathSafety.assertNoSymlinkEscape(scopeAbs, projectRoot);
|
|
2196
2164
|
return { scopeAbs };
|
|
2197
2165
|
} catch (err) {
|
|
2198
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
2166
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
2199
2167
|
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
|
|
2200
2168
|
}
|
|
2201
2169
|
throw err;
|
|
@@ -2204,7 +2172,7 @@ function resolveSearchScope(path$1, projectRoot, allowAbsolute) {
|
|
|
2204
2172
|
async function handleEntry(entry, absDir, state) {
|
|
2205
2173
|
const entryAbs = path.join(absDir, entry.name);
|
|
2206
2174
|
const entryRel = path.relative(state.projectRoot, entryAbs);
|
|
2207
|
-
if (isForbiddenPath(entryRel)) return;
|
|
2175
|
+
if (pathSafety.isForbiddenPath(entryRel)) return;
|
|
2208
2176
|
if (entry.isDirectory()) {
|
|
2209
2177
|
await walk(entryAbs, state);
|
|
2210
2178
|
return;
|
|
@@ -2270,10 +2238,10 @@ function resolveScopeRel(path$1, projectRoot, allowAbsolute) {
|
|
|
2270
2238
|
if (scopeRel === "") return { rel: "" };
|
|
2271
2239
|
if (allowAbsolute && path.isAbsolute(scopeRel)) return { rel: scopeRel };
|
|
2272
2240
|
try {
|
|
2273
|
-
assertNoSymlinkEscape(safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
2241
|
+
pathSafety.assertNoSymlinkEscape(pathSafety.safePathJoin(projectRoot, scopeRel), projectRoot);
|
|
2274
2242
|
return { rel: scopeRel };
|
|
2275
2243
|
} catch (err) {
|
|
2276
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
2244
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
2277
2245
|
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: path$1 }) };
|
|
2278
2246
|
}
|
|
2279
2247
|
throw err;
|
|
@@ -2295,7 +2263,7 @@ async function walkBackend(backend, dirRel, state, depth) {
|
|
|
2295
2263
|
}
|
|
2296
2264
|
async function handleBackendEntry(backend, dirRel, name, state, depth) {
|
|
2297
2265
|
const entryRel = dirRel === "" ? name : `${dirRel}/${name}`;
|
|
2298
|
-
if (isForbiddenPath(entryRel)) return;
|
|
2266
|
+
if (pathSafety.isForbiddenPath(entryRel)) return;
|
|
2299
2267
|
let st;
|
|
2300
2268
|
try {
|
|
2301
2269
|
st = await backend.stat(entryRel);
|
|
@@ -2340,8 +2308,8 @@ function createShellTool(opts) {
|
|
|
2340
2308
|
sandbox
|
|
2341
2309
|
} = opts;
|
|
2342
2310
|
return sdk.Tool.create({
|
|
2343
|
-
name: "shell_exec",
|
|
2344
|
-
description: "Execute a shell command in the project directory. Use this for terminal operations \u2014 running tests, git, package managers, build tools. Do NOT use it for file operations (reading, writing, editing, finding files): prefer the specialized read_file/write_file/edit_file/glob_files/search_text tools, which are path-checked and safer. Only commit, push, or change git state when the user explicitly asks. timeout_ms defaults to 30000 (max 300000); stdout/stderr are capped (~5 MB). Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
|
|
2311
|
+
name: opts.name ?? "shell_exec",
|
|
2312
|
+
description: opts.description ?? "Execute a shell command in the project directory. Use this for terminal operations \u2014 running tests, git, package managers, build tools. Do NOT use it for file operations (reading, writing, editing, finding files): prefer the specialized read_file/write_file/edit_file/glob_files/search_text tools, which are path-checked and safer. Only commit, push, or change git state when the user explicitly asks. timeout_ms defaults to 30000 (max 300000); stdout/stderr are capped (~5 MB). Returns { ok, stdout, stderr, exit_code } or { ok: false, error }.",
|
|
2345
2313
|
inputSchema: zod.z.object({
|
|
2346
2314
|
command: zod.z.string().min(1).describe("Shell command to execute."),
|
|
2347
2315
|
timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
@@ -2361,7 +2329,7 @@ function createShellTool(opts) {
|
|
|
2361
2329
|
});
|
|
2362
2330
|
}
|
|
2363
2331
|
function runShell(cwd, command, timeoutMs) {
|
|
2364
|
-
return new Promise((
|
|
2332
|
+
return new Promise((resolve) => {
|
|
2365
2333
|
const child = child_process.spawn("/bin/sh", ["-c", command], {
|
|
2366
2334
|
cwd,
|
|
2367
2335
|
detached: true,
|
|
@@ -2375,7 +2343,7 @@ function runShell(cwd, command, timeoutMs) {
|
|
|
2375
2343
|
child,
|
|
2376
2344
|
timeoutMs,
|
|
2377
2345
|
() => ({ kind: "timeout" }),
|
|
2378
|
-
(result) =>
|
|
2346
|
+
(result) => resolve(formatResult(result, timeoutMs))
|
|
2379
2347
|
);
|
|
2380
2348
|
child.stdout.on("data", (chunk) => {
|
|
2381
2349
|
if (gate.settled()) return;
|
|
@@ -2413,7 +2381,7 @@ function runShell(cwd, command, timeoutMs) {
|
|
|
2413
2381
|
exitCode: code
|
|
2414
2382
|
}),
|
|
2415
2383
|
(err) => ({ kind: "error", message: err.message }),
|
|
2416
|
-
(result) =>
|
|
2384
|
+
(result) => resolve(formatResult(result, timeoutMs))
|
|
2417
2385
|
);
|
|
2418
2386
|
});
|
|
2419
2387
|
}
|
|
@@ -2795,7 +2763,7 @@ function createWriteFileTool(opts) {
|
|
|
2795
2763
|
content: zod.z.string().describe("UTF-8 content to write.")
|
|
2796
2764
|
}),
|
|
2797
2765
|
handler: async ({ path, content }, ctx) => {
|
|
2798
|
-
if (isForbiddenPath(path)) {
|
|
2766
|
+
if (pathSafety.isForbiddenPath(path)) {
|
|
2799
2767
|
return JSON.stringify({ ok: false, error: "forbidden_path", path });
|
|
2800
2768
|
}
|
|
2801
2769
|
if (filesystem$1) {
|
|
@@ -2817,10 +2785,10 @@ function readBeforeWriteError(guard, path, currentMtimeMs) {
|
|
|
2817
2785
|
async function writeViaLocalFs(projectRoot, path$1, content, guard) {
|
|
2818
2786
|
let absolutePath;
|
|
2819
2787
|
try {
|
|
2820
|
-
absolutePath = safePathJoin(projectRoot, path$1);
|
|
2821
|
-
assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
2788
|
+
absolutePath = pathSafety.safePathJoin(projectRoot, path$1);
|
|
2789
|
+
pathSafety.assertNoSymlinkEscape(absolutePath, projectRoot);
|
|
2822
2790
|
} catch (err) {
|
|
2823
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
2791
|
+
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
2824
2792
|
return JSON.stringify({ ok: false, error: "path_traversal", path: path$1 });
|
|
2825
2793
|
}
|
|
2826
2794
|
throw err;
|
|
@@ -2924,6 +2892,7 @@ exports.createCurrentTimeTool = createCurrentTimeTool;
|
|
|
2924
2892
|
exports.createEditFileTool = createEditFileTool;
|
|
2925
2893
|
exports.createGenericHttpSearchAdapter = createGenericHttpSearchAdapter;
|
|
2926
2894
|
exports.createGitDiffTool = createGitDiffTool;
|
|
2895
|
+
exports.createGitStatusTool = createGitStatusTool;
|
|
2927
2896
|
exports.createGlobTool = createGlobTool;
|
|
2928
2897
|
exports.createInteractiveShellTool = createInteractiveShellTool;
|
|
2929
2898
|
exports.createListDirTool = createListDirTool;
|