reasonix 0.48.1 → 0.49.0

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 (123) hide show
  1. package/dashboard/dist/app.js +123 -16
  2. package/dashboard/dist/app.js.map +1 -1
  3. package/dist/cli/{acp-QJGGHQLA.js → acp-WFQIC6SO.js} +18 -18
  4. package/dist/cli/chat-D32JGNVH.js +51 -0
  5. package/dist/cli/{chunk-TIJ4ZHD6.js → chunk-23ZPCIPR.js} +12 -9
  6. package/dist/cli/chunk-23ZPCIPR.js.map +1 -0
  7. package/dist/cli/{chunk-TKVXTQ3T.js → chunk-3ZZXQ3CZ.js} +27 -27
  8. package/dist/cli/chunk-3ZZXQ3CZ.js.map +1 -0
  9. package/dist/cli/{chunk-H2F4LDNH.js → chunk-7AST3QQ3.js} +2 -2
  10. package/dist/cli/{chunk-XMR2VCGT.js → chunk-7JTKBJ2G.js} +3 -3
  11. package/dist/cli/{chunk-X53B3JIX.js → chunk-7X4JJOO7.js} +2 -61
  12. package/dist/cli/{chunk-X53B3JIX.js.map → chunk-7X4JJOO7.js.map} +1 -1
  13. package/dist/cli/{chunk-WKOXKCF3.js → chunk-ASOLXV67.js} +3 -3
  14. package/dist/cli/{chunk-43ROGEX2.js → chunk-AWEULQG6.js} +10 -9
  15. package/dist/cli/{chunk-43ROGEX2.js.map → chunk-AWEULQG6.js.map} +1 -1
  16. package/dist/cli/{chunk-5AW6NIHU.js → chunk-DFX5ZH5L.js} +2 -2
  17. package/dist/cli/{chunk-4E2BHJU4.js → chunk-GNS7BAT2.js} +2 -2
  18. package/dist/cli/{chunk-5U5LMMFF.js → chunk-J2IHQGPQ.js} +12 -6
  19. package/dist/cli/chunk-J2IHQGPQ.js.map +1 -0
  20. package/dist/cli/{chunk-O3AGYTG2.js → chunk-JGTX4RRQ.js} +3 -3
  21. package/dist/cli/{chunk-SLAFMXAZ.js → chunk-JNTMOX7G.js} +2 -2
  22. package/dist/cli/{chunk-RCLS63KE.js → chunk-MGTBP7GG.js} +2 -2
  23. package/dist/cli/{chunk-JFBGSWQB.js → chunk-MQWO32ZD.js} +323 -159
  24. package/dist/cli/chunk-MQWO32ZD.js.map +1 -0
  25. package/dist/cli/{chunk-FD7SNDWW.js → chunk-O5LIHAMP.js} +8 -4
  26. package/dist/cli/chunk-O5LIHAMP.js.map +1 -0
  27. package/dist/cli/{chunk-PJIQIYTV.js → chunk-PB3MAFEI.js} +3 -3
  28. package/dist/cli/{chunk-NQZ5U37J.js → chunk-PEMG6CUB.js} +2 -2
  29. package/dist/cli/{chunk-KH5JIJJD.js → chunk-PXBQ6IZ7.js} +3 -3
  30. package/dist/cli/{chunk-J2TQAWOM.js → chunk-Q46B3Z7H.js} +25 -10
  31. package/dist/cli/{chunk-J2TQAWOM.js.map → chunk-Q46B3Z7H.js.map} +1 -1
  32. package/dist/cli/{chunk-DABAOQSV.js → chunk-QF32ROX2.js} +1260 -1754
  33. package/dist/cli/chunk-QF32ROX2.js.map +1 -0
  34. package/dist/cli/{chunk-IKSYVBBZ.js → chunk-QX5TWXRZ.js} +2 -2
  35. package/dist/cli/{chunk-R7U44O3Y.js → chunk-TAIKVL35.js} +2 -2
  36. package/dist/cli/{chunk-B5CZL2SE.js → chunk-TEDWJKEI.js} +4 -9
  37. package/dist/cli/chunk-TEDWJKEI.js.map +1 -0
  38. package/dist/cli/{chunk-EO6RPTJG.js → chunk-U5XQDCK7.js} +5 -5
  39. package/dist/cli/{chunk-SWUMD2LX.js → chunk-W46ZMNKO.js} +3 -3
  40. package/dist/cli/{chunk-FPME5QOO.js → chunk-WMTMMSXU.js} +166 -5
  41. package/dist/cli/chunk-WMTMMSXU.js.map +1 -0
  42. package/dist/cli/{chunk-PIC5HJRD.js → chunk-YEF7C4XI.js} +154 -86
  43. package/dist/cli/chunk-YEF7C4XI.js.map +1 -0
  44. package/dist/cli/{chunk-3FULTFRB.js → chunk-ZAEJWKXB.js} +2 -2
  45. package/dist/cli/chunk-ZWHSHFDP.js +6173 -0
  46. package/dist/cli/chunk-ZWHSHFDP.js.map +1 -0
  47. package/dist/cli/{code-OKA5YPOH.js → code-R4IHI7SR.js} +30 -30
  48. package/dist/cli/{commands-3U6PUVSS.js → commands-DRHFCYMO.js} +4 -4
  49. package/dist/cli/{commit-HFB7SRBU.js → commit-AG5KB4YP.js} +3 -3
  50. package/dist/cli/{desktop-G7UMW3CJ.js → desktop-JGL6GORA.js} +19 -19
  51. package/dist/cli/{diff-PGXW4YZD.js → diff-4Z7ETWZO.js} +9 -9
  52. package/dist/cli/{doctor-WWJFLVCB.js → doctor-VA3RHQLB.js} +9 -9
  53. package/dist/cli/index.js +37 -36
  54. package/dist/cli/index.js.map +1 -1
  55. package/dist/cli/{mcp-Y3VKIK3T.js → mcp-LZO4HXFA.js} +34 -23
  56. package/dist/cli/mcp-LZO4HXFA.js.map +1 -0
  57. package/dist/cli/{mcp-browse-4IN2QIFR.js → mcp-browse-C3GXVMYZ.js} +3 -3
  58. package/dist/cli/{mcp-inspect-BUXFXDHB.js → mcp-inspect-ZMYUNFDS.js} +2 -2
  59. package/dist/cli/{prompt-US57R6BA.js → prompt-MC3U5KRP.js} +5 -5
  60. package/dist/cli/{prune-sessions-SEWX7GP6.js → prune-sessions-OEPFH4N6.js} +11 -7
  61. package/dist/cli/prune-sessions-OEPFH4N6.js.map +1 -0
  62. package/dist/cli/{replay-EQJMZRB3.js → replay-4TP7ZUMZ.js} +10 -10
  63. package/dist/cli/{run-KVEI66TR.js → run-6MXQYBOE.js} +16 -15
  64. package/dist/cli/run-6MXQYBOE.js.map +1 -0
  65. package/dist/cli/{server-D6C4GHWN.js → server-Z3IMJNNI.js} +63 -12
  66. package/dist/cli/server-Z3IMJNNI.js.map +1 -0
  67. package/dist/cli/{sessions-TGVS2RQZ.js → sessions-NXQ5SAV7.js} +18 -18
  68. package/dist/cli/sessions-NXQ5SAV7.js.map +1 -0
  69. package/dist/cli/{setup-WLKX6GGG.js → setup-LHZELI6I.js} +6 -6
  70. package/dist/cli/{stats-TCD7Q6MB.js → stats-SUIJ3QWY.js} +6 -6
  71. package/dist/cli/{version-BCWWS2OU.js → version-BIFONEUB.js} +13 -13
  72. package/dist/index.d.ts +63 -15
  73. package/dist/index.js +855 -360
  74. package/dist/index.js.map +1 -1
  75. package/package.json +2 -1
  76. package/dist/cli/chat-ZXF227MP.js +0 -51
  77. package/dist/cli/chunk-5U5LMMFF.js.map +0 -1
  78. package/dist/cli/chunk-6FRNXWDZ.js +0 -2265
  79. package/dist/cli/chunk-6FRNXWDZ.js.map +0 -1
  80. package/dist/cli/chunk-B5CZL2SE.js.map +0 -1
  81. package/dist/cli/chunk-DABAOQSV.js.map +0 -1
  82. package/dist/cli/chunk-FD7SNDWW.js.map +0 -1
  83. package/dist/cli/chunk-FPME5QOO.js.map +0 -1
  84. package/dist/cli/chunk-JFBGSWQB.js.map +0 -1
  85. package/dist/cli/chunk-PIC5HJRD.js.map +0 -1
  86. package/dist/cli/chunk-TIJ4ZHD6.js.map +0 -1
  87. package/dist/cli/chunk-TKVXTQ3T.js.map +0 -1
  88. package/dist/cli/mcp-Y3VKIK3T.js.map +0 -1
  89. package/dist/cli/prune-sessions-SEWX7GP6.js.map +0 -1
  90. package/dist/cli/run-KVEI66TR.js.map +0 -1
  91. package/dist/cli/server-D6C4GHWN.js.map +0 -1
  92. package/dist/cli/sessions-TGVS2RQZ.js.map +0 -1
  93. /package/dist/cli/{acp-QJGGHQLA.js.map → acp-WFQIC6SO.js.map} +0 -0
  94. /package/dist/cli/{chat-ZXF227MP.js.map → chat-D32JGNVH.js.map} +0 -0
  95. /package/dist/cli/{chunk-H2F4LDNH.js.map → chunk-7AST3QQ3.js.map} +0 -0
  96. /package/dist/cli/{chunk-XMR2VCGT.js.map → chunk-7JTKBJ2G.js.map} +0 -0
  97. /package/dist/cli/{chunk-WKOXKCF3.js.map → chunk-ASOLXV67.js.map} +0 -0
  98. /package/dist/cli/{chunk-5AW6NIHU.js.map → chunk-DFX5ZH5L.js.map} +0 -0
  99. /package/dist/cli/{chunk-4E2BHJU4.js.map → chunk-GNS7BAT2.js.map} +0 -0
  100. /package/dist/cli/{chunk-O3AGYTG2.js.map → chunk-JGTX4RRQ.js.map} +0 -0
  101. /package/dist/cli/{chunk-SLAFMXAZ.js.map → chunk-JNTMOX7G.js.map} +0 -0
  102. /package/dist/cli/{chunk-RCLS63KE.js.map → chunk-MGTBP7GG.js.map} +0 -0
  103. /package/dist/cli/{chunk-PJIQIYTV.js.map → chunk-PB3MAFEI.js.map} +0 -0
  104. /package/dist/cli/{chunk-NQZ5U37J.js.map → chunk-PEMG6CUB.js.map} +0 -0
  105. /package/dist/cli/{chunk-KH5JIJJD.js.map → chunk-PXBQ6IZ7.js.map} +0 -0
  106. /package/dist/cli/{chunk-IKSYVBBZ.js.map → chunk-QX5TWXRZ.js.map} +0 -0
  107. /package/dist/cli/{chunk-R7U44O3Y.js.map → chunk-TAIKVL35.js.map} +0 -0
  108. /package/dist/cli/{chunk-EO6RPTJG.js.map → chunk-U5XQDCK7.js.map} +0 -0
  109. /package/dist/cli/{chunk-SWUMD2LX.js.map → chunk-W46ZMNKO.js.map} +0 -0
  110. /package/dist/cli/{chunk-3FULTFRB.js.map → chunk-ZAEJWKXB.js.map} +0 -0
  111. /package/dist/cli/{code-OKA5YPOH.js.map → code-R4IHI7SR.js.map} +0 -0
  112. /package/dist/cli/{commands-3U6PUVSS.js.map → commands-DRHFCYMO.js.map} +0 -0
  113. /package/dist/cli/{commit-HFB7SRBU.js.map → commit-AG5KB4YP.js.map} +0 -0
  114. /package/dist/cli/{desktop-G7UMW3CJ.js.map → desktop-JGL6GORA.js.map} +0 -0
  115. /package/dist/cli/{diff-PGXW4YZD.js.map → diff-4Z7ETWZO.js.map} +0 -0
  116. /package/dist/cli/{doctor-WWJFLVCB.js.map → doctor-VA3RHQLB.js.map} +0 -0
  117. /package/dist/cli/{mcp-browse-4IN2QIFR.js.map → mcp-browse-C3GXVMYZ.js.map} +0 -0
  118. /package/dist/cli/{mcp-inspect-BUXFXDHB.js.map → mcp-inspect-ZMYUNFDS.js.map} +0 -0
  119. /package/dist/cli/{prompt-US57R6BA.js.map → prompt-MC3U5KRP.js.map} +0 -0
  120. /package/dist/cli/{replay-EQJMZRB3.js.map → replay-4TP7ZUMZ.js.map} +0 -0
  121. /package/dist/cli/{setup-WLKX6GGG.js.map → setup-LHZELI6I.js.map} +0 -0
  122. /package/dist/cli/{stats-TCD7Q6MB.js.map → stats-SUIJ3QWY.js.map} +0 -0
  123. /package/dist/cli/{version-BCWWS2OU.js.map → version-BIFONEUB.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,7 +2,14 @@
2
2
  import { createParser } from "eventsource-parser";
3
3
 
4
4
  // src/config.ts
5
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import {
6
+ appendFileSync,
7
+ chmodSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ writeFileSync
12
+ } from "fs";
6
13
  import { homedir } from "os";
7
14
  import { dirname, isAbsolute, join, resolve } from "path";
8
15
  import { z } from "zod";
@@ -429,6 +436,117 @@ function parseMcpSpec(input) {
429
436
  return { transport: "stdio", name, command, args };
430
437
  }
431
438
 
439
+ // src/tools/rate-limit.ts
440
+ var DEFAULT_TOOL_RATE_LIMIT = {
441
+ aggregate: { maxCalls: 200, windowSeconds: 60 },
442
+ tools: {
443
+ run_command: { maxCalls: 60, windowSeconds: 60 },
444
+ run_background: { maxCalls: 10, windowSeconds: 60 }
445
+ }
446
+ };
447
+ var ToolRateLimiter = class {
448
+ config;
449
+ clock;
450
+ aggregate = [];
451
+ tools = /* @__PURE__ */ new Map();
452
+ constructor(config = {}, clock = () => Date.now()) {
453
+ this.config = normalizeToolRateLimitConfig(config);
454
+ this.clock = clock;
455
+ }
456
+ get policy() {
457
+ return this.config;
458
+ }
459
+ consume(tool) {
460
+ if (this.config === false) return { allowed: true };
461
+ const now = this.clock();
462
+ const toolBucket = this.config.tools[tool];
463
+ if (toolBucket !== false && toolBucket !== void 0) {
464
+ const timestamps = this.timestampsFor(tool);
465
+ const blocked = inspectBucket(tool, timestamps, toolBucket, now);
466
+ if (blocked) return { allowed: false, result: blocked };
467
+ }
468
+ const aggregateBlocked = inspectBucket(
469
+ tool,
470
+ this.aggregate,
471
+ this.config.aggregate,
472
+ now,
473
+ "all_tools"
474
+ );
475
+ if (aggregateBlocked) return { allowed: false, result: aggregateBlocked };
476
+ this.aggregate.push(now);
477
+ if (toolBucket !== false && toolBucket !== void 0) this.timestampsFor(tool).push(now);
478
+ return { allowed: true };
479
+ }
480
+ timestampsFor(tool) {
481
+ const existing = this.tools.get(tool);
482
+ if (existing) return existing;
483
+ const created = [];
484
+ this.tools.set(tool, created);
485
+ return created;
486
+ }
487
+ };
488
+ function normalizeToolRateLimitConfig(config) {
489
+ if (config === false || config?.enabled === false) return false;
490
+ const aggregate = normalizeBucket(config?.aggregate, DEFAULT_TOOL_RATE_LIMIT.aggregate);
491
+ const tools = {
492
+ ...DEFAULT_TOOL_RATE_LIMIT.tools
493
+ };
494
+ for (const [name, value] of Object.entries(config?.tools ?? {})) {
495
+ if (value === false) {
496
+ tools[name] = false;
497
+ continue;
498
+ }
499
+ const fallback = DEFAULT_TOOL_RATE_LIMIT.tools[name];
500
+ tools[name] = normalizeBucket(
501
+ value,
502
+ fallback === false || fallback === void 0 ? DEFAULT_TOOL_RATE_LIMIT.aggregate : fallback
503
+ );
504
+ }
505
+ return { aggregate, tools };
506
+ }
507
+ function parseRateLimitedToolResult(result) {
508
+ try {
509
+ const parsed = JSON.parse(result);
510
+ if (!parsed || typeof parsed !== "object") return null;
511
+ const value = parsed;
512
+ if (value.error !== "rate_limited") return null;
513
+ if (typeof value.tool !== "string" || typeof value.scope !== "string") return null;
514
+ if (typeof value.limit !== "number" || typeof value.windowSeconds !== "number") return null;
515
+ if (typeof value.retryAfterMs !== "number" || typeof value.message !== "string") return null;
516
+ return value;
517
+ } catch {
518
+ return null;
519
+ }
520
+ }
521
+ function normalizeBucket(raw, fallback) {
522
+ return {
523
+ maxCalls: positiveInteger(raw?.maxCalls) ?? fallback.maxCalls,
524
+ windowSeconds: positiveInteger(raw?.windowSeconds) ?? fallback.windowSeconds
525
+ };
526
+ }
527
+ function positiveInteger(value) {
528
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
529
+ }
530
+ function inspectBucket(tool, timestamps, bucket, now, scope = tool) {
531
+ const windowMs = bucket.windowSeconds * 1e3;
532
+ while (timestamps.length > 0 && now - timestamps[0] >= windowMs) timestamps.shift();
533
+ if (timestamps.length < bucket.maxCalls) return null;
534
+ const retryAfterMs = Math.max(0, timestamps[0] + windowMs - now);
535
+ return {
536
+ error: "rate_limited",
537
+ tool,
538
+ scope,
539
+ limit: bucket.maxCalls,
540
+ windowSeconds: bucket.windowSeconds,
541
+ retryAfterMs,
542
+ message: `${scope} rate-limited: ${bucket.maxCalls} calls / ${bucket.windowSeconds}s. Wait ${formatWait(retryAfterMs)} or summarize what you know.`
543
+ };
544
+ }
545
+ function formatWait(ms) {
546
+ const seconds = ms / 1e3;
547
+ return `${Number.isInteger(seconds) ? seconds.toFixed(0) : seconds.toFixed(1)}s`;
548
+ }
549
+
432
550
  // src/config.ts
433
551
  var BUILTIN_TYPE_DOCS = {
434
552
  user: "role / skills / preferences",
@@ -542,10 +660,30 @@ function readConfig(path2 = defaultConfigPath()) {
542
660
  return {};
543
661
  }
544
662
  function writeConfig(cfg, path2 = defaultConfigPath()) {
663
+ debugLogConfigWrite(cfg, path2);
545
664
  mkdirSync(dirname(path2), { recursive: true });
546
- writeFileSync(path2, JSON.stringify(cfg, null, 2), "utf8");
665
+ const tmp = `${path2}.${process.pid}.tmp`;
666
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2), "utf8");
547
667
  try {
548
- chmodSync(path2, 384);
668
+ chmodSync(tmp, 384);
669
+ } catch {
670
+ }
671
+ renameSync(tmp, path2);
672
+ }
673
+ function debugLogConfigWrite(cfg, configPath) {
674
+ const debugPath = process.env.REASONIX_DEBUG_PRESET;
675
+ if (!debugPath) return;
676
+ try {
677
+ const stack = new Error("trace").stack ?? "";
678
+ const keys = Object.keys(cfg).sort().join(",");
679
+ const presetField = cfg.preset === void 0 ? "(absent)" : JSON.stringify(cfg.preset);
680
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} writeConfig pid=${process.pid} \u2192 ${configPath}
681
+ keys=[${keys}]
682
+ preset=${presetField}
683
+ ${stack.split("\n").slice(1, 10).map((l) => ` ${l.trim()}`).join("\n")}
684
+
685
+ `;
686
+ appendFileSync(debugPath, line);
549
687
  } catch {
550
688
  }
551
689
  }
@@ -633,7 +771,7 @@ function webSearchEngine(path2 = defaultConfigPath()) {
633
771
  if (cfg === "tavily") return "tavily";
634
772
  if (cfg === "perplexity") return "perplexity";
635
773
  if (cfg === "exa") return "exa";
636
- return "mojeek";
774
+ return "bing";
637
775
  }
638
776
  function webSearchEndpoint(path2 = defaultConfigPath()) {
639
777
  const cfg = readConfig(path2).webSearchEndpoint;
@@ -753,8 +891,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
753
891
  }
754
892
  function sleep(ms, signal) {
755
893
  if (ms <= 0) return Promise.resolve();
756
- return new Promise((resolve13, reject) => {
757
- const timer = setTimeout(resolve13, ms);
894
+ return new Promise((resolve14, reject) => {
895
+ const timer = setTimeout(resolve14, ms);
758
896
  if (signal) {
759
897
  const onAbort = () => {
760
898
  clearTimeout(timer);
@@ -842,8 +980,8 @@ var DeepSeekClient = class {
842
980
  const waitMs = Math.max(0, this.nextChatRequestAt - now);
843
981
  this.nextChatRequestAt = Math.max(now, this.nextChatRequestAt) + this.minChatIntervalMs;
844
982
  if (waitMs <= 0) return;
845
- await new Promise((resolve13, reject) => {
846
- const timer = setTimeout(resolve13, waitMs);
983
+ await new Promise((resolve14, reject) => {
984
+ const timer = setTimeout(resolve14, waitMs);
847
985
  signal?.addEventListener(
848
986
  "abort",
849
987
  () => {
@@ -917,8 +1055,11 @@ var DeepSeekClient = class {
917
1055
  }
918
1056
  async chat(opts) {
919
1057
  const ctrl = new AbortController();
920
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
921
- const signal = opts.signal ?? ctrl.signal;
1058
+ const timer = setTimeout(
1059
+ () => ctrl.abort(new Error(`DeepSeek request timed out after ${this.timeoutMs}ms`)),
1060
+ this.timeoutMs
1061
+ );
1062
+ const signal = opts.signal ? AbortSignal.any([opts.signal, ctrl.signal]) : ctrl.signal;
922
1063
  try {
923
1064
  await this.waitForChatRateLimit(signal);
924
1065
  const resp = await fetchWithRetry(
@@ -953,8 +1094,11 @@ var DeepSeekClient = class {
953
1094
  }
954
1095
  async *stream(opts) {
955
1096
  const ctrl = new AbortController();
956
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
957
- const signal = opts.signal ?? ctrl.signal;
1097
+ const timer = setTimeout(
1098
+ () => ctrl.abort(new Error(`DeepSeek stream timed out after ${this.timeoutMs}ms`)),
1099
+ this.timeoutMs
1100
+ );
1101
+ const signal = opts.signal ? AbortSignal.any([opts.signal, ctrl.signal]) : ctrl.signal;
958
1102
  let resp;
959
1103
  try {
960
1104
  await this.waitForChatRateLimit(signal);
@@ -1053,10 +1197,10 @@ var PauseGate = class {
1053
1197
  `${kind}: no confirmation listener registered \u2014 cannot prompt the user. This tool can only be used inside an interactive Reasonix session.`
1054
1198
  );
1055
1199
  }
1056
- return new Promise((resolve13) => {
1200
+ return new Promise((resolve14) => {
1057
1201
  const id = this._nextId++;
1058
1202
  const request = { id, kind, payload };
1059
- this._pending.set(id, { resolve: resolve13, request });
1203
+ this._pending.set(id, { resolve: resolve14, request });
1060
1204
  for (const fn of this._listeners) {
1061
1205
  try {
1062
1206
  fn(request);
@@ -1211,7 +1355,17 @@ var EN = {
1211
1355
  missingApiKey: "DEEPSEEK_API_KEY is not set and stdin is not a TTY (cannot prompt).\nSet the env var, or run `reasonix chat` once interactively to save a key.\n"
1212
1356
  },
1213
1357
  sessions: {
1214
- emptyHint: "no saved sessions yet \u2014 run `reasonix chat` (sessions are auto-saved unless --no-session)."
1358
+ emptyHint: "no saved sessions yet \u2014 run `reasonix chat` (sessions are auto-saved unless --no-session).",
1359
+ listHeader: "Saved sessions (~/.reasonix/sessions/):",
1360
+ inspectHint: "Inspect: reasonix sessions <name>",
1361
+ resumeHint: "Resume: reasonix chat --session <name>",
1362
+ noSession: 'no session named "{name}" (or it\u2019s empty).',
1363
+ lookedAt: "looked at: {path}",
1364
+ noIdleSessions: "no sessions idle \u2265{days} days. Nothing pruned.",
1365
+ wouldPrune: "would prune {count} session(s) idle \u2265{days} days:",
1366
+ dryRunHint: "re-run without --dry-run to actually delete.",
1367
+ prunedCount: "pruned {count} session(s) idle \u2265{days} days:",
1368
+ daysInvalid: "--days must be a positive integer (got {days})."
1215
1369
  },
1216
1370
  ui: {
1217
1371
  welcome: "Run `reasonix` any time to start chatting \u2014 your settings are remembered.",
@@ -1315,10 +1469,6 @@ var EN = {
1315
1469
  title: "copy / paste",
1316
1470
  rows: [
1317
1471
  { key: "select text", text: "drag to select \u2014 terminal-native (no modifier needed)" },
1318
- {
1319
- key: "/copy",
1320
- text: "vim/tmux-style copy mode \u2014 works in SSH/mosh/tmux where drag-select can't extend past the viewport"
1321
- },
1322
1472
  {
1323
1473
  key: "copy",
1324
1474
  text: "Ctrl+Shift+C (Win/Linux) \xB7 Cmd+C (macOS) \u2014 or auto-copy-on-select if your terminal does it"
@@ -1409,9 +1559,6 @@ var EN = {
1409
1559
  },
1410
1560
  slash: {
1411
1561
  help: { description: "show the full command reference" },
1412
- copy: {
1413
- description: "open vim/tmux-style copy mode \u2014 j/k navigate, v select, y yank to clipboard"
1414
- },
1415
1562
  status: { description: "current model, flags, context, session" },
1416
1563
  preset: {
1417
1564
  description: "model bundle \u2014 auto escalates flash \u2192 pro, flash/pro lock",
@@ -1563,8 +1710,8 @@ var EN = {
1563
1710
  argsHint: "<question>"
1564
1711
  },
1565
1712
  "search-engine": {
1566
- description: "switch web search backend \u2014 mojeek (default, no deps), searxng (self-hosted), metaso (free 100/d), tavily (free 1000/mo), perplexity (AI-native), or exa (AI-native)",
1567
- argsHint: "<mojeek|searxng|metaso|tavily|perplexity|exa> [<key>]"
1713
+ description: "switch web search backend \u2014 bing (default, works from CN without proxy), searxng (self-hosted), metaso (free 100/d), tavily (free 1000/mo), perplexity (AI-native), or exa (AI-native)",
1714
+ argsHint: "<bing|searxng|metaso|tavily|perplexity|exa> [<key>]"
1568
1715
  }
1569
1716
  },
1570
1717
  wizard: {
@@ -1727,6 +1874,8 @@ var EN = {
1727
1874
  verboseOn: "\u25B8 verbose mode on \u2014 full reasoning + tool output",
1728
1875
  verboseOff: "\u25B8 verbose mode off \u2014 head/tail elision restored",
1729
1876
  commandFailed: "! command failed",
1877
+ steerInjected: "\u25B8 steering queued \u2014 will be added after the current step",
1878
+ steerCommandRejected: "\u25B8 commands are disabled while steering a busy turn",
1730
1879
  btwUsage: "\u25B8 /btw <question> \u2014 ask a side question without polluting the conversation context.",
1731
1880
  btwHeader: "\u226B btw",
1732
1881
  btwFailed: "/btw failed",
@@ -1772,7 +1921,9 @@ var EN = {
1772
1921
  editHistoryAlreadyReverted: "(already reverted \u2014 /history shows the batch-level status)",
1773
1922
  editHistoryRevertFile: "/undo {id} {path} \u2192 revert just this file",
1774
1923
  mcpFailed: "MCP {name} failed",
1775
- mcpWarn: "MCP {name} warn"
1924
+ mcpWarn: "MCP {name} warn",
1925
+ unknownTheme: "unknown theme: {name}\navailable: {choices}",
1926
+ themeSaved: "theme saved: {name}\nactive on next launch: {active}"
1776
1927
  },
1777
1928
  hooks: {
1778
1929
  head: "hook {tag} `{cmd}` {decision}{truncTag}",
@@ -1816,6 +1967,7 @@ var EN = {
1816
1967
  balance402: "Out of balance (DeepSeek 402): {inner}. Top up at https://platform.deepseek.com/top_up \u2014 the panel header shows your balance once it's non-zero.",
1817
1968
  badparam422: "Invalid parameter (DeepSeek 422): {inner}",
1818
1969
  badrequest400: "Bad request (DeepSeek 400): {inner}",
1970
+ concurrency429: "DeepSeek concurrency limit hit (429): {inner}. The account has too many in-flight requests (cap: 500 for v4-pro, 2500 for v4-flash, summed across API keys account-wide). Usually means another Reasonix process is sharing the same key, or a parallel subagent fan-out overshot. Wait a few seconds and retry, reduce parallelism, or request a higher cap at https://platform.deepseek.com.",
1819
1971
  deepseek5xxHead: "DeepSeek service unavailable ({status}) \u2014 this is a DeepSeek-side problem, not Reasonix. Already retried 4\xD7 with backoff.",
1820
1972
  deepseek5xxReachable: " DeepSeek's main API answered our health check, but /chat/completions is failing \u2014 partial outage on their side.",
1821
1973
  deepseek5xxUnreachable: " DeepSeek API is unreachable from your network \u2014 could be a wider DS outage or a local network issue.",
@@ -2101,6 +2253,9 @@ var EN = {
2101
2253
  activeNone: "\u25B8 active plan: (none)",
2102
2254
  noArchives: "no archived plans yet for this session \u2014 they auto-archive when every step is done",
2103
2255
  archivedHeader: "Archived ({count}):",
2256
+ evidencePending: " ! evidence pending \u2014 current step needs verification/diff/checkpoint/manual evidence",
2257
+ evidenceLine: " evidence {stepId}: {summary}",
2258
+ archivedEvidenceLine: " evidence: {summary}",
2104
2259
  replayNoSession: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session.",
2105
2260
  replayNoArchives: "no archived plans yet for this session \u2014 `/replay` lights up once a plan completes (auto-archives when every step is done).",
2106
2261
  replayInvalidIndex: "invalid index \u2014 `/replay` takes 1..{max} (newest = 1). Use `/plans` to see the list.",
@@ -2195,7 +2350,7 @@ var EN = {
2195
2350
  currentEngine: "Current web search engine: {engine}",
2196
2351
  endpoint: "SearXNG endpoint: {url}",
2197
2352
  usageHeader: "Usage:",
2198
- usageMojeek: " /search-engine mojeek use Mojeek (default, no external deps)",
2353
+ usageBing: " /search-engine bing use Bing (default, works from CN without proxy)",
2199
2354
  usageSearxng: " /search-engine searxng use SearXNG at default endpoint",
2200
2355
  usageSearxngUrl: " /search-engine searxng <url> use SearXNG at custom endpoint",
2201
2356
  usageMetaso: " /search-engine metaso use Metaso API (100/d free, configure your own API key for more)",
@@ -2291,7 +2446,9 @@ var EN = {
2291
2446
  editorFailed: "external editor:",
2292
2447
  editorMissing: "no $EDITOR / $VISUAL / $GIT_EDITOR set \u2014 export one (e.g. `export EDITOR=nano`) and retry",
2293
2448
  editorExited: "editor exited with code {code}",
2294
- typeaheadStaged: "\u25B8 {count} line(s) staged \xB7 esc recall"
2449
+ typeaheadStaged: "\u25B8 {count} line(s) staged \xB7 esc recall",
2450
+ steerPlaceholder: "type to steer the current task \u2014 commands are disabled while busy",
2451
+ steerHint: "send \u2014 injected mid-turn"
2295
2452
  },
2296
2453
  pathConfirm: {
2297
2454
  title: "Outside-sandbox path",
@@ -2310,7 +2467,13 @@ var EN = {
2310
2467
  denyDesc: "press Tab to add context telling the model why",
2311
2468
  pathLabel: "path",
2312
2469
  sandboxLabel: "sandbox",
2313
- allowPrefixLabel: "prefix"
2470
+ allowPrefixLabel: "prefix",
2471
+ promptTitleRead: "Access path \u2014 read",
2472
+ promptTitleWrite: "Access path \u2014 write",
2473
+ actionAllowRead: "Allow read",
2474
+ actionAllowWrite: "Allow write",
2475
+ actionAlwaysAllow: "Always allow \u2014 {prefix}",
2476
+ actionDeny: "Deny"
2314
2477
  },
2315
2478
  shellConfirm: {
2316
2479
  title: "Shell command",
@@ -2332,7 +2495,12 @@ var EN = {
2332
2495
  timeoutLabel: "timeout",
2333
2496
  waitLabel: "wait",
2334
2497
  previewMore: "\u2026 {n} more line hidden \u2014 press esc, ask the model to split it",
2335
- previewMorePlural: "\u2026 {n} more lines hidden \u2014 press esc, ask the model to split it"
2498
+ previewMorePlural: "\u2026 {n} more lines hidden \u2014 press esc, ask the model to split it",
2499
+ promptTitleRunCommand: "Run command",
2500
+ promptTitleRunBackground: "Run background command",
2501
+ actionRunOnce: "Run once",
2502
+ actionAlwaysAllow: "Always allow \u2014 {prefix}",
2503
+ actionDeny: "Deny"
2336
2504
  },
2337
2505
  editConfirm: {
2338
2506
  footer: "[y/Enter] apply \xB7 [n] reject with reason \xB7 [a] apply rest \xB7 [A] flip AUTO \xB7 [\u2191\u2193/Space] scroll \xB7 [Esc] abort",
@@ -2473,37 +2641,37 @@ var EN = {
2473
2641
  probeFailed: "probe failed \u2014 {message}"
2474
2642
  },
2475
2643
  webErrors: {
2476
- status: "web_search {status} \u2014 try: the search backend returned an error; rephrase the query, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2644
+ status: "web_search {status} \u2014 try: the search backend returned an error; rephrase the query, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2477
2645
  rateLimit429: "web_search 429 \u2014 try: wait 10s before retrying, or rephrase the query; the search backend is rate-limiting this client",
2478
- forbidden403: "web_search 403 \u2014 try: the search backend is blocking this client; switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa, or wait and retry later",
2646
+ forbidden403: "web_search 403 \u2014 try: the search backend is blocking this client; switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa, or wait and retry later",
2479
2647
  serverError5xx: "web_search {status} \u2014 try: open the search URL in a browser; if it loads this is transient and a retry in 30s may help",
2480
- mojeekBlocked: "web_search: Mojeek anti-bot page \u2014 rate-limited or blocked \u2014 try: wait 30s and retry, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2481
- mojeekNoResults: "web_search: 0 results but response doesn't look like a real empty page ({chars} chars, first 120: {preview}) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2648
+ bingBlocked: "web_search: Bing anti-bot page \u2014 rate-limited or blocked \u2014 try: wait 30s and retry, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2649
+ bingNoResults: "web_search: 0 results but response doesn't look like a real empty page ({chars} chars, first 120: {preview}) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2482
2650
  invalidEndpoint: 'web_search: invalid SearXNG endpoint "{endpoint}" \u2014 try: set a valid URL with /search-endpoint http://host:port',
2483
2651
  endpointMustBeHttp: "web_search: SearXNG endpoint must be http(s), got {protocol} \u2014 try: set a valid URL with /search-endpoint http://host:port",
2484
- cannotReach: "web_search: Cannot reach SearXNG server at {endpoint} \u2014 try: install and start SearXNG (https://github.com/searxng/searxng, e.g. `docker run -d -p 8080:8080 searxng/searxng`), or switch to another engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2485
- searxngNoResults: "web_search: 0 results but SearXNG response doesn't look like an empty results page ({chars} chars) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2652
+ cannotReach: "web_search: Cannot reach SearXNG server at {endpoint} \u2014 try: install and start SearXNG (https://github.com/searxng/searxng, e.g. `docker run -d -p 8080:8080 searxng/searxng`), or switch to another engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2653
+ searxngNoResults: "web_search: 0 results but SearXNG response doesn't look like an empty results page ({chars} chars) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2486
2654
  metasoMissingKey: "web_search: Metaso requires an API key \u2014 set METASO_API_KEY or configure one with /search-engine metaso <key>. Get one at https://metaso.cn/search-api/playground",
2487
2655
  metasoDailyLimit: "web_search: Metaso daily search limit reached \u2014 set METASO_API_KEY or get a key at https://metaso.cn/search-api/playground",
2488
2656
  metasoUnauthorized: "web_search: Metaso API key rejected \u2014 check METASO_API_KEY or get one at https://metaso.cn/search-api/playground",
2489
2657
  metasoRateLimit: "web_search: Metaso rate-limited \u2014 wait and retry, or get your own API key at https://metaso.cn/search-api/playground",
2490
- metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2658
+ metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2491
2659
  metasoParseError: "web_search: Metaso returned unparseable response (HTTP {status}) \u2014 try again later",
2492
2660
  metasoApiError: "web_search: Metaso API error (code {code}: {message}) \u2014 try again later",
2493
2661
  tavilyMissingKey: "web_search: Tavily backend requires an API key \u2014 set TAVILY_API_KEY env var or `tavilyApiKey` in ~/.reasonix/config.json; free 1000/mo signup at https://tavily.com",
2494
2662
  tavilyUnauthorized: "web_search: Tavily API key rejected \u2014 check TAVILY_API_KEY or get one at https://tavily.com",
2495
- tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa, or upgrade your Tavily plan",
2496
- tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2663
+ tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa, or upgrade your Tavily plan",
2664
+ tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2497
2665
  tavilyParseError: "web_search: Tavily returned unparseable response (HTTP {status}) \u2014 try again later",
2498
2666
  perplexityMissingKey: "web_search: Perplexity backend requires an API key \u2014 set PERPLEXITY_API_KEY env var or `perplexityApiKey` in ~/.reasonix/config.json; get one at https://perplexity.ai/settings/api",
2499
2667
  perplexityUnauthorized: "web_search: Perplexity API key rejected \u2014 check PERPLEXITY_API_KEY or get one at https://perplexity.ai/settings/api",
2500
- perplexityRateLimit: "web_search: Perplexity rate-limited \u2014 wait and retry, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2501
- perplexityServerError: "web_search: Perplexity server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2668
+ perplexityRateLimit: "web_search: Perplexity rate-limited \u2014 wait and retry, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2669
+ perplexityServerError: "web_search: Perplexity server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2502
2670
  perplexityParseError: "web_search: Perplexity returned unparseable response (HTTP {status}) \u2014 try again later",
2503
2671
  exaMissingKey: "web_search: Exa backend requires an API key \u2014 set EXA_API_KEY env var or `exaApiKey` in ~/.reasonix/config.json; free 1000/mo signup at https://exa.ai",
2504
2672
  exaUnauthorized: "web_search: Exa API key rejected \u2014 check EXA_API_KEY or get one at https://exa.ai",
2505
2673
  exaRateLimit: "web_search: Exa API rate-limited or monthly quota exceeded \u2014 wait or upgrade at https://exa.ai/pricing",
2506
- exaServerError: "web_search: Exa server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2674
+ exaServerError: "web_search: Exa server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2507
2675
  exaParseError: "web_search: Exa returned unparseable response (HTTP {status}) \u2014 try again later",
2508
2676
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2509
2677
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
@@ -2595,19 +2763,6 @@ var EN = {
2595
2763
  categoryProject: "project",
2596
2764
  categoryReference: "reference"
2597
2765
  },
2598
- copyMode: {
2599
- title: "\u2500\u2500 COPY MODE \u2500\u2500",
2600
- help: "j/k or \u2191/\u2193 move \xB7 v select \xB7 y yank \xB7 g/G top/bottom \xB7 q quit",
2601
- statusBar: "line {cur}/{total} \xB7 selection: {sel}",
2602
- statusYanked: "yanked {size} chars (osc52={osc52})",
2603
- statusEmpty: "nothing selected",
2604
- empty: "(no chat content yet \u2014 say something to the model first)",
2605
- labelUser: "you",
2606
- labelAssistant: "assistant",
2607
- labelReasoning: "reasoning",
2608
- yankedToast: "\u25B8 copied {size} chars to clipboard (osc52)",
2609
- yankedToastFile: "\u25B8 copied {size} chars \xB7 file: {path}"
2610
- },
2611
2766
  mcpHealth: {
2612
2767
  noData: "no inspect data",
2613
2768
  healthy: "healthy \xB7 {ms}ms",
@@ -2759,9 +2914,11 @@ var EN = {
2759
2914
  groupSystem: "System",
2760
2915
  descEnter: "Send message",
2761
2916
  descShiftEnter: "New line",
2917
+ descCtrlEnter: "New line",
2918
+ descCtrlJ: "New line",
2762
2919
  descCtrlU: "Clear input",
2763
2920
  descCtrlW: "Delete word",
2764
- descCtrlP: "Toggle shortcut panel",
2921
+ descCtrlP: "Show/hide shortcuts",
2765
2922
  descCtrlX: "Open in editor",
2766
2923
  descArrows: "Input history",
2767
2924
  descPgUpDown: "Scroll page",
@@ -2775,9 +2932,30 @@ var EN = {
2775
2932
  descCtrlC: "Quit",
2776
2933
  descEsc: "Stop / Cancel",
2777
2934
  descCtrlR: "Toggle verbose",
2778
- descCtrlO: "Expand stream",
2935
+ descCtrlO: "Expand reply (streaming only)",
2779
2936
  descHelp: "Show all commands",
2780
2937
  descShiftTab: "Switch edit mode"
2938
+ },
2939
+ mcpCli: {
2940
+ bundledCatalog: "Bundled MCP servers (offline catalog):",
2941
+ justFetched: "just fetched",
2942
+ cachedAge: "cached, {age}",
2943
+ moreAvailable: "more available",
2944
+ allLoaded: "all loaded",
2945
+ morePagesAvailable: "\u25B8 more pages available \u2014 `reasonix mcp list --pages <n>` or --all",
2946
+ installHint: "Install: reasonix mcp install <name>",
2947
+ usageSearch: "usage: reasonix mcp search <query>",
2948
+ usageInstall: "usage: reasonix mcp install <name>",
2949
+ noMatchesFor: 'No matches for "{q}" across {count} loaded entries ({source})',
2950
+ matchCount: '{count} match(es) for "{q}" in {source} registry ({loaded} entries scanned):',
2951
+ moreLoaded: "\u2026 {count} more loaded \u2014 use `reasonix mcp search <query>` to filter",
2952
+ moreMatches: "\u2026 {count} more matches",
2953
+ installed: "Installed: {spec}",
2954
+ noServerFound: 'No MCP server named "{target}" found after walking {pages} page(s) of the {source} registry.',
2955
+ noServerTryMore: "Try: reasonix mcp install {target} --max-pages 100",
2956
+ noInstallMeta: 'Could not derive install metadata for "{name}" \u2014 try `npx -y @smithery/cli install {name}` directly.',
2957
+ buildSpecFailed: "Cannot build install spec for {name}: {message}",
2958
+ alreadyInstalled: "Already installed: {spec}"
2781
2959
  }
2782
2960
  };
2783
2961
 
@@ -2824,7 +3002,17 @@ var zhCN = {
2824
3002
  missingApiKey: "\u672A\u8BBE\u7F6E DEEPSEEK_API_KEY \u4E14\u6807\u51C6\u8F93\u5165\u4E0D\u662F TTY\uFF08\u65E0\u6CD5\u4EA4\u4E92\u5F0F\u8F93\u5165\uFF09\u3002\n\u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5148\u8FD0\u884C `reasonix chat` \u4EA4\u4E92\u4E00\u6B21\u4EE5\u4FDD\u5B58\u5BC6\u94A5\u3002\n"
2825
3003
  },
2826
3004
  sessions: {
2827
- emptyHint: "\u6682\u65E0\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD \u2014 \u8FD0\u884C `reasonix chat`\uFF08\u4F1A\u8BDD\u4F1A\u81EA\u52A8\u4FDD\u5B58\uFF0C\u9664\u975E\u4F7F\u7528\u4E86 --no-session\uFF09\u3002"
3005
+ emptyHint: "\u6682\u65E0\u5DF2\u4FDD\u5B58\u7684\u4F1A\u8BDD \u2014 \u8FD0\u884C `reasonix chat`\uFF08\u4F1A\u8BDD\u4F1A\u81EA\u52A8\u4FDD\u5B58\uFF0C\u9664\u975E\u4F7F\u7528\u4E86 --no-session\uFF09\u3002",
3006
+ listHeader: "\u4FDD\u5B58\u7684\u4F1A\u8BDD (~/.reasonix/sessions/)\uFF1A",
3007
+ inspectHint: "\u67E5\u770B\uFF1Areasonix sessions <name>",
3008
+ resumeHint: "\u6062\u590D\uFF1Areasonix chat --session <name>",
3009
+ noSession: '\u627E\u4E0D\u5230\u4F1A\u8BDD "{name}"\uFF08\u6216\u4E3A\u7A7A\uFF09\u3002',
3010
+ lookedAt: "\u4F4D\u7F6E\uFF1A{path}",
3011
+ noIdleSessions: "\u6CA1\u6709\u95F2\u7F6E \u2265{days} \u5929\u7684\u4F1A\u8BDD\u3002\u65E0\u9700\u6E05\u7406\u3002",
3012
+ wouldPrune: "\u5C06\u6E05\u7406 {count} \u4E2A\u95F2\u7F6E \u2265{days} \u5929\u7684\u4F1A\u8BDD\uFF1A",
3013
+ dryRunHint: "\u53BB\u6389 --dry-run \u53EF\u5B9E\u9645\u6267\u884C\u5220\u9664\u3002",
3014
+ prunedCount: "\u5DF2\u6E05\u7406 {count} \u4E2A\u95F2\u7F6E \u2265{days} \u5929\u7684\u4F1A\u8BDD\uFF1A",
3015
+ daysInvalid: "--days \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF08\u4F20\u5165\u4E86 {days}\uFF09\u3002"
2828
3016
  },
2829
3017
  ui: {
2830
3018
  welcome: "\u968F\u65F6\u8FD0\u884C `reasonix` \u5F00\u59CB\u804A\u5929 \u2014 \u60A8\u7684\u8BBE\u7F6E\u5C06\u88AB\u8BB0\u4F4F\u3002",
@@ -2922,10 +3110,6 @@ var zhCN = {
2922
3110
  title: "\u590D\u5236 / \u7C98\u8D34",
2923
3111
  rows: [
2924
3112
  { key: "\u9009\u4E2D\u6587\u5B57", text: "\u76F4\u63A5\u62D6\u52A8 \u2014 \u7EC8\u7AEF\u539F\u751F\uFF08\u4E0D\u9700\u8981\u4EFB\u4F55\u4FEE\u9970\u952E\uFF09" },
2925
- {
2926
- key: "/copy",
2927
- text: "vim/tmux \u98CE\u683C\u590D\u5236\u6A21\u5F0F \u2014 SSH / mosh / tmux \u4E0B\u62D6\u9009\u8D8A\u8FC7\u53EF\u89C6\u533A\u65E0\u6548\u65F6\u7528\u8FD9\u4E2A"
2928
- },
2929
3113
  {
2930
3114
  key: "\u590D\u5236",
2931
3115
  text: "Ctrl+Shift+C\uFF08Win/Linux\uFF09\xB7 Cmd+C\uFF08macOS\uFF09\u2014 \u6216\u9009\u4E2D\u5373\u590D\u5236\uFF08\u770B\u7EC8\u7AEF\u8BBE\u7F6E\uFF09"
@@ -3016,9 +3200,6 @@ var zhCN = {
3016
3200
  },
3017
3201
  slash: {
3018
3202
  help: { description: "\u663E\u793A\u5B8C\u6574\u547D\u4EE4\u53C2\u8003" },
3019
- copy: {
3020
- description: "\u8FDB\u5165 vim/tmux \u98CE\u683C\u590D\u5236\u6A21\u5F0F \u2014 j/k \u79FB\u52A8\u3001v \u8D77\u9009\u533A\u3001y \u590D\u5236\u5230\u526A\u8D34\u677F"
3021
- },
3022
3203
  status: { description: "\u5F53\u524D\u6A21\u578B\u3001\u6807\u5FD7\u3001\u4E0A\u4E0B\u6587\u3001\u4F1A\u8BDD" },
3023
3204
  preset: {
3024
3205
  description: "\u6A21\u578B\u7EC4\u5408 \u2014 \u81EA\u52A8\u5728 flash \u2192 pro \u4E4B\u95F4\u5207\u6362\uFF0C\u6216\u9501\u5B9A flash/pro",
@@ -3174,8 +3355,8 @@ var zhCN = {
3174
3355
  argsHint: "<question>"
3175
3356
  },
3176
3357
  "search-engine": {
3177
- description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u4F9D\u8D56\uFF09\u3001searxng\uFF08\u81EA\u6258\u7BA1\uFF09\u3001metaso\uFF08\u6BCF\u65E5 100 \u6B21\uFF09\u3001tavily\uFF08\u6BCF\u6708 1000 \u6B21\u514D\u8D39\uFF09\u3001perplexity\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09\u6216 exa\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09",
3178
- argsHint: "<mojeek|searxng|metaso|tavily|perplexity|exa> [<key>]"
3358
+ description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 bing\uFF08\u9ED8\u8BA4\uFF0C\u56FD\u5185\u88F8 IP \u76F4\u8FDE\uFF09\u3001searxng\uFF08\u81EA\u6258\u7BA1\uFF09\u3001metaso\uFF08\u6BCF\u65E5 100 \u6B21\uFF09\u3001tavily\uFF08\u6BCF\u6708 1000 \u6B21\u514D\u8D39\uFF09\u3001perplexity\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09\u6216 exa\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09",
3359
+ argsHint: "<bing|searxng|metaso|tavily|perplexity|exa> [<key>]"
3179
3360
  }
3180
3361
  },
3181
3362
  wizard: {
@@ -3338,6 +3519,8 @@ var zhCN = {
3338
3519
  verboseOn: "\u25B8 \u8BE6\u7EC6\u6A21\u5F0F\u5DF2\u5F00 \u2014 \u663E\u793A\u5B8C\u6574\u63A8\u7406 + \u5DE5\u5177\u8F93\u51FA",
3339
3520
  verboseOff: "\u25B8 \u8BE6\u7EC6\u6A21\u5F0F\u5DF2\u5173 \u2014 \u6062\u590D\u5934\u5C3E\u7701\u7565",
3340
3521
  commandFailed: "! \u547D\u4EE4\u5931\u8D25",
3522
+ steerInjected: "\u25B8 \u5DF2\u52A0\u5165\u5F15\u5BFC\u961F\u5217 \u2014 \u5C06\u5728\u5F53\u524D\u6B65\u9AA4\u540E\u6CE8\u5165",
3523
+ steerCommandRejected: "\u25B8 \u5F53\u524D\u8F6E\u6B21\u5FD9\u788C\u65F6\u4E0D\u80FD\u63D0\u4EA4\u547D\u4EE4\uFF0C\u53EA\u80FD\u8F93\u5165\u666E\u901A\u5F15\u5BFC\u6D88\u606F",
3341
3524
  btwUsage: "\u25B8 /btw <\u95EE\u9898> \u2014 \u987A\u4FBF\u95EE\u4E2A\u9898\u5916\u8BDD\uFF0C\u4E0D\u4F1A\u5199\u5165\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002",
3342
3525
  btwHeader: "\u226B btw",
3343
3526
  btwFailed: "/btw \u8C03\u7528\u5931\u8D25",
@@ -3383,7 +3566,9 @@ var zhCN = {
3383
3566
  editHistoryAlreadyReverted: "\uFF08\u5DF2\u64A4\u9500 \u2014 /history \u663E\u793A\u6279\u6B21\u7EA7\u72B6\u6001\uFF09",
3384
3567
  editHistoryRevertFile: "/undo {id} {path} \u2192 \u4EC5\u8FD8\u539F\u6B64\u6587\u4EF6",
3385
3568
  mcpFailed: "MCP {name} \u5931\u8D25",
3386
- mcpWarn: "MCP {name} \u8B66\u544A"
3569
+ mcpWarn: "MCP {name} \u8B66\u544A",
3570
+ unknownTheme: "\u672A\u77E5\u4E3B\u9898\uFF1A{name}\n\u53EF\u7528\u4E3B\u9898\uFF1A{choices}",
3571
+ themeSaved: "\u4E3B\u9898\u5DF2\u4FDD\u5B58\uFF1A{name}\n\u4E0B\u6B21\u542F\u52A8\u751F\u6548\uFF1A{active}"
3387
3572
  },
3388
3573
  hooks: {
3389
3574
  head: "\u94A9\u5B50 {tag} `{cmd}` {decision}{truncTag}",
@@ -3427,6 +3612,7 @@ var zhCN = {
3427
3612
  balance402: "\u4F59\u989D\u4E0D\u8DB3\uFF08DeepSeek 402\uFF09\uFF1A{inner}\u3002\u5728 https://platform.deepseek.com/top_up \u5145\u503C \u2014 \u4F59\u989D\u975E\u96F6\u65F6\u9762\u677F\u9876\u680F\u4F1A\u663E\u793A\u3002",
3428
3613
  badparam422: "\u53C2\u6570\u9519\u8BEF\uFF08DeepSeek 422\uFF09\uFF1A{inner}",
3429
3614
  badrequest400: "\u8BF7\u6C42\u9519\u8BEF\uFF08DeepSeek 400\uFF09\uFF1A{inner}",
3615
+ concurrency429: "DeepSeek \u5E76\u53D1\u8D85\u9650\uFF08429\uFF09\uFF1A{inner}\u3002\u8D26\u53F7\u5728\u8DD1\u7684\u8BF7\u6C42\u8D85\u8FC7\u4E0A\u9650\uFF08v4-pro 500\u3001v4-flash 2500\uFF0C\u8D26\u53F7\u4E0B\u6240\u6709 API key \u7D2F\u52A0\uFF09\u3002\u901A\u5E38\u662F\u540C\u4E00\u8D26\u53F7\u5F00\u4E86\u591A\u4E2A Reasonix \u8FDB\u7A0B\uFF0C\u6216\u8005\u5E76\u884C subagent \u4E00\u6B21\u53D1\u592A\u591A\u3002\u7B49\u51E0\u79D2\u91CD\u8BD5\u3001\u51CF\u5C11\u5E76\u884C\uFF0C\u6216\u5728 https://platform.deepseek.com \u7533\u8BF7\u6269\u5BB9\u3002",
3430
3616
  deepseek5xxHead: "DeepSeek \u670D\u52A1\u4E0D\u53EF\u7528\uFF08{status}\uFF09 \u2014 \u8FD9\u662F DeepSeek \u670D\u52A1\u7AEF\u95EE\u9898\uFF0C\u4E0D\u662F Reasonix \u6545\u969C\u3002\u5DF2\u6309\u6307\u6570\u9000\u907F\u91CD\u8BD5 4 \u6B21\u3002",
3431
3617
  deepseek5xxReachable: " DeepSeek \u4E3B API \u5065\u5EB7\u68C0\u67E5\u901A\u8FC7\uFF0C\u4F46 /chat/completions \u5728\u6302 \u2014 \u4ED6\u4EEC\u90A3\u8FB9\u90E8\u5206\u670D\u52A1\u5F02\u5E38\u3002",
3432
3618
  deepseek5xxUnreachable: " \u65E0\u6CD5\u4ECE\u4F60\u7684\u7F51\u7EDC\u8BBF\u95EE DeepSeek API \u2014 \u53EF\u80FD\u662F DS \u6574\u4F53\u6545\u969C\uFF0C\u4E5F\u53EF\u80FD\u662F\u672C\u5730\u7F51\u7EDC\u95EE\u9898\u3002",
@@ -3712,6 +3898,9 @@ var zhCN = {
3712
3898
  activeNone: "\u25B8 \u6D3B\u8DC3\u8BA1\u5212\uFF1A\uFF08\u65E0\uFF09",
3713
3899
  noArchives: "\u6B64\u4F1A\u8BDD\u5C1A\u65E0\u5F52\u6863\u8BA1\u5212 \u2014 \u5F53\u6BCF\u4E2A\u6B65\u9AA4\u5B8C\u6210\u65F6\u81EA\u52A8\u5F52\u6863",
3714
3900
  archivedHeader: "\u5DF2\u5F52\u6863\uFF08{count}\uFF09\uFF1A",
3901
+ evidencePending: " ! \u7B49\u5F85 evidence \u2014 \u5F53\u524D\u6B65\u9AA4\u9700\u8981 verification/diff/checkpoint/manual evidence",
3902
+ evidenceLine: " evidence {stepId}: {summary}",
3903
+ archivedEvidenceLine: " evidence: {summary}",
3715
3904
  replayNoSession: "\u672A\u9644\u52A0\u4F1A\u8BDD \u2014 `/replay` \u662F\u6309\u4F1A\u8BDD\u7684\u3002\u5728\u9879\u76EE\u4E2D\u8FD0\u884C `reasonix code` \u4EE5\u83B7\u53D6\u4F1A\u8BDD\u3002",
3716
3905
  replayNoArchives: "\u6B64\u4F1A\u8BDD\u5C1A\u65E0\u5F52\u6863\u8BA1\u5212 \u2014 `/replay` \u5728\u8BA1\u5212\u5B8C\u6210\u540E\u542F\u7528\uFF08\u6BCF\u4E2A\u6B65\u9AA4\u5B8C\u6210\u65F6\u81EA\u52A8\u5F52\u6863\uFF09\u3002",
3717
3906
  replayInvalidIndex: "\u65E0\u6548\u7D22\u5F15 \u2014 `/replay` \u63A5\u53D7 1..{max}\uFF08\u6700\u65B0 = 1\uFF09\u3002\u4F7F\u7528 `/plans` \u67E5\u770B\u5217\u8868\u3002",
@@ -3806,7 +3995,7 @@ var zhCN = {
3806
3995
  currentEngine: "\u5F53\u524D\u7F51\u9875\u641C\u7D22\u5F15\u64CE\uFF1A{engine}",
3807
3996
  endpoint: "SearXNG \u7AEF\u70B9\uFF1A{url}",
3808
3997
  usageHeader: "\u7528\u6CD5\uFF1A",
3809
- usageMojeek: " /search-engine mojeek \u4F7F\u7528 Mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u5916\u90E8\u4F9D\u8D56\uFF09",
3998
+ usageBing: " /search-engine bing \u4F7F\u7528 Bing\uFF08\u9ED8\u8BA4\uFF0C\u56FD\u5185\u88F8 IP \u76F4\u8FDE\uFF0C\u65E0\u9700\u4EE3\u7406\uFF09",
3810
3999
  usageSearxng: " /search-engine searxng \u4F7F\u7528 SearXNG \u9ED8\u8BA4\u7AEF\u70B9",
3811
4000
  usageSearxngUrl: " /search-engine searxng <url> \u4F7F\u7528 SearXNG \u81EA\u5B9A\u4E49\u7AEF\u70B9",
3812
4001
  usageMetaso: " /search-engine metaso \u4F7F\u7528 Metaso API\uFF08\u6BCF\u5929 100 \u6B21\u514D\u8D39\uFF0C\u914D\u7F6E\u4F60\u81EA\u5DF1\u7684 API \u5BC6\u94A5\u53EF\u63D0\u5347\u9650\u989D\uFF09",
@@ -3902,7 +4091,9 @@ var zhCN = {
3902
4091
  editorFailed: "\u5916\u90E8\u7F16\u8F91\u5668\uFF1A",
3903
4092
  editorMissing: "\u672A\u8BBE\u7F6E $EDITOR / $VISUAL / $GIT_EDITOR \u2014 \u8BF7\u5BFC\u51FA\u73AF\u5883\u53D8\u91CF\uFF08\u4F8B\u5982 `export EDITOR=nano`\uFF09\u540E\u91CD\u8BD5",
3904
4093
  editorExited: "\u7F16\u8F91\u5668\u5F02\u5E38\u9000\u51FA\uFF0C\u8FD4\u56DE\u7801 {code}",
3905
- typeaheadStaged: "\u25B8 {count} \u884C\u5DF2\u6682\u5B58 \xB7 esc \u53EC\u56DE"
4094
+ typeaheadStaged: "\u25B8 {count} \u884C\u5DF2\u6682\u5B58 \xB7 esc \u53EC\u56DE",
4095
+ steerPlaceholder: "\u8F93\u5165\u6D88\u606F\u4EE5\u5F15\u5BFC\u5F53\u524D\u4EFB\u52A1 \u2014 \u5FD9\u788C\u65F6\u4E0D\u652F\u6301\u547D\u4EE4",
4096
+ steerHint: "\u53D1\u9001 \u2014 \u56DE\u5408\u5185\u6CE8\u5165"
3906
4097
  },
3907
4098
  pathConfirm: {
3908
4099
  title: "\u6C99\u7BB1\u5916\u8DEF\u5F84",
@@ -3921,7 +4112,13 @@ var zhCN = {
3921
4112
  denyDesc: "\u6309 Tab \u6DFB\u52A0\u8BF4\u660E\uFF0C\u544A\u8BC9\u6A21\u578B\u539F\u56E0",
3922
4113
  pathLabel: "\u8DEF\u5F84",
3923
4114
  sandboxLabel: "\u6C99\u7BB1",
3924
- allowPrefixLabel: "\u524D\u7F00"
4115
+ allowPrefixLabel: "\u524D\u7F00",
4116
+ promptTitleRead: "\u8BBF\u95EE\u8DEF\u5F84 \u2014 \u8BFB\u53D6",
4117
+ promptTitleWrite: "\u8BBF\u95EE\u8DEF\u5F84 \u2014 \u5199\u5165",
4118
+ actionAllowRead: "\u5141\u8BB8\u8BFB\u53D6",
4119
+ actionAllowWrite: "\u5141\u8BB8\u5199\u5165",
4120
+ actionAlwaysAllow: "\u59CB\u7EC8\u5141\u8BB8 \u2014 {prefix}",
4121
+ actionDeny: "\u62D2\u7EDD"
3925
4122
  },
3926
4123
  shellConfirm: {
3927
4124
  title: "Shell \u547D\u4EE4",
@@ -3943,7 +4140,12 @@ var zhCN = {
3943
4140
  timeoutLabel: "\u8D85\u65F6",
3944
4141
  waitLabel: "\u7B49\u5F85",
3945
4142
  previewMore: "\u2026 \u8FD8\u6709 {n} \u884C\u672A\u663E\u793A \u2014 \u6309 esc \u53D6\u6D88\uFF0C\u8BA9\u6A21\u578B\u62C6\u5206\u540E\u518D\u8BD5",
3946
- previewMorePlural: "\u2026 \u8FD8\u6709 {n} \u884C\u672A\u663E\u793A \u2014 \u6309 esc \u53D6\u6D88\uFF0C\u8BA9\u6A21\u578B\u62C6\u5206\u540E\u518D\u8BD5"
4143
+ previewMorePlural: "\u2026 \u8FD8\u6709 {n} \u884C\u672A\u663E\u793A \u2014 \u6309 esc \u53D6\u6D88\uFF0C\u8BA9\u6A21\u578B\u62C6\u5206\u540E\u518D\u8BD5",
4144
+ promptTitleRunCommand: "\u8FD0\u884C\u547D\u4EE4",
4145
+ promptTitleRunBackground: "\u8FD0\u884C\u540E\u53F0\u547D\u4EE4",
4146
+ actionRunOnce: "\u8FD0\u884C\u4E00\u6B21",
4147
+ actionAlwaysAllow: "\u59CB\u7EC8\u5141\u8BB8 \u2014 {prefix}",
4148
+ actionDeny: "\u62D2\u7EDD"
3947
4149
  },
3948
4150
  editConfirm: {
3949
4151
  footer: "[y/Enter] \u5E94\u7528 \xB7 [n] \u62D2\u7EDD\u5E76\u8BF4\u660E \xB7 [a] \u5E94\u7528\u5269\u4F59 \xB7 [A] \u5207\u6362 AUTO \xB7 [\u2191\u2193/Space] \u6EDA\u52A8 \xB7 [Esc] \u4E2D\u6B62",
@@ -4084,37 +4286,37 @@ var zhCN = {
4084
4286
  probeFailed: "\u63A2\u6D4B\u5931\u8D25 \u2014 {message}"
4085
4287
  },
4086
4288
  webErrors: {
4087
- status: "web_search {status} \u2014 try: \u641C\u7D22\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF\uFF1B\u8BF7\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4289
+ status: "web_search {status} \u2014 try: \u641C\u7D22\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF\uFF1B\u8BF7\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4088
4290
  rateLimit429: "web_search 429 \u2014 try: \u7B49\u5F85 10 \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u6539\u5199\u67E5\u8BE2\uFF1B\u641C\u7D22\u540E\u7AEF\u6B63\u5728\u5BF9\u8BE5\u5BA2\u6237\u7AEF\u8FDB\u884C\u9650\u6D41",
4089
- forbidden403: "web_search 403 \u2014 try: \u641C\u7D22\u540E\u7AEF\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5",
4291
+ forbidden403: "web_search 403 \u2014 try: \u641C\u7D22\u540E\u7AEF\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5",
4090
4292
  serverError5xx: "web_search {status} \u2014 try: \u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u641C\u7D22 URL\uFF1B\u82E5\u80FD\u52A0\u8F7D\u5219\u5C5E\u4E34\u65F6\u6545\u969C\uFF0C\u7B49 30 \u79D2\u91CD\u8BD5\u5373\u53EF",
4091
- mojeekBlocked: "web_search: Mojeek \u53CD\u722C\u9875\u9762 \u2014 \u9891\u7387\u9650\u5236\u6216\u88AB\u5C4F\u853D \u2014 try: \u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4092
- mojeekNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46\u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF0C\u524D 120 \u5B57\u7B26\uFF1A{preview}\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4293
+ bingBlocked: "web_search: Bing \u53CD\u722C\u9875\u9762 \u2014 \u9891\u7387\u9650\u5236\u6216\u88AB\u5C4F\u853D \u2014 try: \u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4294
+ bingNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46\u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF0C\u524D 120 \u5B57\u7B26\uFF1A{preview}\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4093
4295
  invalidEndpoint: 'web_search: \u65E0\u6548\u7684 SearXNG \u7AEF\u70B9 "{endpoint}" \u2014 try: \u4F7F\u7528 /search-endpoint http://host:port \u8BBE\u7F6E\u6709\u6548\u7684 URL',
4094
4296
  endpointMustBeHttp: "web_search: SearXNG \u7AEF\u70B9\u5FC5\u987B\u662F http(s) \u534F\u8BAE\uFF0C\u5F53\u524D\u4E3A {protocol} \u2014 try: \u4F7F\u7528 /search-endpoint http://host:port \u8BBE\u7F6E\u6709\u6548\u7684 URL",
4095
- cannotReach: "web_search: \u65E0\u6CD5\u8BBF\u95EE SearXNG \u670D\u52A1\u5668 {endpoint} \u2014 try: \u5B89\u88C5\u5E76\u542F\u52A8 SearXNG\uFF08https://github.com/searxng/searxng\uFF0C\u4F8B\u5982 `docker run -d -p 8080:8080 searxng/searxng`\uFF09\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4096
- searxngNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46 SearXNG \u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4297
+ cannotReach: "web_search: \u65E0\u6CD5\u8BBF\u95EE SearXNG \u670D\u52A1\u5668 {endpoint} \u2014 try: \u5B89\u88C5\u5E76\u542F\u52A8 SearXNG\uFF08https://github.com/searxng/searxng\uFF0C\u4F8B\u5982 `docker run -d -p 8080:8080 searxng/searxng`\uFF09\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4298
+ searxngNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46 SearXNG \u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4097
4299
  metasoMissingKey: "web_search: Metaso \u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E METASO_API_KEY\uFF0C\u6216\u4F7F\u7528 /search-engine metaso <key> \u914D\u7F6E\uFF1B\u53EF\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u5BC6\u94A5",
4098
4300
  metasoDailyLimit: "web_search: Metaso \u6BCF\u65E5\u641C\u7D22\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 \u2014 \u8BBE\u7F6E METASO_API_KEY\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u5BC6\u94A5",
4099
4301
  metasoUnauthorized: "web_search: Metaso API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 METASO_API_KEY\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u5BC6\u94A5",
4100
4302
  metasoRateLimit: "web_search: Metaso \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u81EA\u5DF1\u7684\u5BC6\u94A5",
4101
- metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4303
+ metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4102
4304
  metasoParseError: "web_search: Metaso \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4103
4305
  metasoApiError: "web_search: Metaso API \u9519\u8BEF\uFF08code {code}: {message}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4104
4306
  tavilyMissingKey: "web_search: Tavily \u540E\u7AEF\u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E TAVILY_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 ~/.reasonix/config.json \u4E2D\u914D\u7F6E `tavilyApiKey`\uFF1Bhttps://tavily.com \u6BCF\u6708 1000 \u6B21\u514D\u8D39",
4105
4307
  tavilyUnauthorized: "web_search: Tavily API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 TAVILY_API_KEY\uFF0C\u6216\u5728 https://tavily.com \u83B7\u53D6\u5BC6\u94A5",
4106
- tavilyRateLimit: "web_search: Tavily \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u3001\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
4107
- tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4308
+ tavilyRateLimit: "web_search: Tavily \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u3001\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
4309
+ tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4108
4310
  tavilyParseError: "web_search: Tavily \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4109
4311
  perplexityMissingKey: "web_search: Perplexity \u540E\u7AEF\u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E PERPLEXITY_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 ~/.reasonix/config.json \u4E2D\u914D\u7F6E `perplexityApiKey`\uFF1B\u5728 https://perplexity.ai/settings/api \u83B7\u53D6\u5BC6\u94A5",
4110
4312
  perplexityUnauthorized: "web_search: Perplexity API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 PERPLEXITY_API_KEY\uFF0C\u6216\u5728 https://perplexity.ai/settings/api \u83B7\u53D6\u5BC6\u94A5",
4111
- perplexityRateLimit: "web_search: Perplexity \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4112
- perplexityServerError: "web_search: Perplexity \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4313
+ perplexityRateLimit: "web_search: Perplexity \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4314
+ perplexityServerError: "web_search: Perplexity \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4113
4315
  perplexityParseError: "web_search: Perplexity \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4114
4316
  exaMissingKey: "web_search: Exa \u540E\u7AEF\u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E EXA_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 ~/.reasonix/config.json \u4E2D\u914D\u7F6E `exaApiKey`\uFF1Bhttps://exa.ai \u6BCF\u6708 1000 \u6B21\u514D\u8D39",
4115
4317
  exaUnauthorized: "web_search: Exa API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 EXA_API_KEY\uFF0C\u6216\u5728 https://exa.ai \u83B7\u53D6\u5BC6\u94A5",
4116
4318
  exaRateLimit: "web_search: Exa \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u5347\u7EA7\uFF0C\u6216\u5728 https://exa.ai/pricing \u67E5\u770B\u8BA1\u5212",
4117
- exaServerError: "web_search: Exa \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4319
+ exaServerError: "web_search: Exa \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4118
4320
  exaParseError: "web_search: Exa \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4119
4321
  fetchStatus: "web_fetch {status} for {url} \u2014 try: \u5728\u6D4F\u89C8\u5668\u4E2D\u786E\u8BA4\u8BE5 URL \u80FD\u5426\u8BBF\u95EE\uFF1B\u8BE5\u72B6\u6001\u7801\u8868\u660E\u76EE\u6807\u4E3B\u673A\u8FD4\u56DE\u4E86\u9519\u8BEF\u9875\u9762",
4120
4322
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: \u7B49\u5F85 10 \u79D2\u540E\u91CD\u8BD5\uFF1B\u76EE\u6807\u4E3B\u673A\u6B63\u5728\u5BF9\u8BE5\u5BA2\u6237\u7AEF\u8FDB\u884C\u9650\u6D41",
@@ -4206,19 +4408,6 @@ var zhCN = {
4206
4408
  categoryProject: "\u9879\u76EE",
4207
4409
  categoryReference: "\u53C2\u8003"
4208
4410
  },
4209
- copyMode: {
4210
- title: "\u2500\u2500 \u590D\u5236\u6A21\u5F0F \u2500\u2500",
4211
- help: "j/k \u6216 \u2191/\u2193 \u79FB\u52A8 \xB7 v \u8D77\u9009\u533A \xB7 y \u590D\u5236 \xB7 g/G \u9876/\u5E95 \xB7 q \u9000\u51FA",
4212
- statusBar: "\u7B2C {cur}/{total} \u884C \xB7 \u9009\u533A\uFF1A{sel}",
4213
- statusYanked: "\u5DF2\u590D\u5236 {size} \u5B57\u7B26\uFF08osc52={osc52}\uFF09",
4214
- statusEmpty: "\u672A\u9009\u4E2D\u5185\u5BB9",
4215
- empty: "\uFF08\u8FD8\u6CA1\u6709\u804A\u5929\u5185\u5BB9 \u2014 \u5148\u548C\u6A21\u578B\u8BF4\u70B9\u4EC0\u4E48\uFF09",
4216
- labelUser: "\u4F60",
4217
- labelAssistant: "\u52A9\u624B",
4218
- labelReasoning: "\u63A8\u7406",
4219
- yankedToast: "\u25B8 \u5DF2\u590D\u5236 {size} \u5B57\u7B26\u5230\u526A\u8D34\u677F (osc52)",
4220
- yankedToastFile: "\u25B8 \u5DF2\u590D\u5236 {size} \u5B57\u7B26 \xB7 \u6587\u4EF6\uFF1A{path}"
4221
- },
4222
4411
  mcpHealth: {
4223
4412
  noData: "\u65E0\u68C0\u67E5\u6570\u636E",
4224
4413
  healthy: "\u6B63\u5E38 \xB7 {ms}ms",
@@ -4370,9 +4559,11 @@ var zhCN = {
4370
4559
  groupSystem: "\u7CFB\u7EDF",
4371
4560
  descEnter: "\u53D1\u9001\u6D88\u606F",
4372
4561
  descShiftEnter: "\u6362\u884C",
4562
+ descCtrlEnter: "\u6362\u884C",
4563
+ descCtrlJ: "\u6362\u884C",
4373
4564
  descCtrlU: "\u6E05\u7A7A\u8F93\u5165",
4374
4565
  descCtrlW: "\u5220\u9664\u5355\u8BCD",
4375
- descCtrlP: "\u6253\u5F00/\u5173\u95ED\u5FEB\u6377\u952E\u9762\u677F",
4566
+ descCtrlP: "\u663E\u793A/\u9690\u85CF\u5FEB\u6377\u952E",
4376
4567
  descCtrlX: "\u5728\u7F16\u8F91\u5668\u4E2D\u6253\u5F00",
4377
4568
  descArrows: "\u6D4F\u89C8\u8F93\u5165\u5386\u53F2",
4378
4569
  descPgUpDown: "\u7FFB\u9875",
@@ -4386,9 +4577,30 @@ var zhCN = {
4386
4577
  descCtrlC: "\u9000\u51FA",
4387
4578
  descEsc: "\u505C\u6B62/\u53D6\u6D88",
4388
4579
  descCtrlR: "\u5207\u6362\u8BE6\u7EC6\u6A21\u5F0F",
4389
- descCtrlO: "\u5C55\u5F00\u6D41\u5F0F\u8F93\u51FA",
4580
+ descCtrlO: "\u5C55\u5F00\u56DE\u590D\uFF08\u4EC5\u6D41\u5F0F\u8F93\u51FA\u671F\u95F4\uFF09",
4390
4581
  descHelp: "\u663E\u793A\u6240\u6709\u547D\u4EE4",
4391
4582
  descShiftTab: "\u5207\u6362\u7F16\u8F91\u6A21\u5F0F"
4583
+ },
4584
+ mcpCli: {
4585
+ bundledCatalog: "\u5DF2\u6253\u5305\u7684 MCP \u670D\u52A1\u5668\uFF08\u79BB\u7EBF\u76EE\u5F55\uFF09\uFF1A",
4586
+ justFetched: "\u521A\u521A\u83B7\u53D6",
4587
+ cachedAge: "\u7F13\u5B58\uFF0C{age}",
4588
+ moreAvailable: "\u8FD8\u6709\u66F4\u591A",
4589
+ allLoaded: "\u5DF2\u5168\u90E8\u52A0\u8F7D",
4590
+ morePagesAvailable: "\u25B8 \u8FD8\u6709\u66F4\u591A\u9875\u53EF\u7528 \u2014 `reasonix mcp list --pages <n>` \u6216 --all",
4591
+ installHint: "\u5B89\u88C5\uFF1Areasonix mcp install <name>",
4592
+ usageSearch: "\u7528\u6CD5\uFF1Areasonix mcp search <query>",
4593
+ usageInstall: "\u7528\u6CD5\uFF1Areasonix mcp install <name>",
4594
+ noMatchesFor: '\u672A\u627E\u5230 "{q}" \u7684\u5339\u914D\u9879\uFF08\u5DF2\u68C0\u7D22 {count} \u6761\u8BB0\u5F55\uFF0C\u6765\u6E90\uFF1A{source}\uFF09',
4595
+ matchCount: '\u5728 {source} \u4E2D\u627E\u5230 {count} \u6761 "{q}" \u7684\u5339\u914D\u9879\uFF08\u5DF2\u626B\u63CF {loaded} \u6761\u8BB0\u5F55\uFF09\uFF1A',
4596
+ moreLoaded: "\u2026 \u8FD8\u6709 {count} \u6761\u5DF2\u52A0\u8F7D \u2014 \u4F7F\u7528 `reasonix mcp search <query>` \u7B5B\u9009",
4597
+ moreMatches: "\u2026 \u8FD8\u6709 {count} \u6761\u5339\u914D\u9879",
4598
+ installed: "\u5DF2\u5B89\u88C5\uFF1A{spec}",
4599
+ noServerFound: '\u5728 {source} \u4E2D\u904D\u5386\u4E86 {pages} \u9875\u540E\u672A\u627E\u5230\u540D\u4E3A "{target}" \u7684 MCP \u670D\u52A1\u5668\u3002',
4600
+ noServerTryMore: "\u8BD5\u8BD5\uFF1Areasonix mcp install {target} --max-pages 100",
4601
+ noInstallMeta: '\u65E0\u6CD5\u4E3A "{name}" \u83B7\u53D6\u5B89\u88C5\u5143\u6570\u636E \u2014 \u8BD5\u8BD5 `npx -y @smithery/cli install {name}`\u3002',
4602
+ buildSpecFailed: "\u65E0\u6CD5\u4E3A {name} \u6784\u5EFA\u5B89\u88C5 spec\uFF1A{message}",
4603
+ alreadyInstalled: "\u5DF2\u5B89\u88C5\uFF1A{spec}"
4392
4604
  }
4393
4605
  };
4394
4606
 
@@ -4498,7 +4710,7 @@ function matchesTool(hook, toolName) {
4498
4710
  }
4499
4711
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
4500
4712
  function defaultSpawner(input) {
4501
- return new Promise((resolve13) => {
4713
+ return new Promise((resolve14) => {
4502
4714
  const child = spawn(input.command, {
4503
4715
  cwd: input.cwd,
4504
4716
  shell: true,
@@ -4543,7 +4755,7 @@ function defaultSpawner(input) {
4543
4755
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
4544
4756
  child.once("error", (err) => {
4545
4757
  clearTimeout(timer);
4546
- resolve13({
4758
+ resolve14({
4547
4759
  exitCode: null,
4548
4760
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
4549
4761
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -4554,7 +4766,7 @@ function defaultSpawner(input) {
4554
4766
  });
4555
4767
  child.once("close", (code) => {
4556
4768
  clearTimeout(timer);
4557
- resolve13({
4769
+ resolve14({
4558
4770
  exitCode: code,
4559
4771
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
4560
4772
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -5076,12 +5288,14 @@ var ToolRegistry = class {
5076
5288
  _interceptors = [];
5077
5289
  _auditListener = null;
5078
5290
  _resultAugmenter = null;
5291
+ _rateLimiter;
5079
5292
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
5080
5293
  _lastMalformed = /* @__PURE__ */ new Map();
5081
5294
  /** Per-tool fingerprint of the last host-side gate rejection. */
5082
5295
  _lastGateRejection = /* @__PURE__ */ new Map();
5083
5296
  constructor(opts = {}) {
5084
5297
  this._autoFlatten = opts.autoFlatten !== false;
5298
+ this._rateLimiter = new ToolRateLimiter(opts.rateLimit);
5085
5299
  }
5086
5300
  /** Enable / disable plan-mode enforcement at dispatch. */
5087
5301
  setPlanMode(on) {
@@ -5118,6 +5332,9 @@ var ToolRegistry = class {
5118
5332
  get hasResultAugmenter() {
5119
5333
  return this._resultAugmenter !== null;
5120
5334
  }
5335
+ get rateLimitPolicy() {
5336
+ return this._rateLimiter.policy;
5337
+ }
5121
5338
  register(def) {
5122
5339
  if (!def.name) throw new Error("tool requires a name");
5123
5340
  const internal = { ...def };
@@ -5216,6 +5433,10 @@ var ToolRegistry = class {
5216
5433
  rejectedReason: "aborted"
5217
5434
  });
5218
5435
  }
5436
+ const rateLimit = this._rateLimiter.consume(name);
5437
+ if (!rateLimit.allowed) {
5438
+ return JSON.stringify(rateLimit.result);
5439
+ }
5219
5440
  let finalResult;
5220
5441
  try {
5221
5442
  try {
@@ -5224,7 +5445,8 @@ var ToolRegistry = class {
5224
5445
  }
5225
5446
  const result = await tool.fn(args, {
5226
5447
  signal: opts.signal,
5227
- confirmationGate: opts.confirmationGate
5448
+ confirmationGate: opts.confirmationGate,
5449
+ readTracker: opts.readTracker
5228
5450
  });
5229
5451
  const str = typeof result === "string" ? result : JSON.stringify(result);
5230
5452
  let clipped = str;
@@ -5309,6 +5531,9 @@ function plainTextRejectedReason(name, result) {
5309
5531
  if ((name === "edit_file" || name === "write_file") && /rejected this edit/i.test(result)) {
5310
5532
  return "edit-gate";
5311
5533
  }
5534
+ if ((name === "edit_file" || name === "multi_edit") && /read_file first/i.test(result)) {
5535
+ return "read-before-edit";
5536
+ }
5312
5537
  if ((name === "run_command" || name === "run_background") && /\buser denied:/i.test(result)) {
5313
5538
  return "shell-gate";
5314
5539
  }
@@ -5318,6 +5543,8 @@ function rejectionRecoveryHint(reason) {
5318
5543
  switch (reason) {
5319
5544
  case "edit-gate":
5320
5545
  return "Do not re-emit the same edit. Try a genuinely different edit or ask the user how to proceed.";
5546
+ case "read-before-edit":
5547
+ return "Call read_file on the target path first, then re-issue the edit.";
5321
5548
  case "shell-gate":
5322
5549
  return "Do not retry the same command. Use an allowlisted/read-only command, wait for approval, or ask the user how to proceed.";
5323
5550
  case "engineering-lifecycle":
@@ -5446,12 +5673,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
5446
5673
  let timer;
5447
5674
  let onAbort;
5448
5675
  try {
5449
- await new Promise((resolve13, reject) => {
5676
+ await new Promise((resolve14, reject) => {
5450
5677
  ready.then(
5451
5678
  () => {
5452
5679
  if (settled) return;
5453
5680
  settled = true;
5454
- resolve13();
5681
+ resolve14();
5455
5682
  },
5456
5683
  (err) => {
5457
5684
  if (settled) return;
@@ -5668,14 +5895,14 @@ function buildSyntheticAssistantMessage(content, fallbackModel) {
5668
5895
  // src/memory/session.ts
5669
5896
  import { execFileSync } from "child_process";
5670
5897
  import {
5671
- appendFileSync,
5898
+ appendFileSync as appendFileSync2,
5672
5899
  chmodSync as chmodSync2,
5673
5900
  copyFileSync,
5674
5901
  existsSync as existsSync3,
5675
5902
  mkdirSync as mkdirSync2,
5676
5903
  readFileSync as readFileSync4,
5677
5904
  readdirSync,
5678
- renameSync,
5905
+ renameSync as renameSync2,
5679
5906
  statSync,
5680
5907
  unlinkSync,
5681
5908
  writeFileSync as writeFileSync2
@@ -5732,7 +5959,7 @@ function readSessionMessages(path2) {
5732
5959
  function appendSessionMessage(name, message) {
5733
5960
  const path2 = sessionPath(name);
5734
5961
  mkdirSync2(dirname3(path2), { recursive: true });
5735
- appendFileSync(path2, `${JSON.stringify(message)}
5962
+ appendFileSync2(path2, `${JSON.stringify(message)}
5736
5963
  `, "utf8");
5737
5964
  try {
5738
5965
  chmodSync2(path2, 384);
@@ -5791,13 +6018,13 @@ function renameSession(oldName, newName) {
5791
6018
  const oldJsonl = sessionPath(oldName);
5792
6019
  const newJsonl = sessionPath(newName);
5793
6020
  if (!existsSync3(oldJsonl) || existsSync3(newJsonl)) return false;
5794
- renameSync(oldJsonl, newJsonl);
6021
+ renameSync2(oldJsonl, newJsonl);
5795
6022
  for (const ext of SESSION_SIDECAR_EXTS) {
5796
6023
  const oldP = oldJsonl.replace(/\.jsonl$/, ext);
5797
6024
  const newP = newJsonl.replace(/\.jsonl$/, ext);
5798
6025
  if (existsSync3(oldP)) {
5799
6026
  try {
5800
- renameSync(oldP, newP);
6027
+ renameSync2(oldP, newP);
5801
6028
  } catch {
5802
6029
  }
5803
6030
  }
@@ -5834,7 +6061,7 @@ function rewriteSession(name, messages) {
5834
6061
  copyFileSync(path2, backup);
5835
6062
  chmodPrivate(backup);
5836
6063
  }
5837
- renameSync(tmp, path2);
6064
+ renameSync2(tmp, path2);
5838
6065
  chmodPrivate(path2);
5839
6066
  } catch (err) {
5840
6067
  try {
@@ -6026,6 +6253,10 @@ function round(n, digits) {
6026
6253
  }
6027
6254
 
6028
6255
  // src/context-manager.ts
6256
+ function extractPinnedConstraints(systemPrompt) {
6257
+ const pattern = /# (?:HIGH PRIORITY constraints|User memory|Project memory)[\s\S]*?(?=\n# |\n---|$)/g;
6258
+ return Array.from(systemPrompt.matchAll(pattern), (m) => m[0]).join("\n\n");
6259
+ }
6029
6260
  var HISTORY_FOLD_THRESHOLD = 0.75;
6030
6261
  var HISTORY_FOLD_TAIL_FRACTION = 0.2;
6031
6262
  var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.78;
@@ -6040,20 +6271,25 @@ var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
6040
6271
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
6041
6272
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
6042
6273
  var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
6043
- function extractPinnedSkills(head) {
6274
+ function buildFoldSummaryInstruction(pinnedSkillNames) {
6275
+ const base = "Summarize the conversation above as one self-contained prose recap. Preserve the user's ORIGINAL OBJECTIVE (never paraphrase away negative constraints like 'do NOT do X'), all 'do not' / 'never' / 'avoid' instructions, decisions reached, files inspected or modified, tool results still relevant, and any open todos. Skip turn-by-turn play-by-play. Output plain prose only \u2014 no tool calls, no markdown headings, no SEARCH/REPLACE blocks.";
6276
+ if (pinnedSkillNames.length === 0) return base;
6277
+ const list = pinnedSkillNames.map((n) => `"${n}"`).join(", ");
6278
+ return `${base} The following skill memos are pinned verbatim and appended after your summary \u2014 do NOT quote or paraphrase their bodies: ${list}.`;
6279
+ }
6280
+ function collectPinnedSkills(head) {
6044
6281
  const pinned = /* @__PURE__ */ new Map();
6045
- const stubbedHead = head.map((msg) => {
6046
- if (typeof msg.content !== "string") return msg;
6047
- let hit = false;
6048
- const next = msg.content.replace(SKILL_PIN_REGEX, (full, name) => {
6282
+ for (const msg of head) {
6283
+ if (typeof msg.content !== "string") continue;
6284
+ SKILL_PIN_REGEX.lastIndex = 0;
6285
+ for (const match of msg.content.matchAll(SKILL_PIN_REGEX)) {
6286
+ const name = match[1];
6287
+ const full = match[0];
6049
6288
  pinned.delete(name);
6050
6289
  pinned.set(name, full);
6051
- hit = true;
6052
- return `[skill ${JSON.stringify(name)} memo \u2014 preserved separately, do not summarize.]`;
6053
- });
6054
- return hit ? { ...msg, content: next } : msg;
6055
- });
6056
- return { stubbedHead, pinnedBodies: [...pinned.values()] };
6290
+ }
6291
+ }
6292
+ return { names: [...pinned.keys()], bodies: [...pinned.values()] };
6057
6293
  }
6058
6294
  var ContextManager = class {
6059
6295
  constructor(deps) {
@@ -6149,16 +6385,22 @@ var ContextManager = class {
6149
6385
  const tail = all.slice(boundary);
6150
6386
  const headTokens = totalTokens - cumTokens;
6151
6387
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
6152
- const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
6153
- const summary = await this.summarizeForFold(stubbedHead);
6388
+ const { names: pinnedNames, bodies: pinnedBodies } = collectPinnedSkills(head);
6389
+ const summary = await this.summarizeForFold(head, pinnedNames);
6154
6390
  if (!summary.content) return noop;
6155
6391
  const memoTail = pinnedBodies.length > 0 ? `
6156
6392
 
6157
6393
  ${SKILL_PIN_MEMO_HEADER}
6158
6394
 
6159
6395
  ${pinnedBodies.join("\n\n")}` : "";
6396
+ const constraints = extractPinnedConstraints(this.deps.getSystemPrompt());
6397
+ const constraintTail = constraints ? `
6398
+
6399
+ [PINNED CONSTRAINTS \u2014 preserved verbatim]
6400
+
6401
+ ${constraints}` : "";
6160
6402
  const summaryMsg = buildAssistantMessage(
6161
- HISTORY_FOLD_MARKER + summary.content + memoTail,
6403
+ HISTORY_FOLD_MARKER + summary.content + memoTail + constraintTail,
6162
6404
  [],
6163
6405
  model,
6164
6406
  summary.reasoningContent
@@ -6166,6 +6408,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6166
6408
  const replacement = [summaryMsg, ...tail];
6167
6409
  this.deps.log.compactInPlace(replacement);
6168
6410
  this.persistRewrite(replacement);
6411
+ this.deps.onLogRewrite?.();
6169
6412
  return {
6170
6413
  folded: true,
6171
6414
  beforeMessages: all.length,
@@ -6217,6 +6460,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6217
6460
  if (replacement.length === all.length) return noop;
6218
6461
  this.deps.log.compactInPlace(replacement);
6219
6462
  this.persistRewrite(replacement);
6463
+ this.deps.onLogRewrite?.();
6220
6464
  return {
6221
6465
  folded: true,
6222
6466
  beforeMessages: all.length,
@@ -6235,17 +6479,18 @@ ${pinnedBodies.join("\n\n")}` : "";
6235
6479
  this.persistRewrite([...kept]);
6236
6480
  return true;
6237
6481
  }
6238
- async summarizeForFold(messagesToSummarize) {
6482
+ async summarizeForFold(messagesToSummarize, pinnedSkillNames) {
6239
6483
  const summaryModel = "deepseek-v4-flash";
6240
- const systemPrompt = "You compress conversation history for a coding agent. Output one prose recap that preserves: the user's overall goal, decisions and conclusions reached, files inspected or modified, important tool results still relevant to ongoing work, and any open todos. Skip turn-by-turn play-by-play. No tool calls, no markdown headings, no SEARCH/REPLACE blocks \u2014 plain prose only.";
6241
6484
  const healed = healLoadedMessages(messagesToSummarize, DEFAULT_MAX_RESULT_CHARS).messages;
6485
+ const agentSystem = this.deps.getSystemPrompt();
6486
+ const fewShots = this.deps.getFewShots?.() ?? [];
6487
+ const tools = this.deps.getToolSpecs?.() ?? [];
6488
+ const instruction = buildFoldSummaryInstruction(pinnedSkillNames);
6242
6489
  const messages = [
6243
- { role: "system", content: systemPrompt },
6490
+ { role: "system", content: agentSystem },
6491
+ ...fewShots.map((m) => ({ ...m })),
6244
6492
  ...healed,
6245
- {
6246
- role: "user",
6247
- content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
6248
- }
6493
+ { role: "user", content: instruction }
6249
6494
  ];
6250
6495
  const turnSignal = this.deps.getAbortSignal();
6251
6496
  const foldCtrl = new AbortController();
@@ -6275,9 +6520,9 @@ ${pinnedBodies.join("\n\n")}` : "";
6275
6520
  this.deps.client.chat({
6276
6521
  model: summaryModel,
6277
6522
  messages,
6523
+ tools: tools.length ? tools : void 0,
6278
6524
  signal: foldCtrl.signal,
6279
- thinking: thinkingModeForModel(summaryModel),
6280
- reasoningEffort: "high"
6525
+ thinking: "disabled"
6281
6526
  }),
6282
6527
  abortPromise,
6283
6528
  timeoutPromise
@@ -6362,6 +6607,7 @@ function formatLoopError(err, probe) {
6362
6607
  if (status === "402") return t("errors.balance402", { inner });
6363
6608
  if (status === "422") return t("errors.badparam422", { inner });
6364
6609
  if (status === "400") return t("errors.badrequest400", { inner });
6610
+ if (status === "429") return t("errors.concurrency429", { inner });
6365
6611
  if (is5xxStatus(status)) return formatDeepSeek5xx(status, probe);
6366
6612
  return msg;
6367
6613
  }
@@ -7052,8 +7298,34 @@ function signature(call) {
7052
7298
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
7053
7299
  }
7054
7300
 
7301
+ // src/tools/read-tracker.ts
7302
+ import * as pathMod from "path";
7303
+ var ReadTracker = class _ReadTracker {
7304
+ _seen = /* @__PURE__ */ new Set();
7305
+ static norm(abs) {
7306
+ const resolved = pathMod.resolve(abs);
7307
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7308
+ }
7309
+ markRead(abs) {
7310
+ this._seen.add(_ReadTracker.norm(abs));
7311
+ }
7312
+ hasRead(abs) {
7313
+ return this._seen.has(_ReadTracker.norm(abs));
7314
+ }
7315
+ reset() {
7316
+ this._seen.clear();
7317
+ }
7318
+ get size() {
7319
+ return this._seen.size;
7320
+ }
7321
+ };
7322
+
7055
7323
  // src/loop.ts
7056
7324
  var ESCALATION_MODEL = "deepseek-v4-pro";
7325
+ var MID_TURN_STEER_WRAPPER = "[Mid-turn steer queued by the user. Do not treat this as a new task; use it only as additional guidance for the current task after completing the current step.]";
7326
+ function formatSteerUserMessage(content) {
7327
+ return [MID_TURN_STEER_WRAPPER, content].join("\n");
7328
+ }
7057
7329
  var CacheFirstLoop = class {
7058
7330
  client;
7059
7331
  prefix;
@@ -7062,6 +7334,8 @@ var CacheFirstLoop = class {
7062
7334
  scratch = new VolatileScratch();
7063
7335
  stats = new SessionStats();
7064
7336
  repair;
7337
+ /** Files the model has read this session; gates edit_file / multi_edit so SEARCH text matches on-disk bytes. Cleared on fold / mechanical truncate (the model's byte-level view of the elided history is gone). In-memory only — naturally empty on resume. */
7338
+ readTracker = new ReadTracker();
7065
7339
  // Mutable via configure() — slash commands in the TUI / library callers tweak
7066
7340
  // these mid-session so users don't have to restart.
7067
7341
  model;
@@ -7085,15 +7359,19 @@ var CacheFirstLoop = class {
7085
7359
  _turnAbort = new AbortController();
7086
7360
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
7087
7361
  _inflight = new InflightSet();
7088
- /** Typeahead steer message set by the UI; step() consumes it at the next iter boundary. */
7089
- _steer = null;
7362
+ /** Typeahead steer messages set by the UI; step() consumes one at each iter boundary. */
7363
+ _steerQueue = [];
7090
7364
  /** Set true when a steer was consumed this turn; cleared on next step() entry. */
7091
7365
  _steerConsumed = false;
7092
7366
  /** UI calls this to inject a mid-turn steer message without aborting the current turn.
7093
- * New text resets steerConsumed a fresh steer hasn't been consumed yet. */
7367
+ * New text resets steerConsumed because a fresh steer is queued. */
7094
7368
  steer(text) {
7095
- this._steer = text;
7096
- if (text !== null) this._steerConsumed = false;
7369
+ if (text === null) {
7370
+ this._steerQueue.length = 0;
7371
+ return;
7372
+ }
7373
+ this._steerQueue.push(text);
7374
+ this._steerConsumed = false;
7097
7375
  }
7098
7376
  /** True when a steer was consumed this turn (UI gate to avoid double-submit). */
7099
7377
  get steerConsumed() {
@@ -7179,7 +7457,11 @@ var CacheFirstLoop = class {
7179
7457
  stats: this.stats,
7180
7458
  sessionName: this.sessionName,
7181
7459
  getAbortSignal: () => this._turnAbort.signal,
7182
- getCurrentTurn: () => this._turn
7460
+ getCurrentTurn: () => this._turn,
7461
+ getSystemPrompt: () => this.prefix.system,
7462
+ getToolSpecs: () => this.prefix.toolSpecs,
7463
+ getFewShots: () => this.prefix.fewShots,
7464
+ onLogRewrite: () => this.readTracker.reset()
7183
7465
  });
7184
7466
  }
7185
7467
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -7350,7 +7632,8 @@ ${reason}`
7350
7632
  const result = await this.tools.dispatch(name, args, {
7351
7633
  signal,
7352
7634
  maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
7353
- confirmationGate: this.confirmationGate
7635
+ confirmationGate: this.confirmationGate,
7636
+ readTracker: this.readTracker
7354
7637
  });
7355
7638
  const postReport = await runHooks({
7356
7639
  hooks: this.hooks,
@@ -7378,11 +7661,9 @@ ${reason}`
7378
7661
  return generated;
7379
7662
  }
7380
7663
  _inflightCounter = 0;
7381
- buildMessages(pendingUser) {
7664
+ buildMessages() {
7382
7665
  const healedMessages = this.healActiveLogBeforeSend();
7383
- const msgs = [...this.prefix.toMessages(), ...healedMessages];
7384
- if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
7385
- return msgs;
7666
+ return [...this.prefix.toMessages(), ...healedMessages];
7386
7667
  }
7387
7668
  healActiveLogBeforeSend() {
7388
7669
  const current = this.log.toMessages();
@@ -7463,6 +7744,7 @@ ${reason}`
7463
7744
  cap: this.budgetUsd.toFixed(2)
7464
7745
  })
7465
7746
  };
7747
+ this._steerQueue.length = 0;
7466
7748
  return;
7467
7749
  }
7468
7750
  if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
@@ -7501,8 +7783,8 @@ ${reason}`
7501
7783
  };
7502
7784
  }
7503
7785
  this.appendAndPersist({ role: "user", content: userInput });
7504
- let pendingUser = null;
7505
7786
  const toolSpecs = this.prefix.tools();
7787
+ let rateLimitWarningShown = false;
7506
7788
  for (let iter = 0; ; iter++) {
7507
7789
  if (signal.aborted) {
7508
7790
  try {
@@ -7523,6 +7805,7 @@ ${reason}`
7523
7805
  } finally {
7524
7806
  this._turnAbort = new AbortController();
7525
7807
  }
7808
+ this._steerQueue.length = 0;
7526
7809
  return;
7527
7810
  }
7528
7811
  if (iter > 0) {
@@ -7532,14 +7815,15 @@ ${reason}`
7532
7815
  content: t("loop.toolUploadStatus")
7533
7816
  };
7534
7817
  }
7535
- let messages = this.buildMessages(pendingUser);
7536
- if (this._steer !== null) {
7537
- const steer = this._steer;
7538
- this._steer = null;
7539
- this._steerConsumed = true;
7540
- this.appendAndPersist({ role: "user", content: steer });
7541
- messages = this.buildMessages(pendingUser);
7542
- pendingUser = null;
7818
+ let messages = this.buildMessages();
7819
+ if (this._steerQueue.length > 0) {
7820
+ const steer = this._steerQueue.shift();
7821
+ this._steerConsumed = this._steerQueue.length === 0;
7822
+ this.appendAndPersist({
7823
+ role: "user",
7824
+ content: formatSteerUserMessage(steer)
7825
+ });
7826
+ messages = this.buildMessages();
7543
7827
  yield {
7544
7828
  turn: this._turn,
7545
7829
  role: "steer",
@@ -7556,10 +7840,10 @@ ${reason}`
7556
7840
  content: t("loop.preflightTruncateStatus")
7557
7841
  };
7558
7842
  const result = this.context.mechanicalTruncate(this.model, {
7559
- allowEmpty: pendingUser !== null
7843
+ allowEmpty: false
7560
7844
  });
7561
7845
  if (result.folded) {
7562
- messages = this.buildMessages(pendingUser);
7846
+ messages = this.buildMessages();
7563
7847
  const after = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
7564
7848
  const stillFull = after.needsAction;
7565
7849
  yield {
@@ -7705,6 +7989,7 @@ ${reason}`
7705
7989
  } finally {
7706
7990
  this._turnAbort = new AbortController();
7707
7991
  }
7992
+ this._steerQueue.length = 0;
7708
7993
  return;
7709
7994
  }
7710
7995
  const probe = is5xxError(err) ? await probeDeepSeekReachable(this.client) : void 0;
@@ -7714,6 +7999,7 @@ ${reason}`
7714
7999
  content: "",
7715
8000
  error: formatLoopError(err, probe)
7716
8001
  };
8002
+ this._steerQueue.length = 0;
7717
8003
  return;
7718
8004
  }
7719
8005
  if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && isEscalationRequest(assistantContent)) {
@@ -7794,11 +8080,16 @@ ${reason}`
7794
8080
  };
7795
8081
  }
7796
8082
  if (repairedCalls.length === 0) {
8083
+ if (this._steerQueue.length > 0) {
8084
+ continue;
8085
+ }
7797
8086
  if (allSuppressed) {
7798
8087
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "stuck" });
8088
+ this._steerQueue.length = 0;
7799
8089
  return;
7800
8090
  }
7801
8091
  yield { turn: this._turn, role: "done", content: assistantContent };
8092
+ this._steerQueue.length = 0;
7802
8093
  return;
7803
8094
  }
7804
8095
  const decision = this.context.decideAfterUsage(usage, this.model, this._foldedThisTurn);
@@ -7844,6 +8135,7 @@ ${reason}`
7844
8135
  };
7845
8136
  this.context.trimTrailingToolCalls();
7846
8137
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
8138
+ this._steerQueue.length = 0;
7847
8139
  return;
7848
8140
  }
7849
8141
  const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
@@ -7891,6 +8183,15 @@ ${reason}`
7891
8183
  }
7892
8184
  for (const w of preWarnings) yield w;
7893
8185
  for (const w of postWarnings) yield w;
8186
+ const rateLimited = parseRateLimitedToolResult(result);
8187
+ if (rateLimited && !rateLimitWarningShown) {
8188
+ rateLimitWarningShown = true;
8189
+ yield {
8190
+ turn: this._turn,
8191
+ role: "warning",
8192
+ content: rateLimited.message
8193
+ };
8194
+ }
7894
8195
  this.appendAndPersist({
7895
8196
  role: "tool",
7896
8197
  tool_call_id: call.id ?? "",
@@ -7913,7 +8214,7 @@ ${reason}`
7913
8214
  return {
7914
8215
  client: this.client,
7915
8216
  signal: this._turnAbort.signal,
7916
- buildMessages: () => this.buildMessages(null),
8217
+ buildMessages: () => this.buildMessages(),
7917
8218
  appendAndPersist: (m) => this.appendAndPersist(m),
7918
8219
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
7919
8220
  turn: this._turn
@@ -7938,7 +8239,7 @@ function parsePositiveIntEnv(raw) {
7938
8239
  // src/at-mentions.ts
7939
8240
  import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7940
8241
  import { readdir, stat } from "fs/promises";
7941
- import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
8242
+ import { isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve3 } from "path";
7942
8243
 
7943
8244
  // src/gitignore.ts
7944
8245
  import { readFileSync as readFileSync5 } from "fs";
@@ -7993,7 +8294,7 @@ function listFilesSync(root, opts = {}) {
7993
8294
  function listFilesWithStatsSync(root, opts = {}) {
7994
8295
  const maxResults = Math.max(1, opts.maxResults ?? 2e3);
7995
8296
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
7996
- const rootAbs = resolve2(root);
8297
+ const rootAbs = resolve3(root);
7997
8298
  const respectGi = opts.respectGitignore !== false;
7998
8299
  const out = [];
7999
8300
  const walk2 = (dirAbs, dirRel, layers) => {
@@ -8057,7 +8358,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
8057
8358
  async function walkFilesStream(root, opts) {
8058
8359
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8059
8360
  const respectGi = opts.respectGitignore !== false;
8060
- const rootAbs = resolve2(root);
8361
+ const rootAbs = resolve3(root);
8061
8362
  const progressGap = Math.max(0, opts.progressIntervalMs ?? 100);
8062
8363
  let scanned = 0;
8063
8364
  let halted = false;
@@ -8135,8 +8436,8 @@ async function flushFiles(ents, dirAbs, dirRel, layers, emit) {
8135
8436
  async function listDirectory(root, relDir, opts = {}) {
8136
8437
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8137
8438
  const respectGi = opts.respectGitignore !== false;
8138
- const rootAbs = resolve2(root);
8139
- const dirAbs = resolve2(rootAbs, relDir);
8439
+ const rootAbs = resolve3(root);
8440
+ const dirAbs = resolve3(rootAbs, relDir);
8140
8441
  const rel = relative(rootAbs, dirAbs);
8141
8442
  if (rel.startsWith("..") || isAbsolute2(rel)) return [];
8142
8443
  const layers = [];
@@ -8301,7 +8602,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
8301
8602
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
8302
8603
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
8303
8604
  const fs5 = opts.fs ?? defaultFs;
8304
- const root = resolve2(rootDir);
8605
+ const root = resolve3(rootDir);
8305
8606
  const seen = /* @__PURE__ */ new Map();
8306
8607
  const expansions = [];
8307
8608
  const dirListings = /* @__PURE__ */ new Map();
@@ -8348,7 +8649,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings
8348
8649
  if (isAbsolute2(rawPath)) {
8349
8650
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
8350
8651
  }
8351
- const resolved = resolve2(root, rawPath);
8652
+ const resolved = resolve3(root, rawPath);
8352
8653
  const rel = relative(root, resolved);
8353
8654
  if (rel.startsWith("..") || isAbsolute2(rel)) {
8354
8655
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
@@ -8378,7 +8679,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings
8378
8679
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
8379
8680
  }
8380
8681
  function readSafe(root, rawPath, fs5) {
8381
- const resolved = resolve2(root, rawPath);
8682
+ const resolved = resolve3(root, rawPath);
8382
8683
  try {
8383
8684
  return fs5.read(resolved);
8384
8685
  } catch {
@@ -8488,7 +8789,7 @@ import {
8488
8789
  writeFileSync as writeFileSync4
8489
8790
  } from "fs";
8490
8791
  import { homedir as homedir5 } from "os";
8491
- import { join as join8, resolve as resolve4 } from "path";
8792
+ import { join as join8, resolve as resolve5 } from "path";
8492
8793
 
8493
8794
  // src/frontmatter.ts
8494
8795
  var KEY_RE = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/;
@@ -8549,7 +8850,7 @@ import {
8549
8850
  } from "fs";
8550
8851
  import { accessSync } from "fs";
8551
8852
  import { homedir as homedir4 } from "os";
8552
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
8853
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join7, resolve as resolve4 } from "path";
8553
8854
 
8554
8855
  // src/prompt-fragments.ts
8555
8856
  var TUI_FORMATTING_RULES = `Formatting (rendered in a TUI with a real markdown renderer):
@@ -8601,7 +8902,7 @@ var SkillStore = class {
8601
8902
  disableBuiltins;
8602
8903
  constructor(opts = {}) {
8603
8904
  this.homeDir = opts.homeDir ?? homedir4();
8604
- this.projectRoot = opts.projectRoot ? resolve3(opts.projectRoot) : void 0;
8905
+ this.projectRoot = opts.projectRoot ? resolve4(opts.projectRoot) : void 0;
8605
8906
  const baseDir = this.projectRoot ?? process.cwd();
8606
8907
  this.customSkillPaths = dedupePaths(
8607
8908
  opts.customSkillPaths?.map((p) => resolveCustomSkillPath(p, baseDir, this.homeDir)) ?? []
@@ -8767,7 +9068,7 @@ function dedupePaths(paths) {
8767
9068
  function resolveCustomSkillPath(path2, baseDir, homeDir) {
8768
9069
  const trimmed = path2.trim();
8769
9070
  const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? join7(homeDir, trimmed.slice(2)) : trimmed;
8770
- return resolve3(isAbsolute3(expanded) ? expanded : join7(baseDir, expanded));
9071
+ return resolve4(isAbsolute3(expanded) ? expanded : join7(baseDir, expanded));
8771
9072
  }
8772
9073
  function skillPathStatus(dir) {
8773
9074
  try {
@@ -9040,7 +9341,7 @@ function sanitizeMemoryName(raw) {
9040
9341
  return trimmed;
9041
9342
  }
9042
9343
  function projectHash(rootDir) {
9043
- const abs = resolve4(rootDir);
9344
+ const abs = resolve5(rootDir);
9044
9345
  return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
9045
9346
  }
9046
9347
  function scopeDir(opts) {
@@ -9090,7 +9391,7 @@ var MemoryStore = class {
9090
9391
  projectRoot;
9091
9392
  constructor(opts = {}) {
9092
9393
  this.homeDir = opts.homeDir ?? join8(homedir5(), ".reasonix");
9093
- this.projectRoot = opts.projectRoot ? resolve4(opts.projectRoot) : void 0;
9394
+ this.projectRoot = opts.projectRoot ? resolve5(opts.projectRoot) : void 0;
9094
9395
  }
9095
9396
  /** Directory this store writes `scope` files into, creating it if needed. */
9096
9397
  dir(scope) {
@@ -9346,15 +9647,44 @@ function applyMemoryStack(basePrompt, rootDir, opts = {}) {
9346
9647
 
9347
9648
  // src/tools/filesystem.ts
9348
9649
  import { promises as fs4 } from "fs";
9349
- import * as pathMod5 from "path";
9650
+ import * as pathMod6 from "path";
9350
9651
  import picomatch3 from "picomatch";
9351
9652
 
9653
+ // src/code/file-encoding.ts
9654
+ import { promises as fsp, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
9655
+ import iconv from "iconv-lite";
9656
+ var UTF8_BOM = Buffer.from([239, 187, 191]);
9657
+ function decodeFileBuffer(buf) {
9658
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
9659
+ return { text: buf.subarray(3).toString("utf8"), encoding: "utf8-bom" };
9660
+ }
9661
+ try {
9662
+ return { text: new TextDecoder("utf-8", { fatal: true }).decode(buf), encoding: "utf8" };
9663
+ } catch {
9664
+ }
9665
+ try {
9666
+ return {
9667
+ text: new TextDecoder("gb18030", { fatal: true }).decode(buf),
9668
+ encoding: "gb18030"
9669
+ };
9670
+ } catch {
9671
+ }
9672
+ return { text: buf.toString("utf8"), encoding: "utf8" };
9673
+ }
9674
+ function encodeFile(text, encoding) {
9675
+ if (encoding === "utf8") return Buffer.from(text, "utf8");
9676
+ if (encoding === "utf8-bom") {
9677
+ return Buffer.concat([UTF8_BOM, Buffer.from(text, "utf8")]);
9678
+ }
9679
+ return iconv.encode(text, "gb18030");
9680
+ }
9681
+
9352
9682
  // src/memory/subdir.ts
9353
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
9354
- import { dirname as dirname5, join as join9, relative as relative2, resolve as resolve5 } from "path";
9683
+ import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
9684
+ import { dirname as dirname5, join as join9, relative as relative2, resolve as resolve6 } from "path";
9355
9685
  function findDirMemory(absDir, rootDir) {
9356
- const root = resolve5(rootDir);
9357
- const target = resolve5(absDir);
9686
+ const root = resolve6(rootDir);
9687
+ const target = resolve6(absDir);
9358
9688
  const rel = relative2(root, target);
9359
9689
  if (rel.startsWith("..")) return [];
9360
9690
  const found = [];
@@ -9376,12 +9706,12 @@ function findDirMemory(absDir, rootDir) {
9376
9706
  return found;
9377
9707
  }
9378
9708
  function findSubdirMemoryAncestors(absPath, rootDir) {
9379
- return findDirMemory(dirname5(resolve5(absPath)), rootDir);
9709
+ return findDirMemory(dirname5(resolve6(absPath)), rootDir);
9380
9710
  }
9381
9711
  function readSubdirMemoryContent(path2) {
9382
9712
  let raw;
9383
9713
  try {
9384
- raw = readFileSync10(path2, "utf8");
9714
+ raw = readFileSync11(path2, "utf8");
9385
9715
  } catch {
9386
9716
  return null;
9387
9717
  }
@@ -9399,15 +9729,22 @@ ${content}`;
9399
9729
 
9400
9730
  // src/tools/fs/edit.ts
9401
9731
  import { promises as fs } from "fs";
9402
- import * as pathMod from "path";
9732
+ import * as pathMod2 from "path";
9403
9733
  function displayRel(rootDir, full) {
9404
- return pathMod.relative(rootDir, full).replaceAll("\\", "/");
9734
+ return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
9405
9735
  }
9406
- async function applyEdit(rootDir, abs, args) {
9736
+ var READ_BEFORE_EDIT_MARKER = "read_file first";
9737
+ async function applyEdit(rootDir, abs, args, hasRead) {
9407
9738
  if (args.search.length === 0) {
9408
9739
  throw new Error("edit_file: search cannot be empty");
9409
9740
  }
9410
- const before = await fs.readFile(abs, "utf8");
9741
+ if (hasRead && !hasRead(abs)) {
9742
+ throw new Error(
9743
+ `edit_file: ${displayRel(rootDir, abs)} was not read this session \u2014 ${READ_BEFORE_EDIT_MARKER} so your SEARCH matches the bytes on disk.`
9744
+ );
9745
+ }
9746
+ const beforeBuf = await fs.readFile(abs);
9747
+ const { text: before, encoding } = decodeFileBuffer(beforeBuf);
9411
9748
  const le = before.includes("\r\n") ? "\r\n" : "\n";
9412
9749
  const adaptedSearch = args.search.replace(/\r?\n/g, le);
9413
9750
  const adaptedReplace = args.replace.replace(/\r?\n/g, le);
@@ -9422,7 +9759,7 @@ async function applyEdit(rootDir, abs, args) {
9422
9759
  );
9423
9760
  }
9424
9761
  const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
9425
- await fs.writeFile(abs, after, "utf8");
9762
+ await fs.writeFile(abs, encodeFile(after, encoding));
9426
9763
  const rel = displayRel(rootDir, abs);
9427
9764
  const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
9428
9765
  const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
@@ -9430,7 +9767,7 @@ async function applyEdit(rootDir, abs, args) {
9430
9767
  return `${header}
9431
9768
  ${diff}`;
9432
9769
  }
9433
- async function applyMultiEdit(rootDir, edits) {
9770
+ async function applyMultiEdit(rootDir, edits, hasRead) {
9434
9771
  if (edits.length === 0) {
9435
9772
  throw new Error("multi_edit: edits must contain at least one entry");
9436
9773
  }
@@ -9456,16 +9793,23 @@ async function applyMultiEdit(rootDir, edits) {
9456
9793
  }
9457
9794
  let state = filesByPath.get(e.abs);
9458
9795
  if (!state) {
9796
+ if (hasRead && !hasRead(e.abs)) {
9797
+ throw new Error(
9798
+ `multi_edit: edit #${i + 1} target ${rel} was not read this session \u2014 ${READ_BEFORE_EDIT_MARKER} (no edits applied)`
9799
+ );
9800
+ }
9459
9801
  let before;
9802
+ let encoding;
9460
9803
  try {
9461
- before = await fs.readFile(e.abs, "utf8");
9804
+ const buf = await fs.readFile(e.abs);
9805
+ ({ text: before, encoding } = decodeFileBuffer(buf));
9462
9806
  } catch (err) {
9463
9807
  throw new Error(
9464
9808
  `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
9465
9809
  );
9466
9810
  }
9467
9811
  const le = before.includes("\r\n") ? "\r\n" : "\n";
9468
- state = { before, buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
9812
+ state = { before, buf: before, le, hunks: [], deltaChars: 0, touched: 0, encoding };
9469
9813
  filesByPath.set(e.abs, state);
9470
9814
  }
9471
9815
  const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
@@ -9492,14 +9836,14 @@ ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
9492
9836
  const attempted = [];
9493
9837
  try {
9494
9838
  for (const [abs, state] of filesByPath) {
9495
- attempted.push({ abs, before: state.before });
9496
- await fs.writeFile(abs, state.buf, "utf8");
9839
+ attempted.push({ abs, before: state.before, encoding: state.encoding });
9840
+ await fs.writeFile(abs, encodeFile(state.buf, state.encoding));
9497
9841
  }
9498
9842
  } catch (writeErr) {
9499
9843
  const rollbackFailures = [];
9500
9844
  for (const item of [...attempted].reverse()) {
9501
9845
  try {
9502
- await fs.writeFile(item.abs, item.before, "utf8");
9846
+ await fs.writeFile(item.abs, encodeFile(item.before, item.encoding));
9503
9847
  } catch (restoreErr) {
9504
9848
  rollbackFailures.push(`${displayRel(rootDir, item.abs)}: ${restoreErr.message}`);
9505
9849
  }
@@ -9576,10 +9920,10 @@ function lineDiff(a, b) {
9576
9920
 
9577
9921
  // src/tools/fs/glob.ts
9578
9922
  import { promises as fs2 } from "fs";
9579
- import * as pathMod2 from "path";
9923
+ import * as pathMod3 from "path";
9580
9924
  import picomatch2 from "picomatch";
9581
9925
  function displayRel2(rootDir, full) {
9582
- return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
9926
+ return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
9583
9927
  }
9584
9928
  async function globFiles(ctx, startAbs, args) {
9585
9929
  if (args.signal?.aborted) {
@@ -9601,7 +9945,7 @@ async function globFiles(ctx, startAbs, args) {
9601
9945
  return;
9602
9946
  }
9603
9947
  for (const e of entries) {
9604
- const full = pathMod2.join(dir, e.name);
9948
+ const full = pathMod3.join(dir, e.name);
9605
9949
  if (e.isDirectory()) {
9606
9950
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
9607
9951
  await walk2(full);
@@ -9638,7 +9982,7 @@ async function globFiles(ctx, startAbs, args) {
9638
9982
  }
9639
9983
 
9640
9984
  // src/tools/fs/outline.ts
9641
- import * as pathMod3 from "path";
9985
+ import * as pathMod4 from "path";
9642
9986
  var OUTLINE_MAX_ENTRIES = 30;
9643
9987
  var OUTLINE_TAIL_KEEP = 5;
9644
9988
  var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
@@ -9681,7 +10025,7 @@ var EXT_TO_LANG = {
9681
10025
  ".text": "txt"
9682
10026
  };
9683
10027
  function extractOutline(filename, lines) {
9684
- const ext = pathMod3.extname(filename).toLowerCase();
10028
+ const ext = pathMod4.extname(filename).toLowerCase();
9685
10029
  const lang = EXT_TO_LANG[ext];
9686
10030
  if (!lang) return [];
9687
10031
  switch (lang) {
@@ -9822,7 +10166,7 @@ function formatOutline(entries) {
9822
10166
 
9823
10167
  // src/tools/fs/search.ts
9824
10168
  import { promises as fs3 } from "fs";
9825
- import * as pathMod4 from "path";
10169
+ import * as pathMod5 from "path";
9826
10170
 
9827
10171
  // src/tools/fs/regex-runner.ts
9828
10172
  import { Worker } from "worker_threads";
@@ -9855,7 +10199,7 @@ var RegexRunner = class {
9855
10199
  this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
9856
10200
  }
9857
10201
  testLines(text, source, flags, opts = {}) {
9858
- return new Promise((resolve13, reject) => {
10202
+ return new Promise((resolve14, reject) => {
9859
10203
  if (opts.signal?.aborted) {
9860
10204
  reject(new Error("regex evaluation aborted"));
9861
10205
  return;
@@ -9868,7 +10212,7 @@ var RegexRunner = class {
9868
10212
  this.killWorker();
9869
10213
  reject(new Error(`regex evaluation exceeded ${timeoutMs}ms`));
9870
10214
  }, timeoutMs);
9871
- const entry = { resolve: resolve13, reject, timer };
10215
+ const entry = { resolve: resolve14, reject, timer };
9872
10216
  if (opts.signal) {
9873
10217
  entry.signal = opts.signal;
9874
10218
  entry.onAbort = () => {
@@ -9951,7 +10295,7 @@ function throwIfAborted(signal) {
9951
10295
  throw new DOMException("search aborted by user", "AbortError");
9952
10296
  }
9953
10297
  function displayRel3(rootDir, full) {
9954
- return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
10298
+ return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
9955
10299
  }
9956
10300
  async function searchFiles(ctx, startAbs, args) {
9957
10301
  throwIfAborted(args.signal);
@@ -9975,7 +10319,7 @@ async function searchFiles(ctx, startAbs, args) {
9975
10319
  }
9976
10320
  for (const e of entries) {
9977
10321
  throwIfAborted(args.signal);
9978
- const full = pathMod4.join(dir, e.name);
10322
+ const full = pathMod5.join(dir, e.name);
9979
10323
  const lower = e.name.toLowerCase();
9980
10324
  const hit = re ? re.test(e.name) : lower.includes(needle);
9981
10325
  if (hit) {
@@ -10067,11 +10411,11 @@ async function searchContent(ctx, startAbs, args) {
10067
10411
  throwIfTimedOut();
10068
10412
  if (e.isDirectory()) {
10069
10413
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
10070
- await walk2(pathMod4.join(dir, e.name));
10414
+ await walk2(pathMod5.join(dir, e.name));
10071
10415
  continue;
10072
10416
  }
10073
10417
  if (!e.isFile()) continue;
10074
- const full = pathMod4.join(dir, e.name);
10418
+ const full = pathMod5.join(dir, e.name);
10075
10419
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel3(ctx.rootDir, full))) continue;
10076
10420
  if (ctx.isBinaryByName(e.name)) continue;
10077
10421
  let fh;
@@ -10189,7 +10533,7 @@ var SKIP_DIR_NAMES = new Set(
10189
10533
  );
10190
10534
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
10191
10535
  function displayRel4(rootDir, full) {
10192
- return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
10536
+ return pathMod6.relative(rootDir, full).replaceAll("\\", "/");
10193
10537
  }
10194
10538
  function looksLikeAbsoluteSystemPath(raw) {
10195
10539
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -10198,8 +10542,8 @@ function looksLikeAbsoluteSystemPath(raw) {
10198
10542
  );
10199
10543
  }
10200
10544
  function pathIsUnder(child, parent) {
10201
- const rel = pathMod5.relative(parent, child);
10202
- return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
10545
+ const rel = pathMod6.relative(parent, child);
10546
+ return rel === "" || !rel.startsWith("..") && !pathMod6.isAbsolute(rel);
10203
10547
  }
10204
10548
  var GLOB_METACHARS = /[*?{[]/;
10205
10549
  function compileNameFilter(filter) {
@@ -10231,11 +10575,11 @@ function formatBytes(n) {
10231
10575
  return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
10232
10576
  }
10233
10577
  function registerFilesystemTools(registry, opts) {
10234
- const rootDir = pathMod5.resolve(opts.rootDir);
10578
+ const rootDir = pathMod6.resolve(opts.rootDir);
10235
10579
  const allowWriting = opts.allowWriting !== false;
10236
10580
  const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
10237
10581
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
10238
- const normRoot = pathMod5.resolve(rootDir);
10582
+ const normRoot = pathMod6.resolve(rootDir);
10239
10583
  const sessionApproved = /* @__PURE__ */ new Set();
10240
10584
  const shownSubdirMemory = /* @__PURE__ */ new Set();
10241
10585
  function withSubdirMemory(absPath, body) {
@@ -10268,7 +10612,7 @@ ${body}`;
10268
10612
  if (pathIsUnder(abs, dir)) return;
10269
10613
  }
10270
10614
  const stat2 = await safeLstat(abs);
10271
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod5.dirname(abs);
10615
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod6.dirname(abs);
10272
10616
  let pending = inflightGate.get(allowPrefix);
10273
10617
  if (!pending) {
10274
10618
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -10296,7 +10640,7 @@ ${body}`;
10296
10640
  throw new Error("path must be a non-empty string");
10297
10641
  }
10298
10642
  if (looksLikeAbsoluteSystemPath(raw)) {
10299
- const abs = pathMod5.resolve(raw);
10643
+ const abs = pathMod6.resolve(raw);
10300
10644
  if (pathIsUnder(abs, normRoot)) return abs;
10301
10645
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
10302
10646
  return abs;
@@ -10306,7 +10650,7 @@ ${body}`;
10306
10650
  normalized = normalized.slice(1);
10307
10651
  }
10308
10652
  if (normalized.length === 0) normalized = ".";
10309
- const resolved = pathMod5.resolve(rootDir, normalized);
10653
+ const resolved = pathMod6.resolve(rootDir, normalized);
10310
10654
  if (!pathIsUnder(resolved, normRoot)) {
10311
10655
  throw new Error(
10312
10656
  `path escapes sandbox root (${normRoot}): ${raw} \u2014 use an absolute system path like /Users/foo or C:\\Users\\foo to request approved outside-sandbox access`
@@ -10368,7 +10712,8 @@ ${body}`;
10368
10712
  if (looksBinary(raw)) {
10369
10713
  return `[refused: ${rel} appears to be binary (${formatBytes(sizeBytes)}) \u2014 read_file returns text only. Use get_file_info for stat.]`;
10370
10714
  }
10371
- const text = raw.toString("utf8");
10715
+ const { text } = decodeFileBuffer(raw);
10716
+ ctx?.readTracker?.markRead(abs);
10372
10717
  let lines = text.split(/\r?\n/);
10373
10718
  if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
10374
10719
  const totalLines = lines.length;
@@ -10506,7 +10851,7 @@ ${slice.join("\n")}`);
10506
10851
  lines.push(line);
10507
10852
  emitted++;
10508
10853
  if (e.isDirectory() && !skip) {
10509
- await walk2(pathMod5.join(dir, e.name), depth + 1);
10854
+ await walk2(pathMod6.join(dir, e.name), depth + 1);
10510
10855
  }
10511
10856
  }
10512
10857
  };
@@ -10666,14 +11011,20 @@ ${slice.join("\n")}`);
10666
11011
  },
10667
11012
  fn: async (args, ctx) => {
10668
11013
  const abs = await safePath(args.path, "write_file", ctx, "write");
10669
- await fs4.mkdir(pathMod5.dirname(abs), { recursive: true });
10670
- await fs4.writeFile(abs, args.content, "utf8");
11014
+ await fs4.mkdir(pathMod6.dirname(abs), { recursive: true });
11015
+ let encoding = "utf8";
11016
+ try {
11017
+ encoding = decodeFileBuffer(await fs4.readFile(abs)).encoding;
11018
+ } catch {
11019
+ }
11020
+ await fs4.writeFile(abs, encodeFile(args.content, encoding));
11021
+ ctx?.readTracker?.markRead(abs);
10671
11022
  return `wrote ${args.content.length} chars to ${displayRel4(rootDir, abs)}`;
10672
11023
  }
10673
11024
  });
10674
11025
  registry.register({
10675
11026
  name: "edit_file",
10676
- description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
11027
+ description: "Apply a SEARCH/REPLACE edit to an existing file. Call `read_file` on this path first this session \u2014 the tool refuses otherwise, since SEARCH must match on-disk bytes exactly. `search` is whitespace-sensitive plain text (no regex) and must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
10677
11028
  parameters: {
10678
11029
  type: "object",
10679
11030
  properties: {
@@ -10683,11 +11034,16 @@ ${slice.join("\n")}`);
10683
11034
  },
10684
11035
  required: ["path", "search", "replace"]
10685
11036
  },
10686
- fn: async (args, ctx) => applyEdit(rootDir, await safePath(args.path, "edit_file", ctx, "write"), args)
11037
+ fn: async (args, ctx) => applyEdit(
11038
+ rootDir,
11039
+ await safePath(args.path, "edit_file", ctx, "write"),
11040
+ args,
11041
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11042
+ )
10687
11043
  });
10688
11044
  registry.register({
10689
11045
  name: "multi_edit",
10690
- description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in one call. Edits validate across the full batch before writing. Validation failures leave all files untouched; disk write failures trigger best-effort rollback of files that may have been modified. Per-file edits run in array order, so a later edit can match text inserted by an earlier one. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
11046
+ description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in one call. Every target file must have been `read_file`'d this session \u2014 the tool refuses the whole batch otherwise. Edits validate across the full batch before writing. Validation failures leave all files untouched; disk write failures trigger best-effort rollback of files that may have been modified. Per-file edits run in array order, so a later edit can match text inserted by an earlier one. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
10691
11047
  parameters: {
10692
11048
  type: "object",
10693
11049
  properties: {
@@ -10721,7 +11077,11 @@ ${slice.join("\n")}`);
10721
11077
  replace: e?.replace
10722
11078
  }))
10723
11079
  );
10724
- return applyMultiEdit(rootDir, resolved);
11080
+ return applyMultiEdit(
11081
+ rootDir,
11082
+ resolved,
11083
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11084
+ );
10725
11085
  }
10726
11086
  });
10727
11087
  registry.register({
@@ -10752,7 +11112,7 @@ ${slice.join("\n")}`);
10752
11112
  fn: async (args, ctx) => {
10753
11113
  const src = await safePath(args.source, "move_file", ctx, "write");
10754
11114
  const dst = await safePath(args.destination, "move_file", ctx, "write");
10755
- await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
11115
+ await fs4.mkdir(pathMod6.dirname(dst), { recursive: true });
10756
11116
  await fs4.rename(src, dst);
10757
11117
  return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
10758
11118
  }
@@ -10820,7 +11180,7 @@ ${slice.join("\n")}`);
10820
11180
  fn: async (args, ctx) => {
10821
11181
  const src = await safePath(args.source, "copy_file", ctx);
10822
11182
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
10823
- await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
11183
+ await fs4.mkdir(pathMod6.dirname(dst), { recursive: true });
10824
11184
  await fs4.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
10825
11185
  return `copied ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
10826
11186
  }
@@ -11920,7 +12280,7 @@ ${hint}` : formatted;
11920
12280
  return parentRegistry;
11921
12281
  }
11922
12282
  function forkRegistryExcluding(parent, exclude) {
11923
- const child = new ToolRegistry();
12283
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11924
12284
  for (const spec of parent.specs()) {
11925
12285
  const name = spec.function.name;
11926
12286
  if (exclude.has(name)) continue;
@@ -11932,7 +12292,7 @@ function forkRegistryExcluding(parent, exclude) {
11932
12292
  return child;
11933
12293
  }
11934
12294
  function forkRegistryWithAllowList(parent, allow, alsoExclude) {
11935
- const child = new ToolRegistry();
12295
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11936
12296
  for (const spec of parent.specs()) {
11937
12297
  const name = spec.function.name;
11938
12298
  if (!allow.has(name)) continue;
@@ -12038,11 +12398,11 @@ var SubagentTelemetry = class {
12038
12398
  };
12039
12399
 
12040
12400
  // src/tools/shell.ts
12041
- import * as pathMod10 from "path";
12401
+ import * as pathMod11 from "path";
12042
12402
 
12043
12403
  // src/tools/jobs.ts
12044
12404
  import { spawn as spawn2 } from "child_process";
12045
- import * as pathMod6 from "path";
12405
+ import * as pathMod7 from "path";
12046
12406
  function killProcessTree(pid, signal) {
12047
12407
  if (process.platform === "win32") {
12048
12408
  const args = ["/pid", String(pid), "/T"];
@@ -12102,7 +12462,7 @@ var JobRegistry = class {
12102
12462
  const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
12103
12463
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
12104
12464
  const spawnOpts = {
12105
- cwd: pathMod6.resolve(opts.cwd),
12465
+ cwd: pathMod7.resolve(opts.cwd),
12106
12466
  shell: false,
12107
12467
  windowsHide: true,
12108
12468
  env: process.env,
@@ -12287,16 +12647,16 @@ ${job.output.slice(start)}`;
12287
12647
  let wakeOutput = null;
12288
12648
  if (waitFor === "output-or-exit") {
12289
12649
  racers.push(
12290
- new Promise((resolve13) => {
12291
- wakeOutput = resolve13;
12292
- job.outputWaiters.add(resolve13);
12650
+ new Promise((resolve14) => {
12651
+ wakeOutput = resolve14;
12652
+ job.outputWaiters.add(resolve14);
12293
12653
  })
12294
12654
  );
12295
12655
  }
12296
12656
  let timer = null;
12297
12657
  racers.push(
12298
- new Promise((resolve13) => {
12299
- timer = setTimeout(resolve13, timeoutMs);
12658
+ new Promise((resolve14) => {
12659
+ timer = setTimeout(resolve14, timeoutMs);
12300
12660
  })
12301
12661
  );
12302
12662
  await Promise.race(racers);
@@ -12406,13 +12766,13 @@ function latestOutputSince(before, after) {
12406
12766
  // src/tools/shell/exec.ts
12407
12767
  import { spawn as spawn4, spawnSync } from "child_process";
12408
12768
  import { existsSync as existsSync9, statSync as statSync5 } from "fs";
12409
- import * as pathMod9 from "path";
12769
+ import * as pathMod10 from "path";
12410
12770
 
12411
12771
  // src/tools/shell-chain.ts
12412
12772
  import { spawn as spawn3 } from "child_process";
12413
- import { closeSync, openSync } from "fs";
12773
+ import { constants as constants2, closeSync, lstatSync, openSync, realpathSync } from "fs";
12414
12774
  import { devNull } from "os";
12415
- import * as pathMod7 from "path";
12775
+ import * as pathMod8 from "path";
12416
12776
  var UnsupportedSyntaxError = class extends Error {
12417
12777
  constructor(detail) {
12418
12778
  super(`run_command: ${detail}`);
@@ -12677,7 +13037,49 @@ function isNullDeviceAlias(target) {
12677
13037
  if (process.platform === "win32" && lower === "nul") return true;
12678
13038
  return false;
12679
13039
  }
13040
+ function pathIsUnder2(child, parent) {
13041
+ const rel = pathMod8.relative(parent, child);
13042
+ return rel === "" || !rel.startsWith("..") && !pathMod8.isAbsolute(rel);
13043
+ }
13044
+ function openFlags(mode) {
13045
+ const noFollow = "O_NOFOLLOW" in constants2 ? constants2.O_NOFOLLOW : 0;
13046
+ if (mode === "r") return constants2.O_RDONLY | noFollow;
13047
+ if (mode === "w") return constants2.O_WRONLY | constants2.O_CREAT | constants2.O_TRUNC | noFollow;
13048
+ return constants2.O_WRONLY | constants2.O_CREAT | constants2.O_APPEND | noFollow;
13049
+ }
13050
+ function ensureUnderSandbox(path2, sandboxRoot, target) {
13051
+ if (!pathIsUnder2(path2, sandboxRoot)) {
13052
+ throw new Error(
13053
+ `redirect target "${target}" resolves outside the workspace sandbox (${sandboxRoot})`
13054
+ );
13055
+ }
13056
+ }
13057
+ function resolveRedirectTarget(target, cwd) {
13058
+ const lexicalRoot = pathMod8.resolve(cwd);
13059
+ const sandboxRoot = realpathSync(lexicalRoot);
13060
+ const resolved = pathMod8.resolve(lexicalRoot, target);
13061
+ ensureUnderSandbox(resolved, lexicalRoot, target);
13062
+ try {
13063
+ const stat2 = lstatSync(resolved);
13064
+ if (stat2.isSymbolicLink()) {
13065
+ throw new Error(`redirect target "${target}" is a symbolic link`);
13066
+ }
13067
+ ensureUnderSandbox(realpathSync(resolved), sandboxRoot, target);
13068
+ } catch (err) {
13069
+ const code = err.code;
13070
+ if (code !== "ENOENT") throw err;
13071
+ ensureUnderSandbox(realpathSync(pathMod8.dirname(resolved)), sandboxRoot, target);
13072
+ }
13073
+ return resolved;
13074
+ }
13075
+ function validateRedirectTargets(redirects, cwd) {
13076
+ for (const r of redirects) {
13077
+ if (r.kind === "2>&1" || !r.target || isNullDeviceAlias(r.target)) continue;
13078
+ resolveRedirectTarget(r.target, cwd);
13079
+ }
13080
+ }
12680
13081
  function openRedirects(redirects, cwd) {
13082
+ validateRedirectTargets(redirects, cwd);
12681
13083
  let stdinFd = null;
12682
13084
  let stdoutFd = null;
12683
13085
  let stderrFd = null;
@@ -12685,8 +13087,8 @@ function openRedirects(redirects, cwd) {
12685
13087
  let bothFd = null;
12686
13088
  const toClose = [];
12687
13089
  const open = (target, flags) => {
12688
- const resolved = isNullDeviceAlias(target) ? devNull : pathMod7.resolve(cwd, target);
12689
- const fd = openSync(resolved, flags);
13090
+ const resolved = isNullDeviceAlias(target) ? devNull : resolveRedirectTarget(target, cwd);
13091
+ const fd = openSync(resolved, openFlags(flags), 438);
12690
13092
  toClose.push(fd);
12691
13093
  return fd;
12692
13094
  };
@@ -12793,9 +13195,9 @@ async function runPipeGroup(segments, opts) {
12793
13195
  }
12794
13196
  const exits = await Promise.all(
12795
13197
  children.map(
12796
- (c) => new Promise((resolve13) => {
12797
- c.once("error", () => resolve13(null));
12798
- c.once("close", (code) => resolve13(code));
13198
+ (c) => new Promise((resolve14) => {
13199
+ c.once("error", () => resolve14(null));
13200
+ c.once("close", (code) => resolve14(code));
12799
13201
  })
12800
13202
  )
12801
13203
  );
@@ -12840,7 +13242,7 @@ var OutputBuffer = class {
12840
13242
 
12841
13243
  // src/tools/shell/parse.ts
12842
13244
  import { homedir as homedir7 } from "os";
12843
- import * as pathMod8 from "path";
13245
+ import * as pathMod9 from "path";
12844
13246
 
12845
13247
  // packages/core-utils/src/tildeify.ts
12846
13248
  import { homedir as homedir6 } from "os";
@@ -13045,16 +13447,16 @@ function resolveSensitivePath(token, projectRoot) {
13045
13447
  return null;
13046
13448
  let expanded = token;
13047
13449
  if (expanded.startsWith("~")) {
13048
- expanded = pathMod8.join(homedir7(), expanded.slice(1));
13450
+ expanded = pathMod9.join(homedir7(), expanded.slice(1));
13049
13451
  }
13050
- return pathMod8.resolve(projectRoot, expanded);
13452
+ return pathMod9.resolve(projectRoot, expanded);
13051
13453
  }
13052
13454
  function expandPrefix(prefix) {
13053
- if (prefix.startsWith("~")) return pathMod8.join(homedir7(), prefix.slice(1));
13054
- return pathMod8.resolve(prefix);
13455
+ if (prefix.startsWith("~")) return pathMod9.join(homedir7(), prefix.slice(1));
13456
+ return pathMod9.resolve(prefix);
13055
13457
  }
13056
13458
  function pathStartsWithPrefix(normalized, prefix) {
13057
- return normalized === prefix || normalized.startsWith(`${prefix}${pathMod8.sep}`);
13459
+ return normalized === prefix || normalized.startsWith(`${prefix}${pathMod9.sep}`);
13058
13460
  }
13059
13461
  function matchesGlob(name, pattern) {
13060
13462
  const regex = new RegExp(
@@ -13069,17 +13471,39 @@ function hasSensitivePathArgs(argv, projectRoot, extraPrefixes = [], extraPatter
13069
13471
  for (const token of argv) {
13070
13472
  const resolved = resolveSensitivePath(token, projectRoot);
13071
13473
  if (!resolved) continue;
13072
- const normalized = pathMod8.normalize(resolved);
13474
+ const normalized = pathMod9.normalize(resolved);
13073
13475
  for (const pfx of prefixes) {
13074
13476
  if (pathStartsWithPrefix(normalized, pfx)) return true;
13075
13477
  }
13076
- const base = pathMod8.basename(normalized);
13478
+ const base = pathMod9.basename(normalized);
13077
13479
  for (const pat of patterns) {
13078
13480
  if (matchesGlob(base, pat)) return true;
13079
13481
  }
13080
13482
  }
13081
13483
  return false;
13082
13484
  }
13485
+ function pathIsUnder3(child, parent) {
13486
+ const rel = pathMod9.relative(parent, child);
13487
+ return rel === "" || !rel.startsWith("..") && !pathMod9.isAbsolute(rel);
13488
+ }
13489
+ function redirectTargets(chain) {
13490
+ const targets = [];
13491
+ for (const seg of chain.segments) {
13492
+ for (const r of seg.redirects) {
13493
+ if (r.kind === "2>&1" || !r.target || isNullDeviceAlias(r.target)) continue;
13494
+ targets.push(r.target);
13495
+ }
13496
+ }
13497
+ return targets;
13498
+ }
13499
+ function redirectsEscapeSandbox(chain, projectRoot) {
13500
+ const root = pathMod9.resolve(projectRoot);
13501
+ for (const target of redirectTargets(chain)) {
13502
+ const resolved = pathMod9.resolve(root, target);
13503
+ if (!pathIsUnder3(resolved, root)) return true;
13504
+ }
13505
+ return false;
13506
+ }
13083
13507
  function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
13084
13508
  let argv;
13085
13509
  try {
@@ -13121,6 +13545,18 @@ function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
13121
13545
  return false;
13122
13546
  }
13123
13547
  if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig);
13548
+ const targets = redirectTargets(chain);
13549
+ if (targets.length > 0 && !projectRoot) return false;
13550
+ if (projectRoot) {
13551
+ if (redirectsEscapeSandbox(chain, projectRoot)) return false;
13552
+ if (hasSensitivePathArgs(
13553
+ targets,
13554
+ projectRoot,
13555
+ sensitivePathConfig?.prefixes,
13556
+ sensitivePathConfig?.patterns
13557
+ ))
13558
+ return false;
13559
+ }
13124
13560
  return chainAllowed(chain, (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig));
13125
13561
  }
13126
13562
 
@@ -13189,7 +13625,7 @@ async function runCommand(cmd, opts) {
13189
13625
  };
13190
13626
  const { bin, args, spawnOverrides } = prepareSpawn(argv, { env: normalizedEnv });
13191
13627
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
13192
- return await new Promise((resolve13, reject) => {
13628
+ return await new Promise((resolve14, reject) => {
13193
13629
  let child;
13194
13630
  try {
13195
13631
  child = spawn4(bin, args, effectiveSpawnOpts);
@@ -13243,7 +13679,7 @@ async function runCommand(cmd, opts) {
13243
13679
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
13244
13680
 
13245
13681
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
13246
- resolve13({ exitCode: code, output, timedOut });
13682
+ resolve14({ exitCode: code, output, timedOut });
13247
13683
  });
13248
13684
  });
13249
13685
  }
@@ -13265,16 +13701,16 @@ function resolveExecutable(cmd, opts = {}) {
13265
13701
  const platform = opts.platform ?? process.platform;
13266
13702
  if (platform !== "win32") return cmd;
13267
13703
  if (!cmd) return cmd;
13268
- if (cmd.includes("/") || cmd.includes("\\") || pathMod9.isAbsolute(cmd)) return cmd;
13269
- if (pathMod9.extname(cmd)) return cmd;
13704
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod10.isAbsolute(cmd)) return cmd;
13705
+ if (pathMod10.extname(cmd)) return cmd;
13270
13706
  const env = opts.env ?? process.env;
13271
13707
  const pathExt = (getEnvCaseInsensitive(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
13272
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod9.delimiter);
13708
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod10.delimiter);
13273
13709
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
13274
13710
  const isFile = opts.isFile ?? defaultIsFile;
13275
13711
  for (const dir of pathDirs) {
13276
13712
  for (const ext of pathExt) {
13277
- const full = pathMod9.win32.join(dir, cmd + ext);
13713
+ const full = pathMod10.win32.join(dir, cmd + ext);
13278
13714
  if (isFile(full)) return full;
13279
13715
  }
13280
13716
  }
@@ -13390,8 +13826,8 @@ function withUtf8Codepage(cmdline) {
13390
13826
  function isBareWindowsName(s) {
13391
13827
  if (!s) return false;
13392
13828
  if (s.includes("/") || s.includes("\\")) return false;
13393
- if (pathMod9.isAbsolute(s)) return false;
13394
- if (pathMod9.extname(s)) return false;
13829
+ if (pathMod10.isAbsolute(s)) return false;
13830
+ if (pathMod10.extname(s)) return false;
13395
13831
  return true;
13396
13832
  }
13397
13833
  function quoteForCmdExe(arg) {
@@ -13412,7 +13848,7 @@ var NeedsConfirmationError = class extends Error {
13412
13848
  }
13413
13849
  };
13414
13850
  function registerShellTools(registry, opts) {
13415
- const rootDir = pathMod10.resolve(opts.rootDir);
13851
+ const rootDir = pathMod11.resolve(opts.rootDir);
13416
13852
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
13417
13853
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
13418
13854
  const jobs = opts.jobs ?? new JobRegistry();
@@ -13423,7 +13859,7 @@ function registerShellTools(registry, opts) {
13423
13859
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
13424
13860
  registry.register({
13425
13861
  name: "run_command",
13426
- description: 'Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; mutating / network / install commands gate on user confirmation.\n\nNo real shell \u2014 argv parsed natively for cross-platform parity:\n\u2022 Supported: chains `|`/`||`/`&&`/`;` (each segment allowlist-checked) and file redirects `>`/`>>`/`<`/`2>`/`2>>`/`2>&1`/`&>`.\n\u2022 Rejected: background `&`, heredoc `<<`, `$(\u2026)`, subshells, `$VAR` expansion, glob expansion. Quote operator chars as literals (`grep "a|b" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain. Use `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>` instead.\n\u2022 Filter at source \u2014 `grep -c` / `wc -l` / narrower paths over unbounded dumps.',
13862
+ description: 'Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; mutating / network / install commands gate on user confirmation.\n\nDO NOT use run_command for file operations \u2014 use write_file, edit_file, multi_edit, copy_file, move_file, or delete_file instead. Shell utilities (echo, cp, sed, cat, tee, perl, python -c, etc.) bypass validation, lack rollback, and will trigger user confirmation gates that waste turns.\n\nNo real shell \u2014 argv parsed natively for cross-platform parity:\n\u2022 Supported: chains `|`/`||`/`&&`/`;` (each segment allowlist-checked) and file redirects `>`/`>>`/`<`/`2>`/`2>>`/`2>&1`/`&>`.\n\u2022 Rejected: background `&`, heredoc `<<`, `$(\u2026)`, subshells, `$VAR` expansion, glob expansion. Quote operator chars as literals (`grep "a|b" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain. Use `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>` instead.\n\u2022 Filter at source \u2014 `grep -c` / `wc -l` / narrower paths over unbounded dumps.',
13427
13863
  // Plan-mode gate: allow allowlisted commands through (git status,
13428
13864
  // cargo check, ls, grep …) so the model can actually investigate
13429
13865
  // during planning. Anything that would otherwise trigger a
@@ -13625,11 +14061,11 @@ function registerShellTools(registry, opts) {
13625
14061
  return registry;
13626
14062
  }
13627
14063
  function resolveCwdInsideRoot(rootDir, raw) {
13628
- const root = pathMod10.resolve(rootDir);
14064
+ const root = pathMod11.resolve(rootDir);
13629
14065
  if (!raw || !raw.trim()) return root;
13630
- const resolved = pathMod10.resolve(root, raw);
13631
- const rel = pathMod10.relative(root, resolved);
13632
- if (rel.startsWith("..") || pathMod10.isAbsolute(rel)) {
14066
+ const resolved = pathMod11.resolve(root, raw);
14067
+ const rel = pathMod11.relative(root, resolved);
14068
+ if (rel.startsWith("..") || pathMod11.isAbsolute(rel)) {
13633
14069
  throw new Error(
13634
14070
  `run_background: cwd "${raw}" resolves outside the workspace root (${root}). Pass a workspace-relative path.`
13635
14071
  );
@@ -13677,17 +14113,20 @@ ${r.output}` : header;
13677
14113
  }
13678
14114
 
13679
14115
  // src/tools/web.ts
14116
+ import { lookup } from "dns/promises";
14117
+ import { isIP } from "net";
13680
14118
  import { parse as parseHtml } from "node-html-parser";
13681
14119
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
13682
14120
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
13683
14121
  var DEFAULT_TOPK = 5;
13684
14122
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
13685
14123
  var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
13686
- var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
14124
+ var BING_ENDPOINT = "https://cn.bing.com/search";
13687
14125
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
13688
14126
  var TAVILY_ENDPOINT = "https://api.tavily.com/search";
13689
14127
  var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai/chat/completions";
13690
14128
  var EXA_ENDPOINT = "https://api.exa.ai/answer";
14129
+ var FETCH_MAX_REDIRECTS = 5;
13691
14130
  function searchStatusError(status) {
13692
14131
  if (status === 429) return t("webErrors.rateLimit429");
13693
14132
  if (status === 403) return t("webErrors.forbidden403");
@@ -13700,6 +14139,63 @@ function fetchStatusError(status, url) {
13700
14139
  if (status >= 500 && status <= 599) return t("webErrors.fetchServerError5xx", { status, url });
13701
14140
  return t("webErrors.fetchStatus", { status, url });
13702
14141
  }
14142
+ function parseIpv4(address) {
14143
+ const parts = address.split(".");
14144
+ if (parts.length !== 4) return null;
14145
+ let out = 0;
14146
+ for (const part of parts) {
14147
+ if (!/^\d+$/.test(part)) return null;
14148
+ const n = Number(part);
14149
+ if (!Number.isInteger(n) || n < 0 || n > 255) return null;
14150
+ out = (out << 8) + n;
14151
+ }
14152
+ return out >>> 0;
14153
+ }
14154
+ function ipv4InRange(value, base, bits) {
14155
+ const parsed = parseIpv4(base);
14156
+ if (parsed === null) return false;
14157
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
14158
+ return (value & mask) === (parsed & mask);
14159
+ }
14160
+ function isPrivateIpv4(address) {
14161
+ const value = parseIpv4(address);
14162
+ if (value === null) return false;
14163
+ return ipv4InRange(value, "0.0.0.0", 8) || ipv4InRange(value, "10.0.0.0", 8) || ipv4InRange(value, "100.64.0.0", 10) || ipv4InRange(value, "127.0.0.0", 8) || ipv4InRange(value, "169.254.0.0", 16) || ipv4InRange(value, "172.16.0.0", 12) || ipv4InRange(value, "192.0.0.0", 24) || ipv4InRange(value, "192.0.2.0", 24) || ipv4InRange(value, "192.168.0.0", 16) || ipv4InRange(value, "198.18.0.0", 15) || ipv4InRange(value, "198.51.100.0", 24) || ipv4InRange(value, "203.0.113.0", 24) || ipv4InRange(value, "224.0.0.0", 4) || ipv4InRange(value, "240.0.0.0", 4);
14164
+ }
14165
+ function normalizeIpv6(address) {
14166
+ return address.toLowerCase().replace(/(^|:)0+([0-9a-f])/g, "$1$2");
14167
+ }
14168
+ function isPrivateIpv6(address) {
14169
+ const normalized = normalizeIpv6(address);
14170
+ const mapped = /^::ffff:(?:0+:)?(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
14171
+ if (mapped) return isPrivateIpv4(mapped[1]);
14172
+ return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb") || normalized.startsWith("ff");
14173
+ }
14174
+ function isInternalAddress(address) {
14175
+ const family = isIP(address);
14176
+ if (family === 4) return isPrivateIpv4(address);
14177
+ if (family === 6) return isPrivateIpv6(address);
14178
+ return false;
14179
+ }
14180
+ async function assertPublicHttpUrl(rawUrl) {
14181
+ const url = new URL(rawUrl);
14182
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
14183
+ throw new Error(`web_fetch refuses non-HTTP URL: ${url.protocol}`);
14184
+ }
14185
+ const host = url.hostname;
14186
+ const literal = isIP(host);
14187
+ const addresses = literal ? [host] : (await lookup(host, { all: true, verbatim: true })).map((entry) => entry.address);
14188
+ if (addresses.length === 0 || addresses.some(isInternalAddress)) {
14189
+ throw new Error(`web_fetch refuses internal or reserved host: ${host}`);
14190
+ }
14191
+ return url;
14192
+ }
14193
+ function redirectLocation(resp, currentUrl) {
14194
+ if (resp.status < 300 || resp.status > 399) return null;
14195
+ const location = resp.headers.get("location");
14196
+ if (!location) return null;
14197
+ return new URL(location, currentUrl).toString();
14198
+ }
13703
14199
  async function webSearch(query, opts = {}) {
13704
14200
  if (opts.engine === "metaso") {
13705
14201
  return searchMetaso(query, opts);
@@ -13716,29 +14212,29 @@ async function webSearch(query, opts = {}) {
13716
14212
  if (opts.engine === "exa") {
13717
14213
  return searchExa(query, opts);
13718
14214
  }
13719
- return searchMojeek(query, opts);
14215
+ return searchBing(query, opts);
13720
14216
  }
13721
- async function searchMojeek(query, opts = {}) {
14217
+ async function searchBing(query, opts = {}) {
13722
14218
  const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
13723
- const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
14219
+ const resp = await fetch(`${BING_ENDPOINT}?q=${encodeURIComponent(query)}`, {
13724
14220
  headers: {
13725
14221
  "User-Agent": USER_AGENT,
13726
14222
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
13727
- "Accept-Language": "en-US,en;q=0.9"
14223
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
13728
14224
  },
13729
14225
  signal: opts.signal,
13730
14226
  redirect: "follow"
13731
14227
  });
13732
14228
  if (!resp.ok) throw new Error(searchStatusError(resp.status));
13733
14229
  const html = await resp.text();
13734
- const results = parseMojeekResults(html).slice(0, topK);
14230
+ const results = parseBingResults(html).slice(0, topK);
13735
14231
  if (results.length === 0) {
13736
14232
  if (/no results found|did not match any documents/i.test(html)) return [];
13737
14233
  if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
13738
- throw new Error(t("webErrors.mojeekBlocked"));
14234
+ throw new Error(t("webErrors.bingBlocked"));
13739
14235
  }
13740
14236
  throw new Error(
13741
- t("webErrors.mojeekNoResults", {
14237
+ t("webErrors.bingNoResults", {
13742
14238
  chars: html.length,
13743
14239
  preview: html.slice(0, 120).replace(/\s+/g, " ")
13744
14240
  })
@@ -14055,35 +14551,19 @@ function parseSearxngHtmlResults(html) {
14055
14551
  }
14056
14552
  return results;
14057
14553
  }
14058
- function parseMojeekResults(html) {
14059
- const titles = [];
14060
- const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
14061
- let m;
14062
- while (true) {
14063
- m = titleAnchorRe.exec(html);
14064
- if (m === null) break;
14065
- titles.push(m[0]);
14066
- }
14067
- const snippets = [];
14068
- const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
14069
- while (true) {
14070
- m = snippetRe.exec(html);
14071
- if (m === null) break;
14072
- snippets.push(m[1] ?? "");
14073
- }
14074
- const hrefRe = /href="([^"]+)"/;
14075
- const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
14554
+ function parseBingResults(html) {
14555
+ const root = parseHtml(html);
14076
14556
  const results = [];
14077
- for (let i = 0; i < titles.length; i++) {
14078
- const anchor = titles[i];
14079
- const hrefMatch = anchor.match(hrefRe);
14080
- const innerMatch = anchor.match(innerRe);
14081
- if (!hrefMatch?.[1]) continue;
14082
- results.push({
14083
- title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
14084
- url: hrefMatch[1],
14085
- snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
14086
- });
14557
+ for (const li of root.querySelectorAll("li.b_algo")) {
14558
+ const anchor = li.querySelector("h2 a[href]");
14559
+ if (!anchor) continue;
14560
+ const href = anchor.getAttribute("href");
14561
+ if (!href) continue;
14562
+ const title = anchor.textContent.trim();
14563
+ if (!title) continue;
14564
+ const cap = li.querySelector("div.b_caption p");
14565
+ const snippet = cap ? cap.textContent.trim().replace(/\s+/g, " ") : "";
14566
+ results.push({ title, url: href, snippet });
14087
14567
  }
14088
14568
  return results;
14089
14569
  }
@@ -14099,12 +14579,23 @@ async function webFetch(url, opts = {}) {
14099
14579
  const cancel = () => ctl.abort();
14100
14580
  opts.signal?.addEventListener("abort", cancel, { once: true });
14101
14581
  let resp;
14582
+ let currentUrl = url;
14102
14583
  try {
14103
- resp = await fetch(url, {
14104
- headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
14105
- signal: ctl.signal,
14106
- redirect: "follow"
14107
- });
14584
+ for (let redirects = 0; ; redirects++) {
14585
+ const parsed = await assertPublicHttpUrl(currentUrl);
14586
+ if (ctl.signal.aborted) throw new DOMException("aborted", "AbortError");
14587
+ resp = await fetch(parsed, {
14588
+ headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
14589
+ signal: ctl.signal,
14590
+ redirect: "manual"
14591
+ });
14592
+ const nextUrl = redirectLocation(resp, parsed.toString());
14593
+ if (!nextUrl) break;
14594
+ if (redirects >= FETCH_MAX_REDIRECTS) {
14595
+ throw new Error(`web_fetch redirect limit exceeded for ${url}`);
14596
+ }
14597
+ currentUrl = nextUrl;
14598
+ }
14108
14599
  } catch (err) {
14109
14600
  if (timedOut) {
14110
14601
  throw new Error(t("webErrors.fetchTimeout", { ms: timeoutMs, url }));
@@ -14127,7 +14618,7 @@ async function webFetch(url, opts = {}) {
14127
14618
  const finalText = truncated ? `${text.slice(0, maxChars)}
14128
14619
 
14129
14620
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
14130
- return { url, title, text: finalText, truncated };
14621
+ return { url: currentUrl, title, text: finalText, truncated };
14131
14622
  }
14132
14623
  async function readBodyCapped(resp, maxBytes) {
14133
14624
  if (!resp.body) return await resp.text();
@@ -14199,9 +14690,6 @@ function walkExtract(node, out) {
14199
14690
  for (const child of node.childNodes) walkExtract(child, out);
14200
14691
  if (isBreak) out.push("\n");
14201
14692
  }
14202
- function stripHtml(s) {
14203
- return parseHtml(s).text;
14204
- }
14205
14693
  var HTML_ENTITIES = {
14206
14694
  amp: "&",
14207
14695
  lt: "<",
@@ -14314,12 +14802,12 @@ ${i + 1}. ${r.title}`);
14314
14802
  }
14315
14803
 
14316
14804
  // src/env.ts
14317
- import { readFileSync as readFileSync11 } from "fs";
14318
- import { resolve as resolve11 } from "path";
14805
+ import { readFileSync as readFileSync12 } from "fs";
14806
+ import { resolve as resolve12 } from "path";
14319
14807
  function loadDotenv(path2 = ".env") {
14320
14808
  let raw;
14321
14809
  try {
14322
- raw = readFileSync11(resolve11(process.cwd(), path2), "utf8");
14810
+ raw = readFileSync12(resolve12(process.cwd(), path2), "utf8");
14323
14811
  } catch {
14324
14812
  return;
14325
14813
  }
@@ -14338,7 +14826,7 @@ function loadDotenv(path2 = ".env") {
14338
14826
  }
14339
14827
 
14340
14828
  // src/transcript/log.ts
14341
- import { createWriteStream, readFileSync as readFileSync12 } from "fs";
14829
+ import { createWriteStream, readFileSync as readFileSync13 } from "fs";
14342
14830
  function recordFromLoopEvent(ev, extra) {
14343
14831
  const rec = {
14344
14832
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14381,7 +14869,7 @@ function openTranscriptFile(path2, meta) {
14381
14869
  return stream;
14382
14870
  }
14383
14871
  function readTranscript(path2) {
14384
- const raw = readFileSync12(path2, "utf8");
14872
+ const raw = readFileSync13(path2, "utf8");
14385
14873
  return parseTranscript(raw);
14386
14874
  }
14387
14875
  function parseTranscript(raw) {
@@ -14768,25 +15256,25 @@ function truncate(s, n) {
14768
15256
  }
14769
15257
 
14770
15258
  // src/version.ts
14771
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "fs";
15259
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
14772
15260
  import { homedir as homedir8 } from "os";
14773
- import { dirname as dirname7, join as join14 } from "path";
15261
+ import { dirname as dirname8, join as join14 } from "path";
14774
15262
  import { fileURLToPath as fileURLToPath2 } from "url";
14775
15263
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
14776
15264
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
14777
15265
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
14778
15266
  function readPackageVersion() {
14779
15267
  try {
14780
- let dir = dirname7(fileURLToPath2(import.meta.url));
15268
+ let dir = dirname8(fileURLToPath2(import.meta.url));
14781
15269
  for (let i = 0; i < 6; i++) {
14782
15270
  const p = join14(dir, "package.json");
14783
15271
  if (existsSync10(p)) {
14784
- const pkg = JSON.parse(readFileSync13(p, "utf8"));
15272
+ const pkg = JSON.parse(readFileSync14(p, "utf8"));
14785
15273
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
14786
15274
  return pkg.version;
14787
15275
  }
14788
15276
  }
14789
- const parent = dirname7(dir);
15277
+ const parent = dirname8(dir);
14790
15278
  if (parent === dir) break;
14791
15279
  dir = parent;
14792
15280
  }
@@ -14800,7 +15288,7 @@ function cachePath(homeDirOverride) {
14800
15288
  }
14801
15289
  function readCache(homeDirOverride) {
14802
15290
  try {
14803
- const raw = readFileSync13(cachePath(homeDirOverride), "utf8");
15291
+ const raw = readFileSync14(cachePath(homeDirOverride), "utf8");
14804
15292
  const parsed = JSON.parse(raw);
14805
15293
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
14806
15294
  return parsed;
@@ -14812,8 +15300,8 @@ function readCache(homeDirOverride) {
14812
15300
  function writeCache(entry, homeDirOverride) {
14813
15301
  try {
14814
15302
  const p = cachePath(homeDirOverride);
14815
- mkdirSync5(dirname7(p), { recursive: true });
14816
- writeFileSync5(p, JSON.stringify(entry), "utf8");
15303
+ mkdirSync5(dirname8(p), { recursive: true });
15304
+ writeFileSync6(p, JSON.stringify(entry), "utf8");
14817
15305
  } catch {
14818
15306
  }
14819
15307
  }
@@ -15028,7 +15516,7 @@ var McpClient = class {
15028
15516
  const id = this.nextId++;
15029
15517
  const frame = { jsonrpc: "2.0", id, method, params };
15030
15518
  let abortHandler = null;
15031
- const promise = new Promise((resolve13, reject) => {
15519
+ const promise = new Promise((resolve14, reject) => {
15032
15520
  const timeout = setTimeout(() => {
15033
15521
  this.pending.delete(id);
15034
15522
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -15037,7 +15525,7 @@ var McpClient = class {
15037
15525
  );
15038
15526
  }, this.requestTimeoutMs);
15039
15527
  this.pending.set(id, {
15040
- resolve: resolve13,
15528
+ resolve: resolve14,
15041
15529
  reject,
15042
15530
  timeout
15043
15531
  });
@@ -15169,12 +15657,12 @@ var StdioTransport = class {
15169
15657
  }
15170
15658
  async send(message) {
15171
15659
  if (this.closed) throw new Error("MCP transport is closed");
15172
- return new Promise((resolve13, reject) => {
15660
+ return new Promise((resolve14, reject) => {
15173
15661
  const line = `${JSON.stringify(message)}
15174
15662
  `;
15175
15663
  this.child.stdin.write(line, "utf8", (err) => {
15176
15664
  if (err) reject(err);
15177
- else resolve13();
15665
+ else resolve14();
15178
15666
  });
15179
15667
  });
15180
15668
  }
@@ -15185,8 +15673,8 @@ var StdioTransport = class {
15185
15673
  continue;
15186
15674
  }
15187
15675
  if (this.closed) return;
15188
- const next = await new Promise((resolve13) => {
15189
- this.waiters.push(resolve13);
15676
+ const next = await new Promise((resolve14) => {
15677
+ this.waiters.push(resolve14);
15190
15678
  });
15191
15679
  if (next === null) return;
15192
15680
  yield next;
@@ -15266,8 +15754,8 @@ var SseTransport = class {
15266
15754
  constructor(opts) {
15267
15755
  this.url = opts.url;
15268
15756
  this.headers = opts.headers ?? {};
15269
- this.endpointReady = new Promise((resolve13, reject) => {
15270
- this.resolveEndpoint = resolve13;
15757
+ this.endpointReady = new Promise((resolve14, reject) => {
15758
+ this.resolveEndpoint = resolve14;
15271
15759
  this.rejectEndpoint = reject;
15272
15760
  });
15273
15761
  this.endpointReady.catch(() => void 0);
@@ -15294,8 +15782,8 @@ var SseTransport = class {
15294
15782
  continue;
15295
15783
  }
15296
15784
  if (this.closed) return;
15297
- const next = await new Promise((resolve13) => {
15298
- this.waiters.push(resolve13);
15785
+ const next = await new Promise((resolve14) => {
15786
+ this.waiters.push(resolve14);
15299
15787
  });
15300
15788
  if (next === null) return;
15301
15789
  yield next;
@@ -15481,8 +15969,8 @@ var StreamableHttpTransport = class {
15481
15969
  continue;
15482
15970
  }
15483
15971
  if (this.closed) return;
15484
- const next = await new Promise((resolve13) => {
15485
- this.waiters.push(resolve13);
15972
+ const next = await new Promise((resolve14) => {
15973
+ this.waiters.push(resolve14);
15486
15974
  });
15487
15975
  if (next === null) return;
15488
15976
  yield next;
@@ -15580,13 +16068,13 @@ import {
15580
16068
  ftruncateSync,
15581
16069
  mkdirSync as mkdirSync6,
15582
16070
  openSync as openSync2,
15583
- readFileSync as readFileSync14,
16071
+ readFileSync as readFileSync15,
15584
16072
  readSync,
15585
16073
  unlinkSync as unlinkSync3,
15586
- writeFileSync as writeFileSync6,
16074
+ writeFileSync as writeFileSync7,
15587
16075
  writeSync
15588
16076
  } from "fs";
15589
- import { dirname as dirname8, isAbsolute as isAbsolute7, relative as relative8, resolve as resolve12 } from "path";
16077
+ import { dirname as dirname9, isAbsolute as isAbsolute9, relative as relative10, resolve as resolve13 } from "path";
15590
16078
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
15591
16079
  function parseEditBlocks(text) {
15592
16080
  const out = [];
@@ -15604,29 +16092,29 @@ function parseEditBlocks(text) {
15604
16092
  return out;
15605
16093
  }
15606
16094
  function resolveEditPath(rootDir, rawPath) {
15607
- const absRoot = resolve12(rootDir);
16095
+ const absRoot = resolve13(rootDir);
15608
16096
  if (/^[A-Za-z]:[\\/]/.test(rawPath) || looksLikeAbsoluteSystemPath2(rawPath)) {
15609
- return resolve12(rawPath);
16097
+ return resolve13(rawPath);
15610
16098
  }
15611
16099
  let rooted = rawPath;
15612
16100
  while (rooted.startsWith("/") || rooted.startsWith("\\")) {
15613
16101
  rooted = rooted.slice(1);
15614
16102
  }
15615
- return resolve12(absRoot, rooted || ".");
16103
+ return resolve13(absRoot, rooted || ".");
15616
16104
  }
15617
16105
  function looksLikeAbsoluteSystemPath2(rawPath) {
15618
16106
  return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
15619
16107
  rawPath
15620
16108
  );
15621
16109
  }
15622
- function pathIsUnder2(child, parent) {
15623
- const rel = relative8(parent, child);
15624
- return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
16110
+ function pathIsUnder4(child, parent) {
16111
+ const rel = relative10(parent, child);
16112
+ return rel === "" || !rel.startsWith("..") && !isAbsolute9(rel);
15625
16113
  }
15626
16114
  function applyEditBlock(block, rootDir) {
15627
- const absRoot = resolve12(rootDir);
16115
+ const absRoot = resolve13(rootDir);
15628
16116
  const absTarget = resolveEditPath(rootDir, block.path);
15629
- if (!pathIsUnder2(absTarget, absRoot)) {
16117
+ if (!pathIsUnder4(absTarget, absRoot)) {
15630
16118
  return {
15631
16119
  path: block.path,
15632
16120
  status: "path-escape",
@@ -15636,7 +16124,7 @@ function applyEditBlock(block, rootDir) {
15636
16124
  const searchEmpty = block.search.length === 0;
15637
16125
  if (searchEmpty) {
15638
16126
  try {
15639
- mkdirSync6(dirname8(absTarget), { recursive: true });
16127
+ mkdirSync6(dirname9(absTarget), { recursive: true });
15640
16128
  const fd = openSync2(absTarget, "wx");
15641
16129
  try {
15642
16130
  writeSync(fd, block.replace);
@@ -15679,7 +16167,7 @@ function applyEditBlock(block, rootDir) {
15679
16167
  if (n <= 0) break;
15680
16168
  readBytes += n;
15681
16169
  }
15682
- const content = inBuf.toString("utf8", 0, readBytes);
16170
+ const { text: content, encoding } = decodeFileBuffer(inBuf.subarray(0, readBytes));
15683
16171
  const le = lineEndingOf(content);
15684
16172
  const adaptedSearch = block.search.replace(/\r?\n/g, le);
15685
16173
  const adaptedReplace = block.replace.replace(/\r?\n/g, le);
@@ -15700,7 +16188,7 @@ function applyEditBlock(block, rootDir) {
15700
16188
  };
15701
16189
  }
15702
16190
  const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
15703
- const outBuf = Buffer.from(replaced, "utf8");
16191
+ const outBuf = encodeFile(replaced, encoding);
15704
16192
  ftruncateSync(fd, outBuf.length);
15705
16193
  let written = 0;
15706
16194
  while (written < outBuf.length) {
@@ -15720,10 +16208,12 @@ function applyEditBlocks(blocks, rootDir) {
15720
16208
  return blocks.map((b) => applyEditBlock(b, rootDir));
15721
16209
  }
15722
16210
  function snapshotBeforeEdits(blocks, rootDir) {
16211
+ const absRoot = resolve13(rootDir);
15723
16212
  const seen = /* @__PURE__ */ new Set();
15724
16213
  const snapshots = [];
15725
16214
  for (const b of blocks) {
15726
16215
  const abs = resolveEditPath(rootDir, b.path);
16216
+ if (!pathIsUnder4(abs, absRoot)) continue;
15727
16217
  if (seen.has(abs)) continue;
15728
16218
  seen.add(abs);
15729
16219
  if (!existsSync11(abs)) {
@@ -15731,7 +16221,8 @@ function snapshotBeforeEdits(blocks, rootDir) {
15731
16221
  continue;
15732
16222
  }
15733
16223
  try {
15734
- snapshots.push({ path: b.path, prevContent: readFileSync14(abs, "utf8") });
16224
+ const { text, encoding } = decodeFileBuffer(readFileSync15(abs));
16225
+ snapshots.push({ path: b.path, prevContent: text, prevEncoding: encoding });
15735
16226
  } catch {
15736
16227
  snapshots.push({ path: b.path, prevContent: null });
15737
16228
  }
@@ -15739,10 +16230,10 @@ function snapshotBeforeEdits(blocks, rootDir) {
15739
16230
  return snapshots;
15740
16231
  }
15741
16232
  function restoreSnapshots(snapshots, rootDir) {
15742
- const absRoot = resolve12(rootDir);
16233
+ const absRoot = resolve13(rootDir);
15743
16234
  return snapshots.map((snap) => {
15744
16235
  const abs = resolveEditPath(rootDir, snap.path);
15745
- if (!pathIsUnder2(abs, absRoot)) {
16236
+ if (!pathIsUnder4(abs, absRoot)) {
15746
16237
  return {
15747
16238
  path: snap.path,
15748
16239
  status: "path-escape",
@@ -15758,7 +16249,7 @@ function restoreSnapshots(snapshots, rootDir) {
15758
16249
  message: "removed (the edit had created it)"
15759
16250
  };
15760
16251
  }
15761
- writeFileSync6(abs, snap.prevContent, "utf8");
16252
+ writeFileSync7(abs, encodeFile(snap.prevContent, snap.prevEncoding ?? "utf8"));
15762
16253
  return {
15763
16254
  path: snap.path,
15764
16255
  status: "applied",
@@ -15774,7 +16265,7 @@ function lineEndingOf(text) {
15774
16265
  }
15775
16266
 
15776
16267
  // src/code/prompt.ts
15777
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
16268
+ import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
15778
16269
  import { join as join15 } from "path";
15779
16270
  var DEFAULT_CODE_MODEL = "deepseek-v4-flash";
15780
16271
  function codeSystemBase(modelId) {
@@ -15838,7 +16329,7 @@ the new lines
15838
16329
  >>>>>>> REPLACE
15839
16330
 
15840
16331
  Rules:
15841
- - read_file first so your SEARCH matches byte-for-byte.
16332
+ - **Read before edit (enforced).** You MUST call \`read_file\` on the target this session before \`edit_file\` / \`multi_edit\` will accept it \u2014 the tool refuses unread targets up front, so SEARCH text is grounded in on-disk bytes, not a guess. A fold / mechanical truncate clears the tracker, so re-read after one of those before mutating. \`write_file\` counts as a read for that path (the content is what you just wrote).
15842
16333
  - One edit per block; multiple blocks per response are fine.
15843
16334
  - Create a new file with empty SEARCH:
15844
16335
  path/to/new.ts
@@ -15881,6 +16372,10 @@ When the user says run / start / launch / serve / boot up: start it, verify it c
15881
16372
  - One short paragraph explaining *why*, then the blocks.
15882
16373
  - Silence during exploration is fine \u2014 tool calls first, prose after.
15883
16374
 
16375
+ # Task integrity \u2014 non-negotiable
16376
+
16377
+ The user's original objective and ALL constraints (especially "do NOT do X", "avoid Y", "never Z") remain in force for the entire session. You may NOT unilaterally simplify, narrow, or change the objective to save tokens, time, or steps. If you believe the objective needs adjustment, ask the user \u2014 do NOT decide on your own.
16378
+
15884
16379
  __ESCALATION_CONTRACT__
15885
16380
 
15886
16381
  ${TUI_FORMATTING_RULES}
@@ -15905,7 +16400,7 @@ function codeSystemPrompt(rootDir, opts = {}) {
15905
16400
  if (existsSync12(gitignorePath)) {
15906
16401
  let content;
15907
16402
  try {
15908
- content = readFileSync15(gitignorePath, "utf8");
16403
+ content = readFileSync16(gitignorePath, "utf8");
15909
16404
  } catch {
15910
16405
  }
15911
16406
  if (content !== void 0) {
@@ -15937,21 +16432,21 @@ ${appendParts.join("\n\n")}`;
15937
16432
 
15938
16433
  // src/telemetry/usage.ts
15939
16434
  import {
15940
- appendFileSync as appendFileSync2,
16435
+ appendFileSync as appendFileSync3,
15941
16436
  closeSync as closeSync3,
15942
16437
  existsSync as existsSync13,
15943
16438
  fstatSync as fstatSync2,
15944
16439
  mkdirSync as mkdirSync7,
15945
16440
  openSync as openSync3,
15946
- readFileSync as readFileSync16,
16441
+ readFileSync as readFileSync17,
15947
16442
  readSync as readSync2,
15948
- renameSync as renameSync2,
16443
+ renameSync as renameSync3,
15949
16444
  statSync as statSync6,
15950
16445
  unlinkSync as unlinkSync4,
15951
- writeFileSync as writeFileSync7
16446
+ writeFileSync as writeFileSync8
15952
16447
  } from "fs";
15953
16448
  import { homedir as homedir9 } from "os";
15954
- import { dirname as dirname9, join as join16 } from "path";
16449
+ import { dirname as dirname10, join as join16 } from "path";
15955
16450
  function defaultUsageLogPath(homeDirOverride) {
15956
16451
  return join16(homeDirOverride ?? homedir9(), ".reasonix", "usage.jsonl");
15957
16452
  }
@@ -15992,9 +16487,9 @@ function compactUsageLogIfLarge(path2, now) {
15992
16487
  if (kept.length === lines.filter((l) => l.trim()).length) return;
15993
16488
  const tmp = `${path2}.compacting`;
15994
16489
  try {
15995
- writeFileSync7(tmp, kept.length > 0 ? `${kept.join("\n")}
16490
+ writeFileSync8(tmp, kept.length > 0 ? `${kept.join("\n")}
15996
16491
  ` : "", "utf8");
15997
- renameSync2(tmp, path2);
16492
+ renameSync3(tmp, path2);
15998
16493
  } catch {
15999
16494
  try {
16000
16495
  unlinkSync4(tmp);
@@ -16018,8 +16513,8 @@ function appendUsage(input) {
16018
16513
  if (input.subagent) record.subagent = input.subagent;
16019
16514
  const path2 = input.path ?? defaultUsageLogPath();
16020
16515
  try {
16021
- mkdirSync7(dirname9(path2), { recursive: true });
16022
- appendFileSync2(path2, `${JSON.stringify(record)}
16516
+ mkdirSync7(dirname10(path2), { recursive: true });
16517
+ appendFileSync3(path2, `${JSON.stringify(record)}
16023
16518
  `, "utf8");
16024
16519
  compactUsageLogIfLarge(path2, record.ts);
16025
16520
  } catch {
@@ -16030,7 +16525,7 @@ function readUsageLog(path2 = defaultUsageLogPath()) {
16030
16525
  if (!existsSync13(path2)) return [];
16031
16526
  let raw;
16032
16527
  try {
16033
- raw = readFileSync16(path2, "utf8");
16528
+ raw = readFileSync17(path2, "utf8");
16034
16529
  } catch {
16035
16530
  return [];
16036
16531
  }
@@ -16262,9 +16757,9 @@ export {
16262
16757
  openTranscriptFile,
16263
16758
  outputCostUsd,
16264
16759
  parseAtQuery,
16760
+ parseBingResults,
16265
16761
  parseEditBlocks,
16266
16762
  parseMcpSpec,
16267
- parseMojeekResults,
16268
16763
  parseSearxngHtmlResults,
16269
16764
  parseTranscript,
16270
16765
  prepareSpawn,