reasonix 0.27.1 → 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/cli/index.js CHANGED
@@ -5282,6 +5282,7 @@ function escapeAttr(s) {
5282
5282
  // src/tools/filesystem.ts
5283
5283
  import { promises as fs } from "fs";
5284
5284
  import * as pathMod from "path";
5285
+ import picomatch2 from "picomatch";
5285
5286
  var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
5286
5287
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
5287
5288
  var DEFAULT_AUTO_PREVIEW_LINES = 200;
@@ -5289,6 +5290,20 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
5289
5290
  var AUTO_PREVIEW_TAIL_LINES = 40;
5290
5291
  var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
5291
5292
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
5293
+ function displayRel(rootDir, full) {
5294
+ return pathMod.relative(rootDir, full).replaceAll("\\", "/");
5295
+ }
5296
+ var GLOB_METACHARS = /[*?{[]/;
5297
+ function compileNameFilter(filter) {
5298
+ if (!filter) return null;
5299
+ if (!GLOB_METACHARS.test(filter)) {
5300
+ const needle = filter.toLowerCase();
5301
+ return (name) => name.toLowerCase().includes(needle);
5302
+ }
5303
+ const matchPath = filter.includes("/");
5304
+ const isMatch = picomatch2(filter, { dot: true, nocase: true });
5305
+ return matchPath ? (_n, rel) => isMatch(rel) : (name) => isMatch(name);
5306
+ }
5292
5307
  function isLikelyBinaryByName(name) {
5293
5308
  const dot2 = name.lastIndexOf(".");
5294
5309
  if (dot2 < 0) return false;
@@ -5493,7 +5508,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5493
5508
  });
5494
5509
  registry.register({
5495
5510
  name: "search_files",
5496
- 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.",
5511
+ 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.",
5497
5512
  readOnly: true,
5498
5513
  parameters: {
5499
5514
  type: "object",
@@ -5502,6 +5517,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5502
5517
  pattern: {
5503
5518
  type: "string",
5504
5519
  description: "Substring (or regex) to match against filenames."
5520
+ },
5521
+ include_deps: {
5522
+ type: "boolean",
5523
+ 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."
5505
5524
  }
5506
5525
  },
5507
5526
  required: ["pattern"]
@@ -5509,6 +5528,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5509
5528
  fn: async (args) => {
5510
5529
  const startAbs = safePath(args.path ?? ".");
5511
5530
  const needle = args.pattern.toLowerCase();
5531
+ const includeDeps = args.include_deps === true;
5512
5532
  let re = null;
5513
5533
  try {
5514
5534
  re = new RegExp(args.pattern, "i");
@@ -5529,7 +5549,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5529
5549
  const lower = e.name.toLowerCase();
5530
5550
  const hit = re ? re.test(e.name) : lower.includes(needle);
5531
5551
  if (hit) {
5532
- const rel = pathMod.relative(rootDir, full);
5552
+ const rel = displayRel(rootDir, full);
5533
5553
  if (totalBytes + rel.length + 1 > maxListBytes) {
5534
5554
  matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
5535
5555
  return;
@@ -5537,7 +5557,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5537
5557
  matches.push(rel);
5538
5558
  totalBytes += rel.length + 1;
5539
5559
  }
5540
- if (e.isDirectory()) await walk4(full);
5560
+ if (e.isDirectory()) {
5561
+ if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
5562
+ await walk4(full);
5563
+ }
5541
5564
  }
5542
5565
  };
5543
5566
  await walk4(startAbs);
@@ -5561,7 +5584,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5561
5584
  },
5562
5585
  glob: {
5563
5586
  type: "string",
5564
- 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."
5587
+ 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."
5565
5588
  },
5566
5589
  case_sensitive: {
5567
5590
  type: "boolean",
@@ -5578,7 +5601,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5578
5601
  const startAbs = safePath(args.path ?? ".");
5579
5602
  const caseSensitive = args.case_sensitive === true;
5580
5603
  const includeDeps = args.include_deps === true;
5581
- const nameFilter = typeof args.glob === "string" ? args.glob.toLowerCase() : null;
5604
+ const nameMatch = compileNameFilter(typeof args.glob === "string" ? args.glob : null);
5582
5605
  let re = null;
5583
5606
  try {
5584
5607
  re = new RegExp(args.pattern, caseSensitive ? "" : "i");
@@ -5606,9 +5629,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5606
5629
  continue;
5607
5630
  }
5608
5631
  if (!e.isFile()) continue;
5609
- if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
5610
- if (isLikelyBinaryByName(e.name)) continue;
5611
5632
  const full = pathMod.join(dir, e.name);
5633
+ if (nameMatch && !nameMatch(e.name, displayRel(rootDir, full))) continue;
5634
+ if (isLikelyBinaryByName(e.name)) continue;
5612
5635
  let stat2;
5613
5636
  try {
5614
5637
  stat2 = await fs.stat(full);
@@ -5625,7 +5648,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5625
5648
  const firstNul = raw.indexOf(0);
5626
5649
  if (firstNul !== -1 && firstNul < 8 * 1024) continue;
5627
5650
  const text = raw.toString("utf8");
5628
- const rel = pathMod.relative(rootDir, full);
5651
+ const rel = displayRel(rootDir, full);
5629
5652
  const lines = text.split(/\r?\n/);
5630
5653
  for (let li = 0; li < lines.length; li++) {
5631
5654
  const line = lines[li];
@@ -5690,7 +5713,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5690
5713
  const abs = safePath(args.path);
5691
5714
  await fs.mkdir(pathMod.dirname(abs), { recursive: true });
5692
5715
  await fs.writeFile(abs, args.content, "utf8");
5693
- return `wrote ${args.content.length} chars to ${pathMod.relative(rootDir, abs)}`;
5716
+ return `wrote ${args.content.length} chars to ${displayRel(rootDir, abs)}`;
5694
5717
  }
5695
5718
  });
5696
5719
  registry.register({
@@ -5716,17 +5739,17 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5716
5739
  const adaptedReplace = args.replace.replace(/\r?\n/g, le);
5717
5740
  const firstIdx = before.indexOf(adaptedSearch);
5718
5741
  if (firstIdx < 0) {
5719
- throw new Error(`edit_file: search text not found in ${pathMod.relative(rootDir, abs)}`);
5742
+ throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
5720
5743
  }
5721
5744
  const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
5722
5745
  if (nextIdx >= 0) {
5723
5746
  throw new Error(
5724
- `edit_file: search text appears multiple times in ${pathMod.relative(rootDir, abs)} \u2014 include more context to disambiguate`
5747
+ `edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
5725
5748
  );
5726
5749
  }
5727
5750
  const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
5728
5751
  await fs.writeFile(abs, after, "utf8");
5729
- const rel = pathMod.relative(rootDir, abs);
5752
+ const rel = displayRel(rootDir, abs);
5730
5753
  const header2 = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
5731
5754
  const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
5732
5755
  const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
@@ -5745,7 +5768,7 @@ ${diff}`;
5745
5768
  fn: async (args) => {
5746
5769
  const abs = safePath(args.path);
5747
5770
  await fs.mkdir(abs, { recursive: true });
5748
- return `created ${pathMod.relative(rootDir, abs)}/`;
5771
+ return `created ${displayRel(rootDir, abs)}/`;
5749
5772
  }
5750
5773
  });
5751
5774
  registry.register({
@@ -5764,7 +5787,7 @@ ${diff}`;
5764
5787
  const dst = safePath(args.destination);
5765
5788
  await fs.mkdir(pathMod.dirname(dst), { recursive: true });
5766
5789
  await fs.rename(src, dst);
5767
- return `moved ${pathMod.relative(rootDir, src)} \u2192 ${pathMod.relative(rootDir, dst)}`;
5790
+ return `moved ${displayRel(rootDir, src)} \u2192 ${displayRel(rootDir, dst)}`;
5768
5791
  }
5769
5792
  });
5770
5793
  return registry;
@@ -6898,6 +6921,14 @@ function parseCommandChain(cmd) {
6898
6921
  }
6899
6922
  segments.push(parseSegment(trimmed));
6900
6923
  }
6924
+ for (const seg of segments) {
6925
+ const cmdName = seg.argv[0] ?? "";
6926
+ if (cmdName.toLowerCase() === "cd") {
6927
+ throw new UnsupportedSyntaxError(
6928
+ "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> ...`."
6929
+ );
6930
+ }
6931
+ }
6901
6932
  if (ops.length === 0 && segments[0].redirects.length === 0) return null;
6902
6933
  return { segments, ops };
6903
6934
  }
@@ -7555,7 +7586,7 @@ function registerShellTools(registry, opts) {
7555
7586
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
7556
7587
  registry.register({
7557
7588
  name: "run_command",
7558
- 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.",
7589
+ 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.",
7559
7590
  // Plan-mode gate: allow allowlisted commands through (git status,
7560
7591
  // cargo check, ls, grep …) so the model can actually investigate
7561
7592
  // during planning. Anything that would otherwise trigger a
@@ -15563,6 +15594,9 @@ async function handleSettings(method, _rest, body, ctx) {
15563
15594
  const fields = parseBody8(body);
15564
15595
  const cfg = readConfig(ctx.configPath);
15565
15596
  const changed = [];
15597
+ let langPending = null;
15598
+ let presetPendingLive = null;
15599
+ let effortPendingLive = null;
15566
15600
  if (fields.lang !== void 0) {
15567
15601
  const raw = String(fields.lang);
15568
15602
  const supported = getSupportedLanguages();
@@ -15570,14 +15604,15 @@ async function handleSettings(method, _rest, body, ctx) {
15570
15604
  if (!langCode) {
15571
15605
  return { status: 400, body: { error: `lang must be one of: ${supported.join(", ")}` } };
15572
15606
  }
15573
- setLanguage(langCode);
15607
+ cfg.lang = langCode;
15608
+ langPending = langCode;
15574
15609
  changed.push("lang");
15575
15610
  }
15576
15611
  if (fields.apiKey !== void 0) {
15577
15612
  if (typeof fields.apiKey !== "string" || !isPlausibleKey(fields.apiKey)) {
15578
15613
  return { status: 400, body: { error: "apiKey must be a plausible sk- token" } };
15579
15614
  }
15580
- saveApiKey(fields.apiKey, ctx.configPath);
15615
+ cfg.apiKey = fields.apiKey.trim();
15581
15616
  changed.push("apiKey");
15582
15617
  }
15583
15618
  if (fields.baseUrl !== void 0) {
@@ -15585,7 +15620,6 @@ async function handleSettings(method, _rest, body, ctx) {
15585
15620
  return { status: 400, body: { error: "baseUrl must be a non-empty string" } };
15586
15621
  }
15587
15622
  cfg.baseUrl = fields.baseUrl.trim();
15588
- writeConfig(cfg, ctx.configPath);
15589
15623
  changed.push("baseUrl");
15590
15624
  }
15591
15625
  if (fields.preset !== void 0) {
@@ -15593,16 +15627,15 @@ async function handleSettings(method, _rest, body, ctx) {
15593
15627
  return { status: 400, body: { error: "preset must be auto | flash | pro" } };
15594
15628
  }
15595
15629
  cfg.preset = fields.preset;
15596
- writeConfig(cfg, ctx.configPath);
15597
- ctx.applyPresetLive?.(fields.preset);
15630
+ presetPendingLive = fields.preset;
15598
15631
  changed.push("preset");
15599
15632
  }
15600
15633
  if (fields.reasoningEffort !== void 0) {
15601
15634
  if (typeof fields.reasoningEffort !== "string" || !VALID_EFFORTS.has(fields.reasoningEffort)) {
15602
15635
  return { status: 400, body: { error: "reasoningEffort must be high | max" } };
15603
15636
  }
15604
- saveReasoningEffort(fields.reasoningEffort, ctx.configPath);
15605
- ctx.applyEffortLive?.(fields.reasoningEffort);
15637
+ cfg.reasoningEffort = fields.reasoningEffort;
15638
+ effortPendingLive = fields.reasoningEffort;
15606
15639
  changed.push("reasoningEffort");
15607
15640
  }
15608
15641
  if (fields.search !== void 0) {
@@ -15610,10 +15643,13 @@ async function handleSettings(method, _rest, body, ctx) {
15610
15643
  return { status: 400, body: { error: "search must be a boolean" } };
15611
15644
  }
15612
15645
  cfg.search = fields.search;
15613
- writeConfig(cfg, ctx.configPath);
15614
15646
  changed.push("search");
15615
15647
  }
15616
15648
  if (changed.length > 0) {
15649
+ writeConfig(cfg, ctx.configPath);
15650
+ if (langPending) setLanguage(langPending);
15651
+ if (presetPendingLive) ctx.applyPresetLive?.(presetPendingLive);
15652
+ if (effortPendingLive) ctx.applyEffortLive?.(effortPendingLive);
15617
15653
  ctx.audit?.({ ts: Date.now(), action: "set-settings", payload: { fields: changed } });
15618
15654
  }
15619
15655
  return { status: 200, body: { changed } };