@theokit/sdk-tools 0.21.0 → 0.22.1
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 +136 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -6
- package/dist/index.d.ts +117 -6
- package/dist/index.js +136 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -7,9 +7,9 @@ var zod = require('zod');
|
|
|
7
7
|
var pathSafety = require('@theokit/sdk/path-safety');
|
|
8
8
|
var persistence = require('@theokit/sdk/persistence');
|
|
9
9
|
var filesystem = require('@theokit/sdk/filesystem');
|
|
10
|
-
var child_process = require('child_process');
|
|
11
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');
|
|
@@ -611,8 +611,8 @@ function editScopeError(path, projectRoot) {
|
|
|
611
611
|
function createEditFileTool(opts) {
|
|
612
612
|
const { projectRoot, filesystem } = opts;
|
|
613
613
|
return sdk.Tool.create({
|
|
614
|
-
name: "edit_file",
|
|
615
|
-
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 }.",
|
|
616
616
|
inputSchema: zod.z.object({
|
|
617
617
|
path: zod.z.string().min(1).describe("Project-relative file path."),
|
|
618
618
|
old_string: zod.z.string().min(1).describe("String to find in the file."),
|
|
@@ -684,21 +684,6 @@ function formatError(message, code) {
|
|
|
684
684
|
return `> **Error:** ${prefix}${message}`;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
-
// src/path-scope.ts
|
|
688
|
-
function checkPathScope(path, projectRoot) {
|
|
689
|
-
if (path === void 0 || path === "") return null;
|
|
690
|
-
try {
|
|
691
|
-
const abs = pathSafety.safePathJoin(projectRoot, path);
|
|
692
|
-
pathSafety.assertNoSymlinkEscape(abs, projectRoot);
|
|
693
|
-
return null;
|
|
694
|
-
} catch (err) {
|
|
695
|
-
if (err instanceof pathSafety.PathTraversalError || err instanceof pathSafety.ForbiddenPathError) {
|
|
696
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path });
|
|
697
|
-
}
|
|
698
|
-
throw err;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
687
|
// src/subprocess.ts
|
|
703
688
|
function createSettleGate(timer) {
|
|
704
689
|
let done = false;
|
|
@@ -734,6 +719,85 @@ function attachChildSettlers(child, gate, onClose, onError, resolve) {
|
|
|
734
719
|
});
|
|
735
720
|
}
|
|
736
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);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
737
801
|
// src/git-diff.ts
|
|
738
802
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
739
803
|
var DEFAULT_MAX_STDOUT_BYTES = 5 * 1024 * 1024;
|
|
@@ -787,58 +851,25 @@ function buildDiffArgs(cached, path) {
|
|
|
787
851
|
if (path !== void 0 && path !== "") args.push("--", path);
|
|
788
852
|
return args;
|
|
789
853
|
}
|
|
790
|
-
function
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const child = child_process.spawn("git", args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
802
|
-
const stdoutChunks = [];
|
|
803
|
-
const stderrChunks = [];
|
|
804
|
-
let stdoutBytes = 0;
|
|
805
|
-
let truncated = false;
|
|
806
|
-
const gate = armTimeoutKill(
|
|
807
|
-
child,
|
|
808
|
-
timeoutMs,
|
|
809
|
-
() => ({ kind: "timeout" }),
|
|
810
|
-
resolve
|
|
811
|
-
);
|
|
812
|
-
child.stdout.on("data", (chunk) => {
|
|
813
|
-
if (gate.settled()) return;
|
|
814
|
-
if (stdoutBytes >= maxStdoutBytes) {
|
|
815
|
-
truncated = true;
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const remaining = maxStdoutBytes - stdoutBytes;
|
|
819
|
-
if (chunk.length > remaining) {
|
|
820
|
-
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
821
|
-
stdoutBytes = maxStdoutBytes;
|
|
822
|
-
truncated = true;
|
|
823
|
-
} else {
|
|
824
|
-
stdoutChunks.push(chunk);
|
|
825
|
-
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" });
|
|
826
865
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
(code) => {
|
|
835
|
-
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
836
|
-
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
837
|
-
return code === 0 ? { kind: "ok", stdout, truncated } : { kind: "error", stderr };
|
|
838
|
-
},
|
|
839
|
-
(err) => ({ kind: "error", stderr: err.message }),
|
|
840
|
-
resolve
|
|
841
|
-
);
|
|
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
|
+
}
|
|
842
873
|
});
|
|
843
874
|
}
|
|
844
875
|
var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".theo"]);
|
|
@@ -1510,15 +1541,17 @@ var DEFAULT_MAX_ENTRIES = 500;
|
|
|
1510
1541
|
function createListDirTool(opts) {
|
|
1511
1542
|
const { projectRoot, max = DEFAULT_MAX_ENTRIES, filesystem: filesystem$1 } = opts;
|
|
1512
1543
|
return sdk.Tool.create({
|
|
1513
|
-
name: "list_dir",
|
|
1514
|
-
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.`,
|
|
1515
1546
|
inputSchema: zod.z.object({
|
|
1516
1547
|
path: zod.z.string().min(1).describe("Project-relative directory path. Use '.' for root.")
|
|
1517
1548
|
}),
|
|
1518
1549
|
handler: async ({ path }, ctx) => {
|
|
1519
1550
|
const relative3 = path === "" || path === "." ? "." : path;
|
|
1520
|
-
|
|
1521
|
-
|
|
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);
|
|
1522
1555
|
}
|
|
1523
1556
|
if (filesystem$1) {
|
|
1524
1557
|
const backend = await filesystem.resolveFilesystem(filesystem$1, ctx ?? {});
|
|
@@ -1528,6 +1561,16 @@ function createListDirTool(opts) {
|
|
|
1528
1561
|
}
|
|
1529
1562
|
});
|
|
1530
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
|
+
}
|
|
1531
1574
|
async function listViaLocalFs(projectRoot, relative3, originalPath, max) {
|
|
1532
1575
|
const boundary = resolveDirBoundary(relative3, projectRoot, originalPath);
|
|
1533
1576
|
if ("error" in boundary) return boundary.error;
|
|
@@ -1688,6 +1731,11 @@ function createPlanModeTool(options) {
|
|
|
1688
1731
|
}
|
|
1689
1732
|
|
|
1690
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
|
+
}
|
|
1691
1739
|
function createQuestionTool(opts) {
|
|
1692
1740
|
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
1693
1741
|
return {
|
|
@@ -1700,12 +1748,20 @@ function createQuestionTool(opts) {
|
|
|
1700
1748
|
},
|
|
1701
1749
|
required: ["question"]
|
|
1702
1750
|
},
|
|
1703
|
-
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
|
+
}
|
|
1704
1760
|
const timeout = new Promise((_, reject) => {
|
|
1705
1761
|
setTimeout(() => reject(new Error("timeout")), timeoutMs);
|
|
1706
1762
|
});
|
|
1707
1763
|
try {
|
|
1708
|
-
const answer = await Promise.race([
|
|
1764
|
+
const answer = await Promise.race([askUser(String(input.question ?? "")), timeout]);
|
|
1709
1765
|
return JSON.stringify({ ok: true, answer });
|
|
1710
1766
|
} catch (err) {
|
|
1711
1767
|
if (err instanceof Error && err.message === "timeout") {
|
|
@@ -2043,8 +2099,8 @@ function createSearchTextTool(opts) {
|
|
|
2043
2099
|
const queryKind = regex ? "a JavaScript REGULAR EXPRESSION" : "LITERAL, CASE-SENSITIVE text";
|
|
2044
2100
|
const queryMatch = regex ? "matched as a regex" : "matched as a substring, not a regex";
|
|
2045
2101
|
return sdk.Tool.create({
|
|
2046
|
-
name: "search_text",
|
|
2047
|
-
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 }.`,
|
|
2048
2104
|
inputSchema: zod.z.object({
|
|
2049
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."),
|
|
2050
2106
|
path: zod.z.string().optional().describe(
|
|
@@ -2252,8 +2308,8 @@ function createShellTool(opts) {
|
|
|
2252
2308
|
sandbox
|
|
2253
2309
|
} = opts;
|
|
2254
2310
|
return sdk.Tool.create({
|
|
2255
|
-
name: "shell_exec",
|
|
2256
|
-
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 }.",
|
|
2257
2313
|
inputSchema: zod.z.object({
|
|
2258
2314
|
command: zod.z.string().min(1).describe("Shell command to execute."),
|
|
2259
2315
|
timeout_ms: zod.z.number().int().positive().optional().describe("Timeout in milliseconds (default 30000, max 300000).")
|
|
@@ -2836,6 +2892,7 @@ exports.createCurrentTimeTool = createCurrentTimeTool;
|
|
|
2836
2892
|
exports.createEditFileTool = createEditFileTool;
|
|
2837
2893
|
exports.createGenericHttpSearchAdapter = createGenericHttpSearchAdapter;
|
|
2838
2894
|
exports.createGitDiffTool = createGitDiffTool;
|
|
2895
|
+
exports.createGitStatusTool = createGitStatusTool;
|
|
2839
2896
|
exports.createGlobTool = createGlobTool;
|
|
2840
2897
|
exports.createInteractiveShellTool = createInteractiveShellTool;
|
|
2841
2898
|
exports.createListDirTool = createListDirTool;
|