@wrongstack/tools 0.296.4 → 0.298.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_spawn-stream.d.ts +2 -0
- package/dist/_util.d.ts +29 -1
- package/dist/audit.js +128 -65
- package/dist/bash.js +20 -4
- package/dist/batch-tool-use.js +70 -7
- package/dist/browser/index.js +31 -1
- package/dist/builtin.js +883 -351
- package/dist/codebase-index/index.js +95 -16
- package/dist/codebase-index/project-server-protocol.d.ts +26 -0
- package/dist/codebase-index/project-server.js +76 -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.js +562 -516
- package/dist/format.js +131 -65
- package/dist/git.js +22 -2
- package/dist/glob.js +11 -5
- package/dist/grep.js +160 -104
- package/dist/index.js +834 -290
- package/dist/install.js +135 -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 +143 -71
- package/dist/lint.js +128 -65
- package/dist/next-steps.d.ts +23 -0
- package/dist/next-steps.js +4 -0
- package/dist/outdated.js +101 -26
- package/dist/pack.js +883 -351
- package/dist/patch.js +11 -1
- package/dist/plan.js +32 -10
- package/dist/process-registry.d.ts +9 -0
- package/dist/process-registry.js +15 -0
- package/dist/ps-slash.js +15 -0
- package/dist/read.js +104 -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 +128 -65
- package/dist/todo.js +3 -1
- package/dist/tool-tier.js +883 -351
- 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 +128 -65
- package/dist/write.js +9 -6
- package/package.json +5 -4
- 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/tool-tier.js
CHANGED
|
@@ -81,11 +81,12 @@ function createOutputSpool(opts) {
|
|
|
81
81
|
return {
|
|
82
82
|
write(text) {
|
|
83
83
|
if (finalized || !text) return;
|
|
84
|
-
|
|
84
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
85
|
+
totalBytes += textBytes;
|
|
85
86
|
if (!stream && !failed) {
|
|
86
|
-
if (headBytes +
|
|
87
|
+
if (headBytes + textBytes <= threshold) {
|
|
87
88
|
head += text;
|
|
88
|
-
headBytes +=
|
|
89
|
+
headBytes += textBytes;
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
head += text;
|
|
@@ -95,7 +96,7 @@ function createOutputSpool(opts) {
|
|
|
95
96
|
}
|
|
96
97
|
if (stream) {
|
|
97
98
|
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
98
|
-
droppedBytes +=
|
|
99
|
+
droppedBytes += textBytes;
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
stream.write(text);
|
|
@@ -126,6 +127,73 @@ var init_output_spool = __esm({
|
|
|
126
127
|
}
|
|
127
128
|
});
|
|
128
129
|
|
|
130
|
+
// src/_win32-resolve.ts
|
|
131
|
+
import * as fs from "node:fs";
|
|
132
|
+
import * as path2 from "node:path";
|
|
133
|
+
function resolveWin32Command(cmd) {
|
|
134
|
+
if (process.platform !== "win32") return cmd;
|
|
135
|
+
if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
|
|
136
|
+
return cmd;
|
|
137
|
+
}
|
|
138
|
+
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
139
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
|
|
140
|
+
for (const dir of pathDirs) {
|
|
141
|
+
const base = path2.join(dir, cmd);
|
|
142
|
+
for (const ext of pathext) {
|
|
143
|
+
const full = `${base}${ext}`;
|
|
144
|
+
try {
|
|
145
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
146
|
+
return full;
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return cmd;
|
|
152
|
+
}
|
|
153
|
+
function resolvePowerShell(cmd) {
|
|
154
|
+
if (process.platform !== "win32") return cmd;
|
|
155
|
+
const lower = cmd.toLowerCase();
|
|
156
|
+
if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
|
|
157
|
+
return resolveWin32Command(cmd);
|
|
158
|
+
}
|
|
159
|
+
const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
|
|
160
|
+
const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
|
|
161
|
+
const resolved = resolveWin32Command(primary);
|
|
162
|
+
if (resolved !== primary) {
|
|
163
|
+
const fb = resolveWin32Command(fallback);
|
|
164
|
+
return fb === fallback ? cmd : fb;
|
|
165
|
+
}
|
|
166
|
+
return resolved;
|
|
167
|
+
}
|
|
168
|
+
function assertSafeWin32ShellArgs(args) {
|
|
169
|
+
for (const arg of args) {
|
|
170
|
+
if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function buildWin32CmdShimInvocation(command, args = []) {
|
|
178
|
+
assertSafeWin32ShellArgs([command, ...args]);
|
|
179
|
+
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
180
|
+
return {
|
|
181
|
+
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
182
|
+
args: ["/d", "/c", line],
|
|
183
|
+
windowsVerbatimArguments: true
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function quoteWin32CmdArg(arg) {
|
|
187
|
+
return `"${arg}"`;
|
|
188
|
+
}
|
|
189
|
+
var WIN32_SHELL_META;
|
|
190
|
+
var init_win32_resolve = __esm({
|
|
191
|
+
"src/_win32-resolve.ts"() {
|
|
192
|
+
"use strict";
|
|
193
|
+
WIN32_SHELL_META = /[&|<>"\r\n\0]/;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
129
197
|
// src/circuit-breaker.ts
|
|
130
198
|
var DEFAULT_MAX_CONSECUTIVE_FAILURES, DEFAULT_SLOW_CALL_THRESHOLD_MS, DEFAULT_MAX_SLOW_CALLS, DEFAULT_WINDOW_MS, DEFAULT_MAX_CALLS_PER_WINDOW, DEFAULT_COOLDOWN_MS, CircuitBreaker;
|
|
131
199
|
var init_circuit_breaker = __esm({
|
|
@@ -479,6 +547,7 @@ var init_process_registry = __esm({
|
|
|
479
547
|
}
|
|
480
548
|
/** Get all tracked processes. */
|
|
481
549
|
list() {
|
|
550
|
+
this._pruneAllStale();
|
|
482
551
|
return Array.from(this.processes.values());
|
|
483
552
|
}
|
|
484
553
|
/** Get processes filtered by name (e.g. 'bash', 'exec'). */
|
|
@@ -509,6 +578,7 @@ var init_process_registry = __esm({
|
|
|
509
578
|
* Combined stats for observability — used by /ps and the TUI status bar.
|
|
510
579
|
*/
|
|
511
580
|
stats() {
|
|
581
|
+
this._pruneAllStale();
|
|
512
582
|
return {
|
|
513
583
|
activeCount: this.activeCount,
|
|
514
584
|
backgroundCount: this.activeBackgroundCount,
|
|
@@ -748,74 +818,20 @@ var init_process_registry = __esm({
|
|
|
748
818
|
this.processes.delete(pid);
|
|
749
819
|
}
|
|
750
820
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
764
|
-
const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
|
|
765
|
-
for (const dir of pathDirs) {
|
|
766
|
-
const base = path2.join(dir, cmd);
|
|
767
|
-
for (const ext of pathext) {
|
|
768
|
-
const full = `${base}${ext}`;
|
|
769
|
-
try {
|
|
770
|
-
fs.accessSync(full, fs.constants.X_OK);
|
|
771
|
-
return full;
|
|
772
|
-
} catch {
|
|
821
|
+
/**
|
|
822
|
+
* Remove every stale entry, not just one PID. `list()`/`stats()` — the
|
|
823
|
+
* surfaces the TUI status bar and `/ps` poll — must prune too: a child
|
|
824
|
+
* whose 'close' event never fires (e.g. Windows grandchildren holding stdio
|
|
825
|
+
* open) would otherwise linger in the registry until someone looks up its
|
|
826
|
+
* exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
|
|
827
|
+
* wrong process. RAM-leak audit 2026-07-31, MEDIUM.
|
|
828
|
+
*/
|
|
829
|
+
_pruneAllStale() {
|
|
830
|
+
for (const [pid, entry] of this.processes) {
|
|
831
|
+
if (this._isStaleEntry(entry)) this.processes.delete(pid);
|
|
832
|
+
}
|
|
773
833
|
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
return cmd;
|
|
777
|
-
}
|
|
778
|
-
function resolvePowerShell(cmd) {
|
|
779
|
-
if (process.platform !== "win32") return cmd;
|
|
780
|
-
const lower = cmd.toLowerCase();
|
|
781
|
-
if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
|
|
782
|
-
return resolveWin32Command(cmd);
|
|
783
|
-
}
|
|
784
|
-
const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
|
|
785
|
-
const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
|
|
786
|
-
const resolved = resolveWin32Command(primary);
|
|
787
|
-
if (resolved !== primary) {
|
|
788
|
-
const fb = resolveWin32Command(fallback);
|
|
789
|
-
return fb === fallback ? cmd : fb;
|
|
790
|
-
}
|
|
791
|
-
return resolved;
|
|
792
|
-
}
|
|
793
|
-
function assertSafeWin32ShellArgs(args) {
|
|
794
|
-
for (const arg of args) {
|
|
795
|
-
if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
|
|
796
|
-
throw new Error(
|
|
797
|
-
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
function buildWin32CmdShimInvocation(command, args = []) {
|
|
803
|
-
assertSafeWin32ShellArgs([command, ...args]);
|
|
804
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
805
|
-
return {
|
|
806
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
807
|
-
args: ["/d", "/c", line],
|
|
808
|
-
windowsVerbatimArguments: true
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
function quoteWin32CmdArg(arg) {
|
|
812
|
-
return `"${arg}"`;
|
|
813
|
-
}
|
|
814
|
-
var WIN32_SHELL_META;
|
|
815
|
-
var init_win32_resolve = __esm({
|
|
816
|
-
"src/_win32-resolve.ts"() {
|
|
817
|
-
"use strict";
|
|
818
|
-
WIN32_SHELL_META = /[&|<>"\r\n\0]/;
|
|
834
|
+
};
|
|
819
835
|
}
|
|
820
836
|
});
|
|
821
837
|
|
|
@@ -831,8 +847,11 @@ async function* spawnStream(opts) {
|
|
|
831
847
|
const max = opts.maxBytes ?? 2e5;
|
|
832
848
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
833
849
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
850
|
+
const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
|
|
834
851
|
let stdout = "";
|
|
835
852
|
let stderr = "";
|
|
853
|
+
let stdoutRetainedBytes = 0;
|
|
854
|
+
let stderrRetainedBytes = 0;
|
|
836
855
|
let pending2 = "";
|
|
837
856
|
let error;
|
|
838
857
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
@@ -874,6 +893,7 @@ async function* spawnStream(opts) {
|
|
|
874
893
|
});
|
|
875
894
|
}
|
|
876
895
|
const queue = [];
|
|
896
|
+
let queuedBytes = 0;
|
|
877
897
|
let waiter;
|
|
878
898
|
let paused = false;
|
|
879
899
|
const wake = () => {
|
|
@@ -884,7 +904,7 @@ async function* spawnStream(opts) {
|
|
|
884
904
|
}
|
|
885
905
|
};
|
|
886
906
|
const resume = () => {
|
|
887
|
-
if (paused && queue.length
|
|
907
|
+
if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
|
|
888
908
|
paused = false;
|
|
889
909
|
child.stdout?.resume();
|
|
890
910
|
child.stderr?.resume();
|
|
@@ -894,11 +914,16 @@ async function* spawnStream(opts) {
|
|
|
894
914
|
const s = c.toString();
|
|
895
915
|
stdoutBytes += c.byteLength;
|
|
896
916
|
emitProcessOutput({ pid, stream: "stdout", chunk: c });
|
|
897
|
-
if (
|
|
917
|
+
if (stdoutRetainedBytes < max) {
|
|
918
|
+
const retained = utf8Prefix(s, max - stdoutRetainedBytes);
|
|
919
|
+
stdout += retained;
|
|
920
|
+
stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
921
|
+
}
|
|
898
922
|
spool.write(s);
|
|
899
|
-
queue.push({ kind: "out", data: s });
|
|
923
|
+
queue.push({ kind: "out", data: s, bytes: c.byteLength });
|
|
924
|
+
queuedBytes += c.byteLength;
|
|
900
925
|
wake();
|
|
901
|
-
if (!paused && queue.length >= maxQueue) {
|
|
926
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
902
927
|
paused = true;
|
|
903
928
|
child.stdout?.pause();
|
|
904
929
|
child.stderr?.pause();
|
|
@@ -908,11 +933,16 @@ async function* spawnStream(opts) {
|
|
|
908
933
|
const s = c.toString();
|
|
909
934
|
stderrBytes += c.byteLength;
|
|
910
935
|
emitProcessOutput({ pid, stream: "stderr", chunk: c });
|
|
911
|
-
if (
|
|
936
|
+
if (stderrRetainedBytes < max) {
|
|
937
|
+
const retained = utf8Prefix(s, max - stderrRetainedBytes);
|
|
938
|
+
stderr += retained;
|
|
939
|
+
stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
940
|
+
}
|
|
912
941
|
spool.write(s);
|
|
913
|
-
queue.push({ kind: "err", data: s });
|
|
942
|
+
queue.push({ kind: "err", data: s, bytes: c.byteLength });
|
|
943
|
+
queuedBytes += c.byteLength;
|
|
914
944
|
wake();
|
|
915
|
-
if (!paused && queue.length >= maxQueue) {
|
|
945
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
916
946
|
paused = true;
|
|
917
947
|
child.stdout?.pause();
|
|
918
948
|
child.stderr?.pause();
|
|
@@ -922,7 +952,9 @@ async function* spawnStream(opts) {
|
|
|
922
952
|
child.stderr?.on("data", onErr);
|
|
923
953
|
child.on("error", (e) => {
|
|
924
954
|
error = e.message;
|
|
925
|
-
|
|
955
|
+
const bytes = Buffer.byteLength(e.message, "utf8");
|
|
956
|
+
queue.push({ kind: "error", data: e.message, bytes });
|
|
957
|
+
queuedBytes += bytes;
|
|
926
958
|
wake();
|
|
927
959
|
});
|
|
928
960
|
const completeTelemetry = (code, signal, timedOut = false) => {
|
|
@@ -943,7 +975,13 @@ async function* spawnStream(opts) {
|
|
|
943
975
|
if (typeof pid === "number") registry.unregister(pid);
|
|
944
976
|
const exitCode2 = code ?? (signal ? 1 : 0);
|
|
945
977
|
completeTelemetry(exitCode2, signal ?? void 0);
|
|
946
|
-
queue.push({
|
|
978
|
+
queue.push({
|
|
979
|
+
kind: "close",
|
|
980
|
+
data: "",
|
|
981
|
+
bytes: 0,
|
|
982
|
+
code: exitCode2,
|
|
983
|
+
...signal ? { signal } : {}
|
|
984
|
+
});
|
|
947
985
|
wake();
|
|
948
986
|
});
|
|
949
987
|
const onAbort = () => {
|
|
@@ -955,7 +993,7 @@ async function* spawnStream(opts) {
|
|
|
955
993
|
} catch {
|
|
956
994
|
}
|
|
957
995
|
}
|
|
958
|
-
queue.push({ kind: "close", data: "", code: 124 });
|
|
996
|
+
queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
|
|
959
997
|
completeTelemetry(124, "SIGKILL", true);
|
|
960
998
|
wake();
|
|
961
999
|
};
|
|
@@ -973,6 +1011,7 @@ async function* spawnStream(opts) {
|
|
|
973
1011
|
});
|
|
974
1012
|
}
|
|
975
1013
|
const chunk = queue.shift();
|
|
1014
|
+
queuedBytes -= chunk.bytes;
|
|
976
1015
|
resume();
|
|
977
1016
|
if (chunk.kind === "close") {
|
|
978
1017
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
@@ -999,7 +1038,7 @@ async function* spawnStream(opts) {
|
|
|
999
1038
|
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
1000
1039
|
stderr,
|
|
1001
1040
|
exitCode,
|
|
1002
|
-
truncated:
|
|
1041
|
+
truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
|
|
1003
1042
|
error,
|
|
1004
1043
|
spoolPath: spooled?.path,
|
|
1005
1044
|
spoolBytes: spooled?.bytes
|
|
@@ -1023,13 +1062,27 @@ async function* spawnStream(opts) {
|
|
|
1023
1062
|
}
|
|
1024
1063
|
}
|
|
1025
1064
|
}
|
|
1065
|
+
function utf8Prefix(text, maxBytes) {
|
|
1066
|
+
if (maxBytes <= 0) return "";
|
|
1067
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1068
|
+
let low = 0;
|
|
1069
|
+
let high = text.length;
|
|
1070
|
+
while (low < high) {
|
|
1071
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1072
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
|
|
1073
|
+
else high = mid - 1;
|
|
1074
|
+
}
|
|
1075
|
+
let end = low;
|
|
1076
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
1077
|
+
return text.slice(0, end);
|
|
1078
|
+
}
|
|
1026
1079
|
var isWin;
|
|
1027
1080
|
var init_spawn_stream = __esm({
|
|
1028
1081
|
"src/_spawn-stream.ts"() {
|
|
1029
1082
|
"use strict";
|
|
1030
1083
|
init_output_spool();
|
|
1031
|
-
init_process_registry();
|
|
1032
1084
|
init_win32_resolve();
|
|
1085
|
+
init_process_registry();
|
|
1033
1086
|
isWin = process.platform === "win32";
|
|
1034
1087
|
}
|
|
1035
1088
|
});
|
|
@@ -1043,14 +1096,14 @@ function sha256hex(content) {
|
|
|
1043
1096
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1044
1097
|
}
|
|
1045
1098
|
async function detectPackageManager(cwd) {
|
|
1046
|
-
const { stat:
|
|
1099
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
1047
1100
|
try {
|
|
1048
|
-
await
|
|
1101
|
+
await stat18(`${cwd}/pnpm-lock.yaml`);
|
|
1049
1102
|
return "pnpm";
|
|
1050
1103
|
} catch {
|
|
1051
1104
|
}
|
|
1052
1105
|
try {
|
|
1053
|
-
await
|
|
1106
|
+
await stat18(`${cwd}/yarn.lock`);
|
|
1054
1107
|
return "yarn";
|
|
1055
1108
|
} catch {
|
|
1056
1109
|
}
|
|
@@ -1078,11 +1131,15 @@ function safeResolve(input, ctx) {
|
|
|
1078
1131
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
1079
1132
|
}
|
|
1080
1133
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1081
|
-
|
|
1134
|
+
await resolveRealInsideRoot(absPath, ctx);
|
|
1135
|
+
}
|
|
1136
|
+
async function resolveRealInsideRoot(absPath, ctx) {
|
|
1137
|
+
if (ctx.allowOutsideProjectRoot) return absPath;
|
|
1082
1138
|
const realRoots = await Promise.all(
|
|
1083
1139
|
allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
|
|
1084
1140
|
);
|
|
1085
1141
|
let probe2 = absPath;
|
|
1142
|
+
const pendingTail = [];
|
|
1086
1143
|
for (; ; ) {
|
|
1087
1144
|
let real;
|
|
1088
1145
|
try {
|
|
@@ -1090,13 +1147,16 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1090
1147
|
} catch (err) {
|
|
1091
1148
|
if (err.code === "ENOENT") {
|
|
1092
1149
|
const parent = path3.dirname(probe2);
|
|
1093
|
-
if (parent === probe2) return;
|
|
1150
|
+
if (parent === probe2) return absPath;
|
|
1151
|
+
pendingTail.unshift(path3.basename(probe2));
|
|
1094
1152
|
probe2 = parent;
|
|
1095
1153
|
continue;
|
|
1096
1154
|
}
|
|
1097
1155
|
throw err;
|
|
1098
1156
|
}
|
|
1099
|
-
if (isInsideAny(real, realRoots))
|
|
1157
|
+
if (isInsideAny(real, realRoots)) {
|
|
1158
|
+
return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
|
|
1159
|
+
}
|
|
1100
1160
|
throw new Error(
|
|
1101
1161
|
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
1102
1162
|
);
|
|
@@ -1104,8 +1164,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1104
1164
|
}
|
|
1105
1165
|
async function safeResolveReal(input, ctx) {
|
|
1106
1166
|
const abs = safeResolve(input, ctx);
|
|
1107
|
-
await
|
|
1108
|
-
return abs;
|
|
1167
|
+
return await resolveRealInsideRoot(abs, ctx);
|
|
1109
1168
|
}
|
|
1110
1169
|
function truncateMiddle(s, max) {
|
|
1111
1170
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
@@ -2669,8 +2728,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2669
2728
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2670
2729
|
}
|
|
2671
2730
|
}
|
|
2672
|
-
function collectFileEvidence(directory, fullPath,
|
|
2673
|
-
const lower =
|
|
2731
|
+
function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
|
|
2732
|
+
const lower = basename13.toLowerCase();
|
|
2674
2733
|
const extension = path4.extname(lower);
|
|
2675
2734
|
for (const profile of profiles) {
|
|
2676
2735
|
const detector = profile.detectors.find(
|
|
@@ -2681,7 +2740,7 @@ function collectFileEvidence(directory, fullPath, basename12, profiles, state) {
|
|
|
2681
2740
|
candidate.evidence.push({
|
|
2682
2741
|
kind: detector.kind,
|
|
2683
2742
|
path: fullPath,
|
|
2684
|
-
value:
|
|
2743
|
+
value: basename13,
|
|
2685
2744
|
weight: detector.weight
|
|
2686
2745
|
});
|
|
2687
2746
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2809,8 +2868,8 @@ function normalizeLimits(input) {
|
|
|
2809
2868
|
async function canonicalDirectory(input) {
|
|
2810
2869
|
const resolved = path4.resolve(input);
|
|
2811
2870
|
const real = await fs2.realpath(resolved);
|
|
2812
|
-
const
|
|
2813
|
-
if (!
|
|
2871
|
+
const stat18 = await fs2.stat(real);
|
|
2872
|
+
if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2814
2873
|
return real;
|
|
2815
2874
|
}
|
|
2816
2875
|
async function canonicalInside(input, root, label) {
|
|
@@ -3366,6 +3425,7 @@ import * as fs3 from "node:fs/promises";
|
|
|
3366
3425
|
import * as path6 from "node:path";
|
|
3367
3426
|
async function planLanguageOperation(options) {
|
|
3368
3427
|
validateOperationOptions(options.operation, options.operationOptions?.packages);
|
|
3428
|
+
validateFilterOption(options.operationOptions?.filter);
|
|
3369
3429
|
const detection = await detectLanguageWorkspaces(options);
|
|
3370
3430
|
const candidates = detection.workspaces.filter(
|
|
3371
3431
|
(workspace) => workspace.capabilities.includes(options.operation)
|
|
@@ -3538,6 +3598,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
|
|
|
3538
3598
|
const relative12 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3539
3599
|
return relative12 === "" ? 0 : relative12.split(path6.sep).length;
|
|
3540
3600
|
}
|
|
3601
|
+
function validateFilterOption(filter) {
|
|
3602
|
+
if (filter === void 0) return;
|
|
3603
|
+
if (filter.startsWith("-") || filter.includes(" --")) {
|
|
3604
|
+
throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
|
|
3605
|
+
}
|
|
3606
|
+
if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
|
|
3607
|
+
throw new Error("Invalid test filter (oversized or contains a control character)");
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3541
3610
|
function validateOperationOptions(operation, packages) {
|
|
3542
3611
|
if (!operation.startsWith("package-") || !packages) return;
|
|
3543
3612
|
for (const value of packages) {
|
|
@@ -3579,7 +3648,7 @@ var init_plan = __esm({
|
|
|
3579
3648
|
MAX_ARGUMENT_LENGTH = 4096;
|
|
3580
3649
|
PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3581
3650
|
COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3582
|
-
GO_MODULE_RE = /^(?:[a-z0-9
|
|
3651
|
+
GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
|
|
3583
3652
|
PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
|
|
3584
3653
|
}
|
|
3585
3654
|
});
|
|
@@ -3699,8 +3768,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3699
3768
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3700
3769
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3701
3770
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3702
|
-
const
|
|
3703
|
-
if (
|
|
3771
|
+
const stat18 = await fs4.stat(safeTarget);
|
|
3772
|
+
if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3704
3773
|
return unavailableResult(
|
|
3705
3774
|
options,
|
|
3706
3775
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -3987,8 +4056,8 @@ async function snapshotPaths(paths) {
|
|
|
3987
4056
|
const existing = [];
|
|
3988
4057
|
for (const candidate of paths) {
|
|
3989
4058
|
try {
|
|
3990
|
-
const
|
|
3991
|
-
if (!
|
|
4059
|
+
const stat18 = await fs4.stat(candidate);
|
|
4060
|
+
if (!stat18.isFile()) continue;
|
|
3992
4061
|
existing.push(candidate);
|
|
3993
4062
|
} catch {
|
|
3994
4063
|
}
|
|
@@ -4016,8 +4085,8 @@ async function snapshotSizes(paths) {
|
|
|
4016
4085
|
const sizes = /* @__PURE__ */ new Map();
|
|
4017
4086
|
for (const candidate of paths) {
|
|
4018
4087
|
try {
|
|
4019
|
-
const
|
|
4020
|
-
if (
|
|
4088
|
+
const stat18 = await fs4.stat(candidate);
|
|
4089
|
+
if (stat18.isFile()) sizes.set(candidate, stat18.size);
|
|
4021
4090
|
} catch {
|
|
4022
4091
|
}
|
|
4023
4092
|
}
|
|
@@ -4177,6 +4246,9 @@ var init_execute_tool = __esm({
|
|
|
4177
4246
|
useInstead: ["language_info", "install", "audit", "outdated"]
|
|
4178
4247
|
},
|
|
4179
4248
|
permission: "confirm",
|
|
4249
|
+
// WS-046: gives permission decisions something to key on.
|
|
4250
|
+
// The action performed (build/test/lint); the cwd is where, not what.
|
|
4251
|
+
subjectKey: "action",
|
|
4180
4252
|
mutating: true,
|
|
4181
4253
|
riskTier: "standard",
|
|
4182
4254
|
capabilities: ["shell.restricted", "fs.write"],
|
|
@@ -4409,6 +4481,9 @@ var init_package_tool = __esm({
|
|
|
4409
4481
|
useInstead: ["language_info", "language", "install", "audit", "outdated"]
|
|
4410
4482
|
},
|
|
4411
4483
|
permission: "confirm",
|
|
4484
|
+
// WS-046: gives permission decisions something to key on.
|
|
4485
|
+
// The package operation performed — install/remove are not interchangeable.
|
|
4486
|
+
subjectKey: "operation",
|
|
4412
4487
|
mutating: true,
|
|
4413
4488
|
riskTier: "destructive",
|
|
4414
4489
|
capabilities: ["shell.restricted", "fs.write", "net.outbound", "package.install"],
|
|
@@ -6460,9 +6535,9 @@ function parseSymbols6(opts) {
|
|
|
6460
6535
|
function regexParse2(opts) {
|
|
6461
6536
|
const { file, content, lang } = opts;
|
|
6462
6537
|
const symbols = [];
|
|
6463
|
-
const
|
|
6464
|
-
const isPackageJson =
|
|
6465
|
-
const isTsconfig =
|
|
6538
|
+
const basename13 = path22.basename(file).toLowerCase();
|
|
6539
|
+
const isPackageJson = basename13 === "package.json";
|
|
6540
|
+
const isTsconfig = basename13 === "tsconfig.json" || basename13 === "tsconfig.build.json";
|
|
6466
6541
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6467
6542
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6468
6543
|
const lines = content.split("\n");
|
|
@@ -8358,30 +8433,59 @@ ${hint}` : ""),
|
|
|
8358
8433
|
import {
|
|
8359
8434
|
GOVERNED_TOOL_EXECUTOR_META_KEY
|
|
8360
8435
|
} from "@wrongstack/core/types";
|
|
8436
|
+
var BATCHABLE_TOOL_NAMES = [
|
|
8437
|
+
"read",
|
|
8438
|
+
"glob",
|
|
8439
|
+
"grep",
|
|
8440
|
+
"tree",
|
|
8441
|
+
"codebase-stats",
|
|
8442
|
+
"codebase-search",
|
|
8443
|
+
"language_info",
|
|
8444
|
+
"tool_search",
|
|
8445
|
+
"memory_for_file",
|
|
8446
|
+
"memory_for_path",
|
|
8447
|
+
"memory_search"
|
|
8448
|
+
];
|
|
8449
|
+
var BATCHABLE_TOOLS = new Set(BATCHABLE_TOOL_NAMES);
|
|
8450
|
+
var MAX_BATCH_CALLS = 8;
|
|
8451
|
+
var MAX_PARALLEL_BATCH_CALLS = 3;
|
|
8361
8452
|
var batchToolUseTool = {
|
|
8362
8453
|
name: "batch_tool_use",
|
|
8363
8454
|
category: "Meta",
|
|
8364
|
-
description: "Execute a batch of
|
|
8365
|
-
usageHint:
|
|
8455
|
+
description: "Execute a small batch of independent, read-only discovery calls. Process, mutation, browser-action, MCP, delegation, and nested meta tools are not eligible.",
|
|
8456
|
+
usageHint: `BOUNDED READ-ONLY BATCHING:
|
|
8457
|
+
|
|
8458
|
+
- Use only for a small set of independent reads or searches that are all needed now.
|
|
8459
|
+
- Do not wrap normal sequential reasoning or every tool call in a batch.
|
|
8460
|
+
- \`parallel: true\` (default) runs at most three eligible calls concurrently.
|
|
8461
|
+
- \`stop_on_error: true\` makes it fail fast on the first error.
|
|
8462
|
+
- Eligible tools: ${BATCHABLE_TOOL_NAMES.join(", ")}.`,
|
|
8366
8463
|
permission: "confirm",
|
|
8464
|
+
// Scheduling barrier: keep the outer meta-call out of ToolExecutor's
|
|
8465
|
+
// non-mutating fan-out even though every eligible inner tool is read-only.
|
|
8367
8466
|
mutating: true,
|
|
8368
8467
|
timeoutMs: 12e4,
|
|
8369
|
-
capabilities: ["tool.
|
|
8468
|
+
capabilities: ["tool.meta"],
|
|
8370
8469
|
icon: "meta",
|
|
8371
8470
|
inputSchema: {
|
|
8372
8471
|
type: "object",
|
|
8373
8472
|
properties: {
|
|
8374
8473
|
calls: {
|
|
8375
8474
|
type: "array",
|
|
8475
|
+
maxItems: MAX_BATCH_CALLS,
|
|
8376
8476
|
items: {
|
|
8377
8477
|
type: "object",
|
|
8378
8478
|
properties: {
|
|
8379
|
-
tool: {
|
|
8479
|
+
tool: {
|
|
8480
|
+
type: "string",
|
|
8481
|
+
enum: [...BATCHABLE_TOOL_NAMES],
|
|
8482
|
+
description: "An explicitly batch-safe, read-only discovery tool."
|
|
8483
|
+
},
|
|
8380
8484
|
input: { type: "object" }
|
|
8381
8485
|
},
|
|
8382
8486
|
required: ["tool"]
|
|
8383
8487
|
},
|
|
8384
|
-
description:
|
|
8488
|
+
description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
|
|
8385
8489
|
},
|
|
8386
8490
|
stop_on_error: {
|
|
8387
8491
|
type: "boolean",
|
|
@@ -8404,6 +8508,9 @@ var batchToolUseTool = {
|
|
|
8404
8508
|
stop_on_error: false
|
|
8405
8509
|
};
|
|
8406
8510
|
}
|
|
8511
|
+
if (input.calls.length > MAX_BATCH_CALLS) {
|
|
8512
|
+
throw new Error(`batch_tool_use: calls must contain at most ${MAX_BATCH_CALLS} items`);
|
|
8513
|
+
}
|
|
8407
8514
|
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
|
|
8408
8515
|
if (typeof governedExecute !== "function") {
|
|
8409
8516
|
return {
|
|
@@ -8423,8 +8530,11 @@ var batchToolUseTool = {
|
|
|
8423
8530
|
let succeeded = 0;
|
|
8424
8531
|
let failed = 0;
|
|
8425
8532
|
if (input.parallel !== false) {
|
|
8426
|
-
const
|
|
8427
|
-
|
|
8533
|
+
const allResults = await mapWithConcurrency(
|
|
8534
|
+
input.calls,
|
|
8535
|
+
MAX_PARALLEL_BATCH_CALLS,
|
|
8536
|
+
(call) => executeSingle(call, ctx, governedExecute)
|
|
8537
|
+
);
|
|
8428
8538
|
results.push(...allResults);
|
|
8429
8539
|
succeeded = allResults.filter((r) => r.success).length;
|
|
8430
8540
|
failed = allResults.filter((r) => !r.success).length;
|
|
@@ -8449,8 +8559,28 @@ var batchToolUseTool = {
|
|
|
8449
8559
|
};
|
|
8450
8560
|
}
|
|
8451
8561
|
};
|
|
8562
|
+
async function mapWithConcurrency(items, limit, run) {
|
|
8563
|
+
const results = new Array(items.length);
|
|
8564
|
+
let nextIndex = 0;
|
|
8565
|
+
const worker2 = async () => {
|
|
8566
|
+
while (nextIndex < items.length) {
|
|
8567
|
+
const index = nextIndex++;
|
|
8568
|
+
results[index] = await run(items[index]);
|
|
8569
|
+
}
|
|
8570
|
+
};
|
|
8571
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker2()));
|
|
8572
|
+
return results;
|
|
8573
|
+
}
|
|
8452
8574
|
async function executeSingle(call, ctx, governedExecute) {
|
|
8453
8575
|
const start = Date.now();
|
|
8576
|
+
if (call.tool === batchToolUseTool.name) {
|
|
8577
|
+
return {
|
|
8578
|
+
tool: call.tool,
|
|
8579
|
+
success: false,
|
|
8580
|
+
error: "recursive batch_tool_use execution is not allowed",
|
|
8581
|
+
executionMs: Date.now() - start
|
|
8582
|
+
};
|
|
8583
|
+
}
|
|
8454
8584
|
const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
|
|
8455
8585
|
if (!tool) {
|
|
8456
8586
|
return {
|
|
@@ -8460,6 +8590,14 @@ async function executeSingle(call, ctx, governedExecute) {
|
|
|
8460
8590
|
executionMs: Date.now() - start
|
|
8461
8591
|
};
|
|
8462
8592
|
}
|
|
8593
|
+
if (!BATCHABLE_TOOLS.has(tool.name) || tool.mutating) {
|
|
8594
|
+
return {
|
|
8595
|
+
tool: call.tool,
|
|
8596
|
+
success: false,
|
|
8597
|
+
error: `tool "${call.tool}" is not eligible for batch execution; call it directly. Allowed: ${BATCHABLE_TOOL_NAMES.join(", ")}`,
|
|
8598
|
+
executionMs: Date.now() - start
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
8463
8601
|
try {
|
|
8464
8602
|
const result = await governedExecute(call.tool, call.input);
|
|
8465
8603
|
return {
|
|
@@ -8496,8 +8634,14 @@ import * as path10 from "node:path";
|
|
|
8496
8634
|
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
8497
8635
|
var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8498
8636
|
var sweptRoots = /* @__PURE__ */ new Set();
|
|
8637
|
+
var MAX_SWEPT_ROOTS = 128;
|
|
8499
8638
|
function sweepOldArtifacts(root) {
|
|
8500
8639
|
if (sweptRoots.has(root)) return;
|
|
8640
|
+
while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
|
|
8641
|
+
const oldest = sweptRoots.values().next().value;
|
|
8642
|
+
if (oldest === void 0) break;
|
|
8643
|
+
sweptRoots.delete(oldest);
|
|
8644
|
+
}
|
|
8501
8645
|
sweptRoots.add(root);
|
|
8502
8646
|
void (async () => {
|
|
8503
8647
|
const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
|
|
@@ -8520,8 +8664,8 @@ function sweepOldArtifacts(root) {
|
|
|
8520
8664
|
for (const name of names) {
|
|
8521
8665
|
const target = path10.join(dir, name);
|
|
8522
8666
|
try {
|
|
8523
|
-
const
|
|
8524
|
-
if (
|
|
8667
|
+
const stat18 = await fs7.stat(target);
|
|
8668
|
+
if (stat18.isFile() && stat18.mtimeMs < cutoff) {
|
|
8525
8669
|
await fs7.rm(target, { force: true });
|
|
8526
8670
|
removed++;
|
|
8527
8671
|
}
|
|
@@ -8549,14 +8693,14 @@ var BrowserArtifactStore = class {
|
|
|
8549
8693
|
}
|
|
8550
8694
|
async record(id, sessionId, kind, target, mimeType) {
|
|
8551
8695
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
8552
|
-
const [
|
|
8696
|
+
const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
8553
8697
|
const artifact = {
|
|
8554
8698
|
id,
|
|
8555
8699
|
kind,
|
|
8556
8700
|
sensitivity: "sensitive",
|
|
8557
8701
|
path: target,
|
|
8558
8702
|
mimeType,
|
|
8559
|
-
sizeBytes:
|
|
8703
|
+
sizeBytes: stat18.size,
|
|
8560
8704
|
sha256,
|
|
8561
8705
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8562
8706
|
};
|
|
@@ -8926,7 +9070,7 @@ var BrowserSessionManager = class {
|
|
|
8926
9070
|
constructor(options, launcher = defaultLauncher) {
|
|
8927
9071
|
this.launcher = launcher;
|
|
8928
9072
|
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
8929
|
-
this.allowPrivateHosts = options.allowPrivateHosts ??
|
|
9073
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
8930
9074
|
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
8931
9075
|
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
8932
9076
|
allowPrivateHosts: this.allowPrivateHosts,
|
|
@@ -9149,8 +9293,8 @@ var BrowserSessionManager = class {
|
|
|
9149
9293
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
9150
9294
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
9151
9295
|
}
|
|
9152
|
-
const
|
|
9153
|
-
if (!
|
|
9296
|
+
const stat18 = await fs8.stat(realFile);
|
|
9297
|
+
if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
9154
9298
|
resolved.push(realFile);
|
|
9155
9299
|
}
|
|
9156
9300
|
await this.runPageOperation(
|
|
@@ -9499,6 +9643,9 @@ var browserScreenshotTool = {
|
|
|
9499
9643
|
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
9500
9644
|
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
9501
9645
|
permission: "confirm",
|
|
9646
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9647
|
+
// rule must distinguish clicking one control from another.
|
|
9648
|
+
subjectKey: "selector",
|
|
9502
9649
|
mutating: true,
|
|
9503
9650
|
riskTier: "safe",
|
|
9504
9651
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9528,6 +9675,9 @@ function selectorTool(name, description, run) {
|
|
|
9528
9675
|
description,
|
|
9529
9676
|
usageHint: `${name}({ sessionId, selector })`,
|
|
9530
9677
|
permission: "confirm",
|
|
9678
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9679
|
+
// rule must distinguish clicking one control from another.
|
|
9680
|
+
subjectKey: "selector",
|
|
9531
9681
|
mutating: true,
|
|
9532
9682
|
riskTier: "standard",
|
|
9533
9683
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9557,6 +9707,9 @@ var browserTypeTool = {
|
|
|
9557
9707
|
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.",
|
|
9558
9708
|
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
9559
9709
|
permission: "confirm",
|
|
9710
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9711
|
+
// rule must distinguish clicking one control from another.
|
|
9712
|
+
subjectKey: "selector",
|
|
9560
9713
|
mutating: true,
|
|
9561
9714
|
riskTier: "standard",
|
|
9562
9715
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9591,6 +9744,9 @@ var browserSelectTool = {
|
|
|
9591
9744
|
description: "Select an option in an owned browser session.",
|
|
9592
9745
|
usageHint: "browser_select({ sessionId, selector, value })",
|
|
9593
9746
|
permission: "confirm",
|
|
9747
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9748
|
+
// rule must distinguish clicking one control from another.
|
|
9749
|
+
subjectKey: "selector",
|
|
9594
9750
|
mutating: true,
|
|
9595
9751
|
riskTier: "standard",
|
|
9596
9752
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9619,6 +9775,9 @@ var browserPressTool = {
|
|
|
9619
9775
|
description: "Press a keyboard key in an owned browser session.",
|
|
9620
9776
|
usageHint: "browser_press({ sessionId, key })",
|
|
9621
9777
|
permission: "confirm",
|
|
9778
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9779
|
+
// rule must distinguish clicking one control from another.
|
|
9780
|
+
subjectKey: "key",
|
|
9622
9781
|
mutating: true,
|
|
9623
9782
|
riskTier: "standard",
|
|
9624
9783
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9637,6 +9796,9 @@ var browserDragTool = {
|
|
|
9637
9796
|
description: "Drag one element to another in an owned browser session.",
|
|
9638
9797
|
usageHint: "browser_drag({ sessionId, from, to })",
|
|
9639
9798
|
permission: "confirm",
|
|
9799
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9800
|
+
// rule must distinguish clicking one control from another.
|
|
9801
|
+
subjectKey: "from",
|
|
9640
9802
|
mutating: true,
|
|
9641
9803
|
riskTier: "standard",
|
|
9642
9804
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9686,6 +9848,9 @@ var browserEvaluateTool = {
|
|
|
9686
9848
|
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
9687
9849
|
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
9688
9850
|
permission: "confirm",
|
|
9851
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9852
|
+
// rule must distinguish clicking one control from another.
|
|
9853
|
+
subjectKey: "expression",
|
|
9689
9854
|
mutating: true,
|
|
9690
9855
|
riskTier: "destructive",
|
|
9691
9856
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
@@ -9708,6 +9873,9 @@ var browserUploadTool = {
|
|
|
9708
9873
|
description: "Upload project-local files through a file input in an owned browser session.",
|
|
9709
9874
|
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
9710
9875
|
permission: "confirm",
|
|
9876
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9877
|
+
// rule must distinguish clicking one control from another.
|
|
9878
|
+
subjectKey: "selector",
|
|
9711
9879
|
mutating: true,
|
|
9712
9880
|
riskTier: "destructive",
|
|
9713
9881
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
@@ -10643,6 +10811,7 @@ var REFS_INDEX_SQL = [
|
|
|
10643
10811
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
10644
10812
|
|
|
10645
10813
|
// src/codebase-index/writer-search-helpers.ts
|
|
10814
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
10646
10815
|
function normalizeSearchLimit(limit) {
|
|
10647
10816
|
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
10648
10817
|
}
|
|
@@ -10672,8 +10841,8 @@ function buildWriterSearchWhere(query, filter) {
|
|
|
10672
10841
|
}
|
|
10673
10842
|
if (query.trim()) {
|
|
10674
10843
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
10675
|
-
conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
|
|
10676
|
-
for (const token of tokens) values.push(`%${token}%`);
|
|
10844
|
+
conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
|
|
10845
|
+
for (const token of tokens) values.push(`%${escapeLike(token)}%`);
|
|
10677
10846
|
}
|
|
10678
10847
|
return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
|
|
10679
10848
|
}
|
|
@@ -10782,6 +10951,7 @@ var StorePool = class {
|
|
|
10782
10951
|
|
|
10783
10952
|
// src/codebase-index/writer.ts
|
|
10784
10953
|
var DB_FILE2 = "index.db";
|
|
10954
|
+
var MAX_STATEMENT_CACHE = 128;
|
|
10785
10955
|
var IndexStore = class _IndexStore {
|
|
10786
10956
|
db;
|
|
10787
10957
|
/** Absolute path to this project's index directory. */
|
|
@@ -10819,12 +10989,31 @@ var IndexStore = class _IndexStore {
|
|
|
10819
10989
|
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
10820
10990
|
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
10821
10991
|
bm25Dirty = true;
|
|
10822
|
-
/**
|
|
10992
|
+
/**
|
|
10993
|
+
* Prepare-once helper: compile `sql` on first use, reuse thereafter.
|
|
10994
|
+
*
|
|
10995
|
+
* Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
|
|
10996
|
+
* the fallback search builder emits one `text LIKE ?` clause per query token
|
|
10997
|
+
* — so the SQL varies with the token count and a stream of differently-sized
|
|
10998
|
+
* queries grew the cache without limit. Sage's store already bounds its
|
|
10999
|
+
* equivalent at 128 (WS-096).
|
|
11000
|
+
*
|
|
11001
|
+
* Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
|
|
11002
|
+
* insertRefs, …) at the young end, so a burst of one-off search SQL evicts
|
|
11003
|
+
* itself rather than the reindex hot path.
|
|
11004
|
+
*/
|
|
10823
11005
|
stmt(sql) {
|
|
10824
|
-
|
|
10825
|
-
if (
|
|
10826
|
-
|
|
10827
|
-
this.stmtCache.set(sql,
|
|
11006
|
+
const cached = this.stmtCache.get(sql);
|
|
11007
|
+
if (cached !== void 0) {
|
|
11008
|
+
this.stmtCache.delete(sql);
|
|
11009
|
+
this.stmtCache.set(sql, cached);
|
|
11010
|
+
return cached;
|
|
11011
|
+
}
|
|
11012
|
+
const s = this.db.prepare(sql);
|
|
11013
|
+
this.stmtCache.set(sql, s);
|
|
11014
|
+
if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
|
|
11015
|
+
const oldest = this.stmtCache.keys().next();
|
|
11016
|
+
if (!oldest.done) this.stmtCache.delete(oldest.value);
|
|
10828
11017
|
}
|
|
10829
11018
|
return s;
|
|
10830
11019
|
}
|
|
@@ -11097,7 +11286,7 @@ var IndexStore = class _IndexStore {
|
|
|
11097
11286
|
const { where, values } = built;
|
|
11098
11287
|
const limit = normalizeSearchLimit(opts?.limit);
|
|
11099
11288
|
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
11100
|
-
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment
|
|
11289
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
11101
11290
|
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
11102
11291
|
const rows = this.stmt(sql).all(
|
|
11103
11292
|
...binds
|
|
@@ -11226,11 +11415,13 @@ var IndexStore = class _IndexStore {
|
|
|
11226
11415
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
11227
11416
|
searchRankedFallback(query, filter, limit) {
|
|
11228
11417
|
if (!query.trim()) {
|
|
11229
|
-
const
|
|
11230
|
-
if (
|
|
11231
|
-
return { results: this.search(query, filter, { limit }), total };
|
|
11418
|
+
const total2 = this.countSearch(query, filter);
|
|
11419
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
11420
|
+
return { results: this.search(query, filter, { limit }), total: total2 };
|
|
11232
11421
|
}
|
|
11233
|
-
const
|
|
11422
|
+
const total = this.countSearch(query, filter);
|
|
11423
|
+
if (total === 0) return { results: [], total: 0 };
|
|
11424
|
+
const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
|
|
11234
11425
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
11235
11426
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
11236
11427
|
const bm25 = this.getOrBuildBm25();
|
|
@@ -11256,7 +11447,7 @@ var IndexStore = class _IndexStore {
|
|
|
11256
11447
|
const c = expectDefined2(candidateById.get(id));
|
|
11257
11448
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
11258
11449
|
});
|
|
11259
|
-
return { results, total
|
|
11450
|
+
return { results, total };
|
|
11260
11451
|
}
|
|
11261
11452
|
getAllIndexable() {
|
|
11262
11453
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
@@ -11675,12 +11866,12 @@ var buildIdCache;
|
|
|
11675
11866
|
function projectIndexServerBuildId(entrypoint) {
|
|
11676
11867
|
const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
|
|
11677
11868
|
try {
|
|
11678
|
-
const
|
|
11679
|
-
if (buildIdCache?.file === file && buildIdCache.mtimeMs ===
|
|
11869
|
+
const stat18 = fs11.statSync(file);
|
|
11870
|
+
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
|
|
11680
11871
|
return buildIdCache.buildId;
|
|
11681
11872
|
}
|
|
11682
11873
|
const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
|
|
11683
|
-
buildIdCache = { file, mtimeMs:
|
|
11874
|
+
buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
|
|
11684
11875
|
return buildId;
|
|
11685
11876
|
} catch {
|
|
11686
11877
|
return `unreadable:${path16.basename(file)}`;
|
|
@@ -11850,6 +12041,8 @@ var ProjectServerConnection = class {
|
|
|
11850
12041
|
connectResolve = null;
|
|
11851
12042
|
connectReject = null;
|
|
11852
12043
|
nextId = 1;
|
|
12044
|
+
/** WS-027: read from the owner-only metadata file — see currentAuthToken. */
|
|
12045
|
+
authToken;
|
|
11853
12046
|
pending = /* @__PURE__ */ new Map();
|
|
11854
12047
|
transition(status, options = {}) {
|
|
11855
12048
|
const previous = connectionStates.get(this.endpoint);
|
|
@@ -11870,6 +12063,10 @@ var ProjectServerConnection = class {
|
|
|
11870
12063
|
isConnected() {
|
|
11871
12064
|
return this.socket !== null && !this.socket.destroyed && this.info !== null;
|
|
11872
12065
|
}
|
|
12066
|
+
/** Safe LRU candidate: no request, connect, or health probe is in flight. */
|
|
12067
|
+
isEvictable() {
|
|
12068
|
+
return this.pending.size === 0 && this.connecting === null && this.healthCheck === null;
|
|
12069
|
+
}
|
|
11873
12070
|
async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
|
|
11874
12071
|
await this.ensureConnected(spawnIfMissing);
|
|
11875
12072
|
if (this.healthCheck) return this.healthCheck;
|
|
@@ -11982,6 +12179,31 @@ var ProjectServerConnection = class {
|
|
|
11982
12179
|
this.transition("offline");
|
|
11983
12180
|
maybeStopHeartbeatLoop();
|
|
11984
12181
|
}
|
|
12182
|
+
/**
|
|
12183
|
+
* WS-027: the per-process token, read from the daemon's owner-only metadata
|
|
12184
|
+
* file — never from the `hello` frame, which the daemon sends to every
|
|
12185
|
+
* socket that connects (the mistake WS-028 found in the SAGE daemon).
|
|
12186
|
+
*
|
|
12187
|
+
* Read lazily and re-read while unknown: the daemon starts listening before
|
|
12188
|
+
* it writes metadata (endpoint ownership has to be won first), and a
|
|
12189
|
+
* respawned daemon mints a new token, so a cached wrong one would be sticky.
|
|
12190
|
+
*/
|
|
12191
|
+
currentAuthToken() {
|
|
12192
|
+
if (this.authToken === void 0) {
|
|
12193
|
+
try {
|
|
12194
|
+
const raw = fs12.readFileSync(
|
|
12195
|
+
projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
|
|
12196
|
+
"utf8"
|
|
12197
|
+
);
|
|
12198
|
+
const parsed = JSON.parse(raw);
|
|
12199
|
+
if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
|
|
12200
|
+
this.authToken = parsed.authToken;
|
|
12201
|
+
}
|
|
12202
|
+
} catch {
|
|
12203
|
+
}
|
|
12204
|
+
}
|
|
12205
|
+
return this.authToken;
|
|
12206
|
+
}
|
|
11985
12207
|
request(message, options) {
|
|
11986
12208
|
const socket = this.socket;
|
|
11987
12209
|
if (!socket || socket.destroyed) {
|
|
@@ -12025,7 +12247,7 @@ var ProjectServerConnection = class {
|
|
|
12025
12247
|
return;
|
|
12026
12248
|
}
|
|
12027
12249
|
}
|
|
12028
|
-
this.write({ ...message, id });
|
|
12250
|
+
this.write({ ...message, id, authToken: this.currentAuthToken() });
|
|
12029
12251
|
});
|
|
12030
12252
|
}
|
|
12031
12253
|
async ensureConnected(spawnIfMissing) {
|
|
@@ -12072,6 +12294,7 @@ var ProjectServerConnection = class {
|
|
|
12072
12294
|
connectOnce() {
|
|
12073
12295
|
this.socket?.destroy();
|
|
12074
12296
|
this.socket = null;
|
|
12297
|
+
this.authToken = void 0;
|
|
12075
12298
|
this.info = null;
|
|
12076
12299
|
this.activity = null;
|
|
12077
12300
|
this.health = null;
|
|
@@ -12268,7 +12491,26 @@ var ProjectServerConnection = class {
|
|
|
12268
12491
|
}
|
|
12269
12492
|
};
|
|
12270
12493
|
var connections = /* @__PURE__ */ new Map();
|
|
12494
|
+
var MAX_CACHED_CONNECTIONS = 8;
|
|
12271
12495
|
var heartbeatTimer;
|
|
12496
|
+
function forgetConnection(endpoint, connection) {
|
|
12497
|
+
if (connections.get(endpoint) === connection) connections.delete(endpoint);
|
|
12498
|
+
connection.close();
|
|
12499
|
+
connectionStates.delete(endpoint);
|
|
12500
|
+
if (latestConnectionState.endpoint !== endpoint) return;
|
|
12501
|
+
latestConnectionState = [...connectionStates.values()].at(-1) ?? {
|
|
12502
|
+
status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
|
|
12503
|
+
connected: false
|
|
12504
|
+
};
|
|
12505
|
+
}
|
|
12506
|
+
function trimConnectionCache(protectedConnection) {
|
|
12507
|
+
if (connections.size <= MAX_CACHED_CONNECTIONS) return;
|
|
12508
|
+
for (const [endpoint, connection] of connections) {
|
|
12509
|
+
if (connections.size <= MAX_CACHED_CONNECTIONS) break;
|
|
12510
|
+
if (connection === protectedConnection || !connection.isEvictable()) continue;
|
|
12511
|
+
forgetConnection(endpoint, connection);
|
|
12512
|
+
}
|
|
12513
|
+
}
|
|
12272
12514
|
function ensureHeartbeatLoop() {
|
|
12273
12515
|
if (heartbeatTimer) return;
|
|
12274
12516
|
heartbeatTimer = setInterval(() => {
|
|
@@ -12291,7 +12533,11 @@ function connectionFor(projectRoot, indexDir) {
|
|
|
12291
12533
|
if (!connection) {
|
|
12292
12534
|
connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
|
|
12293
12535
|
connections.set(endpoint, connection);
|
|
12536
|
+
} else {
|
|
12537
|
+
connections.delete(endpoint);
|
|
12538
|
+
connections.set(endpoint, connection);
|
|
12294
12539
|
}
|
|
12540
|
+
trimConnectionCache(connection);
|
|
12295
12541
|
return connection;
|
|
12296
12542
|
}
|
|
12297
12543
|
function callProjectIndexServer(op, args, options) {
|
|
@@ -12658,9 +12904,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12658
12904
|
const statReadParse = await Promise.allSettled(
|
|
12659
12905
|
batchFiles.map(
|
|
12660
12906
|
async (file) => {
|
|
12661
|
-
let
|
|
12907
|
+
let stat18;
|
|
12662
12908
|
try {
|
|
12663
|
-
|
|
12909
|
+
stat18 = await fs17.stat(file, statOpts);
|
|
12664
12910
|
} catch (e) {
|
|
12665
12911
|
if (isAbortError(e)) throw e;
|
|
12666
12912
|
return {
|
|
@@ -12672,21 +12918,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12672
12918
|
missing: isMissingPathError(e)
|
|
12673
12919
|
};
|
|
12674
12920
|
}
|
|
12675
|
-
if (!
|
|
12921
|
+
if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
|
|
12676
12922
|
const lang = detectLang(file);
|
|
12677
|
-
if (!lang) return { file, stat:
|
|
12678
|
-
if (
|
|
12923
|
+
if (!lang) return { file, stat: stat18, lang: "", parsed: null };
|
|
12924
|
+
if (stat18.size > MAX_INDEX_FILE_BYTES) {
|
|
12679
12925
|
return {
|
|
12680
12926
|
file,
|
|
12681
|
-
stat:
|
|
12927
|
+
stat: stat18,
|
|
12682
12928
|
lang,
|
|
12683
12929
|
parsed: null,
|
|
12684
|
-
error: `file too large (${
|
|
12930
|
+
error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
12685
12931
|
};
|
|
12686
12932
|
}
|
|
12687
12933
|
const meta = existingMeta.get(file);
|
|
12688
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
12689
|
-
return { file, stat:
|
|
12934
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
|
|
12935
|
+
return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
|
|
12690
12936
|
}
|
|
12691
12937
|
let content;
|
|
12692
12938
|
try {
|
|
@@ -12695,7 +12941,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12695
12941
|
if (isAbortError(e)) throw e;
|
|
12696
12942
|
return {
|
|
12697
12943
|
file,
|
|
12698
|
-
stat:
|
|
12944
|
+
stat: stat18,
|
|
12699
12945
|
lang,
|
|
12700
12946
|
parsed: null,
|
|
12701
12947
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -12707,13 +12953,13 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12707
12953
|
} catch (e) {
|
|
12708
12954
|
return {
|
|
12709
12955
|
file,
|
|
12710
|
-
stat:
|
|
12956
|
+
stat: stat18,
|
|
12711
12957
|
lang,
|
|
12712
12958
|
parsed: null,
|
|
12713
12959
|
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
12714
12960
|
};
|
|
12715
12961
|
}
|
|
12716
|
-
return { file, stat:
|
|
12962
|
+
return { file, stat: stat18, lang, parsed, content };
|
|
12717
12963
|
}
|
|
12718
12964
|
)
|
|
12719
12965
|
);
|
|
@@ -12734,7 +12980,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12734
12980
|
errors.push(`${file}: ${result.error}`);
|
|
12735
12981
|
continue;
|
|
12736
12982
|
}
|
|
12737
|
-
const { stat:
|
|
12983
|
+
const { stat: stat18, lang, parsed } = result;
|
|
12738
12984
|
if (result.skippedMeta) {
|
|
12739
12985
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
12740
12986
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -12746,7 +12992,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12746
12992
|
store.upsertFile({
|
|
12747
12993
|
file,
|
|
12748
12994
|
lang,
|
|
12749
|
-
mtimeMs: Math.floor(
|
|
12995
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
12750
12996
|
symbolCount: 0,
|
|
12751
12997
|
lastIndexed: Date.now()
|
|
12752
12998
|
});
|
|
@@ -12758,7 +13004,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12758
13004
|
store.replaceEmptyFile({
|
|
12759
13005
|
file,
|
|
12760
13006
|
lang,
|
|
12761
|
-
mtimeMs: Math.floor(
|
|
13007
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
12762
13008
|
symbolCount: 0,
|
|
12763
13009
|
lastIndexed: Date.now()
|
|
12764
13010
|
});
|
|
@@ -12770,7 +13016,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
12770
13016
|
lang,
|
|
12771
13017
|
symbols: parsed.symbols,
|
|
12772
13018
|
refs: parsed.refs ?? [],
|
|
12773
|
-
mtimeMs: Math.floor(
|
|
13019
|
+
mtimeMs: Math.floor(stat18.mtimeMs),
|
|
12774
13020
|
symbolCount: parsed.symbols.length
|
|
12775
13021
|
});
|
|
12776
13022
|
deleteForFiles.push(file);
|
|
@@ -13823,8 +14069,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
13823
14069
|
const content = fs19.readFileSync(epFile, "utf8");
|
|
13824
14070
|
const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
|
|
13825
14071
|
const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
|
|
13826
|
-
|
|
13827
|
-
|
|
14072
|
+
for (; ; ) {
|
|
14073
|
+
const match = reExportRe.exec(strippedContent);
|
|
14074
|
+
if (match === null) break;
|
|
13828
14075
|
const moduleSpec = match[1];
|
|
13829
14076
|
const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
|
|
13830
14077
|
for (const rf of resolvedFiles) {
|
|
@@ -14030,6 +14277,9 @@ var designTool = {
|
|
|
14030
14277
|
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).',
|
|
14031
14278
|
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"}`.',
|
|
14032
14279
|
permission: "confirm",
|
|
14280
|
+
// WS-046: gives permission decisions something to key on.
|
|
14281
|
+
// The action performed; `out` is optional so it cannot be the subject.
|
|
14282
|
+
subjectKey: "action",
|
|
14033
14283
|
mutating: true,
|
|
14034
14284
|
capabilities: ["fs.write"],
|
|
14035
14285
|
timeoutMs: 15e3,
|
|
@@ -14210,9 +14460,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
|
|
|
14210
14460
|
const abs = path25.join(absParent, path25.basename(absResolved));
|
|
14211
14461
|
const rel = path25.relative(root, abs);
|
|
14212
14462
|
if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
14213
|
-
throw new Error(
|
|
14214
|
-
`design: materialize path "${result.path}" would escape the project root`
|
|
14215
|
-
);
|
|
14463
|
+
throw new Error(`design: materialize path "${result.path}" would escape the project root`);
|
|
14216
14464
|
}
|
|
14217
14465
|
let exists = false;
|
|
14218
14466
|
try {
|
|
@@ -14287,6 +14535,7 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
14287
14535
|
import * as fs21 from "node:fs/promises";
|
|
14288
14536
|
import * as path26 from "node:path";
|
|
14289
14537
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
14538
|
+
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
14290
14539
|
var diffTool = {
|
|
14291
14540
|
name: "diff",
|
|
14292
14541
|
category: "Filesystem",
|
|
@@ -14294,6 +14543,7 @@ var diffTool = {
|
|
|
14294
14543
|
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).',
|
|
14295
14544
|
permission: "auto",
|
|
14296
14545
|
mutating: false,
|
|
14546
|
+
maxOutputBytes: 262144,
|
|
14297
14547
|
capabilities: ["fs.read"],
|
|
14298
14548
|
icon: "diff",
|
|
14299
14549
|
timeoutMs: 1e4,
|
|
@@ -14369,8 +14619,8 @@ function findGitDir(cwd) {
|
|
|
14369
14619
|
let dir = cwd;
|
|
14370
14620
|
for (let i = 0; i < 20; i++) {
|
|
14371
14621
|
try {
|
|
14372
|
-
const
|
|
14373
|
-
if (
|
|
14622
|
+
const stat18 = statSync3(path26.join(dir, ".git"));
|
|
14623
|
+
if (stat18.isDirectory()) return dir;
|
|
14374
14624
|
} catch {
|
|
14375
14625
|
}
|
|
14376
14626
|
const parent = path26.dirname(dir);
|
|
@@ -14412,10 +14662,18 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14412
14662
|
};
|
|
14413
14663
|
}
|
|
14414
14664
|
const results = [];
|
|
14665
|
+
let truncated = false;
|
|
14415
14666
|
for (const file of files) {
|
|
14416
14667
|
const absPath = safeResolve(file, ctx);
|
|
14417
|
-
const
|
|
14418
|
-
if (!
|
|
14668
|
+
const stat18 = await fs21.stat(absPath).catch(() => null);
|
|
14669
|
+
if (!stat18?.isFile()) continue;
|
|
14670
|
+
if (stat18.size > MAX_FILE_DUMP_BYTES) {
|
|
14671
|
+
truncated = true;
|
|
14672
|
+
results.push(
|
|
14673
|
+
`--- ${file} (skipped: ${stat18.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
14674
|
+
);
|
|
14675
|
+
continue;
|
|
14676
|
+
}
|
|
14419
14677
|
const content = await fs21.readFile(absPath, "utf8");
|
|
14420
14678
|
const lines = content.split(/\r?\n/);
|
|
14421
14679
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -14423,7 +14681,7 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14423
14681
|
return {
|
|
14424
14682
|
diff: results.join("\n\n"),
|
|
14425
14683
|
files,
|
|
14426
|
-
truncated
|
|
14684
|
+
truncated,
|
|
14427
14685
|
mode: input.mode ?? "unified"
|
|
14428
14686
|
};
|
|
14429
14687
|
}
|
|
@@ -14516,8 +14774,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
14516
14774
|
for (const f of files) {
|
|
14517
14775
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
14518
14776
|
try {
|
|
14519
|
-
const
|
|
14520
|
-
if (
|
|
14777
|
+
const stat18 = await fs22.stat(absPath);
|
|
14778
|
+
if (stat18.isFile()) resolved.push(absPath);
|
|
14521
14779
|
} catch {
|
|
14522
14780
|
}
|
|
14523
14781
|
}
|
|
@@ -15370,7 +15628,7 @@ var editTool = {
|
|
|
15370
15628
|
});
|
|
15371
15629
|
}
|
|
15372
15630
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15373
|
-
const
|
|
15631
|
+
const stat18 = await fs23.stat(absPath).catch((err) => {
|
|
15374
15632
|
if (err.code === "ENOENT") {
|
|
15375
15633
|
throw new ToolValidationError({
|
|
15376
15634
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -15380,7 +15638,7 @@ var editTool = {
|
|
|
15380
15638
|
}
|
|
15381
15639
|
throw err;
|
|
15382
15640
|
});
|
|
15383
|
-
if (!
|
|
15641
|
+
if (!stat18.isFile()) {
|
|
15384
15642
|
throw new ToolValidationError({
|
|
15385
15643
|
message: `edit: "${input.path}" is not a regular file`,
|
|
15386
15644
|
field: "path"
|
|
@@ -15410,7 +15668,7 @@ var editTool = {
|
|
|
15410
15668
|
});
|
|
15411
15669
|
}
|
|
15412
15670
|
}
|
|
15413
|
-
if (autoRead && updated.mtimeMs >
|
|
15671
|
+
if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
|
|
15414
15672
|
throw new ToolValidationError({
|
|
15415
15673
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
15416
15674
|
field: "path",
|
|
@@ -15533,10 +15791,6 @@ import {
|
|
|
15533
15791
|
emitProcessStarted as emitProcessStarted3
|
|
15534
15792
|
} from "@wrongstack/core/observability";
|
|
15535
15793
|
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
|
|
15536
|
-
init_output_spool();
|
|
15537
|
-
init_util();
|
|
15538
|
-
init_process_registry();
|
|
15539
|
-
init_win32_resolve();
|
|
15540
15794
|
|
|
15541
15795
|
// src/_danger-detect.ts
|
|
15542
15796
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -15819,6 +16073,11 @@ function levelRank(level) {
|
|
|
15819
16073
|
}
|
|
15820
16074
|
}
|
|
15821
16075
|
|
|
16076
|
+
// src/exec.ts
|
|
16077
|
+
init_output_spool();
|
|
16078
|
+
init_util();
|
|
16079
|
+
init_win32_resolve();
|
|
16080
|
+
|
|
15822
16081
|
// src/exec-kill-guard.ts
|
|
15823
16082
|
import * as os8 from "node:os";
|
|
15824
16083
|
import * as path29 from "node:path";
|
|
@@ -16055,6 +16314,7 @@ async function checkKillTarget(target) {
|
|
|
16055
16314
|
}
|
|
16056
16315
|
|
|
16057
16316
|
// src/exec.ts
|
|
16317
|
+
init_process_registry();
|
|
16058
16318
|
var isWin4 = process.platform === "win32";
|
|
16059
16319
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
16060
16320
|
// JS / TS toolchain
|
|
@@ -16691,7 +16951,17 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
16691
16951
|
bun: [],
|
|
16692
16952
|
docker: [],
|
|
16693
16953
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
16694
|
-
find: [
|
|
16954
|
+
find: [
|
|
16955
|
+
/^-exec$/,
|
|
16956
|
+
/^-exec;$/,
|
|
16957
|
+
/^-ok$/,
|
|
16958
|
+
/^-ok;$/,
|
|
16959
|
+
/^-execdir$/,
|
|
16960
|
+
/^-execdir;$/,
|
|
16961
|
+
/^-exec=/,
|
|
16962
|
+
/^-ok=/,
|
|
16963
|
+
/^-execdir=/
|
|
16964
|
+
],
|
|
16695
16965
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
16696
16966
|
// and glob patterns that could expand to dangerous targets.
|
|
16697
16967
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
@@ -16762,6 +17032,14 @@ var execTool = {
|
|
|
16762
17032
|
useInstead: ["bash"]
|
|
16763
17033
|
},
|
|
16764
17034
|
permission: "confirm",
|
|
17035
|
+
// WS-046: without this, every exec call collapsed onto the bare tool name.
|
|
17036
|
+
// Three consequences followed: "always allow" stored a rule that could never
|
|
17037
|
+
// match, pressing "no" once blocked ALL exec for the session, and the
|
|
17038
|
+
// permission cache keyed one decision for every command. `subjectKey:
|
|
17039
|
+
// 'command'` renders the FULL invocation (`command` + `args`) — using the
|
|
17040
|
+
// program alone would make `exec git status` and `exec git push --force` the
|
|
17041
|
+
// same subject, so trusting one would silently authorize the other.
|
|
17042
|
+
subjectKey: "command",
|
|
16765
17043
|
mutating: true,
|
|
16766
17044
|
riskTier: "standard",
|
|
16767
17045
|
timeoutMs: DEFAULT_TIMEOUT_MS3,
|
|
@@ -16978,7 +17256,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
16978
17256
|
const pid = child.pid;
|
|
16979
17257
|
if (typeof pid === "number") {
|
|
16980
17258
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
16981
|
-
registry.register({
|
|
17259
|
+
registry.register({
|
|
17260
|
+
pid,
|
|
17261
|
+
name: "exec",
|
|
17262
|
+
command: redactCommand(fullCommand),
|
|
17263
|
+
startedAt: Date.now(),
|
|
17264
|
+
sessionId,
|
|
17265
|
+
child
|
|
17266
|
+
});
|
|
16982
17267
|
}
|
|
16983
17268
|
const timer = setTimeout(() => {
|
|
16984
17269
|
killed = true;
|
|
@@ -17418,6 +17703,9 @@ var formatTool = {
|
|
|
17418
17703
|
description: "Format source files according to project style (Biome). Can also run in check-only mode.",
|
|
17419
17704
|
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.",
|
|
17420
17705
|
permission: "confirm",
|
|
17706
|
+
// WS-046: gives permission decisions something to key on.
|
|
17707
|
+
// The files being rewritten are the subject; the fixer is how, not what.
|
|
17708
|
+
subjectKey: "files",
|
|
17421
17709
|
mutating: true,
|
|
17422
17710
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17423
17711
|
icon: "code",
|
|
@@ -17522,13 +17810,13 @@ var formatTool = {
|
|
|
17522
17810
|
}
|
|
17523
17811
|
};
|
|
17524
17812
|
async function detectFixer(cwd) {
|
|
17525
|
-
const { stat:
|
|
17813
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
17526
17814
|
try {
|
|
17527
|
-
await
|
|
17815
|
+
await stat18(`${cwd}/biome.json`);
|
|
17528
17816
|
return "biome";
|
|
17529
17817
|
} catch {
|
|
17530
17818
|
try {
|
|
17531
|
-
await
|
|
17819
|
+
await stat18(`${cwd}/.prettierrc`);
|
|
17532
17820
|
return "prettier";
|
|
17533
17821
|
} catch {
|
|
17534
17822
|
return "biome";
|
|
@@ -17555,6 +17843,10 @@ var gitTool = {
|
|
|
17555
17843
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
17556
17844
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
17557
17845
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
17846
|
+
// WS-046: gives permission decisions something to key on.
|
|
17847
|
+
// The git subcommand (status/commit/push) is what a trust rule needs to
|
|
17848
|
+
// distinguish; `git status` and `git push --force` must not share a subject.
|
|
17849
|
+
subjectKey: "command",
|
|
17558
17850
|
mutating: true,
|
|
17559
17851
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17560
17852
|
timeoutMs: TIMEOUT_MS2,
|
|
@@ -17623,6 +17915,8 @@ var gitTool = {
|
|
|
17623
17915
|
truncated: false
|
|
17624
17916
|
};
|
|
17625
17917
|
}
|
|
17918
|
+
const branchGuard = validateBranchInput(input);
|
|
17919
|
+
if (branchGuard) return branchGuard;
|
|
17626
17920
|
if (input.command === "worktree") {
|
|
17627
17921
|
const guard = validateWorktreeInput(input, ctx.projectRoot);
|
|
17628
17922
|
if (guard) return guard;
|
|
@@ -17671,6 +17965,18 @@ var gitTool = {
|
|
|
17671
17965
|
return result;
|
|
17672
17966
|
}
|
|
17673
17967
|
};
|
|
17968
|
+
function validateBranchInput(input) {
|
|
17969
|
+
const branch = input.branch;
|
|
17970
|
+
if (branch === void 0) return null;
|
|
17971
|
+
if (!branch.startsWith("-") && !branch.includes(" --")) return null;
|
|
17972
|
+
return {
|
|
17973
|
+
command: input.command,
|
|
17974
|
+
stdout: "",
|
|
17975
|
+
stderr: `unsafe branch name (parsed as a git option): ${branch}`,
|
|
17976
|
+
exitCode: 1,
|
|
17977
|
+
truncated: false
|
|
17978
|
+
};
|
|
17979
|
+
}
|
|
17674
17980
|
function validateWorktreeInput(input, projectRoot) {
|
|
17675
17981
|
const reject = (stderr) => ({
|
|
17676
17982
|
command: "worktree",
|
|
@@ -17679,7 +17985,6 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
17679
17985
|
exitCode: 1,
|
|
17680
17986
|
truncated: false
|
|
17681
17987
|
});
|
|
17682
|
-
if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
|
|
17683
17988
|
if (input.worktreePath?.startsWith("-")) {
|
|
17684
17989
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
17685
17990
|
}
|
|
@@ -17697,8 +18002,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
17697
18002
|
let dir = cwd;
|
|
17698
18003
|
for (let i = 0; i < 20; i++) {
|
|
17699
18004
|
try {
|
|
17700
|
-
const
|
|
17701
|
-
if (
|
|
18005
|
+
const stat18 = statSync4(`${dir}/.git`);
|
|
18006
|
+
if (stat18.isDirectory() || stat18.isFile()) return dir;
|
|
17702
18007
|
} catch {
|
|
17703
18008
|
}
|
|
17704
18009
|
if (dir === root) break;
|
|
@@ -17733,7 +18038,10 @@ function buildArgs(input) {
|
|
|
17733
18038
|
...files.length ? ["--", ...files] : []
|
|
17734
18039
|
];
|
|
17735
18040
|
case "branch":
|
|
17736
|
-
return input.branch ? [
|
|
18041
|
+
return input.branch ? [
|
|
18042
|
+
"branch",
|
|
18043
|
+
...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
|
|
18044
|
+
] : ["branch"];
|
|
17737
18045
|
case "checkout":
|
|
17738
18046
|
return [
|
|
17739
18047
|
"checkout",
|
|
@@ -17826,7 +18134,7 @@ import * as path30 from "node:path";
|
|
|
17826
18134
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
17827
18135
|
|
|
17828
18136
|
// src/_concurrency.ts
|
|
17829
|
-
async function
|
|
18137
|
+
async function mapWithConcurrency2(items, limit, fn) {
|
|
17830
18138
|
if (items.length === 0) return [];
|
|
17831
18139
|
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
17832
18140
|
const results = new Array(items.length);
|
|
@@ -17961,10 +18269,10 @@ var globTool = {
|
|
|
17961
18269
|
}
|
|
17962
18270
|
if (truncated) return;
|
|
17963
18271
|
}
|
|
17964
|
-
await
|
|
18272
|
+
await mapWithConcurrency2(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
17965
18273
|
if (truncated) return;
|
|
17966
18274
|
const remainingSubdirs = truncated ? [] : subdirs;
|
|
17967
|
-
await
|
|
18275
|
+
await mapWithConcurrency2(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
|
|
17968
18276
|
};
|
|
17969
18277
|
await walk(base, "");
|
|
17970
18278
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -17973,12 +18281,16 @@ var globTool = {
|
|
|
17973
18281
|
};
|
|
17974
18282
|
|
|
17975
18283
|
// src/grep.ts
|
|
17976
|
-
import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
|
|
17977
18284
|
import { spawn as spawn11 } from "node:child_process";
|
|
17978
18285
|
import * as fs25 from "node:fs/promises";
|
|
17979
18286
|
import * as path31 from "node:path";
|
|
17980
18287
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
17981
|
-
import {
|
|
18288
|
+
import {
|
|
18289
|
+
buildChildEnv as buildChildEnv5,
|
|
18290
|
+
compileGlob as compileGlob3,
|
|
18291
|
+
DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3,
|
|
18292
|
+
expectDefined as expectDefined7
|
|
18293
|
+
} from "@wrongstack/core/utils";
|
|
17982
18294
|
|
|
17983
18295
|
// src/_regex.ts
|
|
17984
18296
|
var MAX_PATTERN_LEN = 256;
|
|
@@ -18031,6 +18343,8 @@ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
|
|
|
18031
18343
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
18032
18344
|
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
18033
18345
|
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
18346
|
+
var RG_MAX_QUEUE_CHUNKS = 128;
|
|
18347
|
+
var RG_MAX_QUEUE_CHARS = 8 * 1024 * 1024;
|
|
18034
18348
|
var grepTool = {
|
|
18035
18349
|
name: "grep",
|
|
18036
18350
|
category: "Search",
|
|
@@ -18158,9 +18472,19 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18158
18472
|
const FLUSH_AT = 16;
|
|
18159
18473
|
const MAX_BUF_BYTES = 1e6;
|
|
18160
18474
|
let bufOverflow = false;
|
|
18161
|
-
const child = spawn11("rg", args, {
|
|
18475
|
+
const child = spawn11("rg", args, {
|
|
18476
|
+
signal,
|
|
18477
|
+
env: buildChildEnv5(),
|
|
18478
|
+
// rg diagnostics are not part of the tool result. Ignoring stderr avoids
|
|
18479
|
+
// an unread pipe filling up and pinning the child when rg reports many
|
|
18480
|
+
// filesystem errors.
|
|
18481
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18482
|
+
windowsHide: true
|
|
18483
|
+
});
|
|
18162
18484
|
const queue = [];
|
|
18485
|
+
let queuedChars = 0;
|
|
18163
18486
|
let waiter;
|
|
18487
|
+
let paused = false;
|
|
18164
18488
|
const wake = () => {
|
|
18165
18489
|
if (waiter) {
|
|
18166
18490
|
const w = waiter;
|
|
@@ -18168,93 +18492,133 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18168
18492
|
w();
|
|
18169
18493
|
}
|
|
18170
18494
|
};
|
|
18171
|
-
|
|
18172
|
-
queue.
|
|
18495
|
+
const pauseIfFlooded = () => {
|
|
18496
|
+
if (paused || queue.length < RG_MAX_QUEUE_CHUNKS && queuedChars < RG_MAX_QUEUE_CHARS)
|
|
18497
|
+
return;
|
|
18498
|
+
paused = true;
|
|
18499
|
+
child.stdout?.pause();
|
|
18500
|
+
};
|
|
18501
|
+
const resumeIfDrained = () => {
|
|
18502
|
+
if (!paused || queue.length >= RG_MAX_QUEUE_CHUNKS || queuedChars >= RG_MAX_QUEUE_CHARS)
|
|
18503
|
+
return;
|
|
18504
|
+
paused = false;
|
|
18505
|
+
child.stdout?.resume();
|
|
18506
|
+
};
|
|
18507
|
+
const onStdoutData = (c) => {
|
|
18508
|
+
const data = c.toString();
|
|
18509
|
+
queue.push({ kind: "out", data });
|
|
18510
|
+
queuedChars += data.length;
|
|
18173
18511
|
wake();
|
|
18174
|
-
|
|
18175
|
-
|
|
18512
|
+
pauseIfFlooded();
|
|
18513
|
+
};
|
|
18514
|
+
const onError = (e) => {
|
|
18176
18515
|
queue.push({ kind: "error", data: e.message });
|
|
18177
18516
|
wake();
|
|
18178
|
-
}
|
|
18179
|
-
|
|
18517
|
+
};
|
|
18518
|
+
const onClose = () => {
|
|
18180
18519
|
queue.push({ kind: "close", data: "" });
|
|
18181
18520
|
wake();
|
|
18182
|
-
}
|
|
18521
|
+
};
|
|
18522
|
+
child.stdout?.on("data", onStdoutData);
|
|
18523
|
+
child.on("error", onError);
|
|
18524
|
+
child.on("close", onClose);
|
|
18183
18525
|
let pendingBatch = [];
|
|
18184
18526
|
let errored = false;
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18191
|
-
|
|
18192
|
-
if (c.kind === "error") {
|
|
18193
|
-
errored = true;
|
|
18194
|
-
continue;
|
|
18195
|
-
}
|
|
18196
|
-
if (c.kind === "close") break;
|
|
18197
|
-
buf += c.data;
|
|
18198
|
-
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18199
|
-
bufOverflow = true;
|
|
18200
|
-
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18201
|
-
try {
|
|
18202
|
-
child.kill("SIGTERM");
|
|
18203
|
-
} catch {
|
|
18527
|
+
let closed = false;
|
|
18528
|
+
try {
|
|
18529
|
+
for (; ; ) {
|
|
18530
|
+
while (queue.length === 0) {
|
|
18531
|
+
await new Promise((r) => {
|
|
18532
|
+
waiter = r;
|
|
18533
|
+
});
|
|
18204
18534
|
}
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
|
|
18210
|
-
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18535
|
+
const c = expectDefined7(queue.shift());
|
|
18536
|
+
queuedChars = Math.max(0, queuedChars - c.data.length);
|
|
18537
|
+
resumeIfDrained();
|
|
18538
|
+
if (c.kind === "error") {
|
|
18539
|
+
errored = true;
|
|
18540
|
+
continue;
|
|
18541
|
+
}
|
|
18542
|
+
if (c.kind === "close") {
|
|
18543
|
+
closed = true;
|
|
18544
|
+
break;
|
|
18545
|
+
}
|
|
18546
|
+
buf += c.data;
|
|
18547
|
+
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18548
|
+
bufOverflow = true;
|
|
18549
|
+
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18550
|
+
try {
|
|
18551
|
+
child.kill("SIGTERM");
|
|
18552
|
+
} catch {
|
|
18553
|
+
}
|
|
18554
|
+
}
|
|
18555
|
+
const idx = buf.lastIndexOf("\n");
|
|
18556
|
+
if (idx === -1) continue;
|
|
18557
|
+
const ready = buf.slice(0, idx);
|
|
18558
|
+
buf = buf.slice(idx + 1);
|
|
18559
|
+
for (const line of ready.split("\n")) {
|
|
18560
|
+
if (!line) continue;
|
|
18561
|
+
totalLines++;
|
|
18562
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18563
|
+
if (matches.length < limit) {
|
|
18564
|
+
matches.push(line);
|
|
18565
|
+
pendingBatch.push(line);
|
|
18566
|
+
batchSinceFlush++;
|
|
18567
|
+
}
|
|
18568
|
+
}
|
|
18569
|
+
if (batchSinceFlush >= FLUSH_AT) {
|
|
18570
|
+
yield {
|
|
18571
|
+
type: "partial_output",
|
|
18572
|
+
text: pendingBatch.join("\n"),
|
|
18573
|
+
data: { matches_so_far: matches.length }
|
|
18574
|
+
};
|
|
18575
|
+
pendingBatch = [];
|
|
18576
|
+
batchSinceFlush = 0;
|
|
18577
|
+
}
|
|
18578
|
+
}
|
|
18579
|
+
if (buf.trim()) {
|
|
18580
|
+
for (const line of buf.split("\n")) {
|
|
18581
|
+
if (!line) continue;
|
|
18582
|
+
totalLines++;
|
|
18583
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18584
|
+
if (matches.length < limit) {
|
|
18585
|
+
matches.push(line);
|
|
18586
|
+
pendingBatch.push(line);
|
|
18587
|
+
}
|
|
18218
18588
|
}
|
|
18219
18589
|
}
|
|
18220
|
-
if (
|
|
18590
|
+
if (pendingBatch.length > 0) {
|
|
18221
18591
|
yield {
|
|
18222
18592
|
type: "partial_output",
|
|
18223
18593
|
text: pendingBatch.join("\n"),
|
|
18224
18594
|
data: { matches_so_far: matches.length }
|
|
18225
18595
|
};
|
|
18226
|
-
pendingBatch = [];
|
|
18227
|
-
batchSinceFlush = 0;
|
|
18228
18596
|
}
|
|
18229
|
-
|
|
18230
|
-
if (buf.trim()) {
|
|
18231
|
-
for (const line of buf.split("\n")) {
|
|
18232
|
-
if (!line) continue;
|
|
18233
|
-
totalLines++;
|
|
18234
|
-
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18235
|
-
if (matches.length < limit) {
|
|
18236
|
-
matches.push(line);
|
|
18237
|
-
pendingBatch.push(line);
|
|
18238
|
-
}
|
|
18239
|
-
}
|
|
18240
|
-
}
|
|
18241
|
-
if (pendingBatch.length > 0) {
|
|
18597
|
+
if (errored) throw new Error("rg: spawn error");
|
|
18242
18598
|
yield {
|
|
18243
|
-
type: "
|
|
18244
|
-
|
|
18245
|
-
|
|
18599
|
+
type: "final",
|
|
18600
|
+
output: {
|
|
18601
|
+
matches,
|
|
18602
|
+
count: mode === "count" ? totalCount : totalLines,
|
|
18603
|
+
truncated: totalLines > limit || bufOverflow,
|
|
18604
|
+
used: "rg"
|
|
18605
|
+
}
|
|
18246
18606
|
};
|
|
18247
|
-
}
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18607
|
+
} finally {
|
|
18608
|
+
child.stdout?.off("data", onStdoutData);
|
|
18609
|
+
child.off("error", onError);
|
|
18610
|
+
child.off("close", onClose);
|
|
18611
|
+
child.stdout?.destroy();
|
|
18612
|
+
queue.length = 0;
|
|
18613
|
+
queuedChars = 0;
|
|
18614
|
+
waiter = void 0;
|
|
18615
|
+
if (!closed && child.exitCode === null) {
|
|
18616
|
+
try {
|
|
18617
|
+
child.kill("SIGTERM");
|
|
18618
|
+
} catch {
|
|
18619
|
+
}
|
|
18256
18620
|
}
|
|
18257
|
-
}
|
|
18621
|
+
}
|
|
18258
18622
|
}
|
|
18259
18623
|
function parseRgCountLine(line) {
|
|
18260
18624
|
const idx = line.lastIndexOf(":");
|
|
@@ -18284,8 +18648,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18284
18648
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
18285
18649
|
if (globRe) globRe.lastIndex = 0;
|
|
18286
18650
|
try {
|
|
18287
|
-
const
|
|
18288
|
-
if (!
|
|
18651
|
+
const stat18 = await fs25.stat(full);
|
|
18652
|
+
if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
|
|
18289
18653
|
const file = await fs25.open(full, "r");
|
|
18290
18654
|
try {
|
|
18291
18655
|
let bytesReadTotal = 0;
|
|
@@ -18385,8 +18749,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18385
18749
|
files.push({ full, name: e.name });
|
|
18386
18750
|
}
|
|
18387
18751
|
}
|
|
18388
|
-
await
|
|
18389
|
-
await
|
|
18752
|
+
await mapWithConcurrency2(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
18753
|
+
await mapWithConcurrency2(
|
|
18390
18754
|
subdirs,
|
|
18391
18755
|
Math.min(16, NATIVE_SCAN_CONCURRENCY),
|
|
18392
18756
|
({ full, rel }) => walk(full, rel)
|
|
@@ -18405,7 +18769,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18405
18769
|
init_spawn_stream();
|
|
18406
18770
|
init_util();
|
|
18407
18771
|
init_legacy_bridge();
|
|
18408
|
-
import { join as
|
|
18772
|
+
import { join as join25 } from "node:path";
|
|
18409
18773
|
import {
|
|
18410
18774
|
detectEcosystem as detectPackageEcosystem,
|
|
18411
18775
|
recordPackageAction
|
|
@@ -18416,6 +18780,10 @@ var installTool = {
|
|
|
18416
18780
|
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.",
|
|
18417
18781
|
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.",
|
|
18418
18782
|
permission: "confirm",
|
|
18783
|
+
// WS-046: gives permission decisions something to key on.
|
|
18784
|
+
// What gets installed is the subject — installing a package runs its
|
|
18785
|
+
// lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
|
|
18786
|
+
subjectKey: "packages",
|
|
18419
18787
|
mutating: true,
|
|
18420
18788
|
riskTier: "standard",
|
|
18421
18789
|
icon: "package",
|
|
@@ -18585,10 +18953,10 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18585
18953
|
case "pnpm":
|
|
18586
18954
|
case "yarn":
|
|
18587
18955
|
case "npm":
|
|
18588
|
-
return
|
|
18956
|
+
return join25(cwd, "package.json");
|
|
18589
18957
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
18590
18958
|
default:
|
|
18591
|
-
return
|
|
18959
|
+
return join25(cwd, "package.json");
|
|
18592
18960
|
}
|
|
18593
18961
|
}
|
|
18594
18962
|
|
|
@@ -18596,6 +18964,24 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18596
18964
|
init_util();
|
|
18597
18965
|
import * as fs26 from "node:fs/promises";
|
|
18598
18966
|
import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
|
|
18967
|
+
var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
|
|
18968
|
+
var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
|
|
18969
|
+
var JsonFileTooLargeError = class extends Error {
|
|
18970
|
+
constructor(filePath, size) {
|
|
18971
|
+
super(
|
|
18972
|
+
`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.`
|
|
18973
|
+
);
|
|
18974
|
+
this.name = "JsonFileTooLargeError";
|
|
18975
|
+
}
|
|
18976
|
+
};
|
|
18977
|
+
async function readJsonFileBounded(filePath, ctx) {
|
|
18978
|
+
const resolved = await safeResolveReal(filePath, ctx);
|
|
18979
|
+
const stat18 = await fs26.stat(resolved);
|
|
18980
|
+
if (stat18.size > MAX_JSON_FILE_BYTES) {
|
|
18981
|
+
throw new JsonFileTooLargeError(filePath, stat18.size);
|
|
18982
|
+
}
|
|
18983
|
+
return fs26.readFile(resolved, "utf8");
|
|
18984
|
+
}
|
|
18599
18985
|
var jsonTool = {
|
|
18600
18986
|
name: "json",
|
|
18601
18987
|
category: "Data",
|
|
@@ -18603,6 +18989,7 @@ var jsonTool = {
|
|
|
18603
18989
|
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.',
|
|
18604
18990
|
permission: "auto",
|
|
18605
18991
|
mutating: false,
|
|
18992
|
+
maxOutputBytes: 262144,
|
|
18606
18993
|
timeoutMs: 5e3,
|
|
18607
18994
|
capabilities: ["fs.read"],
|
|
18608
18995
|
icon: "json",
|
|
@@ -18669,9 +19056,15 @@ async function executeParse(input, ctx) {
|
|
|
18669
19056
|
let raw;
|
|
18670
19057
|
if (input.file) {
|
|
18671
19058
|
try {
|
|
18672
|
-
raw = await
|
|
18673
|
-
} catch {
|
|
18674
|
-
return {
|
|
19059
|
+
raw = await readJsonFileBounded(input.file, ctx);
|
|
19060
|
+
} catch (error) {
|
|
19061
|
+
return {
|
|
19062
|
+
data: null,
|
|
19063
|
+
formatted: "",
|
|
19064
|
+
type: "unknown",
|
|
19065
|
+
action: "parse",
|
|
19066
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read file"
|
|
19067
|
+
};
|
|
18675
19068
|
}
|
|
18676
19069
|
} else if (input.data) {
|
|
18677
19070
|
raw = input.data;
|
|
@@ -18727,10 +19120,16 @@ async function executeQuery(input, ctx) {
|
|
|
18727
19120
|
let parsed;
|
|
18728
19121
|
if (input.file) {
|
|
18729
19122
|
try {
|
|
18730
|
-
const raw = await
|
|
19123
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18731
19124
|
parsed = JSON.parse(raw);
|
|
18732
|
-
} catch {
|
|
18733
|
-
return {
|
|
19125
|
+
} catch (error) {
|
|
19126
|
+
return {
|
|
19127
|
+
data: null,
|
|
19128
|
+
formatted: "",
|
|
19129
|
+
type: "unknown",
|
|
19130
|
+
action: "query",
|
|
19131
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19132
|
+
};
|
|
18734
19133
|
}
|
|
18735
19134
|
} else if (input.data) {
|
|
18736
19135
|
try {
|
|
@@ -18769,10 +19168,16 @@ async function executeValidate(input, ctx) {
|
|
|
18769
19168
|
let parsed;
|
|
18770
19169
|
if (input.file) {
|
|
18771
19170
|
try {
|
|
18772
|
-
const raw = await
|
|
19171
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18773
19172
|
parsed = JSON.parse(raw);
|
|
18774
|
-
} catch {
|
|
18775
|
-
return {
|
|
19173
|
+
} catch (error) {
|
|
19174
|
+
return {
|
|
19175
|
+
data: null,
|
|
19176
|
+
formatted: "",
|
|
19177
|
+
type: "unknown",
|
|
19178
|
+
action: "validate",
|
|
19179
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19180
|
+
};
|
|
18776
19181
|
}
|
|
18777
19182
|
} else if (input.data) {
|
|
18778
19183
|
try {
|
|
@@ -18811,10 +19216,16 @@ async function executeTransform(input, ctx) {
|
|
|
18811
19216
|
let parsed;
|
|
18812
19217
|
if (input.file) {
|
|
18813
19218
|
try {
|
|
18814
|
-
const raw = await
|
|
19219
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18815
19220
|
parsed = JSON.parse(raw);
|
|
18816
|
-
} catch {
|
|
18817
|
-
return {
|
|
19221
|
+
} catch (error) {
|
|
19222
|
+
return {
|
|
19223
|
+
data: null,
|
|
19224
|
+
formatted: "",
|
|
19225
|
+
type: "unknown",
|
|
19226
|
+
action: "transform",
|
|
19227
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19228
|
+
};
|
|
18818
19229
|
}
|
|
18819
19230
|
} else if (input.data) {
|
|
18820
19231
|
try {
|
|
@@ -19418,6 +19829,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
19418
19829
|
}
|
|
19419
19830
|
|
|
19420
19831
|
// src/kanban-board-inputs.ts
|
|
19832
|
+
function agentSettableGate(enforcement) {
|
|
19833
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
19834
|
+
return { completionGate: { enforcement } };
|
|
19835
|
+
}
|
|
19421
19836
|
function boardCreateInput(input, title) {
|
|
19422
19837
|
return {
|
|
19423
19838
|
title,
|
|
@@ -19430,7 +19845,7 @@ function boardCreateInput(input, title) {
|
|
|
19430
19845
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19431
19846
|
}
|
|
19432
19847
|
} : {},
|
|
19433
|
-
...input.gateEnforcement
|
|
19848
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19434
19849
|
};
|
|
19435
19850
|
}
|
|
19436
19851
|
function boardUpdatePatch(input) {
|
|
@@ -19444,7 +19859,7 @@ function boardUpdatePatch(input) {
|
|
|
19444
19859
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19445
19860
|
}
|
|
19446
19861
|
} : {},
|
|
19447
|
-
...input.gateEnforcement
|
|
19862
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19448
19863
|
};
|
|
19449
19864
|
}
|
|
19450
19865
|
function duplicateBoardOptions(input) {
|
|
@@ -19459,6 +19874,7 @@ function duplicateBoardOptions(input) {
|
|
|
19459
19874
|
|
|
19460
19875
|
// src/kanban-task-inputs.ts
|
|
19461
19876
|
import { randomUUID } from "node:crypto";
|
|
19877
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
19462
19878
|
function taskInput(input) {
|
|
19463
19879
|
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
19464
19880
|
return {
|
|
@@ -19556,6 +19972,10 @@ function taskPatch(input) {
|
|
|
19556
19972
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
19557
19973
|
};
|
|
19558
19974
|
}
|
|
19975
|
+
function clampRequestedCapabilities(requested) {
|
|
19976
|
+
if (requested === void 0) return void 0;
|
|
19977
|
+
return clampSubagentCapabilities(requested).granted;
|
|
19978
|
+
}
|
|
19559
19979
|
function assignmentInput(input) {
|
|
19560
19980
|
return {
|
|
19561
19981
|
agentId: input.agentId,
|
|
@@ -19566,7 +19986,7 @@ function assignmentInput(input) {
|
|
|
19566
19986
|
fallbackProfile: input.fallbackProfile,
|
|
19567
19987
|
fallbackModels: input.fallbackModels,
|
|
19568
19988
|
tools: input.tools,
|
|
19569
|
-
allowedCapabilities: input.allowedCapabilities,
|
|
19989
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
19570
19990
|
assignee: input.assignee,
|
|
19571
19991
|
leaseId: input.leaseId,
|
|
19572
19992
|
claimedAt: input.claimedAt,
|
|
@@ -19593,7 +20013,7 @@ function assignmentForTaskCreate(input) {
|
|
|
19593
20013
|
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
19594
20014
|
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
19595
20015
|
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
19596
|
-
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
|
|
20016
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
19597
20017
|
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
19598
20018
|
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
19599
20019
|
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
@@ -19804,7 +20224,13 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
19804
20224
|
moveTasksToColumnId: { type: "string" },
|
|
19805
20225
|
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
19806
20226
|
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
19807
|
-
gateEnforcement: {
|
|
20227
|
+
gateEnforcement: {
|
|
20228
|
+
type: "string",
|
|
20229
|
+
// WS-023: `'off'` is deliberately absent. The agent whose work this gate
|
|
20230
|
+
// checks must not be able to switch it off; it may only tighten. Turning
|
|
20231
|
+
// a gate off stays a human decision, made through board config.
|
|
20232
|
+
enum: ["strict", "soft"]
|
|
20233
|
+
},
|
|
19808
20234
|
subtasks: {
|
|
19809
20235
|
type: "array",
|
|
19810
20236
|
minItems: 2,
|
|
@@ -19911,7 +20337,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
|
19911
20337
|
description: "Live session work: todos, tasks, and plan items.",
|
|
19912
20338
|
tags: sessionBoardTags(sessionId),
|
|
19913
20339
|
columns: SESSION_KANBAN_COLUMNS,
|
|
19914
|
-
generatedBy: `session-kanban:${sessionId}
|
|
20340
|
+
generatedBy: `session-kanban:${sessionId}`,
|
|
20341
|
+
kind: "session_mirror",
|
|
20342
|
+
retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
|
|
19915
20343
|
});
|
|
19916
20344
|
}
|
|
19917
20345
|
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
@@ -20945,11 +21373,11 @@ var lintTool = {
|
|
|
20945
21373
|
}
|
|
20946
21374
|
};
|
|
20947
21375
|
async function detectLinter(cwd) {
|
|
20948
|
-
const { stat:
|
|
21376
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
20949
21377
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
20950
21378
|
for (const f of checks) {
|
|
20951
21379
|
try {
|
|
20952
|
-
await
|
|
21380
|
+
await stat18(`${cwd}/${f}`);
|
|
20953
21381
|
if (f.includes("biome")) return "biome";
|
|
20954
21382
|
if (f.includes("eslint")) return "eslint";
|
|
20955
21383
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -21193,6 +21621,9 @@ var outdatedTool = {
|
|
|
21193
21621
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
21194
21622
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
21195
21623
|
// search) but missed this one; applying the same contract here.
|
|
21624
|
+
// WS-046: gives permission decisions something to key on.
|
|
21625
|
+
// The working directory scanned.
|
|
21626
|
+
subjectKey: "cwd",
|
|
21196
21627
|
mutating: true,
|
|
21197
21628
|
// Capability is outbound network — the tool only hits the package
|
|
21198
21629
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -21374,6 +21805,10 @@ var patchTool = {
|
|
|
21374
21805
|
useInstead: ["edit"]
|
|
21375
21806
|
},
|
|
21376
21807
|
permission: "confirm",
|
|
21808
|
+
// WS-046: gives permission decisions something to key on.
|
|
21809
|
+
// The target directory. The patch body itself is unstable (every diff
|
|
21810
|
+
// differs), so it could never re-match its own stored rule.
|
|
21811
|
+
subjectKey: "directory",
|
|
21377
21812
|
mutating: true,
|
|
21378
21813
|
capabilities: ["fs.write"],
|
|
21379
21814
|
icon: "edit",
|
|
@@ -21440,8 +21875,8 @@ var patchTool = {
|
|
|
21440
21875
|
const before = beforeContents.get(target) ?? null;
|
|
21441
21876
|
const after = await readTextForTracking(target);
|
|
21442
21877
|
if (after === null || after === before) continue;
|
|
21443
|
-
const
|
|
21444
|
-
if (
|
|
21878
|
+
const stat18 = await fs27.stat(target).catch(() => null);
|
|
21879
|
+
if (stat18) ctx.recordRead?.(target, stat18.mtimeMs, "write", sha256hex(after));
|
|
21445
21880
|
ctx.session?.recordFileChange?.({
|
|
21446
21881
|
path: target,
|
|
21447
21882
|
action: before === null ? "created" : "modified",
|
|
@@ -21466,8 +21901,8 @@ var patchTool = {
|
|
|
21466
21901
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
21467
21902
|
async function readTextForTracking(absPath) {
|
|
21468
21903
|
try {
|
|
21469
|
-
const
|
|
21470
|
-
if (!
|
|
21904
|
+
const stat18 = await fs27.stat(absPath);
|
|
21905
|
+
if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
|
|
21471
21906
|
const buf = await fs27.readFile(absPath);
|
|
21472
21907
|
if (buf.includes(0)) return null;
|
|
21473
21908
|
return buf.toString("utf8");
|
|
@@ -21497,7 +21932,13 @@ function runPatch(args, cwd, signal) {
|
|
|
21497
21932
|
let stdout = "";
|
|
21498
21933
|
let stderr = "";
|
|
21499
21934
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
21500
|
-
const child = spawn14("patch", args, {
|
|
21935
|
+
const child = spawn14("patch", args, {
|
|
21936
|
+
cwd,
|
|
21937
|
+
signal,
|
|
21938
|
+
env,
|
|
21939
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21940
|
+
windowsHide: true
|
|
21941
|
+
});
|
|
21501
21942
|
child.stdout?.on("data", (c) => {
|
|
21502
21943
|
stdout += c.toString();
|
|
21503
21944
|
});
|
|
@@ -21518,6 +21959,7 @@ function extractPatchedFiles(output) {
|
|
|
21518
21959
|
}
|
|
21519
21960
|
|
|
21520
21961
|
// src/plan.ts
|
|
21962
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
21521
21963
|
import {
|
|
21522
21964
|
addPlanItem,
|
|
21523
21965
|
clearPlan,
|
|
@@ -21525,20 +21967,20 @@ import {
|
|
|
21525
21967
|
formatPlan,
|
|
21526
21968
|
getPlanTemplate,
|
|
21527
21969
|
mutatePlan as mutatePlan2,
|
|
21970
|
+
mutateTasks as mutateTasks2,
|
|
21528
21971
|
removePlanItem,
|
|
21529
21972
|
setPlanItemStatus
|
|
21530
21973
|
} from "@wrongstack/core/storage";
|
|
21531
|
-
import {
|
|
21532
|
-
mutateTasks as mutateTasks2
|
|
21533
|
-
} from "@wrongstack/core/storage";
|
|
21534
21974
|
import { formatTaskList } from "@wrongstack/core/utils";
|
|
21535
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
21536
21975
|
var planTool = {
|
|
21537
21976
|
name: "plan",
|
|
21538
21977
|
category: "Session",
|
|
21539
21978
|
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.',
|
|
21540
21979
|
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.',
|
|
21541
21980
|
permission: "confirm",
|
|
21981
|
+
// WS-046: gives permission decisions something to key on.
|
|
21982
|
+
// The action performed; plan has no single file or path subject.
|
|
21983
|
+
subjectKey: "action",
|
|
21542
21984
|
mutating: true,
|
|
21543
21985
|
capabilities: ["fs.write"],
|
|
21544
21986
|
icon: "plan",
|
|
@@ -21595,7 +22037,10 @@ var planTool = {
|
|
|
21595
22037
|
let planPath;
|
|
21596
22038
|
if (input.scope === "project") {
|
|
21597
22039
|
if (typeof sessionPlanPath === "string") {
|
|
21598
|
-
const lastSep = Math.max(
|
|
22040
|
+
const lastSep = Math.max(
|
|
22041
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
22042
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
22043
|
+
);
|
|
21599
22044
|
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
21600
22045
|
}
|
|
21601
22046
|
} else {
|
|
@@ -21632,7 +22077,11 @@ var planTool = {
|
|
|
21632
22077
|
case "start":
|
|
21633
22078
|
case "done": {
|
|
21634
22079
|
if (!input.target) {
|
|
21635
|
-
early = mkResult(
|
|
22080
|
+
early = mkResult(
|
|
22081
|
+
p,
|
|
22082
|
+
false,
|
|
22083
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22084
|
+
);
|
|
21636
22085
|
return p;
|
|
21637
22086
|
}
|
|
21638
22087
|
const next = setPlanItemStatus(
|
|
@@ -21660,7 +22109,11 @@ var planTool = {
|
|
|
21660
22109
|
}
|
|
21661
22110
|
case "promote": {
|
|
21662
22111
|
if (!input.target) {
|
|
21663
|
-
early = mkResult(
|
|
22112
|
+
early = mkResult(
|
|
22113
|
+
p,
|
|
22114
|
+
false,
|
|
22115
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22116
|
+
);
|
|
21664
22117
|
return p;
|
|
21665
22118
|
}
|
|
21666
22119
|
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
@@ -21703,7 +22156,11 @@ var planTool = {
|
|
|
21703
22156
|
return clearPlan(p);
|
|
21704
22157
|
case "taskify": {
|
|
21705
22158
|
if (!input.target) {
|
|
21706
|
-
early = mkResult(
|
|
22159
|
+
early = mkResult(
|
|
22160
|
+
p,
|
|
22161
|
+
false,
|
|
22162
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
22163
|
+
);
|
|
21707
22164
|
return p;
|
|
21708
22165
|
}
|
|
21709
22166
|
let itemIdx = -1;
|
|
@@ -21776,7 +22233,11 @@ var planTool = {
|
|
|
21776
22233
|
${formatTaskList(taskFile.tasks)}`
|
|
21777
22234
|
);
|
|
21778
22235
|
} catch (err) {
|
|
21779
|
-
return mkResult(
|
|
22236
|
+
return mkResult(
|
|
22237
|
+
plan,
|
|
22238
|
+
false,
|
|
22239
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
22240
|
+
);
|
|
21780
22241
|
}
|
|
21781
22242
|
}
|
|
21782
22243
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
@@ -21853,9 +22314,9 @@ var readTool = {
|
|
|
21853
22314
|
}
|
|
21854
22315
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
21855
22316
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
21856
|
-
let
|
|
22317
|
+
let stat18;
|
|
21857
22318
|
try {
|
|
21858
|
-
|
|
22319
|
+
stat18 = await fs28.stat(absPath);
|
|
21859
22320
|
} catch (err) {
|
|
21860
22321
|
const code = err.code;
|
|
21861
22322
|
if (code === "ENOENT") {
|
|
@@ -21874,7 +22335,7 @@ var readTool = {
|
|
|
21874
22335
|
cause: err
|
|
21875
22336
|
});
|
|
21876
22337
|
}
|
|
21877
|
-
if (!
|
|
22338
|
+
if (!stat18.isFile()) {
|
|
21878
22339
|
throw new FsError({
|
|
21879
22340
|
message: `read: "${input.path}" is not a regular file`,
|
|
21880
22341
|
code: "FS_READ_FAILED",
|
|
@@ -21882,23 +22343,23 @@ var readTool = {
|
|
|
21882
22343
|
context: { reason: "not-a-regular-file" }
|
|
21883
22344
|
});
|
|
21884
22345
|
}
|
|
21885
|
-
if (
|
|
22346
|
+
if (stat18.size > MAX_BYTES2) {
|
|
21886
22347
|
throw new FsError({
|
|
21887
|
-
message: `read: file too large (${
|
|
22348
|
+
message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
|
|
21888
22349
|
code: "FS_READ_FAILED",
|
|
21889
22350
|
path: absPath,
|
|
21890
|
-
context: { size:
|
|
22351
|
+
context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
21891
22352
|
});
|
|
21892
22353
|
}
|
|
21893
22354
|
const offset = Math.max(1, input.offset ?? 1);
|
|
21894
22355
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
21895
22356
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
21896
22357
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
21897
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
21898
|
-
ctx.recordRead(absPath,
|
|
22358
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
|
|
22359
|
+
ctx.recordRead(absPath, stat18.mtimeMs);
|
|
21899
22360
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21900
22361
|
return {
|
|
21901
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
22362
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
21902
22363
|
total_lines: prior.totalLines,
|
|
21903
22364
|
encoding: "utf8",
|
|
21904
22365
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -21916,11 +22377,11 @@ var readTool = {
|
|
|
21916
22377
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
21917
22378
|
const total = allLines.length;
|
|
21918
22379
|
if (input.mode === "summary") {
|
|
21919
|
-
ctx.recordRead(absPath,
|
|
21920
|
-
rememberReadRange(ctx, absPath,
|
|
22380
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22381
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
|
|
21921
22382
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21922
22383
|
return {
|
|
21923
|
-
text: summarizeFile(input.path,
|
|
22384
|
+
text: summarizeFile(input.path, stat18.size, allLines),
|
|
21924
22385
|
total_lines: total,
|
|
21925
22386
|
encoding: "utf8",
|
|
21926
22387
|
truncated: total > 200,
|
|
@@ -21932,8 +22393,8 @@ var readTool = {
|
|
|
21932
22393
|
};
|
|
21933
22394
|
}
|
|
21934
22395
|
if (limit === 0) {
|
|
21935
|
-
ctx.recordRead(absPath,
|
|
21936
|
-
rememberReadRange(ctx, absPath,
|
|
22396
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22397
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
|
|
21937
22398
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21938
22399
|
return {
|
|
21939
22400
|
text: "",
|
|
@@ -21945,8 +22406,8 @@ var readTool = {
|
|
|
21945
22406
|
};
|
|
21946
22407
|
}
|
|
21947
22408
|
if (offset > total) {
|
|
21948
|
-
ctx.recordRead(absPath,
|
|
21949
|
-
rememberReadRange(ctx, absPath,
|
|
22409
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22410
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
|
|
21950
22411
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21951
22412
|
return {
|
|
21952
22413
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -21961,8 +22422,8 @@ var readTool = {
|
|
|
21961
22422
|
const truncated = offset - 1 + slice.length < total;
|
|
21962
22423
|
const width = String(offset + slice.length - 1).length;
|
|
21963
22424
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
21964
|
-
ctx.recordRead(absPath,
|
|
21965
|
-
rememberReadRange(ctx, absPath,
|
|
22425
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
|
|
22426
|
+
rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
|
|
21966
22427
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
21967
22428
|
return {
|
|
21968
22429
|
text: numbered,
|
|
@@ -22068,20 +22529,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
22068
22529
|
}
|
|
22069
22530
|
|
|
22070
22531
|
// src/replace.ts
|
|
22071
|
-
import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
|
|
22072
22532
|
import { spawn as spawn15 } from "node:child_process";
|
|
22073
22533
|
import * as fs29 from "node:fs/promises";
|
|
22074
22534
|
import * as path33 from "node:path";
|
|
22535
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22075
22536
|
import {
|
|
22076
22537
|
atomicWrite as atomicWrite3,
|
|
22077
22538
|
buildChildEnv as buildChildEnv9,
|
|
22078
22539
|
compileGlob as compileGlob4,
|
|
22079
22540
|
detectNewlineStyle as detectNewlineStyle2,
|
|
22541
|
+
expectDefined as expectDefined8,
|
|
22080
22542
|
normalizeToLf as normalizeToLf2,
|
|
22081
22543
|
toStyle as toStyle2,
|
|
22082
22544
|
unifiedDiff as unifiedDiff2
|
|
22083
22545
|
} from "@wrongstack/core/utils";
|
|
22084
|
-
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22085
22546
|
init_util();
|
|
22086
22547
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
22087
22548
|
var replaceTool = {
|
|
@@ -22090,6 +22551,10 @@ var replaceTool = {
|
|
|
22090
22551
|
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.",
|
|
22091
22552
|
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.",
|
|
22092
22553
|
permission: "confirm",
|
|
22554
|
+
// WS-046: gives permission decisions something to key on.
|
|
22555
|
+
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
22556
|
+
// where a bulk replace may run.
|
|
22557
|
+
subjectKey: "files",
|
|
22093
22558
|
mutating: true,
|
|
22094
22559
|
capabilities: ["fs.write"],
|
|
22095
22560
|
icon: "edit",
|
|
@@ -22162,8 +22627,8 @@ var replaceTool = {
|
|
|
22162
22627
|
}
|
|
22163
22628
|
const rel = path33.relative(realRoot, realPath);
|
|
22164
22629
|
if (rel.startsWith("..") || path33.isAbsolute(rel)) continue;
|
|
22165
|
-
const
|
|
22166
|
-
if (!
|
|
22630
|
+
const stat18 = await fs29.stat(realPath).catch(() => null);
|
|
22631
|
+
if (!stat18?.isFile()) continue;
|
|
22167
22632
|
let content;
|
|
22168
22633
|
try {
|
|
22169
22634
|
const buf = await fs29.readFile(realPath);
|
|
@@ -22188,7 +22653,7 @@ var replaceTool = {
|
|
|
22188
22653
|
totalReplacements += count;
|
|
22189
22654
|
if (!dryRun) {
|
|
22190
22655
|
const newContent = toStyle2(newContentLf, style);
|
|
22191
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
22656
|
+
await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
|
|
22192
22657
|
const written = await fs29.stat(realPath).catch(() => null);
|
|
22193
22658
|
if (written) {
|
|
22194
22659
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -22228,8 +22693,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
22228
22693
|
const resolved = [];
|
|
22229
22694
|
for (const p of parts) {
|
|
22230
22695
|
const absPath = safeResolve(p, ctx);
|
|
22231
|
-
const
|
|
22232
|
-
if (
|
|
22696
|
+
const stat18 = await fs29.stat(absPath).catch(() => null);
|
|
22697
|
+
if (stat18?.isFile()) {
|
|
22233
22698
|
resolved.push(absPath);
|
|
22234
22699
|
}
|
|
22235
22700
|
}
|
|
@@ -22249,7 +22714,11 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
22249
22714
|
function checkRg() {
|
|
22250
22715
|
return new Promise((resolve16) => {
|
|
22251
22716
|
try {
|
|
22252
|
-
const p = spawn15("rg", ["--version"], {
|
|
22717
|
+
const p = spawn15("rg", ["--version"], {
|
|
22718
|
+
env: buildChildEnv9(),
|
|
22719
|
+
stdio: "ignore",
|
|
22720
|
+
windowsHide: true
|
|
22721
|
+
});
|
|
22253
22722
|
p.on("error", () => resolve16(false));
|
|
22254
22723
|
p.on("close", (code) => resolve16(code === 0));
|
|
22255
22724
|
} catch {
|
|
@@ -22300,8 +22769,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
22300
22769
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
22301
22770
|
const full = path33.join(dir, e.name);
|
|
22302
22771
|
try {
|
|
22303
|
-
const
|
|
22304
|
-
if (
|
|
22772
|
+
const stat18 = await fs29.lstat(full);
|
|
22773
|
+
if (stat18.isSymbolicLink()) continue;
|
|
22305
22774
|
} catch {
|
|
22306
22775
|
continue;
|
|
22307
22776
|
}
|
|
@@ -22425,6 +22894,7 @@ var scaffoldTool = {
|
|
|
22425
22894
|
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.",
|
|
22426
22895
|
permission: "confirm",
|
|
22427
22896
|
mutating: true,
|
|
22897
|
+
subjectKey: "name",
|
|
22428
22898
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
22429
22899
|
icon: "scaffold",
|
|
22430
22900
|
timeoutMs: 3e4,
|
|
@@ -22499,15 +22969,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
22499
22969
|
output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
|
|
22500
22970
|
};
|
|
22501
22971
|
}
|
|
22972
|
+
function escapeRegexLiteral(value) {
|
|
22973
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22974
|
+
}
|
|
22502
22975
|
function substituteVars(content, name, vars) {
|
|
22976
|
+
const kebab = name.toLowerCase().replace(/\s+/g, "-");
|
|
22977
|
+
const pascal = name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase());
|
|
22503
22978
|
let result = content;
|
|
22504
|
-
result = result.replace(/\{\{name\}\}/g,
|
|
22505
|
-
result = result.replace(
|
|
22506
|
-
/\{\{Name\}\}/g,
|
|
22507
|
-
name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
|
|
22508
|
-
);
|
|
22979
|
+
result = result.replace(/\{\{name\}\}/g, () => kebab);
|
|
22980
|
+
result = result.replace(/\{\{Name\}\}/g, () => pascal);
|
|
22509
22981
|
for (const [k, v] of Object.entries(vars)) {
|
|
22510
|
-
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
|
|
22982
|
+
result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
|
|
22511
22983
|
}
|
|
22512
22984
|
return result;
|
|
22513
22985
|
}
|
|
@@ -23443,11 +23915,11 @@ var testTool = {
|
|
|
23443
23915
|
}
|
|
23444
23916
|
};
|
|
23445
23917
|
async function detectRunner(cwd) {
|
|
23446
|
-
const { stat:
|
|
23918
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
23447
23919
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
23448
23920
|
for (const f of candidates) {
|
|
23449
23921
|
try {
|
|
23450
|
-
await
|
|
23922
|
+
await stat18(path35.join(cwd, f));
|
|
23451
23923
|
if (f.includes("vitest")) return "vitest";
|
|
23452
23924
|
if (f.includes("jest")) return "jest";
|
|
23453
23925
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -23844,12 +24316,19 @@ var toolSearchTool = {
|
|
|
23844
24316
|
};
|
|
23845
24317
|
|
|
23846
24318
|
// src/tool-use.ts
|
|
24319
|
+
import {
|
|
24320
|
+
GOVERNED_TOOL_EXECUTOR_META_KEY as GOVERNED_TOOL_EXECUTOR_META_KEY2
|
|
24321
|
+
} from "@wrongstack/core/types";
|
|
23847
24322
|
var toolUseTool = {
|
|
23848
24323
|
name: "tool_use",
|
|
23849
24324
|
category: "Meta",
|
|
23850
24325
|
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.",
|
|
23851
24326
|
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.",
|
|
23852
24327
|
permission: "confirm",
|
|
24328
|
+
// WS-046: gives permission decisions something to key on.
|
|
24329
|
+
// The tool being invoked through this indirection — the whole point of a
|
|
24330
|
+
// permission decision here is WHICH tool is being reached.
|
|
24331
|
+
subjectKey: "tool",
|
|
23853
24332
|
mutating: true,
|
|
23854
24333
|
timeoutMs: 6e4,
|
|
23855
24334
|
capabilities: ["tool.mutate.any"],
|
|
@@ -23868,7 +24347,7 @@ var toolUseTool = {
|
|
|
23868
24347
|
},
|
|
23869
24348
|
required: ["tool"]
|
|
23870
24349
|
},
|
|
23871
|
-
async execute(input, ctx
|
|
24350
|
+
async execute(input, ctx) {
|
|
23872
24351
|
const start = Date.now();
|
|
23873
24352
|
if (!input?.tool) {
|
|
23874
24353
|
return {
|
|
@@ -23887,6 +24366,14 @@ var toolUseTool = {
|
|
|
23887
24366
|
executionMs: Date.now() - start
|
|
23888
24367
|
};
|
|
23889
24368
|
}
|
|
24369
|
+
if (tool.name === toolUseTool.name) {
|
|
24370
|
+
return {
|
|
24371
|
+
tool: input.tool,
|
|
24372
|
+
success: false,
|
|
24373
|
+
error: "tool_use: recursive meta-tool execution is not allowed",
|
|
24374
|
+
executionMs: Date.now() - start
|
|
24375
|
+
};
|
|
24376
|
+
}
|
|
23890
24377
|
if (tool.permission === "deny") {
|
|
23891
24378
|
return {
|
|
23892
24379
|
tool: input.tool,
|
|
@@ -23895,12 +24382,21 @@ var toolUseTool = {
|
|
|
23895
24382
|
executionMs: Date.now() - start
|
|
23896
24383
|
};
|
|
23897
24384
|
}
|
|
24385
|
+
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY2];
|
|
24386
|
+
if (typeof governedExecute !== "function") {
|
|
24387
|
+
return {
|
|
24388
|
+
tool: input.tool,
|
|
24389
|
+
success: false,
|
|
24390
|
+
error: "tool_use: governed nested execution is unavailable; call the tool directly",
|
|
24391
|
+
executionMs: Date.now() - start
|
|
24392
|
+
};
|
|
24393
|
+
}
|
|
23898
24394
|
try {
|
|
23899
|
-
const result = await
|
|
24395
|
+
const result = await governedExecute(input.tool, input.input ?? {});
|
|
23900
24396
|
return {
|
|
23901
24397
|
tool: input.tool,
|
|
23902
|
-
success:
|
|
23903
|
-
result,
|
|
24398
|
+
success: result.success,
|
|
24399
|
+
...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
|
|
23904
24400
|
executionMs: Date.now() - start
|
|
23905
24401
|
};
|
|
23906
24402
|
} catch (e) {
|
|
@@ -23916,10 +24412,12 @@ var toolUseTool = {
|
|
|
23916
24412
|
|
|
23917
24413
|
// src/tree.ts
|
|
23918
24414
|
init_util();
|
|
23919
|
-
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
23920
24415
|
import * as fs32 from "node:fs/promises";
|
|
23921
24416
|
import * as path36 from "node:path";
|
|
24417
|
+
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
23922
24418
|
var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
|
|
24419
|
+
var DEFAULT_MAX_ENTRIES2 = 5e3;
|
|
24420
|
+
var MAX_TREE_OUTPUT_BYTES = 256 * 1024;
|
|
23923
24421
|
var treeTool = {
|
|
23924
24422
|
name: "tree",
|
|
23925
24423
|
category: "Filesystem",
|
|
@@ -23963,6 +24461,12 @@ var treeTool = {
|
|
|
23963
24461
|
show_hidden: {
|
|
23964
24462
|
type: "boolean",
|
|
23965
24463
|
description: "Show hidden files starting with . (default: false)"
|
|
24464
|
+
},
|
|
24465
|
+
max_entries: {
|
|
24466
|
+
type: "integer",
|
|
24467
|
+
description: `Maximum entries retained in the rendered tree (default ${DEFAULT_MAX_ENTRIES2}).`,
|
|
24468
|
+
minimum: 1,
|
|
24469
|
+
maximum: 5e4
|
|
23966
24470
|
}
|
|
23967
24471
|
}
|
|
23968
24472
|
},
|
|
@@ -23976,7 +24480,7 @@ var treeTool = {
|
|
|
23976
24480
|
if (!final) throw new Error("tree: stream ended without final event");
|
|
23977
24481
|
return final;
|
|
23978
24482
|
},
|
|
23979
|
-
async *executeStream(input, ctx) {
|
|
24483
|
+
async *executeStream(input, ctx, opts) {
|
|
23980
24484
|
const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
23981
24485
|
const maxDepth = input.depth ?? 3;
|
|
23982
24486
|
const showFiles = input.show_files ?? true;
|
|
@@ -23984,8 +24488,17 @@ var treeTool = {
|
|
|
23984
24488
|
const showHidden = input.show_hidden ?? false;
|
|
23985
24489
|
const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...input.exclude ?? []]);
|
|
23986
24490
|
const filterGlob = input.glob;
|
|
24491
|
+
const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES2;
|
|
24492
|
+
const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
23987
24493
|
const lines = [basePath];
|
|
23988
24494
|
const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
|
|
24495
|
+
const retention = {
|
|
24496
|
+
entries: 0,
|
|
24497
|
+
outputBytes: Buffer.byteLength(basePath, "utf8"),
|
|
24498
|
+
maxEntries,
|
|
24499
|
+
maxOutputBytes: MAX_TREE_OUTPUT_BYTES,
|
|
24500
|
+
truncated: false
|
|
24501
|
+
};
|
|
23989
24502
|
const queue = [];
|
|
23990
24503
|
const FLUSH_EVERY = 200;
|
|
23991
24504
|
let lastEmittedTotal = 0;
|
|
@@ -24012,12 +24525,19 @@ var treeTool = {
|
|
|
24012
24525
|
isLast: true,
|
|
24013
24526
|
totalFiles: totals.totalFiles,
|
|
24014
24527
|
totalDirs: totals.totalDirs,
|
|
24015
|
-
onProgress: tickProgress
|
|
24528
|
+
onProgress: tickProgress,
|
|
24529
|
+
retention,
|
|
24530
|
+
signal
|
|
24016
24531
|
});
|
|
24017
24532
|
let walkDone = false;
|
|
24018
|
-
walkPromise.
|
|
24019
|
-
|
|
24020
|
-
|
|
24533
|
+
void walkPromise.then(
|
|
24534
|
+
() => {
|
|
24535
|
+
walkDone = true;
|
|
24536
|
+
},
|
|
24537
|
+
() => {
|
|
24538
|
+
walkDone = true;
|
|
24539
|
+
}
|
|
24540
|
+
);
|
|
24021
24541
|
while (!walkDone || queue.length > 0) {
|
|
24022
24542
|
if (queue.length > 0) {
|
|
24023
24543
|
yield expectDefined10(queue.shift());
|
|
@@ -24040,13 +24560,15 @@ var treeTool = {
|
|
|
24040
24560
|
tree: lines.join("\n"),
|
|
24041
24561
|
total_files: totals.totalFiles.value,
|
|
24042
24562
|
total_dirs: totals.totalDirs.value,
|
|
24043
|
-
truncated:
|
|
24563
|
+
truncated: retention.truncated,
|
|
24044
24564
|
path: basePath
|
|
24045
24565
|
}
|
|
24046
24566
|
};
|
|
24047
24567
|
}
|
|
24048
24568
|
};
|
|
24049
24569
|
async function walkDir(dir, depth, opts) {
|
|
24570
|
+
opts.signal.throwIfAborted();
|
|
24571
|
+
if (opts.retention.truncated) return;
|
|
24050
24572
|
const entries = await fs32.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
24051
24573
|
const filtered = entries.filter((e) => {
|
|
24052
24574
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
@@ -24066,6 +24588,8 @@ async function walkDir(dir, depth, opts) {
|
|
|
24066
24588
|
return a.name.localeCompare(b.name);
|
|
24067
24589
|
});
|
|
24068
24590
|
for (let i = 0; i < items.length; i++) {
|
|
24591
|
+
opts.signal.throwIfAborted();
|
|
24592
|
+
if (opts.retention.truncated) return;
|
|
24069
24593
|
const entry = items[i];
|
|
24070
24594
|
if (!entry) continue;
|
|
24071
24595
|
const isLast = i === items.length - 1;
|
|
@@ -24074,7 +24598,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
24074
24598
|
const displayName = entry.name + (entry.isDirectory() ? "/" : "");
|
|
24075
24599
|
if (!opts.showDirs && entry.isDirectory()) continue;
|
|
24076
24600
|
if (!opts.showFiles && entry.isFile()) continue;
|
|
24077
|
-
opts.
|
|
24601
|
+
const line = opts.prefix + branch + displayName;
|
|
24602
|
+
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
|
|
24603
|
+
if (opts.retention.entries >= opts.retention.maxEntries || opts.retention.outputBytes + lineBytes > opts.retention.maxOutputBytes) {
|
|
24604
|
+
opts.retention.truncated = true;
|
|
24605
|
+
return;
|
|
24606
|
+
}
|
|
24607
|
+
opts.lines.push(line);
|
|
24608
|
+
opts.retention.entries++;
|
|
24609
|
+
opts.retention.outputBytes += lineBytes;
|
|
24078
24610
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
24079
24611
|
const childPrefix = opts.prefix + connector;
|
|
24080
24612
|
await walkDir(path36.join(dir, entry.name), depth + 1, {
|
|
@@ -24189,11 +24721,11 @@ var typecheckTool = {
|
|
|
24189
24721
|
}
|
|
24190
24722
|
};
|
|
24191
24723
|
async function findTsConfig(cwd) {
|
|
24192
|
-
const { stat:
|
|
24724
|
+
const { stat: stat18 } = await import("node:fs/promises");
|
|
24193
24725
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
24194
24726
|
for (const f of candidates) {
|
|
24195
24727
|
try {
|
|
24196
|
-
const s = await
|
|
24728
|
+
const s = await stat18(path37.join(cwd, f));
|
|
24197
24729
|
if (s.isFile()) return path37.join(cwd, f);
|
|
24198
24730
|
} catch {
|
|
24199
24731
|
}
|
|
@@ -24268,12 +24800,12 @@ async function prepareWrite(input, ctx) {
|
|
|
24268
24800
|
let existed = false;
|
|
24269
24801
|
let prev = "";
|
|
24270
24802
|
try {
|
|
24271
|
-
const
|
|
24272
|
-
existed =
|
|
24803
|
+
const stat18 = await fs33.stat(absPath);
|
|
24804
|
+
existed = stat18.isFile();
|
|
24273
24805
|
if (existed) {
|
|
24274
24806
|
if (!ctx.hasRead(absPath)) {
|
|
24275
24807
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24276
|
-
ctx.recordRead(absPath,
|
|
24808
|
+
ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
|
|
24277
24809
|
} else {
|
|
24278
24810
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24279
24811
|
}
|
|
@@ -24290,8 +24822,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
24290
24822
|
await atomicWrite5(prepared.absPath, input.content);
|
|
24291
24823
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
24292
24824
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
24293
|
-
const
|
|
24294
|
-
ctx.recordRead(prepared.absPath,
|
|
24825
|
+
const stat18 = await fs33.stat(prepared.absPath);
|
|
24826
|
+
ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
|
|
24295
24827
|
ctx.session.recordFileChange({
|
|
24296
24828
|
path: prepared.absPath,
|
|
24297
24829
|
action: prepared.existed ? "modified" : "created",
|