@wrongstack/tools 0.1.4 → 0.1.7

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