reasonix 0.48.0 → 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 (130) hide show
  1. package/README.md +9 -0
  2. package/dashboard/dist/app.js +123 -16
  3. package/dashboard/dist/app.js.map +1 -1
  4. package/dist/cli/{acp-4ROCGYNH.js → acp-WFQIC6SO.js} +52 -135
  5. package/dist/cli/acp-WFQIC6SO.js.map +1 -0
  6. package/dist/cli/chat-D32JGNVH.js +51 -0
  7. package/dist/cli/{chunk-S2RMQULY.js → chunk-23ZPCIPR.js} +12 -9
  8. package/dist/cli/chunk-23ZPCIPR.js.map +1 -0
  9. package/dist/cli/{chunk-TKVXTQ3T.js → chunk-3ZZXQ3CZ.js} +27 -27
  10. package/dist/cli/chunk-3ZZXQ3CZ.js.map +1 -0
  11. package/dist/cli/{chunk-5OHHAQ4W.js → chunk-7AST3QQ3.js} +2 -2
  12. package/dist/cli/{chunk-MRZG4GBF.js → chunk-7JTKBJ2G.js} +3 -3
  13. package/dist/cli/{chunk-X53B3JIX.js → chunk-7X4JJOO7.js} +2 -61
  14. package/dist/cli/{chunk-X53B3JIX.js.map → chunk-7X4JJOO7.js.map} +1 -1
  15. package/dist/cli/{chunk-MOJYKO2A.js → chunk-ASOLXV67.js} +3 -3
  16. package/dist/cli/{chunk-7M4YYMKW.js → chunk-AWEULQG6.js} +49 -56
  17. package/dist/cli/{chunk-7M4YYMKW.js.map → chunk-AWEULQG6.js.map} +1 -1
  18. package/dist/cli/{chunk-HR5NBKEM.js → chunk-DFX5ZH5L.js} +2 -2
  19. package/dist/cli/{chunk-3WGTGXO4.js → chunk-GNS7BAT2.js} +4 -4
  20. package/dist/cli/chunk-GNS7BAT2.js.map +1 -0
  21. package/dist/cli/{chunk-TE5UIIFL.js → chunk-J2IHQGPQ.js} +12 -6
  22. package/dist/cli/chunk-J2IHQGPQ.js.map +1 -0
  23. package/dist/cli/{chunk-I4M5QJNL.js → chunk-JGTX4RRQ.js} +3 -3
  24. package/dist/cli/{chunk-FY4S7TJZ.js → chunk-JNTMOX7G.js} +10 -2
  25. package/dist/cli/chunk-JNTMOX7G.js.map +1 -0
  26. package/dist/cli/{chunk-OB4BUJBL.js → chunk-MGTBP7GG.js} +5 -2
  27. package/dist/cli/chunk-MGTBP7GG.js.map +1 -0
  28. package/dist/cli/{chunk-OPYALNTT.js → chunk-MQWO32ZD.js} +387 -184
  29. package/dist/cli/chunk-MQWO32ZD.js.map +1 -0
  30. package/dist/cli/{chunk-2QSTA2QV.js → chunk-O5LIHAMP.js} +8 -4
  31. package/dist/cli/chunk-O5LIHAMP.js.map +1 -0
  32. package/dist/cli/{chunk-NMQSUNLB.js → chunk-PB3MAFEI.js} +6 -3
  33. package/dist/cli/chunk-PB3MAFEI.js.map +1 -0
  34. package/dist/cli/{chunk-H4CCXMDD.js → chunk-PEMG6CUB.js} +2 -2
  35. package/dist/cli/{chunk-RUDBUHO4.js → chunk-PXBQ6IZ7.js} +3 -3
  36. package/dist/cli/{chunk-J2TQAWOM.js → chunk-Q46B3Z7H.js} +25 -10
  37. package/dist/cli/{chunk-J2TQAWOM.js.map → chunk-Q46B3Z7H.js.map} +1 -1
  38. package/dist/cli/{chunk-6MZTZO7A.js → chunk-QF32ROX2.js} +2152 -2613
  39. package/dist/cli/chunk-QF32ROX2.js.map +1 -0
  40. package/dist/cli/{chunk-OG5JANQ4.js → chunk-QX5TWXRZ.js} +2 -2
  41. package/dist/cli/{chunk-V4Y732RQ.js → chunk-TAIKVL35.js} +2 -2
  42. package/dist/cli/{chunk-B5CZL2SE.js → chunk-TEDWJKEI.js} +4 -9
  43. package/dist/cli/chunk-TEDWJKEI.js.map +1 -0
  44. package/dist/cli/{chunk-EMMENC4O.js → chunk-U5XQDCK7.js} +5 -5
  45. package/dist/cli/{chunk-DOWEOA6E.js → chunk-W46ZMNKO.js} +3 -3
  46. package/dist/cli/{chunk-CDVSFSAK.js → chunk-WMTMMSXU.js} +184 -8
  47. package/dist/cli/chunk-WMTMMSXU.js.map +1 -0
  48. package/dist/cli/{chunk-YW63N3ZR.js → chunk-YEF7C4XI.js} +270 -96
  49. package/dist/cli/chunk-YEF7C4XI.js.map +1 -0
  50. package/dist/cli/{chunk-JMDE6IO3.js → chunk-ZAEJWKXB.js} +2 -2
  51. package/dist/cli/chunk-ZWHSHFDP.js +6173 -0
  52. package/dist/cli/chunk-ZWHSHFDP.js.map +1 -0
  53. package/dist/cli/{code-PMPJWXEO.js → code-R4IHI7SR.js} +30 -30
  54. package/dist/cli/{commands-QS6TG4G3.js → commands-DRHFCYMO.js} +4 -4
  55. package/dist/cli/{commit-XPRSKUBF.js → commit-AG5KB4YP.js} +3 -3
  56. package/dist/cli/{desktop-562OPWIU.js → desktop-JGL6GORA.js} +60 -23
  57. package/dist/cli/desktop-JGL6GORA.js.map +1 -0
  58. package/dist/cli/{diff-I6W4AUWJ.js → diff-4Z7ETWZO.js} +9 -9
  59. package/dist/cli/{doctor-6XVZKT4U.js → doctor-VA3RHQLB.js} +9 -9
  60. package/dist/cli/index.js +37 -36
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli/{mcp-7W7ANO2Y.js → mcp-LZO4HXFA.js} +34 -23
  63. package/dist/cli/mcp-LZO4HXFA.js.map +1 -0
  64. package/dist/cli/{mcp-browse-LA4I4YIZ.js → mcp-browse-C3GXVMYZ.js} +3 -3
  65. package/dist/cli/{mcp-inspect-LWXXU7BY.js → mcp-inspect-ZMYUNFDS.js} +2 -2
  66. package/dist/cli/{prompt-RKZD4X6Y.js → prompt-MC3U5KRP.js} +5 -4
  67. package/dist/cli/{prune-sessions-SEWX7GP6.js → prune-sessions-OEPFH4N6.js} +11 -7
  68. package/dist/cli/prune-sessions-OEPFH4N6.js.map +1 -0
  69. package/dist/cli/{replay-2X7MVXOI.js → replay-4TP7ZUMZ.js} +10 -10
  70. package/dist/cli/{run-TPKXIJ27.js → run-6MXQYBOE.js} +16 -15
  71. package/dist/cli/run-6MXQYBOE.js.map +1 -0
  72. package/dist/cli/{server-NHQ3QXOZ.js → server-Z3IMJNNI.js} +65 -12
  73. package/dist/cli/server-Z3IMJNNI.js.map +1 -0
  74. package/dist/cli/{sessions-2A4DGSHA.js → sessions-NXQ5SAV7.js} +18 -18
  75. package/dist/cli/sessions-NXQ5SAV7.js.map +1 -0
  76. package/dist/cli/{setup-GOLP7J4C.js → setup-LHZELI6I.js} +6 -6
  77. package/dist/cli/{stats-CGDAFDKI.js → stats-SUIJ3QWY.js} +6 -6
  78. package/dist/cli/{version-FIL4ZFOS.js → version-BIFONEUB.js} +13 -13
  79. package/dist/index.d.ts +71 -17
  80. package/dist/index.js +1040 -391
  81. package/dist/index.js.map +1 -1
  82. package/package.json +6 -2
  83. package/dist/cli/acp-4ROCGYNH.js.map +0 -1
  84. package/dist/cli/chat-GZNB5625.js +0 -51
  85. package/dist/cli/chunk-2QSTA2QV.js.map +0 -1
  86. package/dist/cli/chunk-3WGTGXO4.js.map +0 -1
  87. package/dist/cli/chunk-6MZTZO7A.js.map +0 -1
  88. package/dist/cli/chunk-B5CZL2SE.js.map +0 -1
  89. package/dist/cli/chunk-CDVSFSAK.js.map +0 -1
  90. package/dist/cli/chunk-FY4S7TJZ.js.map +0 -1
  91. package/dist/cli/chunk-NMQSUNLB.js.map +0 -1
  92. package/dist/cli/chunk-OB4BUJBL.js.map +0 -1
  93. package/dist/cli/chunk-OPYALNTT.js.map +0 -1
  94. package/dist/cli/chunk-S2RMQULY.js.map +0 -1
  95. package/dist/cli/chunk-TE5UIIFL.js.map +0 -1
  96. package/dist/cli/chunk-TKVXTQ3T.js.map +0 -1
  97. package/dist/cli/chunk-WZGNXR6E.js +0 -2020
  98. package/dist/cli/chunk-WZGNXR6E.js.map +0 -1
  99. package/dist/cli/chunk-YW63N3ZR.js.map +0 -1
  100. package/dist/cli/desktop-562OPWIU.js.map +0 -1
  101. package/dist/cli/mcp-7W7ANO2Y.js.map +0 -1
  102. package/dist/cli/prune-sessions-SEWX7GP6.js.map +0 -1
  103. package/dist/cli/run-TPKXIJ27.js.map +0 -1
  104. package/dist/cli/server-NHQ3QXOZ.js.map +0 -1
  105. package/dist/cli/sessions-2A4DGSHA.js.map +0 -1
  106. /package/dist/cli/{chat-GZNB5625.js.map → chat-D32JGNVH.js.map} +0 -0
  107. /package/dist/cli/{chunk-5OHHAQ4W.js.map → chunk-7AST3QQ3.js.map} +0 -0
  108. /package/dist/cli/{chunk-MRZG4GBF.js.map → chunk-7JTKBJ2G.js.map} +0 -0
  109. /package/dist/cli/{chunk-MOJYKO2A.js.map → chunk-ASOLXV67.js.map} +0 -0
  110. /package/dist/cli/{chunk-HR5NBKEM.js.map → chunk-DFX5ZH5L.js.map} +0 -0
  111. /package/dist/cli/{chunk-I4M5QJNL.js.map → chunk-JGTX4RRQ.js.map} +0 -0
  112. /package/dist/cli/{chunk-H4CCXMDD.js.map → chunk-PEMG6CUB.js.map} +0 -0
  113. /package/dist/cli/{chunk-RUDBUHO4.js.map → chunk-PXBQ6IZ7.js.map} +0 -0
  114. /package/dist/cli/{chunk-OG5JANQ4.js.map → chunk-QX5TWXRZ.js.map} +0 -0
  115. /package/dist/cli/{chunk-V4Y732RQ.js.map → chunk-TAIKVL35.js.map} +0 -0
  116. /package/dist/cli/{chunk-EMMENC4O.js.map → chunk-U5XQDCK7.js.map} +0 -0
  117. /package/dist/cli/{chunk-DOWEOA6E.js.map → chunk-W46ZMNKO.js.map} +0 -0
  118. /package/dist/cli/{chunk-JMDE6IO3.js.map → chunk-ZAEJWKXB.js.map} +0 -0
  119. /package/dist/cli/{code-PMPJWXEO.js.map → code-R4IHI7SR.js.map} +0 -0
  120. /package/dist/cli/{commands-QS6TG4G3.js.map → commands-DRHFCYMO.js.map} +0 -0
  121. /package/dist/cli/{commit-XPRSKUBF.js.map → commit-AG5KB4YP.js.map} +0 -0
  122. /package/dist/cli/{diff-I6W4AUWJ.js.map → diff-4Z7ETWZO.js.map} +0 -0
  123. /package/dist/cli/{doctor-6XVZKT4U.js.map → doctor-VA3RHQLB.js.map} +0 -0
  124. /package/dist/cli/{mcp-browse-LA4I4YIZ.js.map → mcp-browse-C3GXVMYZ.js.map} +0 -0
  125. /package/dist/cli/{mcp-inspect-LWXXU7BY.js.map → mcp-inspect-ZMYUNFDS.js.map} +0 -0
  126. /package/dist/cli/{prompt-RKZD4X6Y.js.map → prompt-MC3U5KRP.js.map} +0 -0
  127. /package/dist/cli/{replay-2X7MVXOI.js.map → replay-4TP7ZUMZ.js.map} +0 -0
  128. /package/dist/cli/{setup-GOLP7J4C.js.map → setup-LHZELI6I.js.map} +0 -0
  129. /package/dist/cli/{stats-CGDAFDKI.js.map → stats-SUIJ3QWY.js.map} +0 -0
  130. /package/dist/cli/{version-FIL4ZFOS.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",
@@ -466,12 +584,11 @@ function memoryTypeDefaults(typeName, cfg = readConfig()) {
466
584
  if (found.expires) out.expires = found.expires;
467
585
  return out;
468
586
  }
469
- var DEFAULT_METASO_API_KEY = "mk-E384C1DD5E8501BB7EFE27C949AFDE5B";
470
587
  function loadMetasoApiKey(path2 = defaultConfigPath()) {
471
- if (process.env.METASO_API_KEY) return process.env.METASO_API_KEY;
588
+ if (process.env.METASO_API_KEY) return process.env.METASO_API_KEY.trim();
472
589
  const cfg = readConfig(path2).metasoApiKey;
473
590
  if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
474
- return DEFAULT_METASO_API_KEY;
591
+ return void 0;
475
592
  }
476
593
  function loadTavilyApiKey(path2 = defaultConfigPath()) {
477
594
  if (process.env.TAVILY_API_KEY) return process.env.TAVILY_API_KEY.trim();
@@ -543,10 +660,30 @@ function readConfig(path2 = defaultConfigPath()) {
543
660
  return {};
544
661
  }
545
662
  function writeConfig(cfg, path2 = defaultConfigPath()) {
663
+ debugLogConfigWrite(cfg, path2);
546
664
  mkdirSync(dirname(path2), { recursive: true });
547
- writeFileSync(path2, JSON.stringify(cfg, null, 2), "utf8");
665
+ const tmp = `${path2}.${process.pid}.tmp`;
666
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2), "utf8");
667
+ try {
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;
548
676
  try {
549
- chmodSync(path2, 384);
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);
550
687
  } catch {
551
688
  }
552
689
  }
@@ -634,7 +771,7 @@ function webSearchEngine(path2 = defaultConfigPath()) {
634
771
  if (cfg === "tavily") return "tavily";
635
772
  if (cfg === "perplexity") return "perplexity";
636
773
  if (cfg === "exa") return "exa";
637
- return "mojeek";
774
+ return "bing";
638
775
  }
