reasonix 0.47.2 → 0.48.1

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 (129) hide show
  1. package/README.md +14 -26
  2. package/README.zh-CN.md +5 -26
  3. package/dist/cli/{acp-GEOAKSTU.js → acp-QJGGHQLA.js} +52 -135
  4. package/dist/cli/acp-QJGGHQLA.js.map +1 -0
  5. package/dist/cli/{chat-YTPATMMG.js → chat-ZXF227MP.js} +25 -25
  6. package/dist/cli/{chunk-DN4B5S6Y.js → chunk-3FULTFRB.js} +2 -2
  7. package/dist/cli/chunk-43ROGEX2.js +5190 -0
  8. package/dist/cli/chunk-43ROGEX2.js.map +1 -0
  9. package/dist/cli/{chunk-DQ6K5ZQ7.js → chunk-4E2BHJU4.js} +4 -4
  10. package/dist/cli/chunk-4E2BHJU4.js.map +1 -0
  11. package/dist/cli/{chunk-T5A7EY6B.js → chunk-5AW6NIHU.js} +2 -2
  12. package/dist/cli/{chunk-DWPAKZTY.js → chunk-5U5LMMFF.js} +2 -2
  13. package/dist/cli/{chunk-TRSAHHCL.js → chunk-6FRNXWDZ.js} +321 -76
  14. package/dist/cli/chunk-6FRNXWDZ.js.map +1 -0
  15. package/dist/cli/{chunk-KYQVQ5X4.js → chunk-B5CZL2SE.js} +9 -4
  16. package/dist/cli/chunk-B5CZL2SE.js.map +1 -0
  17. package/dist/cli/{chunk-XMHP7BEE.js → chunk-DABAOQSV.js} +2273 -2517
  18. package/dist/cli/chunk-DABAOQSV.js.map +1 -0
  19. package/dist/cli/chunk-EO6RPTJG.js +831 -0
  20. package/dist/cli/chunk-EO6RPTJG.js.map +1 -0
  21. package/dist/cli/{chunk-BQ6HC66J.js → chunk-FD7SNDWW.js} +4 -14
  22. package/dist/cli/chunk-FD7SNDWW.js.map +1 -0
  23. package/dist/cli/chunk-FPME5QOO.js +17747 -0
  24. package/dist/cli/chunk-FPME5QOO.js.map +1 -0
  25. package/dist/cli/{chunk-6QC5RQLE.js → chunk-H2F4LDNH.js} +2 -2
  26. package/dist/cli/{chunk-QCFLPSPH.js → chunk-IKSYVBBZ.js} +2 -2
  27. package/dist/cli/{chunk-5QCB62C4.js → chunk-J2TQAWOM.js} +135 -18
  28. package/dist/cli/{chunk-5QCB62C4.js.map → chunk-J2TQAWOM.js.map} +1 -1
  29. package/dist/cli/{chunk-JBH5RM7X.js → chunk-JFBGSWQB.js} +395 -85
  30. package/dist/cli/chunk-JFBGSWQB.js.map +1 -0
  31. package/dist/cli/{chunk-CCJAP7G3.js → chunk-KH5JIJJD.js} +2 -2
  32. package/dist/cli/{chunk-TDHXB2ER.js → chunk-NQZ5U37J.js} +2 -2
  33. package/dist/cli/{chunk-CNG32VAB.js → chunk-O3AGYTG2.js} +2 -2
  34. package/dist/cli/{chunk-NRQ5UP5T.js → chunk-PIC5HJRD.js} +234 -40
  35. package/dist/cli/chunk-PIC5HJRD.js.map +1 -0
  36. package/dist/cli/{chunk-GH7DC2Y5.js → chunk-PJIQIYTV.js} +6 -3
  37. package/dist/cli/chunk-PJIQIYTV.js.map +1 -0
  38. package/dist/cli/{chunk-KVZZ5U75.js → chunk-R7U44O3Y.js} +2 -2
  39. package/dist/cli/{chunk-ZXSCAODE.js → chunk-RCLS63KE.js} +71 -3
  40. package/dist/cli/chunk-RCLS63KE.js.map +1 -0
  41. package/dist/cli/{chunk-FY4S7TJZ.js → chunk-SLAFMXAZ.js} +10 -2
  42. package/dist/cli/chunk-SLAFMXAZ.js.map +1 -0
  43. package/dist/cli/{chunk-TRWHTFG7.js → chunk-SWUMD2LX.js} +2 -2
  44. package/dist/cli/{chunk-2XY77LW7.js → chunk-TIJ4ZHD6.js} +56 -24
  45. package/dist/cli/chunk-TIJ4ZHD6.js.map +1 -0
  46. package/dist/cli/{chunk-4MFCAZ2W.js → chunk-WKOXKCF3.js} +3 -3
  47. package/dist/cli/{chunk-HUILPCYX.js → chunk-XMR2VCGT.js} +3 -3
  48. package/dist/cli/{code-Q4NRVEDG.js → code-OKA5YPOH.js} +31 -32
  49. package/dist/cli/code-OKA5YPOH.js.map +1 -0
  50. package/dist/cli/{commands-4CDI4GFM.js → commands-3U6PUVSS.js} +4 -4
  51. package/dist/cli/{commit-GW7LDQP5.js → commit-HFB7SRBU.js} +3 -3
  52. package/dist/cli/{desktop-EG6P5SF2.js → desktop-G7UMW3CJ.js} +503 -48
  53. package/dist/cli/desktop-G7UMW3CJ.js.map +1 -0
  54. package/dist/cli/{diff-VI2YX4FN.js → diff-PGXW4YZD.js} +8 -8
  55. package/dist/cli/{doctor-CQTTZP27.js → doctor-WWJFLVCB.js} +9 -9
  56. package/dist/cli/index.js +53 -41
  57. package/dist/cli/index.js.map +1 -1
  58. package/dist/cli/{mcp-J2UCD4RZ.js → mcp-Y3VKIK3T.js} +2 -2
  59. package/dist/cli/{mcp-browse-GSX34JEK.js → mcp-browse-4IN2QIFR.js} +2 -2
  60. package/dist/cli/{mcp-inspect-RRFYF4ZV.js → mcp-inspect-BUXFXDHB.js} +2 -2
  61. package/dist/cli/{prompt-5TQPIVHV.js → prompt-US57R6BA.js} +5 -4
  62. package/dist/cli/{replay-MJCEMODU.js → replay-EQJMZRB3.js} +8 -8
  63. package/dist/cli/{run-P4D5VDYE.js → run-KVEI66TR.js} +14 -14
  64. package/dist/cli/{server-C25JNNZV.js → server-D6C4GHWN.js} +18 -15
  65. package/dist/cli/server-D6C4GHWN.js.map +1 -0
  66. package/dist/cli/{sessions-QIONZJQ6.js → sessions-TGVS2RQZ.js} +13 -13
  67. package/dist/cli/{setup-NLQ6G5G4.js → setup-WLKX6GGG.js} +5 -5
  68. package/dist/cli/{stats-DFZEXHP4.js → stats-TCD7Q6MB.js} +6 -6
  69. package/dist/cli/{version-GR3X3MPI.js → version-BCWWS2OU.js} +13 -13
  70. package/dist/grammars/tree-sitter-go.wasm +0 -0
  71. package/dist/grammars/tree-sitter-java.wasm +0 -0
  72. package/dist/grammars/tree-sitter-javascript.wasm +0 -0
  73. package/dist/grammars/tree-sitter-python.wasm +0 -0
  74. package/dist/grammars/tree-sitter-rust.wasm +0 -0
  75. package/dist/grammars/tree-sitter-tsx.wasm +0 -0
  76. package/dist/grammars/tree-sitter-typescript.wasm +0 -0
  77. package/dist/grammars/web-tree-sitter.wasm +0 -0
  78. package/dist/index.d.ts +46 -12
  79. package/dist/index.js +684 -129
  80. package/dist/index.js.map +1 -1
  81. package/package.json +16 -4
  82. package/dist/cli/acp-GEOAKSTU.js.map +0 -1
  83. package/dist/cli/chunk-2XY77LW7.js.map +0 -1
  84. package/dist/cli/chunk-6CRPCJAU.js +0 -3141
  85. package/dist/cli/chunk-6CRPCJAU.js.map +0 -1
  86. package/dist/cli/chunk-BQ6HC66J.js.map +0 -1
  87. package/dist/cli/chunk-DQ6K5ZQ7.js.map +0 -1
  88. package/dist/cli/chunk-FY4S7TJZ.js.map +0 -1
  89. package/dist/cli/chunk-GH7DC2Y5.js.map +0 -1
  90. package/dist/cli/chunk-JBH5RM7X.js.map +0 -1
  91. package/dist/cli/chunk-KYQVQ5X4.js.map +0 -1
  92. package/dist/cli/chunk-NRQ5UP5T.js.map +0 -1
  93. package/dist/cli/chunk-TRSAHHCL.js.map +0 -1
  94. package/dist/cli/chunk-XD6P7AFH.js +0 -375
  95. package/dist/cli/chunk-XD6P7AFH.js.map +0 -1
  96. package/dist/cli/chunk-XMHP7BEE.js.map +0 -1
  97. package/dist/cli/chunk-YFP3MYMY.js +0 -323
  98. package/dist/cli/chunk-YFP3MYMY.js.map +0 -1
  99. package/dist/cli/chunk-ZXSCAODE.js.map +0 -1
  100. package/dist/cli/code-Q4NRVEDG.js.map +0 -1
  101. package/dist/cli/desktop-EG6P5SF2.js.map +0 -1
  102. package/dist/cli/server-C25JNNZV.js.map +0 -1
  103. /package/dist/cli/{chat-YTPATMMG.js.map → chat-ZXF227MP.js.map} +0 -0
  104. /package/dist/cli/{chunk-DN4B5S6Y.js.map → chunk-3FULTFRB.js.map} +0 -0
  105. /package/dist/cli/{chunk-T5A7EY6B.js.map → chunk-5AW6NIHU.js.map} +0 -0
  106. /package/dist/cli/{chunk-DWPAKZTY.js.map → chunk-5U5LMMFF.js.map} +0 -0
  107. /package/dist/cli/{chunk-6QC5RQLE.js.map → chunk-H2F4LDNH.js.map} +0 -0
  108. /package/dist/cli/{chunk-QCFLPSPH.js.map → chunk-IKSYVBBZ.js.map} +0 -0
  109. /package/dist/cli/{chunk-CCJAP7G3.js.map → chunk-KH5JIJJD.js.map} +0 -0
  110. /package/dist/cli/{chunk-TDHXB2ER.js.map → chunk-NQZ5U37J.js.map} +0 -0
  111. /package/dist/cli/{chunk-CNG32VAB.js.map → chunk-O3AGYTG2.js.map} +0 -0
  112. /package/dist/cli/{chunk-KVZZ5U75.js.map → chunk-R7U44O3Y.js.map} +0 -0
  113. /package/dist/cli/{chunk-TRWHTFG7.js.map → chunk-SWUMD2LX.js.map} +0 -0
  114. /package/dist/cli/{chunk-4MFCAZ2W.js.map → chunk-WKOXKCF3.js.map} +0 -0
  115. /package/dist/cli/{chunk-HUILPCYX.js.map → chunk-XMR2VCGT.js.map} +0 -0
  116. /package/dist/cli/{commands-4CDI4GFM.js.map → commands-3U6PUVSS.js.map} +0 -0
  117. /package/dist/cli/{commit-GW7LDQP5.js.map → commit-HFB7SRBU.js.map} +0 -0
  118. /package/dist/cli/{diff-VI2YX4FN.js.map → diff-PGXW4YZD.js.map} +0 -0
  119. /package/dist/cli/{doctor-CQTTZP27.js.map → doctor-WWJFLVCB.js.map} +0 -0
  120. /package/dist/cli/{mcp-J2UCD4RZ.js.map → mcp-Y3VKIK3T.js.map} +0 -0
  121. /package/dist/cli/{mcp-browse-GSX34JEK.js.map → mcp-browse-4IN2QIFR.js.map} +0 -0
  122. /package/dist/cli/{mcp-inspect-RRFYF4ZV.js.map → mcp-inspect-BUXFXDHB.js.map} +0 -0
  123. /package/dist/cli/{prompt-5TQPIVHV.js.map → prompt-US57R6BA.js.map} +0 -0
  124. /package/dist/cli/{replay-MJCEMODU.js.map → replay-EQJMZRB3.js.map} +0 -0
  125. /package/dist/cli/{run-P4D5VDYE.js.map → run-KVEI66TR.js.map} +0 -0
  126. /package/dist/cli/{sessions-QIONZJQ6.js.map → sessions-TGVS2RQZ.js.map} +0 -0
  127. /package/dist/cli/{setup-NLQ6G5G4.js.map → setup-WLKX6GGG.js.map} +0 -0
  128. /package/dist/cli/{stats-DFZEXHP4.js.map → stats-TCD7Q6MB.js.map} +0 -0
  129. /package/dist/cli/{version-GR3X3MPI.js.map → version-BCWWS2OU.js.map} +0 -0
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { createParser } from "eventsource-parser";
5
5
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
6
  import { homedir } from "os";
7
7
  import { dirname, isAbsolute, join, resolve } from "path";
8
+ import { z } from "zod";
8
9
 
9
10
  // src/cli/ui/theme/tokens.ts
