reasonix 0.25.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -47,8 +47,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
47
47
  }
48
48
  function sleep(ms, signal) {
49
49
  if (ms <= 0) return Promise.resolve();
50
- return new Promise((resolve9, reject) => {
51
- const timer = setTimeout(resolve9, ms);
50
+ return new Promise((resolve10, reject) => {
51
+ const timer = setTimeout(resolve10, ms);
52
52
  if (signal) {
53
53
  const onAbort = () => {
54
54
  clearTimeout(timer);
@@ -523,7 +523,7 @@ function matchesTool(hook, toolName) {
523
523
  }
524
524
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
525
525
  function defaultSpawner(input) {
526
- return new Promise((resolve9) => {
526
+ return new Promise((resolve10) => {
527
527
  const child = spawn(input.command, {
528
528
  cwd: input.cwd,
529
529
  shell: true,
@@ -568,7 +568,7 @@ function defaultSpawner(input) {
568
568
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
569
569
  child.once("error", (err) => {
570
570
  clearTimeout(timer);
571
- resolve9({
571
+ resolve10({
572
572
  exitCode: null,
573
573
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
574
574
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -579,7 +579,7 @@ function defaultSpawner(input) {
579
579
  });
580
580
  child.once("close", (code) => {
581
581
  clearTimeout(timer);
582
- resolve9({
582
+ resolve10({
583
583
  exitCode: code,
584
584
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
585
585
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -2310,8 +2310,8 @@ var CacheFirstLoop = class {
2310
2310
  }
2311
2311
  );
2312
2312
  for (let k = 0; k < budget; k++) {
2313
- const sample = queue.shift() ?? await new Promise((resolve9) => {
2314
- waiter = resolve9;
2313
+ const sample = queue.shift() ?? await new Promise((resolve10) => {
2314
+ waiter = resolve10;
2315
2315
  });
2316
2316
  yield {
2317
2317
  turn: this._turn,
@@ -5367,9 +5367,9 @@ function forkRegistryExcluding(parent, exclude) {
5367
5367
  }
5368
5368
 
5369
5369
  // src/tools/shell.ts
5370
- import { spawn as spawn3, spawnSync } from "child_process";
5370
+ import { spawn as spawn4, spawnSync } from "child_process";
5371
5371
  import { existsSync as existsSync8, statSync as statSync4 } from "fs";
5372
- import * as pathMod3 from "path";
5372
+ import * as pathMod4 from "path";
5373
5373
 
5374
5374
  // src/tools/jobs.ts
5375
5375
  import { spawn as spawn2 } from "child_process";
@@ -5659,6 +5659,417 @@ function snapshot(job) {
5659
5659
  };
5660
5660
  }
5661
5661
 
5662
+ // src/tools/shell-chain.ts
5663
+ import { spawn as spawn3 } from "child_process";
5664
+ import { closeSync, openSync } from "fs";
5665
+ import * as pathMod3 from "path";
5666
+ var UnsupportedSyntaxError = class extends Error {
5667
+ constructor(detail) {
5668
+ super(`run_command: ${detail}`);
5669
+ this.name = "UnsupportedSyntaxError";
5670
+ }
5671
+ };
5672
+ function splitOnChainOps(cmd) {
5673
+ const segs = [];
5674
+ const ops = [];
5675
+ let segStart = 0;
5676
+ let i = 0;
5677
+ let quote = null;
5678
+ let atTokenStart = true;
5679
+ while (i < cmd.length) {
5680
+ const ch = cmd[i];
5681
+ if (quote) {
5682
+ if (ch === quote) quote = null;
5683
+ else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) i++;
5684
+ i++;
5685
+ atTokenStart = false;
5686
+ continue;
5687
+ }
5688
+ if (ch === '"' || ch === "'") {
5689
+ quote = ch;
5690
+ i++;
5691
+ atTokenStart = false;
5692
+ continue;
5693
+ }
5694
+ if (ch === " " || ch === " ") {
5695
+ i++;
5696
+ atTokenStart = true;
5697
+ continue;
5698
+ }
5699
+ if (atTokenStart) {
5700
+ let op = null;
5701
+ let opLen = 0;
5702
+ const next = cmd[i + 1];
5703
+ if (ch === "|" && next === "|") {
5704
+ op = "||";
5705
+ opLen = 2;
5706
+ } else if (ch === "&" && next === "&") {
5707
+ op = "&&";
5708
+ opLen = 2;
5709
+ } else if (ch === "|") {
5710
+ op = "|";
5711
+ opLen = 1;
5712
+ } else if (ch === ";") {
5713
+ op = ";";
5714
+ opLen = 1;
5715
+ }
5716
+ if (op !== null) {
5717
+ segs.push(cmd.slice(segStart, i));
5718
+ ops.push(op);
5719
+ i += opLen;
5720
+ segStart = i;
5721
+ atTokenStart = true;
5722
+ continue;
5723
+ }
5724
+ }
5725
+ i++;
5726
+ atTokenStart = false;
5727
+ }
5728
+ segs.push(cmd.slice(segStart));
5729
+ return { segs, ops };
5730
+ }
5731
+ function parseSegment(segStr) {
5732
+ const argv = [];
5733
+ const redirects = [];
5734
+ let cur = "";
5735
+ let curHasContent = false;
5736
+ let pending = null;
5737
+ let quote = null;
5738
+ const flush = () => {
5739
+ if (!curHasContent && cur.length === 0) return;
5740
+ if (pending) {
5741
+ redirects.push({ kind: pending, target: cur });
5742
+ pending = null;
5743
+ } else {
5744
+ argv.push(cur);
5745
+ }
5746
+ cur = "";
5747
+ curHasContent = false;
5748
+ };
5749
+ let i = 0;
5750
+ while (i < segStr.length) {
5751
+ const ch = segStr[i];
5752
+ if (quote) {
5753
+ if (ch === quote) {
5754
+ quote = null;
5755
+ } else if (ch === "\\" && quote === '"' && i + 1 < segStr.length) {
5756
+ cur += segStr[++i] ?? "";
5757
+ curHasContent = true;
5758
+ } else {
5759
+ cur += ch;
5760
+ curHasContent = true;
5761
+ }
5762
+ i++;
5763
+ continue;
5764
+ }
5765
+ if (ch === '"' || ch === "'") {
5766
+ quote = ch;
5767
+ curHasContent = true;
5768
+ i++;
5769
+ continue;
5770
+ }
5771
+ if (ch === " " || ch === " ") {
5772
+ flush();
5773
+ i++;
5774
+ continue;
5775
+ }
5776
+ if (cur.length === 0 && !curHasContent) {
5777
+ const remaining = segStr.slice(i);
5778
+ let matched = null;
5779
+ if (remaining.startsWith("2>&1")) matched = { op: "2>&1", len: 4 };
5780
+ else if (remaining.startsWith("&>")) matched = { op: "&>", len: 2 };
5781
+ else if (remaining.startsWith("2>>")) matched = { op: "2>>", len: 3 };
5782
+ else if (remaining.startsWith("2>")) matched = { op: "2>", len: 2 };
5783
+ else if (remaining.startsWith(">>")) matched = { op: ">>", len: 2 };
5784
+ else if (remaining.startsWith(">")) matched = { op: ">", len: 1 };
5785
+ else if (remaining.startsWith("<<")) {
5786
+ throw new UnsupportedSyntaxError(
5787
+ `shell operator "<<" is not supported \u2014 heredoc / here-string is not implemented; pass input via a "<" file or the binary's --input flag`
5788
+ );
5789
+ } else if (remaining.startsWith("<")) matched = { op: "<", len: 1 };
5790
+ if (matched) {
5791
+ if (pending !== null) {
5792
+ throw new UnsupportedSyntaxError(
5793
+ `redirect "${pending}" is missing a target file before "${matched.op}"`
5794
+ );
5795
+ }
5796
+ if (matched.op === "2>&1") {
5797
+ redirects.push({ kind: "2>&1", target: "" });
5798
+ } else {
5799
+ pending = matched.op;
5800
+ }
5801
+ i += matched.len;
5802
+ continue;
5803
+ }
5804
+ if (ch === "&") {
5805
+ throw new UnsupportedSyntaxError(
5806
+ 'shell operator "&" is not supported \u2014 background runs need run_background, not run_command. Wrap a literal `&` arg in quotes.'
5807
+ );
5808
+ }
5809
+ }
5810
+ cur += ch;
5811
+ curHasContent = true;
5812
+ i++;
5813
+ }
5814
+ if (quote) throw new Error(`unclosed ${quote} in command`);
5815
+ flush();
5816
+ if (pending) throw new UnsupportedSyntaxError(`redirect "${pending}" is missing a target file`);
5817
+ if (argv.length === 0 && redirects.length > 0) {
5818
+ throw new UnsupportedSyntaxError(
5819
+ "redirect without a command \u2014 segment must have at least one program argument"
5820
+ );
5821
+ }
5822
+ validateRedirectFds(redirects);
5823
+ return { argv, redirects };
5824
+ }
5825
+ function validateRedirectFds(redirects) {
5826
+ let stdin = 0;
5827
+ let stdout = 0;
5828
+ let stderr = 0;
5829
+ for (const r of redirects) {
5830
+ if (r.kind === "<") stdin++;
5831
+ else if (r.kind === ">" || r.kind === ">>") stdout++;
5832
+ else if (r.kind === "2>" || r.kind === "2>>" || r.kind === "2>&1") stderr++;
5833
+ else if (r.kind === "&>") {
5834
+ stdout++;
5835
+ stderr++;
5836
+ }
5837
+ }
5838
+ if (stdin > 1) throw new UnsupportedSyntaxError("multiple `<` stdin redirects in one segment");
5839
+ if (stdout > 1)
5840
+ throw new UnsupportedSyntaxError(
5841
+ "multiple stdout redirects in one segment (`>` / `>>` / `&>` conflict)"
5842
+ );
5843
+ if (stderr > 1)
5844
+ throw new UnsupportedSyntaxError(
5845
+ "multiple stderr redirects in one segment (`2>` / `2>>` / `&>` / `2>&1` conflict)"
5846
+ );
5847
+ }
5848
+ function parseCommandChain(cmd) {
5849
+ const { segs, ops } = splitOnChainOps(cmd);
5850
+ const segments = [];
5851
+ for (let i = 0; i < segs.length; i++) {
5852
+ const trimmed = segs[i].trim();
5853
+ if (trimmed.length === 0) {
5854
+ const op = i === 0 ? ops[0] : ops[i - 1];
5855
+ throw new UnsupportedSyntaxError(
5856
+ i === 0 ? `empty segment before "${op}"` : i === segs.length - 1 ? `chain ends with "${op}"` : `empty segment between "${ops[i - 1]}" and "${ops[i]}"`
5857
+ );
5858
+ }
5859
+ segments.push(parseSegment(trimmed));
5860
+ }
5861
+ if (ops.length === 0 && segments[0].redirects.length === 0) return null;
5862
+ return { segments, ops };
5863
+ }
5864
+ function chainAllowed(chain, isAllowed2) {
5865
+ for (const seg of chain.segments) {
5866
+ if (!isAllowed2(seg.argv.join(" "))) return false;
5867
+ }
5868
+ return true;
5869
+ }
5870
+ function groupChain(chain) {
5871
+ const groups = [{ segments: [chain.segments[0]], opBefore: null }];
5872
+ for (let i = 0; i < chain.ops.length; i++) {
5873
+ const op = chain.ops[i];
5874
+ const next = chain.segments[i + 1];
5875
+ if (op === "|") {
5876
+ groups[groups.length - 1].segments.push(next);
5877
+ } else {
5878
+ groups.push({ segments: [next], opBefore: op });
5879
+ }
5880
+ }
5881
+ return groups;
5882
+ }
5883
+ async function runChain(chain, opts) {
5884
+ const groups = groupChain(chain);
5885
+ const buf = new OutputBuffer(opts.maxOutputChars * 2 * 4);
5886
+ const deadline = Date.now() + opts.timeoutSec * 1e3;
5887
+ let lastExit = 0;
5888
+ let timedOut = false;
5889
+ for (const group of groups) {
5890
+ if (group.opBefore === "&&" && lastExit !== 0) continue;
5891
+ if (group.opBefore === "||" && lastExit === 0) continue;
5892
+ const remainingMs = deadline - Date.now();
5893
+ if (remainingMs <= 0) {
5894
+ timedOut = true;
5895
+ break;
5896
+ }
5897
+ const result = await runPipeGroup(group.segments, {
5898
+ cwd: opts.cwd,
5899
+ timeoutMs: remainingMs,
5900
+ buf,
5901
+ signal: opts.signal
5902
+ });
5903
+ lastExit = result.exitCode;
5904
+ if (result.timedOut) {
5905
+ timedOut = true;
5906
+ break;
5907
+ }
5908
+ if (opts.signal?.aborted) break;
5909
+ }
5910
+ const output = buf.toString();
5911
+ const truncated = output.length > opts.maxOutputChars ? `${output.slice(0, opts.maxOutputChars)}
5912
+
5913
+ [\u2026 truncated ${output.length - opts.maxOutputChars} chars \u2026]` : output;
5914
+ return { exitCode: lastExit, output: truncated, timedOut };
5915
+ }
5916
+ function openRedirects(redirects, cwd) {
5917
+ let stdinFd = null;
5918
+ let stdoutFd = null;
5919
+ let stderrFd = null;
5920
+ let mergeStderrToStdout = false;
5921
+ let bothFd = null;
5922
+ const toClose = [];
5923
+ const open = (target, flags) => {
5924
+ const resolved = pathMod3.resolve(cwd, target);
5925
+ const fd = openSync(resolved, flags);
5926
+ toClose.push(fd);
5927
+ return fd;
5928
+ };
5929
+ for (const r of redirects) {
5930
+ if (r.kind === "<") stdinFd = open(r.target, "r");
5931
+ else if (r.kind === ">") stdoutFd = open(r.target, "w");
5932
+ else if (r.kind === ">>") stdoutFd = open(r.target, "a");
5933
+ else if (r.kind === "2>") stderrFd = open(r.target, "w");
5934
+ else if (r.kind === "2>>") stderrFd = open(r.target, "a");
5935
+ else if (r.kind === "&>") {
5936
+ bothFd = open(r.target, "w");
5937
+ stdoutFd = bothFd;
5938
+ stderrFd = bothFd;
5939
+ } else if (r.kind === "2>&1") {
5940
+ mergeStderrToStdout = true;
5941
+ }
5942
+ }
5943
+ return { stdinFd, stdoutFd, stderrFd, mergeStderrToStdout, toClose };
5944
+ }
5945
+ async function runPipeGroup(segments, opts) {
5946
+ const env = { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
5947
+ const children = [];
5948
+ const allFds = [];
5949
+ let timedOut = false;
5950
+ const killAll = () => {
5951
+ for (const c of children) killProcessTree2(c);
5952
+ };
5953
+ const killTimer = setTimeout(() => {
5954
+ timedOut = true;
5955
+ killAll();
5956
+ }, opts.timeoutMs);
5957
+ const onAbort = () => killAll();
5958
+ if (opts.signal?.aborted) {
5959
+ onAbort();
5960
+ } else {
5961
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
5962
+ }
5963
+ try {
5964
+ for (let i = 0; i < segments.length; i++) {
5965
+ const isFirst = i === 0;
5966
+ const isLast = i === segments.length - 1;
5967
+ const seg = segments[i];
5968
+ const io = openRedirects(seg.redirects, opts.cwd);
5969
+ allFds.push(...io.toClose);
5970
+ const { bin, args, spawnOverrides } = prepareSpawn(seg.argv);
5971
+ const stdoutSpec = io.stdoutFd !== null ? io.stdoutFd : "pipe";
5972
+ const stderrSpec = io.stderrFd !== null ? io.stderrFd : io.mergeStderrToStdout ? stdoutSpec : "pipe";
5973
+ const stdinSpec = io.stdinFd !== null ? io.stdinFd : isFirst ? "ignore" : "pipe";
5974
+ const spawnOpts = {
5975
+ cwd: opts.cwd,
5976
+ shell: false,
5977
+ windowsHide: true,
5978
+ env,
5979
+ stdio: [stdinSpec, stdoutSpec, stderrSpec],
5980
+ ...spawnOverrides
5981
+ };
5982
+ let child;
5983
+ try {
5984
+ child = spawn3(bin, args, spawnOpts);
5985
+ } catch (err) {
5986
+ for (const fd of allFds) tryClose(fd);
5987
+ killAll();
5988
+ clearTimeout(killTimer);
5989
+ opts.signal?.removeEventListener("abort", onAbort);
5990
+ throw err;
5991
+ }
5992
+ children.push(child);
5993
+ if (!isFirst && io.stdinFd === null) {
5994
+ const prev = children[i - 1];
5995
+ prev.stdout?.on("error", () => {
5996
+ });
5997
+ child.stdin?.on("error", () => {
5998
+ });
5999
+ const prevMergesStderr = segments[i - 1].redirects.some((r) => r.kind === "2>&1") && !!prev.stderr;
6000
+ if (prevMergesStderr && prev.stderr) {
6001
+ prev.stderr.on("error", () => {
6002
+ });
6003
+ let openSources = 2;
6004
+ const closeIfDone = () => {
6005
+ if (--openSources === 0) child.stdin?.end();
6006
+ };
6007
+ prev.stdout?.pipe(child.stdin, { end: false });
6008
+ prev.stderr.pipe(child.stdin, { end: false });
6009
+ prev.stdout?.once("end", closeIfDone);
6010
+ prev.stderr.once("end", closeIfDone);
6011
+ } else {
6012
+ prev.stdout?.pipe(child.stdin);
6013
+ }
6014
+ }
6015
+ if (child.stderr && io.stderrFd === null && !(io.mergeStderrToStdout && !isLast)) {
6016
+ child.stderr.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
6017
+ }
6018
+ if (isLast && child.stdout && io.stdoutFd === null) {
6019
+ child.stdout.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
6020
+ if (io.mergeStderrToStdout && child.stderr && io.stderrFd === null) {
6021
+ child.stderr.removeAllListeners("data");
6022
+ child.stderr.on("data", (chunk) => opts.buf.push(toBuf(chunk)));
6023
+ }
6024
+ }
6025
+ }
6026
+ const exits = await Promise.all(
6027
+ children.map(
6028
+ (c) => new Promise((resolve10) => {
6029
+ c.once("error", () => resolve10(null));
6030
+ c.once("close", (code) => resolve10(code));
6031
+ })
6032
+ )
6033
+ );
6034
+ return { exitCode: exits[exits.length - 1] ?? null, timedOut };
6035
+ } finally {
6036
+ for (const fd of allFds) tryClose(fd);
6037
+ clearTimeout(killTimer);
6038
+ opts.signal?.removeEventListener("abort", onAbort);
6039
+ }
6040
+ }
6041
+ function tryClose(fd) {
6042
+ try {
6043
+ closeSync(fd);
6044
+ } catch {
6045
+ }
6046
+ }
6047
+ function toBuf(chunk) {
6048
+ return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
6049
+ }
6050
+ var OutputBuffer = class {
6051
+ constructor(cap) {
6052
+ this.cap = cap;
6053
+ }
6054
+ cap;
6055
+ chunks = [];
6056
+ bytes = 0;
6057
+ push(b) {
6058
+ if (this.bytes >= this.cap) return;
6059
+ const remaining = this.cap - this.bytes;
6060
+ if (b.length > remaining) {
6061
+ this.chunks.push(b.subarray(0, remaining));
6062
+ this.bytes = this.cap;
6063
+ } else {
6064
+ this.chunks.push(b);
6065
+ this.bytes += b.length;
6066
+ }
6067
+ }
6068
+ toString() {
6069
+ return smartDecodeOutput(Buffer.concat(this.chunks));
6070
+ }
6071
+ };
6072
+
5662
6073
  // src/tools/shell.ts
5663
6074
  function killProcessTree2(child) {
5664
6075
  if (!child.pid || child.killed) return;
@@ -5830,17 +6241,31 @@ function isAllowed(cmd, extra = []) {
5830
6241
  }
5831
6242
  return false;
5832
6243
  }
6244
+ function isCommandAllowed(cmd, extra = []) {
6245
+ let chain;
6246
+ try {
6247
+ chain = parseCommandChain(cmd);
6248
+ } catch {
6249
+ return false;
6250
+ }
6251
+ if (chain === null) return isAllowed(cmd, extra);
6252
+ return chainAllowed(chain, (seg) => isAllowed(seg, extra));
6253
+ }
5833
6254
  async function runCommand(cmd, opts) {
6255
+ const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
6256
+ const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
5834
6257
  const argv = tokenizeCommand(cmd);
5835
6258
  if (argv.length === 0) throw new Error("run_command: empty command");
5836
- const operator = detectShellOperator(cmd);
5837
- if (operator !== null) {
5838
- throw new Error(
5839
- `run_command: shell operator "${operator}" is not supported \u2014 this tool spawns one process, no shell expansion. Split into separate run_command calls and combine the output in your reasoning (e.g. instead of \`grep foo *.ts | wc -l\`, call \`grep -c foo *.ts\` or two separate commands). To pass "${operator}" as a literal argument, wrap it in quotes.`
5840
- );
6259
+ const chain = parseCommandChain(cmd);
6260
+ if (chain !== null) {
6261
+ return await runChain(chain, {
6262
+ cwd: opts.cwd,
6263
+ timeoutSec,
6264
+ maxOutputChars: maxChars,
6265
+ signal: opts.signal
6266
+ });
5841
6267
  }
5842
- const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
5843
- const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
6268
+ const timeoutMs = timeoutSec * 1e3;
5844
6269
  const spawnOpts = {
5845
6270
  cwd: opts.cwd,
5846
6271
  shell: false,
@@ -5858,10 +6283,10 @@ async function runCommand(cmd, opts) {
5858
6283
  };
5859
6284
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5860
6285
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5861
- return await new Promise((resolve9, reject) => {
6286
+ return await new Promise((resolve10, reject) => {
5862
6287
  let child;
5863
6288
  try {
5864
- child = spawn3(bin, args, effectiveSpawnOpts);
6289
+ child = spawn4(bin, args, effectiveSpawnOpts);
5865
6290
  } catch (err) {
5866
6291
  reject(err);
5867
6292
  return;
@@ -5912,7 +6337,7 @@ async function runCommand(cmd, opts) {
5912
6337
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5913
6338
 
5914
6339
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5915
- resolve9({ exitCode: code, output, timedOut });
6340
+ resolve10({ exitCode: code, output, timedOut });
5916
6341
  });
5917
6342
  });
5918
6343
  }
@@ -5934,16 +6359,16 @@ function resolveExecutable(cmd, opts = {}) {
5934
6359
  const platform = opts.platform ?? process.platform;
5935
6360
  if (platform !== "win32") return cmd;
5936
6361
  if (!cmd) return cmd;
5937
- if (cmd.includes("/") || cmd.includes("\\") || pathMod3.isAbsolute(cmd)) return cmd;
5938
- if (pathMod3.extname(cmd)) return cmd;
6362
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod4.isAbsolute(cmd)) return cmd;
6363
+ if (pathMod4.extname(cmd)) return cmd;
5939
6364
  const env = opts.env ?? process.env;
5940
6365
  const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
5941
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod3.delimiter);
6366
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod4.delimiter);
5942
6367
  const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
5943
6368
  const isFile = opts.isFile ?? defaultIsFile;
5944
6369
  for (const dir of pathDirs) {
5945
6370
  for (const ext of pathExt) {
5946
- const full = pathMod3.win32.join(dir, cmd + ext);
6371
+ const full = pathMod4.win32.join(dir, cmd + ext);
5947
6372
  if (isFile(full)) return full;
5948
6373
  }
5949
6374
  }
@@ -6013,8 +6438,8 @@ function withUtf8Codepage(cmdline) {
6013
6438
  function isBareWindowsName(s) {
6014
6439
  if (!s) return false;
6015
6440
  if (s.includes("/") || s.includes("\\")) return false;
6016
- if (pathMod3.isAbsolute(s)) return false;
6017
- if (pathMod3.extname(s)) return false;
6441
+ if (pathMod4.isAbsolute(s)) return false;
6442
+ if (pathMod4.extname(s)) return false;
6018
6443
  return true;
6019
6444
  }
6020
6445
  function quoteForCmdExe(arg) {
@@ -6033,7 +6458,7 @@ var NeedsConfirmationError = class extends Error {
6033
6458
  }
6034
6459
  };
6035
6460
  function registerShellTools(registry, opts) {
6036
- const rootDir = pathMod3.resolve(opts.rootDir);
6461
+ const rootDir = pathMod4.resolve(opts.rootDir);
6037
6462
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
6038
6463
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
6039
6464
  const jobs = opts.jobs ?? new JobRegistry();
@@ -6044,7 +6469,7 @@ function registerShellTools(registry, opts) {
6044
6469
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
6045
6470
  registry.register({
6046
6471
  name: "run_command",
6047
- description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 ONE process per call, NO shell expansion. `&&`, `||`, `|`, `;`, `>`, `<`, `2>&1` are all rejected up-front \u2014 split into separate calls and combine results in reasoning. Example: instead of `grep foo *.ts | wc -l`, use `grep -c foo *.ts`; instead of `cd sub && npm test`, use `npm test --prefix sub` (or whatever --cwd flag the binary accepts).\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
6472
+ description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 Chain operators `|`, `||`, `&&`, `;` ARE supported \u2014 parsed natively, no shell invoked, so semantics are identical on Windows / macOS / Linux. Each chain segment is allowlist-checked individually: `git status | grep main` runs if both halves are allowed.\n\u2022 File redirects ARE supported: `>` truncate, `>>` append, `<` stdin from file, `2>` / `2>>` stderr to file, `2>&1` merge stderr\u2192stdout, `&>` both to file. Targets resolve relative to the project root. At most one redirect per fd per segment.\n\u2022 Background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, and process substitution `<(\u2026)` are NOT supported. Wrap a literal `&` arg in quotes; for input use a `<` file or the binary's own --input flag.\n\u2022 Env-var expansion `$VAR` is NOT performed \u2014 `$VAR` is passed as a literal string. Use the binary's own --env flag or substitute the value yourself.\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Glob patterns (`*.ts`) are passed through as literal arguments \u2014 no shell expansion. Use `grep -r`, `rg`, `find -name`, etc.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
6048
6473
  // Plan-mode gate: allow allowlisted commands through (git status,
6049
6474
  // cargo check, ls, grep …) so the model can actually investigate
6050
6475
  // during planning. Anything that would otherwise trigger a
@@ -6053,14 +6478,14 @@ function registerShellTools(registry, opts) {
6053
6478
  if (isAllowAll()) return true;
6054
6479
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
6055
6480
  if (!cmd) return false;
6056
- return isAllowed(cmd, getExtraAllowed());
6481
+ return isCommandAllowed(cmd, getExtraAllowed());
6057
6482
  },
6058
6483
  parameters: {
6059
6484
  type: "object",
6060
6485
  properties: {
6061
6486
  command: {
6062
6487
  type: "string",
6063
- description: 'Full command line. Tokenized with POSIX-ish quoting; no shell expansion. Pipes (`|`), redirects (`>`, `<`, `2>`), and `&&`/`||` chaining are rejected with an error \u2014 split into separate calls instead. To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
6488
+ description: 'Full command line. POSIX-ish quoting. Chain operators `|`, `||`, `&&`, `;` and file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` work natively (no shell). Background `&`, heredoc `<<`, env-var expansion `$VAR`, and command substitution `$(\u2026)` are rejected (or passed through as literal in the case of `$VAR`). To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
6064
6489
  },
6065
6490
  timeoutSec: {
6066
6491
  type: "integer",
@@ -6072,7 +6497,7 @@ function registerShellTools(registry, opts) {
6072
6497
  fn: async (args, ctx) => {
6073
6498
  const cmd = args.command.trim();
6074
6499
  if (!cmd) throw new Error("run_command: empty command");
6075
- if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
6500
+ if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed())) {
6076
6501
  throw new NeedsConfirmationError(cmd);
6077
6502
  }
6078
6503
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
@@ -6436,11 +6861,11 @@ ${i + 1}. ${r.title}`);
6436
6861
 
6437
6862
  // src/env.ts
6438
6863
  import { readFileSync as readFileSync9 } from "fs";
6439
- import { resolve as resolve7 } from "path";
6864
+ import { resolve as resolve8 } from "path";
6440
6865
  function loadDotenv(path2 = ".env") {
6441
6866
  let raw;
6442
6867
  try {
6443
- raw = readFileSync9(resolve7(process.cwd(), path2), "utf8");
6868
+ raw = readFileSync9(resolve8(process.cwd(), path2), "utf8");
6444
6869
  } catch {
6445
6870
  return;
6446
6871
  }
@@ -7194,7 +7619,7 @@ var McpClient = class {
7194
7619
  const id = this.nextId++;
7195
7620
  const frame = { jsonrpc: "2.0", id, method, params };
7196
7621
  let abortHandler = null;
7197
- const promise = new Promise((resolve9, reject) => {
7622
+ const promise = new Promise((resolve10, reject) => {
7198
7623
  const timeout = setTimeout(() => {
7199
7624
  this.pending.delete(id);
7200
7625
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -7203,7 +7628,7 @@ var McpClient = class {
7203
7628
  );
7204
7629
  }, this.requestTimeoutMs);
7205
7630
  this.pending.set(id, {
7206
- resolve: resolve9,
7631
+ resolve: resolve10,
7207
7632
  reject,
7208
7633
  timeout
7209
7634
  });
@@ -7285,7 +7710,7 @@ var McpClient = class {
7285
7710
  };
7286
7711
 
7287
7712
  // src/mcp/stdio.ts
7288
- import { spawn as spawn4 } from "child_process";
7713
+ import { spawn as spawn5 } from "child_process";
7289
7714
  var StdioTransport = class {
7290
7715
  child;
7291
7716
  queue = [];
@@ -7300,14 +7725,14 @@ var StdioTransport = class {
7300
7725
  opts.command,
7301
7726
  ...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
7302
7727
  ].join(" ");
7303
- this.child = spawn4(line, [], {
7728
+ this.child = spawn5(line, [], {
7304
7729
  env,
7305
7730
  cwd: opts.cwd,
7306
7731
  stdio: ["pipe", "pipe", "inherit"],
7307
7732
  shell: true
7308
7733
  });
7309
7734
  } else {
7310
- this.child = spawn4(opts.command, opts.args ?? [], {
7735
+ this.child = spawn5(opts.command, opts.args ?? [], {
7311
7736
  env,
7312
7737
  cwd: opts.cwd,
7313
7738
  stdio: ["pipe", "pipe", "inherit"]
@@ -7326,12 +7751,12 @@ var StdioTransport = class {
7326
7751
  }
7327
7752
  async send(message) {
7328
7753
  if (this.closed) throw new Error("MCP transport is closed");
7329
- return new Promise((resolve9, reject) => {
7754
+ return new Promise((resolve10, reject) => {
7330
7755
  const line = `${JSON.stringify(message)}
7331
7756
  `;
7332
7757
  this.child.stdin.write(line, "utf8", (err) => {
7333
7758
  if (err) reject(err);
7334
- else resolve9();
7759
+ else resolve10();
7335
7760
  });
7336
7761
  });
7337
7762
  }
@@ -7342,8 +7767,8 @@ var StdioTransport = class {
7342
7767
  continue;
7343
7768
  }
7344
7769
  if (this.closed) return;
7345
- const next = await new Promise((resolve9) => {
7346
- this.waiters.push(resolve9);
7770
+ const next = await new Promise((resolve10) => {
7771
+ this.waiters.push(resolve10);
7347
7772
  });
7348
7773
  if (next === null) return;
7349
7774
  yield next;
@@ -7412,8 +7837,8 @@ var SseTransport = class {
7412
7837
  constructor(opts) {
7413
7838
  this.url = opts.url;
7414
7839
  this.headers = opts.headers ?? {};
7415
- this.endpointReady = new Promise((resolve9, reject) => {
7416
- this.resolveEndpoint = resolve9;
7840
+ this.endpointReady = new Promise((resolve10, reject) => {
7841
+ this.resolveEndpoint = resolve10;
7417
7842
  this.rejectEndpoint = reject;
7418
7843
  });
7419
7844
  this.endpointReady.catch(() => void 0);
@@ -7440,8 +7865,8 @@ var SseTransport = class {
7440
7865
  continue;
7441
7866
  }
7442
7867
  if (this.closed) return;
7443
- const next = await new Promise((resolve9) => {
7444
- this.waiters.push(resolve9);
7868
+ const next = await new Promise((resolve10) => {
7869
+ this.waiters.push(resolve10);
7445
7870
  });
7446
7871
  if (next === null) return;
7447
7872
  yield next;
@@ -7627,8 +8052,8 @@ var StreamableHttpTransport = class {
7627
8052
  continue;
7628
8053
  }
7629
8054
  if (this.closed) return;
7630
- const next = await new Promise((resolve9) => {
7631
- this.waiters.push(resolve9);
8055
+ const next = await new Promise((resolve10) => {
8056
+ this.waiters.push(resolve10);
7632
8057
  });
7633
8058
  if (next === null) return;
7634
8059
  yield next;
@@ -7799,7 +8224,7 @@ async function trySection(load) {
7799
8224
 
7800
8225
  // src/code/edit-blocks.ts
7801
8226
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "fs";
7802
- import { dirname as dirname5, resolve as resolve8 } from "path";
8227
+ import { dirname as dirname5, resolve as resolve9 } from "path";
7803
8228
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
7804
8229
  function parseEditBlocks(text) {
7805
8230
  const out = [];
@@ -7817,8 +8242,8 @@ function parseEditBlocks(text) {
7817
8242
  return out;
7818
8243
  }
7819
8244
  function applyEditBlock(block, rootDir) {
7820
- const absRoot = resolve8(rootDir);
7821
- const absTarget = resolve8(absRoot, block.path);
8245
+ const absRoot = resolve9(rootDir);
8246
+ const absTarget = resolve9(absRoot, block.path);
7822
8247
  if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
7823
8248
  return {
7824
8249
  path: block.path,
@@ -7871,13 +8296,13 @@ function applyEditBlocks(blocks, rootDir) {
7871
8296
  return blocks.map((b) => applyEditBlock(b, rootDir));
7872
8297
  }
7873
8298
  function snapshotBeforeEdits(blocks, rootDir) {
7874
- const absRoot = resolve8(rootDir);
8299
+ const absRoot = resolve9(rootDir);
7875
8300
  const seen = /* @__PURE__ */ new Set();
7876
8301
  const snapshots = [];
7877
8302
  for (const b of blocks) {
7878
8303
  if (seen.has(b.path)) continue;
7879
8304
  seen.add(b.path);
7880
- const abs = resolve8(absRoot, b.path);
8305
+ const abs = resolve9(absRoot, b.path);
7881
8306
  if (!existsSync10(abs)) {
7882
8307
  snapshots.push({ path: b.path, prevContent: null });
7883
8308
  continue;
@@ -7891,9 +8316,9 @@ function snapshotBeforeEdits(blocks, rootDir) {
7891
8316
  return snapshots;
7892
8317
  }
7893
8318
  function restoreSnapshots(snapshots, rootDir) {
7894
- const absRoot = resolve8(rootDir);
8319
+ const absRoot = resolve9(rootDir);
7895
8320
  return snapshots.map((snap) => {
7896
- const abs = resolve8(absRoot, snap.path);
8321
+ const abs = resolve9(absRoot, snap.path);
7897
8322
  if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
7898
8323
  return {
7899
8324
  path: snap.path,