reasonix 0.27.0 → 0.27.2

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
@@ -4163,6 +4163,7 @@ function applyMemoryStack(basePrompt, rootDir) {
4163
4163
  // src/tools/filesystem.ts
4164
4164
  import { promises as fs } from "fs";
4165
4165
  import * as pathMod from "path";
4166
+ import picomatch2 from "picomatch";
4166
4167
 
4167
4168
  // src/index/config.ts
4168
4169
  import picomatch from "picomatch";
@@ -4257,6 +4258,20 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
4257
4258
  var AUTO_PREVIEW_TAIL_LINES = 40;
4258
4259
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
4259
4260
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
4261
+ function displayRel(rootDir, full) {
4262
+ return pathMod.relative(rootDir, full).replaceAll("\\", "/");
4263
+ }
4264
+ var GLOB_METACHARS = /[*?{[]/;
4265
+ function compileNameFilter(filter) {
4266
+ if (!filter) return null;
4267
+ if (!GLOB_METACHARS.test(filter)) {
4268
+ const needle = filter.toLowerCase();
4269
+ return (name) => name.toLowerCase().includes(needle);
4270
+ }
4271
+ const matchPath = filter.includes("/");
4272
+ const isMatch = picomatch2(filter, { dot: true, nocase: true });
4273
+ return matchPath ? (_n, rel) => isMatch(rel) : (name) => isMatch(name);
4274
+ }
4260
4275
  function isLikelyBinaryByName(name) {
4261
4276
  const dot = name.lastIndexOf(".");
4262
4277
  if (dot < 0) return false;
@@ -4461,7 +4476,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4461
4476
  });
4462
4477
  registry.register({
4463
4478
  name: "search_files",
4464
- description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line.",
4479
+ description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) by default.",
4465
4480
  readOnly: true,
4466
4481
  parameters: {
4467
4482
  type: "object",
@@ -4470,6 +4485,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4470
4485
  pattern: {
4471
4486
  type: "string",
4472
4487
  description: "Substring (or regex) to match against filenames."
4488
+ },
4489
+ include_deps: {
4490
+ type: "boolean",
4491
+ description: "When true, also walk node_modules / .git / dist / build / etc. Off by default \u2014 most filename searches are about the user's own code."
4473
4492
  }
4474
4493
  },
4475
4494
  required: ["pattern"]
@@ -4477,6 +4496,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4477
4496
  fn: async (args) => {
4478
4497
  const startAbs = safePath(args.path ?? ".");
4479
4498
  const needle = args.pattern.toLowerCase();
4499
+ const includeDeps = args.include_deps === true;
4480
4500
  let re = null;
4481
4501
  try {
4482
4502
  re = new RegExp(args.pattern, "i");
@@ -4497,7 +4517,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4497
4517
  const lower = e.name.toLowerCase();
4498
4518
  const hit = re ? re.test(e.name) : lower.includes(needle);
4499
4519
  if (hit) {
4500
- const rel = pathMod.relative(rootDir, full);
4520
+ const rel = displayRel(rootDir, full);
4501
4521
  if (totalBytes + rel.length + 1 > maxListBytes) {
4502
4522
  matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
4503
4523
  return;
@@ -4505,7 +4525,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4505
4525
  matches.push(rel);
4506
4526
  totalBytes += rel.length + 1;
4507
4527
  }
4508
- if (e.isDirectory()) await walk2(full);
4528
+ if (e.isDirectory()) {
4529
+ if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
4530
+ await walk2(full);
4531
+ }
4509
4532
  }
4510
4533
  };
4511
4534
  await walk2(startAbs);
@@ -4529,7 +4552,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4529
4552
  },
4530
4553
  glob: {
4531
4554
  type: "string",
4532
- description: "Optional file-name suffix or substring filter. Examples: '.ts' (only TypeScript), 'test' (any file with 'test' in the name). Reduces noise when you know the file shape."
4555
+ description: "Optional filename filter. Real glob when the value contains `*`, `?`, `{`, or `[` \u2014 e.g. '*.ts', '**/*.tsx', 'src/**/*.{ts,tsx}'. Plain substring otherwise \u2014 e.g. '.ts' (suffix), 'test' (anywhere in the name). Patterns containing `/` match against the path relative to the search root; otherwise just the basename."
4533
4556
  },
4534
4557
  case_sensitive: {
4535
4558
  type: "boolean",
@@ -4546,7 +4569,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4546
4569
  const startAbs = safePath(args.path ?? ".");
4547
4570
  const caseSensitive = args.case_sensitive === true;
4548
4571
  const includeDeps = args.include_deps === true;
4549
- const nameFilter = typeof args.glob === "string" ? args.glob.toLowerCase() : null;
4572
+ const nameMatch = compileNameFilter(typeof args.glob === "string" ? args.glob : null);
4550
4573
  let re = null;
4551
4574
  try {
4552
4575
  re = new RegExp(args.pattern, caseSensitive ? "" : "i");
@@ -4574,9 +4597,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4574
4597
  continue;
4575
4598
  }
4576
4599
  if (!e.isFile()) continue;
4577
- if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
4578
- if (isLikelyBinaryByName(e.name)) continue;
4579
4600
  const full = pathMod.join(dir, e.name);
4601
+ if (nameMatch && !nameMatch(e.name, displayRel(rootDir, full))) continue;
4602
+ if (isLikelyBinaryByName(e.name)) continue;
4580
4603
  let stat2;
4581
4604
  try {
4582
4605
  stat2 = await fs.stat(full);
@@ -4593,7 +4616,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4593
4616
  const firstNul = raw.indexOf(0);
4594
4617
  if (firstNul !== -1 && firstNul < 8 * 1024) continue;
4595
4618
  const text = raw.toString("utf8");
4596
- const rel = pathMod.relative(rootDir, full);
4619
+ const rel = displayRel(rootDir, full);
4597
4620
  const lines = text.split(/\r?\n/);
4598
4621
  for (let li = 0; li < lines.length; li++) {
4599
4622
  const line = lines[li];
@@ -4658,7 +4681,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4658
4681
  const abs = safePath(args.path);
4659
4682
  await fs.mkdir(pathMod.dirname(abs), { recursive: true });
4660
4683
  await fs.writeFile(abs, args.content, "utf8");
4661
- return `wrote ${args.content.length} chars to ${pathMod.relative(rootDir, abs)}`;
4684
+ return `wrote ${args.content.length} chars to ${displayRel(rootDir, abs)}`;
4662
4685
  }
4663
4686
  });
4664
4687
  registry.register({
@@ -4684,17 +4707,17 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4684
4707
  const adaptedReplace = args.replace.replace(/\r?\n/g, le);
4685
4708
  const firstIdx = before.indexOf(adaptedSearch);
4686
4709
  if (firstIdx < 0) {
4687
- throw new Error(`edit_file: search text not found in ${pathMod.relative(rootDir, abs)}`);
4710
+ throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
4688
4711
  }
4689
4712
  const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
4690
4713
  if (nextIdx >= 0) {
4691
4714
  throw new Error(
4692
- `edit_file: search text appears multiple times in ${pathMod.relative(rootDir, abs)} \u2014 include more context to disambiguate`
4715
+ `edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
4693
4716
  );
4694
4717
  }
4695
4718
  const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
4696
4719
  await fs.writeFile(abs, after, "utf8");
4697
- const rel = pathMod.relative(rootDir, abs);
4720
+ const rel = displayRel(rootDir, abs);
4698
4721
  const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
4699
4722
  const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
4700
4723
  const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
@@ -4713,7 +4736,7 @@ ${diff}`;
4713
4736
  fn: async (args) => {
4714
4737
  const abs = safePath(args.path);
4715
4738
  await fs.mkdir(abs, { recursive: true });
4716
- return `created ${pathMod.relative(rootDir, abs)}/`;
4739
+ return `created ${displayRel(rootDir, abs)}/`;
4717
4740
  }
4718
4741
  });
4719
4742
  registry.register({
@@ -4732,7 +4755,7 @@ ${diff}`;
4732
4755
  const dst = safePath(args.destination);
4733
4756
  await fs.mkdir(pathMod.dirname(dst), { recursive: true });
4734
4757
  await fs.rename(src, dst);
4735
- return `moved ${pathMod.relative(rootDir, src)} \u2192 ${pathMod.relative(rootDir, dst)}`;
4758
+ return `moved ${displayRel(rootDir, src)} \u2192 ${displayRel(rootDir, dst)}`;
4736
4759
  }
4737
4760
  });
4738
4761
  return registry;
@@ -5866,7 +5889,7 @@ function splitOnChainOps(cmd) {
5866
5889
  const ch = cmd[i];
5867
5890
  if (quote) {
5868
5891
  if (ch === quote) quote = null;
5869
- else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) i++;
5892
+ else if (quote === '"' && isDqEscape(ch, cmd[i + 1])) i++;
5870
5893
  i++;
5871
5894
  atTokenStart = false;
5872
5895
  continue;
@@ -5938,7 +5961,7 @@ function parseSegment(segStr) {
5938
5961
  if (quote) {
5939
5962
  if (ch === quote) {
5940
5963
  quote = null;
5941
- } else if (ch === "\\" && quote === '"' && i + 1 < segStr.length) {
5964
+ } else if (quote === '"' && isDqEscape(ch, segStr[i + 1])) {
5942
5965
  cur += segStr[++i] ?? "";
5943
5966
  curHasContent = true;
5944
5967
  } else {
@@ -6044,6 +6067,14 @@ function parseCommandChain(cmd) {
6044
6067
  }
6045
6068
  segments.push(parseSegment(trimmed));
6046
6069
  }
6070
+ for (const seg of segments) {
6071
+ const cmdName = seg.argv[0] ?? "";
6072
+ if (cmdName.toLowerCase() === "cd") {
6073
+ throw new UnsupportedSyntaxError(
6074
+ "cd in parsed command chains does not change cwd for later segments. Use a command-native cwd flag instead, such as `npm --prefix <dir> run <script>`, `git -C <dir> ...`, or `cargo -C <dir> ...`."
6075
+ );
6076
+ }
6077
+ }
6047
6078
  if (ops.length === 0 && segments[0].redirects.length === 0) return null;
6048
6079
  return { segments, ops };
6049
6080
  }
@@ -6341,6 +6372,9 @@ var BUILTIN_ALLOWLIST = [
6341
6372
  "ruff",
6342
6373
  "mypy"
6343
6374
  ];
6375
+ function isDqEscape(prev, next) {
6376
+ return prev === "\\" && (next === '"' || next === "\\");
6377
+ }
6344
6378
  function tokenizeCommand(cmd) {
6345
6379
  const out = [];
6346
6380
  let cur = "";
@@ -6350,7 +6384,7 @@ function tokenizeCommand(cmd) {
6350
6384
  if (quote) {
6351
6385
  if (ch === quote) {
6352
6386
  quote = null;
6353
- } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
6387
+ } else if (quote === '"' && isDqEscape(ch, cmd[i + 1])) {
6354
6388
  cur += cmd[++i];
6355
6389
  } else {
6356
6390
  cur += ch;
@@ -6392,7 +6426,7 @@ function detectShellOperator(cmd) {
6392
6426
  if (quote) {
6393
6427
  if (ch === quote) {
6394
6428
  quote = null;
6395
- } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
6429
+ } else if (quote === '"' && isDqEscape(ch, cmd[i + 1])) {
6396
6430
  cur += cmd[++i];
6397
6431
  curQuoted = true;
6398
6432
  } else {
@@ -6708,7 +6742,7 @@ function registerShellTools(registry, opts) {
6708
6742
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
6709
6743
  registry.register({
6710
6744
  name: "run_command",
6711
- 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.",
6745
+ 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. `cd` also does not persist within parsed chains like `cd dir && command`. Use a command-native cwd flag instead: `npm --prefix <dir> run <script>`, `npm --prefix <dir> exec -- <bin>`, `git -C <dir> ...`, `cargo -C <dir> ...`, `pytest <dir>/tests`.\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.",
6712
6746
  // Plan-mode gate: allow allowlisted commands through (git status,
6713
6747
  // cargo check, ls, grep …) so the model can actually investigate
6714
6748
  // during planning. Anything that would otherwise trigger a