10
11
  function card(fg, tone) {
@@ -465,12 +466,11 @@ function memoryTypeDefaults(typeName, cfg = readConfig()) {
465
466
  if (found.expires) out.expires = found.expires;
466
467
  return out;
467
468
  }
468
- var DEFAULT_METASO_API_KEY = "mk-E384C1DD5E8501BB7EFE27C949AFDE5B";
469
469
  function loadMetasoApiKey(path2 = defaultConfigPath()) {
470
- if (process.env.METASO_API_KEY) return process.env.METASO_API_KEY;
470
+ if (process.env.METASO_API_KEY) return process.env.METASO_API_KEY.trim();
471
471
  const cfg = readConfig(path2).metasoApiKey;
472
472
  if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
473
- return DEFAULT_METASO_API_KEY;
473
+ return void 0;
474
474
  }
475
475
  function loadTavilyApiKey(path2 = defaultConfigPath()) {
476
476
  if (process.env.TAVILY_API_KEY) return process.env.TAVILY_API_KEY.trim();
@@ -478,14 +478,65 @@ function loadTavilyApiKey(path2 = defaultConfigPath()) {
478
478
  if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
479
479
  return void 0;
480
480
  }
481
+ function loadPerplexityApiKey(path2 = defaultConfigPath()) {
482
+ if (process.env.PERPLEXITY_API_KEY) return process.env.PERPLEXITY_API_KEY.trim();
483
+ const cfg = readConfig(path2).perplexityApiKey;
484
+ if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
485
+ return void 0;
486
+ }
487
+ function loadExaApiKey(path2 = defaultConfigPath()) {
488
+ if (process.env.EXA_API_KEY) return process.env.EXA_API_KEY.trim();
489
+ const cfg = readConfig(path2).exaApiKey;
490
+ if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
491
+ return void 0;
492
+ }
481
493
  function defaultConfigPath() {
482
494
  return join(homedir(), ".reasonix", "config.json");
483
495
  }
496
+ var STRING_ARRAY_FIELDS = [
497
+ ["mcp"],
498
+ ["mcpDisabled"],
499
+ ["recentWorkspaces"],
500
+ ["skills", "paths"]
501
+ ];
502
+ var stringArraySchema = z.array(z.string());
503
+ function sanitizeStringArrayField(cfg, segments, filePath) {
504
+ if (segments.length === 0) return;
505
+ let parent = cfg;
506
+ for (let i = 0; i < segments.length - 1; i++) {
507
+ const seg = segments[i];
508
+ const next = parent[seg];
509
+ if (!next || typeof next !== "object" || Array.isArray(next)) return;
510
+ parent = next;
511
+ }
512
+ const leaf = segments[segments.length - 1];
513
+ const value = parent[leaf];
514
+ if (value === void 0) return;
515
+ const fieldName = segments.join(".");
516
+ if (!Array.isArray(value)) {
517
+ console.warn(`reasonix: config "${filePath}" field "${fieldName}" is not an array \u2014 ignoring`);
518
+ delete parent[leaf];
519
+ return;
520
+ }
521
+ const parsed = stringArraySchema.safeParse(value);
522
+ if (parsed.success) return;
523
+ const filtered = value.filter((x) => typeof x === "string");
524
+ console.warn(
525
+ `reasonix: config "${filePath}" field "${fieldName}" had ${value.length - filtered.length} non-string item(s) \u2014 dropped`
526
+ );
527
+ parent[leaf] = filtered;
528
+ }
484
529
  function readConfig(path2 = defaultConfigPath()) {
485
530
  try {
486
- const raw = readFileSync(path2, "utf8");
531
+ const raw = readFileSync(path2, "utf8").replace(/^\uFEFF/, "");
487
532
  const parsed = JSON.parse(raw);
488
- if (parsed && typeof parsed === "object") return parsed;
533
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
534
+ const cfg = parsed;
535
+ for (const segments of STRING_ARRAY_FIELDS) {
536
+ sanitizeStringArrayField(cfg, segments, path2);
537
+ }
538
+ return cfg;
539
+ }
489
540
  } catch {
490
541
  }
491
542
  return {};
@@ -580,6 +631,8 @@ function webSearchEngine(path2 = defaultConfigPath()) {
580
631
  if (cfg === "searxng") return "searxng";
581
632
  if (cfg === "metaso") return "metaso";
582
633
  if (cfg === "tavily") return "tavily";
634
+ if (cfg === "perplexity") return "perplexity";
635
+ if (cfg === "exa") return "exa";
583
636
  return "mojeek";
584
637
  }
585
638
  function webSearchEndpoint(path2 = defaultConfigPath()) {
@@ -615,6 +668,11 @@ function addProjectShellAllowed(rootDir, prefix, path2 = defaultConfigPath()) {
615
668
  cfg.projects[key].shellAllowed = [...existing, trimmed];
616
669
  writeConfig(cfg, path2);
617
670
  }
671
+ function projectHooksTrusted(rootDir, path2 = defaultConfigPath()) {
672
+ const cfg = readConfig(path2);
673
+ const key = findProjectKey(cfg, rootDir);
674
+ return key !== void 0 && cfg.projects?.[key]?.hooksTrusted === true;
675
+ }
618
676
  function loadProjectPathAllowed(rootDir, path2 = defaultConfigPath()) {
619
677
  const cfg = readConfig(path2);
620
678
  const key = findProjectKey(cfg, rootDir);
@@ -1286,6 +1344,8 @@ var EN = {
1286
1344
  tipShownOnce: "shown once",
1287
1345
  modelOverride: "override the default model",
1288
1346
  noSession: "disable session persistence for this run",
1347
+ noMouseHint: "disable SGR mouse tracking; restores native drag-select and right-click",
1348
+ noProxyHint: "ignore HTTPS_PROXY / HTTP_PROXY for this run; go direct",
1289
1349
  resumeHint: "force-resume the named session (even if idle)",
1290
1350
  newHint: "force a fresh session (ignore --session / --continue)",
1291
1351
  transcriptHint: "path to write the JSONL transcript",
@@ -1427,6 +1487,7 @@ var EN = {
1427
1487
  },
1428
1488
  stop: { description: "abort the current model turn (typed alternative to Esc)" },
1429
1489
  feedback: { description: "open a GitHub issue with diagnostic info copied to clipboard" },
1490
+ about: { description: "project info \u2014 version, website, repo, license" },
1430
1491
  keys: { description: "keyboard + mouse + copy/paste reference" },
1431
1492
  plans: { description: "list this session's active + archived plans, newest first" },
1432
1493
  replay: {
@@ -1502,8 +1563,8 @@ var EN = {
1502
1563
  argsHint: "<question>"
1503
1564
  },
1504
1565
  "search-engine": {
1505
- description: "switch web search backend \u2014 mojeek (default, no deps), searxng (self-hosted), or metaso (free quota 100/d)",
1506
- argsHint: "<mojeek|searxng|metaso> [<endpoint>]"
1566
+ description: "switch web search backend \u2014 mojeek (default, no deps), searxng (self-hosted), metaso (free 100/d), tavily (free 1000/mo), perplexity (AI-native), or exa (AI-native)",
1567
+ argsHint: "<mojeek|searxng|metaso|tavily|perplexity|exa> [<key>]"
1507
1568
  }
1508
1569
  },
1509
1570
  wizard: {
@@ -1694,7 +1755,24 @@ var EN = {
1694
1755
  continuingAfter: "\u25B8 continuing after {label}{counter}",
1695
1756
  planStoppedAt: "\u25B8 plan stopped at {label}{counter}",
1696
1757
  revisingAfter: "\u25B8 revising after {label} \u2014 {feedback}",
1697
- historyScrollHint: " \u2191 reading history \xB7 End / PgDn returns to bottom \xB7 \u2193 advances one line"
1758
+ historyScrollHint: " \u2191 reading history \xB7 End / PgDn returns to bottom \xB7 \u2193 advances one line",
1759
+ editHistoryTitle: "Edit history (oldest first):",
1760
+ editHistoryNoCodeMode: "not in code mode",
1761
+ editHistoryNoEdits: "no edits recorded this session yet",
1762
+ editHistoryNoShowId: "usage: /show [id] [path] (omit id for newest; path from the per-file summary)",
1763
+ editHistoryIdNotFound: "no edit #{id} \u2014 run /history to see valid ids",
1764
+ editHistoryLookupFailed: "unexpected: history lookup failed",
1765
+ editHistoryBatchNoFile: `batch #{id} doesn't include "{path}" \u2014 files in this batch: {files}`,
1766
+ editHistoryNoEdits2: "no edits recorded this session \u2014 /history is empty",
1767
+ editHistoryStatusApplied: "applied",
1768
+ editHistoryStatusPartial: "PARTIAL",
1769
+ editHistoryStatusUndone: "UNDONE",
1770
+ editHistoryHelpShow: "/show <id> \u2192 per-file summary \xB7 /show <id> <path> \u2192 full diff of one file",
1771
+ editHistoryHelpUndo: "/undo \u2192 newest non-undone \xB7 /undo <id> [path] \u2192 target a specific batch or file",
1772
+ editHistoryAlreadyReverted: "(already reverted \u2014 /history shows the batch-level status)",
1773
+ editHistoryRevertFile: "/undo {id} {path} \u2192 revert just this file",
1774
+ mcpFailed: "MCP {name} failed",
1775
+ mcpWarn: "MCP {name} warn"
1698
1776
  },
1699
1777
  hooks: {
1700
1778
  head: "hook {tag} `{cmd}` {decision}{truncTag}",
@@ -1717,9 +1795,9 @@ var EN = {
1717
1795
  abortedAtIter: "aborted at iter {iter} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)",
1718
1796
  toolUploadStatus: "tool result uploaded \xB7 model thinking before next response\u2026",
1719
1797
  preflightTruncateStatus: "preflight: context near full, truncating oldest history\u2026",
1720
- preflightTruncated: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \u2014 truncated {beforeMessages} messages \u2192 {afterMessages}. Sending.",
1721
- 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.",
1722
- preflightNoFold: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) and nothing left to truncate \u2014 DeepSeek will likely 400. Run /clear or /new to start fresh.",
1798
+ preflightTruncated: "preflight: request ~{estimate}/{ctxMax} tokens ({pct}%) \xB7 body {bodyKB} KB \u2014 truncated {beforeMessages} messages \u2192 {afterMessages}. Sending.",
1799
+ 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.",
1800
+ 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.",
1723
1801
  flashEscalation: "\u21E7 flash requested escalation \u2014 retrying this turn on {model}{reasonSuffix}",
1724
1802
  harvestStatus: "extracting plan state from reasoning\u2026",
1725
1803
  repeatToolCallWarning: "Caught a repeated tool call \u2014 let the model see the issue and retry with a different approach.",
@@ -1795,6 +1873,10 @@ var EN = {
1795
1873
  loopNoActiveHint: "no active loop. Start one with `/loop <interval> <prompt>` (e.g. /loop 30s npm test).\nCancels on: /loop stop \xB7 Esc \xB7 /clear /new \xB7 any user-typed prompt.",
1796
1874
  loopStarted: '\u25B8 loop started \u2014 re-submitting "{prompt}" every {duration}. Type anything (or /loop stop) to cancel.',
1797
1875
  keysNeedsTui: "/keys needs a TUI context (postKeys wired).",
1876
+ aboutHeader: "Reasonix v{version} \u2014 a cache-first DeepSeek coding agent",
1877
+ aboutWebsiteLabel: "Website",
1878
+ aboutRepoLabel: "GitHub ",
1879
+ aboutLicenseLabel: "License",
1798
1880
  unknownCommand: "unknown command: /{cmd} \u2014 did you mean {list}?",
1799
1881
  unknownCommandShort: "unknown command: /{cmd} (try /help)"
1800
1882
  },
@@ -2002,6 +2084,12 @@ var EN = {
2002
2084
  statusMcp: " mcp {servers} server(s), {tools} tool(s) in registry",
2003
2085
  statusEdits: " edits {count} pending (/apply to commit, /discard to drop)",
2004
2086
  statusPlan: " plan ON \u2014 writes gated (submit_plan + approval)",
2087
+ statusLifecycle: " lifecycle {mode}/{state} \xB7 {progress}{evidence}",
2088
+ lifecycleNoPlan: "no plan",
2089
+ lifecycleEvidencePending: "evidence pending",
2090
+ lifecycleRejected: "lifecycle: {tool} blocked in {state} \u2014 next: {next}",
2091
+ lifecycleEvidenceRejected: "lifecycle: step {stepId} needs evidence \u2014 next: {next}",
2092
+ lifecycleRepeatedRejected: "lifecycle: repeated {tool} rejection \u2014 do not retry identical args",
2005
2093
  statusModeYolo: " mode YOLO \u2014 edits + shell auto-run with no prompt (/undo still rolls back \xB7 Shift+Tab to flip)",
2006
2094
  statusModeAuto: " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)",
2007
2095
  statusModeReview: " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)",
@@ -2112,6 +2200,8 @@ var EN = {
2112
2200
  usageSearxngUrl: " /search-engine searxng <url> use SearXNG at custom endpoint",
2113
2201
  usageMetaso: " /search-engine metaso use Metaso API (100/d free, configure your own API key for more)",
2114
2202
  usageTavily: " /search-engine tavily use Tavily API (LLM-friendly, free 1000/mo \u2014 set TAVILY_API_KEY or tavilyApiKey in config; get one at https://tavily.com)",
2203
+ usagePerplexity: " /search-engine perplexity use Perplexity AI (AI-native answer + citations \u2014 set PERPLEXITY_API_KEY or perplexityApiKey in config; get one at https://perplexity.ai/settings/api)",
2204
+ usageExa: " /search-engine exa use Exa API (AI-native answer + citations, free 1000/mo \u2014 set EXA_API_KEY or exaApiKey in config; sign up at https://exa.ai)",
2115
2205
  alias: "Alias: /se",
2116
2206
  searxngInfo: "SearXNG is a self-hosted metasearch engine (https://github.com/searxng/searxng).",
2117
2207
  searxngInstall: "Install it with: docker run -d -p 8080:8080 searxng/searxng",
@@ -2119,7 +2209,11 @@ var EN = {
2119
2209
  switchedSearxngNote: " Make sure SearXNG is running at {endpoint}.",
2120
2210
  switchedMetasoNote: " There is a daily quota of 100 (configure your own API key for higher limits).",
2121
2211
  switchedTavilyNote: " Set TAVILY_API_KEY or `tavilyApiKey` in config; free 1000/mo at https://tavily.com.",
2122
- confirmed: '\u2713 Web search engine set to "{engine}"{detail}. Next assistant turn will pick up the change.',
2212
+ switchedPerplexityNote: " Set PERPLEXITY_API_KEY or `perplexityApiKey` in config; get one at https://perplexity.ai/settings/api.",
2213
+ switchedExaNote: " Set EXA_API_KEY or `exaApiKey` in config; sign up at https://exa.ai.",
2214
+ keyNeeded: 'No API key configured for "{engine}".\n\n 1. Set the {envVar} environment variable\n 2. Or provide one inline: /search-engine {engine} <your-key>\n 3. Or add "{engine}ApiKey" to ~/.reasonix/config.json\n\nThen retry /search-engine {engine}.',
2215
+ keySaved: " API key saved to config.",
2216
+ confirmed: 'Web search engine set to "{engine}"{detail}. Next assistant turn will pick up the change.',
2123
2217
  confirmedDetail: " ({endpoint})"
2124
2218
  },
2125
2219
  skill: {
@@ -2166,7 +2260,8 @@ var EN = {
2166
2260
  evt: " evt",
2167
2261
  editsLabel: "edits:",
2168
2262
  mcpLoading: "MCP",
2169
- ctx: "ctx"
2263
+ ctx: "ctx",
2264
+ shortcutsHint: "Ctrl+P shortcuts"
2170
2265
  },
2171
2266
  editMode: {
2172
2267
  plan: "PLAN MODE",
@@ -2378,27 +2473,38 @@ var EN = {
2378
2473
  probeFailed: "probe failed \u2014 {message}"
2379
2474
  },
2380
2475
  webErrors: {
2381
- status: "web_search {status} \u2014 try: the search backend returned an error; rephrase the query, or switch engine with /search-engine mojeek|searxng",
2476
+ status: "web_search {status} \u2014 try: the search backend returned an error; rephrase the query, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2382
2477
  rateLimit429: "web_search 429 \u2014 try: wait 10s before retrying, or rephrase the query; the search backend is rate-limiting this client",
2383
- forbidden403: "web_search 403 \u2014 try: the search backend is blocking this client; switch engine with /search-engine mojeek|searxng, or wait and retry later",
2478
+ forbidden403: "web_search 403 \u2014 try: the search backend is blocking this client; switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa, or wait and retry later",
2384
2479
  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",
2385
- mojeekBlocked: "web_search: Mojeek anti-bot page \u2014 rate-limited or blocked \u2014 try: wait 30s and retry, or switch engine with /search-engine searxng",
2386
- 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 searxng",
2480
+ mojeekBlocked: "web_search: Mojeek anti-bot page \u2014 rate-limited or blocked \u2014 try: wait 30s and retry, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2481
+ mojeekNoResults: "web_search: 0 results but response doesn't look like a real empty page ({chars} chars, first 120: {preview}) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2387
2482
  invalidEndpoint: 'web_search: invalid SearXNG endpoint "{endpoint}" \u2014 try: set a valid URL with /search-endpoint http://host:port',
2388
2483
  endpointMustBeHttp: "web_search: SearXNG endpoint must be http(s), got {protocol} \u2014 try: set a valid URL with /search-endpoint http://host:port",
2389
- 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 the default engine with /search-engine mojeek",
2390
- 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",
2391
- 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",
2484
+ cannotReach: "web_search: Cannot reach SearXNG server at {endpoint} \u2014 try: install and start SearXNG (https://github.com/searxng/searxng, e.g. `docker run -d -p 8080:8080 searxng/searxng`), or switch to another engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2485
+ searxngNoResults: "web_search: 0 results but SearXNG response doesn't look like an empty results page ({chars} chars) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2486
+ metasoMissingKey: "web_search: Metaso requires an API key \u2014 set METASO_API_KEY or configure one with /search-engine metaso <key>. Get one at https://metaso.cn/search-api/playground",
2487
+ metasoDailyLimit: "web_search: Metaso daily search limit reached \u2014 set METASO_API_KEY or get a key at https://metaso.cn/search-api/playground",
2392
2488
  metasoUnauthorized: "web_search: Metaso API key rejected \u2014 check METASO_API_KEY or get one at https://metaso.cn/search-api/playground",
2393
2489
  metasoRateLimit: "web_search: Metaso rate-limited \u2014 wait and retry, or get your own API key at https://metaso.cn/search-api/playground",
2394
- metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek",
2490
+ metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2395
2491
  metasoParseError: "web_search: Metaso returned unparseable response (HTTP {status}) \u2014 try again later",
2396
2492
  metasoApiError: "web_search: Metaso API error (code {code}: {message}) \u2014 try again later",
2397
2493
  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",
2398
2494
  tavilyUnauthorized: "web_search: Tavily API key rejected \u2014 check TAVILY_API_KEY or get one at https://tavily.com",
2399
- tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine mojeek, or upgrade your Tavily plan",
2400
- tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek",
2495
+ tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa, or upgrade your Tavily plan",
2496
+ tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2401
2497
  tavilyParseError: "web_search: Tavily returned unparseable response (HTTP {status}) \u2014 try again later",
2498
+ perplexityMissingKey: "web_search: Perplexity backend requires an API key \u2014 set PERPLEXITY_API_KEY env var or `perplexityApiKey` in ~/.reasonix/config.json; get one at https://perplexity.ai/settings/api",
2499
+ perplexityUnauthorized: "web_search: Perplexity API key rejected \u2014 check PERPLEXITY_API_KEY or get one at https://perplexity.ai/settings/api",
2500
+ perplexityRateLimit: "web_search: Perplexity rate-limited \u2014 wait and retry, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2501
+ perplexityServerError: "web_search: Perplexity server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2502
+ perplexityParseError: "web_search: Perplexity returned unparseable response (HTTP {status}) \u2014 try again later",
2503
+ exaMissingKey: "web_search: Exa backend requires an API key \u2014 set EXA_API_KEY env var or `exaApiKey` in ~/.reasonix/config.json; free 1000/mo signup at https://exa.ai",
2504
+ exaUnauthorized: "web_search: Exa API key rejected \u2014 check EXA_API_KEY or get one at https://exa.ai",
2505
+ exaRateLimit: "web_search: Exa API rate-limited or monthly quota exceeded \u2014 wait or upgrade at https://exa.ai/pricing",
2506
+ exaServerError: "web_search: Exa server error ({status}) \u2014 try again later, or switch engine with /search-engine mojeek|searxng|metaso|tavily|perplexity|exa",
2507
+ exaParseError: "web_search: Exa returned unparseable response (HTTP {status}) \u2014 try again later",
2402
2508
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2403
2509
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
2404
2510
  fetchForbidden403: "web_fetch 403 for {url} \u2014 try: the host is blocking this client; the page may require login or block bots \u2014 use web_search snippets instead",
@@ -2566,6 +2672,18 @@ var EN = {
2566
2672
  serverCount: "{count} server{s}",
2567
2673
  footer: "\u2191\u2193 pick \xB7 [r] reconnect \xB7 [d] disable \xB7 esc quit"
2568
2674
  },
2675
+ mcpBrowse: {
2676
+ noResources: "No resources on any connected MCP server (or no servers connected). `/mcp` shows the current set.",
2677
+ readOne: "Read one: `/resource <uri>` \u2014 or use Tab in the picker.",
2678
+ noPrompts: "No prompts on any connected MCP server (or no servers connected). `/mcp` shows the current set.",
2679
+ fetchOne: "Fetch one: `/prompt <name>` \u2014 args are not supported yet; prompts with required args will surface an error from the server.",
2680
+ noServerForResource: 'no server exposes resource "{name}"',
2681
+ resourceHint: "`/resource` with no arg lists what's available.",
2682
+ readFailed: "readResource failed",
2683
+ noServerForPrompt: 'no server exposes prompt "{name}"',
2684
+ promptHint: "`/prompt` with no arg lists what's available.",
2685
+ fetchFailed: "getPrompt failed"
2686
+ },
2569
2687
  mcpLifecycle: {
2570
2688
  handshake: "handshake\u2026",
2571
2689
  connected: "connected",
@@ -2577,7 +2695,9 @@ var EN = {
2577
2695
  disabledDetail: "via /mcp disable {name}",
2578
2696
  failedSetupHint: "\u2192 run `reasonix setup` to remove this entry, or fix the underlying issue (missing npm package, network, etc.).",
2579
2697
  failedSetupConfigHint: "\u2192 run `reasonix setup` to remove broken entries from your saved config.",
2580
- abortedHint: "MCP startup aborted \u2014 {count} server(s) skipped. Run /mcp to retry once you've fixed the underlying issue."
2698
+ abortedHint: "MCP startup aborted \u2014 {count} server(s) skipped. Run /mcp to retry once you've fixed the underlying issue.",
2699
+ toolsReady: "tools ready",
2700
+ warnLabel: "warn"
2581
2701
  },
2582
2702
  checkpointPicker: {
2583
2703
  title: "restore a checkpoint \u2014 {workspace}",
@@ -2623,6 +2743,41 @@ var EN = {
2623
2743
  noRecords: "no records",
2624
2744
  untracked: "(untracked)",
2625
2745
  churned: "(churned \xD7{count})"
2746
+ },
2747
+ builtinSkills: {
2748
+ 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'.",
2749
+ 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'.",
2750
+ 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.",
2751
+ 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.",
2752
+ 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."
2753
+ },
2754
+ shortcutsHelp: {
2755
+ title: "Shortcuts",
2756
+ groupInput: "Input",
2757
+ groupNavigation: "Navigation",
2758
+ groupSession: "Session",
2759
+ groupSystem: "System",
2760
+ descEnter: "Send message",
2761
+ descShiftEnter: "New line",
2762
+ descCtrlU: "Clear input",
2763
+ descCtrlW: "Delete word",
2764
+ descCtrlP: "Toggle shortcut panel",
2765
+ descCtrlX: "Open in editor",
2766
+ descArrows: "Input history",
2767
+ descPgUpDown: "Scroll page",
2768
+ descCtrlL: "Clear screen",
2769
+ descCtrlB: "Toggle sidebar",
2770
+ descNewSession: "New session",
2771
+ descListSessions: "List sessions",
2772
+ descSwitchModel: "Switch model",
2773
+ descSwitchPreset: "Switch preset",
2774
+ descSwitchTheme: "Switch theme",
2775
+ descCtrlC: "Quit",
2776
+ descEsc: "Stop / Cancel",
2777
+ descCtrlR: "Toggle verbose",
2778
+ descCtrlO: "Expand stream",
2779
+ descHelp: "Show all commands",
2780
+ descShiftTab: "Switch edit mode"
2626
2781
  }
2627
2782
  };
2628
2783
 
@@ -2796,6 +2951,8 @@ var zhCN = {
2796
2951
  tipShownOnce: "\u4EC5\u663E\u793A\u4E00\u6B21",
2797
2952
  modelOverride: "\u8986\u76D6\u9ED8\u8BA4\u6A21\u578B",
2798
2953
  noSession: "\u7981\u7528\u672C\u6B21\u8FD0\u884C\u7684\u4F1A\u8BDD\u6301\u4E45\u5316",
2954
+ noMouseHint: "\u5173\u95ED SGR \u9F20\u6807\u8DDF\u8E2A\uFF1B\u6062\u590D\u7EC8\u7AEF\u539F\u751F\u62D6\u9009\u548C\u53F3\u952E\u884C\u4E3A",
2955
+ noProxyHint: "\u672C\u6B21\u8FD0\u884C\u5FFD\u7565 HTTPS_PROXY / HTTP_PROXY\uFF0C\u76F4\u8FDE",
2799
2956
  resumeHint: "\u5F3A\u5236\u6062\u590D\u6307\u5B9A\u4F1A\u8BDD\uFF08\u5373\u4F7F\u7A7A\u95F2\uFF09",
2800
2957
  newHint: "\u5F3A\u5236\u521B\u5EFA\u65B0\u4F1A\u8BDD\uFF08\u5FFD\u7565 --session / --continue\uFF09",
2801
2958
  transcriptHint: "JSONL \u8F6C\u5F55\u7A3F\u7684\u5199\u5165\u8DEF\u5F84",
@@ -2940,6 +3097,7 @@ var zhCN = {
2940
3097
  },
2941
3098
  stop: { description: "\u4E2D\u6B62\u5F53\u524D\u6A21\u578B\u56DE\u5408\uFF08\u6309 Esc \u7684\u66FF\u4EE3\u65B9\u5F0F\uFF09" },
2942
3099
  feedback: { description: "\u6253\u5F00 GitHub Issue\uFF0C\u8BCA\u65AD\u4FE1\u606F\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F" },
3100
+ about: { description: "\u9879\u76EE\u4FE1\u606F \u2014 \u7248\u672C\u3001\u5B98\u7F51\u3001\u4ED3\u5E93\u3001\u534F\u8BAE" },
2943
3101
  plans: { description: "\u5217\u51FA\u6B64\u4F1A\u8BDD\u7684\u6D3B\u8DC3 + \u5F52\u6863\u8BA1\u5212\uFF08\u6700\u65B0\u5728\u524D\uFF09" },
2944
3102
  replay: {
2945
3103
  description: "\u52A0\u8F7D\u5F52\u6863\u8BA1\u5212\u4E3A\u53EA\u8BFB\u7684\u65F6\u95F4\u65C5\u884C\u5FEB\u7167\uFF08\u9ED8\u8BA4\uFF1A\u6700\u65B0\uFF09",
@@ -3016,8 +3174,8 @@ var zhCN = {
3016
3174
  argsHint: "<question>"
3017
3175
  },
3018
3176
  "search-engine": {
3019
- 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\u6216 metaso\uFF08\u6BCF\u65E5 100 \u6B21\u514D\u8D39\u989D\u5EA6\uFF09",
3020
- argsHint: "<mojeek|searxng|metaso> [<endpoint>]"
3177
+ description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 mojeek\uFF08\u9ED8\u8BA4\uFF0C\u65E0\u4F9D\u8D56\uFF09\u3001searxng\uFF08\u81EA\u6258\u7BA1\uFF09\u3001metaso\uFF08\u6BCF\u65E5 100 \u6B21\uFF09\u3001tavily\uFF08\u6BCF\u6708 1000 \u6B21\u514D\u8D39\uFF09\u3001perplexity\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09\u6216 exa\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09",
3178
+ argsHint: "<mojeek|searxng|metaso|tavily|perplexity|exa> [<key>]"
3021
3179
  }
3022
3180
  },
3023
3181
  wizard: {
@@ -3208,7 +3366,24 @@ var zhCN = {
3208
3366
  continuingAfter: "\u25B8 \u5728 {label}{counter} \u4E4B\u540E\u7EE7\u7EED",
3209
3367
  planStoppedAt: "\u25B8 \u8BA1\u5212\u5728 {label}{counter} \u5904\u505C\u6B62",
3210
3368
  revisingAfter: "\u25B8 \u5728 {label} \u4E4B\u540E\u4FEE\u8BA2 \u2014 {feedback}",
3211
- historyScrollHint: " \u2191 \u6B63\u5728\u67E5\u770B\u5386\u53F2 \xB7 End / PgDn \u8FD4\u56DE\u5E95\u90E8 \xB7 \u2193 \u5411\u4E0B\u6EDA\u52A8\u4E00\u884C"
3369
+ historyScrollHint: " \u2191 \u6B63\u5728\u67E5\u770B\u5386\u53F2 \xB7 End / PgDn \u8FD4\u56DE\u5E95\u90E8 \xB7 \u2193 \u5411\u4E0B\u6EDA\u52A8\u4E00\u884C",
3370
+ editHistoryTitle: "\u7F16\u8F91\u5386\u53F2\uFF08\u4ECE\u65E7\u5230\u65B0\uFF09\uFF1A",
3371
+ editHistoryNoCodeMode: "\u4E0D\u5728\u4EE3\u7801\u6A21\u5F0F\u4E2D",
3372
+ editHistoryNoEdits: "\u6B64\u4F1A\u8BDD\u5C1A\u672A\u8BB0\u5F55\u4EFB\u4F55\u7F16\u8F91",
3373
+ editHistoryNoShowId: "\u7528\u6CD5\uFF1A/show [id] [path] \uFF08\u7701\u7565 id \u67E5\u770B\u6700\u65B0\uFF1Bpath \u6765\u81EA\u6587\u4EF6\u6458\u8981\uFF09",
3374
+ editHistoryIdNotFound: "\u672A\u627E\u5230\u7F16\u8F91 #{id} \u2014 \u8FD0\u884C /history \u67E5\u770B\u6709\u6548 ID",
3375
+ editHistoryLookupFailed: "\u610F\u5916\u9519\u8BEF\uFF1A\u5386\u53F2\u67E5\u627E\u5931\u8D25",
3376
+ editHistoryBatchNoFile: '\u6279\u6B21 #{id} \u4E0D\u5305\u542B "{path}" \u2014 \u6B64\u6279\u6B21\u4E2D\u7684\u6587\u4EF6\uFF1A{files}',
3377
+ editHistoryNoEdits2: "\u6B64\u4F1A\u8BDD\u5C1A\u672A\u8BB0\u5F55\u7F16\u8F91 \u2014 /history \u4E3A\u7A7A",
3378
+ editHistoryStatusApplied: "\u5DF2\u5E94\u7528",
3379
+ editHistoryStatusPartial: "\u90E8\u5206\u5E94\u7528",
3380
+ editHistoryStatusUndone: "\u5DF2\u64A4\u9500",
3381
+ editHistoryHelpShow: "/show <id> \u2192 \u6587\u4EF6\u6458\u8981 \xB7 /show <id> <path> \u2192 \u67D0\u4E2A\u6587\u4EF6\u7684\u5B8C\u6574 diff",
3382
+ editHistoryHelpUndo: "/undo \u2192 \u6700\u65B0\u7684\u672A\u64A4\u9500\u9879 \xB7 /undo <id> [path] \u2192 \u6307\u5B9A\u6279\u6B21\u6216\u6587\u4EF6",
3383
+ editHistoryAlreadyReverted: "\uFF08\u5DF2\u64A4\u9500 \u2014 /history \u663E\u793A\u6279\u6B21\u7EA7\u72B6\u6001\uFF09",
3384
+ editHistoryRevertFile: "/undo {id} {path} \u2192 \u4EC5\u8FD8\u539F\u6B64\u6587\u4EF6",
3385
+ mcpFailed: "MCP {name} \u5931\u8D25",
3386
+ mcpWarn: "MCP {name} \u8B66\u544A"
3212
3387
  },
3213
3388
  hooks: {
3214
3389
  head: "\u94A9\u5B50 {tag} `{cmd}` {decision}{truncTag}",
@@ -3231,9 +3406,9 @@ var zhCN = {
3231
3406
  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",
3232
3407
  toolUploadStatus: "\u5DE5\u5177\u7ED3\u679C\u5DF2\u4E0A\u4F20 \xB7 \u6A21\u578B\u5728\u751F\u6210\u4E0B\u4E00\u6761\u54CD\u5E94\u524D\u601D\u8003\u4E2D\u2026",
3233
3408
  preflightTruncateStatus: "\u9884\u68C0\uFF1A\u4E0A\u4E0B\u6587\u63A5\u8FD1\u4E0A\u9650\uFF0C\u6B63\u5728\u88C1\u526A\u6700\u65E9\u5386\u53F2\u2026",
3234
- 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",
3235
- 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",
3236
- 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",
3409
+ 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",
3410
+ 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",
3411
+ 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",
3237
3412
  flashEscalation: "\u21E7 flash \u8BF7\u6C42\u5347\u7EA7 \u2014 \u672C\u8F6E\u6539\u7528 {model}{reasonSuffix}",
3238
3413
  harvestStatus: "\u6B63\u5728\u4ECE\u63A8\u7406\u8FC7\u7A0B\u63D0\u53D6\u8BA1\u5212\u72B6\u6001\u2026",
3239
3414
  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",
@@ -3309,6 +3484,10 @@ var zhCN = {
3309
3484
  loopNoActiveHint: "\u6CA1\u6709\u6D3B\u52A8\u7684\u5FAA\u73AF\u3002\u4F7F\u7528 `/loop <interval> <prompt>` \u542F\u52A8\u4E00\u4E2A\uFF08\u4F8B\u5982 /loop 30s npm test\uFF09\u3002\n\u53D6\u6D88\u65B9\u5F0F\uFF1A/loop stop \xB7 Esc \xB7 /clear /new \xB7 \u4EFB\u4F55\u7528\u6237\u8F93\u5165\u7684\u63D0\u793A\u3002",
3310
3485
  loopStarted: '\u25B8 \u5FAA\u73AF\u5DF2\u542F\u52A8 \u2014 \u6BCF {duration} \u91CD\u65B0\u63D0\u4EA4 "{prompt}"\u3002\u8F93\u5165\u4EFB\u4F55\u5185\u5BB9\uFF08\u6216 /loop stop\uFF09\u53D6\u6D88\u3002',
3311
3486
  keysNeedsTui: "/keys \u9700\u8981 TUI \u4E0A\u4E0B\u6587\uFF08postKeys \u5DF2\u8FDE\u63A5\uFF09\u3002",
3487
+ aboutHeader: "Reasonix v{version} \u2014 \u7F13\u5B58\u4F18\u5148\u7684 DeepSeek \u7F16\u7801\u4EE3\u7406",
3488
+ aboutWebsiteLabel: "\u5B98\u7F51",
3489
+ aboutRepoLabel: "\u4ED3\u5E93",
3490
+ aboutLicenseLabel: "\u534F\u8BAE",
3312
3491
  unknownCommand: "\u672A\u77E5\u547D\u4EE4\uFF1A/{cmd} \u2014 \u4F60\u662F\u4E0D\u662F\u60F3\u7528 {list}\uFF1F",
3313
3492
  unknownCommandShort: "\u672A\u77E5\u547D\u4EE4\uFF1A/{cmd} \uFF08\u8BD5\u8BD5 /help\uFF09"
3314
3493
  },
@@ -3516,6 +3695,12 @@ var zhCN = {
3516
3695
  statusMcp: " MCP {servers} \u4E2A\u670D\u52A1\u5668\uFF0C\u6CE8\u518C\u8868\u4E2D {tools} \u4E2A\u5DE5\u5177",
3517
3696
  statusEdits: " \u7F16\u8F91 {count} \u4E2A\u5F85\u5904\u7406\uFF08/apply \u63D0\u4EA4\uFF0C/discard \u4E22\u5F03\uFF09",
3518
3697
  statusPlan: " \u8BA1\u5212 \u5F00\u542F \u2014 \u5199\u5165\u53D7\u9650\uFF08submit_plan + \u5BA1\u6279\uFF09",
3698
+ statusLifecycle: " \u751F\u547D\u5468\u671F {mode}/{state} \xB7 {progress}{evidence}",
3699
+ lifecycleNoPlan: "\u6682\u65E0\u8BA1\u5212",
3700
+ lifecycleEvidencePending: "\u7B49\u5F85 evidence",
3701
+ lifecycleRejected: "lifecycle\uFF1A{tool} \u5728 {state} \u72B6\u6001\u88AB\u62E6\u622A \u2014 \u4E0B\u4E00\u6B65\uFF1A{next}",
3702
+ lifecycleEvidenceRejected: "lifecycle\uFF1A\u6B65\u9AA4 {stepId} \u9700\u8981 evidence \u2014 \u4E0B\u4E00\u6B65\uFF1A{next}",
3703
+ lifecycleRepeatedRejected: "lifecycle\uFF1A{tool} \u88AB\u91CD\u590D\u62E6\u622A \u2014 \u4E0D\u8981\u7528\u76F8\u540C\u53C2\u6570\u53CD\u590D\u91CD\u8BD5",
3519
3704
  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",
3520
3705
  statusModeAuto: " \u6A21\u5F0F AUTO \u2014 \u7F16\u8F91\u7ACB\u5373\u5E94\u7528\uFF085 \u79D2\u5185\u6309 u \u64A4\u6D88 \xB7 Shift+Tab \u5207\u6362\uFF09",
3521
3706
  statusModeReview: " \u6A21\u5F0F review \u2014 \u7F16\u8F91\u6392\u961F\u7B49\u5F85 /apply \u6216 y\uFF08Shift+Tab \u5207\u6362\uFF09",
@@ -3626,6 +3811,8 @@ var zhCN = {
3626
3811
  usageSearxngUrl: " /search-engine searxng <url> \u4F7F\u7528 SearXNG \u81EA\u5B9A\u4E49\u7AEF\u70B9",
3627
3812
  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",
3628
3813
  usageTavily: " /search-engine tavily \u4F7F\u7528 Tavily API\uFF08LLM \u53CB\u597D\uFF0C\u6BCF\u6708 1000 \u6B21\u514D\u8D39 \u2014 \u8BBE\u7F6E TAVILY_API_KEY \u6216 config \u7684 tavilyApiKey\uFF1B\u6CE8\u518C https://tavily.com\uFF09",
3814
+ usagePerplexity: " /search-engine perplexity \u4F7F\u7528 Perplexity AI\uFF08AI \u76F4\u63A5\u56DE\u7B54 + \u5F15\u7528 \u2014 \u8BBE\u7F6E PERPLEXITY_API_KEY \u6216 config \u7684 perplexityApiKey\uFF1B\u5728 https://perplexity.ai/settings/api \u83B7\u53D6\u5BC6\u94A5\uFF09",
3815
+ usageExa: " /search-engine exa \u4F7F\u7528 Exa API\uFF08AI \u76F4\u63A5\u56DE\u7B54 + \u5F15\u7528\uFF0C\u6BCF\u6708 1000 \u6B21\u514D\u8D39 \u2014 \u8BBE\u7F6E EXA_API_KEY \u6216 config \u7684 exaApiKey\uFF1B\u6CE8\u518C https://exa.ai\uFF09",
3629
3816
  alias: "\u522B\u540D\uFF1A/se",
3630
3817
  searxngInfo: "SearXNG \u662F\u4E00\u4E2A\u81EA\u6258\u7BA1\u7684\u5143\u641C\u7D22\u5F15\u64CE\uFF08https://github.com/searxng/searxng\uFF09\u3002",
3631
3818
  searxngInstall: "\u5B89\u88C5\u547D\u4EE4\uFF1A docker run -d -p 8080:8080 searxng/searxng",
@@ -3633,7 +3820,11 @@ var zhCN = {
3633
3820
  switchedSearxngNote: " \u8BF7\u786E\u4FDD SearXNG \u5728 {endpoint} \u8FD0\u884C\u3002",
3634
3821
  switchedMetasoNote: " \u6BCF\u65E5\u9650\u989D 100 \u6B21\uFF08\u914D\u7F6E\u4F60\u81EA\u5DF1\u7684 API \u5BC6\u94A5\u53EF\u63D0\u5347\u9650\u989D\uFF09\u3002",
3635
3822
  switchedTavilyNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF TAVILY_API_KEY \u6216 config \u4E2D\u7684 `tavilyApiKey`\uFF1Bhttps://tavily.com \u6BCF\u6708 1000 \u6B21\u514D\u8D39\u3002",
3636
- confirmed: '\u2713 \u7F51\u9875\u641C\u7D22\u5F15\u64CE\u5DF2\u8BBE\u4E3A "{engine}"{detail}\u3002\u4E0B\u4E00\u8F6E\u6A21\u578B\u8C03\u7528\u5C06\u751F\u6548\u3002',
3823
+ switchedPerplexityNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF PERPLEXITY_API_KEY \u6216 config \u4E2D\u7684 `perplexityApiKey`\uFF1B\u5728 https://perplexity.ai/settings/api \u83B7\u53D6\u5BC6\u94A5\u3002",
3824
+ switchedExaNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EXA_API_KEY \u6216 config \u4E2D\u7684 `exaApiKey`\uFF1B\u6CE8\u518C https://exa.ai\u3002",
3825
+ keyNeeded: '\u672A\u914D\u7F6E "{engine}" \u7684 API \u5BC6\u94A5\u3002\n\n 1. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF {envVar}\n 2. \u6216\u5185\u8054\u63D0\u4F9B\uFF1A/search-engine {engine} <your-key>\n 3. \u6216\u5728 ~/.reasonix/config.json \u4E2D\u6DFB\u52A0 "{engine}ApiKey"\n\n\u5B8C\u6210\u540E\u91CD\u65B0\u6267\u884C /search-engine {engine}\u3002',
3826
+ keySaved: " API \u5BC6\u94A5\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u3002",
3827
+ confirmed: '\u7F51\u9875\u641C\u7D22\u5F15\u64CE\u5DF2\u8BBE\u4E3A "{engine}"{detail}\u3002\u4E0B\u4E00\u8F6E\u6A21\u578B\u8C03\u7528\u5C06\u751F\u6548\u3002',
3637
3828
  confirmedDetail: "\uFF08{endpoint}\uFF09"
3638
3829
  },
3639
3830
  skill: {
@@ -3680,7 +3871,8 @@ var zhCN = {
3680
3871
  evt: " \u4E8B\u4EF6",
3681
3872
  editsLabel: "\u7F16\u8F91:",
3682
3873
  mcpLoading: "MCP",
3683
- ctx: "\u4E0A\u4E0B\u6587"
3874
+ ctx: "\u4E0A\u4E0B\u6587",
3875
+ shortcutsHint: "Ctrl+P \u5FEB\u6377\u952E"
3684
3876
  },
3685
3877
  editMode: {
3686
3878
  plan: "\u8BA1\u5212",
@@ -3892,27 +4084,38 @@ var zhCN = {
3892
4084
  probeFailed: "\u63A2\u6D4B\u5931\u8D25 \u2014 {message}"
3893
4085
  },
3894
4086
  webErrors: {
3895
- 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 \u5207\u6362\u5F15\u64CE",
4087
+ status: "web_search {status} \u2014 try: \u641C\u7D22\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF\uFF1B\u8BF7\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
3896
4088
  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",
3897
- 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 \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5",
4089
+ forbidden403: "web_search 403 \u2014 try: \u641C\u7D22\u540E\u7AEF\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5",
3898
4090
  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",
3899
- 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 searxng \u5207\u6362\u5F15\u64CE",
3900
- 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 searxng \u5207\u6362\u5F15\u64CE",
4091
+ mojeekBlocked: "web_search: Mojeek \u53CD\u722C\u9875\u9762 \u2014 \u9891\u7387\u9650\u5236\u6216\u88AB\u5C4F\u853D \u2014 try: \u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4092
+ mojeekNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46\u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF0C\u524D 120 \u5B57\u7B26\uFF1A{preview}\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
3901
4093
  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',
3902
4094
  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",
3903
- 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 \u5207\u6362\u5230\u9ED8\u8BA4\u5F15\u64CE",
3904
- 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 \u5207\u6362\u5F15\u64CE",
3905
- 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",
4095
+ cannotReach: "web_search: \u65E0\u6CD5\u8BBF\u95EE SearXNG \u670D\u52A1\u5668 {endpoint} \u2014 try: \u5B89\u88C5\u5E76\u542F\u52A8 SearXNG\uFF08https://github.com/searxng/searxng\uFF0C\u4F8B\u5982 `docker run -d -p 8080:8080 searxng/searxng`\uFF09\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4096
+ searxngNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46 SearXNG \u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4097
+ metasoMissingKey: "web_search: Metaso \u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E METASO_API_KEY\uFF0C\u6216\u4F7F\u7528 /search-engine metaso <key> \u914D\u7F6E\uFF1B\u53EF\u5728 https://metaso.cn/search-api/playground \u83B7\u53D6\u5BC6\u94A5",
4098
+ 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",
3906
4099
  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",
3907
4100
  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",
3908
- metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE",
4101
+ metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
3909
4102
  metasoParseError: "web_search: Metaso \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3910
4103
  metasoApiError: "web_search: Metaso API \u9519\u8BEF\uFF08code {code}: {message}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3911
4104
  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",
3912
4105
  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",
3913
- 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 \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
3914
- tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek \u5207\u6362\u5F15\u64CE",
4106
+ tavilyRateLimit: "web_search: Tavily \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u3001\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
4107
+ tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
3915
4108
  tavilyParseError: "web_search: Tavily \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4109
+ perplexityMissingKey: "web_search: Perplexity \u540E\u7AEF\u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E PERPLEXITY_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 ~/.reasonix/config.json \u4E2D\u914D\u7F6E `perplexityApiKey`\uFF1B\u5728 https://perplexity.ai/settings/api \u83B7\u53D6\u5BC6\u94A5",
4110
+ perplexityUnauthorized: "web_search: Perplexity API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 PERPLEXITY_API_KEY\uFF0C\u6216\u5728 https://perplexity.ai/settings/api \u83B7\u53D6\u5BC6\u94A5",
4111
+ perplexityRateLimit: "web_search: Perplexity \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4112
+ perplexityServerError: "web_search: Perplexity \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4113
+ perplexityParseError: "web_search: Perplexity \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
4114
+ exaMissingKey: "web_search: Exa \u540E\u7AEF\u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E EXA_API_KEY \u73AF\u5883\u53D8\u91CF\uFF0C\u6216\u5728 ~/.reasonix/config.json \u4E2D\u914D\u7F6E `exaApiKey`\uFF1Bhttps://exa.ai \u6BCF\u6708 1000 \u6B21\u514D\u8D39",
4115
+ exaUnauthorized: "web_search: Exa API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 EXA_API_KEY\uFF0C\u6216\u5728 https://exa.ai \u83B7\u53D6\u5BC6\u94A5",
4116
+ exaRateLimit: "web_search: Exa \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u5347\u7EA7\uFF0C\u6216\u5728 https://exa.ai/pricing \u67E5\u770B\u8BA1\u5212",
4117
+ exaServerError: "web_search: Exa \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine mojeek|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
4118
+ exaParseError: "web_search: Exa \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
3916
4119
  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",
3917
4120
  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",
3918
4121
  fetchForbidden403: "web_fetch 403 for {url} \u2014 try: \u76EE\u6807\u4E3B\u673A\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u8BE5\u9875\u9762\u53EF\u80FD\u9700\u8981\u767B\u5F55\u6216\u5C4F\u853D\u722C\u866B \u2014 \u6539\u7528 web_search \u6458\u8981",
@@ -4080,6 +4283,18 @@ var zhCN = {
4080
4283
  serverCount: "{count} \u4E2A\u670D\u52A1\u5668",
4081
4284
  footer: "\u2191\u2193 \u9009\u62E9 \xB7 [r] \u91CD\u8FDE \xB7 [d] \u7981\u7528 \xB7 Esc \u9000\u51FA"
4082
4285
  },
4286
+ mcpBrowse: {
4287
+ 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",
4288
+ readOne: "\u8BFB\u53D6\uFF1A`/resource <uri>` \u2014 \u6216\u5728\u9009\u62E9\u5668\u4E2D\u4F7F\u7528 Tab \u952E\u3002",
4289
+ 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",
4290
+ 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",
4291
+ noServerForResource: '\u6CA1\u6709\u670D\u52A1\u5668\u66B4\u9732\u8D44\u6E90 "{name}"',
4292
+ resourceHint: "`/resource` \u4E0D\u5E26\u53C2\u6570\u53EF\u67E5\u770B\u53EF\u7528\u5217\u8868\u3002",
4293
+ readFailed: "\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25",
4294
+ noServerForPrompt: '\u6CA1\u6709\u670D\u52A1\u5668\u66B4\u9732 prompt "{name}"',
4295
+ promptHint: "`/prompt` \u4E0D\u5E26\u53C2\u6570\u53EF\u67E5\u770B\u53EF\u7528\u5217\u8868\u3002",
4296
+ fetchFailed: "\u83B7\u53D6 prompt \u5931\u8D25"
4297
+ },
4083
4298
  mcpLifecycle: {
4084
4299
  handshake: "\u63E1\u624B\u4E2D\u2026",
4085
4300
  connected: "\u5DF2\u8FDE\u63A5",
@@ -4091,7 +4306,9 @@ var zhCN = {
4091
4306
  disabledDetail: "\u901A\u8FC7 /mcp disable {name}",
4092
4307
  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",
4093
4308
  failedSetupConfigHint: "\u2192 \u8FD0\u884C `reasonix setup` \u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u4E2D\u79FB\u9664\u635F\u574F\u7684\u6761\u76EE\u3002",
4094
- 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"
4309
+ 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",
4310
+ toolsReady: "\u5DE5\u5177\u5C31\u7EEA",
4311
+ warnLabel: "\u8B66\u544A"
4095
4312
  },
4096
4313
  checkpointPicker: {
4097
4314
  title: "\u6062\u590D\u68C0\u67E5\u70B9 \u2014 {workspace}",
@@ -4137,6 +4354,41 @@ var zhCN = {
4137
4354
  noRecords: "\u65E0\u8BB0\u5F55",
4138
4355
  untracked: "\uFF08\u672A\u8FFD\u8E2A\uFF09",
4139
4356
  churned: "\uFF08\u5DF2\u53D8\u66F4 \xD7{count}\uFF09"
4357
+ },
4358
+ builtinSkills: {
4359
+ 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",
4360
+ 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",
4361
+ 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",
4362
+ 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",
4363
+ 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"
4364
+ },
4365
+ shortcutsHelp: {
4366
+ title: "\u5FEB\u6377\u952E",
4367
+ groupInput: "\u8F93\u5165",
4368
+ groupNavigation: "\u5BFC\u822A",
4369
+ groupSession: "\u4F1A\u8BDD",
4370
+ groupSystem: "\u7CFB\u7EDF",
4371
+ descEnter: "\u53D1\u9001\u6D88\u606F",
4372
+ descShiftEnter: "\u6362\u884C",
4373
+ descCtrlU: "\u6E05\u7A7A\u8F93\u5165",
4374
+ descCtrlW: "\u5220\u9664\u5355\u8BCD",
4375
+ descCtrlP: "\u6253\u5F00/\u5173\u95ED\u5FEB\u6377\u952E\u9762\u677F",
4376
+ descCtrlX: "\u5728\u7F16\u8F91\u5668\u4E2D\u6253\u5F00",
4377
+ descArrows: "\u6D4F\u89C8\u8F93\u5165\u5386\u53F2",
4378
+ descPgUpDown: "\u7FFB\u9875",
4379
+ descCtrlL: "\u6E05\u5C4F",
4380
+ descCtrlB: "\u5207\u6362\u4FA7\u8FB9\u680F",
4381
+ descNewSession: "\u65B0\u5EFA\u4F1A\u8BDD",
4382
+ descListSessions: "\u5217\u51FA\u4F1A\u8BDD",
4383
+ descSwitchModel: "\u5207\u6362\u6A21\u578B",
4384
+ descSwitchPreset: "\u5207\u6362\u9884\u8BBE",
4385
+ descSwitchTheme: "\u5207\u6362\u4E3B\u9898",
4386
+ descCtrlC: "\u9000\u51FA",
4387
+ descEsc: "\u505C\u6B62/\u53D6\u6D88",
4388
+ descCtrlR: "\u5207\u6362\u8BE6\u7EC6\u6A21\u5F0F",
4389
+ descCtrlO: "\u5C55\u5F00\u6D41\u5F0F\u8F93\u51FA",
4390
+ descHelp: "\u663E\u793A\u6240\u6709\u547D\u4EE4",
4391
+ descShiftTab: "\u5207\u6362\u7F16\u8F91\u6A21\u5F0F"
4140
4392
  }
4141
4393
  };
4142
4394
 
@@ -4212,7 +4464,7 @@ function readSettingsFile(path2) {
4212
4464
  }
4213
4465
  function loadHooks(opts = {}) {
4214
4466
  const out = [];
4215
- if (opts.projectRoot) {
4467
+ if (opts.projectRoot && (opts.trustProjectHooks === true || projectHooksTrusted(opts.projectRoot, opts.configPath))) {
4216
4468
  const projPath = projectSettingsPath(opts.projectRoot);
4217
4469
  const settings2 = readSettingsFile(projPath);
4218
4470
  if (settings2) appendResolved(out, settings2, "project", projPath);
@@ -4826,8 +5078,8 @@ var ToolRegistry = class {
4826
5078
  _resultAugmenter = null;
4827
5079
  /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
4828
5080
  _lastMalformed = /* @__PURE__ */ new Map();
4829
- /** Per-tool fingerprint of the last host-side interceptor rejection. */
4830
- _lastInterceptorRejection = /* @__PURE__ */ new Map();
5081
+ /** Per-tool fingerprint of the last host-side gate rejection. */
5082
+ _lastGateRejection = /* @__PURE__ */ new Map();
4831
5083
  constructor(opts = {}) {
4832
5084
  this._autoFlatten = opts.autoFlatten !== false;
4833
5085
  }
@@ -4914,20 +5166,21 @@ var ToolRegistry = class {
4914
5166
  if (!tool) {
4915
5167
  return JSON.stringify({ error: `unknown tool: ${name}` });
4916
5168
  }
4917
- const fingerprint = fingerprintArgs(argumentsRaw);
5169
+ const rawFingerprint = rawFingerprintArgs(argumentsRaw);
4918
5170
  let args;
4919
5171
  try {
4920
5172
  args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
4921
5173
  } catch (err) {
4922
5174
  return this._noteMalformed(
4923
5175
  name,
4924
- fingerprint,
5176
+ rawFingerprint,
4925
5177
  `invalid tool arguments JSON: ${err.message}`
4926
5178
  );
4927
5179
  }
4928
5180
  if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
4929
5181
  args = nestArguments(args);
4930
5182
  }
5183
+ const fingerprint = fingerprintArgs(args);
4931
5184
  const missing = tool.parameters ? missingRequiredParam(tool.parameters, args) : null;
4932
5185
  if (missing) {
4933
5186
  return this._noteMalformed(
@@ -4948,7 +5201,7 @@ var ToolRegistry = class {
4948
5201
  try {
4949
5202
  const short = await interceptor(name, args);
4950
5203
  if (typeof short === "string") {
4951
- const guarded = this._noteInterceptorRejection(name, fingerprint, short);
5204
+ const guarded = this._noteGateRejection(name, fingerprint, short);
4952
5205
  return this._augmentResult(name, args, guarded);
4953
5206
  }
4954
5207
  } catch (err) {
@@ -4957,7 +5210,6 @@ var ToolRegistry = class {
4957
5210
  });
4958
5211
  }
4959
5212
  }
4960
- this._lastInterceptorRejection.delete(name);
4961
5213
  if (opts.signal?.aborted) {
4962
5214
  return JSON.stringify({
4963
5215
  error: `${name}: aborted before dispatch (user interrupt)`,
@@ -4995,6 +5247,7 @@ var ToolRegistry = class {
4995
5247
  finalResult = JSON.stringify({ error: `${e.name}: ${e.message}` });
4996
5248
  }
4997
5249
  }
5250
+ finalResult = this._noteGateRejection(name, fingerprint, finalResult);
4998
5251
  return this._augmentResult(name, args, finalResult);
4999
5252
  }
5000
5253
  _augmentResult(name, args, result) {
@@ -5018,18 +5271,18 @@ var ToolRegistry = class {
5018
5271
  }
5019
5272
  return JSON.stringify({ error: `${name}: ${detail}` });
5020
5273
  }
5021
- _noteInterceptorRejection(name, fingerprint, result) {
5022
- const reason = rejectedReason(result);
5274
+ _noteGateRejection(name, fingerprint, result) {
5275
+ const reason = rejectedReason(name, result);
5023
5276
  if (!reason) {
5024
- this._lastInterceptorRejection.delete(name);
5277
+ this._lastGateRejection.delete(name);
5025
5278
  return result;
5026
5279
  }
5027
5280
  const key = `${reason}:${fingerprint}`;
5028
- const prev = this._lastInterceptorRejection.get(name);
5029
- this._lastInterceptorRejection.set(name, key);
5281
+ const prev = this._lastGateRejection.get(name);
5282
+ this._lastGateRejection.set(name, key);
5030
5283
  if (prev === key) {
5031
5284
  return JSON.stringify({
5032
- error: `${name}: same call was just rejected by ${reason} \u2014 do not retry identical args. Switch to read-only exploration, submit or revise the plan, or choose a different tool call.`,
5285
+ error: `${name}: same call was just rejected by ${reason} \u2014 do not retry identical args. ${rejectionRecoveryHint(reason)}`,
5033
5286
  rejectedReason: reason,
5034
5287
  consecutiveInterceptorRejection: true
5035
5288
  });
@@ -5037,16 +5290,44 @@ var ToolRegistry = class {
5037
5290
  return result;
5038
5291
  }
5039
5292
  };
5040
- function rejectedReason(result) {
5293
+ function rejectedReason(name, result) {
5294
+ const textReason = plainTextRejectedReason(name, result);
5295
+ if (textReason) return textReason;
5041
5296
  try {
5042
5297
  const parsed = JSON.parse(result);
5043
5298
  if (!parsed || typeof parsed !== "object") return null;
5044
5299
  const reason = parsed.rejectedReason;
5045
- return typeof reason === "string" && reason ? reason : null;
5300
+ if (typeof reason === "string" && reason) return reason;
5301
+ const error = parsed.error;
5302
+ if (typeof error === "string") return plainTextRejectedReason(name, error);
5303
+ return null;
5046
5304
  } catch {
5047
5305
  return null;
5048
5306
  }
5049
5307
  }
5308
+ function plainTextRejectedReason(name, result) {
5309
+ if ((name === "edit_file" || name === "write_file") && /rejected this edit/i.test(result)) {
5310
+ return "edit-gate";
5311
+ }
5312
+ if ((name === "run_command" || name === "run_background") && /\buser denied:/i.test(result)) {
5313
+ return "shell-gate";
5314
+ }
5315
+ return null;
5316
+ }
5317
+ function rejectionRecoveryHint(reason) {
5318
+ switch (reason) {
5319
+ case "edit-gate":
5320
+ return "Do not re-emit the same edit. Try a genuinely different edit or ask the user how to proceed.";
5321
+ case "shell-gate":
5322
+ return "Do not retry the same command. Use an allowlisted/read-only command, wait for approval, or ask the user how to proceed.";
5323
+ case "engineering-lifecycle":
5324
+ return "Switch to read-only exploration, submit or revise the plan, or choose a different tool call.";
5325
+ case "engineering-lifecycle-evidence":
5326
+ return "Submit completion evidence or revise/checkpoint the plan before marking the step complete.";
5327
+ default:
5328
+ return "Choose a different tool call or ask the user how to proceed.";
5329
+ }
5330
+ }
5050
5331
  function isReadOnlyCall(tool, args) {
5051
5332
  if (tool.readOnlyCheck) {
5052
5333
  try {
@@ -5065,14 +5346,27 @@ function hasDotKey(obj) {
5065
5346
  }
5066
5347
  return false;
5067
5348
  }
5068
- function fingerprintArgs(argumentsRaw) {
5349
+ function rawFingerprintArgs(argumentsRaw) {
5069
5350
  if (typeof argumentsRaw === "string") return argumentsRaw;
5351
+ return fingerprintArgs(argumentsRaw);
5352
+ }
5353
+ function fingerprintArgs(args) {
5070
5354
  try {
5071
- return JSON.stringify(argumentsRaw);
5355
+ return JSON.stringify(sortJson(args));
5072
5356
  } catch {
5073
5357
  return "";
5074
5358
  }
5075
5359
  }
5360
+ function sortJson(value) {
5361
+ if (Array.isArray(value)) return value.map(sortJson);
5362
+ if (!value || typeof value !== "object") return value;
5363
+ const out = {};
5364
+ for (const key of Object.keys(value).sort()) {
5365
+ const item = value[key];
5366
+ if (item !== void 0) out[key] = sortJson(item);
5367
+ }
5368
+ return out;
5369
+ }
5076
5370
  function missingRequiredParam(schema, args) {
5077
5371
  const required = schema.required;
5078
5372
  if (!required || required.length === 0) return null;
@@ -5732,14 +6026,16 @@ function round(n, digits) {
5732
6026
  }
5733
6027
 
5734
6028
  // src/context-manager.ts
5735
- var HISTORY_FOLD_THRESHOLD = 0.5;
6029
+ var HISTORY_FOLD_THRESHOLD = 0.75;
5736
6030
  var HISTORY_FOLD_TAIL_FRACTION = 0.2;
5737
- var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.7;
6031
+ var HISTORY_FOLD_AGGRESSIVE_THRESHOLD = 0.78;
5738
6032
  var HISTORY_FOLD_AGGRESSIVE_TAIL_FRACTION = 0.1;
5739
6033
  var HISTORY_FOLD_MIN_SAVINGS_FRACTION = 0.3;
5740
6034
  var FORCE_SUMMARY_THRESHOLD = 0.8;
5741
6035
  var PREFLIGHT_EMERGENCY_THRESHOLD = 0.95;
5742
6036
  var PREFLIGHT_MECHANICAL_TARGET_FRACTION = 0.7;
6037
+ var MAX_BODY_BYTES = 7e5;
6038
+ var MAX_BODY_BYTES_TARGET = 5e5;
5743
6039
  var HISTORY_FOLD_SUMMARY_TIMEOUT_MS = 15e3;
5744
6040
  var HISTORY_FOLD_MARKER = "[CONVERSATION HISTORY SUMMARY \u2014 earlier turns folded for context efficiency]\n\n";
5745
6041
  var SKILL_PIN_MEMO_HEADER = "[Active skill memos \u2014 preserved verbatim across the fold:]";
@@ -5764,6 +6060,20 @@ var ContextManager = class {
5764
6060
  this.deps = deps;
5765
6061
  }
5766
6062
  deps;
6063
+ /** Real-time token count of the current log — used by Desktop to refresh the
6064
+ * context meter after /compact when no API usage event is available. */
6065
+ getLogTokens() {
6066
+ const entries = this.deps.log.toMessages();
6067
+ let total = 0;
6068
+ for (const e of entries) {
6069
+ const content = typeof e.content === "string" ? e.content : "";
6070
+ total += countTokensBounded(content);
6071
+ if (e.role === "assistant" && Array.isArray(e.tool_calls) && e.tool_calls.length > 0) {
6072
+ total += countTokensBounded(JSON.stringify(e.tool_calls));
6073
+ }
6074
+ }
6075
+ return total;
6076
+ }
5767
6077
  /** Decision after a turn's response — fold, exit with summary, or carry on. */
5768
6078
  decideAfterUsage(usage, model, alreadyFoldedThisTurn) {
5769
6079
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
@@ -5792,14 +6102,25 @@ var ContextManager = class {
5792
6102
  }
5793
6103
  return { kind: "none", ...base };
5794
6104
  }
5795
- /** Local-side preflight before sending a request — catches oversized payloads early. */
6105
+ /** Local-side preflight before sending a request — catches oversized payloads early.
6106
+ * Two independent signals trip mechanical truncate: token estimate above the context-window
6107
+ * fraction, OR JSON body bytes above the gateway limit (see `MAX_BODY_BYTES`). */
5796
6108
  decidePreflight(messages, toolSpecs, model) {
5797
6109
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
5798
6110
  const estimate = estimateRequestTokens(messages, toolSpecs ?? null, true);
6111
+ const estimateBytes = Buffer.byteLength(JSON.stringify(messages), "utf8");
6112
+ const tokensOver = estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD;
6113
+ const bytesOver = estimateBytes > MAX_BODY_BYTES;
6114
+ let trigger = "none";
6115
+ if (tokensOver && bytesOver) trigger = "both";
6116
+ else if (tokensOver) trigger = "tokens";
6117
+ else if (bytesOver) trigger = "bytes";
5799
6118
  return {
5800
- needsAction: estimate / ctxMax > PREFLIGHT_EMERGENCY_THRESHOLD,
6119
+ needsAction: tokensOver || bytesOver,
5801
6120
  estimateTokens: estimate,
5802
- ctxMax
6121
+ estimateBytes,
6122
+ ctxMax,
6123
+ trigger
5803
6124
  };
5804
6125
  }
5805
6126
  /** Replace older turns with one summary message; keep tail within keepRecentTokens budget. */
@@ -5852,10 +6173,13 @@ ${pinnedBodies.join("\n\n")}` : "";
5852
6173
  summaryChars: summary.content.length
5853
6174
  };
5854
6175
  }
5855
- /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail. */
6176
+ /** Pure local emergency compaction for preflight: drop oldest log entries and keep a valid tail.
6177
+ * Bounded by tokens AND bytes — bytes matter because DeepSeek's gateway 400s on bodies past
6178
+ * `MAX_BODY_BYTES` even when the token budget is far from exhausted. */
5856
6179
  mechanicalTruncate(model, opts) {
5857
6180
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model] ?? DEFAULT_CONTEXT_TOKENS;
5858
6181
  const targetTokens = opts?.targetTokens ?? Math.floor(ctxMax * PREFLIGHT_MECHANICAL_TARGET_FRACTION);
6182
+ const targetBytes = opts?.targetBytes ?? MAX_BODY_BYTES_TARGET;
5859
6183
  const all = this.deps.log.toMessages();
5860
6184
  const noop = {
5861
6185
  folded: false,
@@ -5865,6 +6189,7 @@ ${pinnedBodies.join("\n\n")}` : "";
5865
6189
  };
5866
6190
  if (all.length === 0) return noop;
5867
6191
  const tokenCounts = all.map((m) => estimateConversationTokens([m], true));
6192
+ const byteCounts = all.map((m) => Buffer.byteLength(JSON.stringify(m), "utf8"));
5868
6193
  let latestUserBoundary = -1;
5869
6194
  for (let i = all.length - 1; i >= 0; i--) {
5870
6195
  if (all[i].role === "user") {
@@ -5873,12 +6198,15 @@ ${pinnedBodies.join("\n\n")}` : "";
5873
6198
  }
5874
6199
  }
5875
6200
  let cumTokens = 0;
6201
+ let cumBytes = 0;
5876
6202
  let boundary = all.length;
5877
6203
  let foundSafeBoundary = false;
5878
6204
  for (let i = all.length - 1; i >= 0; i--) {
5879
- const next = cumTokens + tokenCounts[i];
5880
- if (next > targetTokens) break;
5881
- cumTokens = next;
6205
+ const nextTokens = cumTokens + tokenCounts[i];
6206
+ const nextBytes = cumBytes + byteCounts[i];
6207
+ if (nextTokens > targetTokens || nextBytes > targetBytes) break;
6208
+ cumTokens = nextTokens;
6209
+ cumBytes = nextBytes;
5882
6210
  if (all[i].role === "user") {
5883
6211
  boundary = i;
5884
6212
  foundSafeBoundary = true;
@@ -6115,13 +6443,11 @@ async function* forceSummaryAfterIterLimit(ctx, opts) {
6115
6443
  content: "The turn is being force-summarized (context guard or stuck-state). Summarize in plain prose what you learned from the tool results above. Do NOT emit any tool calls, function-call markup, DSML invocations, or SEARCH/REPLACE edit blocks \u2014 they will be silently discarded. Just plain text."
6116
6444
  });
6117
6445
  const summaryModel = "deepseek-v4-flash";
6118
- const summaryEffort = "high";
6119
6446
  const resp = await ctx.client.chat({
6120
6447
  model: summaryModel,
6121
6448
  messages,
6122
6449
  signal: ctx.signal,
6123
- thinking: thinkingModeForModel(summaryModel),
6124
- reasoningEffort: summaryEffort
6450
+ thinking: "disabled"
6125
6451
  });
6126
6452
  const rawContent = resp.content?.trim() ?? "";
6127
6453
  const cleaned = stripHallucinatedToolMarkup(rawContent);
@@ -6860,6 +7186,10 @@ var CacheFirstLoop = class {
6860
7186
  async compactHistory(opts) {
6861
7187
  return this.context.fold(this.model, opts);
6862
7188
  }
7189
+ /** Real-time token count of the current log — forwarded to Desktop for meter refresh. */
7190
+ getCurrentLogTokens() {
7191
+ return this.context.getLogTokens();
7192
+ }
6863
7193
  appendAndPersist(message) {
6864
7194
  this.log.append(message);
6865
7195
  if (this.sessionName) {
@@ -7175,21 +7505,24 @@ ${reason}`
7175
7505
  const toolSpecs = this.prefix.tools();
7176
7506
  for (let iter = 0; ; iter++) {
7177
7507
  if (signal.aborted) {
7178
- yield {
7179
- turn: this._turn,
7180
- role: "warning",
7181
- content: t("loop.abortedAtIter", { iter })
7182
- };
7183
- const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
7184
- this.appendAndPersist(buildSyntheticAssistantMessage(stoppedMsg, this.model));
7185
- yield {
7186
- turn: this._turn,
7187
- role: "assistant_final",
7188
- content: stoppedMsg,
7189
- forcedSummary: true
7190
- };
7191
- yield { turn: this._turn, role: "done", content: stoppedMsg };
7192
- this._turnAbort = new AbortController();
7508
+ try {
7509
+ yield {
7510
+ turn: this._turn,
7511
+ role: "warning",
7512
+ content: t("loop.abortedAtIter", { iter })
7513
+ };
7514
+ const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
7515
+ this.appendAndPersist(buildSyntheticAssistantMessage(stoppedMsg, this.model));
7516
+ yield {
7517
+ turn: this._turn,
7518
+ role: "assistant_final",
7519
+ content: stoppedMsg,
7520
+ forcedSummary: true
7521
+ };
7522
+ yield { turn: this._turn, role: "done", content: stoppedMsg };
7523
+ } finally {
7524
+ this._turnAbort = new AbortController();
7525
+ }
7193
7526
  return;
7194
7527
  }
7195
7528
  if (iter > 0) {
@@ -7216,7 +7549,7 @@ ${reason}`
7216
7549
  {
7217
7550
  const decision2 = this.context.decidePreflight(messages, this.prefix.toolSpecs, this.model);
7218
7551
  if (decision2.needsAction) {
7219
- const { estimateTokens: estimate, ctxMax } = decision2;
7552
+ const { estimateTokens: estimate, estimateBytes, ctxMax } = decision2;
7220
7553
  yield {
7221
7554
  turn: this._turn,
7222
7555
  role: "status",
@@ -7238,6 +7571,7 @@ ${reason}`
7238
7571
  estimate: after.estimateTokens.toLocaleString(),
7239
7572
  ctxMax: after.ctxMax.toLocaleString(),
7240
7573
  pct: Math.round(after.estimateTokens / after.ctxMax * 100),
7574
+ bodyKB: Math.round(after.estimateBytes / 1024).toLocaleString(),
7241
7575
  beforeMessages: result.beforeMessages,
7242
7576
  afterMessages: result.afterMessages
7243
7577
  }
@@ -7250,7 +7584,8 @@ ${reason}`
7250
7584
  content: t("loop.preflightNoFold", {
7251
7585
  estimate: estimate.toLocaleString(),
7252
7586
  ctxMax: ctxMax.toLocaleString(),
7253
- pct: Math.round(estimate / ctxMax * 100)
7587
+ pct: Math.round(estimate / ctxMax * 100),
7588
+ bodyKB: Math.round(estimateBytes / 1024).toLocaleString()
7254
7589
  })
7255
7590
  };
7256
7591
  }
@@ -7365,8 +7700,11 @@ ${reason}`
7365
7700
  }
7366
7701
  } catch (err) {
7367
7702
  if (signal.aborted) {
7368
- yield { turn: this._turn, role: "done", content: "" };
7369
- this._turnAbort = new AbortController();
7703
+ try {
7704
+ yield { turn: this._turn, role: "done", content: "" };
7705
+ } finally {
7706
+ this._turnAbort = new AbortController();
7707
+ }
7370
7708
  return;
7371
7709
  }
7372
7710
  const probe = is5xxError(err) ? await probeDeepSeekReachable(this.client) : void 0;
@@ -8465,8 +8803,13 @@ Tips:
8465
8803
  - Add \`allowed-tools: read_file, search_content\` to scope a subagent's tools
8466
8804
  `;
8467
8805
  }
8806
+ function skillDescription(s) {
8807
+ if (s.scope !== "builtin") return s.description;
8808
+ const key = s.name === "security-review" ? "securityReview" : s.name;
8809
+ return t(`builtinSkills.${key}`);
8810
+ }
8468
8811
  function skillIndexLine(s) {
8469
- const safeDesc = s.description.replace(/\n/g, " ").trim();
8812
+ const safeDesc = skillDescription(s).replace(/\n/g, " ").trim();
8470
8813
  const tag = s.runAs === "subagent" ? " [\u{1F9EC} subagent]" : "";
8471
8814
  const max = 130 - s.name.length - tag.length;
8472
8815
  const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}\u2026` : safeDesc;
@@ -9809,8 +10152,8 @@ async function searchContent(ctx, startAbs, args) {
9809
10152
  for (let i = realStart; i <= winEnd; i++) {
9810
10153
  const line = lines[i];
9811
10154
  const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
9812
- const sep3 = hitSet.has(i) ? ":" : "-";
9813
- if (!pushLine(`${rel}:${i + 1}${sep3} ${display}`)) return;
10155
+ const sep2 = hitSet.has(i) ? ":" : "-";
10156
+ if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
9814
10157
  }
9815
10158
  prevWindowEnd = winEnd;
9816
10159
  }
@@ -9841,7 +10184,9 @@ var DEFAULT_OUTLINE_THRESHOLD_BYTES = 64 * 1024;
9841
10184
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
9842
10185
  var HARD_MAX_FILE_BYTES = 32 * 1024 * 1024;
9843
10186
  var OUTLINE_HEAD_LINES = 80;
9844
- var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
10187
+ var SKIP_DIR_NAMES = new Set(
10188
+ DEFAULT_INDEX_EXCLUDES.dirs.filter((d) => d !== ".reasonix")
10189
+ );
9845
10190
  var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
9846
10191
  function displayRel4(rootDir, full) {
9847
10192
  return pathMod5.relative(rootDir, full).replaceAll("\\", "/");
@@ -10868,6 +11213,21 @@ function sanitizeEvidence(raw) {
10868
11213
  }
10869
11214
  return out.length > 0 ? out : void 0;
10870
11215
  }
11216
+ function summarizeEvidence(evidence) {
11217
+ if (!evidence || evidence.length === 0) return void 0;
11218
+ const parts = evidence.map((item) => `${item.kind}: ${item.summary}`);
11219
+ return parts.join("; ");
11220
+ }
11221
+ function compactStepCompletion(update) {
11222
+ const compact = {
11223
+ kind: "step_completed",
11224
+ stepId: update.stepId,
11225
+ result: update.result
11226
+ };
11227
+ const evidenceSummary = summarizeEvidence(update.evidence);
11228
+ if (evidenceSummary) compact.evidenceSummary = evidenceSummary;
11229
+ return compact;
11230
+ }
10871
11231
  function registerSubmitPlan(registry, opts) {
10872
11232
  registry.register({
10873
11233
  name: "submit_plan",
@@ -10981,9 +11341,9 @@ function registerMarkStepComplete(registry, opts) {
10981
11341
  opts.onStepCompleted?.(update);
10982
11342
  const verdict = await (ctx?.confirmationGate ?? pauseGate).ask({
10983
11343
  kind: "plan_checkpoint",
10984
- payload: { stepId, title, result, notes }
11344
+ payload: { stepId, title, result, notes, completion: update }
10985
11345
  });
10986
- if (verdict.type === "continue") return JSON.stringify(update);
11346
+ if (verdict.type === "continue") return JSON.stringify(compactStepCompletion(update));
10987
11347
  if (verdict.type === "revise") {
10988
11348
  if (verdict.feedback) return `revision requested: ${verdict.feedback}`;
10989
11349
  throw new Error("user requested revision at checkpoint");
@@ -11310,12 +11670,40 @@ async function spawnSubagent(opts) {
11310
11670
  let toolIter = 0;
11311
11671
  let summarisingEmitted = false;
11312
11672
  let forcedSummaryFired = false;
11673
+ let outputChars = 0;
11674
+ let reasoningChars = 0;
11675
+ let toolReadChars = 0;
11676
+ let lastStreamEmitAt = 0;
11677
+ let charsSinceLastEmit = 0;
11678
+ const STREAM_EMIT_INTERVAL_MS = 200;
11679
+ const STREAM_EMIT_CHARS = 400;
11680
+ const maybeEmitStreamProgress = (now, force) => {
11681
+ if (!sink?.current) return;
11682
+ if (!force && now - lastStreamEmitAt < STREAM_EMIT_INTERVAL_MS && charsSinceLastEmit < STREAM_EMIT_CHARS) {
11683
+ return;
11684
+ }
11685
+ lastStreamEmitAt = now;
11686
+ charsSinceLastEmit = 0;
11687
+ sink.current({
11688
+ kind: "stream-progress",
11689
+ runId,
11690
+ task: taskPreview,
11691
+ skillName,
11692
+ model,
11693
+ iter: toolIter,
11694
+ elapsedMs: now - startedAt,
11695
+ outputChars,
11696
+ reasoningChars,
11697
+ toolReadChars
11698
+ });
11699
+ };
11313
11700
  try {
11314
11701
  for await (const ev of childLoop.step(opts.task)) {
11315
11702
  sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
11316
11703
  if (ev.role === "tool") {
11317
11704
  toolIter++;
11318
11705
  summarisingEmitted = false;
11706
+ toolReadChars += ev.content?.length ?? 0;
11319
11707
  sink?.current?.({
11320
11708
  kind: "progress",
11321
11709
  runId,
@@ -11325,6 +11713,17 @@ async function spawnSubagent(opts) {
11325
11713
  iter: toolIter,
11326
11714
  elapsedMs: Date.now() - startedAt
11327
11715
  });
11716
+ maybeEmitStreamProgress(Date.now(), true);
11717
+ }
11718
+ if (ev.role === "assistant_delta") {
11719
+ const dContent = ev.content?.length ?? 0;
11720
+ const dReason = ev.reasoningDelta?.length ?? 0;
11721
+ if (dContent > 0 || dReason > 0) {
11722
+ outputChars += dContent;
11723
+ reasoningChars += dReason;
11724
+ charsSinceLastEmit += dContent + dReason;
11725
+ maybeEmitStreamProgress(Date.now(), false);
11726
+ }
11328
11727
  }
11329
11728
  if (ev.role === "assistant_delta" && !summarisingEmitted && (ev.content ?? "").length > 0) {
11330
11729
  summarisingEmitted = true;
@@ -13040,7 +13439,7 @@ function registerShellTools(registry, opts) {
13040
13439
  properties: {
13041
13440
  command: {
13042
13441
  type: "string",
13043
- description: 'Full command line. POSIX-ish quoting. Chain operators `|`, `||`, `&&`, `;` and file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` work natively (no shell). Background `&`, heredoc `<<`, env-var expansion `$VAR`, and command substitution `$(\u2026)` are rejected (or passed through as literal in the case of `$VAR`). To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
13442
+ description: "Full command line. Quoting + chain/redirect rules per the top-level description."
13044
13443
  },
13045
13444
  timeoutSec: {
13046
13445
  type: "integer",
@@ -13287,6 +13686,8 @@ var USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (
13287
13686
  var MOJEEK_ENDPOINT = "https://www.mojeek.com/search";
13288
13687
  var METASO_ENDPOINT = "https://metaso.cn/api/v1";
13289
13688
  var TAVILY_ENDPOINT = "https://api.tavily.com/search";
13689
+ var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai/chat/completions";
13690
+ var EXA_ENDPOINT = "https://api.exa.ai/answer";
13290
13691
  function searchStatusError(status) {
13291
13692
  if (status === 429) return t("webErrors.rateLimit429");
13292
13693
  if (status === 403) return t("webErrors.forbidden403");
@@ -13309,6 +13710,12 @@ async function webSearch(query, opts = {}) {
13309
13710
  if (opts.engine === "tavily") {
13310
13711
  return searchTavily(query, opts);
13311
13712
  }
13713
+ if (opts.engine === "perplexity") {
13714
+ return searchPerplexity(query, opts);
13715
+ }
13716
+ if (opts.engine === "exa") {
13717
+ return searchExa(query, opts);
13718
+ }
13312
13719
  return searchMojeek(query, opts);
13313
13720
  }
13314
13721
  async function searchMojeek(query, opts = {}) {
@@ -13384,6 +13791,7 @@ async function searchSearxng(query, opts = {}) {
13384
13791
  async function searchMetaso(query, opts = {}) {
13385
13792
  const topK = Math.max(1, Math.min(100, opts.topK ?? DEFAULT_TOPK));
13386
13793
  const apiKey = loadMetasoApiKey();
13794
+ if (!apiKey) throw new Error(t("webErrors.metasoMissingKey"));
13387
13795
  let resp;
13388
13796
  try {
13389
13797
  resp = await fetch(`${METASO_ENDPOINT}/search`, {
@@ -13492,6 +13900,121 @@ async function searchTavily(query, opts = {}) {
13492
13900
  snippet: r.content ?? ""
13493
13901
  }));
13494
13902
  }
13903
+ async function searchPerplexity(query, opts = {}) {
13904
+ const topK = Math.max(1, Math.min(20, opts.topK ?? DEFAULT_TOPK));
13905
+ const apiKey = loadPerplexityApiKey();
13906
+ if (!apiKey) throw new Error(t("webErrors.perplexityMissingKey"));
13907
+ let resp;
13908
+ try {
13909
+ resp = await fetch(PERPLEXITY_ENDPOINT, {
13910
+ method: "POST",
13911
+ headers: {
13912
+ Authorization: `Bearer ${apiKey}`,
13913
+ "Content-Type": "application/json"
13914
+ },
13915
+ body: JSON.stringify({
13916
+ model: "sonar",
13917
+ messages: [{ role: "user", content: query }],
13918
+ max_tokens: 1024,
13919
+ return_related_questions: false
13920
+ }),
13921
+ signal: opts.signal
13922
+ });
13923
+ } catch (err) {
13924
+ if (err instanceof TypeError && err.message.includes("fetch")) {
13925
+ throw new Error(t("webErrors.cannotReach", { endpoint: PERPLEXITY_ENDPOINT }));
13926
+ }
13927
+ throw err;
13928
+ }
13929
+ if (!resp.ok) {
13930
+ if (resp.status === 401 || resp.status === 403) {
13931
+ throw new Error(t("webErrors.perplexityUnauthorized"));
13932
+ }
13933
+ if (resp.status === 429) throw new Error(t("webErrors.perplexityRateLimit"));
13934
+ throw new Error(t("webErrors.perplexityServerError", { status: resp.status }));
13935
+ }
13936
+ const raw = await resp.text();
13937
+ let data;
13938
+ try {
13939
+ data = JSON.parse(raw);
13940
+ } catch {
13941
+ throw new Error(t("webErrors.perplexityParseError", { status: resp.status }));
13942
+ }
13943
+ const answer = data.choices?.[0]?.message?.content ?? "";
13944
+ const citations = Array.isArray(data.citations) ? data.citations : [];
13945
+ const results = [];
13946
+ if (answer) {
13947
+ results.push({ title: answer, url: "", snippet: "", answer });
13948
+ }
13949
+ const count = Math.min(citations.length, topK);
13950
+ for (let i = 0; i < count; i++) {
13951
+ const c = citations[i];
13952
+ if (typeof c === "string") {
13953
+ results.push({ title: `Source ${i + 1}`, url: c, snippet: "" });
13954
+ } else if (c && typeof c === "object" && typeof c.url === "string") {
13955
+ const item = c;
13956
+ results.push({
13957
+ title: typeof item.title === "string" ? item.title : `Source ${i + 1}`,
13958
+ url: item.url,
13959
+ snippet: ""
13960
+ });
13961
+ }
13962
+ }
13963
+ return results;
13964
+ }
13965
+ async function searchExa(query, opts = {}) {
13966
+ const topK = Math.max(1, Math.min(20, opts.topK ?? DEFAULT_TOPK));
13967
+ const apiKey = loadExaApiKey();
13968
+ if (!apiKey) throw new Error(t("webErrors.exaMissingKey"));
13969
+ let resp;
13970
+ try {
13971
+ resp = await fetch(EXA_ENDPOINT, {
13972
+ method: "POST",
13973
+ headers: {
13974
+ "x-api-key": apiKey,
13975
+ "Content-Type": "application/json"
13976
+ },
13977
+ body: JSON.stringify({ query, text: true }),
13978
+ signal: opts.signal
13979
+ });
13980
+ } catch (err) {
13981
+ if (err instanceof TypeError && err.message.includes("fetch")) {
13982
+ throw new Error(t("webErrors.cannotReach", { endpoint: EXA_ENDPOINT }));
13983
+ }
13984
+ throw err;
13985
+ }
13986
+ if (!resp.ok) {
13987
+ if (resp.status === 401 || resp.status === 403) {
13988
+ throw new Error(t("webErrors.exaUnauthorized"));
13989
+ }
13990
+ if (resp.status === 429) throw new Error(t("webErrors.exaRateLimit"));
13991
+ throw new Error(t("webErrors.exaServerError", { status: resp.status }));
13992
+ }
13993
+ const raw = await resp.text();
13994
+ let data;
13995
+ try {
13996
+ data = JSON.parse(raw);
13997
+ } catch {
13998
+ throw new Error(t("webErrors.exaParseError", { status: resp.status }));
13999
+ }
14000
+ const answer = data.answer ?? "";
14001
+ const citations = data.citations ?? [];
14002
+ const results = [];
14003
+ if (answer) {
14004
+ results.push({ title: answer, url: "", snippet: "", answer });
14005
+ }
14006
+ const count = Math.min(citations.length, topK);
14007
+ for (let i = 0; i < count; i++) {
14008
+ const c = citations[i];
14009
+ if (!c.url) continue;
14010
+ results.push({
14011
+ title: c.title || `Source ${i + 1}`,
14012
+ url: c.url,
14013
+ snippet: c.text ?? ""
14014
+ });
14015
+ }
14016
+ return results;
14017
+ }
13495
14018
  function parseSearxngHtmlResults(html) {
13496
14019
  const root = parseHtml(html);
13497
14020
  const results = [];
@@ -13710,7 +14233,7 @@ function registerWebTools(registry, opts = {}) {
13710
14233
  const maxFetchChars = opts.maxFetchChars ?? DEFAULT_FETCH_MAX_CHARS;
13711
14234
  registry.register({
13712
14235
  name: "web_search",
13713
- description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this. To change the backend, use /search-engine mojeek|searxng|metaso|tavily.",
14236
+ description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this.",
13714
14237
  readOnly: true,
13715
14238
  parallelSafe: true,
13716
14239
  parameters: {
@@ -13719,7 +14242,7 @@ function registerWebTools(registry, opts = {}) {
13719
14242
  query: { type: "string", description: "Natural-language search query." },
13720
14243
  topK: {
13721
14244
  type: "integer",
13722
- description: `Number of results to return (1..10). Default ${defaultTopK}.`
14245
+ description: `Number of results to return. Default ${defaultTopK}.`
13723
14246
  }
13724
14247
  },
13725
14248
  required: ["query"]
@@ -13763,14 +14286,30 @@ ${page.text}`;
13763
14286
  return registry;
13764
14287
  }
13765
14288
  function formatSearchResults(query, results) {
13766
- const lines = [`query: ${query}`, `
13767
- results (${results.length}):`];
13768
- results.forEach((r, i) => {
14289
+ const lines = [`query: ${query}`];
14290
+ const hasAnswer = results.length > 0 && results[0]?.url === "" && results[0]?.answer;
14291
+ if (hasAnswer) {
14292
+ lines.push("\nanswer:");
14293
+ lines.push(` ${results[0].answer}`);
14294
+ const sources = results.slice(1);
13769
14295
  lines.push(`
14296
+ sources (${sources.length}):`);
14297
+ sources.forEach((r, i) => {
14298
+ lines.push(`
13770
14299
  ${i + 1}. ${r.title}`);
13771
- lines.push(` ${r.url}`);
13772
- if (r.snippet) lines.push(` ${r.snippet}`);
13773
- });
14300
+ lines.push(` ${r.url}`);
14301
+ if (r.snippet) lines.push(` ${r.snippet}`);
14302
+ });
14303
+ } else {
14304
+ lines.push(`
14305
+ results (${results.length}):`);
14306
+ results.forEach((r, i) => {
14307
+ lines.push(`
14308
+ ${i + 1}. ${r.title}`);
14309
+ lines.push(` ${r.url}`);
14310
+ if (r.snippet) lines.push(` ${r.snippet}`);
14311
+ });
14312
+ }
13774
14313
  return lines.join("\n");
13775
14314
  }
13776
14315
 
@@ -15047,7 +15586,7 @@ import {
15047
15586
  writeFileSync as writeFileSync6,
15048
15587
  writeSync
15049
15588
  } from "fs";
15050
- import { dirname as dirname8, resolve as resolve12 } from "path";
15589
+ import { dirname as dirname8, isAbsolute as isAbsolute7, relative as relative8, resolve as resolve12 } from "path";
15051
15590
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
15052
15591
  function parseEditBlocks(text) {
15053
15592
  const out = [];
@@ -15064,10 +15603,30 @@ function parseEditBlocks(text) {
15064
15603
  }
15065
15604
  return out;
15066
15605
  }
15606
+ function resolveEditPath(rootDir, rawPath) {
15607
+ const absRoot = resolve12(rootDir);
15608
+ if (/^[A-Za-z]:[\\/]/.test(rawPath) || looksLikeAbsoluteSystemPath2(rawPath)) {
15609
+ return resolve12(rawPath);
15610
+ }
15611
+ let rooted = rawPath;
15612
+ while (rooted.startsWith("/") || rooted.startsWith("\\")) {
15613
+ rooted = rooted.slice(1);
15614
+ }
15615
+ return resolve12(absRoot, rooted || ".");
15616
+ }
15617
+ function looksLikeAbsoluteSystemPath2(rawPath) {
15618
+ return /^\/(?:home|Users|etc|var|opt|tmp|usr|mnt|Library|Volumes|proc|sys|dev|run|srv|media|Applications|System|root|boot|private)(?:[/\\]|$)/.test(
15619
+ rawPath
15620
+ );
15621
+ }
15622
+ function pathIsUnder2(child, parent) {
15623
+ const rel = relative8(parent, child);
15624
+ return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
15625
+ }
15067
15626
  function applyEditBlock(block, rootDir) {
15068
15627
  const absRoot = resolve12(rootDir);
15069
- const absTarget = resolve12(absRoot, block.path);
15070
- if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep2()}`)) {
15628
+ const absTarget = resolveEditPath(rootDir, block.path);
15629
+ if (!pathIsUnder2(absTarget, absRoot)) {
15071
15630
  return {
15072
15631
  path: block.path,
15073
15632
  status: "path-escape",
@@ -15132,6 +15691,14 @@ function applyEditBlock(block, rootDir) {
15132
15691
  message: "SEARCH text does not match the current file content exactly"
15133
15692
  };
15134
15693
  }
15694
+ const nextIdx = content.indexOf(adaptedSearch, idx + 1);
15695
+ if (nextIdx !== -1) {
15696
+ return {
15697
+ path: block.path,
15698
+ status: "not-found",
15699
+ message: "SEARCH text appears multiple times; include more context to disambiguate"
15700
+ };
15701
+ }
15135
15702
  const replaced = `${content.slice(0, idx)}${adaptedReplace}${content.slice(idx + adaptedSearch.length)}`;
15136
15703
  const outBuf = Buffer.from(replaced, "utf8");
15137
15704
  ftruncateSync(fd, outBuf.length);
@@ -15153,13 +15720,12 @@ function applyEditBlocks(blocks, rootDir) {
15153
15720
  return blocks.map((b) => applyEditBlock(b, rootDir));
15154
15721
  }
15155
15722
  function snapshotBeforeEdits(blocks, rootDir) {
15156
- const absRoot = resolve12(rootDir);
15157
15723
  const seen = /* @__PURE__ */ new Set();
15158
15724
  const snapshots = [];
15159
15725
  for (const b of blocks) {
15160
- if (seen.has(b.path)) continue;
15161
- seen.add(b.path);
15162
- const abs = resolve12(absRoot, b.path);
15726
+ const abs = resolveEditPath(rootDir, b.path);
15727
+ if (seen.has(abs)) continue;
15728
+ seen.add(abs);
15163
15729
  if (!existsSync11(abs)) {
15164
15730
  snapshots.push({ path: b.path, prevContent: null });
15165
15731
  continue;
@@ -15175,8 +15741,8 @@ function snapshotBeforeEdits(blocks, rootDir) {
15175
15741
  function restoreSnapshots(snapshots, rootDir) {
15176
15742
  const absRoot = resolve12(rootDir);
15177
15743
  return snapshots.map((snap) => {
15178
- const abs = resolve12(absRoot, snap.path);
15179
- if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep2()}`)) {
15744
+ const abs = resolveEditPath(rootDir, snap.path);
15745
+ if (!pathIsUnder2(abs, absRoot)) {
15180
15746
  return {
15181
15747
  path: snap.path,
15182
15748
  status: "path-escape",
@@ -15203,9 +15769,6 @@ function restoreSnapshots(snapshots, rootDir) {
15203
15769
  }
15204
15770
  });
15205
15771
  }
15206
- function sep2() {
15207
- return process.platform === "win32" ? "\\" : "/";
15208
- }
15209
15772
  function lineEndingOf(text) {
15210
15773
  return text.includes("\r\n") ? "\r\n" : "\n";
15211
15774
  }
@@ -15333,18 +15896,8 @@ You have BOTH \`semantic_search\` (vector index) and \`search_content\` (literal
15333
15896
  - **Exact-token queries** (a specific identifier, regex, or "find every call to foo") \u2192 call \`search_content\`.
15334
15897
 
15335
15898
  If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall back to \`search_content\`. Don't go the other way \u2014 grepping a paraphrased question wastes turns.`;
15336
- var ENGINEERING_LIFECYCLE_CONTRACT = `
15337
-
15338
- # Engineering lifecycle contract
15339
-
15340
- Reasonix may enforce a prefix-stable Engineering Lifecycle for explicitly enabled high-risk engineering work. The runtime keeps lifecycle state outside the system prompt and tool list, so do not expect stage-specific prompt changes or new tools to appear. Treat any lifecycle block as a host constraint, not as a suggestion.
15341
-
15342
- When high-risk mutations are bounced with \`rejectedReason: "engineering-lifecycle"\`, switch to read-only exploration, then call \`submit_plan\` with concrete steps before trying the mutation again. Add optional per-step \`targets\`, \`acceptance\`, and \`verification\` fields when they clarify scope or success criteria. For medium/high-risk steps, steps with verification criteria, or steps that changed code, \`mark_step_complete\` requires \`evidence\` entries such as verification output, diff summary, checkpoint id, or manual rationale.`;
15343
15899
  function codeSystemPrompt(rootDir, opts = {}) {
15344
- let codeBase = codeSystemBase(opts.modelId ?? DEFAULT_CODE_MODEL);
15345
- if (opts.engineeringLifecycleMode === "strict") {
15346
- codeBase = `${codeBase}${ENGINEERING_LIFECYCLE_CONTRACT}`;
15347
- }
15900
+ const codeBase = codeSystemBase(opts.modelId ?? DEFAULT_CODE_MODEL);
15348
15901
  const base = opts.hasSemanticSearch ? `${codeBase}${SEMANTIC_SEARCH_ROUTING}` : codeBase;
15349
15902
  const withMemory = applyMemoryStack(base, rootDir);
15350
15903
  const gitignorePath = join15(rootDir, ".gitignore");
@@ -15698,8 +16251,10 @@ export {
15698
16251
  loadApiKey,
15699
16252
  loadBaseUrl,
15700
16253
  loadDotenv,
16254
+ loadExaApiKey,
15701
16255
  loadHooks,
15702
16256
  loadMetasoApiKey,
16257
+ loadPerplexityApiKey,
15703
16258
  loadSessionMessages,
15704
16259
  matchesTool,
15705
16260
  memoryEnabled,