@wrongstack/tools 0.1.4 → 0.1.8

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.
Files changed (51) hide show
  1. package/README.md +127 -0
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +103 -5
  4. package/dist/bash.js.map +1 -1
  5. package/dist/builtin.js +550 -258
  6. package/dist/builtin.js.map +1 -1
  7. package/dist/diff.js +5 -9
  8. package/dist/diff.js.map +1 -1
  9. package/dist/document.js +0 -1
  10. package/dist/document.js.map +1 -1
  11. package/dist/edit.js +2 -2
  12. package/dist/edit.js.map +1 -1
  13. package/dist/exec.d.ts +0 -1
  14. package/dist/exec.js +105 -44
  15. package/dist/exec.js.map +1 -1
  16. package/dist/fetch.js +110 -25
  17. package/dist/fetch.js.map +1 -1
  18. package/dist/format.js.map +1 -1
  19. package/dist/git.d.ts +0 -1
  20. package/dist/git.js +9 -9
  21. package/dist/git.js.map +1 -1
  22. package/dist/glob.js +0 -1
  23. package/dist/glob.js.map +1 -1
  24. package/dist/grep.js +58 -3
  25. package/dist/grep.js.map +1 -1
  26. package/dist/index.js +549 -257
  27. package/dist/index.js.map +1 -1
  28. package/dist/install.js.map +1 -1
  29. package/dist/lint.js.map +1 -1
  30. package/dist/logs.js +61 -6
  31. package/dist/logs.js.map +1 -1
  32. package/dist/outdated.js.map +1 -1
  33. package/dist/patch.js +68 -29
  34. package/dist/patch.js.map +1 -1
  35. package/dist/read.js +0 -1
  36. package/dist/read.js.map +1 -1
  37. package/dist/replace.js +59 -9
  38. package/dist/replace.js.map +1 -1
  39. package/dist/scaffold.js +5 -6
  40. package/dist/scaffold.js.map +1 -1
  41. package/dist/test.js.map +1 -1
  42. package/dist/todo.js +1 -1
  43. package/dist/todo.js.map +1 -1
  44. package/dist/tool-use.js +0 -8
  45. package/dist/tool-use.js.map +1 -1
  46. package/dist/tree.js +9 -5
  47. package/dist/tree.js.map +1 -1
  48. package/dist/typecheck.js.map +1 -1
  49. package/dist/write.js +0 -1
  50. package/dist/write.js.map +1 -1
  51. package/package.json +7 -4
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import * as fs4 from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import { dirname } from 'path';
4
- import { spawn } from 'child_process';
5
4
  import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, stripAnsi } from '@wrongstack/core';
5
+ import { spawn } from 'child_process';
6
6
  import * as os from 'os';
7
7
  import * as dns from 'dns/promises';
8
- import * as fsSync from 'fs';
8
+ import * as net from 'net';
9
9
  import { statSync } from 'fs';
10
10
 
11
11
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
@@ -43,83 +43,6 @@ function isBinaryBuffer(buf) {
43
43
  }
44
44
  return false;
45
45
  }
46
- async function* spawnStream(opts) {
47
- const max = opts.maxBytes ?? 2e5;
48
- const flushAt = opts.flushBytes ?? 4 * 1024;
49
- let stdout = "";
50
- let stderr = "";
51
- let pending = "";
52
- let error;
53
- const child = spawn(opts.cmd, opts.args, {
54
- cwd: opts.cwd,
55
- signal: opts.signal,
56
- stdio: ["ignore", "pipe", "pipe"]
57
- });
58
- const queue = [];
59
- let waiter;
60
- const wake = () => {
61
- if (waiter) {
62
- const w = waiter;
63
- waiter = void 0;
64
- w();
65
- }
66
- };
67
- child.stdout?.on("data", (c) => {
68
- const s = c.toString();
69
- if (stdout.length < max) stdout += s;
70
- queue.push({ kind: "out", data: s });
71
- wake();
72
- });
73
- child.stderr?.on("data", (c) => {
74
- const s = c.toString();
75
- if (stderr.length < max) stderr += s;
76
- queue.push({ kind: "err", data: s });
77
- wake();
78
- });
79
- child.on("error", (e) => {
80
- error = e.message;
81
- queue.push({ kind: "error", data: e.message });
82
- wake();
83
- });
84
- child.on("close", (code) => {
85
- queue.push({ kind: "close", data: "", code: code ?? 0 });
86
- wake();
87
- });
88
- let exitCode = 0;
89
- let spawnFailed = false;
90
- for (; ; ) {
91
- while (queue.length === 0) {
92
- await new Promise((resolve2) => {
93
- waiter = resolve2;
94
- });
95
- }
96
- const chunk = queue.shift();
97
- if (chunk.kind === "close") {
98
- if (!spawnFailed) exitCode = chunk.code ?? 0;
99
- break;
100
- }
101
- if (chunk.kind === "error") {
102
- spawnFailed = true;
103
- exitCode = 1;
104
- continue;
105
- }
106
- pending += chunk.data;
107
- if (pending.length >= flushAt) {
108
- yield { type: "partial_output", text: pending };
109
- pending = "";
110
- }
111
- }
112
- if (pending.length > 0) {
113
- yield { type: "partial_output", text: pending };
114
- }
115
- return {
116
- stdout,
117
- stderr,
118
- exitCode,
119
- truncated: stdout.length >= max || stderr.length >= max,
120
- error
121
- };
122
- }
123
46
 
124
47
  // src/read.ts
125
48
  var MAX_BYTES = 5 * 1024 * 1024;
@@ -256,7 +179,8 @@ var editTool = {
256
179
  );
257
180
  }
258
181
  const lastReadMtime = ctx.lastReadMtime(absPath);
259
- if (lastReadMtime !== void 0 && stat9.mtimeMs > lastReadMtime + 1) {
182
+ const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
183
+ if (lastReadMtime !== void 0 && stat9.mtimeMs > lastReadMtime + mtimeTolerance) {
260
184
  throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
261
185
  }
262
186
  const original = await fs4.readFile(absPath, "utf8");
@@ -331,6 +255,48 @@ function findSimilarity(haystack, needle) {
331
255
  }
332
256
  return line;
333
257
  }
258
+
259
+ // src/_regex.ts
260
+ var MAX_PATTERN_LEN = 512;
261
+ var DANGEROUS_PATTERNS = [
262
+ /(\([^)]*[+*][^)]*\))[+*]/,
263
+ // (a+)+, (.*)+, etc — nested quantifier on a group with internal quantifier
264
+ /(\(\?:[^)]*[+*][^)]*\))[+*]/
265
+ // same, with non-capturing group
266
+ ];
267
+ function compileUserRegex(pattern, flags) {
268
+ if (typeof pattern !== "string") {
269
+ return { ok: false, reason: "pattern must be a string" };
270
+ }
271
+ if (pattern.length === 0) {
272
+ return { ok: false, reason: "pattern is empty" };
273
+ }
274
+ if (pattern.length > MAX_PATTERN_LEN) {
275
+ return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
276
+ }
277
+ for (const rx of DANGEROUS_PATTERNS) {
278
+ if (rx.test(pattern)) {
279
+ return {
280
+ ok: false,
281
+ reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
282
+ };
283
+ }
284
+ }
285
+ try {
286
+ return { ok: true, regex: new RegExp(pattern, flags) };
287
+ } catch (err) {
288
+ return {
289
+ ok: false,
290
+ reason: err instanceof Error ? err.message : "invalid regex"
291
+ };
292
+ }
293
+ }
294
+ var MAX_SUBJECT_LEN = 64 * 1024;
295
+ function capSubject(line) {
296
+ return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
297
+ }
298
+
299
+ // src/replace.ts
334
300
  var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
