@wrongstack/tools 0.297.0 → 0.298.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/_spawn-stream.d.ts +2 -0
- package/dist/_util.d.ts +29 -1
- package/dist/audit.js +113 -65
- package/dist/bash.js +5 -4
- package/dist/batch-tool-use.js +70 -7
- package/dist/browser/index.js +31 -1
- package/dist/builtin.js +908 -390
- package/dist/codebase-index/index.js +68 -16
- package/dist/codebase-index/project-server-protocol.d.ts +26 -0
- package/dist/codebase-index/project-server.js +60 -16
- package/dist/codebase-index/worker.js +36 -13
- package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
- package/dist/codebase-index/writer.d.ts +13 -1
- package/dist/design.d.ts +1 -1
- package/dist/design.js +4 -3
- package/dist/diff.js +11 -1
- package/dist/e2e.js +9 -6
- package/dist/edit.js +9 -6
- package/dist/exec-allowlist.d.ts +15 -0
- package/dist/exec.js +627 -568
- package/dist/format.js +116 -65
- package/dist/git.js +22 -2
- package/dist/glob.js +11 -5
- package/dist/grep.js +160 -104
- package/dist/index.js +859 -329
- package/dist/install.js +120 -68
- package/dist/json.js +64 -18
- package/dist/kanban-board-inputs.d.ts +21 -4
- package/dist/kanban.js +20 -5
- package/dist/languages/index.js +128 -71
- package/dist/lint.js +113 -65
- package/dist/outdated.js +86 -26
- package/dist/pack.js +908 -390
- package/dist/patch.js +11 -1
- package/dist/plan.js +32 -10
- package/dist/read.js +77 -23
- package/dist/replace.js +11 -3
- package/dist/scaffold.js +9 -6
- package/dist/session-kanban.js +21 -5
- package/dist/task.js +3 -1
- package/dist/test.js +113 -65
- package/dist/todo.js +3 -1
- package/dist/tool-tier.js +908 -390
- package/dist/tool-use.d.ts +1 -1
- package/dist/tool-use.js +28 -4
- package/dist/tree.d.ts +1 -0
- package/dist/tree.js +44 -8
- package/dist/typecheck.js +113 -65
- package/dist/write.js +9 -6
- package/package.json +7 -6
- package/dist/_concurrency.d.ts.map +0 -1
- package/dist/_danger-detect.d.ts.map +0 -1
- package/dist/_edit-match.d.ts.map +0 -1
- package/dist/_env.d.ts.map +0 -1
- package/dist/_fetch-guard.d.ts.map +0 -1
- package/dist/_output-spool.d.ts.map +0 -1
- package/dist/_redact-command.d.ts.map +0 -1
- package/dist/_regex.d.ts.map +0 -1
- package/dist/_session-shell.d.ts.map +0 -1
- package/dist/_shell-pick.d.ts.map +0 -1
- package/dist/_spawn-stream.d.ts.map +0 -1
- package/dist/_syntax-check.d.ts.map +0 -1
- package/dist/_util.d.ts.map +0 -1
- package/dist/_win32-resolve.d.ts.map +0 -1
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js.map +0 -7
- package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
- package/dist/auto-proceed-loop-guard.js.map +0 -7
- package/dist/bash-kill-guard.d.ts.map +0 -1
- package/dist/bash.d.ts.map +0 -1
- package/dist/bash.js.map +0 -7
- package/dist/batch-tool-use.d.ts.map +0 -1
- package/dist/batch-tool-use.js.map +0 -7
- package/dist/browser/artifacts.d.ts.map +0 -1
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js.map +0 -7
- package/dist/browser/manager.d.ts.map +0 -1
- package/dist/browser/network-guard-proxy.d.ts.map +0 -1
- package/dist/browser/security.d.ts.map +0 -1
- package/dist/browser/tools.d.ts.map +0 -1
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/builtin.d.ts.map +0 -1
- package/dist/builtin.js.map +0 -7
- package/dist/circuit-breaker.d.ts.map +0 -1
- package/dist/circuit-breaker.js.map +0 -7
- package/dist/codebase-index/background-indexer.d.ts.map +0 -1
- package/dist/codebase-index/bm25.d.ts.map +0 -1
- package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
- package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
- package/dist/codebase-index/generic-parser.d.ts.map +0 -1
- package/dist/codebase-index/gitignore.d.ts.map +0 -1
- package/dist/codebase-index/go-parser.d.ts.map +0 -1
- package/dist/codebase-index/index-service.d.ts.map +0 -1
- package/dist/codebase-index/index.d.ts.map +0 -1
- package/dist/codebase-index/index.js.map +0 -7
- package/dist/codebase-index/indexer.d.ts.map +0 -1
- package/dist/codebase-index/json-parser.d.ts.map +0 -1
- package/dist/codebase-index/languages.d.ts.map +0 -1
- package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
- package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
- package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
- package/dist/codebase-index/project-server-client.d.ts.map +0 -1
- package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
- package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
- package/dist/codebase-index/project-server.d.ts.map +0 -1
- package/dist/codebase-index/project-server.js.map +0 -7
- package/dist/codebase-index/py-parser.d.ts.map +0 -1
- package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
- package/dist/codebase-index/rs-parser.d.ts.map +0 -1
- package/dist/codebase-index/schema.d.ts.map +0 -1
- package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
- package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
- package/dist/codebase-index/ts-parser.d.ts.map +0 -1
- package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
- package/dist/codebase-index/worker.d.ts.map +0 -1
- package/dist/codebase-index/worker.js.map +0 -7
- package/dist/codebase-index/writer-admin.d.ts.map +0 -1
- package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
- package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
- package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
- package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
- package/dist/codebase-index/writer-schema.d.ts.map +0 -1
- package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
- package/dist/codebase-index/writer.d.ts.map +0 -1
- package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
- package/dist/design.d.ts.map +0 -1
- package/dist/design.js.map +0 -7
- package/dist/diff.d.ts.map +0 -1
- package/dist/diff.js.map +0 -7
- package/dist/document.d.ts.map +0 -1
- package/dist/document.js.map +0 -7
- package/dist/e2e.d.ts.map +0 -1
- package/dist/e2e.js.map +0 -7
- package/dist/edit.d.ts.map +0 -1
- package/dist/edit.js.map +0 -7
- package/dist/exec-kill-guard.d.ts.map +0 -1
- package/dist/exec.d.ts.map +0 -1
- package/dist/exec.js.map +0 -7
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js.map +0 -7
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js.map +0 -7
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js.map +0 -7
- package/dist/glob.d.ts.map +0 -1
- package/dist/glob.js.map +0 -7
- package/dist/grep.d.ts.map +0 -1
- package/dist/grep.js.map +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/install.d.ts.map +0 -1
- package/dist/install.js.map +0 -7
- package/dist/json.d.ts.map +0 -1
- package/dist/json.js.map +0 -7
- package/dist/kanban-board-inputs.d.ts.map +0 -1
- package/dist/kanban-decomposition-actions.d.ts.map +0 -1
- package/dist/kanban-detail-actions.d.ts.map +0 -1
- package/dist/kanban-evidence-bridge.d.ts.map +0 -1
- package/dist/kanban-presence.d.ts.map +0 -1
- package/dist/kanban-split-task-handler.d.ts.map +0 -1
- package/dist/kanban-task-inputs.d.ts.map +0 -1
- package/dist/kanban-tool-results.d.ts.map +0 -1
- package/dist/kanban-tool-schema.d.ts.map +0 -1
- package/dist/kanban-tool-types.d.ts.map +0 -1
- package/dist/kanban.d.ts.map +0 -1
- package/dist/kanban.js.map +0 -7
- package/dist/languages/detect.d.ts.map +0 -1
- package/dist/languages/diagnostics.d.ts.map +0 -1
- package/dist/languages/execute-tool.d.ts.map +0 -1
- package/dist/languages/execute.d.ts.map +0 -1
- package/dist/languages/index.d.ts.map +0 -1
- package/dist/languages/index.js.map +0 -7
- package/dist/languages/legacy-bridge.d.ts.map +0 -1
- package/dist/languages/package-tool.d.ts.map +0 -1
- package/dist/languages/plan.d.ts.map +0 -1
- package/dist/languages/profile-helpers.d.ts.map +0 -1
- package/dist/languages/profiles/additional.d.ts.map +0 -1
- package/dist/languages/profiles/primary.d.ts.map +0 -1
- package/dist/languages/registry.d.ts.map +0 -1
- package/dist/languages/tool.d.ts.map +0 -1
- package/dist/languages/types.d.ts.map +0 -1
- package/dist/lint.d.ts.map +0 -1
- package/dist/lint.js.map +0 -7
- package/dist/logs.d.ts.map +0 -1
- package/dist/logs.js.map +0 -7
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js.map +0 -7
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js.map +0 -7
- package/dist/next-steps.d.ts.map +0 -1
- package/dist/next-steps.js.map +0 -7
- package/dist/outdated.d.ts.map +0 -1
- package/dist/outdated.js.map +0 -7
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js.map +0 -7
- package/dist/patch.d.ts.map +0 -1
- package/dist/patch.js.map +0 -7
- package/dist/plan.d.ts.map +0 -1
- package/dist/plan.js.map +0 -7
- package/dist/process-guardian.d.ts.map +0 -1
- package/dist/process-registry-persistent.d.ts.map +0 -1
- package/dist/process-registry.d.ts.map +0 -1
- package/dist/process-registry.js.map +0 -7
- package/dist/ps-slash.d.ts.map +0 -1
- package/dist/ps-slash.js.map +0 -7
- package/dist/read.d.ts.map +0 -1
- package/dist/read.js.map +0 -7
- package/dist/replace.d.ts.map +0 -1
- package/dist/replace.js.map +0 -7
- package/dist/scaffold.d.ts.map +0 -1
- package/dist/scaffold.js.map +0 -7
- package/dist/search.d.ts.map +0 -1
- package/dist/search.js.map +0 -7
- package/dist/session-kanban.d.ts.map +0 -1
- package/dist/session-kanban.js.map +0 -7
- package/dist/set-working-dir.d.ts.map +0 -1
- package/dist/skill.d.ts.map +0 -1
- package/dist/skill.js.map +0 -7
- package/dist/spawn-background.d.ts.map +0 -1
- package/dist/task.d.ts.map +0 -1
- package/dist/task.js.map +0 -7
- package/dist/test.d.ts.map +0 -1
- package/dist/test.js.map +0 -7
- package/dist/todo.d.ts.map +0 -1
- package/dist/todo.js.map +0 -7
- package/dist/tool-diff.d.ts.map +0 -1
- package/dist/tool-diff.js.map +0 -7
- package/dist/tool-help.d.ts.map +0 -1
- package/dist/tool-help.js.map +0 -7
- package/dist/tool-icon-map.d.ts.map +0 -1
- package/dist/tool-icons.d.ts.map +0 -1
- package/dist/tool-icons.js.map +0 -7
- package/dist/tool-search.d.ts.map +0 -1
- package/dist/tool-search.js.map +0 -7
- package/dist/tool-summary.d.ts.map +0 -1
- package/dist/tool-summary.js.map +0 -7
- package/dist/tool-tier.d.ts.map +0 -1
- package/dist/tool-tier.js.map +0 -7
- package/dist/tool-use.d.ts.map +0 -1
- package/dist/tool-use.js.map +0 -7
- package/dist/tree.d.ts.map +0 -1
- package/dist/tree.js.map +0 -7
- package/dist/typecheck.d.ts.map +0 -1
- package/dist/typecheck.js.map +0 -7
- package/dist/win32.d.ts.map +0 -1
- package/dist/win32.js.map +0 -7
- package/dist/write.d.ts.map +0 -1
- package/dist/write.js.map +0 -7
package/dist/index.js
CHANGED
|
@@ -148,11 +148,12 @@ function createOutputSpool(opts) {
|
|
|
148
148
|
return {
|
|
149
149
|
write(text) {
|
|
150
150
|
if (finalized || !text) return;
|
|
151
|
-
|
|
151
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
152
|
+
totalBytes += textBytes;
|
|
152
153
|
if (!stream && !failed) {
|
|
153
|
-
if (headBytes +
|
|
154
|
+
if (headBytes + textBytes <= threshold) {
|
|
154
155
|
head += text;
|
|
155
|
-
headBytes +=
|
|
156
|
+
headBytes += textBytes;
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
159
|
head += text;
|
|
@@ -162,7 +163,7 @@ function createOutputSpool(opts) {
|
|
|
162
163
|
}
|
|
163
164
|
if (stream) {
|
|
164
165
|
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
165
|
-
droppedBytes +=
|
|
166
|
+
droppedBytes += textBytes;
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
168
169
|
stream.write(text);
|
|
@@ -849,8 +850,11 @@ async function* spawnStream(opts) {
|
|
|
849
850
|
const max = opts.maxBytes ?? 2e5;
|
|
850
851
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
851
852
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
853
|
+
const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
|
|
852
854
|
let stdout = "";
|
|
853
855
|
let stderr = "";
|
|
856
|
+
let stdoutRetainedBytes = 0;
|
|
857
|
+
let stderrRetainedBytes = 0;
|
|
854
858
|
let pending2 = "";
|
|
855
859
|
let error;
|
|
856
860
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
@@ -892,6 +896,7 @@ async function* spawnStream(opts) {
|
|
|
892
896
|
});
|
|
893
897
|
}
|
|
894
898
|
const queue = [];
|
|
899
|
+
let queuedBytes = 0;
|
|
895
900
|
let waiter;
|
|
896
901
|
let paused = false;
|
|
897
902
|
const wake = () => {
|
|
@@ -902,7 +907,7 @@ async function* spawnStream(opts) {
|
|
|
902
907
|
}
|
|
903
908
|
};
|
|
904
909
|
const resume = () => {
|
|
905
|
-
if (paused && queue.length
|
|
910
|
+
if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
|
|
906
911
|
paused = false;
|
|
907
912
|
child.stdout?.resume();
|
|
908
913
|
child.stderr?.resume();
|
|
@@ -912,11 +917,16 @@ async function* spawnStream(opts) {
|
|
|
912
917
|
const s = c.toString();
|
|
913
918
|
stdoutBytes += c.byteLength;
|
|
914
919
|
emitProcessOutput({ pid, stream: "stdout", chunk: c });
|
|
915
|
-
if (
|
|
920
|
+
if (stdoutRetainedBytes < max) {
|
|
921
|
+
const retained = utf8Prefix(s, max - stdoutRetainedBytes);
|
|
922
|
+
stdout += retained;
|
|
923
|
+
stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
924
|
+
}
|
|
916
925
|
spool.write(s);
|
|
917
|
-
queue.push({ kind: "out", data: s });
|
|
926
|
+
queue.push({ kind: "out", data: s, bytes: c.byteLength });
|
|
927
|
+
queuedBytes += c.byteLength;
|
|
918
928
|
wake();
|
|
919
|
-
if (!paused && queue.length >= maxQueue) {
|
|
929
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
920
930
|
paused = true;
|
|
921
931
|
child.stdout?.pause();
|
|
922
932
|
child.stderr?.pause();
|
|
@@ -926,11 +936,16 @@ async function* spawnStream(opts) {
|
|
|
926
936
|
const s = c.toString();
|
|
927
937
|
stderrBytes += c.byteLength;
|
|
928
938
|
emitProcessOutput({ pid, stream: "stderr", chunk: c });
|
|
929
|
-
if (
|
|
939
|
+
if (stderrRetainedBytes < max) {
|
|
940
|
+
const retained = utf8Prefix(s, max - stderrRetainedBytes);
|
|
941
|
+
stderr += retained;
|
|
942
|
+
stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
943
|
+
}
|
|
930
944
|
spool.write(s);
|
|
931
|
-
queue.push({ kind: "err", data: s });
|
|
945
|
+
queue.push({ kind: "err", data: s, bytes: c.byteLength });
|
|
946
|
+
queuedBytes += c.byteLength;
|
|
932
947
|
wake();
|
|
933
|
-
if (!paused && queue.length >= maxQueue) {
|
|
948
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
934
949
|
paused = true;
|
|
935
950
|
child.stdout?.pause();
|
|
936
951
|
child.stderr?.pause();
|
|
@@ -940,7 +955,9 @@ async function* spawnStream(opts) {
|
|
|
940
955
|
child.stderr?.on("data", onErr);
|
|
941
956
|
child.on("error", (e) => {
|
|
942
957
|
error = e.message;
|
|
943
|
-
|
|
958
|
+
const bytes = Buffer.byteLength(e.message, "utf8");
|
|
959
|
+
queue.push({ kind: "error", data: e.message, bytes });
|
|
960
|
+
queuedBytes += bytes;
|
|
944
961
|
wake();
|
|
945
962
|
});
|
|
946
963
|
const completeTelemetry = (code, signal, timedOut = false) => {
|
|
@@ -961,7 +978,13 @@ async function* spawnStream(opts) {
|
|
|
961
978
|
if (typeof pid === "number") registry.unregister(pid);
|
|
962
979
|
const exitCode2 = code ?? (signal ? 1 : 0);
|
|
963
980
|
completeTelemetry(exitCode2, signal ?? void 0);
|
|
964
|
-
queue.push({
|
|
981
|
+
queue.push({
|
|
982
|
+
kind: "close",
|
|
983
|
+
data: "",
|
|
984
|
+
bytes: 0,
|
|
985
|
+
code: exitCode2,
|
|
986
|
+
...signal ? { signal } : {}
|
|
987
|
+
});
|
|
965
988
|
wake();
|
|
966
989
|
});
|
|
967
990
|
const onAbort = () => {
|
|
@@ -973,7 +996,7 @@ async function* spawnStream(opts) {
|
|
|
973
996
|
} catch {
|
|
974
997
|
}
|
|
975
998
|
}
|
|
976
|
-
queue.push({ kind: "close", data: "", code: 124 });
|
|
999
|
+
queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
|
|
977
1000
|
completeTelemetry(124, "SIGKILL", true);
|
|
978
1001
|
wake();
|
|
979
1002
|
};
|
|
@@ -991,6 +1014,7 @@ async function* spawnStream(opts) {
|
|
|
991
1014
|
});
|
|
992
1015
|
}
|
|
993
1016
|
const chunk = queue.shift();
|
|
1017
|
+
queuedBytes -= chunk.bytes;
|
|
994
1018
|
resume();
|
|
995
1019
|
if (chunk.kind === "close") {
|
|
996
1020
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
@@ -1017,7 +1041,7 @@ async function* spawnStream(opts) {
|
|
|
1017
1041
|
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
1018
1042
|
stderr,
|
|
1019
1043
|
exitCode,
|
|
1020
|
-
truncated:
|
|
1044
|
+
truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
|
|
1021
1045
|
error,
|
|
1022
1046
|
spoolPath: spooled?.path,
|
|
1023
1047
|
spoolBytes: spooled?.bytes
|
|
@@ -1041,13 +1065,27 @@ async function* spawnStream(opts) {
|
|
|
1041
1065
|
}
|
|
1042
1066
|
}
|
|
1043
1067
|
}
|
|
1068
|
+
function utf8Prefix(text, maxBytes) {
|
|
1069
|
+
if (maxBytes <= 0) return "";
|
|
1070
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1071
|
+
let low = 0;
|
|
1072
|
+
let high = text.length;
|
|
1073
|
+
while (low < high) {
|
|
1074
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1075
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
|
|
1076
|
+
else high = mid - 1;
|
|
1077
|
+
}
|
|
1078
|
+
let end = low;
|
|
1079
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
1080
|
+
return text.slice(0, end);
|
|
1081
|
+
}
|
|
1044
1082
|
var isWin;
|
|
1045
1083
|
var init_spawn_stream = __esm({
|
|
1046
1084
|
"src/_spawn-stream.ts"() {
|
|
1047
1085
|
"use strict";
|
|
1048
1086
|
init_output_spool();
|
|
1049
|
-
init_process_registry();
|
|
1050
1087
|
init_win32_resolve();
|
|
1088
|
+
init_process_registry();
|
|
1051
1089
|
isWin = process.platform === "win32";
|
|
1052
1090
|
}
|
|
1053
1091
|
});
|
|
@@ -1061,14 +1099,14 @@ function sha256hex(content) {
|
|
|
1061
1099
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1062
1100
|
}
|
|
1063
1101
|
async function detectPackageManager(cwd) {
|
|
1064
|
-
const { stat:
|
|
1102
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
1065
1103
|
try {
|
|
1066
|
-
await
|
|
1104
|
+
await stat19(`${cwd}/pnpm-lock.yaml`);
|
|
1067
1105
|
return "pnpm";
|
|
1068
1106
|
} catch {
|
|
1069
1107
|
}
|
|
1070
1108
|
try {
|
|
1071
|
-
await
|
|
1109
|
+
await stat19(`${cwd}/yarn.lock`);
|
|
1072
1110
|
return "yarn";
|
|
1073
1111
|
} catch {
|
|
1074
1112
|
}
|
|
@@ -1096,11 +1134,15 @@ function safeResolve(input, ctx) {
|
|
|
1096
1134
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
1097
1135
|
}
|
|
1098
1136
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1099
|
-
|
|
1137
|
+
await resolveRealInsideRoot(absPath, ctx);
|
|
1138
|
+
}
|
|
1139
|
+
async function resolveRealInsideRoot(absPath, ctx) {
|
|
1140
|
+
if (ctx.allowOutsideProjectRoot) return absPath;
|
|
1100
1141
|
const realRoots = await Promise.all(
|
|
1101
1142
|
allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
|
|
1102
1143
|
);
|
|
1103
1144
|
let probe2 = absPath;
|
|
1145
|
+
const pendingTail = [];
|
|
1104
1146
|
for (; ; ) {
|
|
1105
1147
|
let real;
|
|
1106
1148
|
try {
|
|
@@ -1108,13 +1150,16 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1108
1150
|
} catch (err) {
|
|
1109
1151
|
if (err.code === "ENOENT") {
|
|
1110
1152
|
const parent = path3.dirname(probe2);
|
|
1111
|
-
if (parent === probe2) return;
|
|
1153
|
+
if (parent === probe2) return absPath;
|
|
1154
|
+
pendingTail.unshift(path3.basename(probe2));
|
|
1112
1155
|
probe2 = parent;
|
|
1113
1156
|
continue;
|
|
1114
1157
|
}
|
|
1115
1158
|
throw err;
|
|
1116
1159
|
}
|
|
1117
|
-
if (isInsideAny(real, realRoots))
|
|
1160
|
+
if (isInsideAny(real, realRoots)) {
|
|
1161
|
+
return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
|
|
1162
|
+
}
|
|
1118
1163
|
throw new Error(
|
|
1119
1164
|
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
1120
1165
|
);
|
|
@@ -1122,8 +1167,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1122
1167
|
}
|
|
1123
1168
|
async function safeResolveReal(input, ctx) {
|
|
1124
1169
|
const abs = safeResolve(input, ctx);
|
|
1125
|
-
await
|
|
1126
|
-
return abs;
|
|
1170
|
+
return await resolveRealInsideRoot(abs, ctx);
|
|
1127
1171
|
}
|
|
1128
1172
|
function truncateMiddle(s, max) {
|
|
1129
1173
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
@@ -2687,8 +2731,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2687
2731
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2688
2732
|
}
|
|
2689
2733
|
}
|
|
2690
|
-
function collectFileEvidence(directory, fullPath,
|
|
2691
|
-
const lower =
|
|
2734
|
+
function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
|
|
2735
|
+
const lower = basename14.toLowerCase();
|
|
2692
2736
|
const extension = path4.extname(lower);
|
|
2693
2737
|
for (const profile of profiles) {
|
|
2694
2738
|
const detector = profile.detectors.find(
|
|
@@ -2699,7 +2743,7 @@ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
|
|
|
2699
2743
|
candidate.evidence.push({
|
|
2700
2744
|
kind: detector.kind,
|
|
2701
2745
|
path: fullPath,
|
|
2702
|
-
value:
|
|
2746
|
+
value: basename14,
|
|
2703
2747
|
weight: detector.weight
|
|
2704
2748
|
});
|
|
2705
2749
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2827,8 +2871,8 @@ function normalizeLimits(input) {
|
|
|
2827
2871
|
async function canonicalDirectory(input) {
|
|
2828
2872
|
const resolved = path4.resolve(input);
|
|
2829
2873
|
const real = await fs2.realpath(resolved);
|
|
2830
|
-
const
|
|
2831
|
-
if (!
|
|
2874
|
+
const stat19 = await fs2.stat(real);
|
|
2875
|
+
if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2832
2876
|
return real;
|
|
2833
2877
|
}
|
|
2834
2878
|
async function canonicalInside(input, root, label) {
|
|
@@ -3384,6 +3428,7 @@ import * as fs3 from "node:fs/promises";
|
|
|
3384
3428
|
import * as path6 from "node:path";
|
|
3385
3429
|
async function planLanguageOperation(options) {
|
|
3386
3430
|
validateOperationOptions(options.operation, options.operationOptions?.packages);
|
|
3431
|
+
validateFilterOption(options.operationOptions?.filter);
|
|
3387
3432
|
const detection = await detectLanguageWorkspaces(options);
|
|
3388
3433
|
const candidates = detection.workspaces.filter(
|
|
3389
3434
|
(workspace) => workspace.capabilities.includes(options.operation)
|
|
@@ -3556,6 +3601,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
|
|
|
3556
3601
|
const relative13 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3557
3602
|
return relative13 === "" ? 0 : relative13.split(path6.sep).length;
|
|
3558
3603
|
}
|
|
3604
|
+
function validateFilterOption(filter) {
|
|
3605
|
+
if (filter === void 0) return;
|
|
3606
|
+
if (filter.startsWith("-") || filter.includes(" --")) {
|
|
3607
|
+
throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
|
|
3608
|
+
}
|
|
3609
|
+
if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
|
|
3610
|
+
throw new Error("Invalid test filter (oversized or contains a control character)");
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3559
3613
|
function validateOperationOptions(operation, packages) {
|
|
3560
3614
|
if (!operation.startsWith("package-") || !packages) return;
|
|
3561
3615
|
for (const value of packages) {
|
|
@@ -3597,7 +3651,7 @@ var init_plan = __esm({
|
|
|
3597
3651
|
MAX_ARGUMENT_LENGTH = 4096;
|
|
3598
3652
|
PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3599
3653
|
COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3600
|
-
GO_MODULE_RE = /^(?:[a-z0-9
|
|
3654
|
+
GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
|
|
3601
3655
|
PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
|
|
3602
3656
|
}
|
|
3603
3657
|
});
|
|
@@ -3717,8 +3771,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3717
3771
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3718
3772
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3719
3773
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3720
|
-
const
|
|
3721
|
-
if (
|
|
3774
|
+
const stat19 = await fs4.stat(safeTarget);
|
|
3775
|
+
if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3722
3776
|
return unavailableResult(
|
|
3723
3777
|
options,
|
|
3724
3778
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -4005,8 +4059,8 @@ async function snapshotPaths(paths) {
|
|
|
4005
4059
|
const existing = [];
|
|
4006
4060
|
for (const candidate of paths) {
|
|
4007
4061
|
try {
|
|
4008
|
-
const
|
|
4009
|
-
if (!
|
|
4062
|
+
const stat19 = await fs4.stat(candidate);
|
|
4063
|
+
if (!stat19.isFile()) continue;
|
|
4010
4064
|
existing.push(candidate);
|
|
4011
4065
|
} catch {
|
|
4012
4066
|
}
|
|
@@ -4034,8 +4088,8 @@ async function snapshotSizes(paths) {
|
|
|
4034
4088
|
const sizes = /* @__PURE__ */ new Map();
|
|
4035
4089
|
for (const candidate of paths) {
|
|
4036
4090
|
try {
|
|
4037
|
-
const
|
|
4038
|
-
if (
|
|
4091
|
+
const stat19 = await fs4.stat(candidate);
|
|
4092
|
+
if (stat19.isFile()) sizes.set(candidate, stat19.size);
|
|
4039
4093
|
} catch {
|
|
4040
4094
|
}
|
|
4041
4095
|
}
|
|
@@ -4195,6 +4249,9 @@ var init_execute_tool = __esm({
|
|
|
4195
4249
|
useInstead: ["language_info", "install", "audit", "outdated"]
|
|
4196
4250
|
},
|
|
4197
4251
|
permission: "confirm",
|
|
4252
|
+
// WS-046: gives permission decisions something to key on.
|
|
4253
|
+
// The action performed (build/test/lint); the cwd is where, not what.
|
|
4254
|
+
subjectKey: "action",
|
|
4198
4255
|
mutating: true,
|
|
4199
4256
|
riskTier: "standard",
|
|
4200
4257
|
capabilities: ["shell.restricted", "fs.write"],
|
|
@@ -4427,6 +4484,9 @@ var init_package_tool = __esm({
|
|
|
4427
4484
|
useInstead: ["language_info", "language", "install", "audit", "outdated"]
|
|
4428
4485
|
},
|
|
4429
4486
|
permission: "confirm",
|
|
4487
|
+
// WS-046: gives permission decisions something to key on.
|
|
4488
|
+
// The package operation performed — install/remove are not interchangeable.
|
|
4489
|
+
subjectKey: "operation",
|
|
4430
4490
|
mutating: true,
|
|
4431
4491
|
riskTier: "destructive",
|
|
4432
4492
|
capabilities: ["shell.restricted", "fs.write", "net.outbound", "package.install"],
|
|
@@ -6481,9 +6541,9 @@ function parseSymbols6(opts) {
|
|
|
6481
6541
|
function regexParse2(opts) {
|
|
6482
6542
|
const { file, content, lang } = opts;
|
|
6483
6543
|
const symbols = [];
|
|
6484
|
-
const
|
|
6485
|
-
const isPackageJson =
|
|
6486
|
-
const isTsconfig =
|
|
6544
|
+
const basename14 = path22.basename(file).toLowerCase();
|
|
6545
|
+
const isPackageJson = basename14 === "package.json";
|
|
6546
|
+
const isTsconfig = basename14 === "tsconfig.json" || basename14 === "tsconfig.build.json";
|
|
6487
6547
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6488
6548
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6489
6549
|
const lines = content.split("\n");
|
|
@@ -7088,6 +7148,48 @@ var RULES = [
|
|
|
7088
7148
|
return args.some((a) => /^[0-7]{3,4}$/.test(a) && /7/.test(a));
|
|
7089
7149
|
},
|
|
7090
7150
|
reason: "chmod with world-writable octal mode (e.g. 777)"
|
|
7151
|
+
},
|
|
7152
|
+
// ----- network recon / offensive scanners (caution) -----
|
|
7153
|
+
// These tools are purpose-built for network reconnaissance and offensive
|
|
7154
|
+
// security. They have legitimate uses in dev (security testing, infra
|
|
7155
|
+
// debugging) but their presence in an agent's command stream is worth a
|
|
7156
|
+
// caution banner so the user can notice an unexpected scan.
|
|
7157
|
+
{
|
|
7158
|
+
id: "network-scanner",
|
|
7159
|
+
level: "caution",
|
|
7160
|
+
test: (cmd) => cmd === "nmap" || cmd === "masscan" || cmd === "zmap" || cmd === "nuclei" || cmd === "hping3" || cmd === "naabu" || cmd === "katana" || cmd === "amass" || cmd === "subfinder" || cmd === "httpx" || cmd === "rustscan" || cmd === "zgrab",
|
|
7161
|
+
reason: "network scanner / offensive reconnaissance tool"
|
|
7162
|
+
},
|
|
7163
|
+
// ----- process termination (caution) -----
|
|
7164
|
+
// kill/killall/pkill terminate processes. The exec kill guard
|
|
7165
|
+
// (exec-kill-guard.ts) hard-blocks attempts on protected WrongStack
|
|
7166
|
+
// processes; this rule adds a caution banner for generic process
|
|
7167
|
+
// termination so the user sees a warning even for non-protected targets.
|
|
7168
|
+
{
|
|
7169
|
+
id: "process-kill",
|
|
7170
|
+
level: "caution",
|
|
7171
|
+
test: (cmd) => cmd === "kill" || cmd === "killall" || cmd === "pkill",
|
|
7172
|
+
reason: "process termination command"
|
|
7173
|
+
},
|
|
7174
|
+
// ----- shell/interpreter launchers (caution) -----
|
|
7175
|
+
// `env bash -c '…'`, `timeout sh`, `nohup perl -e '…'` etc. wrap a child
|
|
7176
|
+
// binary that bypasses the allowlist name-gate entirely (the wrapper
|
|
7177
|
+
// resolves the child from PATH, not from the allowlist). We flag caution
|
|
7178
|
+
// when a known launcher has a shell/interpreter as its target program.
|
|
7179
|
+
{
|
|
7180
|
+
id: "shell-launcher",
|
|
7181
|
+
level: "caution",
|
|
7182
|
+
test: (cmd, args) => {
|
|
7183
|
+
const launchers = ["env", "timeout", "nohup", "nice", "script", "expect", "tmux", "screen", "byobu", "dtach"];
|
|
7184
|
+
if (!launchers.includes(cmd)) return false;
|
|
7185
|
+
const shells = ["sh", "bash", "zsh", "fish", "python", "python3", "perl", "ruby", "node", "pwsh", "powershell", "cmd"];
|
|
7186
|
+
if (cmd === "env") {
|
|
7187
|
+
const prog = args.find((a) => !a.includes("=") && !a.startsWith("-"));
|
|
7188
|
+
return prog !== void 0 && shells.includes(prog);
|
|
7189
|
+
}
|
|
7190
|
+
return args.some((a) => shells.includes(a));
|
|
7191
|
+
},
|
|
7192
|
+
reason: "launcher wrapping a shell/interpreter (bypasses allowlist name-gate)"
|
|
7091
7193
|
}
|
|
7092
7194
|
];
|
|
7093
7195
|
function detectDanger(cmd, args, bypass) {
|
|
@@ -8695,30 +8797,59 @@ ${hint}` : ""),
|
|
|
8695
8797
|
import {
|
|
8696
8798
|
GOVERNED_TOOL_EXECUTOR_META_KEY
|
|
8697
8799
|
} from "@wrongstack/core/types";
|
|
8800
|
+
var BATCHABLE_TOOL_NAMES = [
|
|
8801
|
+
"read",
|
|
8802
|
+
"glob",
|
|
8803
|
+
"grep",
|
|
8804
|
+
"tree",
|
|
8805
|
+
"codebase-stats",
|
|
8806
|
+
"codebase-search",
|
|
8807
|
+
"language_info",
|
|
8808
|
+
"tool_search",
|
|
8809
|
+
"memory_for_file",
|
|
8810
|
+
"memory_for_path",
|
|
8811
|
+
"memory_search"
|
|
8812
|
+
];
|
|
8813
|
+
var BATCHABLE_TOOLS = new Set(BATCHABLE_TOOL_NAMES);
|
|
8814
|
+
var MAX_BATCH_CALLS = 8;
|
|
8815
|
+
var MAX_PARALLEL_BATCH_CALLS = 3;
|
|
8698
8816
|
var batchToolUseTool = {
|
|
8699
8817
|
name: "batch_tool_use",
|
|
8700
8818
|
category: "Meta",
|
|
8701
|
-
description: "Execute a batch of
|
|
8702
|
-
usageHint:
|
|
8819
|
+
description: "Execute a small batch of independent, read-only discovery calls. Process, mutation, browser-action, MCP, delegation, and nested meta tools are not eligible.",
|
|
8820
|
+
usageHint: `BOUNDED READ-ONLY BATCHING:
|
|
8821
|
+
|
|
8822
|
+
- Use only for a small set of independent reads or searches that are all needed now.
|
|
8823
|
+
- Do not wrap normal sequential reasoning or every tool call in a batch.
|
|
8824
|
+
- \`parallel: true\` (default) runs at most three eligible calls concurrently.
|
|
8825
|
+
- \`stop_on_error: true\` makes it fail fast on the first error.
|
|
8826
|
+
- Eligible tools: ${BATCHABLE_TOOL_NAMES.join(", ")}.`,
|
|
8703
8827
|
permission: "confirm",
|
|
8828
|
+
// Scheduling barrier: keep the outer meta-call out of ToolExecutor's
|
|
8829
|
+
// non-mutating fan-out even though every eligible inner tool is read-only.
|
|
8704
8830
|
mutating: true,
|
|
8705
8831
|
timeoutMs: 12e4,
|
|
8706
|
-
capabilities: ["tool.
|
|
8832
|
+
capabilities: ["tool.meta"],
|
|
8707
8833
|
icon: "meta",
|
|
8708
8834
|
inputSchema: {
|
|
8709
8835
|
type: "object",
|
|
8710
8836
|
properties: {
|
|
8711
8837
|
calls: {
|
|
8712
8838
|
type: "array",
|
|
8839
|
+
maxItems: MAX_BATCH_CALLS,
|
|
8713
8840
|
items: {
|
|
8714
8841
|
type: "object",
|
|
8715
8842
|
properties: {
|
|
8716
|
-
tool: {
|
|
8843
|
+
tool: {
|
|
8844
|
+
type: "string",
|
|
8845
|
+
enum: [...BATCHABLE_TOOL_NAMES],
|
|
8846
|
+
description: "An explicitly batch-safe, read-only discovery tool."
|
|
8847
|
+
},
|
|
8717
8848
|
input: { type: "object" }
|
|
8718
8849
|
},
|
|
8719
8850
|
required: ["tool"]
|
|
8720
8851
|
},
|
|
8721
|
-
description:
|
|
8852
|
+
description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
|
|
8722
8853
|
},
|
|
8723
8854
|
stop_on_error: {
|
|
8724
8855
|
type: "boolean",
|
|
@@ -8741,6 +8872,9 @@ var batchToolUseTool = {
|
|
|
8741
8872
|
stop_on_error: false
|
|
8742
8873
|
};
|
|
8743
8874
|
}
|
|
8875
|
+
if (input.calls.length > MAX_BATCH_CALLS) {
|
|
8876
|
+
throw new Error(`batch_tool_use: calls must contain at most ${MAX_BATCH_CALLS} items`);
|
|
8877
|
+
}
|
|
8744
8878
|
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
|
|
8745
8879
|
if (typeof governedExecute !== "function") {
|
|
8746
8880
|
return {
|
|
@@ -8760,8 +8894,11 @@ var batchToolUseTool = {
|
|
|
8760
8894
|
let succeeded = 0;
|
|
8761
8895
|
let failed = 0;
|
|
8762
8896
|
if (input.parallel !== false) {
|
|
8763
|
-
const
|
|
8764
|
-
|
|
8897
|
+
const allResults = await mapWithConcurrency(
|
|
8898
|
+
input.calls,
|
|
8899
|
+
MAX_PARALLEL_BATCH_CALLS,
|
|
8900
|
+
(call) => executeSingle(call, ctx, governedExecute)
|
|
8901
|
+
);
|
|
8765
8902
|
results.push(...allResults);
|
|
8766
8903
|
succeeded = allResults.filter((r) => r.success).length;
|
|
8767
8904
|
failed = allResults.filter((r) => !r.success).length;
|
|
@@ -8786,8 +8923,28 @@ var batchToolUseTool = {
|
|
|
8786
8923
|
};
|
|
8787
8924
|
}
|
|
8788
8925
|
};
|
|
8926
|
+
async function mapWithConcurrency(items, limit, run) {
|
|
8927
|
+
const results = new Array(items.length);
|
|
8928
|
+
let nextIndex = 0;
|
|
8929
|
+
const worker2 = async () => {
|
|
8930
|
+
while (nextIndex < items.length) {
|
|
8931
|
+
const index = nextIndex++;
|
|
8932
|
+
results[index] = await run(items[index]);
|
|
8933
|
+
}
|
|
8934
|
+
};
|
|
8935
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker2()));
|
|
8936
|
+
return results;
|
|
8937
|
+
}
|
|
8789
8938
|
async function executeSingle(call, ctx, governedExecute) {
|
|
8790
8939
|
const start = Date.now();
|
|
8940
|
+
if (call.tool === batchToolUseTool.name) {
|
|
8941
|
+
return {
|
|
8942
|
+
tool: call.tool,
|
|
8943
|
+
success: false,
|
|
8944
|
+
error: "recursive batch_tool_use execution is not allowed",
|
|
8945
|
+
executionMs: Date.now() - start
|
|
8946
|
+
};
|
|
8947
|
+
}
|
|
8791
8948
|
const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
|
|
8792
8949
|
if (!tool) {
|
|
8793
8950
|
return {
|
|
@@ -8797,6 +8954,14 @@ async function executeSingle(call, ctx, governedExecute) {
|
|
|
8797
8954
|
executionMs: Date.now() - start
|
|
8798
8955
|
};
|
|
8799
8956
|
}
|
|
8957
|
+
if (!BATCHABLE_TOOLS.has(tool.name) || tool.mutating) {
|
|
8958
|
+
return {
|
|
8959
|
+
tool: call.tool,
|
|
8960
|
+
success: false,
|
|
8961
|
+
error: `tool "${call.tool}" is not eligible for batch execution; call it directly. Allowed: ${BATCHABLE_TOOL_NAMES.join(", ")}`,
|
|
8962
|
+
executionMs: Date.now() - start
|
|
8963
|
+
};
|
|
8964
|
+
}
|
|
8800
8965
|
try {
|
|
8801
8966
|
const result = await governedExecute(call.tool, call.input);
|
|
8802
8967
|
return {
|
|
@@ -8823,8 +8988,14 @@ import * as path10 from "node:path";
|
|
|
8823
8988
|
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
8824
8989
|
var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8825
8990
|
var sweptRoots = /* @__PURE__ */ new Set();
|
|
8991
|
+
var MAX_SWEPT_ROOTS = 128;
|
|
8826
8992
|
function sweepOldArtifacts(root) {
|
|
8827
8993
|
if (sweptRoots.has(root)) return;
|
|
8994
|
+
while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
|
|
8995
|
+
const oldest = sweptRoots.values().next().value;
|
|
8996
|
+
if (oldest === void 0) break;
|
|
8997
|
+
sweptRoots.delete(oldest);
|
|
8998
|
+
}
|
|
8828
8999
|
sweptRoots.add(root);
|
|
8829
9000
|
void (async () => {
|
|
8830
9001
|
const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
|
|
@@ -8847,8 +9018,8 @@ function sweepOldArtifacts(root) {
|
|
|
8847
9018
|
for (const name of names) {
|
|
8848
9019
|
const target = path10.join(dir, name);
|
|
8849
9020
|
try {
|
|
8850
|
-
const
|
|
8851
|
-
if (
|
|
9021
|
+
const stat19 = await fs7.stat(target);
|
|
9022
|
+
if (stat19.isFile() && stat19.mtimeMs < cutoff) {
|
|
8852
9023
|
await fs7.rm(target, { force: true });
|
|
8853
9024
|
removed++;
|
|
8854
9025
|
}
|
|
@@ -8876,14 +9047,14 @@ var BrowserArtifactStore = class {
|
|
|
8876
9047
|
}
|
|
8877
9048
|
async record(id, sessionId, kind, target, mimeType) {
|
|
8878
9049
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
8879
|
-
const [
|
|
9050
|
+
const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
8880
9051
|
const artifact = {
|
|
8881
9052
|
id,
|
|
8882
9053
|
kind,
|
|
8883
9054
|
sensitivity: "sensitive",
|
|
8884
9055
|
path: target,
|
|
8885
9056
|
mimeType,
|
|
8886
|
-
sizeBytes:
|
|
9057
|
+
sizeBytes: stat19.size,
|
|
8887
9058
|
sha256,
|
|
8888
9059
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8889
9060
|
};
|
|
@@ -9258,7 +9429,7 @@ var BrowserSessionManager = class {
|
|
|
9258
9429
|
constructor(options, launcher = defaultLauncher) {
|
|
9259
9430
|
this.launcher = launcher;
|
|
9260
9431
|
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
9261
|
-
this.allowPrivateHosts = options.allowPrivateHosts ??
|
|
9432
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
9262
9433
|
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
9263
9434
|
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
9264
9435
|
allowPrivateHosts: this.allowPrivateHosts,
|
|
@@ -9481,8 +9652,8 @@ var BrowserSessionManager = class {
|
|
|
9481
9652
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
9482
9653
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
9483
9654
|
}
|
|
9484
|
-
const
|
|
9485
|
-
if (!
|
|
9655
|
+
const stat19 = await fs8.stat(realFile);
|
|
9656
|
+
if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
9486
9657
|
resolved.push(realFile);
|
|
9487
9658
|
}
|
|
9488
9659
|
await this.runPageOperation(
|
|
@@ -9834,6 +10005,9 @@ var browserScreenshotTool = {
|
|
|
9834
10005
|
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
9835
10006
|
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
9836
10007
|
permission: "confirm",
|
|
10008
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10009
|
+
// rule must distinguish clicking one control from another.
|
|
10010
|
+
subjectKey: "selector",
|
|
9837
10011
|
mutating: true,
|
|
9838
10012
|
riskTier: "safe",
|
|
9839
10013
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9863,6 +10037,9 @@ function selectorTool(name, description, run) {
|
|
|
9863
10037
|
description,
|
|
9864
10038
|
usageHint: `${name}({ sessionId, selector })`,
|
|
9865
10039
|
permission: "confirm",
|
|
10040
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10041
|
+
// rule must distinguish clicking one control from another.
|
|
10042
|
+
subjectKey: "selector",
|
|
9866
10043
|
mutating: true,
|
|
9867
10044
|
riskTier: "standard",
|
|
9868
10045
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9892,6 +10069,9 @@ var browserTypeTool = {
|
|
|
9892
10069
|
description: "Fill an input in an owned browser session. Use secretEnv instead of text for credentials so values stay out of tool arguments and session audit.",
|
|
9893
10070
|
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
9894
10071
|
permission: "confirm",
|
|
10072
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10073
|
+
// rule must distinguish clicking one control from another.
|
|
10074
|
+
subjectKey: "selector",
|
|
9895
10075
|
mutating: true,
|
|
9896
10076
|
riskTier: "standard",
|
|
9897
10077
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9926,6 +10106,9 @@ var browserSelectTool = {
|
|
|
9926
10106
|
description: "Select an option in an owned browser session.",
|
|
9927
10107
|
usageHint: "browser_select({ sessionId, selector, value })",
|
|
9928
10108
|
permission: "confirm",
|
|
10109
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10110
|
+
// rule must distinguish clicking one control from another.
|
|
10111
|
+
subjectKey: "selector",
|
|
9929
10112
|
mutating: true,
|
|
9930
10113
|
riskTier: "standard",
|
|
9931
10114
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9954,6 +10137,9 @@ var browserPressTool = {
|
|
|
9954
10137
|
description: "Press a keyboard key in an owned browser session.",
|
|
9955
10138
|
usageHint: "browser_press({ sessionId, key })",
|
|
9956
10139
|
permission: "confirm",
|
|
10140
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10141
|
+
// rule must distinguish clicking one control from another.
|
|
10142
|
+
subjectKey: "key",
|
|
9957
10143
|
mutating: true,
|
|
9958
10144
|
riskTier: "standard",
|
|
9959
10145
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9972,6 +10158,9 @@ var browserDragTool = {
|
|
|
9972
10158
|
description: "Drag one element to another in an owned browser session.",
|
|
9973
10159
|
usageHint: "browser_drag({ sessionId, from, to })",
|
|
9974
10160
|
permission: "confirm",
|
|
10161
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10162
|
+
// rule must distinguish clicking one control from another.
|
|
10163
|
+
subjectKey: "from",
|
|
9975
10164
|
mutating: true,
|
|
9976
10165
|
riskTier: "standard",
|
|
9977
10166
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -10021,6 +10210,9 @@ var browserEvaluateTool = {
|
|
|
10021
10210
|
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
10022
10211
|
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
10023
10212
|
permission: "confirm",
|
|
10213
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10214
|
+
// rule must distinguish clicking one control from another.
|
|
10215
|
+
subjectKey: "expression",
|
|
10024
10216
|
mutating: true,
|
|
10025
10217
|
riskTier: "destructive",
|
|
10026
10218
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
@@ -10043,6 +10235,9 @@ var browserUploadTool = {
|
|
|
10043
10235
|
description: "Upload project-local files through a file input in an owned browser session.",
|
|
10044
10236
|
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
10045
10237
|
permission: "confirm",
|
|
10238
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10239
|
+
// rule must distinguish clicking one control from another.
|
|
10240
|
+
subjectKey: "selector",
|
|
10046
10241
|
mutating: true,
|
|
10047
10242
|
riskTier: "destructive",
|
|
10048
10243
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
@@ -10986,6 +11181,7 @@ var REFS_INDEX_SQL = [
|
|
|
10986
11181
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
10987
11182
|
|
|
10988
11183
|
// src/codebase-index/writer-search-helpers.ts
|
|
11184
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
10989
11185
|
function normalizeSearchLimit(limit) {
|
|
10990
11186
|
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
10991
11187
|
}
|
|
@@ -11015,8 +11211,8 @@ function buildWriterSearchWhere(query, filter) {
|
|
|
11015
11211
|
}
|
|
11016
11212
|
if (query.trim()) {
|
|
11017
11213
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
11018
|
-
conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
|
|
11019
|
-
for (const token of tokens) values.push(`%${token}%`);
|
|
11214
|
+
conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
|
|
11215
|
+
for (const token of tokens) values.push(`%${escapeLike(token)}%`);
|
|
11020
11216
|
}
|
|
11021
11217
|
return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
|
|
11022
11218
|
}
|
|
@@ -11125,6 +11321,7 @@ var StorePool = class {
|
|
|
11125
11321
|
|
|
11126
11322
|
// src/codebase-index/writer.ts
|
|
11127
11323
|
var DB_FILE2 = "index.db";
|
|
11324
|
+
var MAX_STATEMENT_CACHE = 128;
|
|
11128
11325
|
var IndexStore = class _IndexStore {
|
|
11129
11326
|
db;
|
|
11130
11327
|
/** Absolute path to this project's index directory. */
|
|
@@ -11162,12 +11359,31 @@ var IndexStore = class _IndexStore {
|
|
|
11162
11359
|
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
11163
11360
|
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
11164
11361
|
bm25Dirty = true;
|
|
11165
|
-
/**
|
|
11362
|
+
/**
|
|
11363
|
+
* Prepare-once helper: compile `sql` on first use, reuse thereafter.
|
|
11364
|
+
*
|
|
11365
|
+
* Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
|
|
11366
|
+
* the fallback search builder emits one `text LIKE ?` clause per query token
|
|
11367
|
+
* — so the SQL varies with the token count and a stream of differently-sized
|
|
11368
|
+
* queries grew the cache without limit. Sage's store already bounds its
|
|
11369
|
+
* equivalent at 128 (WS-096).
|
|
11370
|
+
*
|
|
11371
|
+
* Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
|
|
11372
|
+
* insertRefs, …) at the young end, so a burst of one-off search SQL evicts
|
|
11373
|
+
* itself rather than the reindex hot path.
|
|
11374
|
+
*/
|
|
11166
11375
|
stmt(sql) {
|
|
11167
|
-
|
|
11168
|
-
if (
|
|
11169
|
-
|
|
11170
|
-
this.stmtCache.set(sql,
|
|
11376
|
+
const cached = this.stmtCache.get(sql);
|
|
11377
|
+
if (cached !== void 0) {
|
|
11378
|
+
this.stmtCache.delete(sql);
|
|
11379
|
+
this.stmtCache.set(sql, cached);
|
|
11380
|
+
return cached;
|
|
11381
|
+
}
|
|
11382
|
+
const s = this.db.prepare(sql);
|
|
11383
|
+
this.stmtCache.set(sql, s);
|
|
11384
|
+
if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
|
|
11385
|
+
const oldest = this.stmtCache.keys().next();
|
|
11386
|
+
if (!oldest.done) this.stmtCache.delete(oldest.value);
|
|
11171
11387
|
}
|
|
11172
11388
|
return s;
|
|
11173
11389
|
}
|
|
@@ -11440,7 +11656,7 @@ var IndexStore = class _IndexStore {
|
|
|
11440
11656
|
const { where, values } = built;
|
|
11441
11657
|
const limit = normalizeSearchLimit(opts?.limit);
|
|
11442
11658
|
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
11443
|
-
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment
|
|
11659
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
11444
11660
|
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
11445
11661
|
const rows = this.stmt(sql).all(
|
|
11446
11662
|
...binds
|
|
@@ -11569,11 +11785,13 @@ var IndexStore = class _IndexStore {
|
|
|
11569
11785
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
11570
11786
|
searchRankedFallback(query, filter, limit) {
|
|
11571
11787
|
if (!query.trim()) {
|
|
11572
|
-
const
|
|
11573
|
-
if (
|
|
11574
|
-
return { results: this.search(query, filter, { limit }), total };
|
|
11788
|
+
const total2 = this.countSearch(query, filter);
|
|
11789
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
11790
|
+
return { results: this.search(query, filter, { limit }), total: total2 };
|
|
11575
11791
|
}
|
|
11576
|
-
const
|
|
11792
|
+
const total = this.countSearch(query, filter);
|
|
11793
|
+
if (total === 0) return { results: [], total: 0 };
|
|
11794
|
+
const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
|
|
11577
11795
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
11578
11796
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
11579
11797
|
const bm25 = this.getOrBuildBm25();
|
|
@@ -11599,7 +11817,7 @@ var IndexStore = class _IndexStore {
|
|
|
11599
11817
|
const c = expectDefined2(candidateById.get(id));
|
|
11600
11818
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
11601
11819
|
});
|
|
11602
|
-
return { results, total
|
|
11820
|
+
return { results, total };
|
|
11603
11821
|
}
|
|
11604
11822
|
getAllIndexable() {
|
|
11605
11823
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
@@ -12018,12 +12236,12 @@ var buildIdCache;
|
|
|
12018
12236
|
function projectIndexServerBuildId(entrypoint) {
|
|
12019
12237
|
const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
|
|
12020
12238
|
try {
|
|
12021
|
-
const
|
|
12022
|
-
if (buildIdCache?.file === file && buildIdCache.mtimeMs ===
|
|
12239
|
+
const stat19 = fs11.statSync(file);
|
|
12240
|
+
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
|
|
12023
12241
|
return buildIdCache.buildId;
|
|
12024
12242
|
}
|
|
12025
12243
|
const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
|
|
12026
|
-
buildIdCache = { file, mtimeMs:
|
|
12244
|
+
buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
|
|
12027
12245
|
return buildId;
|
|
12028
12246
|
} catch {
|
|
12029
12247
|
return `unreadable:${path16.basename(file)}`;
|
|
@@ -12193,6 +12411,8 @@ var ProjectServerConnection = class {
|
|
|
12193
12411
|
connectResolve = null;
|
|
12194
12412
|
connectReject = null;
|
|
12195
12413
|
nextId = 1;
|
|
12414
|
+
/** WS-027: read from the owner-only metadata file — see currentAuthToken. */
|
|
12415
|
+
authToken;
|
|
12196
12416
|
pending = /* @__PURE__ */ new Map();
|
|
12197
12417
|
transition(status, options = {}) {
|
|
12198
12418
|
const previous = connectionStates.get(this.endpoint);
|
|
@@ -12329,6 +12549,31 @@ var ProjectServerConnection = class {
|
|
|
12329
12549
|
this.transition("offline");
|
|
12330
12550
|
maybeStopHeartbeatLoop();
|
|
12331
12551
|
}
|
|
12552
|
+
/**
|
|
12553
|
+
* WS-027: the per-process token, read from the daemon's owner-only metadata
|
|
12554
|
+
* file — never from the `hello` frame, which the daemon sends to every
|
|
12555
|
+
* socket that connects (the mistake WS-028 found in the SAGE daemon).
|
|
12556
|
+
*
|
|
12557
|
+
* Read lazily and re-read while unknown: the daemon starts listening before
|
|
12558
|
+
* it writes metadata (endpoint ownership has to be won first), and a
|
|
12559
|
+
* respawned daemon mints a new token, so a cached wrong one would be sticky.
|
|
12560
|
+
*/
|
|
12561
|
+
currentAuthToken() {
|
|
12562
|
+
if (this.authToken === void 0) {
|
|
12563
|
+
try {
|
|
12564
|
+
const raw = fs12.readFileSync(
|
|
12565
|
+
projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
|
|
12566
|
+
"utf8"
|
|
12567
|
+
);
|
|
12568
|
+
const parsed = JSON.parse(raw);
|
|
12569
|
+
if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
|
|
12570
|
+
this.authToken = parsed.authToken;
|
|
12571
|
+
}
|
|
12572
|
+
} catch {
|
|
12573
|
+
}
|
|
12574
|
+
}
|
|
12575
|
+
return this.authToken;
|
|
12576
|
+
}
|
|
12332
12577
|
request(message, options) {
|
|
12333
12578
|
const socket = this.socket;
|
|
12334
12579
|
if (!socket || socket.destroyed) {
|
|
@@ -12372,7 +12617,7 @@ var ProjectServerConnection = class {
|
|
|
12372
12617
|
return;
|
|
12373
12618
|
}
|
|
12374
12619
|
}
|
|
12375
|
-
this.write({ ...message, id });
|
|
12620
|
+
this.write({ ...message, id, authToken: this.currentAuthToken() });
|
|
12376
12621
|
});
|
|
12377
12622
|
}
|
|
12378
12623
|
async ensureConnected(spawnIfMissing) {
|
|
@@ -12419,6 +12664,7 @@ var ProjectServerConnection = class {
|
|
|
12419
12664
|
connectOnce() {
|
|
12420
12665
|
this.socket?.destroy();
|
|
12421
12666
|
this.socket = null;
|
|
12667
|
+
this.authToken = void 0;
|
|
12422
12668
|
this.info = null;
|
|
12423
12669
|
this.activity = null;
|
|
12424
12670
|
this.health = null;
|
|
@@ -13062,9 +13308,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13062
13308
|
const statReadParse = await Promise.allSettled(
|
|
13063
13309
|
batchFiles.map(
|
|
13064
13310
|
async (file) => {
|
|
13065
|
-
let
|
|
13311
|
+
let stat19;
|
|
13066
13312
|
try {
|
|
13067
|
-
|
|
13313
|
+
stat19 = await fs17.stat(file, statOpts);
|
|
13068
13314
|
} catch (e) {
|
|
13069
13315
|
if (isAbortError(e)) throw e;
|
|
13070
13316
|
return {
|
|
@@ -13076,21 +13322,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13076
13322
|
missing: isMissingPathError(e)
|
|
13077
13323
|
};
|
|
13078
13324
|
}
|
|
13079
|
-
if (!
|
|
13325
|
+
if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
|
|
13080
13326
|
const lang = detectLang(file);
|
|
13081
|
-
if (!lang) return { file, stat:
|
|
13082
|
-
if (
|
|
13327
|
+
if (!lang) return { file, stat: stat19, lang: "", parsed: null };
|
|
13328
|
+
if (stat19.size > MAX_INDEX_FILE_BYTES) {
|
|
13083
13329
|
return {
|
|
13084
13330
|
file,
|
|
13085
|
-
stat:
|
|
13331
|
+
stat: stat19,
|
|
13086
13332
|
lang,
|
|
13087
13333
|
parsed: null,
|
|
13088
|
-
error: `file too large (${
|
|
13334
|
+
error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
13089
13335
|
};
|
|
13090
13336
|
}
|
|
13091
13337
|
const meta = existingMeta.get(file);
|
|
13092
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
13093
|
-
return { file, stat:
|
|
13338
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
|
|
13339
|
+
return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
|
|
13094
13340
|
}
|
|
13095
13341
|
let content;
|
|
13096
13342
|
try {
|
|
@@ -13099,7 +13345,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13099
13345
|
if (isAbortError(e)) throw e;
|
|
13100
13346
|
return {
|
|
13101
13347
|
file,
|
|
13102
|
-
stat:
|
|
13348
|
+
stat: stat19,
|
|
13103
13349
|
lang,
|
|
13104
13350
|
parsed: null,
|
|
13105
13351
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -13111,13 +13357,13 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13111
13357
|
} catch (e) {
|
|
13112
13358
|
return {
|
|
13113
13359
|
file,
|
|
13114
|
-
stat:
|
|
13360
|
+
stat: stat19,
|
|
13115
13361
|
lang,
|
|
13116
13362
|
parsed: null,
|
|
13117
13363
|
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
13118
13364
|
};
|
|
13119
13365
|
}
|
|
13120
|
-
return { file, stat:
|
|
13366
|
+
return { file, stat: stat19, lang, parsed, content };
|
|
13121
13367
|
}
|
|
13122
13368
|
)
|
|
13123
13369
|
);
|
|
@@ -13138,7 +13384,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13138
13384
|
errors.push(`${file}: ${result.error}`);
|
|
13139
13385
|
continue;
|
|
13140
13386
|
}
|
|
13141
|
-
const { stat:
|
|
13387
|
+
const { stat: stat19, lang, parsed } = result;
|
|
13142
13388
|
if (result.skippedMeta) {
|
|
13143
13389
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
13144
13390
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -13150,7 +13396,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13150
13396
|
store.upsertFile({
|
|
13151
13397
|
file,
|
|
13152
13398
|
lang,
|
|
13153
|
-
mtimeMs: Math.floor(
|
|
13399
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13154
13400
|
symbolCount: 0,
|
|
13155
13401
|
lastIndexed: Date.now()
|
|
13156
13402
|
});
|
|
@@ -13162,7 +13408,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13162
13408
|
store.replaceEmptyFile({
|
|
13163
13409
|
file,
|
|
13164
13410
|
lang,
|
|
13165
|
-
mtimeMs: Math.floor(
|
|
13411
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13166
13412
|
symbolCount: 0,
|
|
13167
13413
|
lastIndexed: Date.now()
|
|
13168
13414
|
});
|
|
@@ -13174,7 +13420,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13174
13420
|
lang,
|
|
13175
13421
|
symbols: parsed.symbols,
|
|
13176
13422
|
refs: parsed.refs ?? [],
|
|
13177
|
-
mtimeMs: Math.floor(
|
|
13423
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13178
13424
|
symbolCount: parsed.symbols.length
|
|
13179
13425
|
});
|
|
13180
13426
|
deleteForFiles.push(file);
|
|
@@ -14370,8 +14616,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
14370
14616
|
const content = fs19.readFileSync(epFile, "utf8");
|
|
14371
14617
|
const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
|
|
14372
14618
|
const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
|
|
14373
|
-
|
|
14374
|
-
|
|
14619
|
+
for (; ; ) {
|
|
14620
|
+
const match = reExportRe.exec(strippedContent);
|
|
14621
|
+
if (match === null) break;
|
|
14375
14622
|
const moduleSpec = match[1];
|
|
14376
14623
|
const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
|
|
14377
14624
|
for (const rf of resolvedFiles) {
|
|
@@ -14577,6 +14824,9 @@ var designTool = {
|
|
|
14577
14824
|
description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
|
|
14578
14825
|
usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
|
|
14579
14826
|
permission: "confirm",
|
|
14827
|
+
// WS-046: gives permission decisions something to key on.
|
|
14828
|
+
// The action performed; `out` is optional so it cannot be the subject.
|
|
14829
|
+
subjectKey: "action",
|
|
14580
14830
|
mutating: true,
|
|
14581
14831
|
capabilities: ["fs.write"],
|
|
14582
14832
|
timeoutMs: 15e3,
|
|
@@ -14757,9 +15007,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
|
|
|
14757
15007
|
const abs = path25.join(absParent, path25.basename(absResolved));
|
|
14758
15008
|
const rel = path25.relative(root, abs);
|
|
14759
15009
|
if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
14760
|
-
throw new Error(
|
|
14761
|
-
`design: materialize path "${result.path}" would escape the project root`
|
|
14762
|
-
);
|
|
15010
|
+
throw new Error(`design: materialize path "${result.path}" would escape the project root`);
|
|
14763
15011
|
}
|
|
14764
15012
|
let exists = false;
|
|
14765
15013
|
try {
|
|
@@ -14834,6 +15082,7 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
14834
15082
|
import * as fs21 from "node:fs/promises";
|
|
14835
15083
|
import * as path26 from "node:path";
|
|
14836
15084
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
15085
|
+
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
14837
15086
|
var diffTool = {
|
|
14838
15087
|
name: "diff",
|
|
14839
15088
|
category: "Filesystem",
|
|
@@ -14841,6 +15090,7 @@ var diffTool = {
|
|
|
14841
15090
|
usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
|
|
14842
15091
|
permission: "auto",
|
|
14843
15092
|
mutating: false,
|
|
15093
|
+
maxOutputBytes: 262144,
|
|
14844
15094
|
capabilities: ["fs.read"],
|
|
14845
15095
|
icon: "diff",
|
|
14846
15096
|
timeoutMs: 1e4,
|
|
@@ -14916,8 +15166,8 @@ function findGitDir(cwd) {
|
|
|
14916
15166
|
let dir = cwd;
|
|
14917
15167
|
for (let i = 0; i < 20; i++) {
|
|
14918
15168
|
try {
|
|
14919
|
-
const
|
|
14920
|
-
if (
|
|
15169
|
+
const stat19 = statSync3(path26.join(dir, ".git"));
|
|
15170
|
+
if (stat19.isDirectory()) return dir;
|
|
14921
15171
|
} catch {
|
|
14922
15172
|
}
|
|
14923
15173
|
const parent = path26.dirname(dir);
|
|
@@ -14959,10 +15209,18 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14959
15209
|
};
|
|
14960
15210
|
}
|
|
14961
15211
|
const results = [];
|
|
15212
|
+
let truncated = false;
|
|
14962
15213
|
for (const file of files) {
|
|
14963
15214
|
const absPath = safeResolve(file, ctx);
|
|
14964
|
-
const
|
|
14965
|
-
if (!
|
|
15215
|
+
const stat19 = await fs21.stat(absPath).catch(() => null);
|
|
15216
|
+
if (!stat19?.isFile()) continue;
|
|
15217
|
+
if (stat19.size > MAX_FILE_DUMP_BYTES) {
|
|
15218
|
+
truncated = true;
|
|
15219
|
+
results.push(
|
|
15220
|
+
`--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
15221
|
+
);
|
|
15222
|
+
continue;
|
|
15223
|
+
}
|
|
14966
15224
|
const content = await fs21.readFile(absPath, "utf8");
|
|
14967
15225
|
const lines = content.split(/\r?\n/);
|
|
14968
15226
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -14970,7 +15228,7 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14970
15228
|
return {
|
|
14971
15229
|
diff: results.join("\n\n"),
|
|
14972
15230
|
files,
|
|
14973
|
-
truncated
|
|
15231
|
+
truncated,
|
|
14974
15232
|
mode: input.mode ?? "unified"
|
|
14975
15233
|
};
|
|
14976
15234
|
}
|
|
@@ -15063,8 +15321,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
15063
15321
|
for (const f of files) {
|
|
15064
15322
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
15065
15323
|
try {
|
|
15066
|
-
const
|
|
15067
|
-
if (
|
|
15324
|
+
const stat19 = await fs22.stat(absPath);
|
|
15325
|
+
if (stat19.isFile()) resolved.push(absPath);
|
|
15068
15326
|
} catch {
|
|
15069
15327
|
}
|
|
15070
15328
|
}
|
|
@@ -15917,7 +16175,7 @@ var editTool = {
|
|
|
15917
16175
|
});
|
|
15918
16176
|
}
|
|
15919
16177
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15920
|
-
const
|
|
16178
|
+
const stat19 = await fs23.stat(absPath).catch((err) => {
|
|
15921
16179
|
if (err.code === "ENOENT") {
|
|
15922
16180
|
throw new ToolValidationError({
|
|
15923
16181
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -15927,7 +16185,7 @@ var editTool = {
|
|
|
15927
16185
|
}
|
|
15928
16186
|
throw err;
|
|
15929
16187
|
});
|
|
15930
|
-
if (!
|
|
16188
|
+
if (!stat19.isFile()) {
|
|
15931
16189
|
throw new ToolValidationError({
|
|
15932
16190
|
message: `edit: "${input.path}" is not a regular file`,
|
|
15933
16191
|
field: "path"
|
|
@@ -15957,7 +16215,7 @@ var editTool = {
|
|
|
15957
16215
|
});
|
|
15958
16216
|
}
|
|
15959
16217
|
}
|
|
15960
|
-
if (autoRead && updated.mtimeMs >
|
|
16218
|
+
if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
|
|
15961
16219
|
throw new ToolValidationError({
|
|
15962
16220
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
15963
16221
|
field: "path",
|
|
@@ -16082,7 +16340,6 @@ import {
|
|
|
16082
16340
|
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
|
|
16083
16341
|
init_output_spool();
|
|
16084
16342
|
init_util();
|
|
16085
|
-
init_process_registry();
|
|
16086
16343
|
init_win32_resolve();
|
|
16087
16344
|
|
|
16088
16345
|
// src/exec-kill-guard.ts
|
|
@@ -16320,8 +16577,7 @@ async function checkKillTarget(target) {
|
|
|
16320
16577
|
return { blocked: false };
|
|
16321
16578
|
}
|
|
16322
16579
|
|
|
16323
|
-
// src/exec.ts
|
|
16324
|
-
var isWin4 = process.platform === "win32";
|
|
16580
|
+
// src/exec-allowlist.ts
|
|
16325
16581
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
16326
16582
|
// JS / TS toolchain
|
|
16327
16583
|
"node",
|
|
@@ -16400,7 +16656,11 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16400
16656
|
"docker",
|
|
16401
16657
|
"podman",
|
|
16402
16658
|
"kubectl",
|
|
16403
|
-
// network
|
|
16659
|
+
// network fetch tools. Per-arg safety is NOT enforced by BLOCKED_ARG_PATTERNS
|
|
16660
|
+
// (no curl/wget entries) — the real boundary is the per-call `confirm`
|
|
16661
|
+
// permission gate (exec.ts permission:'confirm', subjectKey:'command'),
|
|
16662
|
+
// the `pipe-to-shell` danger rule, and the kill guard. The allowlist is a
|
|
16663
|
+
// guidance layer, not a security boundary on its own.
|
|
16404
16664
|
"curl",
|
|
16405
16665
|
"wget",
|
|
16406
16666
|
// common POSIX file/text utilities
|
|
@@ -16453,9 +16713,15 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16453
16713
|
"powershell.exe",
|
|
16454
16714
|
"pwsh",
|
|
16455
16715
|
"pwsh.exe",
|
|
16456
|
-
// [core] Extended default allowlist (added 4b3d18d1 + this commit).
|
|
16457
|
-
//
|
|
16458
|
-
//
|
|
16716
|
+
// [core] Extended default allowlist (added 4b3d18d1 + this commit). Broadly
|
|
16717
|
+
// used dev binaries — NOT all non-destructive: this section contains
|
|
16718
|
+
// process/system tools (kill, chown, mount, userdel), network scanners
|
|
16719
|
+
// (nmap, masscan, zmap, nuclei), and launchers (env, perl, timeout, tmux).
|
|
16720
|
+
// The real safety boundary for these is the per-call `confirm` permission
|
|
16721
|
+
// gate (exec.ts permission:'confirm', subjectKey:'command' renders the full
|
|
16722
|
+
// invocation) plus the danger rules in _danger-detect.ts. Per-arg hard-deny
|
|
16723
|
+
// is still enforced by BLOCKED_ARG_PATTERNS + bash-kill-guard.ts, but only
|
|
16724
|
+
// for the commands that have entries there (git, find, rm).
|
|
16459
16725
|
// --- Archives & compression ---
|
|
16460
16726
|
"7z",
|
|
16461
16727
|
"7za",
|
|
@@ -16627,10 +16893,7 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16627
16893
|
"git-lfs",
|
|
16628
16894
|
"tig",
|
|
16629
16895
|
"lazygit",
|
|
16630
|
-
// --- POSIX text utilities (extended
|
|
16631
|
-
// grep/find/echo/awk/mkdir/cp/mv/rm/touch from the base list above are
|
|
16632
|
-
// omitted — the Set is de-duplicated at runtime but a clean literal is
|
|
16633
|
-
// easier to maintain) ---
|
|
16896
|
+
// --- POSIX text utilities (extended) ---
|
|
16634
16897
|
"gawk",
|
|
16635
16898
|
"tr",
|
|
16636
16899
|
"cut",
|
|
@@ -16663,39 +16926,16 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16663
16926
|
"locate",
|
|
16664
16927
|
"which",
|
|
16665
16928
|
"whereis",
|
|
16666
|
-
|
|
16667
|
-
|
|
16668
|
-
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
"fc",
|
|
16673
|
-
"jobs",
|
|
16674
|
-
"bg",
|
|
16675
|
-
"fg",
|
|
16676
|
-
"wait",
|
|
16677
|
-
"ulimit",
|
|
16678
|
-
"umask",
|
|
16929
|
+
// NOTE: shell builtins with no standalone binary (type, hash, pushd,
|
|
16930
|
+
// popd, dirs, history, fc, jobs, bg, fg, wait, ulimit, umask, trap,
|
|
16931
|
+
// exit, return, source, ., alias, unalias, set, unset, export, readonly,
|
|
16932
|
+
// typeset, declare, local, eval, exec) are intentionally excluded —
|
|
16933
|
+
// spawn() would fail with ENOENT on every platform because they only
|
|
16934
|
+
// exist inside a shell process. Use the `bash` tool for builtins.
|
|
16679
16935
|
"nice",
|
|
16680
16936
|
"nohup",
|
|
16681
16937
|
"timeout",
|
|
16682
16938
|
"time",
|
|
16683
|
-
"trap",
|
|
16684
|
-
"exit",
|
|
16685
|
-
"return",
|
|
16686
|
-
"source",
|
|
16687
|
-
".",
|
|
16688
|
-
"alias",
|
|
16689
|
-
"unalias",
|
|
16690
|
-
"set",
|
|
16691
|
-
"unset",
|
|
16692
|
-
"export",
|
|
16693
|
-
"readonly",
|
|
16694
|
-
"typeset",
|
|
16695
|
-
"declare",
|
|
16696
|
-
"local",
|
|
16697
|
-
"eval",
|
|
16698
|
-
"exec",
|
|
16699
16939
|
// --- Process / system inspection ---
|
|
16700
16940
|
"htop",
|
|
16701
16941
|
"top",
|
|
@@ -16926,6 +17166,10 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16926
17166
|
"sublert",
|
|
16927
17167
|
"chaos"
|
|
16928
17168
|
]);
|
|
17169
|
+
|
|
17170
|
+
// src/exec.ts
|
|
17171
|
+
init_process_registry();
|
|
17172
|
+
var isWin4 = process.platform === "win32";
|
|
16929
17173
|
var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
|
|
16930
17174
|
var normalizeCmd = (c) => c.trim();
|
|
16931
17175
|
function configureExecPolicy(opts = {}) {
|
|
@@ -16986,7 +17230,17 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
16986
17230
|
bun: [],
|
|
16987
17231
|
docker: [],
|
|
16988
17232
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
16989
|
-
find: [
|
|
17233
|
+
find: [
|
|
17234
|
+
/^-exec$/,
|
|
17235
|
+
/^-exec;$/,
|
|
17236
|
+
/^-ok$/,
|
|
17237
|
+
/^-ok;$/,
|
|
17238
|
+
/^-execdir$/,
|
|
17239
|
+
/^-execdir;$/,
|
|
17240
|
+
/^-exec=/,
|
|
17241
|
+
/^-ok=/,
|
|
17242
|
+
/^-execdir=/
|
|
17243
|
+
],
|
|
16990
17244
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
16991
17245
|
// and glob patterns that could expand to dangerous targets.
|
|
16992
17246
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
@@ -17057,6 +17311,14 @@ var execTool = {
|
|
|
17057
17311
|
useInstead: ["bash"]
|
|
17058
17312
|
},
|
|
17059
17313
|
permission: "confirm",
|
|
17314
|
+
// WS-046: without this, every exec call collapsed onto the bare tool name.
|
|
17315
|
+
// Three consequences followed: "always allow" stored a rule that could never
|
|
17316
|
+
// match, pressing "no" once blocked ALL exec for the session, and the
|
|
17317
|
+
// permission cache keyed one decision for every command. `subjectKey:
|
|
17318
|
+
// 'command'` renders the FULL invocation (`command` + `args`) — using the
|
|
17319
|
+
// program alone would make `exec git status` and `exec git push --force` the
|
|
17320
|
+
// same subject, so trusting one would silently authorize the other.
|
|
17321
|
+
subjectKey: "command",
|
|
17060
17322
|
mutating: true,
|
|
17061
17323
|
riskTier: "standard",
|
|
17062
17324
|
timeoutMs: DEFAULT_TIMEOUT_MS3,
|
|
@@ -17273,7 +17535,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
17273
17535
|
const pid = child.pid;
|
|
17274
17536
|
if (typeof pid === "number") {
|
|
17275
17537
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
17276
|
-
registry.register({
|
|
17538
|
+
registry.register({
|
|
17539
|
+
pid,
|
|
17540
|
+
name: "exec",
|
|
17541
|
+
command: redactCommand(fullCommand),
|
|
17542
|
+
startedAt: Date.now(),
|
|
17543
|
+
sessionId,
|
|
17544
|
+
child
|
|
17545
|
+
});
|
|
17277
17546
|
}
|
|
17278
17547
|
const timer = setTimeout(() => {
|
|
17279
17548
|
killed = true;
|
|
@@ -17713,6 +17982,9 @@ var formatTool = {
|
|
|
17713
17982
|
description: "Format source files according to project style (Biome). Can also run in check-only mode.",
|
|
17714
17983
|
usageHint: "RUN REGULARLY:\n\n- Use on changed files before committing.\n- `check: true` verifies formatting without making changes (useful in CI-like flows).\nThis project has very consistent formatting expectations. Always ensure your changes are formatted.",
|
|
17715
17984
|
permission: "confirm",
|
|
17985
|
+
// WS-046: gives permission decisions something to key on.
|
|
17986
|
+
// The files being rewritten are the subject; the fixer is how, not what.
|
|
17987
|
+
subjectKey: "files",
|
|
17716
17988
|
mutating: true,
|
|
17717
17989
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17718
17990
|
icon: "code",
|
|
@@ -17817,13 +18089,13 @@ var formatTool = {
|
|
|
17817
18089
|
}
|
|
17818
18090
|
};
|
|
17819
18091
|
async function detectFixer(cwd) {
|
|
17820
|
-
const { stat:
|
|
18092
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
17821
18093
|
try {
|
|
17822
|
-
await
|
|
18094
|
+
await stat19(`${cwd}/biome.json`);
|
|
17823
18095
|
return "biome";
|
|
17824
18096
|
} catch {
|
|
17825
18097
|
try {
|
|
17826
|
-
await
|
|
18098
|
+
await stat19(`${cwd}/.prettierrc`);
|
|
17827
18099
|
return "prettier";
|
|
17828
18100
|
} catch {
|
|
17829
18101
|
return "biome";
|
|
@@ -17850,6 +18122,10 @@ var gitTool = {
|
|
|
17850
18122
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
17851
18123
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
17852
18124
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
18125
|
+
// WS-046: gives permission decisions something to key on.
|
|
18126
|
+
// The git subcommand (status/commit/push) is what a trust rule needs to
|
|
18127
|
+
// distinguish; `git status` and `git push --force` must not share a subject.
|
|
18128
|
+
subjectKey: "command",
|
|
17853
18129
|
mutating: true,
|
|
17854
18130
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17855
18131
|
timeoutMs: TIMEOUT_MS2,
|
|
@@ -17918,6 +18194,8 @@ var gitTool = {
|
|
|
17918
18194
|
truncated: false
|
|
17919
18195
|
};
|
|
17920
18196
|
}
|
|
18197
|
+
const branchGuard = validateBranchInput(input);
|
|
18198
|
+
if (branchGuard) return branchGuard;
|
|
17921
18199
|
if (input.command === "worktree") {
|
|
17922
18200
|
const guard = validateWorktreeInput(input, ctx.projectRoot);
|
|
17923
18201
|
if (guard) return guard;
|
|
@@ -17966,6 +18244,18 @@ var gitTool = {
|
|
|
17966
18244
|
return result;
|
|
17967
18245
|
}
|
|
17968
18246
|
};
|
|
18247
|
+
function validateBranchInput(input) {
|
|
18248
|
+
const branch = input.branch;
|
|
18249
|
+
if (branch === void 0) return null;
|
|
18250
|
+
if (!branch.startsWith("-") && !branch.includes(" --")) return null;
|
|
18251
|
+
return {
|
|
18252
|
+
command: input.command,
|
|
18253
|
+
stdout: "",
|
|
18254
|
+
stderr: `unsafe branch name (parsed as a git option): ${branch}`,
|
|
18255
|
+
exitCode: 1,
|
|
18256
|
+
truncated: false
|
|
18257
|
+
};
|
|
18258
|
+
}
|
|
17969
18259
|
function validateWorktreeInput(input, projectRoot) {
|
|
17970
18260
|
const reject = (stderr) => ({
|
|
17971
18261
|
command: "worktree",
|
|
@@ -17974,7 +18264,6 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
17974
18264
|
exitCode: 1,
|
|
17975
18265
|
truncated: false
|
|
17976
18266
|
});
|
|
17977
|
-
if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
|
|
17978
18267
|
if (input.worktreePath?.startsWith("-")) {
|
|
17979
18268
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
17980
18269
|
}
|
|
@@ -17992,8 +18281,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
17992
18281
|
let dir = cwd;
|
|
17993
18282
|
for (let i = 0; i < 20; i++) {
|
|
17994
18283
|
try {
|
|
17995
|
-
const
|
|
17996
|
-
if (
|
|
18284
|
+
const stat19 = statSync4(`${dir}/.git`);
|
|
18285
|
+
if (stat19.isDirectory() || stat19.isFile()) return dir;
|
|
17997
18286
|
} catch {
|
|
17998
18287
|
}
|
|
17999
18288
|
if (dir === root) break;
|
|
@@ -18028,7 +18317,10 @@ function buildArgs(input) {
|
|
|
18028
18317
|
...files.length ? ["--", ...files] : []
|
|
18029
18318
|
];
|
|
18030
18319
|
case "branch":
|
|
18031
|
-
return input.branch ? [
|
|
18320
|
+
return input.branch ? [
|
|
18321
|
+
"branch",
|
|
18322
|
+
...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
|
|
18323
|
+
] : ["branch"];
|
|
18032
18324
|
case "checkout":
|
|
18033
18325
|
return [
|
|
18034
18326
|
"checkout",
|
|
@@ -18121,7 +18413,7 @@ import * as path30 from "node:path";
|
|
|
18121
18413
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
18122
18414
|
|
|
18123
18415
|
// src/_concurrency.ts
|
|
18124
|
-
async function
|
|
18416
|
+
async function mapWithConcurrency2(items, limit, fn) {
|
|
18125
18417
|
if (items.length === 0) return [];
|
|
18126
18418
|
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
18127
18419
|
const results = new Array(items.length);
|
|
@@ -18256,10 +18548,10 @@ var globTool = {
|
|
|
18256
18548
|
}
|
|
18257
18549
|
if (truncated) return;
|
|
18258
18550
|
}
|
|
18259
|
-
await
|
|
18551
|
+
await mapWithConcurrency2(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
18260
18552
|
if (truncated) return;
|
|
18261
18553
|
const remainingSubdirs = truncated ? [] : subdirs;
|
|
18262
|
-
await
|
|
18554
|
+
await mapWithConcurrency2(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk2(full, rel));
|
|
18263
18555
|
};
|
|
18264
18556
|
await walk2(base, "");
|
|
18265
18557
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -18268,12 +18560,16 @@ var globTool = {
|
|
|
18268
18560
|
};
|
|
18269
18561
|
|
|
18270
18562
|
// src/grep.ts
|
|
18271
|
-
import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
|
|
18272
18563
|
import { spawn as spawn11 } from "node:child_process";
|
|
18273
18564
|
import * as fs25 from "node:fs/promises";
|
|
18274
18565
|
import * as path31 from "node:path";
|
|
18275
18566
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
18276
|
-
import {
|
|
18567
|
+
import {
|
|
18568
|
+
buildChildEnv as buildChildEnv5,
|
|
18569
|
+
compileGlob as compileGlob3,
|
|
18570
|
+
DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3,
|
|
18571
|
+
expectDefined as expectDefined7
|
|
18572
|
+
} from "@wrongstack/core/utils";
|
|
18277
18573
|
|
|
18278
18574
|
// src/_regex.ts
|
|
18279
18575
|
var MAX_PATTERN_LEN = 256;
|
|
@@ -18326,6 +18622,8 @@ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
|
|
|
18326
18622
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
18327
18623
|
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
18328
18624
|
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
18625
|
+
var RG_MAX_QUEUE_CHUNKS = 128;
|
|
18626
|
+
var RG_MAX_QUEUE_CHARS = 8 * 1024 * 1024;
|
|
18329
18627
|
var grepTool = {
|
|
18330
18628
|
name: "grep",
|
|
18331
18629
|
category: "Search",
|
|
@@ -18453,9 +18751,19 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18453
18751
|
const FLUSH_AT = 16;
|
|
18454
18752
|
const MAX_BUF_BYTES = 1e6;
|
|
18455
18753
|
let bufOverflow = false;
|
|
18456
|
-
const child = spawn11("rg", args, {
|
|
18754
|
+
const child = spawn11("rg", args, {
|
|
18755
|
+
signal,
|
|
18756
|
+
env: buildChildEnv5(),
|
|
18757
|
+
// rg diagnostics are not part of the tool result. Ignoring stderr avoids
|
|
18758
|
+
// an unread pipe filling up and pinning the child when rg reports many
|
|
18759
|
+
// filesystem errors.
|
|
18760
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18761
|
+
windowsHide: true
|
|
18762
|
+
});
|
|
18457
18763
|
const queue = [];
|
|
18764
|
+
let queuedChars = 0;
|
|
18458
18765
|
let waiter;
|
|
18766
|
+
let paused = false;
|
|
18459
18767
|
const wake = () => {
|
|
18460
18768
|
if (waiter) {
|
|
18461
18769
|
const w = waiter;
|
|
@@ -18463,93 +18771,133 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18463
18771
|
w();
|
|
18464
18772
|
}
|
|
18465
18773
|
};
|
|
18466
|
-
|
|
18467
|
-
queue.
|
|
18774
|
+
const pauseIfFlooded = () => {
|
|
18775
|
+
if (paused || queue.length < RG_MAX_QUEUE_CHUNKS && queuedChars < RG_MAX_QUEUE_CHARS)
|
|
18776
|
+
return;
|
|
18777
|
+
paused = true;
|
|
18778
|
+
child.stdout?.pause();
|
|
18779
|
+
};
|
|
18780
|
+
const resumeIfDrained = () => {
|
|
18781
|
+
if (!paused || queue.length >= RG_MAX_QUEUE_CHUNKS || queuedChars >= RG_MAX_QUEUE_CHARS)
|
|
18782
|
+
return;
|
|
18783
|
+
paused = false;
|
|
18784
|
+
child.stdout?.resume();
|
|
18785
|
+
};
|
|
18786
|
+
const onStdoutData = (c) => {
|
|
18787
|
+
const data = c.toString();
|
|
18788
|
+
queue.push({ kind: "out", data });
|
|
18789
|
+
queuedChars += data.length;
|
|
18468
18790
|
wake();
|
|
18469
|
-
|
|
18470
|
-
|
|
18791
|
+
pauseIfFlooded();
|
|
18792
|
+
};
|
|
18793
|
+
const onError = (e) => {
|
|
18471
18794
|
queue.push({ kind: "error", data: e.message });
|
|
18472
18795
|
wake();
|
|
18473
|
-
}
|
|
18474
|
-
|
|
18796
|
+
};
|
|
18797
|
+
const onClose = () => {
|
|
18475
18798
|
queue.push({ kind: "close", data: "" });
|
|
18476
18799
|
wake();
|
|
18477
|
-
}
|
|
18800
|
+
};
|
|
18801
|
+
child.stdout?.on("data", onStdoutData);
|
|
18802
|
+
child.on("error", onError);
|
|
18803
|
+
child.on("close", onClose);
|
|
18478
18804
|
let pendingBatch = [];
|
|
18479
18805
|
let errored = false;
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
if (c.kind === "error") {
|
|
18488
|
-
errored = true;
|
|
18489
|
-
continue;
|
|
18490
|
-
}
|
|
18491
|
-
if (c.kind === "close") break;
|
|
18492
|
-
buf += c.data;
|
|
18493
|
-
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18494
|
-
bufOverflow = true;
|
|
18495
|
-
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18496
|
-
try {
|
|
18497
|
-
child.kill("SIGTERM");
|
|
18498
|
-
} catch {
|
|
18806
|
+
let closed = false;
|
|
18807
|
+
try {
|
|
18808
|
+
for (; ; ) {
|
|
18809
|
+
while (queue.length === 0) {
|
|
18810
|
+
await new Promise((r) => {
|
|
18811
|
+
waiter = r;
|
|
18812
|
+
});
|
|
18499
18813
|
}
|
|
18500
|
-
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
|
|
18814
|
+
const c = expectDefined7(queue.shift());
|
|
18815
|
+
queuedChars = Math.max(0, queuedChars - c.data.length);
|
|
18816
|
+
resumeIfDrained();
|
|
18817
|
+
if (c.kind === "error") {
|
|
18818
|
+
errored = true;
|
|
18819
|
+
continue;
|
|
18820
|
+
}
|
|
18821
|
+
if (c.kind === "close") {
|
|
18822
|
+
closed = true;
|
|
18823
|
+
break;
|
|
18824
|
+
}
|
|
18825
|
+
buf += c.data;
|
|
18826
|
+
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18827
|
+
bufOverflow = true;
|
|
18828
|
+
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18829
|
+
try {
|
|
18830
|
+
child.kill("SIGTERM");
|
|
18831
|
+
} catch {
|
|
18832
|
+
}
|
|
18833
|
+
}
|
|
18834
|
+
const idx = buf.lastIndexOf("\n");
|
|
18835
|
+
if (idx === -1) continue;
|
|
18836
|
+
const ready = buf.slice(0, idx);
|
|
18837
|
+
buf = buf.slice(idx + 1);
|
|
18838
|
+
for (const line of ready.split("\n")) {
|
|
18839
|
+
if (!line) continue;
|
|
18840
|
+
totalLines++;
|
|
18841
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18842
|
+
if (matches.length < limit) {
|
|
18843
|
+
matches.push(line);
|
|
18844
|
+
pendingBatch.push(line);
|
|
18845
|
+
batchSinceFlush++;
|
|
18846
|
+
}
|
|
18847
|
+
}
|
|
18848
|
+
if (batchSinceFlush >= FLUSH_AT) {
|
|
18849
|
+
yield {
|
|
18850
|
+
type: "partial_output",
|
|
18851
|
+
text: pendingBatch.join("\n"),
|
|
18852
|
+
data: { matches_so_far: matches.length }
|
|
18853
|
+
};
|
|
18854
|
+
pendingBatch = [];
|
|
18855
|
+
batchSinceFlush = 0;
|
|
18856
|
+
}
|
|
18857
|
+
}
|
|
18858
|
+
if (buf.trim()) {
|
|
18859
|
+
for (const line of buf.split("\n")) {
|
|
18860
|
+
if (!line) continue;
|
|
18861
|
+
totalLines++;
|
|
18862
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18863
|
+
if (matches.length < limit) {
|
|
18864
|
+
matches.push(line);
|
|
18865
|
+
pendingBatch.push(line);
|
|
18866
|
+
}
|
|
18513
18867
|
}
|
|
18514
18868
|
}
|
|
18515
|
-
if (
|
|
18869
|
+
if (pendingBatch.length > 0) {
|
|
18516
18870
|
yield {
|
|
18517
18871
|
type: "partial_output",
|
|
18518
18872
|
text: pendingBatch.join("\n"),
|
|
18519
18873
|
data: { matches_so_far: matches.length }
|
|
18520
18874
|
};
|
|
18521
|
-
pendingBatch = [];
|
|
18522
|
-
batchSinceFlush = 0;
|
|
18523
|
-
}
|
|
18524
|
-
}
|
|
18525
|
-
if (buf.trim()) {
|
|
18526
|
-
for (const line of buf.split("\n")) {
|
|
18527
|
-
if (!line) continue;
|
|
18528
|
-
totalLines++;
|
|
18529
|
-
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18530
|
-
if (matches.length < limit) {
|
|
18531
|
-
matches.push(line);
|
|
18532
|
-
pendingBatch.push(line);
|
|
18533
|
-
}
|
|
18534
18875
|
}
|
|
18535
|
-
|
|
18536
|
-
if (pendingBatch.length > 0) {
|
|
18876
|
+
if (errored) throw new Error("rg: spawn error");
|
|
18537
18877
|
yield {
|
|
18538
|
-
type: "
|
|
18539
|
-
|
|
18540
|
-
|
|
18878
|
+
type: "final",
|
|
18879
|
+
output: {
|
|
18880
|
+
matches,
|
|
18881
|
+
count: mode === "count" ? totalCount : totalLines,
|
|
18882
|
+
truncated: totalLines > limit || bufOverflow,
|
|
18883
|
+
used: "rg"
|
|
18884
|
+
}
|
|
18541
18885
|
};
|
|
18542
|
-
}
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18886
|
+
} finally {
|
|
18887
|
+
child.stdout?.off("data", onStdoutData);
|
|
18888
|
+
child.off("error", onError);
|
|
18889
|
+
child.off("close", onClose);
|
|
18890
|
+
child.stdout?.destroy();
|
|
18891
|
+
queue.length = 0;
|
|
18892
|
+
queuedChars = 0;
|
|
18893
|
+
waiter = void 0;
|
|
18894
|
+
if (!closed && child.exitCode === null) {
|
|
18895
|
+
try {
|
|
18896
|
+
child.kill("SIGTERM");
|
|
18897
|
+
} catch {
|
|
18898
|
+
}
|
|
18551
18899
|
}
|
|
18552
|
-
}
|
|
18900
|
+
}
|
|
18553
18901
|
}
|
|
18554
18902
|
function parseRgCountLine(line) {
|
|
18555
18903
|
const idx = line.lastIndexOf(":");
|
|
@@ -18579,8 +18927,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18579
18927
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
18580
18928
|
if (globRe) globRe.lastIndex = 0;
|
|
18581
18929
|
try {
|
|
18582
|
-
const
|
|
18583
|
-
if (!
|
|
18930
|
+
const stat19 = await fs25.stat(full);
|
|
18931
|
+
if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
|
|
18584
18932
|
const file = await fs25.open(full, "r");
|
|
18585
18933
|
try {
|
|
18586
18934
|
let bytesReadTotal = 0;
|
|
@@ -18680,8 +19028,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18680
19028
|
files.push({ full, name: e.name });
|
|
18681
19029
|
}
|
|
18682
19030
|
}
|
|
18683
|
-
await
|
|
18684
|
-
await
|
|
19031
|
+
await mapWithConcurrency2(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
19032
|
+
await mapWithConcurrency2(
|
|
18685
19033
|
subdirs,
|
|
18686
19034
|
Math.min(16, NATIVE_SCAN_CONCURRENCY),
|
|
18687
19035
|
({ full, rel }) => walk2(full, rel)
|
|
@@ -18700,7 +19048,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18700
19048
|
init_spawn_stream();
|
|
18701
19049
|
init_util();
|
|
18702
19050
|
init_legacy_bridge();
|
|
18703
|
-
import { join as
|
|
19051
|
+
import { join as join25 } from "node:path";
|
|
18704
19052
|
import {
|
|
18705
19053
|
detectEcosystem as detectPackageEcosystem,
|
|
18706
19054
|
recordPackageAction
|
|
@@ -18711,6 +19059,10 @@ var installTool = {
|
|
|
18711
19059
|
description: "Install, update or manage packages using the detected package manager (pnpm/npm/yarn). Strongly preferred over raw shell commands for dependency management because it is structured and safer.",
|
|
18712
19060
|
usageHint: "ALWAYS USE THIS INSTEAD OF BASH FOR PACKAGE WORK:\n\n- Empty `packages` \u2192 normal `install` (respects lockfile).\n- Provide names \u2192 adds/updates specific packages.\n- `dry_run: true` for safe preview.\n- Set `save` appropriately.\nThis tool has proper capability declaration and is heavily recommended in the security posture of the project.",
|
|
18713
19061
|
permission: "confirm",
|
|
19062
|
+
// WS-046: gives permission decisions something to key on.
|
|
19063
|
+
// What gets installed is the subject — installing a package runs its
|
|
19064
|
+
// lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
|
|
19065
|
+
subjectKey: "packages",
|
|
18714
19066
|
mutating: true,
|
|
18715
19067
|
riskTier: "standard",
|
|
18716
19068
|
icon: "package",
|
|
@@ -18880,10 +19232,10 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18880
19232
|
case "pnpm":
|
|
18881
19233
|
case "yarn":
|
|
18882
19234
|
case "npm":
|
|
18883
|
-
return
|
|
19235
|
+
return join25(cwd, "package.json");
|
|
18884
19236
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
18885
19237
|
default:
|
|
18886
|
-
return
|
|
19238
|
+
return join25(cwd, "package.json");
|
|
18887
19239
|
}
|
|
18888
19240
|
}
|
|
18889
19241
|
|
|
@@ -18891,6 +19243,24 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18891
19243
|
init_util();
|
|
18892
19244
|
import * as fs26 from "node:fs/promises";
|
|
18893
19245
|
import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
|
|
19246
|
+
var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
|
|
19247
|
+
var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
|
|
19248
|
+
var JsonFileTooLargeError = class extends Error {
|
|
19249
|
+
constructor(filePath, size) {
|
|
19250
|
+
super(
|
|
19251
|
+
`json: "${filePath}" is ${size} bytes \u2014 exceeds the ${MAX_JSON_FILE_BYTES_HUMAN} file-size limit. Extract the relevant portion into a smaller file first, or use a shell tool to query it directly.`
|
|
19252
|
+
);
|
|
19253
|
+
this.name = "JsonFileTooLargeError";
|
|
19254
|
+
}
|
|
19255
|
+
};
|
|
19256
|
+
async function readJsonFileBounded(filePath, ctx) {
|
|
19257
|
+
const resolved = await safeResolveReal(filePath, ctx);
|
|
19258
|
+
const stat19 = await fs26.stat(resolved);
|
|
19259
|
+
if (stat19.size > MAX_JSON_FILE_BYTES) {
|
|
19260
|
+
throw new JsonFileTooLargeError(filePath, stat19.size);
|
|
19261
|
+
}
|
|
19262
|
+
return fs26.readFile(resolved, "utf8");
|
|
19263
|
+
}
|
|
18894
19264
|
var jsonTool = {
|
|
18895
19265
|
name: "json",
|
|
18896
19266
|
category: "Data",
|
|
@@ -18898,6 +19268,7 @@ var jsonTool = {
|
|
|
18898
19268
|
usageHint: 'VERY USEFUL FOR DATA INSPECTION:\n\n- `action: "parse"` (default): read/pretty-print/convert JSON, JSON5, or YAML from `file` or `data`.\n- `action: "query"`: JMESPath-like query (`a.b[0].c`, `items[*].name`, filters, functions).\n- `action: "validate"`: validate data against a JSON Schema (`schema` param).\n- `action: "transform"`: chain multiple JMESPath transforms (`transforms` param).\n- `action: "merge"`: deep merge `base` and `patch` objects (`conflictResolution` param).\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.',
|
|
18899
19269
|
permission: "auto",
|
|
18900
19270
|
mutating: false,
|
|
19271
|
+
maxOutputBytes: 262144,
|
|
18901
19272
|
timeoutMs: 5e3,
|
|
18902
19273
|
capabilities: ["fs.read"],
|
|
18903
19274
|
icon: "json",
|
|
@@ -18964,9 +19335,15 @@ async function executeParse(input, ctx) {
|
|
|
18964
19335
|
let raw;
|
|
18965
19336
|
if (input.file) {
|
|
18966
19337
|
try {
|
|
18967
|
-
raw = await
|
|
18968
|
-
} catch {
|
|
18969
|
-
return {
|
|
19338
|
+
raw = await readJsonFileBounded(input.file, ctx);
|
|
19339
|
+
} catch (error) {
|
|
19340
|
+
return {
|
|
19341
|
+
data: null,
|
|
19342
|
+
formatted: "",
|
|
19343
|
+
type: "unknown",
|
|
19344
|
+
action: "parse",
|
|
19345
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read file"
|
|
19346
|
+
};
|
|
18970
19347
|
}
|
|
18971
19348
|
} else if (input.data) {
|
|
18972
19349
|
raw = input.data;
|
|
@@ -19022,10 +19399,16 @@ async function executeQuery(input, ctx) {
|
|
|
19022
19399
|
let parsed;
|
|
19023
19400
|
if (input.file) {
|
|
19024
19401
|
try {
|
|
19025
|
-
const raw = await
|
|
19402
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19026
19403
|
parsed = JSON.parse(raw);
|
|
19027
|
-
} catch {
|
|
19028
|
-
return {
|
|
19404
|
+
} catch (error) {
|
|
19405
|
+
return {
|
|
19406
|
+
data: null,
|
|
19407
|
+
formatted: "",
|
|
19408
|
+
type: "unknown",
|
|
19409
|
+
action: "query",
|
|
19410
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19411
|
+
};
|
|
19029
19412
|
}
|
|
19030
19413
|
} else if (input.data) {
|
|
19031
19414
|
try {
|
|
@@ -19064,10 +19447,16 @@ async function executeValidate(input, ctx) {
|
|
|
19064
19447
|
let parsed;
|
|
19065
19448
|
if (input.file) {
|
|
19066
19449
|
try {
|
|
19067
|
-
const raw = await
|
|
19450
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19068
19451
|
parsed = JSON.parse(raw);
|
|
19069
|
-
} catch {
|
|
19070
|
-
return {
|
|
19452
|
+
} catch (error) {
|
|
19453
|
+
return {
|
|
19454
|
+
data: null,
|
|
19455
|
+
formatted: "",
|
|
19456
|
+
type: "unknown",
|
|
19457
|
+
action: "validate",
|
|
19458
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19459
|
+
};
|
|
19071
19460
|
}
|
|
19072
19461
|
} else if (input.data) {
|
|
19073
19462
|
try {
|
|
@@ -19106,10 +19495,16 @@ async function executeTransform(input, ctx) {
|
|
|
19106
19495
|
let parsed;
|
|
19107
19496
|
if (input.file) {
|
|
19108
19497
|
try {
|
|
19109
|
-
const raw = await
|
|
19498
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19110
19499
|
parsed = JSON.parse(raw);
|
|
19111
|
-
} catch {
|
|
19112
|
-
return {
|
|
19500
|
+
} catch (error) {
|
|
19501
|
+
return {
|
|
19502
|
+
data: null,
|
|
19503
|
+
formatted: "",
|
|
19504
|
+
type: "unknown",
|
|
19505
|
+
action: "transform",
|
|
19506
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19507
|
+
};
|
|
19113
19508
|
}
|
|
19114
19509
|
} else if (input.data) {
|
|
19115
19510
|
try {
|
|
@@ -19713,6 +20108,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
19713
20108
|
}
|
|
19714
20109
|
|
|
19715
20110
|
// src/kanban-board-inputs.ts
|
|
20111
|
+
function agentSettableGate(enforcement) {
|
|
20112
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
20113
|
+
return { completionGate: { enforcement } };
|
|
20114
|
+
}
|
|
19716
20115
|
function boardCreateInput(input, title) {
|
|
19717
20116
|
return {
|
|
19718
20117
|
title,
|
|
@@ -19725,7 +20124,7 @@ function boardCreateInput(input, title) {
|
|
|
19725
20124
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19726
20125
|
}
|
|
19727
20126
|
} : {},
|
|
19728
|
-
...input.gateEnforcement
|
|
20127
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19729
20128
|
};
|
|
19730
20129
|
}
|
|
19731
20130
|
function boardUpdatePatch(input) {
|
|
@@ -19739,7 +20138,7 @@ function boardUpdatePatch(input) {
|
|
|
19739
20138
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19740
20139
|
}
|
|
19741
20140
|
} : {},
|
|
19742
|
-
...input.gateEnforcement
|
|
20141
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19743
20142
|
};
|
|
19744
20143
|
}
|
|
19745
20144
|
function duplicateBoardOptions(input) {
|
|
@@ -19754,6 +20153,7 @@ function duplicateBoardOptions(input) {
|
|
|
19754
20153
|
|
|
19755
20154
|
// src/kanban-task-inputs.ts
|
|
19756
20155
|
import { randomUUID } from "node:crypto";
|
|
20156
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
19757
20157
|
function taskInput(input) {
|
|
19758
20158
|
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
19759
20159
|
return {
|
|
@@ -19851,6 +20251,10 @@ function taskPatch(input) {
|
|
|
19851
20251
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
19852
20252
|
};
|
|
19853
20253
|
}
|
|
20254
|
+
function clampRequestedCapabilities(requested) {
|
|
20255
|
+
if (requested === void 0) return void 0;
|
|
20256
|
+
return clampSubagentCapabilities(requested).granted;
|
|
20257
|
+
}
|
|
19854
20258
|
function assignmentInput(input) {
|
|
19855
20259
|
return {
|
|
19856
20260
|
agentId: input.agentId,
|
|
@@ -19861,7 +20265,7 @@ function assignmentInput(input) {
|
|
|
19861
20265
|
fallbackProfile: input.fallbackProfile,
|
|
19862
20266
|
fallbackModels: input.fallbackModels,
|
|
19863
20267
|
tools: input.tools,
|
|
19864
|
-
allowedCapabilities: input.allowedCapabilities,
|
|
20268
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
19865
20269
|
assignee: input.assignee,
|
|
19866
20270
|
leaseId: input.leaseId,
|
|
19867
20271
|
claimedAt: input.claimedAt,
|
|
@@ -19888,7 +20292,7 @@ function assignmentForTaskCreate(input) {
|
|
|
19888
20292
|
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
19889
20293
|
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
19890
20294
|
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
19891
|
-
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
|
|
20295
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
19892
20296
|
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
19893
20297
|
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
19894
20298
|
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
@@ -20099,7 +20503,13 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
20099
20503
|
moveTasksToColumnId: { type: "string" },
|
|
20100
20504
|
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
20101
20505
|
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
20102
|
-
gateEnforcement: {
|
|
20506
|
+
gateEnforcement: {
|
|
20507
|
+
type: "string",
|
|
20508
|
+
// WS-023: `'off'` is deliberately absent. The agent whose work this gate
|
|
20509
|
+
// checks must not be able to switch it off; it may only tighten. Turning
|
|
20510
|
+
// a gate off stays a human decision, made through board config.
|
|
20511
|
+
enum: ["strict", "soft"]
|
|
20512
|
+
},
|
|
20103
20513
|
subtasks: {
|
|
20104
20514
|
type: "array",
|
|
20105
20515
|
minItems: 2,
|
|
@@ -20141,7 +20551,7 @@ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
|
20141
20551
|
|
|
20142
20552
|
// src/session-kanban.ts
|
|
20143
20553
|
import { watch } from "node:fs";
|
|
20144
|
-
import { basename as
|
|
20554
|
+
import { basename as basename13, dirname as dirname15 } from "node:path";
|
|
20145
20555
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
20146
20556
|
import {
|
|
20147
20557
|
loadPlan,
|
|
@@ -20231,7 +20641,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
|
20231
20641
|
description: "Live session work: todos, tasks, and plan items.",
|
|
20232
20642
|
tags: sessionBoardTags(sessionId),
|
|
20233
20643
|
columns: SESSION_KANBAN_COLUMNS,
|
|
20234
|
-
generatedBy: `session-kanban:${sessionId}
|
|
20644
|
+
generatedBy: `session-kanban:${sessionId}`,
|
|
20645
|
+
kind: "session_mirror",
|
|
20646
|
+
retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
|
|
20235
20647
|
});
|
|
20236
20648
|
}
|
|
20237
20649
|
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
@@ -20466,12 +20878,26 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
|
20466
20878
|
function fireAndForget(context, work) {
|
|
20467
20879
|
void work.catch((err) => {
|
|
20468
20880
|
const message = err instanceof Error ? err.message : String(err);
|
|
20469
|
-
console.warn(
|
|
20881
|
+
console.warn(
|
|
20882
|
+
JSON.stringify({
|
|
20883
|
+
level: "warn",
|
|
20884
|
+
event: "session-kanban",
|
|
20885
|
+
context,
|
|
20886
|
+
message,
|
|
20887
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20888
|
+
})
|
|
20889
|
+
);
|
|
20470
20890
|
});
|
|
20471
20891
|
}
|
|
20472
20892
|
function broadcastTodoUpdate(context, todos) {
|
|
20473
20893
|
const sessionId = context.session?.id ?? "";
|
|
20474
20894
|
if (!context.agentId || !sessionId) return;
|
|
20895
|
+
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
20896
|
+
for (const todo of todos) {
|
|
20897
|
+
if (todo.status === "completed") statusCounts.completed++;
|
|
20898
|
+
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
20899
|
+
else statusCounts.pending++;
|
|
20900
|
+
}
|
|
20475
20901
|
const projectDir = resolveWstackPaths3({ projectRoot: context.projectRoot }).projectDir;
|
|
20476
20902
|
const mailbox = getSharedProjectMailbox(projectDir);
|
|
20477
20903
|
void mailbox.send({
|
|
@@ -20483,11 +20909,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
20483
20909
|
kind: "kanban.todos.updated",
|
|
20484
20910
|
sessionId,
|
|
20485
20911
|
revision: context.state.revision,
|
|
20486
|
-
todos
|
|
20912
|
+
todoCount: todos.length,
|
|
20913
|
+
statusCounts
|
|
20487
20914
|
}),
|
|
20488
20915
|
priority: "normal",
|
|
20489
|
-
senderSessionId: sessionId
|
|
20490
|
-
ttlMs: 6 * 60 * 60 * 1e3
|
|
20916
|
+
senderSessionId: sessionId
|
|
20491
20917
|
}).catch(() => {
|
|
20492
20918
|
});
|
|
20493
20919
|
}
|
|
@@ -20623,8 +21049,8 @@ function attachSessionKanbanMirror(context) {
|
|
|
20623
21049
|
const name = filename?.toString();
|
|
20624
21050
|
const currentPlanPath = context.meta["plan.path"];
|
|
20625
21051
|
const currentTaskPath = context.meta["task.path"];
|
|
20626
|
-
const planName = typeof currentPlanPath === "string" ?
|
|
20627
|
-
const taskName = typeof currentTaskPath === "string" ?
|
|
21052
|
+
const planName = typeof currentPlanPath === "string" ? basename13(currentPlanPath) : "";
|
|
21053
|
+
const taskName = typeof currentTaskPath === "string" ? basename13(currentTaskPath) : "";
|
|
20628
21054
|
if (name && name !== planName && name !== taskName) return;
|
|
20629
21055
|
if (timer) clearTimeout(timer);
|
|
20630
21056
|
timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
|
|
@@ -21640,11 +22066,11 @@ var lintTool = {
|
|
|
21640
22066
|
}
|
|
21641
22067
|
};
|
|
21642
22068
|
async function detectLinter(cwd) {
|
|
21643
|
-
const { stat:
|
|
22069
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
21644
22070
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
21645
22071
|
for (const f of checks) {
|
|
21646
22072
|
try {
|
|
21647
|
-
await
|
|
22073
|
+
await stat19(`${cwd}/${f}`);
|
|
21648
22074
|
if (f.includes("biome")) return "biome";
|
|
21649
22075
|
if (f.includes("eslint")) return "eslint";
|
|
21650
22076
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -21888,6 +22314,9 @@ var outdatedTool = {
|
|
|
21888
22314
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
21889
22315
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
21890
22316
|
// search) but missed this one; applying the same contract here.
|
|
22317
|
+
// WS-046: gives permission decisions something to key on.
|
|
22318
|
+
// The working directory scanned.
|
|
22319
|
+
subjectKey: "cwd",
|
|
21891
22320
|
mutating: true,
|
|
21892
22321
|
// Capability is outbound network — the tool only hits the package
|
|
21893
22322
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -22069,6 +22498,10 @@ var patchTool = {
|
|
|
22069
22498
|
useInstead: ["edit"]
|
|
22070
22499
|
},
|
|
22071
22500
|
permission: "confirm",
|
|
22501
|
+
// WS-046: gives permission decisions something to key on.
|
|
22502
|
+
// The target directory. The patch body itself is unstable (every diff
|
|
22503
|
+
// differs), so it could never re-match its own stored rule.
|
|
22504
|
+
subjectKey: "directory",
|
|
22072
22505
|
mutating: true,
|
|
22073
22506
|
capabilities: ["fs.write"],
|
|
22074
22507
|
icon: "edit",
|
|
@@ -22135,8 +22568,8 @@ var patchTool = {
|
|
|
22135
22568
|
const before = beforeContents.get(target) ?? null;
|
|
22136
22569
|
const after = await readTextForTracking(target);
|
|
22137
22570
|
if (after === null || after === before) continue;
|
|
22138
|
-
const
|
|
22139
|
-
if (
|
|
22571
|
+
const stat19 = await fs27.stat(target).catch(() => null);
|
|
22572
|
+
if (stat19) ctx.recordRead?.(target, stat19.mtimeMs, "write", sha256hex(after));
|
|
22140
22573
|
ctx.session?.recordFileChange?.({
|
|
22141
22574
|
path: target,
|
|
22142
22575
|
action: before === null ? "created" : "modified",
|
|
@@ -22161,8 +22594,8 @@ var patchTool = {
|
|
|
22161
22594
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
22162
22595
|
async function readTextForTracking(absPath) {
|
|
22163
22596
|
try {
|
|
22164
|
-
const
|
|
22165
|
-
if (!
|
|
22597
|
+
const stat19 = await fs27.stat(absPath);
|
|
22598
|
+
if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
|
|
22166
22599
|
const buf = await fs27.readFile(absPath);
|
|
22167
22600
|
if (buf.includes(0)) return null;
|
|
22168
22601
|
return buf.toString("utf8");
|
|
@@ -22192,7 +22625,13 @@ function runPatch(args, cwd, signal) {
|
|
|
22192
22625
|
let stdout = "";
|
|
22193
22626
|
let stderr = "";
|
|
22194
22627
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
22195
|
-
const child = spawn14("patch", args, {
|
|
22628
|
+
const child = spawn14("patch", args, {
|
|
22629
|
+
cwd,
|
|
22630
|
+
signal,
|
|
22631
|
+
env,
|
|
22632
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22633
|
+
windowsHide: true
|
|
22634
|
+
});
|
|
22196
22635
|
child.stdout?.on("data", (c) => {
|
|
22197
22636
|
stdout += c.toString();
|
|
22198
22637
|
});
|
|
@@ -22213,6 +22652,7 @@ function extractPatchedFiles(output) {
|
|
|
22213
22652
|
}
|
|
22214
22653
|
|
|
22215
22654
|
// src/plan.ts
|
|
22655
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22216
22656
|
import {
|
|
22217
22657
|
addPlanItem,
|
|
22218
22658
|
clearPlan,
|
|
@@ -22220,20 +22660,20 @@ import {
|
|
|
22220
22660
|
formatPlan,
|
|
22221
22661
|
getPlanTemplate,
|
|
22222
22662
|
mutatePlan as mutatePlan2,
|
|
22663
|
+
mutateTasks as mutateTasks2,
|
|
22223
22664
|
removePlanItem,
|
|
22224
22665
|
setPlanItemStatus
|
|
22225
22666
|
} from "@wrongstack/core/storage";
|
|
22226
|
-
import {
|
|
22227
|
-
mutateTasks as mutateTasks2
|
|
22228
|
-
} from "@wrongstack/core/storage";
|
|
22229
22667
|
import { formatTaskList } from "@wrongstack/core/utils";
|
|
22230
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22231
22668
|
var planTool = {
|
|
22232
22669
|
name: "plan",
|
|
22233
22670
|
category: "Session",
|
|
22234
22671
|
description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
|
|
22235
22672
|
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
|
|
22236
22673
|
permission: "confirm",
|
|
22674
|
+
// WS-046: gives permission decisions something to key on.
|
|
22675
|
+
// The action performed; plan has no single file or path subject.
|
|
22676
|
+
subjectKey: "action",
|
|
22237
22677
|
mutating: true,
|
|
22238
22678
|
capabilities: ["fs.write"],
|
|
22239
22679
|
icon: "plan",
|
|
@@ -22290,7 +22730,10 @@ var planTool = {
|
|
|
22290
22730
|
let planPath;
|
|
22291
22731
|
if (input.scope === "project") {
|
|
22292
22732
|
if (typeof sessionPlanPath === "string") {
|
|
22293
|
-
const lastSep = Math.max(
|
|
22733
|
+
const lastSep = Math.max(
|
|
22734
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
22735
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
22736
|
+
);
|
|
22294
22737
|
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
22295
22738
|
}
|
|
22296
22739
|
} else {
|
|
@@ -22327,7 +22770,11 @@ var planTool = {
|
|
|
22327
22770
|
case "start":
|
|
22328
22771
|
case "done": {
|
|
22329
22772
|
if (!input.target) {
|
|
22330
|
-
early = mkResult(
|
|
22773
|
+
early = mkResult(
|
|
22774
|
+
p,
|
|
22775
|
+
false,
|
|
22776
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22777
|
+
);
|
|
22331
22778
|
return p;
|
|
22332
22779
|
}
|
|
22333
22780
|
const next = setPlanItemStatus(
|
|
@@ -22355,7 +22802,11 @@ var planTool = {
|
|
|
22355
22802
|
}
|
|
22356
22803
|
case "promote": {
|
|
22357
22804
|
if (!input.target) {
|
|
22358
|
-
early = mkResult(
|
|
22805
|
+
early = mkResult(
|
|
22806
|
+
p,
|
|
22807
|
+
false,
|
|
22808
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22809
|
+
);
|
|
22359
22810
|
return p;
|
|
22360
22811
|
}
|
|
22361
22812
|
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
@@ -22398,7 +22849,11 @@ var planTool = {
|
|
|
22398
22849
|
return clearPlan(p);
|
|
22399
22850
|
case "taskify": {
|
|
22400
22851
|
if (!input.target) {
|
|
22401
|
-
early = mkResult(
|
|
22852
|
+
early = mkResult(
|
|
22853
|
+
p,
|
|
22854
|
+
false,
|
|
22855
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
22856
|
+
);
|
|
22402
22857
|
return p;
|
|
22403
22858
|
}
|
|
22404
22859
|
let itemIdx = -1;
|
|
@@ -22471,7 +22926,11 @@ var planTool = {
|
|
|
22471
22926
|
${formatTaskList(taskFile.tasks)}`
|
|
22472
22927
|
);
|
|
22473
22928
|
} catch (err) {
|
|
22474
|
-
return mkResult(
|
|
22929
|
+
return mkResult(
|
|
22930
|
+
plan,
|
|
22931
|
+
false,
|
|
22932
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
22933
|
+
);
|
|
22475
22934
|
}
|
|
22476
22935
|
}
|
|
22477
22936
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
@@ -22548,9 +23007,9 @@ var readTool = {
|
|
|
22548
23007
|
}
|
|
22549
23008
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
22550
23009
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
22551
|
-
let
|
|
23010
|
+
let stat19;
|
|
22552
23011
|
try {
|
|
22553
|
-
|
|
23012
|
+
stat19 = await fs28.stat(absPath);
|
|
22554
23013
|
} catch (err) {
|
|
22555
23014
|
const code = err.code;
|
|
22556
23015
|
if (code === "ENOENT") {
|
|
@@ -22569,7 +23028,7 @@ var readTool = {
|
|
|
22569
23028
|
cause: err
|
|
22570
23029
|
});
|
|
22571
23030
|
}
|
|
22572
|
-
if (!
|
|
23031
|
+
if (!stat19.isFile()) {
|
|
22573
23032
|
throw new FsError({
|
|
22574
23033
|
message: `read: "${input.path}" is not a regular file`,
|
|
22575
23034
|
code: "FS_READ_FAILED",
|
|
@@ -22577,23 +23036,23 @@ var readTool = {
|
|
|
22577
23036
|
context: { reason: "not-a-regular-file" }
|
|
22578
23037
|
});
|
|
22579
23038
|
}
|
|
22580
|
-
if (
|
|
23039
|
+
if (stat19.size > MAX_BYTES2) {
|
|
22581
23040
|
throw new FsError({
|
|
22582
|
-
message: `read: file too large (${
|
|
23041
|
+
message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
|
|
22583
23042
|
code: "FS_READ_FAILED",
|
|
22584
23043
|
path: absPath,
|
|
22585
|
-
context: { size:
|
|
23044
|
+
context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
22586
23045
|
});
|
|
22587
23046
|
}
|
|
22588
23047
|
const offset = Math.max(1, input.offset ?? 1);
|
|
22589
23048
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
22590
23049
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
22591
23050
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
22592
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
22593
|
-
ctx.recordRead(absPath,
|
|
23051
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
|
|
23052
|
+
ctx.recordRead(absPath, stat19.mtimeMs);
|
|
22594
23053
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22595
23054
|
return {
|
|
22596
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
23055
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat19.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
22597
23056
|
total_lines: prior.totalLines,
|
|
22598
23057
|
encoding: "utf8",
|
|
22599
23058
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -22611,11 +23070,11 @@ var readTool = {
|
|
|
22611
23070
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
22612
23071
|
const total = allLines.length;
|
|
22613
23072
|
if (input.mode === "summary") {
|
|
22614
|
-
ctx.recordRead(absPath,
|
|
22615
|
-
rememberReadRange(ctx, absPath,
|
|
23073
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23074
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
|
|
22616
23075
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22617
23076
|
return {
|
|
22618
|
-
text: summarizeFile(input.path,
|
|
23077
|
+
text: summarizeFile(input.path, stat19.size, allLines),
|
|
22619
23078
|
total_lines: total,
|
|
22620
23079
|
encoding: "utf8",
|
|
22621
23080
|
truncated: total > 200,
|
|
@@ -22627,8 +23086,8 @@ var readTool = {
|
|
|
22627
23086
|
};
|
|
22628
23087
|
}
|
|
22629
23088
|
if (limit === 0) {
|
|
22630
|
-
ctx.recordRead(absPath,
|
|
22631
|
-
rememberReadRange(ctx, absPath,
|
|
23089
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23090
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
|
|
22632
23091
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22633
23092
|
return {
|
|
22634
23093
|
text: "",
|
|
@@ -22640,8 +23099,8 @@ var readTool = {
|
|
|
22640
23099
|
};
|
|
22641
23100
|
}
|
|
22642
23101
|
if (offset > total) {
|
|
22643
|
-
ctx.recordRead(absPath,
|
|
22644
|
-
rememberReadRange(ctx, absPath,
|
|
23102
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23103
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
|
|
22645
23104
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22646
23105
|
return {
|
|
22647
23106
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -22656,8 +23115,8 @@ var readTool = {
|
|
|
22656
23115
|
const truncated = offset - 1 + slice.length < total;
|
|
22657
23116
|
const width = String(offset + slice.length - 1).length;
|
|
22658
23117
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
22659
|
-
ctx.recordRead(absPath,
|
|
22660
|
-
rememberReadRange(ctx, absPath,
|
|
23118
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23119
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
|
|
22661
23120
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22662
23121
|
return {
|
|
22663
23122
|
text: numbered,
|
|
@@ -22763,20 +23222,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
22763
23222
|
}
|
|
22764
23223
|
|
|
22765
23224
|
// src/replace.ts
|
|
22766
|
-
import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
|
|
22767
23225
|
import { spawn as spawn15 } from "node:child_process";
|
|
22768
23226
|
import * as fs29 from "node:fs/promises";
|
|
22769
23227
|
import * as path33 from "node:path";
|
|
23228
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22770
23229
|
import {
|
|
22771
23230
|
atomicWrite as atomicWrite3,
|
|
22772
23231
|
buildChildEnv as buildChildEnv9,
|
|
22773
23232
|
compileGlob as compileGlob4,
|
|
22774
23233
|
detectNewlineStyle as detectNewlineStyle2,
|
|
23234
|
+
expectDefined as expectDefined8,
|
|
22775
23235
|
normalizeToLf as normalizeToLf2,
|
|
22776
23236
|
toStyle as toStyle2,
|
|
22777
23237
|
unifiedDiff as unifiedDiff2
|
|
22778
23238
|
} from "@wrongstack/core/utils";
|
|
22779
|
-
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22780
23239
|
init_util();
|
|
22781
23240
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
22782
23241
|
var replaceTool = {
|
|
@@ -22785,6 +23244,10 @@ var replaceTool = {
|
|
|
22785
23244
|
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
|
|
22786
23245
|
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
22787
23246
|
permission: "confirm",
|
|
23247
|
+
// WS-046: gives permission decisions something to key on.
|
|
23248
|
+
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
23249
|
+
// where a bulk replace may run.
|
|
23250
|
+
subjectKey: "files",
|
|
22788
23251
|
mutating: true,
|
|
22789
23252
|
capabilities: ["fs.write"],
|
|
22790
23253
|
icon: "edit",
|
|
@@ -22857,8 +23320,8 @@ var replaceTool = {
|
|
|
22857
23320
|
}
|
|
22858
23321
|
const rel = path33.relative(realRoot, realPath);
|
|
22859
23322
|
if (rel.startsWith("..") || path33.isAbsolute(rel)) continue;
|
|
22860
|
-
const
|
|
22861
|
-
if (!
|
|
23323
|
+
const stat19 = await fs29.stat(realPath).catch(() => null);
|
|
23324
|
+
if (!stat19?.isFile()) continue;
|
|
22862
23325
|
let content;
|
|
22863
23326
|
try {
|
|
22864
23327
|
const buf = await fs29.readFile(realPath);
|
|
@@ -22883,7 +23346,7 @@ var replaceTool = {
|
|
|
22883
23346
|
totalReplacements += count;
|
|
22884
23347
|
if (!dryRun) {
|
|
22885
23348
|
const newContent = toStyle2(newContentLf, style);
|
|
22886
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
23349
|
+
await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
|
|
22887
23350
|
const written = await fs29.stat(realPath).catch(() => null);
|
|
22888
23351
|
if (written) {
|
|
22889
23352
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -22923,8 +23386,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
22923
23386
|
const resolved = [];
|
|
22924
23387
|
for (const p of parts) {
|
|
22925
23388
|
const absPath = safeResolve(p, ctx);
|
|
22926
|
-
const
|
|
22927
|
-
if (
|
|
23389
|
+
const stat19 = await fs29.stat(absPath).catch(() => null);
|
|
23390
|
+
if (stat19?.isFile()) {
|
|
22928
23391
|
resolved.push(absPath);
|
|
22929
23392
|
}
|
|
22930
23393
|
}
|
|
@@ -22944,7 +23407,11 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
22944
23407
|
function checkRg() {
|
|
22945
23408
|
return new Promise((resolve17) => {
|
|
22946
23409
|
try {
|
|
22947
|
-
const p = spawn15("rg", ["--version"], {
|
|
23410
|
+
const p = spawn15("rg", ["--version"], {
|
|
23411
|
+
env: buildChildEnv9(),
|
|
23412
|
+
stdio: "ignore",
|
|
23413
|
+
windowsHide: true
|
|
23414
|
+
});
|
|
22948
23415
|
p.on("error", () => resolve17(false));
|
|
22949
23416
|
p.on("close", (code) => resolve17(code === 0));
|
|
22950
23417
|
} catch {
|
|
@@ -22995,8 +23462,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
22995
23462
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
22996
23463
|
const full = path33.join(dir, e.name);
|
|
22997
23464
|
try {
|
|
22998
|
-
const
|
|
22999
|
-
if (
|
|
23465
|
+
const stat19 = await fs29.lstat(full);
|
|
23466
|
+
if (stat19.isSymbolicLink()) continue;
|
|
23000
23467
|
} catch {
|
|
23001
23468
|
continue;
|
|
23002
23469
|
}
|
|
@@ -23120,6 +23587,7 @@ var scaffoldTool = {
|
|
|
23120
23587
|
usageHint: "PREFERRED FOR SCAFFOLDING:\n\n- Use built-in templates when they match your needs (e.g. react-component, npm-package).\n- Supports `dry_run` so you can preview exactly what will be created.\n- Has the powerful `fs.write.outside-project` capability \u2014 review paths carefully.\nMuch cleaner and safer than manually writing multiple files.",
|
|
23121
23588
|
permission: "confirm",
|
|
23122
23589
|
mutating: true,
|
|
23590
|
+
subjectKey: "name",
|
|
23123
23591
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
23124
23592
|
icon: "scaffold",
|
|
23125
23593
|
timeoutMs: 3e4,
|
|
@@ -23194,15 +23662,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
23194
23662
|
output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
|
|
23195
23663
|
};
|
|
23196
23664
|
}
|
|
23665
|
+
function escapeRegexLiteral(value) {
|
|
23666
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23667
|
+
}
|
|
23197
23668
|
function substituteVars(content, name, vars) {
|
|
23669
|
+
const kebab = name.toLowerCase().replace(/\s+/g, "-");
|
|
23670
|
+
const pascal = name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase());
|
|
23198
23671
|
let result = content;
|
|
23199
|
-
result = result.replace(/\{\{name\}\}/g,
|
|
23200
|
-
result = result.replace(
|
|
23201
|
-
/\{\{Name\}\}/g,
|
|
23202
|
-
name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
|
|
23203
|
-
);
|
|
23672
|
+
result = result.replace(/\{\{name\}\}/g, () => kebab);
|
|
23673
|
+
result = result.replace(/\{\{Name\}\}/g, () => pascal);
|
|
23204
23674
|
for (const [k, v] of Object.entries(vars)) {
|
|
23205
|
-
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
|
|
23675
|
+
result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
|
|
23206
23676
|
}
|
|
23207
23677
|
return result;
|
|
23208
23678
|
}
|
|
@@ -24138,11 +24608,11 @@ var testTool = {
|
|
|
24138
24608
|
}
|
|
24139
24609
|
};
|
|
24140
24610
|
async function detectRunner(cwd) {
|
|
24141
|
-
const { stat:
|
|
24611
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
24142
24612
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
24143
24613
|
for (const f of candidates) {
|
|
24144
24614
|
try {
|
|
24145
|
-
await
|
|
24615
|
+
await stat19(path35.join(cwd, f));
|
|
24146
24616
|
if (f.includes("vitest")) return "vitest";
|
|
24147
24617
|
if (f.includes("jest")) return "jest";
|
|
24148
24618
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -24539,12 +25009,19 @@ var toolSearchTool = {
|
|
|
24539
25009
|
};
|
|
24540
25010
|
|
|
24541
25011
|
// src/tool-use.ts
|
|
25012
|
+
import {
|
|
25013
|
+
GOVERNED_TOOL_EXECUTOR_META_KEY as GOVERNED_TOOL_EXECUTOR_META_KEY2
|
|
25014
|
+
} from "@wrongstack/core/types";
|
|
24542
25015
|
var toolUseTool = {
|
|
24543
25016
|
name: "tool_use",
|
|
24544
25017
|
category: "Meta",
|
|
24545
25018
|
description: "Directly execute any registered tool by its exact name, bypassing normal discovery. This is a powerful meta-tool intended for cases where the agent has a clear plan and knows precisely which tool to invoke.",
|
|
24546
25019
|
usageHint: "ADVANCED META TOOL \u2014 USE WITH CARE:\n\n- Only use when you are certain of the exact tool name and its expected input shape.\n- Prefer using the normal tool calling mechanism when possible.\n- Very useful in batch-tool-use or when orchestrating complex workflows programmatically.\n- The call still goes through full permission checks and capability validation.",
|
|
24547
25020
|
permission: "confirm",
|
|
25021
|
+
// WS-046: gives permission decisions something to key on.
|
|
25022
|
+
// The tool being invoked through this indirection — the whole point of a
|
|
25023
|
+
// permission decision here is WHICH tool is being reached.
|
|
25024
|
+
subjectKey: "tool",
|
|
24548
25025
|
mutating: true,
|
|
24549
25026
|
timeoutMs: 6e4,
|
|
24550
25027
|
capabilities: ["tool.mutate.any"],
|
|
@@ -24563,7 +25040,7 @@ var toolUseTool = {
|
|
|
24563
25040
|
},
|
|
24564
25041
|
required: ["tool"]
|
|
24565
25042
|
},
|
|
24566
|
-
async execute(input, ctx
|
|
25043
|
+
async execute(input, ctx) {
|
|
24567
25044
|
const start = Date.now();
|
|
24568
25045
|
if (!input?.tool) {
|
|
24569
25046
|
return {
|
|
@@ -24582,6 +25059,14 @@ var toolUseTool = {
|
|
|
24582
25059
|
executionMs: Date.now() - start
|
|
24583
25060
|
};
|
|
24584
25061
|
}
|
|
25062
|
+
if (tool.name === toolUseTool.name) {
|
|
25063
|
+
return {
|
|
25064
|
+
tool: input.tool,
|
|
25065
|
+
success: false,
|
|
25066
|
+
error: "tool_use: recursive meta-tool execution is not allowed",
|
|
25067
|
+
executionMs: Date.now() - start
|
|
25068
|
+
};
|
|
25069
|
+
}
|
|
24585
25070
|
if (tool.permission === "deny") {
|
|
24586
25071
|
return {
|
|
24587
25072
|
tool: input.tool,
|
|
@@ -24590,12 +25075,21 @@ var toolUseTool = {
|
|
|
24590
25075
|
executionMs: Date.now() - start
|
|
24591
25076
|
};
|
|
24592
25077
|
}
|
|
25078
|
+
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY2];
|
|
25079
|
+
if (typeof governedExecute !== "function") {
|
|
25080
|
+
return {
|
|
25081
|
+
tool: input.tool,
|
|
25082
|
+
success: false,
|
|
25083
|
+
error: "tool_use: governed nested execution is unavailable; call the tool directly",
|
|
25084
|
+
executionMs: Date.now() - start
|
|
25085
|
+
};
|
|
25086
|
+
}
|
|
24593
25087
|
try {
|
|
24594
|
-
const result = await
|
|
25088
|
+
const result = await governedExecute(input.tool, input.input ?? {});
|
|
24595
25089
|
return {
|
|
24596
25090
|
tool: input.tool,
|
|
24597
|
-
success:
|
|
24598
|
-
result,
|
|
25091
|
+
success: result.success,
|
|
25092
|
+
...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
|
|
24599
25093
|
executionMs: Date.now() - start
|
|
24600
25094
|
};
|
|
24601
25095
|
} catch (e) {
|
|
@@ -24611,10 +25105,12 @@ var toolUseTool = {
|
|
|
24611
25105
|
|
|
24612
25106
|
// src/tree.ts
|
|
24613
25107
|
init_util();
|
|
24614
|
-
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
24615
25108
|
import * as fs32 from "node:fs/promises";
|
|
24616
25109
|
import * as path36 from "node:path";
|
|
25110
|
+
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
24617
25111
|
var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
|
|
25112
|
+
var DEFAULT_MAX_ENTRIES2 = 5e3;
|
|
25113
|
+
var MAX_TREE_OUTPUT_BYTES = 256 * 1024;
|
|
24618
25114
|
var treeTool = {
|
|
24619
25115
|
name: "tree",
|
|
24620
25116
|
category: "Filesystem",
|
|
@@ -24658,6 +25154,12 @@ var treeTool = {
|
|
|
24658
25154
|
show_hidden: {
|
|
24659
25155
|
type: "boolean",
|
|
24660
25156
|
description: "Show hidden files starting with . (default: false)"
|
|
25157
|
+
},
|
|
25158
|
+
max_entries: {
|
|
25159
|
+
type: "integer",
|
|
25160
|
+
description: `Maximum entries retained in the rendered tree (default ${DEFAULT_MAX_ENTRIES2}).`,
|
|
25161
|
+
minimum: 1,
|
|
25162
|
+
maximum: 5e4
|
|
24661
25163
|
}
|
|
24662
25164
|
}
|
|
24663
25165
|
},
|
|
@@ -24671,7 +25173,7 @@ var treeTool = {
|
|
|
24671
25173
|
if (!final) throw new Error("tree: stream ended without final event");
|
|
24672
25174
|
return final;
|
|
24673
25175
|
},
|
|
24674
|
-
async *executeStream(input, ctx) {
|
|
25176
|
+
async *executeStream(input, ctx, opts) {
|
|
24675
25177
|
const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
24676
25178
|
const maxDepth = input.depth ?? 3;
|
|
24677
25179
|
const showFiles = input.show_files ?? true;
|
|
@@ -24679,8 +25181,17 @@ var treeTool = {
|
|
|
24679
25181
|
const showHidden = input.show_hidden ?? false;
|
|
24680
25182
|
const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...input.exclude ?? []]);
|
|
24681
25183
|
const filterGlob = input.glob;
|
|
25184
|
+
const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES2;
|
|
25185
|
+
const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
24682
25186
|
const lines = [basePath];
|
|
24683
25187
|
const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
|
|
25188
|
+
const retention = {
|
|
25189
|
+
entries: 0,
|
|
25190
|
+
outputBytes: Buffer.byteLength(basePath, "utf8"),
|
|
25191
|
+
maxEntries,
|
|
25192
|
+
maxOutputBytes: MAX_TREE_OUTPUT_BYTES,
|
|
25193
|
+
truncated: false
|
|
25194
|
+
};
|
|
24684
25195
|
const queue = [];
|
|
24685
25196
|
const FLUSH_EVERY = 200;
|
|
24686
25197
|
let lastEmittedTotal = 0;
|
|
@@ -24707,12 +25218,19 @@ var treeTool = {
|
|
|
24707
25218
|
isLast: true,
|
|
24708
25219
|
totalFiles: totals.totalFiles,
|
|
24709
25220
|
totalDirs: totals.totalDirs,
|
|
24710
|
-
onProgress: tickProgress
|
|
25221
|
+
onProgress: tickProgress,
|
|
25222
|
+
retention,
|
|
25223
|
+
signal
|
|
24711
25224
|
});
|
|
24712
25225
|
let walkDone = false;
|
|
24713
|
-
walkPromise.
|
|
24714
|
-
|
|
24715
|
-
|
|
25226
|
+
void walkPromise.then(
|
|
25227
|
+
() => {
|
|
25228
|
+
walkDone = true;
|
|
25229
|
+
},
|
|
25230
|
+
() => {
|
|
25231
|
+
walkDone = true;
|
|
25232
|
+
}
|
|
25233
|
+
);
|
|
24716
25234
|
while (!walkDone || queue.length > 0) {
|
|
24717
25235
|
if (queue.length > 0) {
|
|
24718
25236
|
yield expectDefined10(queue.shift());
|
|
@@ -24735,13 +25253,15 @@ var treeTool = {
|
|
|
24735
25253
|
tree: lines.join("\n"),
|
|
24736
25254
|
total_files: totals.totalFiles.value,
|
|
24737
25255
|
total_dirs: totals.totalDirs.value,
|
|
24738
|
-
truncated:
|
|
25256
|
+
truncated: retention.truncated,
|
|
24739
25257
|
path: basePath
|
|
24740
25258
|
}
|
|
24741
25259
|
};
|
|
24742
25260
|
}
|
|
24743
25261
|
};
|
|
24744
25262
|
async function walkDir(dir, depth, opts) {
|
|
25263
|
+
opts.signal.throwIfAborted();
|
|
25264
|
+
if (opts.retention.truncated) return;
|
|
24745
25265
|
const entries = await fs32.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
24746
25266
|
const filtered = entries.filter((e) => {
|
|
24747
25267
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
@@ -24761,6 +25281,8 @@ async function walkDir(dir, depth, opts) {
|
|
|
24761
25281
|
return a.name.localeCompare(b.name);
|
|
24762
25282
|
});
|
|
24763
25283
|
for (let i = 0; i < items.length; i++) {
|
|
25284
|
+
opts.signal.throwIfAborted();
|
|
25285
|
+
if (opts.retention.truncated) return;
|
|
24764
25286
|
const entry = items[i];
|
|
24765
25287
|
if (!entry) continue;
|
|
24766
25288
|
const isLast = i === items.length - 1;
|
|
@@ -24769,7 +25291,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
24769
25291
|
const displayName = entry.name + (entry.isDirectory() ? "/" : "");
|
|
24770
25292
|
if (!opts.showDirs && entry.isDirectory()) continue;
|
|
24771
25293
|
if (!opts.showFiles && entry.isFile()) continue;
|
|
24772
|
-
opts.
|
|
25294
|
+
const line = opts.prefix + branch + displayName;
|
|
25295
|
+
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
|
|
25296
|
+
if (opts.retention.entries >= opts.retention.maxEntries || opts.retention.outputBytes + lineBytes > opts.retention.maxOutputBytes) {
|
|
25297
|
+
opts.retention.truncated = true;
|
|
25298
|
+
return;
|
|
25299
|
+
}
|
|
25300
|
+
opts.lines.push(line);
|
|
25301
|
+
opts.retention.entries++;
|
|
25302
|
+
opts.retention.outputBytes += lineBytes;
|
|
24773
25303
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
24774
25304
|
const childPrefix = opts.prefix + connector;
|
|
24775
25305
|
await walkDir(path36.join(dir, entry.name), depth + 1, {
|
|
@@ -24884,11 +25414,11 @@ var typecheckTool = {
|
|
|
24884
25414
|
}
|
|
24885
25415
|
};
|
|
24886
25416
|
async function findTsConfig(cwd) {
|
|
24887
|
-
const { stat:
|
|
25417
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
24888
25418
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
24889
25419
|
for (const f of candidates) {
|
|
24890
25420
|
try {
|
|
24891
|
-
const s = await
|
|
25421
|
+
const s = await stat19(path37.join(cwd, f));
|
|
24892
25422
|
if (s.isFile()) return path37.join(cwd, f);
|
|
24893
25423
|
} catch {
|
|
24894
25424
|
}
|
|
@@ -24963,12 +25493,12 @@ async function prepareWrite(input, ctx) {
|
|
|
24963
25493
|
let existed = false;
|
|
24964
25494
|
let prev = "";
|
|
24965
25495
|
try {
|
|
24966
|
-
const
|
|
24967
|
-
existed =
|
|
25496
|
+
const stat19 = await fs33.stat(absPath);
|
|
25497
|
+
existed = stat19.isFile();
|
|
24968
25498
|
if (existed) {
|
|
24969
25499
|
if (!ctx.hasRead(absPath)) {
|
|
24970
25500
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24971
|
-
ctx.recordRead(absPath,
|
|
25501
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
|
|
24972
25502
|
} else {
|
|
24973
25503
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24974
25504
|
}
|
|
@@ -24985,8 +25515,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
24985
25515
|
await atomicWrite5(prepared.absPath, input.content);
|
|
24986
25516
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
24987
25517
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
24988
|
-
const
|
|
24989
|
-
ctx.recordRead(prepared.absPath,
|
|
25518
|
+
const stat19 = await fs33.stat(prepared.absPath);
|
|
25519
|
+
ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(input.content));
|
|
24990
25520
|
ctx.session.recordFileChange({
|
|
24991
25521
|
path: prepared.absPath,
|
|
24992
25522
|
action: prepared.existed ? "modified" : "created",
|
|
@@ -26174,9 +26704,9 @@ async function walk(root, dir, out) {
|
|
|
26174
26704
|
} else if (e.isFile()) {
|
|
26175
26705
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
26176
26706
|
try {
|
|
26177
|
-
const
|
|
26707
|
+
const stat19 = await fs34.stat(fullPath);
|
|
26178
26708
|
const rel = path38.relative(root, fullPath).split(path38.sep).join("/");
|
|
26179
|
-
out.push({ path: rel, bytes:
|
|
26709
|
+
out.push({ path: rel, bytes: stat19.size });
|
|
26180
26710
|
} catch {
|
|
26181
26711
|
}
|
|
26182
26712
|
}
|