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
@@ -3,7 +3,7 @@ import { createRequire as __cr } from 'node:module'; if (typeof globalThis.requi
3
3
  import {
4
4
  MemoryStore,
5
5
  sanitizeMemoryName
6
- } from "./chunk-FD7SNDWW.js";
6
+ } from "./chunk-O5LIHAMP.js";
7
7
  import {
8
8
  countTokens,
9
9
  countTokensBounded,
@@ -12,23 +12,25 @@ import {
12
12
  } from "./chunk-6OWJV3YW.js";
13
13
  import {
14
14
  Usage
15
- } from "./chunk-5U5LMMFF.js";
15
+ } from "./chunk-J2IHQGPQ.js";
16
16
  import {
17
17
  applyEdit,
18
18
  applyMultiEdit,
19
+ decodeFileBuffer,
20
+ encodeFile,
19
21
  pauseGate
20
- } from "./chunk-6FRNXWDZ.js";
22
+ } from "./chunk-ZWHSHFDP.js";
21
23
  import {
22
24
  NEGATIVE_CLAIM_RULE,
23
25
  PROJECT_MEMORY_FILES,
24
26
  PROJECT_MEMORY_MAX_CHARS,
25
27
  TUI_FORMATTING_RULES,
26
28
  memoryEnabled
27
- } from "./chunk-SLAFMXAZ.js";
29
+ } from "./chunk-JNTMOX7G.js";
28
30
  import {
29
31
  formatHookOutcomeMessage,
30
32
  runHooks
31
- } from "./chunk-PJIQIYTV.js";
33
+ } from "./chunk-PB3MAFEI.js";
32
34
  import {
33
35
  ignoredByLayers,
34
36
  loadGitignoreAt,
@@ -46,12 +48,13 @@ import {
46
48
  DEEPSEEK_CONTEXT_TOKENS,
47
49
  DEFAULT_CONTEXT_TOKENS,
48
50
  SessionStats
49
- } from "./chunk-IKSYVBBZ.js";
51
+ } from "./chunk-QX5TWXRZ.js";
50
52
  import {
51
53
  t
52
- } from "./chunk-PIC5HJRD.js";
54
+ } from "./chunk-YEF7C4XI.js";
53
55
  import {
54
56
  DEFAULT_INDEX_EXCLUDES,
57
+ ToolRateLimiter,
55
58
  addProjectPathAllowed,
56
59
  loadExaApiKey,
57
60
  loadMemoryTypeRegistry,
@@ -59,10 +62,11 @@ import {
59
62
  loadPerplexityApiKey,
60
63
  loadProjectPathAllowed,
61
64
  loadTavilyApiKey,
65
+ parseRateLimitedToolResult,
62
66
  require_picomatch,
63
67
  webSearchEndpoint,
64
68
  webSearchEngine
65
- } from "./chunk-FPME5QOO.js";
69
+ } from "./chunk-WMTMMSXU.js";
66
70
  import {
67
71
  __commonJS,
68
72
  __esm,
@@ -6052,12 +6056,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
6052
6056
  let timer;
6053
6057
  let onAbort;
6054
6058
  try {
6055
- await new Promise((resolve5, reject) => {
6059
+ await new Promise((resolve6, reject) => {
6056
6060
  ready.then(
6057
6061
  () => {
6058
6062
  if (settled) return;
6059
6063
  settled = true;
6060
- resolve5();
6064
+ resolve6();
6061
6065
  },
6062
6066
  (err) => {
6063
6067
  if (settled) return;
@@ -6314,12 +6318,14 @@ var ToolRegistry = class {
6314
6318
  _interceptors = [];
6315
6319
  _auditListener = null;
6316
6320
  _resultAugmenter = null;
6321
+ _rateLimiter;
6317
6322
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
6318
6323
  _lastMalformed = /* @__PURE__ */ new Map();
6319
6324
  /** Per-tool fingerprint of the last host-side gate rejection. */
6320
6325
  _lastGateRejection = /* @__PURE__ */ new Map();
6321
6326
  constructor(opts = {}) {
6322
6327
  this._autoFlatten = opts.autoFlatten !== false;
6328
+ this._rateLimiter = new ToolRateLimiter(opts.rateLimit);
6323
6329
  }
6324
6330
  /** Enable / disable plan-mode enforcement at dispatch. */
6325
6331
  setPlanMode(on) {
@@ -6356,6 +6362,9 @@ var ToolRegistry = class {
6356
6362
  get hasResultAugmenter() {
6357
6363
  return this._resultAugmenter !== null;
6358
6364
  }
6365
+ get rateLimitPolicy() {
6366
+ return this._rateLimiter.policy;
6367
+ }
6359
6368
  register(def) {
6360
6369
  if (!def.name) throw new Error("tool requires a name");
6361
6370
  const internal = { ...def };
@@ -6454,6 +6463,10 @@ var ToolRegistry = class {
6454
6463
  rejectedReason: "aborted"
6455
6464
  });
6456
6465
  }
6466
+ const rateLimit = this._rateLimiter.consume(name);
6467
+ if (!rateLimit.allowed) {
6468
+ return JSON.stringify(rateLimit.result);
6469
+ }
6457
6470
  let finalResult;
6458
6471
  try {
6459
6472
  try {
@@ -6462,7 +6475,8 @@ var ToolRegistry = class {
6462
6475
  }
6463
6476
  const result = await tool.fn(args, {
6464
6477
  signal: opts.signal,
6465
- confirmationGate: opts.confirmationGate
6478
+ confirmationGate: opts.confirmationGate,
6479
+ readTracker: opts.readTracker
6466
6480
  });
6467
6481
  const str = typeof result === "string" ? result : JSON.stringify(result);
6468
6482
  let clipped = str;
@@ -6547,6 +6561,9 @@ function plainTextRejectedReason(name, result) {
6547
6561
  if ((name === "edit_file" || name === "write_file") && /rejected this edit/i.test(result)) {
6548
6562
  return "edit-gate";
6549
6563
  }
6564
+ if ((name === "edit_file" || name === "multi_edit") && /read_file first/i.test(result)) {
6565
+ return "read-before-edit";
6566
+ }
6550
6567
  if ((name === "run_command" || name === "run_background") && /\buser denied:/i.test(result)) {
6551
6568
  return "shell-gate";
6552
6569
  }
@@ -6556,6 +6573,8 @@ function rejectionRecoveryHint(reason) {
6556
6573
  switch (reason) {
6557
6574
  case "edit-gate":
6558
6575
  return "Do not re-emit the same edit. Try a genuinely different edit or ask the user how to proceed.";
6576
+ case "read-before-edit":
6577
+ return "Call read_file on the target path first, then re-issue the edit.";
6559
6578
  case "shell-gate":
6560
6579
  return "Do not retry the same command. Use an allowlisted/read-only command, wait for approval, or ask the user how to proceed.";
6561
6580
  case "engineering-lifecycle":
@@ -6757,6 +6776,10 @@ function buildSyntheticAssistantMessage(content, fallbackModel) {
6757
6776
  }
6758
6777
 
6759
6778
  // src/context-manager.ts
6779
+ function extractPinnedConstraints(systemPrompt) {
6780
+ const pattern = /# (?:HIGH PRIORITY constraints|User memory|Project memory)[\s\S]*?(?=\n# |\n---|$)/g;
6781
+ return Array.from(systemPrompt.matchAll(pattern), (m) => m[0]).join("\n\n");
6782
+ }
6760
6783
  var HISTORY_FOLD_THRESHOLD = 0.75;
6761
6784
  var HISTORY_FOLD_TAIL_FRACTION = 0.2;
6762
6785
  var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.78;
@@ -6771,20 +6794,25 @@ var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
6771
6794
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
6772
6795
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
6773
6796
  var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
6774
- function extractPinnedSkills(head) {
6797
+ function buildFoldSummaryInstruction(pinnedSkillNames) {
6798
+ 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.";
6799
+ if (pinnedSkillNames.length === 0) return base;
6800
+ const list = pinnedSkillNames.map((n) => `"${n}"`).join(", ");
6801
+ return `${base} The following skill memos are pinned verbatim and appended after your summary \u2014 do NOT quote or paraphrase their bodies: ${list}.`;
6802
+ }
6803
+ function collectPinnedSkills(head) {
6775
6804
  const pinned = /* @__PURE__ */ new Map();
6776
- const stubbedHead = head.map((msg) => {
6777
- if (typeof msg.content !== "string") return msg;
6778
- let hit = false;
6779
- const next = msg.content.replace(SKILL_PIN_REGEX, (full, name) => {
6805
+ for (const msg of head) {
6806
+ if (typeof msg.content !== "string") continue;
6807
+ SKILL_PIN_REGEX.lastIndex = 0;
6808
+ for (const match of msg.content.matchAll(SKILL_PIN_REGEX)) {
6809
+ const name = match[1];
6810
+ const full = match[0];
6780
6811
  pinned.delete(name);
6781
6812
  pinned.set(name, full);
6782
- hit = true;
6783
- return `[skill ${JSON.stringify(name)} memo \u2014 preserved separately, do not summarize.]`;
6784
- });
6785
- return hit ? { ...msg, content: next } : msg;
6786
- });
6787
- return { stubbedHead, pinnedBodies: [...pinned.values()] };
6813
+ }
6814
+ }
6815
+ return { names: [...pinned.keys()], bodies: [...pinned.values()] };
6788
6816
  }
6789
6817
  var ContextManager = class {
6790
6818
  constructor(deps) {
@@ -6880,16 +6908,22 @@ var ContextManager = class {
6880
6908
  const tail = all.slice(boundary);
6881
6909
  const headTokens = totalTokens - cumTokens;
6882
6910
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
6883
- const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
6884
- const summary = await this.summarizeForFold(stubbedHead);
6911
+ const { names: pinnedNames, bodies: pinnedBodies } = collectPinnedSkills(head);
6912
+ const summary = await this.summarizeForFold(head, pinnedNames);
6885
6913
  if (!summary.content) return noop;
6886
6914
  const memoTail = pinnedBodies.length > 0 ? `
6887
6915
 
6888
6916
  ${SKILL_PIN_MEMO_HEADER}
6889
6917
 
6890
6918
  ${pinnedBodies.join("\n\n")}` : "";
6919
+ const constraints = extractPinnedConstraints(this.deps.getSystemPrompt());
6920
+ const constraintTail = constraints ? `
6921
+
6922
+ [PINNED CONSTRAINTS \u2014 preserved verbatim]
6923
+
6924
+ ${constraints}` : "";
6891
6925
  const summaryMsg = buildAssistantMessage(
6892
- HISTORY_FOLD_MARKER + summary.content + memoTail,
6926
+ HISTORY_FOLD_MARKER + summary.content + memoTail + constraintTail,
6893
6927
  [],
6894
6928
  model,
6895
6929
  summary.reasoningContent
@@ -6897,6 +6931,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6897
6931
  const replacement = [summaryMsg, ...tail];
6898
6932
  this.deps.log.compactInPlace(replacement);
6899
6933
  this.persistRewrite(replacement);
6934
+ this.deps.onLogRewrite?.();
6900
6935
  return {
6901
6936
  folded: true,
6902
6937
  beforeMessages: all.length,
@@ -6948,6 +6983,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6948
6983
  if (replacement.length === all.length) return noop;
6949
6984
  this.deps.log.compactInPlace(replacement);
6950
6985
  this.persistRewrite(replacement);
6986
+ this.deps.onLogRewrite?.();
6951
6987
  return {
6952
6988
  folded: true,
6953
6989
  beforeMessages: all.length,
@@ -6966,17 +7002,18 @@ ${pinnedBodies.join("\n\n")}` : "";
6966
7002
  this.persistRewrite([...kept]);
6967
7003
  return true;
6968
7004
  }
6969
- async summarizeForFold(messagesToSummarize) {
7005
+ async summarizeForFold(messagesToSummarize, pinnedSkillNames) {
6970
7006
  const summaryModel = "deepseek-v4-flash";
6971
- 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.";
6972
7007
  const healed = healLoadedMessages(messagesToSummarize, DEFAULT_MAX_RESULT_CHARS).messages;
7008
+ const agentSystem = this.deps.getSystemPrompt();
7009
+ const fewShots = this.deps.getFewShots?.() ?? [];
7010
+ const tools = this.deps.getToolSpecs?.() ?? [];
7011
+ const instruction = buildFoldSummaryInstruction(pinnedSkillNames);
6973
7012
  const messages = [
6974
- { role: "system", content: systemPrompt },
7013
+ { role: "system", content: agentSystem },
7014
+ ...fewShots.map((m) => ({ ...m })),
6975
7015
  ...healed,
6976
- {
6977
- role: "user",
6978
- content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
6979
- }
7016
+ { role: "user", content: instruction }
6980
7017
  ];
6981
7018
  const turnSignal = this.deps.getAbortSignal();
6982
7019
  const foldCtrl = new AbortController();
@@ -7006,9 +7043,9 @@ ${pinnedBodies.join("\n\n")}` : "";
7006
7043
  this.deps.client.chat({
7007
7044
  model: summaryModel,
7008
7045
  messages,
7046
+ tools: tools.length ? tools : void 0,
7009
7047
  signal: foldCtrl.signal,
7010
- thinking: thinkingModeForModel(summaryModel),
7011
- reasoningEffort: "high"
7048
+ thinking: "disabled"
7012
7049
  }),
7013
7050
  abortPromise,
7014
7051
  timeoutPromise
@@ -7093,6 +7130,7 @@ function formatLoopError(err, probe) {
7093
7130
  if (status === "402") return t("errors.balance402", { inner });
7094
7131
  if (status === "422") return t("errors.badparam422", { inner });
7095
7132
  if (status === "400") return t("errors.badrequest400", { inner });
7133
+ if (status === "429") return t("errors.concurrency429", { inner });
7096
7134
  if (is5xxStatus(status)) return formatDeepSeek5xx(status, probe);
7097
7135
  return msg;
7098
7136
  }
@@ -7675,8 +7713,34 @@ function signature(call) {
7675
7713
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
7676
7714
  }
7677
7715
 
7716
+ // src/tools/read-tracker.ts
7717
+ import * as pathMod from "path";
7718
+ var ReadTracker = class _ReadTracker {
7719
+ _seen = /* @__PURE__ */ new Set();
7720
+ static norm(abs) {
7721
+ const resolved = pathMod.resolve(abs);
7722
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7723
+ }
7724
+ markRead(abs) {
7725
+ this._seen.add(_ReadTracker.norm(abs));
7726
+ }
7727
+ hasRead(abs) {
7728
+ return this._seen.has(_ReadTracker.norm(abs));
7729
+ }
7730
+ reset() {
7731
+ this._seen.clear();
7732
+ }
7733
+ get size() {
7734
+ return this._seen.size;
7735
+ }
7736
+ };
7737
+
7678
7738
  // src/loop.ts
7679
7739
  var ESCALATION_MODEL = "deepseek-v4-pro";
7740
+ 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.]";
7741
+ function formatSteerUserMessage(content) {
7742
+ return [MID_TURN_STEER_WRAPPER, content].join("\n");
7743
+ }
7680
7744
  var CacheFirstLoop = class {
7681
7745
  client;
7682
7746
  prefix;
@@ -7685,6 +7749,8 @@ var CacheFirstLoop = class {
7685
7749
  scratch = new VolatileScratch();
7686
7750
  stats = new SessionStats();
7687
7751
  repair;
7752
+ /** 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. */
7753
+ readTracker = new ReadTracker();
7688
7754
  // Mutable via configure() — slash commands in the TUI / library callers tweak
7689
7755
  // these mid-session so users don't have to restart.
7690
7756
  model;
@@ -7708,15 +7774,19 @@ var CacheFirstLoop = class {
7708
7774
  _turnAbort = new AbortController();
7709
7775
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
7710
7776
  _inflight = new InflightSet();
7711
- /** Typeahead steer message set by the UI; step() consumes it at the next iter boundary. */
7712
- _steer = null;
7777
+ /** Typeahead steer messages set by the UI; step() consumes one at each iter boundary. */
7778
+ _steerQueue = [];
7713
7779
  /** Set true when a steer was consumed this turn; cleared on next step() entry. */
7714
7780
  _steerConsumed = false;
7715
7781
  /** UI calls this to inject a mid-turn steer message without aborting the current turn.
7716
- * New text resets steerConsumed a fresh steer hasn't been consumed yet. */
7782
+ * New text resets steerConsumed because a fresh steer is queued. */
7717
7783
  steer(text) {
7718
- this._steer = text;
7719
- if (text !== null) this._steerConsumed = false;
7784
+ if (text === null) {
7785
+ this._steerQueue.length = 0;
7786
+ return;
7787
+ }
7788
+ this._steerQueue.push(text);
7789
+ this._steerConsumed = false;
7720
7790
  }
7721
7791
  /** True when a steer was consumed this turn (UI gate to avoid double-submit). */
7722
7792
  get steerConsumed() {
@@ -7802,7 +7872,11 @@ var CacheFirstLoop = class {
7802
7872
  stats: this.stats,
7803
7873
  sessionName: this.sessionName,
7804
7874
  getAbortSignal: () => this._turnAbort.signal,
7805
- getCurrentTurn: () => this._turn
7875
+ getCurrentTurn: () => this._turn,
7876
+ getSystemPrompt: () => this.prefix.system,
7877
+ getToolSpecs: () => this.prefix.toolSpecs,
7878
+ getFewShots: () => this.prefix.fewShots,
7879
+ onLogRewrite: () => this.readTracker.reset()
7806
7880
  });
7807
7881
  }
7808
7882
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -7973,7 +8047,8 @@ ${reason}`
7973
8047
  const result = await this.tools.dispatch(name, args, {
7974
8048
  signal,
7975
8049
  maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
7976
- confirmationGate: this.confirmationGate
8050
+ confirmationGate: this.confirmationGate,
8051
+ readTracker: this.readTracker
7977
8052
  });
7978
8053
  const postReport = await runHooks({
7979
8054
  hooks: this.hooks,
@@ -8001,11 +8076,9 @@ ${reason}`
8001
8076
  return generated;
8002
8077
  }
8003
8078
  _inflightCounter = 0;
8004
- buildMessages(pendingUser) {
8079
+ buildMessages() {
8005
8080
  const healedMessages = this.healActiveLogBeforeSend();
8006
- const msgs = [...this.prefix.toMessages(), ...healedMessages];
8007
- if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
8008
- return msgs;
8081
+ return [...this.prefix.toMessages(), ...healedMessages];
8009
8082
  }
8010
8083
  healActiveLogBeforeSend() {
8011
8084
  const current = this.log.toMessages();
@@ -8086,6 +8159,7 @@ ${reason}`
8086
8159
  cap: this.budgetUsd.toFixed(2)
8087
8160
  })
8088
8161
  };
8162
+ this._steerQueue.length = 0;
8089
8163
  return;
8090
8164
  }
8091
8165
  if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
@@ -8124,8 +8198,8 @@ ${reason}`
8124
8198
  };
8125
8199
  }
8126
8200
  this.appendAndPersist({ role: "user", content: userInput });
8127
- let pendingUser = null;
8128
8201
  const toolSpecs = this.prefix.tools();
8202
+ let rateLimitWarningShown = false;
8129
8203
  for (let iter = 0; ; iter++) {
8130
8204
  if (signal.aborted) {
8131
8205
  try {
@@ -8146,6 +8220,7 @@ ${reason}`
8146
8220
  } finally {
8147
8221
  this._turnAbort = new AbortController();
8148
8222
  }
8223
+ this._steerQueue.length = 0;
8149
8224
  return;
8150
8225
  }
8151
8226
  if (iter > 0) {
@@ -8155,14 +8230,15 @@ ${reason}`
8155
8230
  content: t("loop.toolUploadStatus")
8156
8231
  };
8157
8232
  }
8158
- let messages = this.buildMessages(pendingUser);
8159
- if (this._steer !== null) {
8160
- const steer = this._steer;
8161
- this._steer = null;
8162
- this._steerConsumed = true;
8163
- this.appendAndPersist({ role: "user", content: steer });
8164
- messages = this.buildMessages(pendingUser);
8165
- pendingUser = null;
8233
+ let messages = this.buildMessages();
8234
+ if (this._steerQueue.length > 0) {
8235
+ const steer = this._steerQueue.shift();
8236
+ this._steerConsumed = this._steerQueue.length === 0;
8237
+ this.appendAndPersist({
8238
+ role: "user",
8239
+ content: formatSteerUserMessage(steer)
8240
+ });
8241
+ messages = this.buildMessages();
8166
8242
  yield {
8167
8243
  turn: this._turn,
8168
8244
  role: "steer",
@@ -8179,10 +8255,10 @@ ${reason}`
8179
8255
  content: t("loop.preflightTruncateStatus")
8180
8256
  };
8181
8257
  const result = this.context.mechanicalTruncate(this.model, {
8182
- allowEmpty: pendingUser !== null
8258
+ allowEmpty: false
8183
8259
  });
8184
8260
  if (result.folded) {
8185
- messages = this.buildMessages(pendingUser);
8261
+ messages = this.buildMessages();
8186
8262
  const after = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
8187
8263
  const stillFull = after.needsAction;
8188
8264
  yield {
@@ -8328,6 +8404,7 @@ ${reason}`
8328
8404
  } finally {
8329
8405
  this._turnAbort = new AbortController();
8330
8406
  }
8407
+ this._steerQueue.length = 0;
8331
8408
  return;
8332
8409
  }
8333
8410
  const probe = is5xxError(err) ? await probeDeepSeekReachable(this.client) : void 0;
@@ -8337,6 +8414,7 @@ ${reason}`
8337
8414
  content: "",
8338
8415
  error: formatLoopError(err, probe)
8339
8416
  };
8417
+ this._steerQueue.length = 0;
8340
8418
  return;
8341
8419
  }
8342
8420
  if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && isEscalationRequest(assistantContent)) {
@@ -8417,11 +8495,16 @@ ${reason}`
8417
8495
  };
8418
8496
  }
8419
8497
  if (repairedCalls.length === 0) {
8498
+ if (this._steerQueue.length > 0) {
8499
+ continue;
8500
+ }
8420
8501
  if (allSuppressed) {
8421
8502
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "stuck" });
8503
+ this._steerQueue.length = 0;
8422
8504
  return;
8423
8505
  }
8424
8506
  yield { turn: this._turn, role: "done", content: assistantContent };
8507
+ this._steerQueue.length = 0;
8425
8508
  return;
8426
8509
  }
8427
8510
  const decision = this.context.decideAfterUsage(usage, this.model, this._foldedThisTurn);
@@ -8467,6 +8550,7 @@ ${reason}`
8467
8550
  };
8468
8551
  this.context.trimTrailingToolCalls();
8469
8552
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
8553
+ this._steerQueue.length = 0;
8470
8554
  return;
8471
8555
  }
8472
8556
  const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
@@ -8514,6 +8598,15 @@ ${reason}`
8514
8598
  }
8515
8599
  for (const w of preWarnings) yield w;
8516
8600
  for (const w of postWarnings) yield w;
8601
+ const rateLimited = parseRateLimitedToolResult(result);
8602
+ if (rateLimited && !rateLimitWarningShown) {
8603
+ rateLimitWarningShown = true;
8604
+ yield {
8605
+ turn: this._turn,
8606
+ role: "warning",
8607
+ content: rateLimited.message
8608
+ };
8609
+ }
8517
8610
  this.appendAndPersist({
8518
8611
  role: "tool",
8519
8612
  tool_call_id: call.id ?? "",
@@ -8536,7 +8629,7 @@ ${reason}`
8536
8629
  return {
8537
8630
  client: this.client,
8538
8631
  signal: this._turnAbort.signal,
8539
- buildMessages: () => this.buildMessages(null),
8632
+ buildMessages: () => this.buildMessages(),
8540
8633
  appendAndPersist: (m) => this.appendAndPersist(m),
8541
8634
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
8542
8635
  turn: this._turn
@@ -8561,7 +8654,7 @@ function parsePositiveIntEnv(raw) {
8561
8654
  // src/at-mentions.ts
8562
8655
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
8563
8656
  import { readdir, stat } from "fs/promises";
8564
- import { isAbsolute, join, relative, resolve } from "path";
8657
+ import { isAbsolute, join, relative, resolve as resolve2 } from "path";
8565
8658
 
8566
8659
  // src/at-mentions-url.ts
8567
8660
  var AT_URL_PATTERN = /(?<=^|\s)@(https?:\/\/\S+)/g;
@@ -8692,7 +8785,7 @@ function listFilesSync(root, opts = {}) {
8692
8785
  function listFilesWithStatsSync(root, opts = {}) {
8693
8786
  const maxResults = Math.max(1, opts.maxResults ?? 2e3);
8694
8787
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8695
- const rootAbs = resolve(root);
8788
+ const rootAbs = resolve2(root);
8696
8789
  const respectGi = opts.respectGitignore !== false;
8697
8790
  const out = [];
8698
8791
  const walk2 = (dirAbs, dirRel, layers) => {
@@ -8756,7 +8849,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
8756
8849
  async function walkFilesStream(root, opts) {
8757
8850
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8758
8851
  const respectGi = opts.respectGitignore !== false;
8759
- const rootAbs = resolve(root);
8852
+ const rootAbs = resolve2(root);
8760
8853
  const progressGap = Math.max(0, opts.progressIntervalMs ?? 100);
8761
8854
  let scanned = 0;
8762
8855
  let halted = false;
@@ -8834,8 +8927,8 @@ async function flushFiles(ents, dirAbs, dirRel, layers, emit) {
8834
8927
  async function listDirectory(root, relDir, opts = {}) {
8835
8928
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8836
8929
  const respectGi = opts.respectGitignore !== false;
8837
- const rootAbs = resolve(root);
8838
- const dirAbs = resolve(rootAbs, relDir);
8930
+ const rootAbs = resolve2(root);
8931
+ const dirAbs = resolve2(rootAbs, relDir);
8839
8932
  const rel = relative(rootAbs, dirAbs);
8840
8933
  if (rel.startsWith("..") || isAbsolute(rel)) return [];
8841
8934
  const layers = [];
@@ -9000,7 +9093,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
9000
9093
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
9001
9094
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
9002
9095
  const fs4 = opts.fs ?? defaultFs;
9003
- const root = resolve(rootDir);
9096
+ const root = resolve2(rootDir);
9004
9097
  const seen = /* @__PURE__ */ new Map();
9005
9098
  const expansions = [];
9006
9099
  const dirListings = /* @__PURE__ */ new Map();
@@ -9047,7 +9140,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs4, dirListings
9047
9140
  if (isAbsolute(rawPath)) {
9048
9141
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
9049
9142
  }
9050
- const resolved = resolve(root, rawPath);
9143
+ const resolved = resolve2(root, rawPath);
9051
9144
  const rel = relative(root, resolved);
9052
9145
  if (rel.startsWith("..") || isAbsolute(rel)) {
9053
9146
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
@@ -9077,7 +9170,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs4, dirListings
9077
9170
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
9078
9171
  }
9079
9172
  function readSafe(root, rawPath, fs4) {
9080
- const resolved = resolve(root, rawPath);
9173
+ const resolved = resolve2(root, rawPath);
9081
9174
  try {
9082
9175
  return fs4.read(resolved);
9083
9176
  } catch {
@@ -9353,16 +9446,19 @@ function registerChoiceTool(registry, opts = {}) {
9353
9446
 
9354
9447
  // src/tools/web.ts
9355
9448
  var import_node_html_parser = __toESM(require_dist(), 1);
9449
+ import { lookup } from "dns/promises";
9450
+ import { isIP } from "net";
9356
9451
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
9357
9452
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
9358
9453
  var DEFAULT_TOPK = 5;
9359
9454
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
9360
9455
  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";
9361
- var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
9456
+ var BING_ENDPOINT = "https://cn.bing.com/search";
9362
9457
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
9363
9458
  var TAVILY_ENDPOINT = "https://api.tavily.com/search";
9364
9459
  var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai/chat/completions";
9365
9460
  var EXA_ENDPOINT = "https://api.exa.ai/answer";
9461
+ var FETCH_MAX_REDIRECTS = 5;
9366
9462
  function searchStatusError(status) {
9367
9463
  if (status === 429) return t("webErrors.rateLimit429");
9368
9464
  if (status === 403) return t("webErrors.forbidden403");
@@ -9375,6 +9471,63 @@ function fetchStatusError(status, url) {
9375
9471
  if (status >= 500 && status <= 599) return t("webErrors.fetchServerError5xx", { status, url });
9376
9472
  return t("webErrors.fetchStatus", { status, url });
9377
9473
  }
9474
+ function parseIpv4(address) {
9475
+ const parts = address.split(".");
9476
+ if (parts.length !== 4) return null;
9477
+ let out = 0;
9478
+ for (const part of parts) {
9479
+ if (!/^\d+$/.test(part)) return null;
9480
+ const n = Number(part);
9481
+ if (!Number.isInteger(n) || n < 0 || n > 255) return null;
9482
+ out = (out << 8) + n;
9483
+ }
9484
+ return out >>> 0;
9485
+ }
9486
+ function ipv4InRange(value, base, bits) {
9487
+ const parsed = parseIpv4(base);
9488
+ if (parsed === null) return false;
9489
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
9490
+ return (value & mask) === (parsed & mask);
9491
+ }
9492
+ function isPrivateIpv4(address) {
9493
+ const value = parseIpv4(address);
9494
+ if (value === null) return false;
9495
+ 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);
9496
+ }
9497
+ function normalizeIpv6(address) {
9498
+ return address.toLowerCase().replace(/(^|:)0+([0-9a-f])/g, "$1$2");
9499
+ }
9500
+ function isPrivateIpv6(address) {
9501
+ const normalized = normalizeIpv6(address);
9502
+ const mapped = /^::ffff:(?:0+:)?(\d+\.\d+\.\d+\.\d+)$/i.exec(normalized);
9503
+ if (mapped) return isPrivateIpv4(mapped[1]);
9504
+ 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");
9505
+ }
9506
+ function isInternalAddress(address) {
9507
+ const family = isIP(address);
9508
+ if (family === 4) return isPrivateIpv4(address);
9509
+ if (family === 6) return isPrivateIpv6(address);
9510
+ return false;
9511
+ }
9512
+ async function assertPublicHttpUrl(rawUrl) {
9513
+ const url = new URL(rawUrl);
9514
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
9515
+ throw new Error(`web_fetch refuses non-HTTP URL: ${url.protocol}`);
9516
+ }
9517
+ const host = url.hostname;
9518
+ const literal = isIP(host);
9519
+ const addresses = literal ? [host] : (await lookup(host, { all: true, verbatim: true })).map((entry) => entry.address);
9520
+ if (addresses.length === 0 || addresses.some(isInternalAddress)) {
9521
+ throw new Error(`web_fetch refuses internal or reserved host: ${host}`);
9522
+ }
9523
+ return url;
9524
+ }
9525
+ function redirectLocation(resp, currentUrl) {
9526
+ if (resp.status < 300 || resp.status > 399) return null;
9527
+ const location = resp.headers.get("location");
9528
+ if (!location) return null;
9529
+ return new URL(location, currentUrl).toString();
9530
+ }
9378
9531
  async function webSearch(query, opts = {}) {
9379
9532
  if (opts.engine === "metaso") {
9380
9533
  return searchMetaso(query, opts);
@@ -9391,29 +9544,29 @@ async function webSearch(query, opts = {}) {
9391
9544
  if (opts.engine === "exa") {
9392
9545
  return searchExa(query, opts);
9393
9546
  }
9394
- return searchMojeek(query, opts);
9547
+ return searchBing(query, opts);
9395
9548
  }
9396
- async function searchMojeek(query, opts = {}) {
9549
+ async function searchBing(query, opts = {}) {
9397
9550
  const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
9398
- const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
9551
+ const resp = await fetch(`${BING_ENDPOINT}?q=${encodeURIComponent(query)}`, {
9399
9552
  headers: {
9400
9553
  "User-Agent": USER_AGENT,
9401
9554
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
9402
- "Accept-Language": "en-US,en;q=0.9"
9555
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
9403
9556
  },
9404
9557
  signal: opts.signal,
9405
9558
  redirect: "follow"
9406
9559
  });
9407
9560
  if (!resp.ok) throw new Error(searchStatusError(resp.status));
9408
9561
  const html = await resp.text();
9409
- const results = parseMojeekResults(html).slice(0, topK);
9562
+ const results = parseBingResults(html).slice(0, topK);
9410
9563
  if (results.length === 0) {
9411
9564
  if (/no results found|did not match any documents/i.test(html)) return [];
9412
9565
  if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
9413
- throw new Error(t("webErrors.mojeekBlocked"));
9566
+ throw new Error(t("webErrors.bingBlocked"));
9414
9567
  }
9415
9568
  throw new Error(
9416
- t("webErrors.mojeekNoResults", {
9569
+ t("webErrors.bingNoResults", {
9417
9570
  chars: html.length,
9418
9571
  preview: html.slice(0, 120).replace(/\s+/g, " ")
9419
9572
  })
@@ -9730,35 +9883,19 @@ function parseSearxngHtmlResults(html) {
9730
9883
  }
9731
9884
  return results;
9732
9885
  }
9733
- function parseMojeekResults(html) {
9734
- const titles = [];
9735
- const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
9736
- let m;
9737
- while (true) {
9738
- m = titleAnchorRe.exec(html);
9739
- if (m === null) break;
9740
- titles.push(m[0]);
9741
- }
9742
- const snippets = [];
9743
- const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
9744
- while (true) {
9745
- m = snippetRe.exec(html);
9746
- if (m === null) break;
9747
- snippets.push(m[1] ?? "");
9748
- }
9749
- const hrefRe = /href="([^"]+)"/;
9750
- const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
9886
+ function parseBingResults(html) {
9887
+ const root = (0, import_node_html_parser.parse)(html);
9751
9888
  const results = [];
9752
- for (let i = 0; i < titles.length; i++) {
9753
- const anchor = titles[i];
9754
- const hrefMatch = anchor.match(hrefRe);
9755
- const innerMatch = anchor.match(innerRe);
9756
- if (!hrefMatch?.[1]) continue;
9757
- results.push({
9758
- title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
9759
- url: hrefMatch[1],
9760
- snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
9761
- });
9889
+ for (const li of root.querySelectorAll("li.b_algo")) {
9890
+ const anchor = li.querySelector("h2 a[href]");
9891
+ if (!anchor) continue;
9892
+ const href = anchor.getAttribute("href");
9893
+ if (!href) continue;
9894
+ const title = anchor.textContent.trim();
9895
+ if (!title) continue;
9896
+ const cap = li.querySelector("div.b_caption p");
9897
+ const snippet = cap ? cap.textContent.trim().replace(/\s+/g, " ") : "";
9898
+ results.push({ title, url: href, snippet });
9762
9899
  }
9763
9900
  return results;
9764
9901
  }
@@ -9774,12 +9911,23 @@ async function webFetch(url, opts = {}) {
9774
9911
  const cancel = () => ctl.abort();
9775
9912
  opts.signal?.addEventListener("abort", cancel, { once: true });
9776
9913
  let resp;
9914
+ let currentUrl = url;
9777
9915
  try {
9778
- resp = await fetch(url, {
9779
- headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
9780
- signal: ctl.signal,
9781
- redirect: "follow"
9782
- });
9916
+ for (let redirects = 0; ; redirects++) {
9917
+ const parsed = await assertPublicHttpUrl(currentUrl);
9918
+ if (ctl.signal.aborted) throw new DOMException("aborted", "AbortError");
9919
+ resp = await fetch(parsed, {
9920
+ headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
9921
+ signal: ctl.signal,
9922
+ redirect: "manual"
9923
+ });
9924
+ const nextUrl = redirectLocation(resp, parsed.toString());
9925
+ if (!nextUrl) break;
9926
+ if (redirects >= FETCH_MAX_REDIRECTS) {
9927
+ throw new Error(`web_fetch redirect limit exceeded for ${url}`);
9928
+ }
9929
+ currentUrl = nextUrl;
9930
+ }
9783
9931
  } catch (err) {
9784
9932
  if (timedOut) {
9785
9933
  throw new Error(t("webErrors.fetchTimeout", { ms: timeoutMs, url }));
@@ -9802,7 +9950,7 @@ async function webFetch(url, opts = {}) {
9802
9950
  const finalText = truncated ? `${text.slice(0, maxChars)}
9803
9951
 
9804
9952
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
9805
- return { url, title, text: finalText, truncated };
9953
+ return { url: currentUrl, title, text: finalText, truncated };
9806
9954
  }
9807
9955
  async function readBodyCapped(resp, maxBytes) {
9808
9956
  if (!resp.body) return await resp.text();
@@ -9874,9 +10022,6 @@ function walkExtract(node, out) {
9874
10022
  for (const child of node.childNodes) walkExtract(child, out);
9875
10023
  if (isBreak) out.push("\n");
9876
10024
  }
9877
- function stripHtml(s) {
9878
- return (0, import_node_html_parser.parse)(s).text;
9879
- }
9880
10025
  var HTML_ENTITIES = {
9881
10026
  amp: "&",
9882
10027
  lt: "<",
@@ -9991,14 +10136,14 @@ ${i + 1}. ${r.title}`);
9991
10136
  // src/tools/filesystem.ts
9992
10137
  var import_picomatch2 = __toESM(require_picomatch(), 1);
9993
10138
  import { promises as fs3 } from "fs";
9994
- import * as pathMod4 from "path";
10139
+ import * as pathMod5 from "path";
9995
10140
 
9996
10141
  // src/memory/subdir.ts
9997
10142
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
9998
- import { dirname, join as join2, relative as relative2, resolve as resolve2 } from "path";
10143
+ import { dirname, join as join2, relative as relative2, resolve as resolve3 } from "path";
9999
10144
  function findDirMemory(absDir, rootDir) {
10000
- const root = resolve2(rootDir);
10001
- const target = resolve2(absDir);
10145
+ const root = resolve3(rootDir);
10146
+ const target = resolve3(absDir);
10002
10147
  const rel = relative2(root, target);
10003
10148
  if (rel.startsWith("..")) return [];
10004
10149
  const found = [];
@@ -10020,7 +10165,7 @@ function findDirMemory(absDir, rootDir) {
10020
10165
  return found;
10021
10166
  }
10022
10167
  function findSubdirMemoryAncestors(absPath, rootDir) {
10023
- return findDirMemory(dirname(resolve2(absPath)), rootDir);
10168
+ return findDirMemory(dirname(resolve3(absPath)), rootDir);
10024
10169
  }
10025
10170
  function readSubdirMemoryContent(path) {
10026
10171
  let raw;
@@ -10044,9 +10189,9 @@ ${content}`;
10044
10189
  // src/tools/fs/glob.ts
10045
10190
  var import_picomatch = __toESM(require_picomatch(), 1);
10046
10191
  import { promises as fs } from "fs";
10047
- import * as pathMod from "path";
10192
+ import * as pathMod2 from "path";
10048
10193
  function displayRel(rootDir, full) {
10049
- return pathMod.relative(rootDir, full).replaceAll("\\", "/");
10194
+ return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
10050
10195
  }
10051
10196
  async function globFiles(ctx, startAbs, args) {
10052
10197
  if (args.signal?.aborted) {
@@ -10068,7 +10213,7 @@ async function globFiles(ctx, startAbs, args) {
10068
10213
  return;
10069
10214
  }
10070
10215
  for (const e of entries) {
10071
- const full = pathMod.join(dir, e.name);
10216
+ const full = pathMod2.join(dir, e.name);
10072
10217
  if (e.isDirectory()) {
10073
10218
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
10074
10219
  await walk2(full);
@@ -10105,7 +10250,7 @@ async function globFiles(ctx, startAbs, args) {
10105
10250
  }
10106
10251
 
10107
10252
  // src/tools/fs/outline.ts
10108
- import * as pathMod2 from "path";
10253
+ import * as pathMod3 from "path";
10109
10254
  var OUTLINE_MAX_ENTRIES = 30;
10110
10255
  var OUTLINE_TAIL_KEEP = 5;
10111
10256
  var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
@@ -10148,7 +10293,7 @@ var EXT_TO_LANG = {
10148
10293
  ".text": "txt"
10149
10294
  };
10150
10295
  function extractOutline(filename, lines) {
10151
- const ext = pathMod2.extname(filename).toLowerCase();
10296
+ const ext = pathMod3.extname(filename).toLowerCase();
10152
10297
  const lang = EXT_TO_LANG[ext];
10153
10298
  if (!lang) return [];
10154
10299
  switch (lang) {
@@ -10289,7 +10434,7 @@ function formatOutline(entries) {
10289
10434
 
10290
10435
  // src/tools/fs/search.ts
10291
10436
  import { promises as fs2 } from "fs";
10292
- import * as pathMod3 from "path";
10437
+ import * as pathMod4 from "path";
10293
10438
 
10294
10439
  // src/tools/fs/regex-runner.ts
10295
10440
  import { Worker } from "worker_threads";
@@ -10322,7 +10467,7 @@ var RegexRunner = class {
10322
10467
  this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
10323
10468
  }
10324
10469
  testLines(text, source, flags, opts = {}) {
10325
- return new Promise((resolve5, reject) => {
10470
+ return new Promise((resolve6, reject) => {
10326
10471
  if (opts.signal?.aborted) {
10327
10472
  reject(new Error("regex evaluation aborted"));
10328
10473
  return;
@@ -10335,7 +10480,7 @@ var RegexRunner = class {
10335
10480
  this.killWorker();
10336
10481
  reject(new Error(`regex evaluation exceeded ${timeoutMs}ms`));
10337
10482
  }, timeoutMs);
10338
- const entry = { resolve: resolve5, reject, timer };
10483
+ const entry = { resolve: resolve6, reject, timer };
10339
10484
  if (opts.signal) {
10340
10485
  entry.signal = opts.signal;
10341
10486
  entry.onAbort = () => {
@@ -10418,7 +10563,7 @@ function throwIfAborted(signal) {
10418
10563
  throw new DOMException("search aborted by user", "AbortError");
10419
10564
  }
10420
10565
  function displayRel2(rootDir, full) {
10421
- return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
10566
+ return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
10422
10567
  }
10423
10568
  async function searchFiles(ctx, startAbs, args) {
10424
10569
  throwIfAborted(args.signal);
@@ -10442,7 +10587,7 @@ async function searchFiles(ctx, startAbs, args) {
10442
10587
  }
10443
10588
  for (const e of entries) {
10444
10589
  throwIfAborted(args.signal);
10445
- const full = pathMod3.join(dir, e.name);
10590
+ const full = pathMod4.join(dir, e.name);
10446
10591
  const lower = e.name.toLowerCase();
10447
10592
  const hit = re ? re.test(e.name) : lower.includes(needle);
10448
10593
  if (hit) {
@@ -10534,11 +10679,11 @@ async function searchContent(ctx, startAbs, args) {
10534
10679
  throwIfTimedOut();
10535
10680
  if (e.isDirectory()) {
10536
10681
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
10537
- await walk2(pathMod3.join(dir, e.name));
10682
+ await walk2(pathMod4.join(dir, e.name));
10538
10683
  continue;
10539
10684
  }
10540
10685
  if (!e.isFile()) continue;
10541
- const full = pathMod3.join(dir, e.name);
10686
+ const full = pathMod4.join(dir, e.name);
10542
10687
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel2(ctx.rootDir, full))) continue;
10543
10688
  if (ctx.isBinaryByName(e.name)) continue;
10544
10689
  let fh;
@@ -10656,7 +10801,7 @@ var SKIP_DIR_NAMES = new Set(
10656
10801
  );
10657
10802
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
10658
10803
  function displayRel3(rootDir, full) {
10659
- return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
10804
+ return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
10660
10805
  }
10661
10806
  function looksLikeAbsoluteSystemPath(raw) {
10662
10807
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -10665,8 +10810,8 @@ function looksLikeAbsoluteSystemPath(raw) {
10665
10810
  );
10666
10811
  }
10667
10812
  function pathIsUnder(child, parent) {
10668
- const rel = pathMod4.relative(parent, child);
10669
- return rel === "" || !rel.startsWith("..") && !pathMod4.isAbsolute(rel);
10813
+ const rel = pathMod5.relative(parent, child);
10814
+ return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
10670
10815
  }
10671
10816
  var GLOB_METACHARS = /[*?{[]/;
10672
10817
  function compileNameFilter(filter) {
@@ -10698,11 +10843,11 @@ function formatBytes(n) {
10698
10843
  return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
10699
10844
  }
10700
10845
  function registerFilesystemTools(registry, opts) {
10701
- const rootDir = pathMod4.resolve(opts.rootDir);
10846
+ const rootDir = pathMod5.resolve(opts.rootDir);
10702
10847
  const allowWriting = opts.allowWriting !== false;
10703
10848
  const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
10704
10849
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
10705
- const normRoot = pathMod4.resolve(rootDir);
10850
+ const normRoot = pathMod5.resolve(rootDir);
10706
10851
  const sessionApproved = /* @__PURE__ */ new Set();
10707
10852
  const shownSubdirMemory = /* @__PURE__ */ new Set();
10708
10853
  function withSubdirMemory(absPath, body) {
@@ -10735,7 +10880,7 @@ ${body}`;
10735
10880
  if (pathIsUnder(abs, dir)) return;
10736
10881
  }
10737
10882
  const stat2 = await safeLstat(abs);
10738
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod4.dirname(abs);
10883
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod5.dirname(abs);
10739
10884
  let pending = inflightGate.get(allowPrefix);
10740
10885
  if (!pending) {
10741
10886
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -10763,7 +10908,7 @@ ${body}`;
10763
10908
  throw new Error("path must be a non-empty string");
10764
10909
  }
10765
10910
  if (looksLikeAbsoluteSystemPath(raw)) {
10766
- const abs = pathMod4.resolve(raw);
10911
+ const abs = pathMod5.resolve(raw);
10767
10912
  if (pathIsUnder(abs, normRoot)) return abs;
10768
10913
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
10769
10914
  return abs;
@@ -10773,7 +10918,7 @@ ${body}`;
10773
10918
  normalized = normalized.slice(1);
10774
10919
  }
10775
10920
  if (normalized.length === 0) normalized = ".";
10776
- const resolved = pathMod4.resolve(rootDir, normalized);
10921
+ const resolved = pathMod5.resolve(rootDir, normalized);
10777
10922
  if (!pathIsUnder(resolved, normRoot)) {
10778
10923
  throw new Error(
10779
10924
  `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`
@@ -10835,7 +10980,8 @@ ${body}`;
10835
10980
  if (looksBinary(raw)) {
10836
10981
  return `[refused: ${rel} appears to be binary (${formatBytes(sizeBytes)}) \u2014 read_file returns text only. Use get_file_info for stat.]`;
10837
10982
  }
10838
- const text = raw.toString("utf8");
10983
+ const { text } = decodeFileBuffer(raw);
10984
+ ctx?.readTracker?.markRead(abs);
10839
10985
  let lines = text.split(/\r?\n/);
10840
10986
  if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
10841
10987
  const totalLines = lines.length;
@@ -10973,7 +11119,7 @@ ${slice.join("\n")}`);
10973
11119
  lines.push(line);
10974
11120
  emitted++;
10975
11121
  if (e.isDirectory() && !skip) {
10976
- await walk2(pathMod4.join(dir, e.name), depth + 1);
11122
+ await walk2(pathMod5.join(dir, e.name), depth + 1);
10977
11123
  }
10978
11124
  }
10979
11125
  };
@@ -11133,14 +11279,20 @@ ${slice.join("\n")}`);
11133
11279
  },
11134
11280
  fn: async (args, ctx) => {
11135
11281
  const abs = await safePath(args.path, "write_file", ctx, "write");
11136
- await fs3.mkdir(pathMod4.dirname(abs), { recursive: true });
11137
- await fs3.writeFile(abs, args.content, "utf8");
11282
+ await fs3.mkdir(pathMod5.dirname(abs), { recursive: true });
11283
+ let encoding = "utf8";
11284
+ try {
11285
+ encoding = decodeFileBuffer(await fs3.readFile(abs)).encoding;
11286
+ } catch {
11287
+ }
11288
+ await fs3.writeFile(abs, encodeFile(args.content, encoding));
11289
+ ctx?.readTracker?.markRead(abs);
11138
11290
  return `wrote ${args.content.length} chars to ${displayRel3(rootDir, abs)}`;
11139
11291
  }
11140
11292
  });
11141
11293
  registry.register({
11142
11294
  name: "edit_file",
11143
- 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.",
11295
+ 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.",
11144
11296
  parameters: {
11145
11297
  type: "object",
11146
11298
  properties: {
@@ -11150,11 +11302,16 @@ ${slice.join("\n")}`);
11150
11302
  },
11151
11303
  required: ["path", "search", "replace"]
11152
11304
  },
11153
- fn: async (args, ctx) => applyEdit(rootDir, await safePath(args.path, "edit_file", ctx, "write"), args)
11305
+ fn: async (args, ctx) => applyEdit(
11306
+ rootDir,
11307
+ await safePath(args.path, "edit_file", ctx, "write"),
11308
+ args,
11309
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11310
+ )
11154
11311
  });
11155
11312
  registry.register({
11156
11313
  name: "multi_edit",
11157
- 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.",
11314
+ 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.",
11158
11315
  parameters: {
11159
11316
  type: "object",
11160
11317
  properties: {
@@ -11188,7 +11345,11 @@ ${slice.join("\n")}`);
11188
11345
  replace: e?.replace
11189
11346
  }))
11190
11347
  );
11191
- return applyMultiEdit(rootDir, resolved);
11348
+ return applyMultiEdit(
11349
+ rootDir,
11350
+ resolved,
11351
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11352
+ );
11192
11353
  }
11193
11354
  });
11194
11355
  registry.register({
@@ -11219,7 +11380,7 @@ ${slice.join("\n")}`);
11219
11380
  fn: async (args, ctx) => {
11220
11381
  const src = await safePath(args.source, "move_file", ctx, "write");
11221
11382
  const dst = await safePath(args.destination, "move_file", ctx, "write");
11222
- await fs3.mkdir(pathMod4.dirname(dst), { recursive: true });
11383
+ await fs3.mkdir(pathMod5.dirname(dst), { recursive: true });
11223
11384
  await fs3.rename(src, dst);
11224
11385
  return `moved ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
11225
11386
  }
@@ -11287,7 +11448,7 @@ ${slice.join("\n")}`);
11287
11448
  fn: async (args, ctx) => {
11288
11449
  const src = await safePath(args.source, "copy_file", ctx);
11289
11450
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
11290
- await fs3.mkdir(pathMod4.dirname(dst), { recursive: true });
11451
+ await fs3.mkdir(pathMod5.dirname(dst), { recursive: true });
11291
11452
  await fs3.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
11292
11453
  return `copied ${displayRel3(rootDir, src)} \u2192 ${displayRel3(rootDir, dst)}`;
11293
11454
  }
@@ -11992,7 +12153,7 @@ function formatSubagentResult(r) {
11992
12153
  });
11993
12154
  }
11994
12155
  function forkRegistryExcluding(parent, exclude) {
11995
- const child = new ToolRegistry();
12156
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11996
12157
  for (const spec of parent.specs()) {
11997
12158
  const name = spec.function.name;
11998
12159
  if (exclude.has(name)) continue;
@@ -12004,7 +12165,7 @@ function forkRegistryExcluding(parent, exclude) {
12004
12165
  return child;
12005
12166
  }
12006
12167
  function forkRegistryWithAllowList(parent, allow, alsoExclude) {
12007
- const child = new ToolRegistry();
12168
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
12008
12169
  for (const spec of parent.specs()) {
12009
12170
  const name = spec.function.name;
12010
12171
  if (!allow.has(name)) continue;
@@ -12031,7 +12192,7 @@ import {
12031
12192
  writeFileSync,
12032
12193
  writeSync
12033
12194
  } from "fs";
12034
- import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative6, resolve as resolve4 } from "path";
12195
+ import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative6, resolve as resolve5 } from "path";
12035
12196
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
12036
12197
  function parseEditBlocks(text) {
12037
12198
  const out = [];
@@ -12049,15 +12210,15 @@ function parseEditBlocks(text) {
12049
12210
  return out;
12050
12211
  }
12051
12212
  function resolveEditPath(rootDir, rawPath) {
12052
- const absRoot = resolve4(rootDir);
12213
+ const absRoot = resolve5(rootDir);
12053
12214
  if (/^[A-Za-z]:[\\/]/.test(rawPath) || looksLikeAbsoluteSystemPath2(rawPath)) {
12054
- return resolve4(rawPath);
12215
+ return resolve5(rawPath);
12055
12216
  }
12056
12217
  let rooted = rawPath;
12057
12218
  while (rooted.startsWith("/") || rooted.startsWith("\\")) {
12058
12219
  rooted = rooted.slice(1);
12059
12220
  }
12060
- return resolve4(absRoot, rooted || ".");
12221
+ return resolve5(absRoot, rooted || ".");
12061
12222
  }
12062
12223
  function looksLikeAbsoluteSystemPath2(rawPath) {
12063
12224
  return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
@@ -12069,7 +12230,7 @@ function pathIsUnder2(child, parent) {
12069
12230
  return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
12070
12231
  }
12071
12232
  function applyEditBlock(block, rootDir) {
12072
- const absRoot = resolve4(rootDir);
12233
+ const absRoot = resolve5(rootDir);
12073
12234
  const absTarget = resolveEditPath(rootDir, block.path);
12074
12235
  if (!pathIsUnder2(absTarget, absRoot)) {
12075
12236
  return {
@@ -12124,7 +12285,7 @@ function applyEditBlock(block, rootDir) {
12124
12285
  if (n <= 0) break;
12125
12286
  readBytes += n;
12126
12287
  }
12127
- const content = inBuf.toString("utf8", 0, readBytes);
12288
+ const { text: content, encoding } = decodeFileBuffer(inBuf.subarray(0, readBytes));
12128
12289
  const le = lineEndingOf(content);
12129
12290
  const adaptedSearch = block.search.replace(/\r?\n/g, le);
12130
12291
  const adaptedReplace = block.replace.replace(/\r?\n/g, le);
@@ -12145,7 +12306,7 @@ function applyEditBlock(block, rootDir) {
12145
12306
  };
12146
12307
  }
12147
12308
  const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
12148
- const outBuf = Buffer.from(replaced, "utf8");
12309
+ const outBuf = encodeFile(replaced, encoding);
12149
12310
  ftruncateSync(fd, outBuf.length);
12150
12311
  let written = 0;
12151
12312
  while (written < outBuf.length) {
@@ -12169,7 +12330,7 @@ function toWholeFileEditBlock(path, content, rootDir) {
12169
12330
  let search = "";
12170
12331
  if (existsSync3(abs)) {
12171
12332
  try {
12172
- search = readFileSync3(abs, "utf8");
12333
+ search = decodeFileBuffer(readFileSync3(abs)).text;
12173
12334
  } catch {
12174
12335
  search = "";
12175
12336
  }
@@ -12177,10 +12338,12 @@ function toWholeFileEditBlock(path, content, rootDir) {
12177
12338
  return { path, search, replace: content, offset: 0 };
12178
12339
  }
12179
12340
  function snapshotBeforeEdits(blocks, rootDir) {
12341
+ const absRoot = resolve5(rootDir);
12180
12342
  const seen = /* @__PURE__ */ new Set();
12181
12343
  const snapshots = [];
12182
12344
  for (const b of blocks) {
12183
12345
  const abs = resolveEditPath(rootDir, b.path);
12346
+ if (!pathIsUnder2(abs, absRoot)) continue;
12184
12347
  if (seen.has(abs)) continue;
12185
12348
  seen.add(abs);
12186
12349
  if (!existsSync3(abs)) {
@@ -12188,7 +12351,8 @@ function snapshotBeforeEdits(blocks, rootDir) {
12188
12351
  continue;
12189
12352
  }
12190
12353
  try {
12191
- snapshots.push({ path: b.path, prevContent: readFileSync3(abs, "utf8") });
12354
+ const { text, encoding } = decodeFileBuffer(readFileSync3(abs));
12355
+ snapshots.push({ path: b.path, prevContent: text, prevEncoding: encoding });
12192
12356
  } catch {
12193
12357
  snapshots.push({ path: b.path, prevContent: null });
12194
12358
  }
@@ -12196,7 +12360,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
12196
12360
  return snapshots;
12197
12361
  }
12198
12362
  function restoreSnapshots(snapshots, rootDir) {
12199
- const absRoot = resolve4(rootDir);
12363
+ const absRoot = resolve5(rootDir);
12200
12364
  return snapshots.map((snap) => {
12201
12365
  const abs = resolveEditPath(rootDir, snap.path);
12202
12366
  if (!pathIsUnder2(abs, absRoot)) {
@@ -12215,7 +12379,7 @@ function restoreSnapshots(snapshots, rootDir) {
12215
12379
  message: "removed (the edit had created it)"
12216
12380
  };
12217
12381
  }
12218
- writeFileSync(abs, snap.prevContent, "utf8");
12382
+ writeFileSync(abs, encodeFile(snap.prevContent, snap.prevEncoding ?? "utf8"));
12219
12383
  return {
12220
12384
  path: snap.path,
12221
12385
  status: "applied",
@@ -12267,4 +12431,4 @@ export {
12267
12431
  he/he.js:
12268
12432
  (*! https://mths.be/he v1.2.0 by @mathias | MIT license *)
12269
12433
  */
12270
- //# sourceMappingURL=chunk-JFBGSWQB.js.map
12434
+ //# sourceMappingURL=chunk-MQWO32ZD.js.map