335
301
  var replaceTool = {
336
302
  name: "replace",
@@ -358,23 +324,38 @@ var replaceTool = {
358
324
  if (!input?.pattern) throw new Error("replace: pattern is required");
359
325
  if (input.replacement === void 0) throw new Error("replace: replacement is required");
360
326
  if (!input?.files) throw new Error("replace: files is required");
361
- const re = new RegExp(input.pattern, "g");
327
+ const replaceAll = input.replace_all ?? true;
328
+ const compiled = compileUserRegex(input.pattern, replaceAll ? "g" : "");
329
+ if (!compiled.ok) {
330
+ throw new Error(`replace: ${compiled.reason}`);
331
+ }
332
+ const re = compiled.regex;
362
333
  const globRe = input.glob ? compileGlob(input.glob) : null;
363
334
  const dryRun = input.dry_run ?? false;
364
- const replaceAll = input.replace_all ?? true;
365
335
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
366
336
  const fileList = await resolveFiles(filesInput, ctx, globRe);
367
337
  const results = [];
368
338
  let totalReplacements = 0;
369
339
  for (const absPath of fileList) {
370
- const stat9 = await fs4.stat(absPath).catch((err) => {
340
+ const lstat2 = await fs4.lstat(absPath).catch((err) => {
371
341
  if (err.code === "ENOENT") return null;
372
342
  throw err;
373
343
  });
344
+ if (!lstat2 || !lstat2.isFile()) continue;
345
+ if (lstat2.isSymbolicLink()) continue;
346
+ let realPath;
347
+ try {
348
+ realPath = await fs4.realpath(absPath);
349
+ } catch {
350
+ continue;
351
+ }
352
+ const rel = path.relative(ctx.projectRoot, realPath);
353
+ if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
354
+ const stat9 = await fs4.stat(realPath).catch(() => null);
374
355
  if (!stat9 || !stat9.isFile()) continue;
375
356
  let content;
376
357
  try {
377
- const buf = await fs4.readFile(absPath);
358
+ const buf = await fs4.readFile(realPath);
378
359
  if (isBinaryBuffer(buf)) continue;
379
360
  content = buf.toString("utf8");
380
361
  } catch {
@@ -385,13 +366,12 @@ var replaceTool = {
385
366
  re.lastIndex = 0;
386
367
  const matches = [...contentLf.matchAll(re)];
387
368
  if (matches.length === 0) continue;
388
- const newContentLf = replaceAll ? contentLf.replace(re, input.replacement) : contentLf.replace(re, input.replacement);
369
+ const newContentLf = contentLf.replace(re, input.replacement);
389
370
  re.lastIndex = 0;
390
- const actualCount = replaceAll ? matches.length : 1;
391
- totalReplacements += actualCount;
371
+ totalReplacements += matches.length;
392
372
  if (!dryRun) {
393
373
  const newContent = toStyle(newContentLf, style);
394
- await atomicWrite(absPath, newContent, { mode: stat9.mode & 511 });
374
+ await atomicWrite(realPath, newContent, { mode: stat9.mode & 511 });
395
375
  }
396
376
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), { fromFile: absPath, toFile: absPath }) : void 0;
397
377
  results.push({
@@ -438,13 +418,13 @@ async function globFiles(pattern, base, extraGlob) {
438
418
  return await globNative(pattern, base, extraGlob);
439
419
  }
440
420
  function checkRg() {
441
- return new Promise((resolve2) => {
421
+ return new Promise((resolve4) => {
442
422
  try {
443
423
  const p = spawn("rg", ["--version"], { stdio: "ignore" });
444
- p.on("error", () => resolve2(false));
445
- p.on("close", (code) => resolve2(code === 0));
424
+ p.on("error", () => resolve4(false));
425
+ p.on("close", (code) => resolve4(code === 0));
446
426
  } catch {
447
- resolve2(false);
427
+ resolve4(false);
448
428
  }
449
429
  });
450
430
  }
@@ -456,10 +436,10 @@ function spawnRgFind(pattern, base) {
456
436
  buf += chunk.toString();
457
437
  });
458
438
  return {
459
- promise: new Promise((resolve2, reject) => {
439
+ promise: new Promise((resolve4, reject) => {
460
440
  child.on("error", reject);
461
441
  child.on("close", () => {
462
- resolve2(buf.split("\n").filter(Boolean));
442
+ resolve4(buf.split("\n").filter(Boolean));
463
443
  });
464
444
  })
465
445
  };
@@ -616,13 +596,13 @@ var grepTool = {
616
596
  }
617
597
  };
618
598
  async function detectRg(signal) {
619
- return new Promise((resolve2) => {
599
+ return new Promise((resolve4) => {
620
600
  try {
621
601
  const p = spawn("rg", ["--version"], { stdio: "ignore", signal });
622
- p.on("error", () => resolve2(false));
623
- p.on("close", (code) => resolve2(code === 0));
602
+ p.on("error", () => resolve4(false));
603
+ p.on("close", (code) => resolve4(code === 0));
624
604
  } catch {
625
- resolve2(false);
605
+ resolve4(false);
626
606
  }
627
607
  });
628
608
  }
@@ -642,6 +622,8 @@ async function* runRgStream(input, base, mode, limit, signal) {
642
622
  let totalLines = 0;
643
623
  let batchSinceFlush = 0;
644
624
  const FLUSH_AT = 16;
625
+ const MAX_BUF_BYTES = 1e6;
626
+ let bufOverflow = false;
645
627
  const child = spawn("rg", args, { signal, stdio: ["ignore", "pipe", "pipe"] });
646
628
  const queue = [];
647
629
  let waiter;
@@ -679,6 +661,14 @@ async function* runRgStream(input, base, mode, limit, signal) {
679
661
  }
680
662
  if (c.kind === "close") break;
681
663
  buf += c.data;
664
+ if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
665
+ bufOverflow = true;
666
+ buf = buf.slice(-MAX_BUF_BYTES);
667
+ try {
668
+ child.kill("SIGTERM");
669
+ } catch {
670
+ }
671
+ }
682
672
  const idx = buf.lastIndexOf("\n");
683
673
  if (idx === -1) continue;
684
674
  const ready = buf.slice(0, idx);
@@ -725,14 +715,18 @@ async function* runRgStream(input, base, mode, limit, signal) {
725
715
  output: {
726
716
  matches,
727
717
  count: totalLines,
728
- truncated: totalLines > limit,
718
+ truncated: totalLines > limit || bufOverflow,
729
719
  used: "rg"
730
720
  }
731
721
  };
732
722
  }
733
723
  async function runNative(input, base, mode, limit, signal) {
734
724
  const flags = input.case_insensitive ? "i" : "";
735
- const re = new RegExp(input.pattern, flags);
725
+ const compiled = compileUserRegex(input.pattern, flags);
726
+ if (!compiled.ok) {
727
+ throw new Error(`grep: ${compiled.reason}`);
728
+ }
729
+ const re = compiled.regex;
736
730
  const globRe = input.glob ? compileGlob(input.glob) : null;
737
731
  const matches = [];
738
732
  const fileMatches = /* @__PURE__ */ new Map();
@@ -749,6 +743,7 @@ async function runNative(input, base, mode, limit, signal) {
749
743
  for (const e of entries) {
750
744
  if (stopped) return;
751
745
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
746
+ if (e.isSymbolicLink()) continue;
752
747
  const full = path.join(dir, e.name);
753
748
  if (e.isDirectory()) {
754
749
  await walk(full);
@@ -764,7 +759,7 @@ async function runNative(input, base, mode, limit, signal) {
764
759
  const lines = text.split(/\r?\n/);
765
760
  let fileHits = 0;
766
761
  for (let i = 0; i < lines.length; i++) {
767
- const ln = lines[i] ?? "";
762
+ const ln = capSubject(lines[i] ?? "");
768
763
  re.lastIndex = 0;
769
764
  if (re.test(ln)) {
770
765
  fileHits++;
@@ -797,6 +792,86 @@ async function runNative(input, base, mode, limit, signal) {
797
792
  used: "native"
798
793
  };
799
794
  }
795
+
796
+ // src/_env.ts
797
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
798
+ "PATH",
799
+ "HOME",
800
+ "USER",
801
+ "USERNAME",
802
+ "LOGNAME",
803
+ "SHELL",
804
+ "LANG",
805
+ "LC_ALL",
806
+ "LC_CTYPE",
807
+ "TERM",
808
+ "TZ",
809
+ "TMPDIR",
810
+ "TEMP",
811
+ "TMP",
812
+ "PWD",
813
+ "OLDPWD",
814
+ "COMSPEC",
815
+ "SYSTEMROOT",
816
+ "SYSTEMDRIVE",
817
+ "WINDIR",
818
+ "PROGRAMFILES",
819
+ "PROGRAMFILES(X86)",
820
+ "PROGRAMDATA",
821
+ "APPDATA",
822
+ "LOCALAPPDATA",
823
+ "USERPROFILE",
824
+ "PUBLIC",
825
+ "PATHEXT"
826
+ ]);
827
+ var SECRET_NAME_PARTS = [
828
+ "TOKEN",
829
+ "SECRET",
830
+ "PASSWORD",
831
+ "PASSWD",
832
+ "AUTH",
833
+ "CRED",
834
+ "BEARER",
835
+ "COOKIE",
836
+ "PRIVATE"
837
+ ];
838
+ function looksSecret(name) {
839
+ const upper = name.toUpperCase();
840
+ for (const p of SECRET_NAME_PARTS) {
841
+ if (upper.includes(p)) return true;
842
+ }
843
+ if (/(?:^|_)KEY(?:$|_|S$)/i.test(upper)) return true;
844
+ if (/API[_-]?KEY/i.test(upper)) return true;
845
+ if (/ACCESS[_-]?KEY/i.test(upper)) return true;
846
+ if (/SESSION[_-]?ID/i.test(upper) === false && /SESSION/i.test(upper)) {
847
+ return true;
848
+ }
849
+ return false;
850
+ }
851
+ function buildChildEnv(sessionId) {
852
+ const passthrough = process.env["WRONGSTACK_BASH_ENV_PASSTHROUGH"] === "1";
853
+ const out = {};
854
+ for (const [k, v] of Object.entries(process.env)) {
855
+ if (v === void 0) continue;
856
+ if (passthrough) {
857
+ out[k] = v;
858
+ continue;
859
+ }
860
+ const upper = k.toUpperCase();
861
+ if (ALLOWED_KEYS.has(upper)) {
862
+ out[k] = v;
863
+ continue;
864
+ }
865
+ if (looksSecret(upper)) continue;
866
+ if (upper.startsWith("NODE_") || upper.startsWith("NPM_") || upper.startsWith("PNPM_") || upper.startsWith("YARN_") || upper.startsWith("GIT_") || upper.startsWith("CI") || upper.startsWith("XDG_") || upper === "EDITOR" || upper === "VISUAL" || upper === "PAGER") {
867
+ out[k] = v;
868
+ }
869
+ }
870
+ if (sessionId) out["WRONGSTACK_SESSION_ID"] = sessionId;
871
+ return out;
872
+ }
873
+
874
+ // src/bash.ts
800
875
  var MAX_OUTPUT = 32768;
801
876
  var DEFAULT_TIMEOUT = 3e4;
802
877
  var STREAM_FLUSH_INTERVAL_MS = 200;
@@ -807,6 +882,10 @@ var bashTool = {
807
882
  usageHint: "Runs via `bash -c` (or `cmd /c` on Windows). Cwd is the project root. Default timeout 30s. Output truncated from the middle if oversized. Use for git, npm, builds, tests.",
808
883
  permission: "confirm",
809
884
  mutating: true,
885
+ // Trust rules match on the literal `command` string. Without subjectKey
886
+ // the policy heuristic would have done the same here, but declaring it
887
+ // explicitly removes the implicit cross-tool aliasing.
888
+ subjectKey: "command",
810
889
  timeoutMs: 3e4,
811
890
  maxOutputBytes: MAX_OUTPUT,
812
891
  estimatedDurationMs: 3e3,
@@ -833,13 +912,13 @@ var bashTool = {
833
912
  const isWin = os.platform() === "win32";
834
913
  const shell = isWin ? process.env["COMSPEC"] ?? "cmd.exe" : process.env["SHELL"] ?? "/bin/bash";
835
914
  const args = isWin ? ["/c", input.command] : ["-c", input.command];
836
- const env = { ...process.env };
837
- env["WRONGSTACK_SESSION_ID"] = ctx.session.id;
915
+ const env = buildChildEnv(ctx.session?.id);
916
+ const detached = isWin ? !!input.background : true;
838
917
  const child = spawn(shell, args, {
839
918
  cwd: ctx.projectRoot,
840
919
  env,
841
920
  stdio: input.background ? "ignore" : ["ignore", "pipe", "pipe"],
842
- detached: input.background,
921
+ detached,
843
922
  signal: opts.signal
844
923
  });
845
924
  if (input.background) {
@@ -869,10 +948,26 @@ var bashTool = {
869
948
  }
870
949
  } else {
871
950
  try {
872
- child.kill("SIGTERM");
951
+ if (typeof child.pid === "number") {
952
+ try {
953
+ process.kill(-child.pid, "SIGTERM");
954
+ } catch {
955
+ child.kill("SIGTERM");
956
+ }
957
+ } else {
958
+ child.kill("SIGTERM");
959
+ }
873
960
  const killTimer = setTimeout(() => {
874
961
  try {
875
- child.kill("SIGKILL");
962
+ if (typeof child.pid === "number") {
963
+ try {
964
+ process.kill(-child.pid, "SIGKILL");
965
+ } catch {
966
+ child.kill("SIGKILL");
967
+ }
968
+ } else {
969
+ child.kill("SIGKILL");
970
+ }
876
971
  } catch {
877
972
  }
878
973
  }, 2e3);
@@ -894,10 +989,10 @@ var bashTool = {
894
989
  queue.push(c);
895
990
  }
896
991
  };
897
- const next = () => new Promise((resolve2) => {
992
+ const next = () => new Promise((resolve4) => {
898
993
  const c = queue.shift();
899
- if (c) resolve2(c);
900
- else resolveNext = resolve2;
994
+ if (c) resolve4(c);
995
+ else resolveNext = resolve4;
901
996
  });
902
997
  let lastFlush = Date.now();
903
998
  const flush = () => {
@@ -989,32 +1084,13 @@ var ALLOWED_COMMANDS = {
989
1084
  docker: ["--version", "ps", "images", "build"],
990
1085
  kubectl: ["version", "get", "describe", "logs"]
991
1086
  };
992
- var FORBIDDEN_PATTERNS = [
993
- /;\s*rm\s+-rf/i,
994
- /\|\s*rm\s/i,
995
- /\&\&\s*rm/i,
996
- /\$\(.*rm/s,
997
- /`.*rm/s,
998
- /eval\s*\(/i,
999
- /exec\s+/i,
1000
- /nc\s+-e/i,
1001
- /bash\s+-i/i,
1002
- /\/dev\/tcp\//i,
1003
- /curl\s+.*\|/i,
1004
- /wget\s+.*\|/i,
1005
- /chmod\s+777/i,
1006
- /chmod\s+4755/i,
1007
- />\s*\/dev\//i,
1008
- /2>\s*\/dev\//i,
1009
- /tee\s+/i
1010
- ];
1011
1087
  var MAX_ARGS = 20;
1012
1088
  var MAX_OUTPUT2 = 2e5;
1013
1089
  var TIMEOUT_MS = 3e4;
1014
1090
  var execTool = {
1015
1091
  name: "exec",
1016
1092
  description: "Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.",
1017
- usageHint: "Set `command` (must be in allowlist). `args` passed through. Unknown commands require `allow_unknown: true`. Blocks dangerous patterns.",
1093
+ usageHint: "Set `command` (must be in allowlist). `args` passed through. For arbitrary shell access use the `bash` tool instead.",
1018
1094
  permission: "confirm",
1019
1095
  mutating: false,
1020
1096
  timeoutMs: TIMEOUT_MS,
@@ -1023,57 +1099,56 @@ var execTool = {
1023
1099
  properties: {
1024
1100
  command: { type: "string", description: "Command to run (must be in allowlist)" },
1025
1101
  args: { type: "array", items: { type: "string" }, description: "Arguments" },
1026
- cwd: { type: "string", description: "Working directory" },
1027
- timeout: { type: "integer", description: "Timeout in ms (default: 30000)" },
1028
- allow_unknown: {
1029
- type: "boolean",
1030
- description: "Allow commands not in allowlist (DANGEROUS, use with caution)"
1031
- }
1102
+ cwd: { type: "string", description: "Working directory (must resolve inside project root)" },
1103
+ timeout: { type: "integer", description: "Timeout in ms (default: 30000)" }
1032
1104
  },
1033
1105
  required: ["command"]
1034
1106
  },
1035
1107
  async execute(input, ctx, opts) {
1036
1108
  const cmd = input.command.trim();
1037
1109
  if (!cmd) return { command: cmd, args: [], stdout: "", stderr: "Empty command", exitCode: 1, truncated: false, allowed: false };
1038
- if (FORBIDDEN_PATTERNS.some((re) => re.test(cmd))) {
1110
+ if (!(cmd in ALLOWED_COMMANDS)) {
1039
1111
  return {
1040
1112
  command: cmd,
1041
1113
  args: input.args ?? [],
1042
1114
  stdout: "",
1043
- stderr: `Command blocked: dangerous pattern detected`,
1115
+ stderr: `Command "${cmd}" not in allowlist. Use the bash tool for arbitrary commands.`,
1044
1116
  exitCode: 1,
1045
1117
  truncated: false,
1046
1118
  allowed: false
1047
1119
  };
1048
1120
  }
1049
- const allowedCommands = { ...ALLOWED_COMMANDS };
1050
- if (input.allow_unknown) {
1051
- allowedCommands[cmd] = [];
1052
- }
1053
- if (!(cmd in allowedCommands)) {
1121
+ const args = (input.args ?? []).slice(0, MAX_ARGS);
1122
+ const timeout = Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS);
1123
+ const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
1124
+ const rel = path.relative(ctx.projectRoot, requestedCwd);
1125
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
1054
1126
  return {
1055
1127
  command: cmd,
1056
- args: input.args ?? [],
1128
+ args,
1057
1129
  stdout: "",
1058
- stderr: `Command "${cmd}" not in allowlist. Set allow_unknown: true to bypass.`,
1130
+ stderr: `cwd "${input.cwd}" resolves outside project root`,
1059
1131
  exitCode: 1,
1060
1132
  truncated: false,
1061
1133
  allowed: false
1062
1134
  };
1063
1135
  }
1064
- const args = (input.args ?? []).slice(0, MAX_ARGS);
1065
- const timeout = Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS);
1066
- const cwd = input.cwd ?? ctx.cwd;
1136
+ const cwd = requestedCwd;
1067
1137
  const signal = opts.signal;
1068
- return runCommand(cmd, args, cwd, timeout, signal);
1138
+ return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
1069
1139
  }
1070
1140
  };
1071
- function runCommand(cmd, args, cwd, timeout, signal) {
1072
- return new Promise((resolve2) => {
1141
+ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1142
+ return new Promise((resolve4) => {
1073
1143
  let stdout = "";
1074
1144
  let stderr = "";
1075
1145
  let killed = false;
1076
- const child = spawn(cmd, args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
1146
+ const child = spawn(cmd, args, {
1147
+ cwd,
1148
+ signal,
1149
+ env: buildChildEnv(sessionId),
1150
+ stdio: ["ignore", "pipe", "pipe"]
1151
+ });
1077
1152
  const timer = setTimeout(() => {
1078
1153
  killed = true;
1079
1154
  child.kill("SIGTERM");
@@ -1086,7 +1161,7 @@ function runCommand(cmd, args, cwd, timeout, signal) {
1086
1161
  });
1087
1162
  child.on("close", (code) => {
1088
1163
  clearTimeout(timer);
1089
- resolve2({
1164
+ resolve4({
1090
1165
  command: cmd,
1091
1166
  args,
1092
1167
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -1098,7 +1173,7 @@ function runCommand(cmd, args, cwd, timeout, signal) {
1098
1173
  });
1099
1174
  child.on("error", (err) => {
1100
1175
  clearTimeout(timer);
1101
- resolve2({
1176
+ resolve4({
1102
1177
  command: cmd,
1103
1178
  args,
1104
1179
  stdout: stdout.slice(0, MAX_OUTPUT2),
@@ -1112,17 +1187,6 @@ function runCommand(cmd, args, cwd, timeout, signal) {
1112
1187
  }
1113
1188
  var MAX_BYTES2 = 131072;
1114
1189
  var TIMEOUT_MS2 = 2e4;
1115
- var PRIVATE_RANGES = [
1116
- /^10\./,
1117
- /^192\.168\./,
1118
- /^172\.(1[6-9]|2[0-9]|3[01])\./,
1119
- /^127\./,
1120
- /^0\./,
1121
- /^169\.254\./,
1122
- /^::1$/,
1123
- /^fc/i,
1124
- /^fe80:/i
1125
- ];
1126
1190
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
1127
1191
  async function fetchWithRedirectLimit(url, maxRedirects, signal) {
1128
1192
  const headers = {
@@ -1132,6 +1196,14 @@ async function fetchWithRedirectLimit(url, maxRedirects, signal) {
1132
1196
  let redirectCount = 0;
1133
1197
  let currentUrl = url;
1134
1198
  for (; ; ) {
1199
+ const parsed = new URL(currentUrl);
1200
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
1201
+ throw new Error(`fetch: redirect to unsupported protocol "${parsed.protocol}"`);
1202
+ }
1203
+ if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
1204
+ throw new Error("fetch: redirect to http:// blocked (HTTPS required by default)");
1205
+ }
1206
+ await assertNotPrivate(parsed.hostname);
1135
1207
  const res = await fetch(currentUrl, {
1136
1208
  redirect: "manual",
1137
1209
  signal,
@@ -1157,6 +1229,11 @@ var fetchTool = {
1157
1229
  usageHint: "HTTPS only by default. Localhost and RFC1918 ranges blocked unless WRONGSTACK_FETCH_ALLOW_PRIVATE=1. Max 5 redirects, 20s timeout, 128KB cap.",
1158
1230
  permission: "confirm",
1159
1231
  mutating: false,
1232
+ // Trust rules for fetch match on the literal URL — declare it explicitly
1233
+ // so a user can trust `https://api.example.com/*` without accidentally
1234
+ // matching that pattern on any other tool that happens to have a `url`
1235
+ // input field.
1236
+ subjectKey: "url",
1160
1237
  timeoutMs: TIMEOUT_MS2,
1161
1238
  maxOutputBytes: MAX_BYTES2,
1162
1239
  inputSchema: {
@@ -1244,35 +1321,118 @@ var fetchTool = {
1244
1321
  };
1245
1322
  async function assertNotPrivate(hostname) {
1246
1323
  if (ALLOW_PRIVATE) return;
1247
- if (PRIVATE_RANGES.some((r) => r.test(hostname))) {
1248
- throw new Error(`fetch: blocked private/loopback address "${hostname}"`);
1249
- }
1250
- if (hostname === "localhost" || hostname.endsWith(".localhost")) {
1324
+ const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
1325
+ if (host === "localhost" || host.endsWith(".localhost")) {
1251
1326
  throw new Error("fetch: blocked localhost target");
1252
1327
  }
1253
- try {
1254
- const records = await dns.lookup(hostname, { all: true });
1255
- for (const r of records) {
1256
- if (PRIVATE_RANGES.some((re) => re.test(r.address))) {
1257
- throw new Error(`fetch: resolved to private address ${r.address}`);
1328
+ const ipVersion = net.isIP(host);
1329
+ if (ipVersion === 4) {
1330
+ if (isPrivateIPv4(host)) {
1331
+ throw new Error(`fetch: blocked private/loopback address "${host}"`);
1332
+ }
1333
+ } else if (ipVersion === 6) {
1334
+ if (isPrivateIPv6(host)) {
1335
+ throw new Error(`fetch: blocked private/loopback address "${host}"`);
1336
+ }
1337
+ } else {
1338
+ try {
1339
+ const records = await dns.lookup(host, { all: true });
1340
+ for (const r of records) {
1341
+ const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
1342
+ if (bad) {
1343
+ throw new Error(`fetch: resolved to private address ${r.address}`);
1344
+ }
1258
1345
  }
1346
+ } catch (err) {
1347
+ if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
1259
1348
  }
1260
- } catch (err) {
1261
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
1262
1349
  }
1263
1350
  }
1351
+ function isPrivateIPv4(addr) {
1352
+ const parts = addr.split(".").map((p) => Number.parseInt(p, 10));
1353
+ if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) {
1354
+ return true;
1355
+ }
1356
+ const [a, b, c] = parts;
1357
+ if (a === 0) return true;
1358
+ if (a === 10) return true;
1359
+ if (a === 127) return true;
1360
+ if (a === 169 && b === 254) return true;
1361
+ if (a === 172 && b >= 16 && b <= 31) return true;
1362
+ if (a === 192 && b === 168) return true;
1363
+ if (a === 192 && b === 0 && c === 0) return true;
1364
+ if (a === 100 && b >= 64 && b <= 127) return true;
1365
+ if (a >= 224) return true;
1366
+ return false;
1367
+ }
1368
+ function isPrivateIPv6(addr) {
1369
+ const lower = addr.toLowerCase();
1370
+ if (lower === "::" || lower === "::1") return true;
1371
+ const groups = expandIPv6(lower);
1372
+ if (!groups) return true;
1373
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
1374
+ const a = (groups[6] ?? 0) >> 8;
1375
+ const b = (groups[6] ?? 0) & 255;
1376
+ const c = (groups[7] ?? 0) >> 8;
1377
+ const d = (groups[7] ?? 0) & 255;
1378
+ return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
1379
+ }
1380
+ const high = groups[0] ?? 0;
1381
+ if ((high & 65024) === 64512) return true;
1382
+ if ((high & 65472) === 65152) return true;
1383
+ if ((high & 65280) === 65280) return true;
1384
+ return false;
1385
+ }
1386
+ function expandIPv6(addr) {
1387
+ const parts = addr.split("::");
1388
+ if (parts.length > 2) return null;
1389
+ const parseGroups = (s) => {
1390
+ if (s === "") return [];
1391
+ const out = [];
1392
+ for (const g of s.split(":")) {
1393
+ if (g.length === 0 || g.length > 4) return null;
1394
+ const n = Number.parseInt(g, 16);
1395
+ if (Number.isNaN(n) || n < 0 || n > 65535) return null;
1396
+ out.push(n);
1397
+ }
1398
+ return out;
1399
+ };
1400
+ if (parts.length === 1) {
1401
+ const groups = parseGroups(parts[0] ?? "");
1402
+ if (!groups || groups.length !== 8) return null;
1403
+ return groups;
1404
+ }
1405
+ const head = parseGroups(parts[0] ?? "");
1406
+ const tail = parseGroups(parts[1] ?? "");
1407
+ if (!head || !tail) return null;
1408
+ const fill = 8 - head.length - tail.length;
1409
+ if (fill < 0) return null;
1410
+ return [...head, ...new Array(fill).fill(0), ...tail];
1411
+ }
1264
1412
  function combineSignals(...sigs) {
1265
1413
  if (typeof AbortSignal.any === "function") {
1266
1414
  return AbortSignal.any(sigs);
1267
1415
  }
1268
1416
  const ctrl = new AbortController();
1417
+ const cleanups = [];
1418
+ const detach = () => {
1419
+ for (const fn of cleanups) fn();
1420
+ cleanups.length = 0;
1421
+ };
1269
1422
  for (const s of sigs) {
1270
1423
  if (s.aborted) {
1424
+ detach();
1271
1425
  ctrl.abort(s.reason);
1272
- break;
1426
+ return ctrl.signal;
1273
1427
  }
1274
- s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
1428
+ const onAbort = () => {
1429
+ detach();
1430
+ ctrl.abort(s.reason);
1431
+ };
1432
+ s.addEventListener("abort", onAbort, { once: true });
1433
+ cleanups.push(() => s.removeEventListener("abort", onAbort));
1275
1434
  }
1435
+ ctrl.signal.addEventListener("abort", detach, { once: true });
1276
1436
  return ctrl.signal;
1277
1437
  }
1278
1438
  function prettyJson(s) {
@@ -1557,7 +1717,7 @@ var todoTool = {
1557
1717
  }
1558
1718
  }
1559
1719
  }
1560
- ctx.todos = items;
1720
+ ctx.state.replaceTodos(items);
1561
1721
  return {
1562
1722
  count: items.length,
1563
1723
  in_progress: items.filter((t) => t.status === "in_progress").length
@@ -1571,10 +1731,10 @@ var gitTool = {
1571
1731
  description: "Run git commands. Wraps common operations: status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset.",
1572
1732
  usageHint: "Prefer built-in subcommands over raw args. `command` is required. `message` for commits. `branch` for checkout/branch. `files` for status/diff. `format` for log.",
1573
1733
  permission: "confirm",
1574
- mutating: ["commit", "push", "pull", "checkout", "stash", "reset"].includes(
1575
- "commit"
1576
- // this is for type-check only; runtime check below
1577
- ),
1734
+ // Conservative: any of these may mutate. The non-mutating commands
1735
+ // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
1736
+ // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
1737
+ mutating: true,
1578
1738
  timeoutMs: TIMEOUT_MS4,
1579
1739
  inputSchema: {
1580
1740
  type: "object",
@@ -1596,7 +1756,6 @@ var gitTool = {
1596
1756
  ],
1597
1757
  description: "Git subcommand"
1598
1758
  },
1599
- args: { type: "string", description: "Raw args string (bypasses subcommand logic)" },
1600
1759
  files: {
1601
1760
  type: "string",
1602
1761
  description: 'File(s) for status/diff: single path, comma-separated list, or "**/*.ts" glob'
@@ -1615,12 +1774,12 @@ var gitTool = {
1615
1774
  },
1616
1775
  async execute(input, ctx, opts) {
1617
1776
  if (!input?.command) throw new Error("git: command is required");
1618
- const gitDir = findGitDir(ctx.cwd);
1777
+ const gitDir = findGitDir(ctx.cwd, ctx.projectRoot);
1619
1778
  if (!gitDir) {
1620
1779
  return {
1621
1780
  command: input.command,
1622
1781
  stdout: "",
1623
- stderr: "Not in a git repository",
1782
+ stderr: "Not in a git repository (within project root)",
1624
1783
  exitCode: 128,
1625
1784
  truncated: false
1626
1785
  };
@@ -1629,7 +1788,8 @@ var gitTool = {
1629
1788
  return await runGit(args, gitDir, opts.signal);
1630
1789
  }
1631
1790
  };
1632
- function findGitDir(cwd) {
1791
+ function findGitDir(cwd, projectRoot) {
1792
+ const root = projectRoot;
1633
1793
  let dir = cwd;
1634
1794
  for (let i = 0; i < 20; i++) {
1635
1795
  try {
@@ -1637,6 +1797,7 @@ function findGitDir(cwd) {
1637
1797
  if (stat9.isDirectory()) return dir;
1638
1798
  } catch {
1639
1799
  }
1800
+ if (dir === root) break;
1640
1801
  const parent = dirname(dir);
1641
1802
  if (parent === dir) break;
1642
1803
  dir = parent;
@@ -1644,7 +1805,6 @@ function findGitDir(cwd) {
1644
1805
  return null;
1645
1806
  }
1646
1807
  function buildArgs(input) {
1647
- if (input.args) return input.args.split(/\s+/).filter(Boolean);
1648
1808
  const limit = input.limit ?? 20;
1649
1809
  const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim()).filter(Boolean) : [];
1650
1810
  switch (input.command) {
@@ -1691,7 +1851,7 @@ function buildArgs(input) {
1691
1851
  }
1692
1852
  }
1693
1853
  function runGit(args, cwd, signal) {
1694
- return new Promise((resolve2) => {
1854
+ return new Promise((resolve4) => {
1695
1855
  let stdout = "";
1696
1856
  let stderr = "";
1697
1857
  const child = spawn("git", args, {
@@ -1710,7 +1870,7 @@ function runGit(args, cwd, signal) {
1710
1870
  }
1711
1871
  });
1712
1872
  child.on("error", (err) => {
1713
- resolve2({
1873
+ resolve4({
1714
1874
  command: args[0],
1715
1875
  stdout,
1716
1876
  stderr: err.message,
@@ -1719,7 +1879,7 @@ function runGit(args, cwd, signal) {
1719
1879
  });
1720
1880
  });
1721
1881
  child.on("close", (code) => {
1722
- resolve2({
1882
+ resolve4({
1723
1883
  command: args[0],
1724
1884
  stdout: stdout.slice(0, MAX_OUTPUT3),
1725
1885
  stderr: stderr.slice(0, MAX_OUTPUT3),
@@ -1749,51 +1909,90 @@ var patchTool = {
1749
1909
  async execute(input, ctx, opts) {
1750
1910
  if (!input?.patch) throw new Error("patch: patch content is required");
1751
1911
  const dir = input.directory ? safeResolve(input.directory, ctx) : ctx.cwd;
1752
- const strip = input.strip ?? 1;
1912
+ const strip = Math.max(1, input.strip ?? 1);
1753
1913
  const dryRun = input.dry_run ?? false;
1754
- const patchFile = path.join(dir, `.wstack_patch_${Date.now()}.diff`);
1755
- await fs4.writeFile(patchFile, input.patch, "utf8");
1756
- const args = [
1757
- "-p" + strip,
1758
- "--merge",
1759
- ...dryRun ? ["--dry-run"] : [],
1760
- "-i",
1761
- patchFile
1762
- ];
1763
- const result = await runPatch(args, dir, opts.signal);
1764
- await fs4.unlink(patchFile).catch(() => {
1765
- });
1766
- if (result.exitCode !== 0 && !dryRun) {
1914
+ const targets = extractDiffTargets(input.patch);
1915
+ for (const t of targets) {
1916
+ const stripped = stripPathComponents(t, strip);
1917
+ if (!stripped) continue;
1918
+ const candidate = path.resolve(dir, stripped);
1919
+ const rel = path.relative(ctx.projectRoot, candidate);
1920
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
1921
+ return {
1922
+ applied: 0,
1923
+ rejected: 1,
1924
+ files: [],
1925
+ dry_run: dryRun,
1926
+ message: `patch refused: target "${t}" resolves outside project root`
1927
+ };
1928
+ }
1929
+ }
1930
+ const tmpDir = await fs4.mkdtemp(path.join(dir, ".wstack_patch_"));
1931
+ try {
1932
+ await fs4.chmod(tmpDir, 448).catch(() => {
1933
+ });
1934
+ const patchFile = path.join(tmpDir, "in.diff");
1935
+ await fs4.writeFile(patchFile, input.patch, { mode: 384 });
1936
+ const args = [
1937
+ `-p${strip}`,
1938
+ "--merge",
1939
+ ...dryRun ? ["--dry-run"] : [],
1940
+ "-i",
1941
+ patchFile
1942
+ ];
1943
+ const result = await runPatch(args, dir, opts.signal);
1944
+ if (result.exitCode !== 0 && !dryRun) {
1945
+ return {
1946
+ applied: 0,
1947
+ rejected: 1,
1948
+ files: [],
1949
+ dry_run: dryRun,
1950
+ message: `patch failed: ${result.stderr || result.stdout}`
1951
+ };
1952
+ }
1953
+ const patched = extractPatchedFiles(result.stdout);
1767
1954
  return {
1768
- applied: 0,
1769
- rejected: 1,
1770
- files: [],
1955
+ applied: patched.length,
1956
+ rejected: 0,
1957
+ files: patched,
1771
1958
  dry_run: dryRun,
1772
- message: `patch failed: ${result.stderr || result.stdout}`
1959
+ message: result.stdout || "patch applied"
1773
1960
  };
1961
+ } finally {
1962
+ await fs4.rm(tmpDir, { recursive: true, force: true }).catch(() => {
1963
+ });
1774
1964
  }
1775
- return {
1776
- applied: result.stdout.includes("patching file") ? 1 : 0,
1777
- rejected: 0,
1778
- files: extractPatchedFiles(result.stdout),
1779
- dry_run: dryRun,
1780
- message: result.stdout || "patch applied"
1781
- };
1782
1965
  }
1783
1966
  };
1967
+ function extractDiffTargets(patch) {
1968
+ const out = [];
1969
+ const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
1970
+ for (const m of patch.matchAll(re)) {
1971
+ const target = m[1]?.trim();
1972
+ if (!target || target === "/dev/null") continue;
1973
+ out.push(target);
1974
+ }
1975
+ return out;
1976
+ }
1977
+ function stripPathComponents(p, strip) {
1978
+ const parts = p.replace(/\\/g, "/").split("/");
1979
+ if (parts.length <= strip) return void 0;
1980
+ return parts.slice(strip).join("/");
1981
+ }
1784
1982
  function runPatch(args, cwd, signal) {
1785
- return new Promise((resolve2) => {
1983
+ return new Promise((resolve4) => {
1786
1984
  let stdout = "";
1787
1985
  let stderr = "";
1788
- const child = spawn("patch", args, { cwd, signal, stdio: ["pipe", "pipe", "pipe"] });
1986
+ const env = { ...process.env, LANG: "C", LC_ALL: "C" };
1987
+ const child = spawn("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"] });
1789
1988
  child.stdout?.on("data", (c) => {
1790
1989
  stdout += c.toString();
1791
1990
  });
1792
1991
  child.stderr?.on("data", (c) => {
1793
1992
  stderr += c.toString();
1794
1993
  });
1795
- child.on("close", (code) => resolve2({ exitCode: code ?? 1, stdout, stderr }));
1796
- child.on("error", (e) => resolve2({ exitCode: 1, stdout: "", stderr: e.message }));
1994
+ child.on("close", (code) => resolve4({ exitCode: code ?? 1, stdout, stderr }));
1995
+ child.on("error", (e) => resolve4({ exitCode: 1, stdout: "", stderr: e.message }));
1797
1996
  });
1798
1997
  }
1799
1998
  function extractPatchedFiles(output) {
@@ -1872,8 +2071,8 @@ var jsonTool = {
1872
2071
  };
1873
2072
  }
1874
2073
  };
1875
- function query(data, path10) {
1876
- const parts = path10.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
2074
+ function query(data, path12) {
2075
+ const parts = path12.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
1877
2076
  let current = data;
1878
2077
  for (const part of parts) {
1879
2078
  if (current === null || current === void 0) return void 0;
@@ -1979,18 +2178,18 @@ function findGitDir2(cwd) {
1979
2178
  let dir = cwd;
1980
2179
  for (let i = 0; i < 20; i++) {
1981
2180
  try {
1982
- const stat9 = __require("fs").statSync(`${dir}/.git`);
2181
+ const stat9 = statSync(path.join(dir, ".git"));
1983
2182
  if (stat9.isDirectory()) return dir;
1984
2183
  } catch {
1985
2184
  }
1986
- const parent = __require("path").dirname(dir);
2185
+ const parent = path.dirname(dir);
1987
2186
  if (parent === dir) break;
1988
2187
  dir = parent;
1989
2188
  }
1990
2189
  return null;
1991
2190
  }
1992
2191
  function runGit2(args, cwd, signal) {
1993
- return new Promise((resolve2) => {
2192
+ return new Promise((resolve4) => {
1994
2193
  let stdout = "";
1995
2194
  let stderr = "";
1996
2195
  const child = spawn("git", args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
@@ -2000,8 +2199,8 @@ function runGit2(args, cwd, signal) {
2000
2199
  child.stderr?.on("data", (c) => {
2001
2200
  stderr += c.toString();
2002
2201
  });
2003
- child.on("close", (code) => resolve2({ stdout, stderr, exitCode: code ?? 0 }));
2004
- child.on("error", (e) => resolve2({ stdout: "", stderr: e.message, exitCode: 1 }));
2202
+ child.on("close", (code) => resolve4({ stdout, stderr, exitCode: code ?? 0 }));
2203
+ child.on("error", (e) => resolve4({ stdout: "", stderr: e.message, exitCode: 1 }));
2005
2204
  });
2006
2205
  }
2007
2206
  async function fileDiff(input, ctx, signal) {
@@ -2124,10 +2323,15 @@ var treeTool = {
2124
2323
  if (queue.length > 0) {
2125
2324
  yield queue.shift();
2126
2325
  } else {
2127
- await Promise.race([
2128
- walkPromise,
2129
- new Promise((r) => setTimeout(r, 50))
2130
- ]).catch(() => void 0);
2326
+ let pollTimer;
2327
+ const poll = new Promise((r) => {
2328
+ pollTimer = setTimeout(r, 50);
2329
+ });
2330
+ try {
2331
+ await Promise.race([walkPromise, poll]).catch(() => void 0);
2332
+ } finally {
2333
+ if (pollTimer) clearTimeout(pollTimer);
2334
+ }
2131
2335
  }
2132
2336
  }
2133
2337
  await walkPromise;
@@ -2182,6 +2386,83 @@ async function walkDir(dir, depth, opts) {
2182
2386
  }
2183
2387
  }
2184
2388
  }
2389
+ async function* spawnStream(opts) {
2390
+ const max = opts.maxBytes ?? 2e5;
2391
+ const flushAt = opts.flushBytes ?? 4 * 1024;
2392
+ let stdout = "";
2393
+ let stderr = "";
2394
+ let pending = "";
2395
+ let error;
2396
+ const child = spawn(opts.cmd, opts.args, {
2397
+ cwd: opts.cwd,
2398
+ signal: opts.signal,
2399
+ stdio: ["ignore", "pipe", "pipe"]
2400
+ });
2401
+ const queue = [];
2402
+ let waiter;
2403
+ const wake = () => {
2404
+ if (waiter) {
2405
+ const w = waiter;
2406
+ waiter = void 0;
2407
+ w();
2408
+ }
2409
+ };
2410
+ child.stdout?.on("data", (c) => {
2411
+ const s = c.toString();
2412
+ if (stdout.length < max) stdout += s;
2413
+ queue.push({ kind: "out", data: s });
2414
+ wake();
2415
+ });
2416
+ child.stderr?.on("data", (c) => {
2417
+ const s = c.toString();
2418
+ if (stderr.length < max) stderr += s;
2419
+ queue.push({ kind: "err", data: s });
2420
+ wake();
2421
+ });
2422
+ child.on("error", (e) => {
2423
+ error = e.message;
2424
+ queue.push({ kind: "error", data: e.message });
2425
+ wake();
2426
+ });
2427
+ child.on("close", (code) => {
2428
+ queue.push({ kind: "close", data: "", code: code ?? 0 });
2429
+ wake();
2430
+ });
2431
+ let exitCode = 0;
2432
+ let spawnFailed = false;
2433
+ for (; ; ) {
2434
+ while (queue.length === 0) {
2435
+ await new Promise((resolve4) => {
2436
+ waiter = resolve4;
2437
+ });
2438
+ }
2439
+ const chunk = queue.shift();
2440
+ if (chunk.kind === "close") {
2441
+ if (!spawnFailed) exitCode = chunk.code ?? 0;
2442
+ break;
2443
+ }
2444
+ if (chunk.kind === "error") {
2445
+ spawnFailed = true;
2446
+ exitCode = 1;
2447
+ continue;
2448
+ }
2449
+ pending += chunk.data;
2450
+ if (pending.length >= flushAt) {
2451
+ yield { type: "partial_output", text: pending };
2452
+ pending = "";
2453
+ }
2454
+ }
2455
+ if (pending.length > 0) {
2456
+ yield { type: "partial_output", text: pending };
2457
+ }
2458
+ return {
2459
+ stdout,
2460
+ stderr,
2461
+ exitCode,
2462
+ truncated: stdout.length >= max || stderr.length >= max,
2463
+ error
2464
+ };
2465
+ }
2185
2466
 
2186
2467
  // src/lint.ts
2187
2468
  var lintTool = {
@@ -2836,7 +3117,7 @@ async function detectManager2(cwd) {
2836
3117
  return "npm";
2837
3118
  }
2838
3119
  function runOutdated(manager, args, cwd, signal) {
2839
- return new Promise((resolve2) => {
3120
+ return new Promise((resolve4) => {
2840
3121
  let stdout = "";
2841
3122
  let stderr = "";
2842
3123
  const MAX = 1e5;
@@ -2849,9 +3130,9 @@ function runOutdated(manager, args, cwd, signal) {
2849
3130
  });
2850
3131
  child.on("close", (code) => {
2851
3132
  const result = parseOutdatedOutput(stdout, code ?? 0);
2852
- resolve2(result);
3133
+ resolve4(result);
2853
3134
  });
2854
- child.on("error", (e) => resolve2({
3135
+ child.on("error", (e) => resolve4({
2855
3136
  exit_code: 1,
2856
3137
  packages: [],
2857
3138
  total: 0,
@@ -2937,7 +3218,14 @@ var logsTool = {
2937
3218
  async execute(input, ctx, opts) {
2938
3219
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
2939
3220
  const lines = input.lines ?? 100;
2940
- const filterRe = input.filter ? new RegExp(input.filter, "i") : null;
3221
+ let filterRe = null;
3222
+ if (input.filter) {
3223
+ const compiled = compileUserRegex(input.filter, "i");
3224
+ if (!compiled.ok) {
3225
+ throw new Error(`logs: ${compiled.reason}`);
3226
+ }
3227
+ filterRe = compiled.regex;
3228
+ }
2941
3229
  if (input.service) {
2942
3230
  return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
2943
3231
  }
@@ -2957,7 +3245,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
2957
3245
  const args = ["logs"];
2958
3246
  if (lines > 0) args.push("--tail", String(lines));
2959
3247
  args.push("--timestamps", service);
2960
- return new Promise((resolve2) => {
3248
+ return new Promise((resolve4) => {
2961
3249
  let stdout = "";
2962
3250
  let stderr = "";
2963
3251
  const MAX = 2e5;
@@ -2971,7 +3259,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
2971
3259
  child.on("close", (code) => {
2972
3260
  const output = stdout + stderr;
2973
3261
  const entries = parseLogLines(output, filterRe);
2974
- resolve2({
3262
+ resolve4({
2975
3263
  source: `docker:${service}`,
2976
3264
  entries,
2977
3265
  total: entries.length,
@@ -2979,7 +3267,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
2979
3267
  stream_mode: false
2980
3268
  });
2981
3269
  });
2982
- child.on("error", (e) => resolve2({
3270
+ child.on("error", (e) => resolve4({
2983
3271
  source: `docker:${service}`,
2984
3272
  entries: [],
2985
3273
  total: 0,
@@ -2988,29 +3276,41 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
2988
3276
  }));
2989
3277
  });
2990
3278
  }
2991
- async function fileLogs(path10, lines, filterRe, stream) {
3279
+ var MAX_TAIL_LINES = 1e5;
3280
+ async function fileLogs(path12, lines, filterRe, stream) {
2992
3281
  const { createInterface } = await import('readline');
2993
3282
  const { createReadStream } = await import('fs');
2994
3283
  const entries = [];
2995
- const allLines = [];
3284
+ const effLines = lines > 0 ? Math.min(lines, MAX_TAIL_LINES) : MAX_TAIL_LINES;
3285
+ const window = new Array(effLines);
3286
+ let writeIdx = 0;
3287
+ let totalLines = 0;
2996
3288
  const rl = createInterface({
2997
- input: createReadStream(path10),
3289
+ input: createReadStream(path12),
2998
3290
  crlfDelay: Number.POSITIVE_INFINITY
2999
3291
  });
3000
3292
  for await (const line of rl) {
3001
3293
  if (filterRe && !filterRe.test(line)) continue;
3002
- allLines.push(line);
3294
+ window[writeIdx] = line;
3295
+ writeIdx = (writeIdx + 1) % effLines;
3296
+ totalLines++;
3297
+ }
3298
+ const ordered = [];
3299
+ const start = totalLines >= effLines ? writeIdx : 0;
3300
+ const count = Math.min(totalLines, effLines);
3301
+ for (let i = 0; i < count; i++) {
3302
+ const v = window[(start + i) % effLines];
3303
+ if (v !== void 0) ordered.push(v);
3003
3304
  }
3004
- const sliced = lines > 0 ? allLines.slice(-lines) : allLines;
3005
- for (const line of sliced) {
3305
+ for (const line of ordered) {
3006
3306
  const parsed = parseLine(line);
3007
3307
  if (parsed) entries.push(parsed);
3008
3308
  }
3009
3309
  return {
3010
- source: path10,
3310
+ source: path12,
3011
3311
  entries,
3012
3312
  total: entries.length,
3013
- truncated: allLines.length > lines && lines > 0,
3313
+ truncated: totalLines > effLines,
3014
3314
  stream_mode: stream
3015
3315
  };
3016
3316
  }
@@ -3287,7 +3587,7 @@ var scaffoldTool = {
3287
3587
  const vars = { name, ...input.vars };
3288
3588
  const builtIn = BUILT_IN_TEMPLATES[input.template];
3289
3589
  if (builtIn) {
3290
- return handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
3590
+ return await handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
3291
3591
  }
3292
3592
  return {
3293
3593
  template: input.template,
@@ -3299,15 +3599,15 @@ var scaffoldTool = {
3299
3599
  };
3300
3600
  }
3301
3601
  };
3302
- function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
3602
+ async function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
3303
3603
  const files = [];
3304
3604
  let filesCreated = 0;
3305
3605
  for (const [filePath, content] of Object.entries(templateFiles)) {
3306
3606
  const resolvedPath = substituteVars(filePath, name, vars);
3307
3607
  const fullPath = path.join(cwd, resolvedPath);
3308
3608
  if (!dryRun) {
3309
- fsSync.mkdirSync(path.dirname(fullPath), { recursive: true });
3310
- fsSync.writeFileSync(fullPath, substituteVars(content, name, vars), "utf8");
3609
+ await fs4.mkdir(path.dirname(fullPath), { recursive: true });
3610
+ await fs4.writeFile(fullPath, substituteVars(content, name, vars), "utf8");
3311
3611
  }
3312
3612
  files.push(resolvedPath);
3313
3613
  filesCreated++;
@@ -3447,14 +3747,6 @@ var toolUseTool = {
3447
3747
  executionMs: Date.now() - start
3448
3748
  };
3449
3749
  }
3450
- if (tool.permission === "confirm" && input.input) {
3451
- return {
3452
- tool: input.tool,
3453
- success: false,
3454
- error: `tool_use: tool "${input.tool}" requires confirmation`,
3455
- executionMs: Date.now() - start
3456
- };
3457
- }
3458
3750
  try {
3459
3751
  const result = await tool.execute(input.input, ctx, opts);
3460
3752
  return {