639
776
  function webSearchEndpoint(path2 = defaultConfigPath()) {
640
777
  const cfg = readConfig(path2).webSearchEndpoint;
@@ -669,6 +806,11 @@ function addProjectShellAllowed(rootDir, prefix, path2 = defaultConfigPath()) {
669
806
  cfg.projects[key].shellAllowed = [...existing, trimmed];
670
807
  writeConfig(cfg, path2);
671
808
  }
809
+ function projectHooksTrusted(rootDir, path2 = defaultConfigPath()) {
810
+ const cfg = readConfig(path2);
811
+ const key = findProjectKey(cfg, rootDir);
812
+ return key !== void 0 && cfg.projects?.[key]?.hooksTrusted === true;
813
+ }
672
814
  function loadProjectPathAllowed(rootDir, path2 = defaultConfigPath()) {
673
815
  const cfg = readConfig(path2);
674
816
  const key = findProjectKey(cfg, rootDir);
@@ -749,8 +891,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
749
891
  }
750
892
  function sleep(ms, signal) {
751
893
  if (ms <= 0) return Promise.resolve();
752
- return new Promise((resolve13, reject) => {
753
- const timer = setTimeout(resolve13, ms);
894
+ return new Promise((resolve14, reject) => {
895
+ const timer = setTimeout(resolve14, ms);
754
896
  if (signal) {
755
897
  const onAbort = () => {
756
898
  clearTimeout(timer);
@@ -838,8 +980,8 @@ var DeepSeekClient = class {
838
980
  const waitMs = Math.max(0, this.nextChatRequestAt - now);
839
981
  this.nextChatRequestAt = Math.max(now, this.nextChatRequestAt) + this.minChatIntervalMs;
840
982
  if (waitMs <= 0) return;
841
- await new Promise((resolve13, reject) => {
842
- const timer = setTimeout(resolve13, waitMs);
983
+ await new Promise((resolve14, reject) => {
984
+ const timer = setTimeout(resolve14, waitMs);
843
985
  signal?.addEventListener(
844
986
  "abort",
845
987
  () => {
@@ -913,8 +1055,11 @@ var DeepSeekClient = class {
913
1055
  }
914
1056
  async chat(opts) {
915
1057
  const ctrl = new AbortController();
916
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
917
- 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;
918
1063
  try {
919
1064
  await this.waitForChatRateLimit(signal);
920
1065
  const resp = await fetchWithRetry(
@@ -949,8 +1094,11 @@ var DeepSeekClient = class {
949
1094
  }
950
1095
  async *stream(opts) {
951
1096
  const ctrl = new AbortController();
952
- const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
953
- 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;
954
1102
  let resp;
955
1103
  try {
956
1104
  await this.waitForChatRateLimit(signal);
@@ -1049,10 +1197,10 @@ var PauseGate = class {
1049
1197
  `${kind}: no confirmation listener registered \u2014 cannot prompt the user. This tool can only be used inside an interactive Reasonix session.`
1050
1198
  );
1051
1199
  }
1052
- return new Promise((resolve13) => {
1200
+ return new Promise((resolve14) => {
1053
1201
  const id = this._nextId++;
1054
1202
  const request = { id, kind, payload };
1055
- this._pending.set(id, { resolve: resolve13, request });
1203
+ this._pending.set(id, { resolve: resolve14, request });
1056
1204
  for (const fn of this._listeners) {
1057
1205
  try {
1058
1206
  fn(request);
@@ -1207,7 +1355,17 @@ var EN = {
1207
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"
1208
1356
  },
1209
1357
  sessions: {
1210
- 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})."
1211
1369
  },
1212
1370
  ui: {
1213
1371
  welcome: "Run `reasonix` any time to start chatting \u2014 your settings are remembered.",
@@ -1311,10 +1469,6 @@ var EN = {
1311
1469
  title: "copy / paste",
1312
1470
  rows: [
1313
1471
  { key: "select text", text: "drag to select \u2014 terminal-native (no modifier needed)" },
1314
- {
1315
- key: "/copy",
1316
- text: "vim/tmux-style copy mode \u2014 works in SSH/mosh/tmux where drag-select can't extend past the viewport"
1317
- },
1318
1472
  {
1319
1473
  key: "copy",
1320
1474
  text: "Ctrl+Shift+C (Win/Linux) \xB7 Cmd+C (macOS) \u2014 or auto-copy-on-select if your terminal does it"
@@ -1405,9 +1559,6 @@ var EN = {
1405
1559
  },
1406
1560
  slash: {
1407
1561
  help: { description: "show the full command reference" },
1408
- copy: {
1409
- description: "open vim/tmux-style copy mode \u2014 j/k navigate, v select, y yank to clipboard"
1410
- },
1411
1562
  status: { description: "current model, flags, context, session" },
1412
1563
  preset: {
1413
1564
  description: "model bundle \u2014 auto escalates flash \u2192 pro, flash/pro lock",
@@ -1559,8 +1710,8 @@ var EN = {
1559
1710
  argsHint: "<question>"
1560
1711
  },
1561
1712
  "search-engine": {
1562
- 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)",
1563
- 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>]"
1564
1715
  }
1565
1716
  },
1566
1717
  wizard: {
@@ -1723,6 +1874,8 @@ var EN = {
1723
1874
  verboseOn: "\u25B8 verbose mode on \u2014 full reasoning + tool output",
1724
1875
  verboseOff: "\u25B8 verbose mode off \u2014 head/tail elision restored",
1725
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",
1726
1879
  btwUsage: "\u25B8 /btw <question> \u2014 ask a side question without polluting the conversation context.",
1727
1880
  btwHeader: "\u226B btw",
1728
1881
  btwFailed: "/btw failed",
@@ -1766,7 +1919,11 @@ var EN = {
1766
1919
  editHistoryHelpShow: "/show <id> \u2192 per-file summary \xB7 /show <id> <path> \u2192 full diff of one file",
1767
1920
  editHistoryHelpUndo: "/undo \u2192 newest non-undone \xB7 /undo <id> [path] \u2192 target a specific batch or file",
1768
1921
  editHistoryAlreadyReverted: "(already reverted \u2014 /history shows the batch-level status)",
1769
- editHistoryRevertFile: "/undo {id} {path} \u2192 revert just this file"
1922
+ editHistoryRevertFile: "/undo {id} {path} \u2192 revert just this file",
1923
+ mcpFailed: "MCP {name} failed",
1924
+ mcpWarn: "MCP {name} warn",
1925
+ unknownTheme: "unknown theme: {name}\navailable: {choices}",
1926
+ themeSaved: "theme saved: {name}\nactive on next launch: {active}"
1770
1927
  },
1771
1928
  hooks: {
1772
1929
  head: "hook {tag} `{cmd}` {decision}{truncTag}",
@@ -1789,9 +1946,9 @@ var EN = {
1789
1946
  abortedAtIter: "aborted at iter {iter} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)",
1790
1947
  toolUploadStatus: "tool result uploaded \xB7 model thinking before next response\u2026",
1791
1948
  preflightTruncateStatus: "preflight: context near full, truncating oldest history\u2026",
1792
- preflightTruncated: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \u2014 truncated {beforeMessages} messages \u2192 {afterMessages}. Sending.",
1793
- preflightTruncatedStillFull: "preflight: request still ~{estimate}/{ctxMax} tokens ({pct}%) after truncating {beforeMessages} messages \u2192 {afterMessages}. DeepSeek will likely 400. Run /clear or /new to start fresh.",
1794
- preflightNoFold: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) and nothing left to truncate \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.",
1949
+ preflightTruncated: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \xB7 body {bodyKB} KB \u2014 truncated {beforeMessages} messages \u2192 {afterMessages}. Sending.",
1950
+ preflightTruncatedStillFull: "preflight: request still ~{estimate}/{ctxMax} tokens ({pct}%) \xB7 body {bodyKB} KB after truncating {beforeMessages} messages \u2192 {afterMessages}. DeepSeek will likely 400. Run /clear or /new to start fresh.",
1951
+ preflightNoFold: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \xB7 body {bodyKB} KB and nothing left to truncate \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.",
1795
1952
  flashEscalation: "\u21E7 flash requested escalation \u2014 retrying this turn on {model}{reasonSuffix}",
1796
1953
  harvestStatus: "extracting plan state from reasoning\u2026",
1797
1954
  repeatToolCallWarning: "Caught a repeated tool call \u2014 let the model see the issue and retry with a different approach.",
@@ -1810,6 +1967,7 @@ var EN = {
1810
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.",
1811
1968
  badparam422: "Invalid parameter (DeepSeek 422): {inner}",
1812
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.",
1813
1971
  deepseek5xxHead: "DeepSeek service unavailable ({status}) \u2014 this is a DeepSeek-side problem, not Reasonix. Already retried 4\xD7 with backoff.",
1814
1972
  deepseek5xxReachable: " DeepSeek's main API answered our health check, but /chat/completions is failing \u2014 partial outage on their side.",
1815
1973
  deepseek5xxUnreachable: " DeepSeek API is unreachable from your network \u2014 could be a wider DS outage or a local network issue.",
@@ -2078,6 +2236,12 @@ var EN = {
2078
2236
  statusMcp: " mcp {servers} server(s), {tools} tool(s) in registry",
2079
2237
  statusEdits: " edits {count} pending (/apply to commit, /discard to drop)",
2080
2238
  statusPlan: " plan ON \u2014 writes gated (submit_plan + approval)",
2239
+ statusLifecycle: " lifecycle {mode}/{state} \xB7 {progress}{evidence}",
2240
+ lifecycleNoPlan: "no plan",
2241
+ lifecycleEvidencePending: "evidence pending",
2242
+ lifecycleRejected: "lifecycle: {tool} blocked in {state} \u2014 next: {next}",
2243
+ lifecycleEvidenceRejected: "lifecycle: step {stepId} needs evidence \u2014 next: {next}",
2244
+ lifecycleRepeatedRejected: "lifecycle: repeated {tool} rejection \u2014 do not retry identical args",
2081
2245
  statusModeYolo: " mode YOLO \u2014 edits + shell auto-run with no prompt (/undo still rolls back \xB7 Shift+Tab to flip)",
2082
2246
  statusModeAuto: " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)",
2083
2247
  statusModeReview: " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)",
@@ -2089,6 +2253,9 @@ var EN = {
2089
2253
  activeNone: "\u25B8 active plan: (none)",
2090
2254
  noArchives: "no archived plans yet for this session \u2014 they auto-archive when every step is done",
2091
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}",
2092
2259
  replayNoSession: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session.",
2093
2260
  replayNoArchives: "no archived plans yet for this session \u2014 `/replay` lights up once a plan completes (auto-archives when every step is done).",
2094
2261
  replayInvalidIndex: "invalid index \u2014 `/replay` takes 1..{max} (newest = 1). Use `/plans` to see the list.",
@@ -2183,7 +2350,7 @@ var EN = {
2183
2350
  currentEngine: "Current web search engine: {engine}",
2184
2351
  endpoint: "SearXNG endpoint: {url}",
2185
2352
  usageHeader: "Usage:",
2186
- usageMojeek: " /search-engine mojeek use Mojeek (default, no external deps)",
2353
+ usageBing: " /search-engine bing use Bing (default, works from CN without proxy)",
2187
2354
  usageSearxng: " /search-engine searxng use SearXNG at default endpoint",
2188
2355
  usageSearxngUrl: " /search-engine searxng <url> use SearXNG at custom endpoint",
2189
2356
  usageMetaso: " /search-engine metaso use Metaso API (100/d free, configure your own API key for more)",
@@ -2248,7 +2415,8 @@ var EN = {
2248
2415
  evt: " evt",
2249
2416
  editsLabel: "edits:",
2250
2417
  mcpLoading: "MCP",
2251
- ctx: "ctx"
2418
+ ctx: "ctx",
2419
+ shortcutsHint: "Ctrl+P shortcuts"
2252
2420
  },
2253
2421
  editMode: {
2254
2422
  plan: "PLAN MODE",
@@ -2278,7 +2446,9 @@ var EN = {
2278
2446
  editorFailed: "external editor:",
2279
2447
  editorMissing: "no $EDITOR / $VISUAL / $GIT_EDITOR set \u2014 export one (e.g. `export EDITOR=nano`) and retry",
2280
2448
  editorExited: "editor exited with code {code}",
2281
- 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"
2282
2452
  },
2283
2453
  pathConfirm: {
2284
2454
  title: "Outside-sandbox path",
@@ -2297,7 +2467,13 @@ var EN = {
2297
2467
  denyDesc: "press Tab to add context telling the model why",
2298
2468
  pathLabel: "path",
2299
2469
  sandboxLabel: "sandbox",
2300
- 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"
2301
2477
  },
2302
2478
  shellConfirm: {
2303
2479
  title: "Shell command",
@@ -2319,7 +2495,12 @@ var EN = {
2319
2495
  timeoutLabel: "timeout",
2320
2496
  waitLabel: "wait",
2321
2497
  previewMore: "\u2026 {n} more line hidden \u2014 press esc, ask the model to split it",
2322
- 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"
2323
2504
  },
2324
2505
  editConfirm: {
2325
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",
@@ -2460,36 +2641,37 @@ var EN = {
2460
2641
  probeFailed: "probe failed \u2014 {message}"
2461
2642
  },
2462
2643
  webErrors: {
2463
- 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",
2464
2645
  rateLimit429: "web_search 429 \u2014 try: wait 10s before retrying, or rephrase the query; the search backend is rate-limiting this client",
2465
- 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",
2466
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",
2467
- 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",
2468
- 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",
2469
2650
  invalidEndpoint: 'web_search: invalid SearXNG endpoint "{endpoint}" \u2014 try: set a valid URL with /search-endpoint http://host:port',
2470
2651
  endpointMustBeHttp: "web_search: SearXNG endpoint must be http(s), got {protocol} \u2014 try: set a valid URL with /search-endpoint http://host:port",
2471
- 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",
2472
- 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",
2473
- metasoDailyLimit: "web_search: daily search limit reached for the default API key \u2014 set your own METASO_API_KEY env var or get one at https://metaso.cn/search-api/playground",
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",
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",
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",
2474
2656
  metasoUnauthorized: "web_search: Metaso API key rejected \u2014 check METASO_API_KEY or get one at https://metaso.cn/search-api/playground",
2475
2657
  metasoRateLimit: "web_search: Metaso rate-limited \u2014 wait and retry, or get your own API key at https://metaso.cn/search-api/playground",
2476
- 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",
2477
2659
  metasoParseError: "web_search: Metaso returned unparseable response (HTTP {status}) \u2014 try again later",
2478
2660
  metasoApiError: "web_search: Metaso API error (code {code}: {message}) \u2014 try again later",
2479
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",
2480
2662
  tavilyUnauthorized: "web_search: Tavily API key rejected \u2014 check TAVILY_API_KEY or get one at https://tavily.com",
2481
- 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",
2482
- 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",
2483
2665
  tavilyParseError: "web_search: Tavily returned unparseable response (HTTP {status}) \u2014 try again later",
2484
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",
2485
2667
  perplexityUnauthorized: "web_search: Perplexity API key rejected \u2014 check PERPLEXITY_API_KEY or get one at https://perplexity.ai/settings/api",
2486
- perplexityRateLimit: "web_search: Perplexity rate-limited \u2014 wait and retry, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2487
- 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",
2488
2670
  perplexityParseError: "web_search: Perplexity returned unparseable response (HTTP {status}) \u2014 try again later",
2489
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",
2490
2672
  exaUnauthorized: "web_search: Exa API key rejected \u2014 check EXA_API_KEY or get one at https://exa.ai",
2491
2673
  exaRateLimit: "web_search: Exa API rate-limited or monthly quota exceeded \u2014 wait or upgrade at https://exa.ai/pricing",
2492
- 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",
2493
2675
  exaParseError: "web_search: Exa returned unparseable response (HTTP {status}) \u2014 try again later",
2494
2676
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2495
2677
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
@@ -2581,19 +2763,6 @@ var EN = {
2581
2763
  categoryProject: "project",
2582
2764
  categoryReference: "reference"
2583
2765
  },
2584
- copyMode: {
2585
- title: "\u2500\u2500 COPY MODE \u2500\u2500",
2586
- help: "j/k or \u2191/\u2193 move \xB7 v select \xB7 y yank \xB7 g/G top/bottom \xB7 q quit",
2587
- statusBar: "line {cur}/{total} \xB7 selection: {sel}",
2588
- statusYanked: "yanked {size} chars (osc52={osc52})",
2589
- statusEmpty: "nothing selected",
2590
- empty: "(no chat content yet \u2014 say something to the model first)",
2591
- labelUser: "you",
2592
- labelAssistant: "assistant",
2593
- labelReasoning: "reasoning",
2594
- yankedToast: "\u25B8 copied {size} chars to clipboard (osc52)",
2595
- yankedToastFile: "\u25B8 copied {size} chars \xB7 file: {path}"
2596
- },
2597
2766
  mcpHealth: {
2598
2767
  noData: "no inspect data",
2599
2768
  healthy: "healthy \xB7 {ms}ms",
@@ -2662,7 +2831,13 @@ var EN = {
2662
2831
  noResources: "No resources on any connected MCP server (or no servers connected). `/mcp` shows the current set.",
2663
2832
  readOne: "Read one: `/resource <uri>` \u2014 or use Tab in the picker.",
2664
2833
  noPrompts: "No prompts on any connected MCP server (or no servers connected). `/mcp` shows the current set.",
2665
- fetchOne: "Fetch one: `/prompt <name>` \u2014 args are not supported yet; prompts with required args will surface an error from the server."
2834
+ fetchOne: "Fetch one: `/prompt <name>` \u2014 args are not supported yet; prompts with required args will surface an error from the server.",
2835
+ noServerForResource: 'no server exposes resource "{name}"',
2836
+ resourceHint: "`/resource` with no arg lists what's available.",
2837
+ readFailed: "readResource failed",
2838
+ noServerForPrompt: 'no server exposes prompt "{name}"',
2839
+ promptHint: "`/prompt` with no arg lists what's available.",
2840
+ fetchFailed: "getPrompt failed"
2666
2841
  },
2667
2842
  mcpLifecycle: {
2668
2843
  handshake: "handshake\u2026",
@@ -2675,7 +2850,9 @@ var EN = {
2675
2850
  disabledDetail: "via /mcp disable {name}",
2676
2851
  failedSetupHint: "\u2192 run `reasonix setup` to remove this entry, or fix the underlying issue (missing npm package, network, etc.).",
2677
2852
  failedSetupConfigHint: "\u2192 run `reasonix setup` to remove broken entries from your saved config.",
2678
- abortedHint: "MCP startup aborted \u2014 {count} server(s) skipped. Run /mcp to retry once you've fixed the underlying issue."
2853
+ abortedHint: "MCP startup aborted \u2014 {count} server(s) skipped. Run /mcp to retry once you've fixed the underlying issue.",
2854
+ toolsReady: "tools ready",
2855
+ warnLabel: "warn"
2679
2856
  },
2680
2857
  checkpointPicker: {
2681
2858
  title: "restore a checkpoint \u2014 {workspace}",
@@ -2721,6 +2898,64 @@ var EN = {
2721
2898
  noRecords: "no records",
2722
2899
  untracked: "(untracked)",
2723
2900
  churned: "(churned \xD7{count})"
2901
+ },
2902
+ builtinSkills: {
2903
+ explore: "Explore the codebase in an isolated subagent \u2014 wide-net read-only investigation that returns one distilled answer. Best for: 'find all places that\u2026', 'how does X work across the project', 'survey the code for Y'.",
2904
+ research: "Research a question by combining web search + code reading in an isolated subagent. Best for: 'is X feature supported by lib Y', 'what\u2019s the canonical way to do Z', 'compare our impl against the spec'.",
2905
+ review: "Review the pending changes (current branch diff by default) in an isolated subagent \u2014 flags correctness, security, missing tests, hidden behavior changes; reports verdict + per-issue file:line. Read-only; the parent decides what to act on.",
2906
+ securityReview: "Security-focused review of the current branch diff in an isolated subagent \u2014 flags injection/authz/secrets/deserialization/path-traversal/crypto issues, severity-tagged. Read-only. Use when shipping changes that touch auth, input parsing, file IO, or external requests.",
2907
+ test: "Run the project\u2019s test suite, diagnose failures, propose SEARCH/REPLACE fixes, re-run until green (or stop after 2 fix attempts on the same failure). Inlined \u2014 runs in the parent loop so you see the edit blocks and can /apply them. Detects npm/pnpm/yarn/pytest/go/cargo."
2908
+ },
2909
+ shortcutsHelp: {
2910
+ title: "Shortcuts",
2911
+ groupInput: "Input",
2912
+ groupNavigation: "Navigation",
2913
+ groupSession: "Session",
2914
+ groupSystem: "System",
2915
+ descEnter: "Send message",
2916
+ descShiftEnter: "New line",
2917
+ descCtrlEnter: "New line",
2918
+ descCtrlJ: "New line",
2919
+ descCtrlU: "Clear input",
2920
+ descCtrlW: "Delete word",
2921
+ descCtrlP: "Show/hide shortcuts",
2922
+ descCtrlX: "Open in editor",
2923
+ descArrows: "Input history",
2924
+ descPgUpDown: "Scroll page",
2925
+ descCtrlL: "Clear screen",
2926
+ descCtrlB: "Toggle sidebar",
2927
+ descNewSession: "New session",
2928
+ descListSessions: "List sessions",
2929
+ descSwitchModel: "Switch model",
2930
+ descSwitchPreset: "Switch preset",
2931
+ descSwitchTheme: "Switch theme",
2932
+ descCtrlC: "Quit",
2933
+ descEsc: "Stop / Cancel",
2934
+ descCtrlR: "Toggle verbose",
2935
+ descCtrlO: "Expand reply (streaming only)",
2936
+ descHelp: "Show all commands",
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}"
2724
2959
  }
2725
2960
  };
2726
2961
 
@@ -2767,7 +3002,17 @@ var zhCN = {
2767
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"
2768
3003
  },
2769
3004
  sessions: {
2770
- 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"
2771
3016
  },
2772
3017
  ui: {
2773
3018
  welcome: "\u968F\u65F6\u8FD0\u884C `reasonix` \u5F00\u59CB\u804A\u5929 \u2014 \u60A8\u7684\u8BBE\u7F6E\u5C06\u88AB\u8BB0\u4F4F\u3002",
@@ -2865,10 +3110,6 @@ var zhCN = {
2865
3110
  title: "\u590D\u5236 / \u7C98\u8D34",
2866
3111
  rows: [
2867
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" },
2868
- {
2869
- key: "/copy",
2870
- 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"
2871
- },
2872
3113
  {
2873
3114
  key: "\u590D\u5236",
2874
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"
@@ -2959,9 +3200,6 @@ var zhCN = {
2959
3200
  },
2960
3201
  slash: {
2961
3202
  help: { description: "\u663E\u793A\u5B8C\u6574\u547D\u4EE4\u53C2\u8003" },
2962
- copy: {
2963
- 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"
2964
- },
2965
3203
  status: { description: "\u5F53\u524D\u6A21\u578B\u3001\u6807\u5FD7\u3001\u4E0A\u4E0B\u6587\u3001\u4F1A\u8BDD" },
2966
3204
  preset: {
2967
3205
  description: "\u6A21\u578B\u7EC4\u5408 \u2014 \u81EA\u52A8\u5728 flash \u2192 pro \u4E4B\u95F4\u5207\u6362\uFF0C\u6216\u9501\u5B9A flash/pro",
@@ -3117,8 +3355,8 @@ var zhCN = {
3117
3355
  argsHint: "<question>"
3118
3356
  },
3119
3357
  "search-engine": {
3120
- 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",
3121
- 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>]"
3122
3360
  }
3123
3361
  },
3124
3362
  wizard: {
@@ -3281,6 +3519,8 @@ var zhCN = {
3281
3519
  verboseOn: "\u25B8 \u8BE6\u7EC6\u6A21\u5F0F\u5DF2\u5F00 \u2014 \u663E\u793A\u5B8C\u6574\u63A8\u7406 + \u5DE5\u5177\u8F93\u51FA",
3282
3520
  verboseOff: "\u25B8 \u8BE6\u7EC6\u6A21\u5F0F\u5DF2\u5173 \u2014 \u6062\u590D\u5934\u5C3E\u7701\u7565",
3283
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",
3284
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",
3285
3525
  btwHeader: "\u226B btw",
3286
3526
  btwFailed: "/btw \u8C03\u7528\u5931\u8D25",
@@ -3324,7 +3564,11 @@ var zhCN = {
3324
3564
  editHistoryHelpShow: "/show <id> \u2192 \u6587\u4EF6\u6458\u8981 \xB7 /show <id> <path> \u2192 \u67D0\u4E2A\u6587\u4EF6\u7684\u5B8C\u6574 diff",
3325
3565
  editHistoryHelpUndo: "/undo \u2192 \u6700\u65B0\u7684\u672A\u64A4\u9500\u9879 \xB7 /undo <id> [path] \u2192 \u6307\u5B9A\u6279\u6B21\u6216\u6587\u4EF6",
3326
3566
  editHistoryAlreadyReverted: "\uFF08\u5DF2\u64A4\u9500 \u2014 /history \u663E\u793A\u6279\u6B21\u7EA7\u72B6\u6001\uFF09",
3327
- editHistoryRevertFile: "/undo {id} {path} \u2192 \u4EC5\u8FD8\u539F\u6B64\u6587\u4EF6"
3567
+ editHistoryRevertFile: "/undo {id} {path} \u2192 \u4EC5\u8FD8\u539F\u6B64\u6587\u4EF6",
3568
+ mcpFailed: "MCP {name} \u5931\u8D25",
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}"
3328
3572
  },
3329
3573
  hooks: {
3330
3574
  head: "\u94A9\u5B50 {tag} `{cmd}` {decision}{truncTag}",
@@ -3347,9 +3591,9 @@ var zhCN = {
3347
3591
  abortedAtIter: "\u5728\u7B2C {iter} \u6B21\u5DE5\u5177\u8C03\u7528\u5904\u4E2D\u65AD \u2014 \u672A\u751F\u6210\u603B\u7ED3\u5373\u505C\u6B62\uFF08\u6309 \u2191 + Enter \u6216 /retry \u6062\u590D\uFF09",
3348
3592
  toolUploadStatus: "\u5DE5\u5177\u7ED3\u679C\u5DF2\u4E0A\u4F20 \xB7 \u6A21\u578B\u5728\u751F\u6210\u4E0B\u4E00\u6761\u54CD\u5E94\u524D\u601D\u8003\u4E2D\u2026",
3349
3593
  preflightTruncateStatus: "\u9884\u68C0\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\uFF0C\u6B63\u5728\u88C1\u526A\u6700\u65E9\u5386\u53F2\u2026",
3350
- preflightTruncated: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u2014 \u5DF2\u88C1\u526A {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages}\u3002\u53D1\u9001\u4E2D\u3002",
3351
- preflightTruncatedStillFull: "\u9884\u68C0\uFF1A\u88C1\u526A {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages} \u540E\uFF0C\u8BF7\u6C42\u4ECD\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
3352
- preflightNoFold: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\u4E14\u6CA1\u6709\u53EF\u88C1\u526A\u7684\u5185\u5BB9 \u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
3594
+ preflightTruncated: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\xB7 body {bodyKB} KB \u2014 \u5DF2\u88C1\u526A {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages}\u3002\u53D1\u9001\u4E2D\u3002",
3595
+ preflightTruncatedStillFull: "\u9884\u68C0\uFF1A\u88C1\u526A {beforeMessages} \u6761\u6D88\u606F \u2192 {afterMessages} \u540E\uFF0C\u8BF7\u6C42\u4ECD\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\xB7 body {bodyKB} KB \u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
3596
+ preflightNoFold: "\u9884\u68C0\uFF1A\u8BF7\u6C42\u7EA6 {estimate}/{ctxMax} tokens\uFF08{pct}%\uFF09\xB7 body {bodyKB} KB \u4E14\u6CA1\u6709\u53EF\u88C1\u526A\u7684\u5185\u5BB9 \u2014 DeepSeek \u5927\u6982\u7387\u4F1A\u8FD4\u56DE 400\u3002\u8BF7\u8FD0\u884C /clear \u6216 /new \u91CD\u65B0\u5F00\u59CB\u3002",
3353
3597
  flashEscalation: "\u21E7 flash \u8BF7\u6C42\u5347\u7EA7 \u2014 \u672C\u8F6E\u6539\u7528 {model}{reasonSuffix}",
3354
3598
  harvestStatus: "\u6B63\u5728\u4ECE\u63A8\u7406\u8FC7\u7A0B\u63D0\u53D6\u8BA1\u5212\u72B6\u6001\u2026",
3355
3599
  repeatToolCallWarning: "\u62E6\u622A\u5230\u91CD\u590D\u5DE5\u5177\u8C03\u7528 \u2014 \u8BA9\u6A21\u578B\u5BDF\u89C9\u95EE\u9898\u5E76\u6362\u79CD\u65B9\u5F0F\u91CD\u8BD5\u3002",
@@ -3368,6 +3612,7 @@ var zhCN = {
3368
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",
3369
3613
  badparam422: "\u53C2\u6570\u9519\u8BEF\uFF08DeepSeek 422\uFF09\uFF1A{inner}",
3370
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",
3371
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",
3372
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",
3373
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",
@@ -3636,6 +3881,12 @@ var zhCN = {
3636
3881
  statusMcp: " MCP {servers} \u4E2A\u670D\u52A1\u5668\uFF0C\u6CE8\u518C\u8868\u4E2D {tools} \u4E2A\u5DE5\u5177",
3637
3882
  statusEdits: " \u7F16\u8F91 {count} \u4E2A\u5F85\u5904\u7406\uFF08/apply \u63D0\u4EA4\uFF0C/discard \u4E22\u5F03\uFF09",
3638
3883
  statusPlan: " \u8BA1\u5212 \u5F00\u542F \u2014 \u5199\u5165\u53D7\u9650\uFF08submit_plan + \u5BA1\u6279\uFF09",
3884
+ statusLifecycle: " \u751F\u547D\u5468\u671F {mode}/{state} \xB7 {progress}{evidence}",
3885
+ lifecycleNoPlan: "\u6682\u65E0\u8BA1\u5212",
3886
+ lifecycleEvidencePending: "\u7B49\u5F85 evidence",
3887
+ lifecycleRejected: "lifecycle\uFF1A{tool} \u5728 {state} \u72B6\u6001\u88AB\u62E6\u622A \u2014 \u4E0B\u4E00\u6B65\uFF1A{next}",
3888
+ lifecycleEvidenceRejected: "lifecycle\uFF1A\u6B65\u9AA4 {stepId} \u9700\u8981 evidence \u2014 \u4E0B\u4E00\u6B65\uFF1A{next}",
3889
+ lifecycleRepeatedRejected: "lifecycle\uFF1A{tool} \u88AB\u91CD\u590D\u62E6\u622A \u2014 \u4E0D\u8981\u7528\u76F8\u540C\u53C2\u6570\u53CD\u590D\u91CD\u8BD5",
3639
3890
  statusModeYolo: " \u6A21\u5F0F YOLO \u2014 \u7F16\u8F91 + shell \u81EA\u52A8\u8FD0\u884C\uFF0C\u65E0\u63D0\u793A\uFF08/undo \u4ECD\u53EF\u56DE\u6EDA \xB7 Shift+Tab \u5207\u6362\uFF09",
3640
3891
  statusModeAuto: " \u6A21\u5F0F AUTO \u2014 \u7F16\u8F91\u7ACB\u5373\u5E94\u7528\uFF085 \u79D2\u5185\u6309 u \u64A4\u6D88 \xB7 Shift+Tab \u5207\u6362\uFF09",
3641
3892
  statusModeReview: " \u6A21\u5F0F review \u2014 \u7F16\u8F91\u6392\u961F\u7B49\u5F85 /apply \u6216 y\uFF08Shift+Tab \u5207\u6362\uFF09",
@@ -3647,6 +3898,9 @@ var zhCN = {
3647
3898
  activeNone: "\u25B8 \u6D3B\u8DC3\u8BA1\u5212\uFF1A\uFF08\u65E0\uFF09",
3648
3899
  noArchives: "\u6B64\u4F1A\u8BDD\u5C1A\u65E0\u5F52\u6863\u8BA1\u5212 \u2014 \u5F53\u6BCF\u4E2A\u6B65\u9AA4\u5B8C\u6210\u65F6\u81EA\u52A8\u5F52\u6863",
3649
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}",
3650
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",
3651
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",
3652
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",
@@ -3741,7 +3995,7 @@ var zhCN = {
3741
3995
  currentEngine: "\u5F53\u524D\u7F51\u9875\u641C\u7D22\u5F15\u64CE\uFF1A{engine}",
3742
3996
  endpoint: "SearXNG \u7AEF\u70B9\uFF1A{url}",
3743
3997
  usageHeader: "\u7528\u6CD5\uFF1A",
3744
- 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",
3745
3999
  usageSearxng: " /search-engine searxng \u4F7F\u7528 SearXNG \u9ED8\u8BA4\u7AEF\u70B9",
3746
4000
  usageSearxngUrl: " /search-engine searxng <url> \u4F7F\u7528 SearXNG \u81EA\u5B9A\u4E49\u7AEF\u70B9",
3747
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",
@@ -3806,7 +4060,8 @@ var zhCN = {
3806
4060
  evt: " \u4E8B\u4EF6",
3807
4061
  editsLabel: "\u7F16\u8F91:",
3808
4062
  mcpLoading: "MCP",
3809
- ctx: "\u4E0A\u4E0B\u6587"
4063
+ ctx: "\u4E0A\u4E0B\u6587",
4064
+ shortcutsHint: "Ctrl+P \u5FEB\u6377\u952E"
3810
4065
  },
3811
4066
  editMode: {
3812
4067
  plan: "\u8BA1\u5212",
@@ -3836,7 +4091,9 @@ var zhCN = {
3836
4091
  editorFailed: "\u5916\u90E8\u7F16\u8F91\u5668\uFF1A",
3837
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",
3838
4093
  editorExited: "\u7F16\u8F91\u5668\u5F02\u5E38\u9000\u51FA\uFF0C\u8FD4\u56DE\u7801 {code}",
3839
- 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"
3840
4097
  },
3841
4098
  pathConfirm: {
3842
4099
  title: "\u6C99\u7BB1\u5916\u8DEF\u5F84",
@@ -3855,7 +4112,13 @@ var zhCN = {
3855
4112
  denyDesc: "\u6309 Tab \u6DFB\u52A0\u8BF4\u660E\uFF0C\u544A\u8BC9\u6A21\u578B\u539F\u56E0",
3856
4113
  pathLabel: "\u8DEF\u5F84",
3857
4114
  sandboxLabel: "\u6C99\u7BB1",
3858
- 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"
3859
4122
  },
3860
4123
  shellConfirm: {
3861
4124
  title: "Shell \u547D\u4EE4",
@@ -3877,7 +4140,12 @@ var zhCN = {
3877
4140
  timeoutLabel: "\u8D85\u65F6",
3878
4141
  waitLabel: "\u7B49\u5F85",
3879
4142
  previewMore: "\u2026 \u8FD8\u6709 {n} \u884C\u672A\u663E\u793A \u2014 \u6309 esc \u53D6\u6D88\uFF0C\u8BA9\u6A21\u578B\u62C6\u5206\u540E\u518D\u8BD5",
3880
- 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"
3881
4149
  },
3882
4150
  editConfirm: {
3883
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",
@@ -4018,36 +4286,37 @@ var zhCN = {
4018
4286
  probeFailed: "\u63A2\u6D4B\u5931\u8D25 \u2014 {message}"
4019
4287
  },
4020
4288
  webErrors: {
4021
- 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",
4022
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",
4023
- 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",
4024
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",
4025
- 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",
4026
- 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",
4027
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',
4028
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",
4029
- 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",
4030
- 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",
4031
- metasoDailyLimit: "web_search: \u9ED8\u8BA4 API \u5BC6\u94A5\u7684\u6BCF\u65E5\u641C\u7D22\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650 \u2014 \u8BBE\u7F6E METASO_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u81EA\u5DF1\u7684\u5BC6\u94A5",
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",
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",
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",
4032
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",
4033
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",
4034
- 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",
4035
4304
  metasoParseError: "web_search: Metaso \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4036
4305
  metasoApiError: "web_search: Metaso API \u9519\u8BEF\uFF08code {code}: {message}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4037
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",
4038
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",
4039
- 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",
4040
- 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",
4041
4310
  tavilyParseError: "web_search: Tavily \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4042
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",
4043
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",
4044
- 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",
4045
- 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",
4046
4315
  perplexityParseError: "web_search: Perplexity \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4047
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",
4048
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",
4049
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",
4050
- 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",
4051
4320
  exaParseError: "web_search: Exa \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4052
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",
4053
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",
@@ -4139,19 +4408,6 @@ var zhCN = {
4139
4408
  categoryProject: "\u9879\u76EE",
4140
4409
  categoryReference: "\u53C2\u8003"
4141
4410
  },
4142
- copyMode: {
4143
- title: "\u2500\u2500 \u590D\u5236\u6A21\u5F0F \u2500\u2500",
4144
- 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",
4145
- statusBar: "\u7B2C {cur}/{total} \u884C \xB7 \u9009\u533A\uFF1A{sel}",
4146
- statusYanked: "\u5DF2\u590D\u5236 {size} \u5B57\u7B26\uFF08osc52={osc52}\uFF09",
4147
- statusEmpty: "\u672A\u9009\u4E2D\u5185\u5BB9",
4148
- empty: "\uFF08\u8FD8\u6CA1\u6709\u804A\u5929\u5185\u5BB9 \u2014 \u5148\u548C\u6A21\u578B\u8BF4\u70B9\u4EC0\u4E48\uFF09",
4149
- labelUser: "\u4F60",
4150
- labelAssistant: "\u52A9\u624B",
4151
- labelReasoning: "\u63A8\u7406",
4152
- yankedToast: "\u25B8 \u5DF2\u590D\u5236 {size} \u5B57\u7B26\u5230\u526A\u8D34\u677F (osc52)",
4153
- yankedToastFile: "\u25B8 \u5DF2\u590D\u5236 {size} \u5B57\u7B26 \xB7 \u6587\u4EF6\uFF1A{path}"
4154
- },
4155
4411
  mcpHealth: {
4156
4412
  noData: "\u65E0\u68C0\u67E5\u6570\u636E",
4157
4413
  healthy: "\u6B63\u5E38 \xB7 {ms}ms",
@@ -4220,7 +4476,13 @@ var zhCN = {
4220
4476
  noResources: "\u6CA1\u6709\u4EFB\u4F55\u5DF2\u8FDE\u63A5 MCP \u670D\u52A1\u5668\u4E0A\u7684\u8D44\u6E90\uFF08\u6216\u65E0\u670D\u52A1\u5668\u8FDE\u63A5\uFF09\u3002`/mcp` \u663E\u793A\u5F53\u524D\u5217\u8868\u3002",
4221
4477
  readOne: "\u8BFB\u53D6\uFF1A`/resource <uri>` \u2014 \u6216\u5728\u9009\u62E9\u5668\u4E2D\u4F7F\u7528 Tab \u952E\u3002",
4222
4478
  noPrompts: "\u6CA1\u6709\u4EFB\u4F55\u5DF2\u8FDE\u63A5 MCP \u670D\u52A1\u5668\u4E0A\u7684\u63D0\u793A\uFF08\u6216\u65E0\u670D\u52A1\u5668\u8FDE\u63A5\uFF09\u3002`/mcp` \u663E\u793A\u5F53\u524D\u5217\u8868\u3002",
4223
- fetchOne: "\u83B7\u53D6\uFF1A`/prompt <name>` \u2014 \u6682\u4E0D\u652F\u6301\u53C2\u6570\uFF1B\u5E26\u5FC5\u9700\u53C2\u6570\u7684\u63D0\u793A\u5C06\u8FD4\u56DE\u670D\u52A1\u5668\u9519\u8BEF\u3002"
4479
+ fetchOne: "\u83B7\u53D6\uFF1A`/prompt <name>` \u2014 \u6682\u4E0D\u652F\u6301\u53C2\u6570\uFF1B\u5E26\u5FC5\u9700\u53C2\u6570\u7684\u63D0\u793A\u5C06\u8FD4\u56DE\u670D\u52A1\u5668\u9519\u8BEF\u3002",
4480
+ noServerForResource: '\u6CA1\u6709\u670D\u52A1\u5668\u66B4\u9732\u8D44\u6E90 "{name}"',
4481
+ resourceHint: "`/resource` \u4E0D\u5E26\u53C2\u6570\u53EF\u67E5\u770B\u53EF\u7528\u5217\u8868\u3002",
4482
+ readFailed: "\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25",
4483
+ noServerForPrompt: '\u6CA1\u6709\u670D\u52A1\u5668\u66B4\u9732 prompt "{name}"',
4484
+ promptHint: "`/prompt` \u4E0D\u5E26\u53C2\u6570\u53EF\u67E5\u770B\u53EF\u7528\u5217\u8868\u3002",
4485
+ fetchFailed: "\u83B7\u53D6 prompt \u5931\u8D25"
4224
4486
  },
4225
4487
  mcpLifecycle: {
4226
4488
  handshake: "\u63E1\u624B\u4E2D\u2026",
@@ -4233,7 +4495,9 @@ var zhCN = {
4233
4495
  disabledDetail: "\u901A\u8FC7 /mcp disable {name}",
4234
4496
  failedSetupHint: "\u2192 \u8FD0\u884C `reasonix setup` \u79FB\u9664\u6B64\u6761\u76EE\uFF0C\u6216\u4FEE\u590D\u5E95\u5C42\u95EE\u9898\uFF08\u7F3A\u5C11 npm \u5305\u3001\u7F51\u7EDC\u7B49\uFF09\u3002",
4235
4497
  failedSetupConfigHint: "\u2192 \u8FD0\u884C `reasonix setup` \u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u4E2D\u79FB\u9664\u635F\u574F\u7684\u6761\u76EE\u3002",
4236
- abortedHint: "\u5DF2\u4E2D\u65AD MCP \u542F\u52A8 \u2014 \u8DF3\u8FC7 {count} \u4E2A\u670D\u52A1\u5668\u3002\u95EE\u9898\u4FEE\u590D\u540E\u7528 /mcp \u91CD\u65B0\u8FDE\u63A5\u3002"
4498
+ abortedHint: "\u5DF2\u4E2D\u65AD MCP \u542F\u52A8 \u2014 \u8DF3\u8FC7 {count} \u4E2A\u670D\u52A1\u5668\u3002\u95EE\u9898\u4FEE\u590D\u540E\u7528 /mcp \u91CD\u65B0\u8FDE\u63A5\u3002",
4499
+ toolsReady: "\u5DE5\u5177\u5C31\u7EEA",
4500
+ warnLabel: "\u8B66\u544A"
4237
4501
  },
4238
4502
  checkpointPicker: {
4239
4503
  title: "\u6062\u590D\u68C0\u67E5\u70B9 \u2014 {workspace}",
@@ -4279,6 +4543,64 @@ var zhCN = {
4279
4543
  noRecords: "\u65E0\u8BB0\u5F55",
4280
4544
  untracked: "\uFF08\u672A\u8FFD\u8E2A\uFF09",
4281
4545
  churned: "\uFF08\u5DF2\u53D8\u66F4 \xD7{count}\uFF09"
4546
+ },
4547
+ builtinSkills: {
4548
+ explore: "\u5728\u9694\u79BB\u5B50 agent \u4E2D\u63A2\u7D22\u4EE3\u7801\u5E93 \u2014 \u53EA\u8BFB\u5BBD\u7F51\u8C03\u67E5\uFF0C\u8FD4\u56DE\u4E00\u4E2A\u7CBE\u70BC\u7ED3\u8BBA",
4549
+ research: "\u7ED3\u5408\u4EE3\u7801\u9605\u8BFB\u4E0E\u7F51\u7EDC\u641C\u7D22\u8FDB\u884C\u8C03\u7814 \u2014 \u5728\u9694\u79BB\u5B50 agent \u4E2D\u7EFC\u5408\u4FE1\u606F\u5E76\u8FD4\u56DE\u7ED3\u8BBA",
4550
+ review: "\u5BA1\u67E5\u5F53\u524D\u5206\u652F\u53D8\u66F4 \u2014 \u68C0\u67E5\u6B63\u786E\u6027\u3001\u5B89\u5168\u6027\u3001\u7F3A\u5931\u6D4B\u8BD5\u3001\u9690\u85CF\u884C\u4E3A\u53D8\u66F4",
4551
+ securityReview: "\u5B89\u5168\u4E13\u9879\u5BA1\u67E5 \u2014 \u6807\u8BB0\u6CE8\u5165/\u8BA4\u8BC1/\u5BC6\u94A5/\u53CD\u5E8F\u5217\u5316/\u8DEF\u5F84\u7A7F\u8D8A/\u52A0\u5BC6\u95EE\u9898",
4552
+ test: "\u8FD0\u884C\u6D4B\u8BD5\u5957\u4EF6\u5E76\u8BCA\u65AD\u5931\u8D25 \u2014 \u81EA\u52A8\u8BC6\u522B\u6D4B\u8BD5\u6846\u67B6\uFF0C\u4FEE\u590D\u540E\u91CD\u8DD1\u76F4\u81F3\u901A\u8FC7"
4553
+ },
4554
+ shortcutsHelp: {
4555
+ title: "\u5FEB\u6377\u952E",
4556
+ groupInput: "\u8F93\u5165",
4557
+ groupNavigation: "\u5BFC\u822A",
4558
+ groupSession: "\u4F1A\u8BDD",
4559
+ groupSystem: "\u7CFB\u7EDF",
4560
+ descEnter: "\u53D1\u9001\u6D88\u606F",
4561
+ descShiftEnter: "\u6362\u884C",
4562
+ descCtrlEnter: "\u6362\u884C",
4563
+ descCtrlJ: "\u6362\u884C",
4564
+ descCtrlU: "\u6E05\u7A7A\u8F93\u5165",
4565
+ descCtrlW: "\u5220\u9664\u5355\u8BCD",
4566
+ descCtrlP: "\u663E\u793A/\u9690\u85CF\u5FEB\u6377\u952E",
4567
+ descCtrlX: "\u5728\u7F16\u8F91\u5668\u4E2D\u6253\u5F00",
4568
+ descArrows: "\u6D4F\u89C8\u8F93\u5165\u5386\u53F2",
4569
+ descPgUpDown: "\u7FFB\u9875",
4570
+ descCtrlL: "\u6E05\u5C4F",
4571
+ descCtrlB: "\u5207\u6362\u4FA7\u8FB9\u680F",
4572
+ descNewSession: "\u65B0\u5EFA\u4F1A\u8BDD",
4573
+ descListSessions: "\u5217\u51FA\u4F1A\u8BDD",
4574
+ descSwitchModel: "\u5207\u6362\u6A21\u578B",
4575
+ descSwitchPreset: "\u5207\u6362\u9884\u8BBE",
4576
+ descSwitchTheme: "\u5207\u6362\u4E3B\u9898",
4577
+ descCtrlC: "\u9000\u51FA",
4578
+ descEsc: "\u505C\u6B62/\u53D6\u6D88",
4579
+ descCtrlR: "\u5207\u6362\u8BE6\u7EC6\u6A21\u5F0F",
4580
+ descCtrlO: "\u5C55\u5F00\u56DE\u590D\uFF08\u4EC5\u6D41\u5F0F\u8F93\u51FA\u671F\u95F4\uFF09",
4581
+ descHelp: "\u663E\u793A\u6240\u6709\u547D\u4EE4",
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}"
4282
4604
  }
4283
4605
  };
4284
4606
 
@@ -4354,7 +4676,7 @@ function readSettingsFile(path2) {
4354
4676
  }
4355
4677
  function loadHooks(opts = {}) {
4356
4678
  const out = [];
4357
- if (opts.projectRoot) {
4679
+ if (opts.projectRoot && (opts.trustProjectHooks === true || projectHooksTrusted(opts.projectRoot, opts.configPath))) {
4358
4680
  const projPath = projectSettingsPath(opts.projectRoot);
4359
4681
  const settings2 = readSettingsFile(projPath);
4360
4682
  if (settings2) appendResolved(out, settings2, "project", projPath);
@@ -4388,7 +4710,7 @@ function matchesTool(hook, toolName) {
4388
4710
  }
4389
4711
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
4390
4712
  function defaultSpawner(input) {
4391
- return new Promise((resolve13) => {
4713
+ return new Promise((resolve14) => {
4392
4714
  const child = spawn(input.command, {
4393
4715
  cwd: input.cwd,
4394
4716
  shell: true,
@@ -4433,7 +4755,7 @@ function defaultSpawner(input) {
4433
4755
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
4434
4756
  child.once("error", (err) => {
4435
4757
  clearTimeout(timer);
4436
- resolve13({
4758
+ resolve14({
4437
4759
  exitCode: null,
4438
4760
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
4439
4761
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -4444,7 +4766,7 @@ function defaultSpawner(input) {
4444
4766
  });
4445
4767
  child.once("close", (code) => {
4446
4768
  clearTimeout(timer);
4447
- resolve13({
4769
+ resolve14({
4448
4770
  exitCode: code,
4449
4771
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
4450
4772
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -4966,12 +5288,14 @@ var ToolRegistry = class {
4966
5288
  _interceptors = [];
4967
5289
  _auditListener = null;
4968
5290
  _resultAugmenter = null;
5291
+ _rateLimiter;
4969
5292
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
4970
5293
  _lastMalformed = /* @__PURE__ */ new Map();
4971
5294
  /** Per-tool fingerprint of the last host-side gate rejection. */
4972
5295
  _lastGateRejection = /* @__PURE__ */ new Map();
4973
5296
  constructor(opts = {}) {
4974
5297
  this._autoFlatten = opts.autoFlatten !== false;
5298
+ this._rateLimiter = new ToolRateLimiter(opts.rateLimit);
4975
5299
  }
4976
5300
  /** Enable / disable plan-mode enforcement at dispatch. */
4977
5301
  setPlanMode(on) {
@@ -5008,6 +5332,9 @@ var ToolRegistry = class {
5008
5332
  get hasResultAugmenter() {
5009
5333
  return this._resultAugmenter !== null;
5010
5334
  }
5335
+ get rateLimitPolicy() {
5336
+ return this._rateLimiter.policy;
5337
+ }
5011
5338
  register(def) {
5012
5339
  if (!def.name) throw new Error("tool requires a name");
5013
5340
  const internal = { ...def };
@@ -5106,6 +5433,10 @@ var ToolRegistry = class {
5106
5433
  rejectedReason: "aborted"
5107
5434
  });
5108
5435
  }
5436
+ const rateLimit = this._rateLimiter.consume(name);
5437
+ if (!rateLimit.allowed) {
5438
+ return JSON.stringify(rateLimit.result);
5439
+ }
5109
5440
  let finalResult;
5110
5441
  try {
5111
5442
  try {
@@ -5114,7 +5445,8 @@ var ToolRegistry = class {
5114
5445
  }
5115
5446
  const result = await tool.fn(args, {
5116
5447
  signal: opts.signal,
5117
- confirmationGate: opts.confirmationGate
5448
+ confirmationGate: opts.confirmationGate,
5449
+ readTracker: opts.readTracker
5118
5450
  });
5119
5451
  const str = typeof result === "string" ? result : JSON.stringify(result);
5120
5452
  let clipped = str;
@@ -5199,6 +5531,9 @@ function plainTextRejectedReason(name, result) {
5199
5531
  if ((name === "edit_file" || name === "write_file") && /rejected this edit/i.test(result)) {
5200
5532
  return "edit-gate";
5201
5533
  }
5534
+ if ((name === "edit_file" || name === "multi_edit") && /read_file first/i.test(result)) {
5535
+ return "read-before-edit";
5536
+ }
5202
5537
  if ((name === "run_command" || name === "run_background") && /\buser denied:/i.test(result)) {
5203
5538
  return "shell-gate";
5204
5539
  }
@@ -5208,6 +5543,8 @@ function rejectionRecoveryHint(reason) {
5208
5543
  switch (reason) {
5209
5544
  case "edit-gate":
5210
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.";
5211
5548
  case "shell-gate":
5212
5549
  return "Do not retry the same command. Use an allowlisted/read-only command, wait for approval, or ask the user how to proceed.";
5213
5550
  case "engineering-lifecycle":
@@ -5336,12 +5673,12 @@ async function waitForReady(ready, timeoutMs, serverName, signal) {
5336
5673
  let timer;
5337
5674
  let onAbort;
5338
5675
  try {
5339
- await new Promise((resolve13, reject) => {
5676
+ await new Promise((resolve14, reject) => {
5340
5677
  ready.then(
5341
5678
  () => {
5342
5679
  if (settled) return;
5343
5680
  settled = true;
5344
- resolve13();
5681
+ resolve14();
5345
5682
  },
5346
5683
  (err) => {
5347
5684
  if (settled) return;
@@ -5558,14 +5895,14 @@ function buildSyntheticAssistantMessage(content, fallbackModel) {
5558
5895
  // src/memory/session.ts
5559
5896
  import { execFileSync } from "child_process";
5560
5897
  import {
5561
- appendFileSync,
5898
+ appendFileSync as appendFileSync2,
5562
5899
  chmodSync as chmodSync2,
5563
5900
  copyFileSync,
5564
5901
  existsSync as existsSync3,
5565
5902
  mkdirSync as mkdirSync2,
5566
5903
  readFileSync as readFileSync4,
5567
5904
  readdirSync,
5568
- renameSync,
5905
+ renameSync as renameSync2,
5569
5906
  statSync,
5570
5907
  unlinkSync,
5571
5908
  writeFileSync as writeFileSync2
@@ -5622,7 +5959,7 @@ function readSessionMessages(path2) {
5622
5959
  function appendSessionMessage(name, message) {
5623
5960
  const path2 = sessionPath(name);
5624
5961
  mkdirSync2(dirname3(path2), { recursive: true });
5625
- appendFileSync(path2, `${JSON.stringify(message)}
5962
+ appendFileSync2(path2, `${JSON.stringify(message)}
5626
5963
  `, "utf8");
5627
5964
  try {
5628
5965
  chmodSync2(path2, 384);
@@ -5681,13 +6018,13 @@ function renameSession(oldName, newName) {
5681
6018
  const oldJsonl = sessionPath(oldName);
5682
6019
  const newJsonl = sessionPath(newName);
5683
6020
  if (!existsSync3(oldJsonl) || existsSync3(newJsonl)) return false;
5684
- renameSync(oldJsonl, newJsonl);
6021
+ renameSync2(oldJsonl, newJsonl);
5685
6022
  for (const ext of SESSION_SIDECAR_EXTS) {
5686
6023
  const oldP = oldJsonl.replace(/\.jsonl$/, ext);
5687
6024
  const newP = newJsonl.replace(/\.jsonl$/, ext);
5688
6025
  if (existsSync3(oldP)) {
5689
6026
  try {
5690
- renameSync(oldP, newP);
6027
+ renameSync2(oldP, newP);
5691
6028
  } catch {
5692
6029
  }
5693
6030
  }
@@ -5724,7 +6061,7 @@ function rewriteSession(name, messages) {
5724
6061
  copyFileSync(path2, backup);
5725
6062
  chmodPrivate(backup);
5726
6063
  }
5727
- renameSync(tmp, path2);
6064
+ renameSync2(tmp, path2);
5728
6065
  chmodPrivate(path2);
5729
6066
  } catch (err) {
5730
6067
  try {
@@ -5916,32 +6253,43 @@ function round(n, digits) {
5916
6253
  }
5917
6254
 
5918
6255
  // src/context-manager.ts
5919
- var HISTORY_FOLD_THRESHOLD = 0.5;
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
+ }
6260
+ var HISTORY_FOLD_THRESHOLD = 0.75;
5920
6261
  var HISTORY_FOLD_TAIL_FRACTION = 0.2;
5921
- var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.7;
6262
+ var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.78;
5922
6263
  var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
5923
6264
  var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
5924
6265
  var FORCE_SUMMARY_THRESHOLD = 0.8;
5925
6266
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
5926
6267
  var PREFLIGHT_MECHANICAL_TARGET_FRACTION = 0.7;
6268
+ var MAX_BODY_BYTES = 7e5;
6269
+ var MAX_BODY_BYTES_TARGET = 5e5;
5927
6270
  var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
5928
6271
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
5929
6272
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
5930
6273
  var SKILL_PIN_REGEX = /<skill-pin name="([^"]+)">\n[\s\S]*?\n<\/skill-pin>/g;
5931
- 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) {
5932
6281
  const pinned = /* @__PURE__ */ new Map();
5933
- const stubbedHead = head.map((msg) => {
5934
- if (typeof msg.content !== "string") return msg;
5935
- let hit = false;
5936
- 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];
5937
6288
  pinned.delete(name);
5938
6289
  pinned.set(name, full);
5939
- hit = true;
5940
- return `[skill ${JSON.stringify(name)} memo \u2014 preserved separately, do not summarize.]`;
5941
- });
5942
- return hit ? { ...msg, content: next } : msg;
5943
- });
5944
- return { stubbedHead, pinnedBodies: [...pinned.values()] };
6290
+ }
6291
+ }
6292
+ return { names: [...pinned.keys()], bodies: [...pinned.values()] };
5945
6293
  }
5946
6294
  var ContextManager = class {
5947
6295
  constructor(deps) {
@@ -5990,14 +6338,25 @@ var ContextManager = class {
5990
6338
  }
5991
6339
  return { kind: "none", ...base };
5992
6340
  }
5993
- /** Local-side preflight before sending a request — catches oversized payloads early. */
6341
+ /** Local-side preflight before sending a request — catches oversized payloads early.
6342
+ * Two independent signals trip mechanical truncate: token estimate above the context-window
6343
+ * fraction, OR JSON body bytes above the gateway limit (see `MAX_BODY_BYTES`). */
5994
6344
  decidePreflight(messages, toolSpecs, model) {
5995
6345
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
5996
6346
  const estimate = estimateRequestTokens(messages, toolSpecs ?? null, true);
6347
+ const estimateBytes = Buffer.byteLength(JSON.stringify(messages), "utf8");
6348
+ const tokensOver = estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD;
6349
+ const bytesOver = estimateBytes > MAX_BODY_BYTES;
6350
+ let trigger = "none";
6351
+ if (tokensOver && bytesOver) trigger = "both";
6352
+ else if (tokensOver) trigger = "tokens";
6353
+ else if (bytesOver) trigger = "bytes";
5997
6354
  return {
5998
- needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
6355
+ needsAction: tokensOver || bytesOver,
5999
6356
  estimateTokens: estimate,
6000
- ctxMax
6357
+ estimateBytes,
6358
+ ctxMax,
6359
+ trigger
6001
6360
  };
6002
6361
  }
6003
6362
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -6026,16 +6385,22 @@ var ContextManager = class {
6026
6385
  const tail = all.slice(boundary);
6027
6386
  const headTokens = totalTokens - cumTokens;
6028
6387
  if (headTokens < totalTokens * HISTORY_FOLD_MIN_SAVINGS_FRACTION) return noop;
6029
- const { stubbedHead, pinnedBodies } = extractPinnedSkills(head);
6030
- const summary = await this.summarizeForFold(stubbedHead);
6388
+ const { names: pinnedNames, bodies: pinnedBodies } = collectPinnedSkills(head);
6389
+ const summary = await this.summarizeForFold(head, pinnedNames);
6031
6390
  if (!summary.content) return noop;
6032
6391
  const memoTail = pinnedBodies.length > 0 ? `
6033
6392
 
6034
6393
  ${SKILL_PIN_MEMO_HEADER}
6035
6394
 
6036
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}` : "";
6037
6402
  const summaryMsg = buildAssistantMessage(
6038
- HISTORY_FOLD_MARKER + summary.content + memoTail,
6403
+ HISTORY_FOLD_MARKER + summary.content + memoTail + constraintTail,
6039
6404
  [],
6040
6405
  model,
6041
6406
  summary.reasoningContent
@@ -6043,6 +6408,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6043
6408
  const replacement = [summaryMsg, ...tail];
6044
6409
  this.deps.log.compactInPlace(replacement);
6045
6410
  this.persistRewrite(replacement);
6411
+ this.deps.onLogRewrite?.();
6046
6412
  return {
6047
6413
  folded: true,
6048
6414
  beforeMessages: all.length,
@@ -6050,10 +6416,13 @@ ${pinnedBodies.join("\n\n")}` : "";
6050
6416
  summaryChars: summary.content.length
6051
6417
  };
6052
6418
  }
6053
- /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail. */
6419
+ /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail.
6420
+ * Bounded by tokens AND bytes — bytes matter because DeepSeek's gateway 400s on bodies past
6421
+ * `MAX_BODY_BYTES` even when the token budget is far from exhausted. */
6054
6422
  mechanicalTruncate(model, opts) {
6055
6423
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
6056
6424
  const targetTokens = opts?.targetTokens ?? Math.floor(ctxMax * PREFLIGHT_MECHANICAL_TARGET_FRACTION);
6425
+ const targetBytes = opts?.targetBytes ?? MAX_BODY_BYTES_TARGET;
6057
6426
  const all = this.deps.log.toMessages();
6058
6427
  const noop = {
6059
6428
  folded: false,
@@ -6063,6 +6432,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6063
6432
  };
6064
6433
  if (all.length === 0) return noop;
6065
6434
  const tokenCounts = all.map((m) => estimateConversationTokens([m], true));
6435
+ const byteCounts = all.map((m) => Buffer.byteLength(JSON.stringify(m), "utf8"));
6066
6436
  let latestUserBoundary = -1;
6067
6437
  for (let i = all.length - 1; i >= 0; i--) {
6068
6438
  if (all[i].role === "user") {
@@ -6071,12 +6441,15 @@ ${pinnedBodies.join("\n\n")}` : "";
6071
6441
  }
6072
6442
  }
6073
6443
  let cumTokens = 0;
6444
+ let cumBytes = 0;
6074
6445
  let boundary = all.length;
6075
6446
  let foundSafeBoundary = false;
6076
6447
  for (let i = all.length - 1; i >= 0; i--) {
6077
- const next = cumTokens + tokenCounts[i];
6078
- if (next > targetTokens) break;
6079
- cumTokens = next;
6448
+ const nextTokens = cumTokens + tokenCounts[i];
6449
+ const nextBytes = cumBytes + byteCounts[i];
6450
+ if (nextTokens > targetTokens || nextBytes > targetBytes) break;
6451
+ cumTokens = nextTokens;
6452
+ cumBytes = nextBytes;
6080
6453
  if (all[i].role === "user") {
6081
6454
  boundary = i;
6082
6455
  foundSafeBoundary = true;
@@ -6087,6 +6460,7 @@ ${pinnedBodies.join("\n\n")}` : "";
6087
6460
  if (replacement.length === all.length) return noop;
6088
6461
  this.deps.log.compactInPlace(replacement);
6089
6462
  this.persistRewrite(replacement);
6463
+ this.deps.onLogRewrite?.();
6090
6464
  return {
6091
6465
  folded: true,
6092
6466
  beforeMessages: all.length,
@@ -6105,17 +6479,18 @@ ${pinnedBodies.join("\n\n")}` : "";
6105
6479
  this.persistRewrite([...kept]);
6106
6480
  return true;
6107
6481
  }
6108
- async summarizeForFold(messagesToSummarize) {
6482
+ async summarizeForFold(messagesToSummarize, pinnedSkillNames) {
6109
6483
  const summaryModel = "deepseek-v4-flash";
6110
- 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.";
6111
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);
6112
6489
  const messages = [
6113
- { role: "system", content: systemPrompt },
6490
+ { role: "system", content: agentSystem },
6491
+ ...fewShots.map((m) => ({ ...m })),
6114
6492
  ...healed,
6115
- {
6116
- role: "user",
6117
- content: "Summarize the conversation above as plain prose. This summary replaces the original turns to free context \u2014 make it self-contained."
6118
- }
6493
+ { role: "user", content: instruction }
6119
6494
  ];
6120
6495
  const turnSignal = this.deps.getAbortSignal();
6121
6496
  const foldCtrl = new AbortController();
@@ -6145,9 +6520,9 @@ ${pinnedBodies.join("\n\n")}` : "";
6145
6520
  this.deps.client.chat({
6146
6521
  model: summaryModel,
6147
6522
  messages,
6523
+ tools: tools.length ? tools : void 0,
6148
6524
  signal: foldCtrl.signal,
6149
- thinking: thinkingModeForModel(summaryModel),
6150
- reasoningEffort: "high"
6525
+ thinking: "disabled"
6151
6526
  }),
6152
6527
  abortPromise,
6153
6528
  timeoutPromise
@@ -6232,6 +6607,7 @@ function formatLoopError(err, probe) {
6232
6607
  if (status === "402") return t("errors.balance402", { inner });
6233
6608
  if (status === "422") return t("errors.badparam422", { inner });
6234
6609
  if (status === "400") return t("errors.badrequest400", { inner });
6610
+ if (status === "429") return t("errors.concurrency429", { inner });
6235
6611
  if (is5xxStatus(status)) return formatDeepSeek5xx(status, probe);
6236
6612
  return msg;
6237
6613
  }
@@ -6922,8 +7298,34 @@ function signature(call) {
6922
7298
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
6923
7299
  }
6924
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
+
6925
7323
  // src/loop.ts
6926
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
+ }
6927
7329
  var CacheFirstLoop = class {
6928
7330
  client;
6929
7331
  prefix;
@@ -6932,6 +7334,8 @@ var CacheFirstLoop = class {
6932
7334
  scratch = new VolatileScratch();
6933
7335
  stats = new SessionStats();
6934
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();
6935
7339
  // Mutable via configure() — slash commands in the TUI / library callers tweak
6936
7340
  // these mid-session so users don't have to restart.
6937
7341
  model;
@@ -6955,15 +7359,19 @@ var CacheFirstLoop = class {
6955
7359
  _turnAbort = new AbortController();
6956
7360
  /** Authoritative running-id set — UI cards consult this instead of trusting end-event delivery. Insert at dispatch entry, delete in finally. */
6957
7361
  _inflight = new InflightSet();
6958
- /** Typeahead steer message set by the UI; step() consumes it at the next iter boundary. */
6959
- _steer = null;
7362
+ /** Typeahead steer messages set by the UI; step() consumes one at each iter boundary. */
7363
+ _steerQueue = [];
6960
7364
  /** Set true when a steer was consumed this turn; cleared on next step() entry. */
6961
7365
  _steerConsumed = false;
6962
7366
  /** UI calls this to inject a mid-turn steer message without aborting the current turn.
6963
- * New text resets steerConsumed a fresh steer hasn't been consumed yet. */
7367
+ * New text resets steerConsumed because a fresh steer is queued. */
6964
7368
  steer(text) {
6965
- this._steer = text;
6966
- 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;
6967
7375
  }
6968
7376
  /** True when a steer was consumed this turn (UI gate to avoid double-submit). */
6969
7377
  get steerConsumed() {
@@ -7049,7 +7457,11 @@ var CacheFirstLoop = class {
7049
7457
  stats: this.stats,
7050
7458
  sessionName: this.sessionName,
7051
7459
  getAbortSignal: () => this._turnAbort.signal,
7052
- 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()
7053
7465
  });
7054
7466
  }
7055
7467
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -7220,7 +7632,8 @@ ${reason}`
7220
7632
  const result = await this.tools.dispatch(name, args, {
7221
7633
  signal,
7222
7634
  maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
7223
- confirmationGate: this.confirmationGate
7635
+ confirmationGate: this.confirmationGate,
7636
+ readTracker: this.readTracker
7224
7637
  });
7225
7638
  const postReport = await runHooks({
7226
7639
  hooks: this.hooks,
@@ -7248,11 +7661,9 @@ ${reason}`
7248
7661
  return generated;
7249
7662
  }
7250
7663
  _inflightCounter = 0;
7251
- buildMessages(pendingUser) {
7664
+ buildMessages() {
7252
7665
  const healedMessages = this.healActiveLogBeforeSend();
7253
- const msgs = [...this.prefix.toMessages(), ...healedMessages];
7254
- if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
7255
- return msgs;
7666
+ return [...this.prefix.toMessages(), ...healedMessages];
7256
7667
  }
7257
7668
  healActiveLogBeforeSend() {
7258
7669
  const current = this.log.toMessages();
@@ -7333,6 +7744,7 @@ ${reason}`
7333
7744
  cap: this.budgetUsd.toFixed(2)
7334
7745
  })
7335
7746
  };
7747
+ this._steerQueue.length = 0;
7336
7748
  return;
7337
7749
  }
7338
7750
  if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
@@ -7371,8 +7783,8 @@ ${reason}`
7371
7783
  };
7372
7784
  }
7373
7785
  this.appendAndPersist({ role: "user", content: userInput });
7374
- let pendingUser = null;
7375
7786
  const toolSpecs = this.prefix.tools();
7787
+ let rateLimitWarningShown = false;
7376
7788
  for (let iter = 0; ; iter++) {
7377
7789
  if (signal.aborted) {
7378
7790
  try {
@@ -7393,6 +7805,7 @@ ${reason}`
7393
7805
  } finally {
7394
7806
  this._turnAbort = new AbortController();
7395
7807
  }
7808
+ this._steerQueue.length = 0;
7396
7809
  return;
7397
7810
  }
7398
7811
  if (iter > 0) {
@@ -7402,14 +7815,15 @@ ${reason}`
7402
7815
  content: t("loop.toolUploadStatus")
7403
7816
  };
7404
7817
  }
7405
- let messages = this.buildMessages(pendingUser);
7406
- if (this._steer !== null) {
7407
- const steer = this._steer;
7408
- this._steer = null;
7409
- this._steerConsumed = true;
7410
- this.appendAndPersist({ role: "user", content: steer });
7411
- messages = this.buildMessages(pendingUser);
7412
- 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();
7413
7827
  yield {
7414
7828
  turn: this._turn,
7415
7829
  role: "steer",
@@ -7419,17 +7833,17 @@ ${reason}`
7419
7833
  {
7420
7834
  const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
7421
7835
  if (decision2.needsAction) {
7422
- const { estimateTokens: estimate, ctxMax } = decision2;
7836
+ const { estimateTokens: estimate, estimateBytes, ctxMax } = decision2;
7423
7837
  yield {
7424
7838
  turn: this._turn,
7425
7839
  role: "status",
7426
7840
  content: t("loop.preflightTruncateStatus")
7427
7841
  };
7428
7842
  const result = this.context.mechanicalTruncate(this.model, {
7429
- allowEmpty: pendingUser !== null
7843
+ allowEmpty: false
7430
7844
  });
7431
7845
  if (result.folded) {
7432
- messages = this.buildMessages(pendingUser);
7846
+ messages = this.buildMessages();
7433
7847
  const after = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
7434
7848
  const stillFull = after.needsAction;
7435
7849
  yield {
@@ -7441,6 +7855,7 @@ ${reason}`
7441
7855
  estimate: after.estimateTokens.toLocaleString(),
7442
7856
  ctxMax: after.ctxMax.toLocaleString(),
7443
7857
  pct: Math.round(after.estimateTokens / after.ctxMax * 100),
7858
+ bodyKB: Math.round(after.estimateBytes / 1024).toLocaleString(),
7444
7859
  beforeMessages: result.beforeMessages,
7445
7860
  afterMessages: result.afterMessages
7446
7861
  }
@@ -7453,7 +7868,8 @@ ${reason}`
7453
7868
  content: t("loop.preflightNoFold", {
7454
7869
  estimate: estimate.toLocaleString(),
7455
7870
  ctxMax: ctxMax.toLocaleString(),
7456
- pct: Math.round(estimate / ctxMax * 100)
7871
+ pct: Math.round(estimate / ctxMax * 100),
7872
+ bodyKB: Math.round(estimateBytes / 1024).toLocaleString()
7457
7873
  })
7458
7874
  };
7459
7875
  }
@@ -7573,6 +7989,7 @@ ${reason}`
7573
7989
  } finally {
7574
7990
  this._turnAbort = new AbortController();
7575
7991
  }
7992
+ this._steerQueue.length = 0;
7576
7993
  return;
7577
7994
  }
7578
7995
  const probe = is5xxError(err) ? await probeDeepSeekReachable(this.client) : void 0;
@@ -7582,6 +7999,7 @@ ${reason}`
7582
7999
  content: "",
7583
8000
  error: formatLoopError(err, probe)
7584
8001
  };
8002
+ this._steerQueue.length = 0;
7585
8003
  return;
7586
8004
  }
7587
8005
  if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && isEscalationRequest(assistantContent)) {
@@ -7662,11 +8080,16 @@ ${reason}`
7662
8080
  };
7663
8081
  }
7664
8082
  if (repairedCalls.length === 0) {
8083
+ if (this._steerQueue.length > 0) {
8084
+ continue;
8085
+ }
7665
8086
  if (allSuppressed) {
7666
8087
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "stuck" });
8088
+ this._steerQueue.length = 0;
7667
8089
  return;
7668
8090
  }
7669
8091
  yield { turn: this._turn, role: "done", content: assistantContent };
8092
+ this._steerQueue.length = 0;
7670
8093
  return;
7671
8094
  }
7672
8095
  const decision = this.context.decideAfterUsage(usage, this.model, this._foldedThisTurn);
@@ -7712,6 +8135,7 @@ ${reason}`
7712
8135
  };
7713
8136
  this.context.trimTrailingToolCalls();
7714
8137
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
8138
+ this._steerQueue.length = 0;
7715
8139
  return;
7716
8140
  }
7717
8141
  const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
@@ -7759,6 +8183,15 @@ ${reason}`
7759
8183
  }
7760
8184
  for (const w of preWarnings) yield w;
7761
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
+ }
7762
8195
  this.appendAndPersist({
7763
8196
  role: "tool",
7764
8197
  tool_call_id: call.id ?? "",
@@ -7781,7 +8214,7 @@ ${reason}`
7781
8214
  return {
7782
8215
  client: this.client,
7783
8216
  signal: this._turnAbort.signal,
7784
- buildMessages: () => this.buildMessages(null),
8217
+ buildMessages: () => this.buildMessages(),
7785
8218
  appendAndPersist: (m) => this.appendAndPersist(m),
7786
8219
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
7787
8220
  turn: this._turn
@@ -7806,7 +8239,7 @@ function parsePositiveIntEnv(raw) {
7806
8239
  // src/at-mentions.ts
7807
8240
  import { existsSync as existsSync4, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
7808
8241
  import { readdir, stat } from "fs/promises";
7809
- 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";
7810
8243
 
7811
8244
  // src/gitignore.ts
7812
8245
  import { readFileSync as readFileSync5 } from "fs";
@@ -7861,7 +8294,7 @@ function listFilesSync(root, opts = {}) {
7861
8294
  function listFilesWithStatsSync(root, opts = {}) {
7862
8295
  const maxResults = Math.max(1, opts.maxResults ?? 2e3);
7863
8296
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
7864
- const rootAbs = resolve2(root);
8297
+ const rootAbs = resolve3(root);
7865
8298
  const respectGi = opts.respectGitignore !== false;
7866
8299
  const out = [];
7867
8300
  const walk2 = (dirAbs, dirRel, layers) => {
@@ -7925,7 +8358,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
7925
8358
  async function walkFilesStream(root, opts) {
7926
8359
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
7927
8360
  const respectGi = opts.respectGitignore !== false;
7928
- const rootAbs = resolve2(root);
8361
+ const rootAbs = resolve3(root);
7929
8362
  const progressGap = Math.max(0, opts.progressIntervalMs ?? 100);
7930
8363
  let scanned = 0;
7931
8364
  let halted = false;
@@ -8003,8 +8436,8 @@ async function flushFiles(ents, dirAbs, dirRel, layers, emit) {
8003
8436
  async function listDirectory(root, relDir, opts = {}) {
8004
8437
  const ignoreDirs = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
8005
8438
  const respectGi = opts.respectGitignore !== false;
8006
- const rootAbs = resolve2(root);
8007
- const dirAbs = resolve2(rootAbs, relDir);
8439
+ const rootAbs = resolve3(root);
8440
+ const dirAbs = resolve3(rootAbs, relDir);
8008
8441
  const rel = relative(rootAbs, dirAbs);
8009
8442
  if (rel.startsWith("..") || isAbsolute2(rel)) return [];
8010
8443
  const layers = [];
@@ -8169,7 +8602,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
8169
8602
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
8170
8603
  const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
8171
8604
  const fs5 = opts.fs ?? defaultFs;
8172
- const root = resolve2(rootDir);
8605
+ const root = resolve3(rootDir);
8173
8606
  const seen = /* @__PURE__ */ new Map();
8174
8607
  const expansions = [];
8175
8608
  const dirListings = /* @__PURE__ */ new Map();
@@ -8216,7 +8649,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings
8216
8649
  if (isAbsolute2(rawPath)) {
8217
8650
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
8218
8651
  }
8219
- const resolved = resolve2(root, rawPath);
8652
+ const resolved = resolve3(root, rawPath);
8220
8653
  const rel = relative(root, resolved);
8221
8654
  if (rel.startsWith("..") || isAbsolute2(rel)) {
8222
8655
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
@@ -8246,7 +8679,7 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings
8246
8679
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
8247
8680
  }
8248
8681
  function readSafe(root, rawPath, fs5) {
8249
- const resolved = resolve2(root, rawPath);
8682
+ const resolved = resolve3(root, rawPath);
8250
8683
  try {
8251
8684
  return fs5.read(resolved);
8252
8685
  } catch {
@@ -8356,7 +8789,7 @@ import {
8356
8789
  writeFileSync as writeFileSync4
8357
8790
  } from "fs";
8358
8791
  import { homedir as homedir5 } from "os";
8359
- import { join as join8, resolve as resolve4 } from "path";
8792
+ import { join as join8, resolve as resolve5 } from "path";
8360
8793
 
8361
8794
  // src/frontmatter.ts
8362
8795
  var KEY_RE = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/;
@@ -8417,7 +8850,7 @@ import {
8417
8850
  } from "fs";
8418
8851
  import { accessSync } from "fs";
8419
8852
  import { homedir as homedir4 } from "os";
8420
- 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";
8421
8854
 
8422
8855
  // src/prompt-fragments.ts
8423
8856
  var TUI_FORMATTING_RULES = `Formatting (rendered in a TUI with a real markdown renderer):
@@ -8469,7 +8902,7 @@ var SkillStore = class {
8469
8902
  disableBuiltins;
8470
8903
  constructor(opts = {}) {
8471
8904
  this.homeDir = opts.homeDir ?? homedir4();
8472
- this.projectRoot = opts.projectRoot ? resolve3(opts.projectRoot) : void 0;
8905
+ this.projectRoot = opts.projectRoot ? resolve4(opts.projectRoot) : void 0;
8473
8906
  const baseDir = this.projectRoot ?? process.cwd();
8474
8907
  this.customSkillPaths = dedupePaths(
8475
8908
  opts.customSkillPaths?.map((p) => resolveCustomSkillPath(p, baseDir, this.homeDir)) ?? []
@@ -8635,7 +9068,7 @@ function dedupePaths(paths) {
8635
9068
  function resolveCustomSkillPath(path2, baseDir, homeDir) {
8636
9069
  const trimmed = path2.trim();
8637
9070
  const expanded = trimmed === "~" ? homeDir : trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? join7(homeDir, trimmed.slice(2)) : trimmed;
8638
- return resolve3(isAbsolute3(expanded) ? expanded : join7(baseDir, expanded));
9071
+ return resolve4(isAbsolute3(expanded) ? expanded : join7(baseDir, expanded));
8639
9072
  }
8640
9073
  function skillPathStatus(dir) {
8641
9074
  try {
@@ -8671,8 +9104,13 @@ Tips:
8671
9104
  - Add \`allowed-tools: read_file, search_content\` to scope a subagent's tools
8672
9105
  `;
8673
9106
  }
9107
+ function skillDescription(s) {
9108
+ if (s.scope !== "builtin") return s.description;
9109
+ const key = s.name === "security-review" ? "securityReview" : s.name;
9110
+ return t(`builtinSkills.${key}`);
9111
+ }
8674
9112
  function skillIndexLine(s) {
8675
- const safeDesc = s.description.replace(/\n/g, " ").trim();
9113
+ const safeDesc = skillDescription(s).replace(/\n/g, " ").trim();
8676
9114
  const tag = s.runAs === "subagent" ? " [\u{1F9EC} subagent]" : "";
8677
9115
  const max = 130 - s.name.length - tag.length;
8678
9116
  const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
@@ -8903,7 +9341,7 @@ function sanitizeMemoryName(raw) {
8903
9341
  return trimmed;
8904
9342
  }
8905
9343
  function projectHash(rootDir) {
8906
- const abs = resolve4(rootDir);
9344
+ const abs = resolve5(rootDir);
8907
9345
  return createHash2("sha1").update(abs).digest("hex").slice(0, 16);
8908
9346
  }
8909
9347
  function scopeDir(opts) {
@@ -8953,7 +9391,7 @@ var MemoryStore = class {
8953
9391
  projectRoot;
8954
9392
  constructor(opts = {}) {
8955
9393
  this.homeDir = opts.homeDir ?? join8(homedir5(), ".reasonix");
8956
- this.projectRoot = opts.projectRoot ? resolve4(opts.projectRoot) : void 0;
9394
+ this.projectRoot = opts.projectRoot ? resolve5(opts.projectRoot) : void 0;
8957
9395
  }
8958
9396
  /** Directory this store writes `scope` files into, creating it if needed. */
8959
9397
  dir(scope) {
@@ -9209,15 +9647,44 @@ function applyMemoryStack(basePrompt, rootDir, opts = {}) {
9209
9647
 
9210
9648
  // src/tools/filesystem.ts
9211
9649
  import { promises as fs4 } from "fs";
9212
- import * as pathMod5 from "path";
9650
+ import * as pathMod6 from "path";
9213
9651
  import picomatch3 from "picomatch";
9214
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
+
9215
9682
  // src/memory/subdir.ts
9216
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
9217
- 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";
9218
9685
  function findDirMemory(absDir, rootDir) {
9219
- const root = resolve5(rootDir);
9220
- const target = resolve5(absDir);
9686
+ const root = resolve6(rootDir);
9687
+ const target = resolve6(absDir);
9221
9688
  const rel = relative2(root, target);
9222
9689
  if (rel.startsWith("..")) return [];
9223
9690
  const found = [];
@@ -9239,12 +9706,12 @@ function findDirMemory(absDir, rootDir) {
9239
9706
  return found;
9240
9707
  }
9241
9708
  function findSubdirMemoryAncestors(absPath, rootDir) {
9242
- return findDirMemory(dirname5(resolve5(absPath)), rootDir);
9709
+ return findDirMemory(dirname5(resolve6(absPath)), rootDir);
9243
9710
  }
9244
9711
  function readSubdirMemoryContent(path2) {
9245
9712
  let raw;
9246
9713
  try {
9247
- raw = readFileSync10(path2, "utf8");
9714
+ raw = readFileSync11(path2, "utf8");
9248
9715
  } catch {
9249
9716
  return null;
9250
9717
  }
@@ -9262,15 +9729,22 @@ ${content}`;
9262
9729
 
9263
9730
  // src/tools/fs/edit.ts
9264
9731
  import { promises as fs } from "fs";
9265
- import * as pathMod from "path";
9732
+ import * as pathMod2 from "path";
9266
9733
  function displayRel(rootDir, full) {
9267
- return pathMod.relative(rootDir, full).replaceAll("\\", "/");
9734
+ return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
9268
9735
  }
9269
- async function applyEdit(rootDir, abs, args) {
9736
+ var READ_BEFORE_EDIT_MARKER = "read_file first";
9737
+ async function applyEdit(rootDir, abs, args, hasRead) {
9270
9738
  if (args.search.length === 0) {
9271
9739
  throw new Error("edit_file: search cannot be empty");
9272
9740
  }
9273
- 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);
9274
9748
  const le = before.includes("\r\n") ? "\r\n" : "\n";
9275
9749
  const adaptedSearch = args.search.replace(/\r?\n/g, le);
9276
9750
  const adaptedReplace = args.replace.replace(/\r?\n/g, le);
@@ -9285,7 +9759,7 @@ async function applyEdit(rootDir, abs, args) {
9285
9759
  );
9286
9760
  }
9287
9761
  const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
9288
- await fs.writeFile(abs, after, "utf8");
9762
+ await fs.writeFile(abs, encodeFile(after, encoding));
9289
9763
  const rel = displayRel(rootDir, abs);
9290
9764
  const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
9291
9765
  const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
@@ -9293,7 +9767,7 @@ async function applyEdit(rootDir, abs, args) {
9293
9767
  return `${header}
9294
9768
  ${diff}`;
9295
9769
  }
9296
- async function applyMultiEdit(rootDir, edits) {
9770
+ async function applyMultiEdit(rootDir, edits, hasRead) {
9297
9771
  if (edits.length === 0) {
9298
9772
  throw new Error("multi_edit: edits must contain at least one entry");
9299
9773
  }
@@ -9319,16 +9793,23 @@ async function applyMultiEdit(rootDir, edits) {
9319
9793
  }
9320
9794
  let state = filesByPath.get(e.abs);
9321
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
+ }
9322
9801
  let before;
9802
+ let encoding;
9323
9803
  try {
9324
- before = await fs.readFile(e.abs, "utf8");
9804
+ const buf = await fs.readFile(e.abs);
9805
+ ({ text: before, encoding } = decodeFileBuffer(buf));
9325
9806
  } catch (err) {
9326
9807
  throw new Error(
9327
9808
  `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
9328
9809
  );
9329
9810
  }
9330
9811
  const le = before.includes("\r\n") ? "\r\n" : "\n";
9331
- state = { before, buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
9812
+ state = { before, buf: before, le, hunks: [], deltaChars: 0, touched: 0, encoding };
9332
9813
  filesByPath.set(e.abs, state);
9333
9814
  }
9334
9815
  const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
@@ -9355,14 +9836,14 @@ ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
9355
9836
  const attempted = [];
9356
9837
  try {
9357
9838
  for (const [abs, state] of filesByPath) {
9358
- attempted.push({ abs, before: state.before });
9359
- 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));
9360
9841
  }
9361
9842
  } catch (writeErr) {
9362
9843
  const rollbackFailures = [];
9363
9844
  for (const item of [...attempted].reverse()) {
9364
9845
  try {
9365
- await fs.writeFile(item.abs, item.before, "utf8");
9846
+ await fs.writeFile(item.abs, encodeFile(item.before, item.encoding));
9366
9847
  } catch (restoreErr) {
9367
9848
  rollbackFailures.push(`${displayRel(rootDir, item.abs)}: ${restoreErr.message}`);
9368
9849
  }
@@ -9439,10 +9920,10 @@ function lineDiff(a, b) {
9439
9920
 
9440
9921
  // src/tools/fs/glob.ts
9441
9922
  import { promises as fs2 } from "fs";
9442
- import * as pathMod2 from "path";
9923
+ import * as pathMod3 from "path";
9443
9924
  import picomatch2 from "picomatch";
9444
9925
  function displayRel2(rootDir, full) {
9445
- return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
9926
+ return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
9446
9927
  }
9447
9928
  async function globFiles(ctx, startAbs, args) {
9448
9929
  if (args.signal?.aborted) {
@@ -9464,7 +9945,7 @@ async function globFiles(ctx, startAbs, args) {
9464
9945
  return;
9465
9946
  }
9466
9947
  for (const e of entries) {
9467
- const full = pathMod2.join(dir, e.name);
9948
+ const full = pathMod3.join(dir, e.name);
9468
9949
  if (e.isDirectory()) {
9469
9950
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
9470
9951
  await walk2(full);
@@ -9501,7 +9982,7 @@ async function globFiles(ctx, startAbs, args) {
9501
9982
  }
9502
9983
 
9503
9984
  // src/tools/fs/outline.ts
9504
- import * as pathMod3 from "path";
9985
+ import * as pathMod4 from "path";
9505
9986
  var OUTLINE_MAX_ENTRIES = 30;
9506
9987
  var OUTLINE_TAIL_KEEP = 5;
9507
9988
  var TS_EXPORT_RE = /^export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type|enum)\s+\*?\s*(\w+)/;
@@ -9544,7 +10025,7 @@ var EXT_TO_LANG = {
9544
10025
  ".text": "txt"
9545
10026
  };
9546
10027
  function extractOutline(filename, lines) {
9547
- const ext = pathMod3.extname(filename).toLowerCase();
10028
+ const ext = pathMod4.extname(filename).toLowerCase();
9548
10029
  const lang = EXT_TO_LANG[ext];
9549
10030
  if (!lang) return [];
9550
10031
  switch (lang) {
@@ -9685,7 +10166,7 @@ function formatOutline(entries) {
9685
10166
 
9686
10167
  // src/tools/fs/search.ts
9687
10168
  import { promises as fs3 } from "fs";
9688
- import * as pathMod4 from "path";
10169
+ import * as pathMod5 from "path";
9689
10170
 
9690
10171
  // src/tools/fs/regex-runner.ts
9691
10172
  import { Worker } from "worker_threads";
@@ -9718,7 +10199,7 @@ var RegexRunner = class {
9718
10199
  this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
9719
10200
  }
9720
10201
  testLines(text, source, flags, opts = {}) {
9721
- return new Promise((resolve13, reject) => {
10202
+ return new Promise((resolve14, reject) => {
9722
10203
  if (opts.signal?.aborted) {
9723
10204
  reject(new Error("regex evaluation aborted"));
9724
10205
  return;
@@ -9731,7 +10212,7 @@ var RegexRunner = class {
9731
10212
  this.killWorker();
9732
10213
  reject(new Error(`regex evaluation exceeded ${timeoutMs}ms`));
9733
10214
  }, timeoutMs);
9734
- const entry = { resolve: resolve13, reject, timer };
10215
+ const entry = { resolve: resolve14, reject, timer };
9735
10216
  if (opts.signal) {
9736
10217
  entry.signal = opts.signal;
9737
10218
  entry.onAbort = () => {
@@ -9814,7 +10295,7 @@ function throwIfAborted(signal) {
9814
10295
  throw new DOMException("search aborted by user", "AbortError");
9815
10296
  }
9816
10297
  function displayRel3(rootDir, full) {
9817
- return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
10298
+ return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
9818
10299
  }
9819
10300
  async function searchFiles(ctx, startAbs, args) {
9820
10301
  throwIfAborted(args.signal);
@@ -9838,7 +10319,7 @@ async function searchFiles(ctx, startAbs, args) {
9838
10319
  }
9839
10320
  for (const e of entries) {
9840
10321
  throwIfAborted(args.signal);
9841
- const full = pathMod4.join(dir, e.name);
10322
+ const full = pathMod5.join(dir, e.name);
9842
10323
  const lower = e.name.toLowerCase();
9843
10324
  const hit = re ? re.test(e.name) : lower.includes(needle);
9844
10325
  if (hit) {
@@ -9930,11 +10411,11 @@ async function searchContent(ctx, startAbs, args) {
9930
10411
  throwIfTimedOut();
9931
10412
  if (e.isDirectory()) {
9932
10413
  if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
9933
- await walk2(pathMod4.join(dir, e.name));
10414
+ await walk2(pathMod5.join(dir, e.name));
9934
10415
  continue;
9935
10416
  }
9936
10417
  if (!e.isFile()) continue;
9937
- const full = pathMod4.join(dir, e.name);
10418
+ const full = pathMod5.join(dir, e.name);
9938
10419
  if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel3(ctx.rootDir, full))) continue;
9939
10420
  if (ctx.isBinaryByName(e.name)) continue;
9940
10421
  let fh;
@@ -10015,8 +10496,8 @@ async function searchContent(ctx, startAbs, args) {
10015
10496
  for (let i = realStart; i <= winEnd; i++) {
10016
10497
  const line = lines[i];
10017
10498
  const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
10018
- const sep3 = hitSet.has(i) ? ":" : "-";
10019
- if (!pushLine(`${rel}:${i + 1}${sep3} ${display}`)) return;
10499
+ const sep2 = hitSet.has(i) ? ":" : "-";
10500
+ if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
10020
10501
  }
10021
10502
  prevWindowEnd = winEnd;
10022
10503
  }
@@ -10052,7 +10533,7 @@ var SKIP_DIR_NAMES = new Set(
10052
10533
  );
10053
10534
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
10054
10535
  function displayRel4(rootDir, full) {
10055
- return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
10536
+ return pathMod6.relative(rootDir, full).replaceAll("\\", "/");
10056
10537
  }
10057
10538
  function looksLikeAbsoluteSystemPath(raw) {
10058
10539
  if (/^[A-Za-z]:[\\/]/.test(raw)) return true;
@@ -10061,8 +10542,8 @@ function looksLikeAbsoluteSystemPath(raw) {
10061
10542
  );
10062
10543
  }
10063
10544
  function pathIsUnder(child, parent) {
10064
- const rel = pathMod5.relative(parent, child);
10065
- return rel === "" || !rel.startsWith("..") && !pathMod5.isAbsolute(rel);
10545
+ const rel = pathMod6.relative(parent, child);
10546
+ return rel === "" || !rel.startsWith("..") && !pathMod6.isAbsolute(rel);
10066
10547
  }
10067
10548
  var GLOB_METACHARS = /[*?{[]/;
10068
10549
  function compileNameFilter(filter) {
@@ -10094,11 +10575,11 @@ function formatBytes(n) {
10094
10575
  return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GiB`;
10095
10576
  }
10096
10577
  function registerFilesystemTools(registry, opts) {
10097
- const rootDir = pathMod5.resolve(opts.rootDir);
10578
+ const rootDir = pathMod6.resolve(opts.rootDir);
10098
10579
  const allowWriting = opts.allowWriting !== false;
10099
10580
  const outlineThresholdBytes = opts.outlineThresholdBytes ?? DEFAULT_OUTLINE_THRESHOLD_BYTES;
10100
10581
  const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
10101
- const normRoot = pathMod5.resolve(rootDir);
10582
+ const normRoot = pathMod6.resolve(rootDir);
10102
10583
  const sessionApproved = /* @__PURE__ */ new Set();
10103
10584
  const shownSubdirMemory = /* @__PURE__ */ new Set();
10104
10585
  function withSubdirMemory(absPath, body) {
@@ -10131,7 +10612,7 @@ ${body}`;
10131
10612
  if (pathIsUnder(abs, dir)) return;
10132
10613
  }
10133
10614
  const stat2 = await safeLstat(abs);
10134
- const allowPrefix = stat2?.isDirectory() ? abs : pathMod5.dirname(abs);
10615
+ const allowPrefix = stat2?.isDirectory() ? abs : pathMod6.dirname(abs);
10135
10616
  let pending = inflightGate.get(allowPrefix);
10136
10617
  if (!pending) {
10137
10618
  const gate = ctx?.confirmationGate ?? pauseGate;
@@ -10159,7 +10640,7 @@ ${body}`;
10159
10640
  throw new Error("path must be a non-empty string");
10160
10641
  }
10161
10642
  if (looksLikeAbsoluteSystemPath(raw)) {
10162
- const abs = pathMod5.resolve(raw);
10643
+ const abs = pathMod6.resolve(raw);
10163
10644
  if (pathIsUnder(abs, normRoot)) return abs;
10164
10645
  await ensureOutsideSandboxAllowed(abs, intent, toolName, ctx);
10165
10646
  return abs;
@@ -10169,7 +10650,7 @@ ${body}`;
10169
10650
  normalized = normalized.slice(1);
10170
10651
  }
10171
10652
  if (normalized.length === 0) normalized = ".";
10172
- const resolved = pathMod5.resolve(rootDir, normalized);
10653
+ const resolved = pathMod6.resolve(rootDir, normalized);
10173
10654
  if (!pathIsUnder(resolved, normRoot)) {
10174
10655
  throw new Error(
10175
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`
@@ -10231,7 +10712,8 @@ ${body}`;
10231
10712
  if (looksBinary(raw)) {
10232
10713
  return `[refused: ${rel} appears to be binary (${formatBytes(sizeBytes)}) \u2014 read_file returns text only. Use get_file_info for stat.]`;
10233
10714
  }
10234
- const text = raw.toString("utf8");
10715
+ const { text } = decodeFileBuffer(raw);
10716
+ ctx?.readTracker?.markRead(abs);
10235
10717
  let lines = text.split(/\r?\n/);
10236
10718
  if (lines.length > 0 && lines[lines.length - 1] === "") lines = lines.slice(0, -1);
10237
10719
  const totalLines = lines.length;
@@ -10369,7 +10851,7 @@ ${slice.join("\n")}`);
10369
10851
  lines.push(line);
10370
10852
  emitted++;
10371
10853
  if (e.isDirectory() && !skip) {
10372
- await walk2(pathMod5.join(dir, e.name), depth + 1);
10854
+ await walk2(pathMod6.join(dir, e.name), depth + 1);
10373
10855
  }
10374
10856
  }
10375
10857
  };
@@ -10529,14 +11011,20 @@ ${slice.join("\n")}`);
10529
11011
  },
10530
11012
  fn: async (args, ctx) => {
10531
11013
  const abs = await safePath(args.path, "write_file", ctx, "write");
10532
- await fs4.mkdir(pathMod5.dirname(abs), { recursive: true });
10533
- 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);
10534
11022
  return `wrote ${args.content.length} chars to ${displayRel4(rootDir, abs)}`;
10535
11023
  }
10536
11024
  });
10537
11025
  registry.register({
10538
11026
  name: "edit_file",
10539
- 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.",
10540
11028
  parameters: {
10541
11029
  type: "object",
10542
11030
  properties: {
@@ -10546,11 +11034,16 @@ ${slice.join("\n")}`);
10546
11034
  },
10547
11035
  required: ["path", "search", "replace"]
10548
11036
  },
10549
- 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
+ )
10550
11043
  });
10551
11044
  registry.register({
10552
11045
  name: "multi_edit",
10553
- 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.",
10554
11047
  parameters: {
10555
11048
  type: "object",
10556
11049
  properties: {
@@ -10584,7 +11077,11 @@ ${slice.join("\n")}`);
10584
11077
  replace: e?.replace
10585
11078
  }))
10586
11079
  );
10587
- return applyMultiEdit(rootDir, resolved);
11080
+ return applyMultiEdit(
11081
+ rootDir,
11082
+ resolved,
11083
+ ctx?.readTracker ? (abs) => ctx.readTracker.hasRead(abs) : void 0
11084
+ );
10588
11085
  }
10589
11086
  });
10590
11087
  registry.register({
@@ -10615,7 +11112,7 @@ ${slice.join("\n")}`);
10615
11112
  fn: async (args, ctx) => {
10616
11113
  const src = await safePath(args.source, "move_file", ctx, "write");
10617
11114
  const dst = await safePath(args.destination, "move_file", ctx, "write");
10618
- await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
11115
+ await fs4.mkdir(pathMod6.dirname(dst), { recursive: true });
10619
11116
  await fs4.rename(src, dst);
10620
11117
  return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
10621
11118
  }
@@ -10683,7 +11180,7 @@ ${slice.join("\n")}`);
10683
11180
  fn: async (args, ctx) => {
10684
11181
  const src = await safePath(args.source, "copy_file", ctx);
10685
11182
  const dst = await safePath(args.destination, "copy_file", ctx, "write");
10686
- await fs4.mkdir(pathMod5.dirname(dst), { recursive: true });
11183
+ await fs4.mkdir(pathMod6.dirname(dst), { recursive: true });
10687
11184
  await fs4.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
10688
11185
  return `copied ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
10689
11186
  }
@@ -11783,7 +12280,7 @@ ${hint}` : formatted;
11783
12280
  return parentRegistry;
11784
12281
  }
11785
12282
  function forkRegistryExcluding(parent, exclude) {
11786
- const child = new ToolRegistry();
12283
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11787
12284
  for (const spec of parent.specs()) {
11788
12285
  const name = spec.function.name;
11789
12286
  if (exclude.has(name)) continue;
@@ -11795,7 +12292,7 @@ function forkRegistryExcluding(parent, exclude) {
11795
12292
  return child;
11796
12293
  }
11797
12294
  function forkRegistryWithAllowList(parent, allow, alsoExclude) {
11798
- const child = new ToolRegistry();
12295
+ const child = new ToolRegistry({ rateLimit: parent.rateLimitPolicy });
11799
12296
  for (const spec of parent.specs()) {
11800
12297
  const name = spec.function.name;
11801
12298
  if (!allow.has(name)) continue;
@@ -11901,11 +12398,11 @@ var SubagentTelemetry = class {
11901
12398
  };
11902
12399
 
11903
12400
  // src/tools/shell.ts
11904
- import * as pathMod10 from "path";
12401
+ import * as pathMod11 from "path";
11905
12402
 
11906
12403
  // src/tools/jobs.ts
11907
12404
  import { spawn as spawn2 } from "child_process";
11908
- import * as pathMod6 from "path";
12405
+ import * as pathMod7 from "path";
11909
12406
  function killProcessTree(pid, signal) {
11910
12407
  if (process.platform === "win32") {
11911
12408
  const args = ["/pid", String(pid), "/T"];
@@ -11965,7 +12462,7 @@ var JobRegistry = class {
11965
12462
  const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
11966
12463
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
11967
12464
  const spawnOpts = {
11968
- cwd: pathMod6.resolve(opts.cwd),
12465
+ cwd: pathMod7.resolve(opts.cwd),
11969
12466
  shell: false,
11970
12467
  windowsHide: true,
11971
12468
  env: process.env,
@@ -12150,16 +12647,16 @@ ${job.output.slice(start)}`;
12150
12647
  let wakeOutput = null;
12151
12648
  if (waitFor === "output-or-exit") {
12152
12649
  racers.push(
12153
- new Promise((resolve13) => {
12154
- wakeOutput = resolve13;
12155
- job.outputWaiters.add(resolve13);
12650
+ new Promise((resolve14) => {
12651
+ wakeOutput = resolve14;
12652
+ job.outputWaiters.add(resolve14);
12156
12653
  })
12157
12654
  );
12158
12655
  }
12159
12656
  let timer = null;
12160
12657
  racers.push(
12161
- new Promise((resolve13) => {
12162
- timer = setTimeout(resolve13, timeoutMs);
12658
+ new Promise((resolve14) => {
12659
+ timer = setTimeout(resolve14, timeoutMs);
12163
12660
  })
12164
12661
  );
12165
12662
  await Promise.race(racers);
@@ -12269,13 +12766,13 @@ function latestOutputSince(before, after) {
12269
12766
  // src/tools/shell/exec.ts
12270
12767
  import { spawn as spawn4, spawnSync } from "child_process";
12271
12768
  import { existsSync as existsSync9, statSync as statSync5 } from "fs";
12272
- import * as pathMod9 from "path";
12769
+ import * as pathMod10 from "path";
12273
12770
 
12274
12771
  // src/tools/shell-chain.ts
12275
12772
  import { spawn as spawn3 } from "child_process";
12276
- import { closeSync, openSync } from "fs";
12773
+ import { constants as constants2, closeSync, lstatSync, openSync, realpathSync } from "fs";
12277
12774
  import { devNull } from "os";
12278
- import * as pathMod7 from "path";
12775
+ import * as pathMod8 from "path";
12279
12776
  var UnsupportedSyntaxError = class extends Error {
12280
12777
  constructor(detail) {
12281
12778
  super(`run_command: ${detail}`);
@@ -12540,7 +13037,49 @@ function isNullDeviceAlias(target) {
12540
13037
  if (process.platform === "win32" && lower === "nul") return true;
12541
13038
  return false;
12542
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
+ }
12543
13081
  function openRedirects(redirects, cwd) {
13082
+ validateRedirectTargets(redirects, cwd);
12544
13083
  let stdinFd = null;
12545
13084
  let stdoutFd = null;
12546
13085
  let stderrFd = null;
@@ -12548,8 +13087,8 @@ function openRedirects(redirects, cwd) {
12548
13087
  let bothFd = null;
12549
13088
  const toClose = [];
12550
13089
  const open = (target, flags) => {
12551
- const resolved = isNullDeviceAlias(target) ? devNull : pathMod7.resolve(cwd, target);
12552
- const fd = openSync(resolved, flags);
13090
+ const resolved = isNullDeviceAlias(target) ? devNull : resolveRedirectTarget(target, cwd);
13091
+ const fd = openSync(resolved, openFlags(flags), 438);
12553
13092
  toClose.push(fd);
12554
13093
  return fd;
12555
13094
  };
@@ -12656,9 +13195,9 @@ async function runPipeGroup(segments, opts) {
12656
13195
  }
12657
13196
  const exits = await Promise.all(
12658
13197
  children.map(
12659
- (c) => new Promise((resolve13) => {
12660
- c.once("error", () => resolve13(null));
12661
- c.once("close", (code) => resolve13(code));
13198
+ (c) => new Promise((resolve14) => {
13199
+ c.once("error", () => resolve14(null));
13200
+ c.once("close", (code) => resolve14(code));
12662
13201
  })
12663
13202
  )
12664
13203
  );
@@ -12703,7 +13242,7 @@ var OutputBuffer = class {
12703
13242
 
12704
13243
  // src/tools/shell/parse.ts
12705
13244
  import { homedir as homedir7 } from "os";
12706
- import * as pathMod8 from "path";
13245
+ import * as pathMod9 from "path";
12707
13246
 
12708
13247
  // packages/core-utils/src/tildeify.ts
12709
13248
  import { homedir as homedir6 } from "os";
@@ -12908,16 +13447,16 @@ function resolveSensitivePath(token, projectRoot) {
12908
13447
  return null;
12909
13448
  let expanded = token;
12910
13449
  if (expanded.startsWith("~")) {
12911
- expanded = pathMod8.join(homedir7(), expanded.slice(1));
13450
+ expanded = pathMod9.join(homedir7(), expanded.slice(1));
12912
13451
  }
12913
- return pathMod8.resolve(projectRoot, expanded);
13452
+ return pathMod9.resolve(projectRoot, expanded);
12914
13453
  }
12915
13454
  function expandPrefix(prefix) {
12916
- if (prefix.startsWith("~")) return pathMod8.join(homedir7(), prefix.slice(1));
12917
- return pathMod8.resolve(prefix);
13455
+ if (prefix.startsWith("~")) return pathMod9.join(homedir7(), prefix.slice(1));
13456
+ return pathMod9.resolve(prefix);
12918
13457
  }
12919
13458
  function pathStartsWithPrefix(normalized, prefix) {
12920
- return normalized === prefix || normalized.startsWith(`${prefix}${pathMod8.sep}`);
13459
+ return normalized === prefix || normalized.startsWith(`${prefix}${pathMod9.sep}`);
12921
13460
  }
12922
13461
  function matchesGlob(name, pattern) {
12923
13462
  const regex = new RegExp(
@@ -12932,17 +13471,39 @@ function hasSensitivePathArgs(argv, projectRoot, extraPrefixes = [], extraPatter
12932
13471
  for (const token of argv) {
12933
13472
  const resolved = resolveSensitivePath(token, projectRoot);
12934
13473
  if (!resolved) continue;
12935
- const normalized = pathMod8.normalize(resolved);
13474
+ const normalized = pathMod9.normalize(resolved);
12936
13475
  for (const pfx of prefixes) {
12937
13476
  if (pathStartsWithPrefix(normalized, pfx)) return true;
12938
13477
  }
12939
- const base = pathMod8.basename(normalized);
13478
+ const base = pathMod9.basename(normalized);
12940
13479
  for (const pat of patterns) {
12941
13480
  if (matchesGlob(base, pat)) return true;
12942
13481
  }
12943
13482
  }
12944
13483
  return false;
12945
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
+ }
12946
13507
  function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
12947
13508
  let argv;
12948
13509
  try {
@@ -12984,6 +13545,18 @@ function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
12984
13545
  return false;
12985
13546
  }
12986
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
+ }
12987
13560
  return chainAllowed(chain, (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig));
12988
13561
  }
12989
13562
 
@@ -13052,7 +13625,7 @@ async function runCommand(cmd, opts) {
13052
13625
  };
13053
13626
  const { bin, args, spawnOverrides } = prepareSpawn(argv, { env: normalizedEnv });
13054
13627
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
13055
- return await new Promise((resolve13, reject) => {
13628
+ return await new Promise((resolve14, reject) => {
13056
13629
  let child;
13057
13630
  try {
13058
13631
  child = spawn4(bin, args, effectiveSpawnOpts);
@@ -13106,7 +13679,7 @@ async function runCommand(cmd, opts) {
13106
13679
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
13107
13680
 
13108
13681
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
13109
- resolve13({ exitCode: code, output, timedOut });
13682
+ resolve14({ exitCode: code, output, timedOut });
13110
13683
  });
13111
13684
  });
13112
13685
  }
@@ -13128,16 +13701,16 @@ function resolveExecutable(cmd, opts = {}) {
13128
13701
  const platform = opts.platform ?? process.platform;
13129
13702
  if (platform !== "win32") return cmd;
13130
13703
  if (!cmd) return cmd;
13131
- if (cmd.includes("/") || cmd.includes("\\") || pathMod9.isAbsolute(cmd)) return cmd;
13132
- if (pathMod9.extname(cmd)) return cmd;
13704
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod10.isAbsolute(cmd)) return cmd;
13705
+ if (pathMod10.extname(cmd)) return cmd;
13133
13706
  const env = opts.env ?? process.env;
13134
13707
  const pathExt = (getEnvCaseInsensitive(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
13135
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod9.delimiter);
13708
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod10.delimiter);
13136
13709
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
13137
13710
  const isFile = opts.isFile ?? defaultIsFile;
13138
13711
  for (const dir of pathDirs) {
13139
13712
  for (const ext of pathExt) {
13140
- const full = pathMod9.win32.join(dir, cmd + ext);
13713
+ const full = pathMod10.win32.join(dir, cmd + ext);
13141
13714
  if (isFile(full)) return full;
13142
13715
  }
13143
13716
  }
@@ -13253,8 +13826,8 @@ function withUtf8Codepage(cmdline) {
13253
13826
  function isBareWindowsName(s) {
13254
13827
  if (!s) return false;
13255
13828
  if (s.includes("/") || s.includes("\\")) return false;
13256
- if (pathMod9.isAbsolute(s)) return false;
13257
- if (pathMod9.extname(s)) return false;
13829
+ if (pathMod10.isAbsolute(s)) return false;
13830
+ if (pathMod10.extname(s)) return false;
13258
13831
  return true;
13259
13832
  }
13260
13833
  function quoteForCmdExe(arg) {
@@ -13275,7 +13848,7 @@ var NeedsConfirmationError = class extends Error {
13275
13848
  }
13276
13849
  };
13277
13850
  function registerShellTools(registry, opts) {
13278
- const rootDir = pathMod10.resolve(opts.rootDir);
13851
+ const rootDir = pathMod11.resolve(opts.rootDir);
13279
13852
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
13280
13853
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
13281
13854
  const jobs = opts.jobs ?? new JobRegistry();
@@ -13286,7 +13859,7 @@ function registerShellTools(registry, opts) {
13286
13859
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
13287
13860
  registry.register({
13288
13861
  name: "run_command",
13289
- 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.',
13290
13863
  // Plan-mode gate: allow allowlisted commands through (git status,
13291
13864
  // cargo check, ls, grep …) so the model can actually investigate
13292
13865
  // during planning. Anything that would otherwise trigger a
@@ -13488,11 +14061,11 @@ function registerShellTools(registry, opts) {
13488
14061
  return registry;
13489
14062
  }
13490
14063
  function resolveCwdInsideRoot(rootDir, raw) {
13491
- const root = pathMod10.resolve(rootDir);
14064
+ const root = pathMod11.resolve(rootDir);
13492
14065
  if (!raw || !raw.trim()) return root;
13493
- const resolved = pathMod10.resolve(root, raw);
13494
- const rel = pathMod10.relative(root, resolved);
13495
- 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)) {
13496
14069
  throw new Error(
13497
14070
  `run_background: cwd "${raw}" resolves outside the workspace root (${root}). Pass a workspace-relative path.`
13498
14071
  );
@@ -13540,17 +14113,20 @@ ${r.output}` : header;
13540
14113
  }
13541
14114
 
13542
14115
  // src/tools/web.ts
14116
+ import { lookup } from "dns/promises";
14117
+ import { isIP } from "net";
13543
14118
  import { parse as parseHtml } from "node-html-parser";
13544
14119
  var DEFAULT_FETCH_MAX_CHARS = 32e3;
13545
14120
  var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
13546
14121
  var DEFAULT_TOPK = 5;
13547
14122
  var FETCH_MAX_BYTES = 10 * 1024 * 1024;
13548
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";
13549
- var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
14124
+ var BING_ENDPOINT = "https://cn.bing.com/search";
13550
14125
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
13551
14126
  var TAVILY_ENDPOINT = "https://api.tavily.com/search";
13552
14127
  var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai/chat/completions";
13553
14128
  var EXA_ENDPOINT = "https://api.exa.ai/answer";
14129
+ var FETCH_MAX_REDIRECTS = 5;
13554
14130
  function searchStatusError(status) {
13555
14131
  if (status === 429) return t("webErrors.rateLimit429");
13556
14132
  if (status === 403) return t("webErrors.forbidden403");
@@ -13563,6 +14139,63 @@ function fetchStatusError(status, url) {
13563
14139
  if (status >= 500 && status <= 599) return t("webErrors.fetchServerError5xx", { status, url });
13564
14140
  return t("webErrors.fetchStatus", { status, url });
13565
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
+ }
13566
14199
  async function webSearch(query, opts = {}) {
13567
14200
  if (opts.engine === "metaso") {
13568
14201
  return searchMetaso(query, opts);
@@ -13579,29 +14212,29 @@ async function webSearch(query, opts = {}) {
13579
14212
  if (opts.engine === "exa") {
13580
14213
  return searchExa(query, opts);
13581
14214
  }
13582
- return searchMojeek(query, opts);
14215
+ return searchBing(query, opts);
13583
14216
  }
13584
- async function searchMojeek(query, opts = {}) {
14217
+ async function searchBing(query, opts = {}) {
13585
14218
  const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
13586
- const resp = await fetch(`${MOJEEK_ENDPOINT}?q=${encodeURIComponent(query)}`, {
14219
+ const resp = await fetch(`${BING_ENDPOINT}?q=${encodeURIComponent(query)}`, {
13587
14220
  headers: {
13588
14221
  "User-Agent": USER_AGENT,
13589
14222
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9",
13590
- "Accept-Language": "en-US,en;q=0.9"
14223
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
13591
14224
  },
13592
14225
  signal: opts.signal,
13593
14226
  redirect: "follow"
13594
14227
  });
13595
14228
  if (!resp.ok) throw new Error(searchStatusError(resp.status));
13596
14229
  const html = await resp.text();
13597
- const results = parseMojeekResults(html).slice(0, topK);
14230
+ const results = parseBingResults(html).slice(0, topK);
13598
14231
  if (results.length === 0) {
13599
14232
  if (/no results found|did not match any documents/i.test(html)) return [];
13600
14233
  if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
13601
- throw new Error(t("webErrors.mojeekBlocked"));
14234
+ throw new Error(t("webErrors.bingBlocked"));
13602
14235
  }
13603
14236
  throw new Error(
13604
- t("webErrors.mojeekNoResults", {
14237
+ t("webErrors.bingNoResults", {
13605
14238
  chars: html.length,
13606
14239
  preview: html.slice(0, 120).replace(/\s+/g, " ")
13607
14240
  })
@@ -13654,6 +14287,7 @@ async function searchSearxng(query, opts = {}) {
13654
14287
  async function searchMetaso(query, opts = {}) {
13655
14288
  const topK = Math.max(1, Math.min(100, opts.topK ?? DEFAULT_TOPK));
13656
14289
  const apiKey = loadMetasoApiKey();
14290
+ if (!apiKey) throw new Error(t("webErrors.metasoMissingKey"));
13657
14291
  let resp;
13658
14292
  try {
13659
14293
  resp = await fetch(`${METASO_ENDPOINT}/search`, {
@@ -13917,35 +14551,19 @@ function parseSearxngHtmlResults(html) {
13917
14551
  }
13918
14552
  return results;
13919
14553
  }
13920
- function parseMojeekResults(html) {
13921
- const titles = [];
13922
- const titleAnchorRe = /<a\b[^>]*\bclass="title"[^>]*>[\s\S]*?<\/a>/g;
13923
- let m;
13924
- while (true) {
13925
- m = titleAnchorRe.exec(html);
13926
- if (m === null) break;
13927
- titles.push(m[0]);
13928
- }
13929
- const snippets = [];
13930
- const snippetRe = /<p\b[^>]*\bclass="s"[^>]*>([\s\S]*?)<\/p>/g;
13931
- while (true) {
13932
- m = snippetRe.exec(html);
13933
- if (m === null) break;
13934
- snippets.push(m[1] ?? "");
13935
- }
13936
- const hrefRe = /href="([^"]+)"/;
13937
- const innerRe = /<a\b[^>]*>([\s\S]*?)<\/a>/;
14554
+ function parseBingResults(html) {
14555
+ const root = parseHtml(html);
13938
14556
  const results = [];
13939
- for (let i = 0; i < titles.length; i++) {
13940
- const anchor = titles[i];
13941
- const hrefMatch = anchor.match(hrefRe);
13942
- const innerMatch = anchor.match(innerRe);
13943
- if (!hrefMatch?.[1]) continue;
13944
- results.push({
13945
- title: decodeHtmlEntities(stripHtml(innerMatch?.[1] ?? "")).trim(),
13946
- url: hrefMatch[1],
13947
- snippet: decodeHtmlEntities(stripHtml(snippets[i] ?? "")).replace(/\s+/g, " ").trim()
13948
- });
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 });
13949
14567
  }
13950
14568
  return results;
13951
14569
  }
@@ -13961,12 +14579,23 @@ async function webFetch(url, opts = {}) {
13961
14579
  const cancel = () => ctl.abort();
13962
14580
  opts.signal?.addEventListener("abort", cancel, { once: true });
13963
14581
  let resp;
14582
+ let currentUrl = url;
13964
14583
  try {
13965
- resp = await fetch(url, {
13966
- headers: { "User-Agent": USER_AGENT, Accept: "text/html,text/plain,*/*" },
13967
- signal: ctl.signal,
13968
- redirect: "follow"
13969
- });
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
+ }
13970
14599
  } catch (err) {
13971
14600
  if (timedOut) {
13972
14601
  throw new Error(t("webErrors.fetchTimeout", { ms: timeoutMs, url }));
@@ -13989,7 +14618,7 @@ async function webFetch(url, opts = {}) {
13989
14618
  const finalText = truncated ? `${text.slice(0, maxChars)}
13990
14619
 
13991
14620
  [\u2026 truncated ${text.length - maxChars} chars \u2026]` : text;
13992
- return { url, title, text: finalText, truncated };
14621
+ return { url: currentUrl, title, text: finalText, truncated };
13993
14622
  }
13994
14623
  async function readBodyCapped(resp, maxBytes) {
13995
14624
  if (!resp.body) return await resp.text();
@@ -14061,9 +14690,6 @@ function walkExtract(node, out) {
14061
14690
  for (const child of node.childNodes) walkExtract(child, out);
14062
14691
  if (isBreak) out.push("\n");
14063
14692
  }
14064
- function stripHtml(s) {
14065
- return parseHtml(s).text;
14066
- }
14067
14693
  var HTML_ENTITIES = {
14068
14694
  amp: "&",
14069
14695
  lt: "<",
@@ -14176,12 +14802,12 @@ ${i + 1}. ${r.title}`);
14176
14802
  }
14177
14803
 
14178
14804
  // src/env.ts
14179
- import { readFileSync as readFileSync11 } from "fs";
14180
- import { resolve as resolve11 } from "path";
14805
+ import { readFileSync as readFileSync12 } from "fs";
14806
+ import { resolve as resolve12 } from "path";
14181
14807
  function loadDotenv(path2 = ".env") {
14182
14808
  let raw;
14183
14809
  try {
14184
- raw = readFileSync11(resolve11(process.cwd(), path2), "utf8");
14810
+ raw = readFileSync12(resolve12(process.cwd(), path2), "utf8");
14185
14811
  } catch {
14186
14812
  return;
14187
14813
  }
@@ -14200,7 +14826,7 @@ function loadDotenv(path2 = ".env") {
14200
14826
  }
14201
14827
 
14202
14828
  // src/transcript/log.ts
14203
- import { createWriteStream, readFileSync as readFileSync12 } from "fs";
14829
+ import { createWriteStream, readFileSync as readFileSync13 } from "fs";
14204
14830
  function recordFromLoopEvent(ev, extra) {
14205
14831
  const rec = {
14206
14832
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14243,7 +14869,7 @@ function openTranscriptFile(path2, meta) {
14243
14869
  return stream;
14244
14870
  }
14245
14871
  function readTranscript(path2) {
14246
- const raw = readFileSync12(path2, "utf8");
14872
+ const raw = readFileSync13(path2, "utf8");
14247
14873
  return parseTranscript(raw);
14248
14874
  }
14249
14875
  function parseTranscript(raw) {
@@ -14630,25 +15256,25 @@ function truncate(s, n) {
14630
15256
  }
14631
15257
 
14632
15258
  // src/version.ts
14633
- 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";
14634
15260
  import { homedir as homedir8 } from "os";
14635
- import { dirname as dirname7, join as join14 } from "path";
15261
+ import { dirname as dirname8, join as join14 } from "path";
14636
15262
  import { fileURLToPath as fileURLToPath2 } from "url";
14637
15263
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
14638
15264
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
14639
15265
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
14640
15266
  function readPackageVersion() {
14641
15267
  try {
14642
- let dir = dirname7(fileURLToPath2(import.meta.url));
15268
+ let dir = dirname8(fileURLToPath2(import.meta.url));
14643
15269
  for (let i = 0; i < 6; i++) {
14644
15270
  const p = join14(dir, "package.json");
14645
15271
  if (existsSync10(p)) {
14646
- const pkg = JSON.parse(readFileSync13(p, "utf8"));
15272
+ const pkg = JSON.parse(readFileSync14(p, "utf8"));
14647
15273
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
14648
15274
  return pkg.version;
14649
15275
  }
14650
15276
  }
14651
- const parent = dirname7(dir);
15277
+ const parent = dirname8(dir);
14652
15278
  if (parent === dir) break;
14653
15279
  dir = parent;
14654
15280
  }
@@ -14662,7 +15288,7 @@ function cachePath(homeDirOverride) {
14662
15288
  }
14663
15289
  function readCache(homeDirOverride) {
14664
15290
  try {
14665
- const raw = readFileSync13(cachePath(homeDirOverride), "utf8");
15291
+ const raw = readFileSync14(cachePath(homeDirOverride), "utf8");
14666
15292
  const parsed = JSON.parse(raw);
14667
15293
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
14668
15294
  return parsed;
@@ -14674,8 +15300,8 @@ function readCache(homeDirOverride) {
14674
15300
  function writeCache(entry, homeDirOverride) {
14675
15301
  try {
14676
15302
  const p = cachePath(homeDirOverride);
14677
- mkdirSync5(dirname7(p), { recursive: true });
14678
- writeFileSync5(p, JSON.stringify(entry), "utf8");
15303
+ mkdirSync5(dirname8(p), { recursive: true });
15304
+ writeFileSync6(p, JSON.stringify(entry), "utf8");
14679
15305
  } catch {
14680
15306
  }
14681
15307
  }
@@ -14890,7 +15516,7 @@ var McpClient = class {
14890
15516
  const id = this.nextId++;
14891
15517
  const frame = { jsonrpc: "2.0", id, method, params };
14892
15518
  let abortHandler = null;
14893
- const promise = new Promise((resolve13, reject) => {
15519
+ const promise = new Promise((resolve14, reject) => {
14894
15520
  const timeout = setTimeout(() => {
14895
15521
  this.pending.delete(id);
14896
15522
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -14899,7 +15525,7 @@ var McpClient = class {
14899
15525
  );
14900
15526
  }, this.requestTimeoutMs);
14901
15527
  this.pending.set(id, {
14902
- resolve: resolve13,
15528
+ resolve: resolve14,
14903
15529
  reject,
14904
15530
  timeout
14905
15531
  });
@@ -15031,12 +15657,12 @@ var StdioTransport = class {
15031
15657
  }
15032
15658
  async send(message) {
15033
15659
  if (this.closed) throw new Error("MCP transport is closed");
15034
- return new Promise((resolve13, reject) => {
15660
+ return new Promise((resolve14, reject) => {
15035
15661
  const line = `${JSON.stringify(message)}
15036
15662
  `;
15037
15663
  this.child.stdin.write(line, "utf8", (err) => {
15038
15664
  if (err) reject(err);
15039
- else resolve13();
15665
+ else resolve14();
15040
15666
  });
15041
15667
  });
15042
15668
  }
@@ -15047,8 +15673,8 @@ var StdioTransport = class {
15047
15673
  continue;
15048
15674
  }
15049
15675
  if (this.closed) return;
15050
- const next = await new Promise((resolve13) => {
15051
- this.waiters.push(resolve13);
15676
+ const next = await new Promise((resolve14) => {
15677
+ this.waiters.push(resolve14);
15052
15678
  });
15053
15679
  if (next === null) return;
15054
15680
  yield next;
@@ -15128,8 +15754,8 @@ var SseTransport = class {
15128
15754
  constructor(opts) {
15129
15755
  this.url = opts.url;
15130
15756
  this.headers = opts.headers ?? {};
15131
- this.endpointReady = new Promise((resolve13, reject) => {
15132
- this.resolveEndpoint = resolve13;
15757
+ this.endpointReady = new Promise((resolve14, reject) => {
15758
+ this.resolveEndpoint = resolve14;
15133
15759
  this.rejectEndpoint = reject;
15134
15760
  });
15135
15761
  this.endpointReady.catch(() => void 0);
@@ -15156,8 +15782,8 @@ var SseTransport = class {
15156
15782
  continue;
15157
15783
  }
15158
15784
  if (this.closed) return;
15159
- const next = await new Promise((resolve13) => {
15160
- this.waiters.push(resolve13);
15785
+ const next = await new Promise((resolve14) => {
15786
+ this.waiters.push(resolve14);
15161
15787
  });
15162
15788
  if (next === null) return;
15163
15789
  yield next;
@@ -15343,8 +15969,8 @@ var StreamableHttpTransport = class {
15343
15969
  continue;
15344
15970
  }
15345
15971
  if (this.closed) return;
15346
- const next = await new Promise((resolve13) => {
15347
- this.waiters.push(resolve13);
15972
+ const next = await new Promise((resolve14) => {
15973
+ this.waiters.push(resolve14);
15348
15974
  });
15349
15975
  if (next === null) return;
15350
15976
  yield next;
@@ -15442,13 +16068,13 @@ import {
15442
16068
  ftruncateSync,
15443
16069
  mkdirSync as mkdirSync6,
15444
16070
  openSync as openSync2,
15445
- readFileSync as readFileSync14,
16071
+ readFileSync as readFileSync15,
15446
16072
  readSync,
15447
16073
  unlinkSync as unlinkSync3,
15448
- writeFileSync as writeFileSync6,
16074
+ writeFileSync as writeFileSync7,
15449
16075
  writeSync
15450
16076
  } from "fs";
15451
- import { dirname as dirname8, resolve as resolve12 } from "path";
16077
+ import { dirname as dirname9, isAbsolute as isAbsolute9, relative as relative10, resolve as resolve13 } from "path";
15452
16078
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
15453
16079
  function parseEditBlocks(text) {
15454
16080
  const out = [];
@@ -15465,10 +16091,30 @@ function parseEditBlocks(text) {
15465
16091
  }
15466
16092
  return out;
15467
16093
  }
16094
+ function resolveEditPath(rootDir, rawPath) {
16095
+ const absRoot = resolve13(rootDir);
16096
+ if (/^[A-Za-z]:[\\/]/.test(rawPath) || looksLikeAbsoluteSystemPath2(rawPath)) {
16097
+ return resolve13(rawPath);
16098
+ }
16099
+ let rooted = rawPath;
16100
+ while (rooted.startsWith("/") || rooted.startsWith("\\")) {
16101
+ rooted = rooted.slice(1);
16102
+ }
16103
+ return resolve13(absRoot, rooted || ".");
16104
+ }
16105
+ function looksLikeAbsoluteSystemPath2(rawPath) {
16106
+ return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
16107
+ rawPath
16108
+ );
16109
+ }
16110
+ function pathIsUnder4(child, parent) {
16111
+ const rel = relative10(parent, child);
16112
+ return rel === "" || !rel.startsWith("..") && !isAbsolute9(rel);
16113
+ }
15468
16114
  function applyEditBlock(block, rootDir) {
15469
- const absRoot = resolve12(rootDir);
15470
- const absTarget = resolve12(absRoot, block.path);
15471
- if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep2()}`)) {
16115
+ const absRoot = resolve13(rootDir);
16116
+ const absTarget = resolveEditPath(rootDir, block.path);
16117
+ if (!pathIsUnder4(absTarget, absRoot)) {
15472
16118
  return {
15473
16119
  path: block.path,
15474
16120
  status: "path-escape",
@@ -15478,7 +16124,7 @@ function applyEditBlock(block, rootDir) {
15478
16124
  const searchEmpty = block.search.length === 0;
15479
16125
  if (searchEmpty) {
15480
16126
  try {
15481
- mkdirSync6(dirname8(absTarget), { recursive: true });
16127
+ mkdirSync6(dirname9(absTarget), { recursive: true });
15482
16128
  const fd = openSync2(absTarget, "wx");
15483
16129
  try {
15484
16130
  writeSync(fd, block.replace);
@@ -15521,7 +16167,7 @@ function applyEditBlock(block, rootDir) {
15521
16167
  if (n <= 0) break;
15522
16168
  readBytes += n;
15523
16169
  }
15524
- const content = inBuf.toString("utf8", 0, readBytes);
16170
+ const { text: content, encoding } = decodeFileBuffer(inBuf.subarray(0, readBytes));
15525
16171
  const le = lineEndingOf(content);
15526
16172
  const adaptedSearch = block.search.replace(/\r?\n/g, le);
15527
16173
  const adaptedReplace = block.replace.replace(/\r?\n/g, le);
@@ -15542,7 +16188,7 @@ function applyEditBlock(block, rootDir) {
15542
16188
  };
15543
16189
  }
15544
16190
  const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
15545
- const outBuf = Buffer.from(replaced, "utf8");
16191
+ const outBuf = encodeFile(replaced, encoding);
15546
16192
  ftruncateSync(fd, outBuf.length);
15547
16193
  let written = 0;
15548
16194
  while (written < outBuf.length) {
@@ -15562,19 +16208,21 @@ function applyEditBlocks(blocks, rootDir) {
15562
16208
  return blocks.map((b) => applyEditBlock(b, rootDir));
15563
16209
  }
15564
16210
  function snapshotBeforeEdits(blocks, rootDir) {
15565
- const absRoot = resolve12(rootDir);
16211
+ const absRoot = resolve13(rootDir);
15566
16212
  const seen = /* @__PURE__ */ new Set();
15567
16213
  const snapshots = [];
15568
16214
  for (const b of blocks) {
15569
- if (seen.has(b.path)) continue;
15570
- seen.add(b.path);
15571
- const abs = resolve12(absRoot, b.path);
16215
+ const abs = resolveEditPath(rootDir, b.path);
16216
+ if (!pathIsUnder4(abs, absRoot)) continue;
16217
+ if (seen.has(abs)) continue;
16218
+ seen.add(abs);
15572
16219
  if (!existsSync11(abs)) {
15573
16220
  snapshots.push({ path: b.path, prevContent: null });
15574
16221
  continue;
15575
16222
  }
15576
16223
  try {
15577
- 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 });
15578
16226
  } catch {
15579
16227
  snapshots.push({ path: b.path, prevContent: null });
15580
16228
  }
@@ -15582,10 +16230,10 @@ function snapshotBeforeEdits(blocks, rootDir) {
15582
16230
  return snapshots;
15583
16231
  }
15584
16232
  function restoreSnapshots(snapshots, rootDir) {
15585
- const absRoot = resolve12(rootDir);
16233
+ const absRoot = resolve13(rootDir);
15586
16234
  return snapshots.map((snap) => {
15587
- const abs = resolve12(absRoot, snap.path);
15588
- if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep2()}`)) {
16235
+ const abs = resolveEditPath(rootDir, snap.path);
16236
+ if (!pathIsUnder4(abs, absRoot)) {
15589
16237
  return {
15590
16238
  path: snap.path,
15591
16239
  status: "path-escape",
@@ -15601,7 +16249,7 @@ function restoreSnapshots(snapshots, rootDir) {
15601
16249
  message: "removed (the edit had created it)"
15602
16250
  };
15603
16251
  }
15604
- writeFileSync6(abs, snap.prevContent, "utf8");
16252
+ writeFileSync7(abs, encodeFile(snap.prevContent, snap.prevEncoding ?? "utf8"));
15605
16253
  return {
15606
16254
  path: snap.path,
15607
16255
  status: "applied",
@@ -15612,15 +16260,12 @@ function restoreSnapshots(snapshots, rootDir) {
15612
16260
  }
15613
16261
  });
15614
16262
  }
15615
- function sep2() {
15616
- return process.platform === "win32" ? "\\" : "/";
15617
- }
15618
16263
  function lineEndingOf(text) {
15619
16264
  return text.includes("\r\n") ? "\r\n" : "\n";
15620
16265
  }
15621
16266
 
15622
16267
  // src/code/prompt.ts
15623
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
16268
+ import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
15624
16269
  import { join as join15 } from "path";
15625
16270
  var DEFAULT_CODE_MODEL = "deepseek-v4-flash";
15626
16271
  function codeSystemBase(modelId) {
@@ -15684,7 +16329,7 @@ the new lines
15684
16329
  >>>>>>> REPLACE
15685
16330
 
15686
16331
  Rules:
15687
- - 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).
15688
16333
  - One edit per block; multiple blocks per response are fine.
15689
16334
  - Create a new file with empty SEARCH:
15690
16335
  path/to/new.ts
@@ -15727,6 +16372,10 @@ When the user says run / start / launch / serve / boot up: start it, verify it c
15727
16372
  - One short paragraph explaining *why*, then the blocks.
15728
16373
  - Silence during exploration is fine \u2014 tool calls first, prose after.
15729
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
+
15730
16379
  __ESCALATION_CONTRACT__
15731
16380
 
15732
16381
  ${TUI_FORMATTING_RULES}
@@ -15751,7 +16400,7 @@ function codeSystemPrompt(rootDir, opts = {}) {
15751
16400
  if (existsSync12(gitignorePath)) {
15752
16401
  let content;
15753
16402
  try {
15754
- content = readFileSync15(gitignorePath, "utf8");
16403
+ content = readFileSync16(gitignorePath, "utf8");
15755
16404
  } catch {
15756
16405
  }
15757
16406
  if (content !== void 0) {
@@ -15783,21 +16432,21 @@ ${appendParts.join("\n\n")}`;
15783
16432
 
15784
16433
  // src/telemetry/usage.ts
15785
16434
  import {
15786
- appendFileSync as appendFileSync2,
16435
+ appendFileSync as appendFileSync3,
15787
16436
  closeSync as closeSync3,
15788
16437
  existsSync as existsSync13,
15789
16438
  fstatSync as fstatSync2,
15790
16439
  mkdirSync as mkdirSync7,
15791
16440
  openSync as openSync3,
15792
- readFileSync as readFileSync16,
16441
+ readFileSync as readFileSync17,
15793
16442
  readSync as readSync2,
15794
- renameSync as renameSync2,
16443
+ renameSync as renameSync3,
15795
16444
  statSync as statSync6,
15796
16445
  unlinkSync as unlinkSync4,
15797
- writeFileSync as writeFileSync7
16446
+ writeFileSync as writeFileSync8
15798
16447
  } from "fs";
15799
16448
  import { homedir as homedir9 } from "os";
15800
- import { dirname as dirname9, join as join16 } from "path";
16449
+ import { dirname as dirname10, join as join16 } from "path";
15801
16450
  function defaultUsageLogPath(homeDirOverride) {
15802
16451
  return join16(homeDirOverride ?? homedir9(), ".reasonix", "usage.jsonl");
15803
16452
  }
@@ -15838,9 +16487,9 @@ function compactUsageLogIfLarge(path2, now) {
15838
16487
  if (kept.length === lines.filter((l) => l.trim()).length) return;
15839
16488
  const tmp = `${path2}.compacting`;
15840
16489
  try {
15841
- writeFileSync7(tmp, kept.length > 0 ? `${kept.join("\n")}
16490
+ writeFileSync8(tmp, kept.length > 0 ? `${kept.join("\n")}
15842
16491
  ` : "", "utf8");
15843
- renameSync2(tmp, path2);
16492
+ renameSync3(tmp, path2);
15844
16493
  } catch {
15845
16494
  try {
15846
16495
  unlinkSync4(tmp);
@@ -15864,8 +16513,8 @@ function appendUsage(input) {
15864
16513
  if (input.subagent) record.subagent = input.subagent;
15865
16514
  const path2 = input.path ?? defaultUsageLogPath();
15866
16515
  try {
15867
- mkdirSync7(dirname9(path2), { recursive: true });
15868
- appendFileSync2(path2, `${JSON.stringify(record)}
16516
+ mkdirSync7(dirname10(path2), { recursive: true });
16517
+ appendFileSync3(path2, `${JSON.stringify(record)}
15869
16518
  `, "utf8");
15870
16519
  compactUsageLogIfLarge(path2, record.ts);
15871
16520
  } catch {
@@ -15876,7 +16525,7 @@ function readUsageLog(path2 = defaultUsageLogPath()) {
15876
16525
  if (!existsSync13(path2)) return [];
15877
16526
  let raw;
15878
16527
  try {
15879
- raw = readFileSync16(path2, "utf8");
16528
+ raw = readFileSync17(path2, "utf8");
15880
16529
  } catch {
15881
16530
  return [];
15882
16531
  }
@@ -16108,9 +16757,9 @@ export {
16108
16757
  openTranscriptFile,
16109
16758
  outputCostUsd,
16110
16759
  parseAtQuery,
16760
+ parseBingResults,
16111
16761
  parseEditBlocks,
16112
16762
  parseMcpSpec,
16113
- parseMojeekResults,
16114
16763
  parseSearxngHtmlResults,
16115
16764
  parseTranscript,
16116
16765
  prepareSpawn,