reasonix 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +1 -0
  2. package/README.zh-CN.md +1 -0
  3. package/dashboard/dist/app.css +1 -1
  4. package/dashboard/dist/app.js +13 -13
  5. package/dashboard/dist/app.js.map +1 -1
  6. package/dist/cli/{acp-NEUYWGUU.js → acp-ABNDGEYC.js} +63 -30
  7. package/dist/cli/acp-ABNDGEYC.js.map +1 -0
  8. package/dist/cli/chat-377YZV56.js +49 -0
  9. package/dist/cli/{chunk-FY5UERSG.js → chunk-2WZT27GR.js} +9 -9
  10. package/dist/cli/{chunk-B4MOGWHW.js → chunk-4EHRIP5U.js} +7 -7
  11. package/dist/cli/chunk-4EHRIP5U.js.map +1 -0
  12. package/dist/cli/{chunk-RCC73DWQ.js → chunk-4SBXAHR6.js} +4 -4
  13. package/dist/cli/{chunk-5YLEKX2V.js → chunk-4V4TKQMB.js} +4 -4
  14. package/dist/cli/{chunk-5YLEKX2V.js.map → chunk-4V4TKQMB.js.map} +1 -1
  15. package/dist/cli/chunk-7ZO6H6ZK.js +54 -0
  16. package/dist/cli/chunk-7ZO6H6ZK.js.map +1 -0
  17. package/dist/cli/{chunk-3OXD5CBM.js → chunk-A6GSOADP.js} +17870 -16070
  18. package/dist/cli/chunk-A6GSOADP.js.map +1 -0
  19. package/dist/cli/{chunk-Z663GVUB.js → chunk-APOSDBAU.js} +3 -3
  20. package/dist/cli/{chunk-CTRM32BP.js → chunk-B5JISV5I.js} +2 -2
  21. package/dist/cli/{chunk-HNZ4727T.js → chunk-DFHI2MRB.js} +412 -152
  22. package/dist/cli/chunk-DFHI2MRB.js.map +1 -0
  23. package/dist/cli/{chunk-CGVW5W7N.js → chunk-EPIHGOM3.js} +14 -14
  24. package/dist/cli/{chunk-CGVW5W7N.js.map → chunk-EPIHGOM3.js.map} +1 -1
  25. package/dist/cli/{chunk-77JIQ7SL.js → chunk-EQFZIHKJ.js} +8 -8
  26. package/dist/cli/chunk-EQFZIHKJ.js.map +1 -0
  27. package/dist/cli/{chunk-XNMXVL6C.js → chunk-FB27YXPX.js} +2 -2
  28. package/dist/cli/{chunk-ARBGTNHM.js → chunk-FK7NXDRP.js} +2 -2
  29. package/dist/cli/{chunk-AOYUW3HR.js → chunk-GCNBIWK7.js} +22 -2
  30. package/dist/cli/chunk-GCNBIWK7.js.map +1 -0
  31. package/dist/cli/{chunk-XBYHNZ5Z.js → chunk-GMQVINZK.js} +13 -5
  32. package/dist/cli/chunk-GMQVINZK.js.map +1 -0
  33. package/dist/cli/{chunk-MVLPXZAA.js → chunk-GOASYYZ4.js} +43 -11
  34. package/dist/cli/{chunk-MVLPXZAA.js.map → chunk-GOASYYZ4.js.map} +1 -1
  35. package/dist/cli/{chunk-WPY7AFS6.js → chunk-I4SH5Z7S.js} +2 -2
  36. package/dist/cli/{chunk-MW64SQUE.js → chunk-J26XOB2T.js} +2 -2
  37. package/dist/cli/{chunk-DLTE4GBY.js → chunk-J4MYMBJ7.js} +3 -3
  38. package/dist/cli/{chunk-CFJY64UA.js → chunk-LRO63VNK.js} +2 -2
  39. package/dist/cli/{chunk-XUZHBQSM.js → chunk-MQJR7YQ2.js} +2 -2
  40. package/dist/cli/{chunk-CPCUMMSR.js → chunk-NVI4XPOQ.js} +3 -3
  41. package/dist/cli/{chunk-RHQOGG43.js → chunk-OHSVEXFF.js} +3 -3
  42. package/dist/cli/chunk-OHSVEXFF.js.map +1 -0
  43. package/dist/cli/{chunk-AMSK3ZLB.js → chunk-P5SUHDUQ.js} +2 -2
  44. package/dist/cli/{chunk-GFJJEW3Z.js → chunk-QSKDP3OS.js} +55 -5
  45. package/dist/cli/chunk-QSKDP3OS.js.map +1 -0
  46. package/dist/cli/{chunk-D6WRFR6V.js → chunk-R7JMQMLD.js} +6 -5
  47. package/dist/cli/chunk-R7JMQMLD.js.map +1 -0
  48. package/dist/cli/{chunk-VVPV5HU6.js → chunk-RRZIIMAF.js} +2 -2
  49. package/dist/cli/{chunk-GNRKXRRE.js → chunk-S3QII236.js} +369 -359
  50. package/dist/cli/{chunk-GNRKXRRE.js.map → chunk-S3QII236.js.map} +1 -1
  51. package/dist/cli/{chunk-HI6THNAZ.js → chunk-TGP7JGHN.js} +32 -14
  52. package/dist/cli/chunk-TGP7JGHN.js.map +1 -0
  53. package/dist/cli/{chunk-CLHMV6OL.js → chunk-U7G72DHQ.js} +83 -42
  54. package/dist/cli/chunk-U7G72DHQ.js.map +1 -0
  55. package/dist/cli/{chunk-OMNRXZNA.js → chunk-URAI4YRL.js} +2 -2
  56. package/dist/cli/{chunk-6QBUXA73.js → chunk-V4AXMN4X.js} +2 -2
  57. package/dist/cli/{chunk-2W4F3RIZ.js → chunk-XHP6NYOT.js} +3 -2
  58. package/dist/cli/{chunk-2W4F3RIZ.js.map → chunk-XHP6NYOT.js.map} +1 -1
  59. package/dist/cli/{code-WN6D4VZO.js → code-JPFZJYVW.js} +34 -34
  60. package/dist/cli/{commands-DHETOY7O.js → commands-IUL2CLKH.js} +4 -4
  61. package/dist/cli/{commit-BBUYAKZV.js → commit-JT7LYBTL.js} +3 -3
  62. package/dist/cli/{config-KV7VV5LG.js → config-T4RWI5NG.js} +6 -2
  63. package/dist/cli/{desktop-LJVXWXNF.js → desktop-AUBW2SLL.js} +122 -38
  64. package/dist/cli/desktop-AUBW2SLL.js.map +1 -0
  65. package/dist/cli/devtools-O5HOMAGZ.js +3 -0
  66. package/dist/cli/diff-NINZHUJR.js +165 -0
  67. package/dist/cli/diff-NINZHUJR.js.map +1 -0
  68. package/dist/cli/{doctor-GI5LOEZL.js → doctor-OMAYGY4F.js} +10 -10
  69. package/dist/cli/{events-LBKMLFM4.js → events-5IVFJ4H3.js} +5 -5
  70. package/dist/cli/index.js +38 -38
  71. package/dist/cli/{mcp-DKEJK5ND.js → mcp-ECGJACAP.js} +3 -3
  72. package/dist/cli/{mcp-browse-V7KWDY32.js → mcp-browse-NGEOHVJB.js} +15 -15
  73. package/dist/cli/mcp-browse-NGEOHVJB.js.map +1 -0
  74. package/dist/cli/{mcp-inspect-MTABNHVM.js → mcp-inspect-ZIMNRG7G.js} +5 -5
  75. package/dist/cli/{prompt-ATI7DKHF.js → prompt-JCC3A7AA.js} +5 -5
  76. package/dist/cli/{prune-sessions-YQQSZTZS.js → prune-sessions-TE4BJYO2.js} +4 -4
  77. package/dist/cli/{replay-ZJQ4I4CJ.js → replay-2UUTCRTG.js} +29 -29
  78. package/dist/cli/replay-2UUTCRTG.js.map +1 -0
  79. package/dist/cli/{run-HFPRNWJY.js → run-ABQYOPVM.js} +22 -22
  80. package/dist/cli/{server-UHKO2VVM.js → server-MPCXIW2O.js} +27 -25
  81. package/dist/cli/{server-UHKO2VVM.js.map → server-MPCXIW2O.js.map} +1 -1
  82. package/dist/cli/{sessions-IQEWWUH3.js → sessions-YBPRGIAF.js} +16 -16
  83. package/dist/cli/{setup-5BYKCL62.js → setup-A34LF2QE.js} +42 -42
  84. package/dist/cli/setup-A34LF2QE.js.map +1 -0
  85. package/dist/cli/{stats-OFCGOQMZ.js → stats-GKG5JZQX.js} +6 -6
  86. package/dist/cli/stats-GKG5JZQX.js.map +1 -0
  87. package/dist/cli/{version-EODUFAAI.js → version-JD6QSM4X.js} +16 -16
  88. package/dist/index.d.ts +36 -5
  89. package/dist/index.js +443 -73
  90. package/dist/index.js.map +1 -1
  91. package/package.json +7 -2
  92. package/dist/cli/acp-NEUYWGUU.js.map +0 -1
  93. package/dist/cli/chat-QA6IVFJD.js +0 -49
  94. package/dist/cli/chunk-3OXD5CBM.js.map +0 -1
  95. package/dist/cli/chunk-77JIQ7SL.js.map +0 -1
  96. package/dist/cli/chunk-AOYUW3HR.js.map +0 -1
  97. package/dist/cli/chunk-B4MOGWHW.js.map +0 -1
  98. package/dist/cli/chunk-CLHMV6OL.js.map +0 -1
  99. package/dist/cli/chunk-D6WRFR6V.js.map +0 -1
  100. package/dist/cli/chunk-GFJJEW3Z.js.map +0 -1
  101. package/dist/cli/chunk-HI6THNAZ.js.map +0 -1
  102. package/dist/cli/chunk-HNZ4727T.js.map +0 -1
  103. package/dist/cli/chunk-I3NE5S63.js +0 -54
  104. package/dist/cli/chunk-I3NE5S63.js.map +0 -1
  105. package/dist/cli/chunk-RHQOGG43.js.map +0 -1
  106. package/dist/cli/chunk-XBYHNZ5Z.js.map +0 -1
  107. package/dist/cli/desktop-LJVXWXNF.js.map +0 -1
  108. package/dist/cli/diff-2JHMQAHI.js +0 -165
  109. package/dist/cli/diff-2JHMQAHI.js.map +0 -1
  110. package/dist/cli/mcp-browse-V7KWDY32.js.map +0 -1
  111. package/dist/cli/replay-ZJQ4I4CJ.js.map +0 -1
  112. package/dist/cli/setup-5BYKCL62.js.map +0 -1
  113. /package/dist/cli/{chat-QA6IVFJD.js.map → chat-377YZV56.js.map} +0 -0
  114. /package/dist/cli/{chunk-FY5UERSG.js.map → chunk-2WZT27GR.js.map} +0 -0
  115. /package/dist/cli/{chunk-RCC73DWQ.js.map → chunk-4SBXAHR6.js.map} +0 -0
  116. /package/dist/cli/{chunk-Z663GVUB.js.map → chunk-APOSDBAU.js.map} +0 -0
  117. /package/dist/cli/{chunk-CTRM32BP.js.map → chunk-B5JISV5I.js.map} +0 -0
  118. /package/dist/cli/{chunk-XNMXVL6C.js.map → chunk-FB27YXPX.js.map} +0 -0
  119. /package/dist/cli/{chunk-ARBGTNHM.js.map → chunk-FK7NXDRP.js.map} +0 -0
  120. /package/dist/cli/{chunk-WPY7AFS6.js.map → chunk-I4SH5Z7S.js.map} +0 -0
  121. /package/dist/cli/{chunk-MW64SQUE.js.map → chunk-J26XOB2T.js.map} +0 -0
  122. /package/dist/cli/{chunk-DLTE4GBY.js.map → chunk-J4MYMBJ7.js.map} +0 -0
  123. /package/dist/cli/{chunk-CFJY64UA.js.map → chunk-LRO63VNK.js.map} +0 -0
  124. /package/dist/cli/{chunk-XUZHBQSM.js.map → chunk-MQJR7YQ2.js.map} +0 -0
  125. /package/dist/cli/{chunk-CPCUMMSR.js.map → chunk-NVI4XPOQ.js.map} +0 -0
  126. /package/dist/cli/{chunk-AMSK3ZLB.js.map → chunk-P5SUHDUQ.js.map} +0 -0
  127. /package/dist/cli/{chunk-VVPV5HU6.js.map → chunk-RRZIIMAF.js.map} +0 -0
  128. /package/dist/cli/{chunk-OMNRXZNA.js.map → chunk-URAI4YRL.js.map} +0 -0
  129. /package/dist/cli/{chunk-6QBUXA73.js.map → chunk-V4AXMN4X.js.map} +0 -0
  130. /package/dist/cli/{code-WN6D4VZO.js.map → code-JPFZJYVW.js.map} +0 -0
  131. /package/dist/cli/{commands-DHETOY7O.js.map → commands-IUL2CLKH.js.map} +0 -0
  132. /package/dist/cli/{commit-BBUYAKZV.js.map → commit-JT7LYBTL.js.map} +0 -0
  133. /package/dist/cli/{config-KV7VV5LG.js.map → config-T4RWI5NG.js.map} +0 -0
  134. /package/dist/cli/{doctor-GI5LOEZL.js.map → devtools-O5HOMAGZ.js.map} +0 -0
  135. /package/dist/cli/{prompt-ATI7DKHF.js.map → doctor-OMAYGY4F.js.map} +0 -0
  136. /package/dist/cli/{events-LBKMLFM4.js.map → events-5IVFJ4H3.js.map} +0 -0
  137. /package/dist/cli/{mcp-DKEJK5ND.js.map → mcp-ECGJACAP.js.map} +0 -0
  138. /package/dist/cli/{mcp-inspect-MTABNHVM.js.map → mcp-inspect-ZIMNRG7G.js.map} +0 -0
  139. /package/dist/cli/{stats-OFCGOQMZ.js.map → prompt-JCC3A7AA.js.map} +0 -0
  140. /package/dist/cli/{prune-sessions-YQQSZTZS.js.map → prune-sessions-TE4BJYO2.js.map} +0 -0
  141. /package/dist/cli/{run-HFPRNWJY.js.map → run-ABQYOPVM.js.map} +0 -0
  142. /package/dist/cli/{sessions-IQEWWUH3.js.map → sessions-YBPRGIAF.js.map} +0 -0
  143. /package/dist/cli/{version-EODUFAAI.js.map → version-JD6QSM4X.js.map} +0 -0
package/dist/index.js CHANGED
@@ -634,6 +634,20 @@ function loadExaApiKey(path2 = defaultConfigPath()) {
634
634
  if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
635
635
  return void 0;
636
636
  }
637
+ function loadOllamaApiKey(path2 = defaultConfigPath()) {
638
+ if (process.env.OLLAMA_API_KEY) return process.env.OLLAMA_API_KEY.trim();
639
+ if (process.env.ollamaApiKey) return process.env.ollamaApiKey.trim();
640
+ const cfg = readConfig(path2).ollamaApiKey;
641
+ if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
642
+ return void 0;
643
+ }
644
+ function loadBraveApiKey(path2 = defaultConfigPath()) {
645
+ if (process.env.BRAVE_SEARCH_API_KEY) return process.env.BRAVE_SEARCH_API_KEY.trim();
646
+ if (process.env.BRAVE_API_KEY) return process.env.BRAVE_API_KEY.trim();
647
+ const cfg = readConfig(path2).braveApiKey;
648
+ if (cfg && typeof cfg === "string" && cfg.trim()) return cfg.trim();
649
+ return void 0;
650
+ }
637
651
  function defaultConfigPath() {
638
652
  return join(homedir(), ".reasonix", "config.json");
639
653
  }
@@ -786,6 +800,8 @@ function webSearchEngine(path2 = defaultConfigPath()) {
786
800
  if (cfg === "tavily") return "tavily";
787
801
  if (cfg === "perplexity") return "perplexity";
788
802
  if (cfg === "exa") return "exa";
803
+ if (cfg === "brave") return "brave";
804
+ if (cfg === "ollama") return "ollama";
789
805
  return "bing";
790
806
  }
791
807
  function webSearchEndpoint(path2 = defaultConfigPath()) {
@@ -795,8 +811,10 @@ function webSearchEndpoint(path2 = defaultConfigPath()) {
795
811
  }
796
812
  function saveApiKey(key, path2 = defaultConfigPath()) {
797
813
  const cfg = readConfig(path2);
798
- cfg.apiKey = key.trim();
814
+ const trimmed = key.trim();
815
+ cfg.apiKey = trimmed;
799
816
  writeConfig(cfg, path2);
817
+ if (trimmed) process.env.DEEPSEEK_API_KEY = trimmed;
800
818
  }
801
819
  function findProjectKey(cfg, rootDir) {
802
820
  const projects = cfg.projects;
@@ -970,6 +988,44 @@ var Usage = class _Usage {
970
988
  );
971
989
  }
972
990
  };
991
+ function replaceLoneSurrogates(value) {
992
+ let out = "";
993
+ let last = 0;
994
+ for (let i = 0; i < value.length; i++) {
995
+ const code = value.charCodeAt(i);
996
+ if (code >= 55296 && code <= 56319) {
997
+ const next = value.charCodeAt(i + 1);
998
+ if (next >= 56320 && next <= 57343) {
999
+ i++;
1000
+ } else {
1001
+ out += value.slice(last, i);
1002
+ out += "\uFFFD";
1003
+ last = i + 1;
1004
+ }
1005
+ continue;
1006
+ }
1007
+ if (code >= 56320 && code <= 57343) {
1008
+ out += value.slice(last, i);
1009
+ out += "\uFFFD";
1010
+ last = i + 1;
1011
+ }
1012
+ }
1013
+ if (last === 0) return value;
1014
+ return out + value.slice(last);
1015
+ }
1016
+ function sanitizeJsonTransportValue(value) {
1017
+ if (typeof value === "string") return replaceLoneSurrogates(value);
1018
+ if (value === null || typeof value !== "object") return value;
1019
+ if (Array.isArray(value)) return value.map((item) => sanitizeJsonTransportValue(item));
1020
+ const out = {};
1021
+ for (const [key, item] of Object.entries(value)) {
1022
+ out[key] = sanitizeJsonTransportValue(item);
1023
+ }
1024
+ return out;
1025
+ }
1026
+ function stringifyJsonTransport(value) {
1027
+ return JSON.stringify(sanitizeJsonTransportValue(value));
1028
+ }
973
1029
  var DeepSeekClient = class {
974
1030
  apiKey;
975
1031
  baseUrl;
@@ -1019,6 +1075,7 @@ var DeepSeekClient = class {
1019
1075
  messages: opts.messages,
1020
1076
  stream
1021
1077
  };
1078
+ if (stream) payload.stream_options = { include_usage: true };
1022
1079
  if (opts.tools?.length) payload.tools = opts.tools;
1023
1080
  if (opts.temperature !== void 0) payload.temperature = opts.temperature;
1024
1081
  if (opts.maxTokens !== void 0) payload.max_tokens = opts.maxTokens;
@@ -1092,7 +1149,7 @@ var DeepSeekClient = class {
1092
1149
  Authorization: `Bearer ${this.apiKey}`,
1093
1150
  "Content-Type": "application/json"
1094
1151
  },
1095
- body: JSON.stringify(this.buildPayload(opts, false)),
1152
+ body: stringifyJsonTransport(this.buildPayload(opts, false)),
1096
1153
  signal
1097
1154
  },
1098
1155
  { ...this.retry, signal }
@@ -1133,7 +1190,7 @@ var DeepSeekClient = class {
1133
1190
  "Content-Type": "application/json",
1134
1191
  Accept: "text/event-stream"
1135
1192
  },
1136
- body: JSON.stringify(this.buildPayload(opts, true)),
1193
+ body: stringifyJsonTransport(this.buildPayload(opts, true)),
1137
1194
  signal
1138
1195
  },
1139
1196
  { ...this.retry, signal }
@@ -1192,7 +1249,18 @@ var DeepSeekClient = class {
1192
1249
  continue;
1193
1250
  }
1194
1251
  if (done) break;
1195
- const { value, done: streamDone } = await reader.read();
1252
+ let value;
1253
+ let streamDone;
1254
+ try {
1255
+ ({ value, done: streamDone } = await reader.read());
1256
+ } catch (readErr) {
1257
+ const cause = readErr instanceof Error ? readErr : new Error(String(readErr));
1258
+ const code = "code" in cause && typeof cause.code === "string" ? cause.code : void 0;
1259
+ throw Object.assign(new Error(`SSE body read failed: ${cause.message}`), {
1260
+ phase: "stream_body_read",
1261
+ code
1262
+ });
1263
+ }
1196
1264
  if (streamDone) break;
1197
1265
  parser.feed(decoder.decode(value, { stream: true }));
1198
1266
  }
@@ -1728,8 +1796,8 @@ var EN = {
1728
1796
  argsHint: "<question>"
1729
1797
  },
1730
1798
  "search-engine": {
1731
- description: "switch web search backend \u2014 bing (default, works from CN without proxy), searxng (self-hosted), metaso (free 100/d), tavily (free 1000/mo), perplexity (AI-native), or exa (AI-native)",
1732
- argsHint: "<bing|searxng|metaso|tavily|perplexity|exa> [<key>]"
1799
+ description: "switch web search backend \u2014 bing (default, works from CN without proxy), searxng (self-hosted), metaso (free 100/d), tavily (free 1000/mo), perplexity (AI-native), exa (AI-native), or ollama (Ollama cloud web search)",
1800
+ argsHint: "<bing|searxng|metaso|tavily|perplexity|exa|brave|ollama> [<key>]"
1733
1801
  }
1734
1802
  },
1735
1803
  wizard: {
@@ -2374,6 +2442,8 @@ var EN = {
2374
2442
  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)",
2375
2443
  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)",
2376
2444
  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)",
2445
+ usageOllama: " /search-engine ollama use Ollama cloud web search \u2014 set OLLAMA_API_KEY or ollamaApiKey in config; get one at https://ollama.com/settings/keys",
2446
+ usageBrave: " /search-engine brave use Brave Search API (independent index, free 2000/mo \u2014 set BRAVE_SEARCH_API_KEY or braveApiKey in config; get one at https://brave.com/search/api/)",
2377
2447
  alias: "Alias: /se",
2378
2448
  searxngInfo: "SearXNG is a self-hosted metasearch engine (https://github.com/searxng/searxng).",
2379
2449
  searxngInstall: "Install it with: docker run -d -p 8080:8080 searxng/searxng",
@@ -2383,6 +2453,8 @@ var EN = {
2383
2453
  switchedTavilyNote: " Set TAVILY_API_KEY or `tavilyApiKey` in config; free 1000/mo at https://tavily.com.",
2384
2454
  switchedPerplexityNote: " Set PERPLEXITY_API_KEY or `perplexityApiKey` in config; get one at https://perplexity.ai/settings/api.",
2385
2455
  switchedExaNote: " Set EXA_API_KEY or `exaApiKey` in config; sign up at https://exa.ai.",
2456
+ switchedOllamaNote: " Set OLLAMA_API_KEY or `ollamaApiKey` in config; get one at https://ollama.com/settings/keys.",
2457
+ switchedBraveNote: " Set BRAVE_SEARCH_API_KEY (or BRAVE_API_KEY) or `braveApiKey` in config; free 2000/mo at https://brave.com/search/api/.",
2386
2458
  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}.',
2387
2459
  keySaved: " API key saved to config.",
2388
2460
  confirmed: 'Web search engine set to "{engine}"{detail}. Next assistant turn will pick up the change.',
@@ -2667,38 +2739,43 @@ var EN = {
2667
2739
  probeFailed: "probe failed \u2014 {message}"
2668
2740
  },
2669
2741
  webErrors: {
2670
- status: "web_search {status} \u2014 try: the search backend returned an error; rephrase the query, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2742
+ status: "web_search {status} \u2014 try: the search backend returned an error; rephrase the query, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2671
2743
  rateLimit429: "web_search 429 \u2014 try: wait 10s before retrying, or rephrase the query; the search backend is rate-limiting this client",
2672
- forbidden403: "web_search 403 \u2014 try: the search backend is blocking this client; switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa, or wait and retry later",
2744
+ forbidden403: "web_search 403 \u2014 try: the search backend is blocking this client; switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave, or wait and retry later",
2673
2745
  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",
2674
- bingBlocked: "web_search: Bing anti-bot page \u2014 rate-limited or blocked \u2014 try: wait 30s and retry, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2675
- bingNoResults: "web_search: 0 results but response doesn't look like a real empty page ({chars} chars, first 120: {preview}) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2746
+ bingBlocked: "web_search: Bing anti-bot page \u2014 rate-limited or blocked \u2014 try: wait 30s and retry, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2747
+ bingNoResults: "web_search: 0 results but response doesn't look like a real empty page ({chars} chars, first 120: {preview}) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2676
2748
  invalidEndpoint: 'web_search: invalid SearXNG endpoint "{endpoint}" \u2014 try: set a valid URL with /search-endpoint http://host:port',
2677
2749
  endpointMustBeHttp: "web_search: SearXNG endpoint must be http(s), got {protocol} \u2014 try: set a valid URL with /search-endpoint http://host:port",
2678
- cannotReach: "web_search: Cannot reach SearXNG server at {endpoint} \u2014 try: install and start SearXNG (https://github.com/searxng/searxng, e.g. `docker run -d -p 8080:8080 searxng/searxng`), or switch to another engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2679
- searxngNoResults: "web_search: 0 results but SearXNG response doesn't look like an empty results page ({chars} chars) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2750
+ cannotReach: "web_search: Cannot reach SearXNG server at {endpoint} \u2014 try: install and start SearXNG (https://github.com/searxng/searxng, e.g. `docker run -d -p 8080:8080 searxng/searxng`), or switch to another engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2751
+ searxngNoResults: "web_search: 0 results but SearXNG response doesn't look like an empty results page ({chars} chars) \u2014 try: rephrase the query with simpler terms, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2680
2752
  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",
2681
2753
  metasoDailyLimit: "web_search: Metaso daily search limit reached \u2014 set METASO_API_KEY or get a key at https://metaso.cn/search-api/playground",
2682
2754
  metasoUnauthorized: "web_search: Metaso API key rejected \u2014 check METASO_API_KEY or get one at https://metaso.cn/search-api/playground",
2683
2755
  metasoRateLimit: "web_search: Metaso rate-limited \u2014 wait and retry, or get your own API key at https://metaso.cn/search-api/playground",
2684
- metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2756
+ metasoServerError: "web_search: Metaso server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2685
2757
  metasoParseError: "web_search: Metaso returned unparseable response (HTTP {status}) \u2014 try again later",
2686
2758
  metasoApiError: "web_search: Metaso API error (code {code}: {message}) \u2014 try again later",
2687
2759
  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",
2688
2760
  tavilyUnauthorized: "web_search: Tavily API key rejected \u2014 check TAVILY_API_KEY or get one at https://tavily.com",
2689
- tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa, or upgrade your Tavily plan",
2690
- tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2761
+ tavilyRateLimit: "web_search: Tavily rate-limited or monthly quota exceeded \u2014 wait, switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave, or upgrade your Tavily plan",
2762
+ tavilyServerError: "web_search: Tavily server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2691
2763
  tavilyParseError: "web_search: Tavily returned unparseable response (HTTP {status}) \u2014 try again later",
2692
2764
  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",
2693
2765
  perplexityUnauthorized: "web_search: Perplexity API key rejected \u2014 check PERPLEXITY_API_KEY or get one at https://perplexity.ai/settings/api",
2694
- perplexityRateLimit: "web_search: Perplexity rate-limited \u2014 wait and retry, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2695
- perplexityServerError: "web_search: Perplexity server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2766
+ perplexityRateLimit: "web_search: Perplexity rate-limited \u2014 wait and retry, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2767
+ perplexityServerError: "web_search: Perplexity server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2696
2768
  perplexityParseError: "web_search: Perplexity returned unparseable response (HTTP {status}) \u2014 try again later",
2697
2769
  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",
2698
2770
  exaUnauthorized: "web_search: Exa API key rejected \u2014 check EXA_API_KEY or get one at https://exa.ai",
2699
2771
  exaRateLimit: "web_search: Exa API rate-limited or monthly quota exceeded \u2014 wait or upgrade at https://exa.ai/pricing",
2700
- exaServerError: "web_search: Exa server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa",
2772
+ exaServerError: "web_search: Exa server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2701
2773
  exaParseError: "web_search: Exa returned unparseable response (HTTP {status}) \u2014 try again later",
2774
+ braveMissingKey: "web_search: Brave Search requires an API key \u2014 set BRAVE_SEARCH_API_KEY (or BRAVE_API_KEY) env var or `braveApiKey` in ~/.reasonix/config.json; free 2000/mo signup at https://brave.com/search/api/",
2775
+ braveUnauthorized: "web_search: Brave Search API key rejected \u2014 check BRAVE_SEARCH_API_KEY or get one at https://brave.com/search/api/",
2776
+ braveRateLimit: "web_search: Brave Search API rate-limited or monthly quota exceeded \u2014 wait or upgrade at https://brave.com/search/api/",
2777
+ braveServerError: "web_search: Brave Search server error ({status}) \u2014 try again later, or switch engine with /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
2778
+ braveParseError: "web_search: Brave Search returned unparseable response (HTTP {status}) \u2014 try again later",
2702
2779
  fetchStatus: "web_fetch {status} for {url} \u2014 try: confirm the URL resolves in a browser; status suggests the host returned an error page",
2703
2780
  fetchRateLimit429: "web_fetch 429 for {url} \u2014 try: wait 10s before retrying; the host is rate-limiting this client",
2704
2781
  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",
@@ -4037,6 +4114,7 @@ var de = {
4037
4114
  usageTavily: " /search-engine tavily Tavily-API verwenden (LLM-freundlich, kostenlos 1000/Monat \u2014 setze TAVILY_API_KEY oder tavilyApiKey in der Konfiguration; erhalte einen unter https://tavily.com)",
4038
4115
  usagePerplexity: " /search-engine perplexity Perplexity AI verwenden (AI-native Antwort + Quellenangaben \u2014 setze PERPLEXITY_API_KEY oder perplexityApiKey in der Konfiguration; erhalte einen unter https://perplexity.ai/settings/api)",
4039
4116
  usageExa: " /search-engine exa Exa-API verwenden (AI-native Antwort + Quellenangaben, kostenlos 1000/Monat \u2014 setze EXA_API_KEY oder exaApiKey in der Konfiguration; registriere dich unter https://exa.ai)",
4117
+ usageBrave: " /search-engine brave Brave Search API nutzen (unabh\xE4ngiger Index, kostenlos 2000/Monat \u2014 setze BRAVE_SEARCH_API_KEY oder braveApiKey in der Konfiguration; Schl\xFCssel unter https://brave.com/search/api/)",
4040
4118
  alias: "Alias: /se",
4041
4119
  searxngInfo: "SearXNG ist eine selbst gehostete Metasuchmaschine (https://github.com/searxng/searxng).",
4042
4120
  searxngInstall: "Installiere mit: docker run -d -p 8080:8080 searxng/searxng",
@@ -4046,6 +4124,7 @@ var de = {
4046
4124
  switchedTavilyNote: " Setze TAVILY_API_KEY oder `tavilyApiKey` in der Konfiguration; kostenlos 1000/Monat unter https://tavily.com.",
4047
4125
  switchedPerplexityNote: " Setze PERPLEXITY_API_KEY oder `perplexityApiKey` in der Konfiguration; erhalte einen unter https://perplexity.ai/settings/api.",
4048
4126
  switchedExaNote: " Setze EXA_API_KEY oder `exaApiKey` in der Konfiguration; registriere dich unter https://exa.ai.",
4127
+ switchedBraveNote: " Setze BRAVE_SEARCH_API_KEY (oder BRAVE_API_KEY) oder `braveApiKey` in der Konfiguration; 2000 kostenlose Zugriffe pro Monat unter https://brave.com/search/api/.",
4049
4128
  keyNeeded: 'Kein API-Schl\xFCssel f\xFCr "{engine}" konfiguriert.\n\n 1. Setze die {envVar}-Umgebungsvariable\n 2. Oder gib ihn inline an: /search-engine {engine} <dein-schl\xFCssel>\n 3. Oder f\xFCge "{engine}ApiKey" zu ~/.reasonix/config.json hinzu\n\nWiederhole dann /search-engine {engine}.',
4050
4129
  keySaved: " API-Schl\xFCssel in der Konfiguration gespeichert.",
4051
4130
  confirmed: 'Websuchmaschine auf "{engine}" gesetzt{detail}. Der n\xE4chste Assistenten-Turn \xFCbernimmt die \xC4nderung.',
@@ -4350,38 +4429,43 @@ var de = {
4350
4429
  },
4351
4430
  webErrors: {
4352
4431
  ...EN.webErrors,
4353
- status: "web_search {status} \u2014 versuche: Das Such-Backend hat einen Fehler zur\xFCckgegeben; formuliere die Abfrage um oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4432
+ status: "web_search {status} \u2014 versuche: Das Such-Backend hat einen Fehler zur\xFCckgegeben; formuliere die Abfrage um oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4354
4433
  rateLimit429: "web_search 429 \u2014 versuche: 10s warten vor erneuter Abfrage oder Abfrage umformulieren; das Such-Backend hat das Rate-Limit f\xFCr diesen Client erreicht",
4355
- forbidden403: "web_search 403 \u2014 versuche: Das Such-Backend blockiert diesen Client; wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa oder warte und versuche es sp\xE4ter erneut",
4434
+ forbidden403: "web_search 403 \u2014 versuche: Das Such-Backend blockiert diesen Client; wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama oder warte und versuche es sp\xE4ter erneut",
4356
4435
  serverError5xx: "web_search {status} \u2014 versuche: \xD6ffne die Such-URL in einem Browser; falls sie l\xE4dt, ist dies vor\xFCbergehend und ein erneuter Versuch in 30s kann helfen",
4357
- bingBlocked: "web_search: Bing-Anti-Bot-Seite \u2014 Rate-Limit erreicht oder blockiert \u2014 versuche: 30s warten und erneut versuchen, oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4358
- bingNoResults: "web_search: 0 Ergebnisse, aber die Antwort sieht nicht wie eine echte leere Seite aus ({chars} Zeichen, erste 120: {preview}) \u2014 versuche: formuliere die Abfrage mit einfacheren Begriffen um oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4436
+ bingBlocked: "web_search: Bing-Anti-Bot-Seite \u2014 Rate-Limit erreicht oder blockiert \u2014 versuche: 30s warten und erneut versuchen, oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4437
+ bingNoResults: "web_search: 0 Ergebnisse, aber die Antwort sieht nicht wie eine echte leere Seite aus ({chars} Zeichen, erste 120: {preview}) \u2014 versuche: formuliere die Abfrage mit einfacheren Begriffen um oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4359
4438
  invalidEndpoint: 'web_search: ung\xFCltiger SearXNG-Endpunkt "{endpoint}" \u2014 versuche: setze eine g\xFCltige URL mit /search-endpoint http://host:port',
4360
4439
  endpointMustBeHttp: "web_search: SearXNG-Endpunkt muss http(s) sein, {protocol} erhalten \u2014 versuche: setze eine g\xFCltige URL mit /search-endpoint http://host:port",
4361
- cannotReach: "web_search: SearXNG-Server unter {endpoint} nicht erreichbar \u2014 versuche: SearXNG installieren und starten (https://github.com/searxng/searxng, z.B. `docker run -d -p 8080:8080 searxng/searxng`), oder wechsle zu einer anderen Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4362
- searxngNoResults: "web_search: 0 Ergebnisse, aber SearXNG-Antwort sieht nicht wie eine leere Ergebnisseite aus ({chars} Zeichen) \u2014 versuche: formuliere die Abfrage mit einfacheren Begriffen um oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4440
+ cannotReach: "web_search: SearXNG-Server unter {endpoint} nicht erreichbar \u2014 versuche: SearXNG installieren und starten (https://github.com/searxng/searxng, z.B. `docker run -d -p 8080:8080 searxng/searxng`), oder wechsle zu einer anderen Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4441
+ searxngNoResults: "web_search: 0 Ergebnisse, aber SearXNG-Antwort sieht nicht wie eine leere Ergebnisseite aus ({chars} Zeichen) \u2014 versuche: formuliere die Abfrage mit einfacheren Begriffen um oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4363
4442
  metasoMissingKey: "web_search: Metaso ben\xF6tigt einen API-Schl\xFCssel \u2014 setze METASO_API_KEY oder konfiguriere einen mit /search-engine metaso <schl\xFCssel>. Erhalte einen unter https://metaso.cn/search-api/playground",
4364
4443
  metasoDailyLimit: "web_search: Metaso-Tageslimit erreicht \u2014 setze METASO_API_KEY oder erhalte einen Schl\xFCssel unter https://metaso.cn/search-api/playground",
4365
4444
  metasoUnauthorized: "web_search: Metaso-API-Schl\xFCssel abgelehnt \u2014 \xFCberpr\xFCfe METASO_API_KEY oder erhalte einen unter https://metaso.cn/search-api/playground",
4366
4445
  metasoRateLimit: "web_search: Metaso-Rate-Limit erreicht \u2014 warte und versuche es erneut, oder erhalte einen eigenen API-Schl\xFCssel unter https://metaso.cn/search-api/playground",
4367
- metasoServerError: "web_search: Metaso-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4446
+ metasoServerError: "web_search: Metaso-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4368
4447
  metasoParseError: "web_search: Metaso hat unparsbare Antwort zur\xFCckgegeben (HTTP {status}) \u2014 versuche es sp\xE4ter erneut",
4369
4448
  metasoApiError: "web_search: Metaso-API-Fehler (Code {code}: {message}) \u2014 versuche es sp\xE4ter erneut",
4370
4449
  tavilyMissingKey: "web_search: Tavily-Backend ben\xF6tigt einen API-Schl\xFCssel \u2014 setze TAVILY_API_KEY-Umgebungsvariable oder `tavilyApiKey` in ~/.reasonix/config.json; kostenlose 1000/Monat-Registrierung unter https://tavily.com",
4371
4450
  tavilyUnauthorized: "web_search: Tavily-API-Schl\xFCssel abgelehnt \u2014 \xFCberpr\xFCfe TAVILY_API_KEY oder erhalte einen unter https://tavily.com",
4372
- tavilyRateLimit: "web_search: Tavily-Rate-Limit erreicht oder monatliches Kontingent \xFCberschritten \u2014 warte, wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa oder upgrade deinen Tavily-Plan",
4373
- tavilyServerError: "web_search: Tavily-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4451
+ tavilyRateLimit: "web_search: Tavily-Rate-Limit erreicht oder monatliches Kontingent \xFCberschritten \u2014 warte, wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama oder upgrade deinen Tavily-Plan",
4452
+ tavilyServerError: "web_search: Tavily-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4374
4453
  tavilyParseError: "web_search: Tavily hat unparsbare Antwort zur\xFCckgegeben (HTTP {status}) \u2014 versuche es sp\xE4ter erneut",
4375
4454
  perplexityMissingKey: "web_search: Perplexity-Backend ben\xF6tigt einen API-Schl\xFCssel \u2014 setze PERPLEXITY_API_KEY-Umgebungsvariable oder `perplexityApiKey` in ~/.reasonix/config.json; erhalte einen unter https://perplexity.ai/settings/api",
4376
4455
  perplexityUnauthorized: "web_search: Perplexity-API-Schl\xFCssel abgelehnt \u2014 \xFCberpr\xFCfe PERPLEXITY_API_KEY oder erhalte einen unter https://perplexity.ai/settings/api",
4377
- perplexityRateLimit: "web_search: Perplexity-Rate-Limit erreicht \u2014 warte und versuche es erneut, oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4378
- perplexityServerError: "web_search: Perplexity-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4456
+ perplexityRateLimit: "web_search: Perplexity-Rate-Limit erreicht \u2014 warte und versuche es erneut, oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4457
+ perplexityServerError: "web_search: Perplexity-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4379
4458
  perplexityParseError: "web_search: Perplexity hat unparsbare Antwort zur\xFCckgegeben (HTTP {status}) \u2014 versuche es sp\xE4ter erneut",
4380
4459
  exaMissingKey: "web_search: Exa-Backend ben\xF6tigt einen API-Schl\xFCssel \u2014 setze EXA_API_KEY-Umgebungsvariable oder `exaApiKey` in ~/.reasonix/config.json; kostenlose 1000/Monat-Registrierung unter https://exa.ai",
4381
4460
  exaUnauthorized: "web_search: Exa-API-Schl\xFCssel abgelehnt \u2014 \xFCberpr\xFCfe EXA_API_KEY oder erhalte einen unter https://exa.ai",
4382
4461
  exaRateLimit: "web_search: Exa-API-Rate-Limit erreicht oder monatliches Kontingent \xFCberschritten \u2014 warte oder upgrade unter https://exa.ai/pricing",
4383
- exaServerError: "web_search: Exa-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa",
4462
+ exaServerError: "web_search: Exa-Serverfehler ({status}) \u2014 versuche es sp\xE4ter erneut oder wechsle die Engine mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4384
4463
  exaParseError: "web_search: Exa hat unparsbare Antwort zur\xFCckgegeben (HTTP {status}) \u2014 versuche es sp\xE4ter erneut",
4464
+ braveMissingKey: "web_search: F\xFCr Brave Search ist ein API-Schl\xFCssel erforderlich \u2014 setze die Umgebungsvariable BRAVE_SEARCH_API_KEY (oder BRAVE_API_KEY) oder `braveApiKey` in ~/.reasonix/config.json; kostenlose Anmeldung mit 2000 Einheiten pro Monat unter https://brave.com/search/api/",
4465
+ braveUnauthorized: "web_search: Brave-Such-API-Schl\xFCssel abgelehnt \u2014 \xFCberpr\xFCfe BRAVE_SEARCH_API_KEY oder beantrage einen unter https://brave.com/search/api/",
4466
+ braveRateLimit: "web_search: Die Brave Search API unterliegt einer Ratenbegrenzung oder das monatliche Kontingent wurde \xFCberschritten \u2014 warten oder ein Upgrade durchf\xFChren unter https://brave.com/search/api/",
4467
+ braveServerError: "web_search: Fehler beim Brave-Suchserver ({status}) \u2014 sp\xE4ter erneut versuchen oder die Engine wechseln mit /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave|ollama",
4468
+ braveParseError: "web_search: Brave Search hat eine nicht auswertbare Antwort zur\xFCckgegeben (HTTP {status}) \u2014 sp\xE4ter erneut versuchen",
4385
4469
  fetchStatus: "web_fetch {status} f\xFCr {url} \u2014 versuche: Best\xE4tige, dass die URL im Browser aufgel\xF6st wird; der Status deutet darauf hin, dass der Host eine Fehlerseite zur\xFCckgegeben hat",
4386
4470
  fetchRateLimit429: "web_fetch 429 f\xFCr {url} \u2014 versuche: 10s warten vor erneuter Abfrage; der Host ratelimitet diesen Client",
4387
4471
  fetchForbidden403: "web_fetch 403 f\xFCr {url} \u2014 versuche: Der Host blockiert diesen Client; die Seite erfordert m\xF6glicherweise eine Anmeldung oder blockiert Bots \u2014 verwende stattdessen web_search-Ausz\xFCge",
@@ -5187,6 +5271,22 @@ var ru = {
5187
5271
  riskMed: " \u0441\u0440\u0435\u0434",
5188
5272
  riskHigh: " \u0432\u044B\u0441",
5189
5273
  completeMsg: "\u25B8 \u043F\u043B\u0430\u043D \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u2014 \u0432\u0441\u0435 {total} \u0448\u0430\u0433(\u043E\u0432) \u0441\u0434\u0435\u043B\u0430\u043D\u044B \xB7 \u0430\u0440\u0445\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D"
5274
+ },
5275
+ webErrors: {
5276
+ ...EN.webErrors,
5277
+ braveMissingKey: "web_search: \u0414\u043B\u044F \u0440\u0430\u0431\u043E\u0442\u044B Brave Search \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u043A\u043B\u044E\u0447 API \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0443\u044E \u0441\u0440\u0435\u0434\u044B BRAVE_SEARCH_API_KEY (\u0438\u043B\u0438 BRAVE_API_KEY) \u0438\u043B\u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 `braveApiKey` \u0432 \u0444\u0430\u0439\u043B\u0435 ~/.reasonix/config.json; \u0431\u0435\u0441\u043F\u043B\u0430\u0442\u043D\u0430\u044F \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F \u0441 \u043B\u0438\u043C\u0438\u0442\u043E\u043C 2000 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 \u043C\u0435\u0441\u044F\u0446 \u043D\u0430 \u0441\u0430\u0439\u0442\u0435 https://brave.com/search/api/",
5278
+ braveUnauthorized: "web_search: \u041A\u043B\u044E\u0447 API Brave Search \u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 BRAVE_SEARCH_API_KEY \u0438\u043B\u0438 \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u0435 \u043D\u043E\u0432\u044B\u0439 \u043A\u043B\u044E\u0447 \u043D\u0430 \u0441\u0430\u0439\u0442\u0435 https://brave.com/search/api/",
5279
+ braveRateLimit: "web_search: \u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u043B\u0438\u043C\u0438\u0442 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u0438\u043B\u0438 \u043C\u0435\u0441\u044F\u0447\u043D\u0430\u044F \u043A\u0432\u043E\u0442\u0430 \u0434\u043B\u044F Brave Search API \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043D\u0430 \u043F\u043B\u0430\u0442\u043D\u0443\u044E \u0432\u0435\u0440\u0441\u0438\u044E \u043D\u0430 \u0441\u0430\u0439\u0442\u0435 https://brave.com/search/api/",
5280
+ braveServerError: "web_search: \u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Brave Search ({status}) \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0436\u0435 \u0438\u043B\u0438 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0440\u0443\u0433\u043E\u0439 \u043F\u043E\u0438\u0441\u043A\u043E\u0432\u0438\u043A \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave",
5281
+ braveParseError: "web_search: Brave Search \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435\u0440\u0430\u0437\u0431\u043E\u0440\u0447\u0438\u0432\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 (HTTP {status}) \u2014 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0436\u0435"
5282
+ },
5283
+ handlers: {
5284
+ ...EN.handlers,
5285
+ webSearchEngine: {
5286
+ ...EN.handlers.webSearchEngine,
5287
+ usageBrave: " /search-engine brave \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 Brave Search API (\u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u044B\u0439 \u0438\u043D\u0434\u0435\u043A\u0441, \u0431\u0435\u0441\u043F\u043B\u0430\u0442\u043D\u043E 2000 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 \u043C\u0435\u0441\u044F\u0446 \u2014 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 BRAVE_SEARCH_API_KEY \u0438\u043B\u0438 braveApiKey \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438; \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u043A\u043B\u044E\u0447 \u043C\u043E\u0436\u043D\u043E \u043D\u0430 \u0441\u0430\u0439\u0442\u0435 https://brave.com/search/api/)",
5288
+ switchedBraveNote: " \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 BRAVE_SEARCH_API_KEY (\u0438\u043B\u0438 BRAVE_API_KEY) \u0438\u043B\u0438 `braveApiKey` \u0432 \u0444\u0430\u0439\u043B\u0435 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438; 2000 \u0431\u0435\u0441\u043F\u043B\u0430\u0442\u043D\u044B\u0445 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 \u043C\u0435\u0441\u044F\u0446 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u043F\u043E \u0430\u0434\u0440\u0435\u0441\u0443 https://brave.com/search/api/."
5289
+ }
5190
5290
  }
5191
5291
  };
5192
5292
 
@@ -5581,8 +5681,8 @@ var zhCN = {
5581
5681
  argsHint: "<question>"
5582
5682
  },
5583
5683
  "search-engine": {
5584
- description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 bing\uFF08\u9ED8\u8BA4\uFF0C\u56FD\u5185\u88F8 IP \u76F4\u8FDE\uFF09\u3001searxng\uFF08\u81EA\u6258\u7BA1\uFF09\u3001metaso\uFF08\u6BCF\u65E5 100 \u6B21\uFF09\u3001tavily\uFF08\u6BCF\u6708 1000 \u6B21\u514D\u8D39\uFF09\u3001perplexity\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09\u6216 exa\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09",
5585
- argsHint: "<bing|searxng|metaso|tavily|perplexity|exa> [<key>]"
5684
+ description: "\u5207\u6362\u7F51\u7EDC\u641C\u7D22\u540E\u7AEF \u2014 bing\uFF08\u9ED8\u8BA4\uFF0C\u56FD\u5185\u88F8 IP \u76F4\u8FDE\uFF09\u3001searxng\uFF08\u81EA\u6258\u7BA1\uFF09\u3001metaso\uFF08\u6BCF\u65E5 100 \u6B21\uFF09\u3001tavily\uFF08\u6BCF\u6708 1000 \u6B21\u514D\u8D39\uFF09\u3001perplexity\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09\u3001exa\uFF08AI \u76F4\u63A5\u56DE\u7B54\uFF09\u6216 ollama\uFF08Ollama \u4E91\u7AEF\u641C\u7D22\uFF09",
5685
+ argsHint: "<bing|searxng|metaso|tavily|perplexity|exa|brave|ollama> [<key>]"
5586
5686
  }
5587
5687
  },
5588
5688
  wizard: {
@@ -6227,6 +6327,8 @@ var zhCN = {
6227
6327
  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",
6228
6328
  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",
6229
6329
  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",
6330
+ usageOllama: " /search-engine ollama \u4F7F\u7528 Ollama \u4E91\u7AEF\u7F51\u9875\u641C\u7D22 \u2014 \u8BBE\u7F6E OLLAMA_API_KEY \u6216 config \u7684 ollamaApiKey\uFF1B\u5728 https://ollama.com/settings/keys \u83B7\u53D6\u5BC6\u94A5",
6331
+ usageBrave: " /search-engine brave \u4F7F\u7528 Brave Search API\uFF08\u72EC\u7ACB\u7D22\u5F15\uFF0C\u6BCF\u6708 2000 \u6B21\u514D\u8D39 \u2014 \u8BBE\u7F6E BRAVE_SEARCH_API_KEY \u6216 config \u7684 braveApiKey\uFF1B\u5728 https://brave.com/search/api/ \u83B7\u53D6\u5BC6\u94A5\uFF09",
6230
6332
  alias: "\u522B\u540D\uFF1A/se",
6231
6333
  searxngInfo: "SearXNG \u662F\u4E00\u4E2A\u81EA\u6258\u7BA1\u7684\u5143\u641C\u7D22\u5F15\u64CE\uFF08https://github.com/searxng/searxng\uFF09\u3002",
6232
6334
  searxngInstall: "\u5B89\u88C5\u547D\u4EE4\uFF1A docker run -d -p 8080:8080 searxng/searxng",
@@ -6236,6 +6338,8 @@ var zhCN = {
6236
6338
  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",
6237
6339
  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",
6238
6340
  switchedExaNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EXA_API_KEY \u6216 config \u4E2D\u7684 `exaApiKey`\uFF1B\u6CE8\u518C https://exa.ai\u3002",
6341
+ switchedOllamaNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF OLLAMA_API_KEY \u6216 config \u4E2D\u7684 `ollamaApiKey`\uFF1B\u5728 https://ollama.com/settings/keys \u83B7\u53D6\u5BC6\u94A5\u3002",
6342
+ switchedBraveNote: " \u8BF7\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF BRAVE_SEARCH_API_KEY \u6216 config \u4E2D\u7684 `braveApiKey`\uFF1Bhttps://brave.com/search/api/ \u6BCF\u6708 2000 \u6B21\u514D\u8D39\u3002",
6239
6343
  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',
6240
6344
  keySaved: " API \u5BC6\u94A5\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u3002",
6241
6345
  confirmed: '\u7F51\u9875\u641C\u7D22\u5F15\u64CE\u5DF2\u8BBE\u4E3A "{engine}"{detail}\u3002\u4E0B\u4E00\u8F6E\u6A21\u578B\u8C03\u7528\u5C06\u751F\u6548\u3002',
@@ -6520,38 +6624,43 @@ var zhCN = {
6520
6624
  probeFailed: "\u63A2\u6D4B\u5931\u8D25 \u2014 {message}"
6521
6625
  },
6522
6626
  webErrors: {
6523
- status: "web_search {status} \u2014 try: \u641C\u7D22\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF\uFF1B\u8BF7\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6627
+ status: "web_search {status} \u2014 try: \u641C\u7D22\u540E\u7AEF\u8FD4\u56DE\u9519\u8BEF\uFF1B\u8BF7\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6524
6628
  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",
6525
- forbidden403: "web_search 403 \u2014 try: \u641C\u7D22\u540E\u7AEF\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5",
6629
+ forbidden403: "web_search 403 \u2014 try: \u641C\u7D22\u540E\u7AEF\u62D2\u7EDD\u8BE5\u5BA2\u6237\u7AEF\u8BBF\u95EE\uFF1B\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5",
6526
6630
  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",
6527
- bingBlocked: "web_search: Bing \u53CD\u722C\u9875\u9762 \u2014 \u9891\u7387\u9650\u5236\u6216\u88AB\u5C4F\u853D \u2014 try: \u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6528
- bingNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46\u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF0C\u524D 120 \u5B57\u7B26\uFF1A{preview}\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6631
+ bingBlocked: "web_search: Bing \u53CD\u722C\u9875\u9762 \u2014 \u9891\u7387\u9650\u5236\u6216\u88AB\u5C4F\u853D \u2014 try: \u7B49\u5F85 30 \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6632
+ bingNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46\u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF0C\u524D 120 \u5B57\u7B26\uFF1A{preview}\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6529
6633
  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',
6530
6634
  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",
6531
- cannotReach: "web_search: \u65E0\u6CD5\u8BBF\u95EE SearXNG \u670D\u52A1\u5668 {endpoint} \u2014 try: \u5B89\u88C5\u5E76\u542F\u52A8 SearXNG\uFF08https://github.com/searxng/searxng\uFF0C\u4F8B\u5982 `docker run -d -p 8080:8080 searxng/searxng`\uFF09\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6532
- searxngNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46 SearXNG \u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6635
+ cannotReach: "web_search: \u65E0\u6CD5\u8BBF\u95EE SearXNG \u670D\u52A1\u5668 {endpoint} \u2014 try: \u5B89\u88C5\u5E76\u542F\u52A8 SearXNG\uFF08https://github.com/searxng/searxng\uFF0C\u4F8B\u5982 `docker run -d -p 8080:8080 searxng/searxng`\uFF09\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6636
+ searxngNoResults: "web_search: \u8FD4\u56DE 0 \u6761\u7ED3\u679C\u4F46 SearXNG \u54CD\u5E94\u770B\u8D77\u6765\u4E0D\u662F\u6B63\u5E38\u7A7A\u7ED3\u679C\u9875\uFF08{chars} \u5B57\u7B26\uFF09\u2014 try: \u4F7F\u7528\u66F4\u7B80\u5355\u7684\u5173\u952E\u8BCD\u6539\u5199\u67E5\u8BE2\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6533
6637
  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",
6534
6638
  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",
6535
6639
  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",
6536
6640
  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",
6537
- metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6641
+ metasoServerError: "web_search: Metaso \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6538
6642
  metasoParseError: "web_search: Metaso \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
6539
6643
  metasoApiError: "web_search: Metaso API \u9519\u8BEF\uFF08code {code}: {message}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
6540
6644
  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",
6541
6645
  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",
6542
- tavilyRateLimit: "web_search: Tavily \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u3001\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
6543
- tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6646
+ tavilyRateLimit: "web_search: Tavily \u8BF7\u6C42\u9891\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u3001\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE\uFF0C\u6216\u5347\u7EA7 Tavily \u8BA1\u5212",
6647
+ tavilyServerError: "web_search: Tavily \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6544
6648
  tavilyParseError: "web_search: Tavily \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
6545
6649
  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",
6546
6650
  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",
6547
- perplexityRateLimit: "web_search: Perplexity \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6548
- perplexityServerError: "web_search: Perplexity \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6651
+ perplexityRateLimit: "web_search: Perplexity \u8BF7\u6C42\u9891\u7387\u9650\u5236 \u2014 \u7B49\u5F85\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6652
+ perplexityServerError: "web_search: Perplexity \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6549
6653
  perplexityParseError: "web_search: Perplexity \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
6550
6654
  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",
6551
6655
  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",
6552
6656
  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",
6553
- exaServerError: "web_search: Exa \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa \u5207\u6362\u5F15\u64CE",
6657
+ exaServerError: "web_search: Exa \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6554
6658
  exaParseError: "web_search: Exa \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
6659
+ braveMissingKey: "web_search: Brave Search \u9700\u8981 API \u5BC6\u94A5 \u2014 \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF BRAVE_SEARCH_API_KEY\uFF08\u6216 BRAVE_API_KEY\uFF09\u6216 config \u7684 `braveApiKey`\uFF1Bhttps://brave.com/search/api/ \u6BCF\u6708 2000 \u6B21\u514D\u8D39",
6660
+ braveUnauthorized: "web_search: Brave Search API \u5BC6\u94A5\u88AB\u62D2\u7EDD \u2014 \u68C0\u67E5 BRAVE_SEARCH_API_KEY \u6216\u5728 https://brave.com/search/api/ \u83B7\u53D6\u5BC6\u94A5",
6661
+ braveRateLimit: "web_search: Brave Search API \u8FBE\u5230\u901F\u7387\u9650\u5236\u6216\u6708\u5EA6\u914D\u989D\u7528\u5C3D \u2014 \u7B49\u5F85\u6216\u5347\u7EA7 https://brave.com/search/api/",
6662
+ braveServerError: "web_search: Brave Search \u670D\u52A1\u5668\u9519\u8BEF\uFF08{status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u4F7F\u7528 /search-engine bing|searxng|metaso|tavily|perplexity|exa|brave \u5207\u6362\u5F15\u64CE",
6663
+ braveParseError: "web_search: Brave Search \u8FD4\u56DE\u65E0\u6CD5\u89E3\u6790\u7684\u54CD\u5E94\uFF08HTTP {status}\uFF09\u2014 \u7A0D\u540E\u91CD\u8BD5",
6555
6664
  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",
6556
6665
  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",
6557
6666
  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",
@@ -7493,14 +7602,19 @@ import {
7493
7602
  writeFileSync as writeFileSync2
7494
7603
  } from "fs";
7495
7604
  import { homedir as homedir3 } from "os";
7496
- import { join as join4, relative, resolve as resolve2 } from "path";
7605
+ import { join as join4, parse, relative, resolve as resolve2 } from "path";
7497
7606
  var TRUNCATED_DIR = "truncated-results";
7498
7607
  var DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
7499
7608
  function sanitizeToolName(name) {
7500
7609
  return name.replace(/[^\w\-]/g, "_").slice(0, 48) || "unknown";
7501
7610
  }
7611
+ function useHomeFallback(rootDir) {
7612
+ if (!rootDir) return true;
7613
+ const abs = resolve2(rootDir);
7614
+ return abs === parse(abs).root;
7615
+ }
7502
7616
  function storageDir(rootDir) {
7503
- const base = rootDir ? join4(resolve2(rootDir), ".reasonix") : join4(homedir3(), ".reasonix");
7617
+ const base = useHomeFallback(rootDir) ? join4(homedir3(), ".reasonix") : join4(resolve2(rootDir), ".reasonix");
7504
7618
  return join4(base, TRUNCATED_DIR);
7505
7619
  }
7506
7620
  function resultFilename(toolName) {
@@ -7522,7 +7636,7 @@ function saveTruncatedResult(content, toolName, rootDir) {
7522
7636
  chmodSync2(absPath, 384);
7523
7637
  } catch {
7524
7638
  }
7525
- if (rootDir) {
7639
+ if (!useHomeFallback(rootDir)) {
7526
7640
  const absRoot = resolve2(rootDir);
7527
7641
  return relative(absRoot, absPath).replaceAll("\\", "/");
7528
7642
  }
@@ -8081,8 +8195,8 @@ function truncateForModel(s, maxChars, extraNote) {
8081
8195
  if (s.length <= maxChars) return s;
8082
8196
  const tailBudget = Math.min(1024, Math.floor(maxChars * 0.1));
8083
8197
  const headBudget = Math.max(0, maxChars - tailBudget);
8084
- const head = s.slice(0, headBudget);
8085
- const tail = s.slice(-tailBudget);
8198
+ const head = sliceAlignedToCodepoint(s, headBudget);
8199
+ const tail = sliceSuffixAlignedToCodepoint(s, tailBudget);
8086
8200
  const dropped = s.length - head.length - tail.length;
8087
8201
  const note = extraNote ? ` \u2014 ${extraNote}` : "";
8088
8202
  return `${head}
@@ -8091,6 +8205,21 @@ function truncateForModel(s, maxChars, extraNote) {
8091
8205
 
8092
8206
  ${tail}`;
8093
8207
  }
8208
+ function sliceAlignedToCodepoint(s, end) {
8209
+ if (end <= 0) return "";
8210
+ if (end >= s.length) return s;
8211
+ const last = s.charCodeAt(end - 1);
8212
+ if (last >= 55296 && last <= 56319) return s.slice(0, end - 1);
8213
+ return s.slice(0, end);
8214
+ }
8215
+ function sliceSuffixAlignedToCodepoint(s, len) {
8216
+ if (len <= 0) return "";
8217
+ if (len >= s.length) return s;
8218
+ const start = s.length - len;
8219
+ const first = s.charCodeAt(start);
8220
+ if (first >= 56320 && first <= 57343) return s.slice(start + 1);
8221
+ return s.slice(start);
8222
+ }
8094
8223
  function truncateForModelByTokens(s, maxTokens, extraNote) {
8095
8224
  if (maxTokens <= 0) return "";
8096
8225
  if (s.length <= maxTokens) return s;
@@ -8128,28 +8257,28 @@ function sizePrefixToTokens(s, budget) {
8128
8257
  let size = Math.min(s.length, budget * 4);
8129
8258
  for (let iter = 0; iter < 6; iter++) {
8130
8259
  if (size <= 0) return "";
8131
- const slice = s.slice(0, size);
8260
+ const slice = sliceAlignedToCodepoint(s, size);
8132
8261
  const count = countTokens(slice);
8133
8262
  if (count <= budget) return slice;
8134
8263
  const next = Math.floor(size * (budget / count) * 0.95);
8135
- if (next >= size) return s.slice(0, Math.max(0, size - 1));
8264
+ if (next >= size) return sliceAlignedToCodepoint(s, Math.max(0, size - 1));
8136
8265
  size = next;
8137
8266
  }
8138
- return s.slice(0, Math.max(0, size));
8267
+ return sliceAlignedToCodepoint(s, Math.max(0, size));
8139
8268
  }
8140
8269
  function sizeSuffixToTokens(s, budget) {
8141
8270
  if (budget <= 0 || s.length === 0) return "";
8142
8271
  let size = Math.min(s.length, budget * 4);
8143
8272
  for (let iter = 0; iter < 6; iter++) {
8144
8273
  if (size <= 0) return "";
8145
- const slice = s.slice(-size);
8274
+ const slice = sliceSuffixAlignedToCodepoint(s, size);
8146
8275
  const count = countTokens(slice);
8147
8276
  if (count <= budget) return slice;
8148
8277
  const next = Math.floor(size * (budget / count) * 0.95);
8149
- if (next >= size) return s.slice(-Math.max(0, size - 1));
8278
+ if (next >= size) return sliceSuffixAlignedToCodepoint(s, Math.max(0, size - 1));
8150
8279
  size = next;
8151
8280
  }
8152
- return s.slice(-Math.max(0, size));
8281
+ return sliceSuffixAlignedToCodepoint(s, Math.max(0, size));
8153
8282
  }
8154
8283
  function blockToString(block) {
8155
8284
  if (block.type === "text") return block.text;
@@ -8993,6 +9122,16 @@ function is5xxError(err) {
8993
9122
  const m = /^DeepSeek (5\d{2}):/.exec(err.message ?? "");
8994
9123
  return m !== null;
8995
9124
  }
9125
+ function is4xxError(err) {
9126
+ if (!(err instanceof Error)) return false;
9127
+ return /^DeepSeek (4\d{2}):/.test(err.message ?? "");
9128
+ }
9129
+ function errorMeta(err) {
9130
+ if (!(err instanceof Error)) return {};
9131
+ const code = "code" in err && typeof err.code === "string" ? err.code : void 0;
9132
+ const phase = "phase" in err && typeof err.phase === "string" ? err.phase : void 0;
9133
+ return { code, phase };
9134
+ }
8996
9135
  async function probeDeepSeekReachable(client, timeoutMs = 1500) {
8997
9136
  const balance = await client.getBalance({ signal: AbortSignal.timeout(timeoutMs) });
8998
9137
  return { reachable: balance !== null };
@@ -9065,9 +9204,8 @@ async function* forceSummaryAfterIterLimit(ctx, opts) {
9065
9204
  role: "user",
9066
9205
  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."
9067
9206
  });
9068
- const summaryModel = "deepseek-v4-flash";
9069
9207
  const resp = await ctx.client.chat({
9070
- model: summaryModel,
9208
+ model: ctx.model,
9071
9209
  messages,
9072
9210
  signal: ctx.signal,
9073
9211
  thinking: "disabled"
@@ -9079,8 +9217,8 @@ async function* forceSummaryAfterIterLimit(ctx, opts) {
9079
9217
  const annotated = `${reasonPrefix}
9080
9218
 
9081
9219
  ${summary}`;
9082
- const summaryStats = ctx.recordStats(summaryModel, resp.usage ?? new Usage());
9083
- ctx.appendAndPersist(buildAssistantMessage(summary, [], summaryModel, resp.reasoningContent));
9220
+ const summaryStats = ctx.recordStats(ctx.model, resp.usage ?? new Usage());
9221
+ ctx.appendAndPersist(buildAssistantMessage(summary, [], ctx.model, resp.reasoningContent));
9084
9222
  yield {
9085
9223
  turn: ctx.turn,
9086
9224
  role: "assistant_final",
@@ -9091,11 +9229,18 @@ ${summary}`;
9091
9229
  yield { turn: ctx.turn, role: "done", content: summary };
9092
9230
  } catch (err) {
9093
9231
  const label = errorLabelFor(opts.reason);
9232
+ const message = t("summary.failedAfterReason", { label, message: err.message });
9094
9233
  yield {
9095
9234
  turn: ctx.turn,
9096
9235
  role: "error",
9097
9236
  content: "",
9098
- error: t("summary.failedAfterReason", { label, message: err.message })
9237
+ error: message,
9238
+ errorDetail: {
9239
+ name: "ForceSummaryFailed",
9240
+ message,
9241
+ retryable: true,
9242
+ recoverable: true
9243
+ }
9099
9244
  };
9100
9245
  yield { turn: ctx.turn, role: "done", content: "" };
9101
9246
  }
@@ -10282,14 +10427,21 @@ ${reason}`
10282
10427
  if (this.budgetUsd !== null) {
10283
10428
  const spent = this.stats.totalCost;
10284
10429
  if (spent >= this.budgetUsd) {
10430
+ const message = t("loop.budgetExhausted", {
10431
+ spent: spent.toFixed(4),
10432
+ cap: this.budgetUsd.toFixed(2)
10433
+ });
10285
10434
  yield {
10286
10435
  turn: this._turn,
10287
10436
  role: "error",
10288
10437
  content: "",
10289
- error: t("loop.budgetExhausted", {
10290
- spent: spent.toFixed(4),
10291
- cap: this.budgetUsd.toFixed(2)
10292
- })
10438
+ error: message,
10439
+ errorDetail: {
10440
+ name: "BudgetExhausted",
10441
+ message,
10442
+ retryable: false,
10443
+ recoverable: false
10444
+ }
10293
10445
  };
10294
10446
  this._steerQueue.length = 0;
10295
10447
  return;
@@ -10443,11 +10595,22 @@ ${reason}`
10443
10595
  const upstreamHost = this.client.baseUrl;
10444
10596
  const dsHost = isDeepSeekHost(upstreamHost);
10445
10597
  const probe = is5xxError(err) && dsHost ? await probeDeepSeekReachable(this.client) : void 0;
10598
+ const cause = err instanceof Error ? err : new Error(String(err));
10599
+ const retryable = !is4xxError(cause) && cause.name !== "AbortError";
10600
+ const { code, phase } = errorMeta(cause);
10446
10601
  yield {
10447
10602
  turn: this._turn,
10448
10603
  role: "error",
10449
10604
  content: "",
10450
- error: formatLoopError(err, probe, { upstreamHost })
10605
+ error: formatLoopError(err, probe, { upstreamHost }),
10606
+ errorDetail: {
10607
+ name: cause.name,
10608
+ message: cause.message,
10609
+ phase,
10610
+ code,
10611
+ retryable,
10612
+ recoverable: false
10613
+ }
10451
10614
  };
10452
10615
  this._steerQueue.length = 0;
10453
10616
  return;
@@ -10592,7 +10755,8 @@ ${reason}`
10592
10755
  buildMessages: () => this.buildMessages(),
10593
10756
  appendAndPersist: (m) => this.appendAndPersist(m),
10594
10757
  recordStats: (model, usage) => this.stats.record(this._turn, model, usage),
10595
- turn: this._turn
10758
+ turn: this._turn,
10759
+ model: this.model
10596
10760
  };
10597
10761
  }
10598
10762
  async run(userInput, onEvent) {
@@ -15452,7 +15616,7 @@ function parseCommandChain(cmd) {
15452
15616
  const cmdName = seg.argv[0] ?? "";
15453
15617
  if (cmdName.toLowerCase() === "cd") {
15454
15618
  throw new UnsupportedSyntaxError(
15455
- "cd in parsed command chains does not change cwd for later segments. Use a command-native cwd flag instead, such as `npm --prefix <dir> run <script>`, `git -C <dir> ...`, or `cargo -C <dir> ...`."
15619
+ "cd in parsed command chains does not change cwd for later segments. By default, run generated scripts from the directory where the script was written; do not assume an input/data directory is the cwd just because the task reads files there. Pass input/data paths as arguments unless the command truly depends on that cwd. For package tools, use command-native cwd flags such as `npm --prefix <dir> run <script>`, `git -C <dir> ...`, or `cargo -C <dir> ...`."
15456
15620
  );
15457
15621
  }
15458
15622
  }
@@ -16334,7 +16498,7 @@ function registerShellTools(registry, opts) {
16334
16498
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
16335
16499
  registry.register({
16336
16500
  name: "run_command",
16337
- description: 'Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; mutating / network / install commands gate on user confirmation.\n\nDO NOT use run_command for file operations \u2014 use write_file, edit_file, multi_edit, copy_file, move_file, or delete_file instead. Shell utilities (echo, cp, sed, cat, tee, perl, python -c, etc.) bypass validation, lack rollback, and will trigger user confirmation gates that waste turns.\n\nNo real shell \u2014 argv parsed natively for cross-platform parity:\n\u2022 Supported: chains `|`/`||`/`&&`/`;` (each segment allowlist-checked) and file redirects `>`/`>>`/`<`/`2>`/`2>>`/`2>&1`/`&>`.\n\u2022 Rejected: background `&`, heredoc `<<`, `$(\u2026)`, subshells, `$VAR` expansion, glob expansion. Quote operator chars as literals (`grep "a|b" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain. Use `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>` instead.\n\u2022 Filter at source \u2014 `grep -c` / `wc -l` / narrower paths over unbounded dumps.',
16501
+ description: 'Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; mutating / network / install commands gate on user confirmation.\n\nDO NOT use run_command for file operations \u2014 use write_file, edit_file, multi_edit, copy_file, move_file, or delete_file instead. Shell utilities (echo, cp, sed, cat, tee, perl, python -c, etc.) bypass validation, lack rollback, and will trigger user confirmation gates that waste turns.\n\nNo real shell \u2014 argv parsed natively for cross-platform parity:\n\u2022 Supported: chains `|`/`||`/`&&`/`;` (each segment allowlist-checked) and file redirects `>`/`>>`/`<`/`2>`/`2>>`/`2>&1`/`&>`.\n\u2022 Rejected: background `&`, heredoc `<<`, `$(\u2026)`, subshells, `$VAR` expansion, glob expansion. Quote operator chars as literals (`grep "a|b" file`).\n\u2022 `cd` is rejected in chains. By default, run generated scripts from the directory where the script was written; do not assume an input/data directory is the cwd. Pass input/data paths as arguments unless the command truly depends on that cwd. For package tools, use `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>`.\n\u2022 Filter at source \u2014 `grep -c` / `wc -l` / narrower paths over unbounded dumps.',
16338
16502
  // Plan-mode gate: allow allowlisted commands through (git status,
16339
16503
  // cargo check, ls, grep …) so the model can actually investigate
16340
16504
  // during planning. Anything that would otherwise trigger a
@@ -16601,6 +16765,9 @@ var METASO_ENDPOINT = "https://metaso.cn/api/v1";
16601
16765
  var TAVILY_ENDPOINT = "https://api.tavily.com/search";
16602
16766
  var PERPLEXITY_ENDPOINT = "https://api.perplexity.ai/chat/completions";
16603
16767
  var EXA_ENDPOINT = "https://api.exa.ai/answer";
16768
+ var BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
16769
+ var OLLAMA_WEB_SEARCH_ENDPOINT = "https://ollama.com/api/web_search";
16770
+ var OLLAMA_WEB_FETCH_ENDPOINT = "https://ollama.com/api/web_fetch";
16604
16771
  var FETCH_MAX_REDIRECTS = 5;
16605
16772
  function searchStatusError(status) {
16606
16773
  if (status === 429) return t("webErrors.rateLimit429");
@@ -16652,6 +16819,22 @@ function isInternalAddress(address) {
16652
16819
  if (family === 6) return isPrivateIpv6(address);
16653
16820
  return false;
16654
16821
  }
16822
+ async function dohResolve(host) {
16823
+ const url = new URL("https://1.1.1.1/dns-query");
16824
+ url.searchParams.set("name", host);
16825
+ url.searchParams.set("type", "A");
16826
+ const resp = await fetch(url.toString(), {
16827
+ headers: { Accept: "application/dns-json" },
16828
+ signal: AbortSignal.timeout(5e3)
16829
+ });
16830
+ if (!resp.ok) throw new Error(`DoH resolve failed: HTTP ${resp.status} for ${host}`);
16831
+ const data = await resp.json();
16832
+ if (data.Status !== 0)
16833
+ throw new Error(`DoH resolve failed: DNS status ${data.Status} for ${host}`);
16834
+ const addresses = (data.Answer ?? []).filter((a) => a.type === 1).map((a) => a.data);
16835
+ if (addresses.length === 0) throw new Error(`DoH resolve returned no A records for ${host}`);
16836
+ return addresses;
16837
+ }
16655
16838
  async function assertPublicHttpUrl(rawUrl) {
16656
16839
  const url = new URL(rawUrl);
16657
16840
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -16659,10 +16842,22 @@ async function assertPublicHttpUrl(rawUrl) {
16659
16842
  }
16660
16843
  const host = url.hostname;
16661
16844
  const literal = isIP(host);
16662
- const addresses = literal ? [host] : (await lookup(host, { all: true, verbatim: true })).map((entry) => entry.address);
16663
- if (addresses.length === 0 || addresses.some(isInternalAddress)) {
16845
+ if (literal) {
16846
+ if (isInternalAddress(host)) {
16847
+ throw new Error(`web_fetch refuses internal or reserved host: ${host}`);
16848
+ }
16849
+ return url;
16850
+ }
16851
+ const sysAddrs = (await lookup(host, { all: true, verbatim: true })).map((e) => e.address);
16852
+ if (sysAddrs.length === 0) {
16664
16853
  throw new Error(`web_fetch refuses internal or reserved host: ${host}`);
16665
16854
  }
16855
+ if (sysAddrs.some(isInternalAddress)) {
16856
+ const dohAddrs = await dohResolve(host).catch(() => null);
16857
+ if (!dohAddrs || dohAddrs.some(isInternalAddress)) {
16858
+ throw new Error(`web_fetch refuses internal or reserved host: ${host}`);
16859
+ }
16860
+ }
16666
16861
  return url;
16667
16862
  }
16668
16863
  function redirectLocation(resp, currentUrl) {
@@ -16687,6 +16882,12 @@ async function webSearch(query, opts = {}) {
16687
16882
  if (opts.engine === "exa") {
16688
16883
  return searchExa(query, opts);
16689
16884
  }
16885
+ if (opts.engine === "ollama") {
16886
+ return searchOllama(query, opts);
16887
+ }
16888
+ if (opts.engine === "brave") {
16889
+ return searchBrave(query, opts);
16890
+ }
16690
16891
  return searchBing(query, opts);
16691
16892
  }
16692
16893
  async function searchBing(query, opts = {}) {
@@ -16986,6 +17187,154 @@ async function searchExa(query, opts = {}) {
16986
17187
  }
16987
17188
  return results;
16988
17189
  }
17190
+ async function searchOllama(query, opts = {}) {
17191
+ const topK = Math.max(1, Math.min(10, opts.topK ?? DEFAULT_TOPK));
17192
+ const apiKey = loadOllamaApiKey(opts.configPath);
17193
+ if (!apiKey) {
17194
+ throw new Error(
17195
+ "web_search: Ollama web search requires an API key \u2014 set OLLAMA_API_KEY, `ollamaApiKey`, or use /search-engine ollama <key>."
17196
+ );
17197
+ }
17198
+ let resp;
17199
+ try {
17200
+ resp = await fetch(OLLAMA_WEB_SEARCH_ENDPOINT, {
17201
+ method: "POST",
17202
+ headers: {
17203
+ Authorization: `Bearer ${apiKey}`,
17204
+ "Content-Type": "application/json",
17205
+ Accept: "application/json"
17206
+ },
17207
+ body: JSON.stringify({ query, max_results: topK }),
17208
+ signal: opts.signal
17209
+ });
17210
+ } catch (err) {
17211
+ if (err instanceof TypeError && err.message.includes("fetch")) {
17212
+ throw new Error(t("webErrors.cannotReach", { endpoint: OLLAMA_WEB_SEARCH_ENDPOINT }));
17213
+ }
17214
+ throw err;
17215
+ }
17216
+ if (!resp.ok) {
17217
+ if (resp.status === 401 || resp.status === 403) {
17218
+ throw new Error("web_search: Ollama API key rejected \u2014 check OLLAMA_API_KEY.");
17219
+ }
17220
+ if (resp.status === 429) {
17221
+ throw new Error("web_search: Ollama web search is rate-limited or quota-limited.");
17222
+ }
17223
+ throw new Error(`web_search: Ollama web search returned HTTP ${resp.status}.`);
17224
+ }
17225
+ let data;
17226
+ try {
17227
+ data = await resp.json();
17228
+ } catch {
17229
+ throw new Error(`web_search: Ollama returned unparseable response (HTTP ${resp.status}).`);
17230
+ }
17231
+ return (data.results ?? []).slice(0, topK).map((r, i) => ({
17232
+ title: r.title || `Result ${i + 1}`,
17233
+ url: r.url || "",
17234
+ snippet: r.content ?? ""
17235
+ }));
17236
+ }
17237
+ async function searchBrave(query, opts = {}) {
17238
+ const topK = Math.max(1, Math.min(20, opts.topK ?? DEFAULT_TOPK));
17239
+ const apiKey = loadBraveApiKey(opts.configPath);
17240
+ if (!apiKey) throw new Error(t("webErrors.braveMissingKey"));
17241
+ const url = `${BRAVE_ENDPOINT}?q=${encodeURIComponent(query)}&count=${topK}`;
17242
+ let resp;
17243
+ try {
17244
+ resp = await fetch(url, {
17245
+ headers: {
17246
+ Accept: "application/json",
17247
+ "Accept-Encoding": "gzip",
17248
+ "X-Subscription-Token": apiKey
17249
+ },
17250
+ signal: opts.signal
17251
+ });
17252
+ } catch (err) {
17253
+ if (err instanceof TypeError && err.message.includes("fetch")) {
17254
+ throw new Error(t("webErrors.cannotReach", { endpoint: BRAVE_ENDPOINT }));
17255
+ }
17256
+ throw err;
17257
+ }
17258
+ if (!resp.ok) {
17259
+ if (resp.status === 401 || resp.status === 403) {
17260
+ throw new Error(t("webErrors.braveUnauthorized"));
17261
+ }
17262
+ if (resp.status === 429) {
17263
+ throw new Error(t("webErrors.braveRateLimit"));
17264
+ }
17265
+ throw new Error(t("webErrors.braveServerError", { status: resp.status }));
17266
+ }
17267
+ const raw = await resp.text();
17268
+ let data;
17269
+ try {
17270
+ data = JSON.parse(raw);
17271
+ } catch {
17272
+ throw new Error(t("webErrors.braveParseError", { status: resp.status }));
17273
+ }
17274
+ const results = data.web?.results ?? [];
17275
+ return results.slice(0, topK).map((r) => ({
17276
+ title: r.title ?? "",
17277
+ url: r.url ?? "",
17278
+ snippet: r.description ?? ""
17279
+ }));
17280
+ }
17281
+ async function webFetchOllama(url, opts = {}) {
17282
+ const apiKey = loadOllamaApiKey(opts.configPath);
17283
+ if (!apiKey) {
17284
+ throw new Error(
17285
+ "web_fetch: Ollama web fetch requires an API key \u2014 set OLLAMA_API_KEY, `ollamaApiKey`, or use /search-engine ollama <key>."
17286
+ );
17287
+ }
17288
+ const ctrl = new AbortController();
17289
+ const timeout = setTimeout(
17290
+ () => ctrl.abort(
17291
+ new Error(
17292
+ t("webErrors.fetchTimeout", { ms: opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS, url })
17293
+ )
17294
+ ),
17295
+ opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS
17296
+ );
17297
+ const signal = opts.signal ? AbortSignal.any([opts.signal, ctrl.signal]) : ctrl.signal;
17298
+ let resp;
17299
+ try {
17300
+ resp = await fetch(OLLAMA_WEB_FETCH_ENDPOINT, {
17301
+ method: "POST",
17302
+ headers: {
17303
+ Authorization: `Bearer ${apiKey}`,
17304
+ "Content-Type": "application/json",
17305
+ Accept: "application/json"
17306
+ },
17307
+ body: JSON.stringify({ url }),
17308
+ signal
17309
+ });
17310
+ } finally {
17311
+ clearTimeout(timeout);
17312
+ }
17313
+ if (!resp.ok) {
17314
+ if (resp.status === 401 || resp.status === 403) {
17315
+ throw new Error("web_fetch: Ollama API key rejected \u2014 check OLLAMA_API_KEY.");
17316
+ }
17317
+ if (resp.status === 429) {
17318
+ throw new Error("web_fetch: Ollama web fetch is rate-limited or quota-limited.");
17319
+ }
17320
+ throw new Error(`web_fetch: Ollama web fetch returned HTTP ${resp.status} for ${url}.`);
17321
+ }
17322
+ let data;
17323
+ try {
17324
+ data = await resp.json();
17325
+ } catch {
17326
+ throw new Error(`web_fetch: Ollama returned unparseable response for ${url}.`);
17327
+ }
17328
+ const maxChars = opts.maxChars ?? DEFAULT_FETCH_MAX_CHARS;
17329
+ const text = data.content ?? "";
17330
+ return {
17331
+ url,
17332
+ title: data.title,
17333
+ text: text.length > maxChars ? text.slice(0, maxChars) : text,
17334
+ truncated: text.length > maxChars,
17335
+ links: data.links
17336
+ };
17337
+ }
16989
17338
  function parseSearxngHtmlResults(html) {
16990
17339
  const root = parseHtml(html);
16991
17340
  const results = [];
@@ -17211,13 +17560,14 @@ function registerWebTools(registry, opts = {}) {
17211
17560
  required: ["query"]
17212
17561
  },
17213
17562
  fn: async (args, ctx) => {
17214
- const engine = webSearchEngine();
17215
- const endpoint = webSearchEndpoint();
17563
+ const engine = webSearchEngine(opts.configPath);
17564
+ const endpoint = webSearchEndpoint(opts.configPath);
17216
17565
  const results = await webSearch(args.query, {
17217
17566
  topK: args.topK ?? defaultTopK,
17218
17567
  signal: ctx?.signal,
17219
17568
  engine,
17220
- endpoint
17569
+ endpoint,
17570
+ configPath: opts.configPath
17221
17571
  });
17222
17572
  return formatSearchResults(args.query, results);
17223
17573
  }
@@ -17238,6 +17588,22 @@ function registerWebTools(registry, opts = {}) {
17238
17588
  if (!/^https?:\/\//i.test(args.url)) {
17239
17589
  throw new Error(t("webErrors.fetchInvalidUrl"));
17240
17590
  }
17591
+ if (webSearchEngine(opts.configPath) === "ollama") {
17592
+ const page2 = await webFetchOllama(args.url, {
17593
+ maxChars: maxFetchChars,
17594
+ signal: ctx?.signal,
17595
+ configPath: opts.configPath
17596
+ });
17597
+ const header2 = page2.title ? `${page2.title}
17598
+ ${page2.url}` : page2.url;
17599
+ const links = page2.links?.length ? `
17600
+
17601
+ links:
17602
+ ${page2.links.join("\n")}` : "";
17603
+ return `${header2}
17604
+
17605
+ ${page2.text}${links}`;
17606
+ }
17241
17607
  const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
17242
17608
  const header = page.title ? `${page.title}
17243
17609
  ${page.url}` : page.url;
@@ -17312,6 +17678,7 @@ function recordFromLoopEvent(ev, extra) {
17312
17678
  if (ev.toolName !== void 0) rec.tool = ev.toolName;
17313
17679
  if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
17314
17680
  if (ev.error !== void 0) rec.error = ev.error;
17681
+ if (ev.errorDetail !== void 0) rec.errorDetail = ev.errorDetail;
17315
17682
  if (ev.stats) {
17316
17683
  rec.usage = {
17317
17684
  prompt_tokens: ev.stats.usage.promptTokens,
@@ -18920,6 +19287,7 @@ Skip dependency, build, and VCS directories unless asked (the pinned .gitignore
18920
19287
 
18921
19288
  - **Filesystem tools** (\`read_file\`, \`list_directory\`, \`edit_file\`, etc.): paths resolve against the sandbox root. Relative, POSIX-absolute (\`/\` = project root), and OS-absolute (e.g. \`D:\\\\path\\\\foo.cpp\`) all work as long as they resolve INSIDE the sandbox. Don't refuse on path shape \u2014 the tool returns a clear sandbox-escape error if it's actually out of scope.
18922
19289
  - **\`run_command\`**: cwd pinned to project root. Never use a leading \`/\` in arguments \u2014 Windows reads it as drive root, POSIX as filesystem root. Use relative paths.
19290
+ - By default, run generated scripts from the directory where the script was written. Do not assume an input or data directory is the cwd just because the task reads files there; pass data paths as arguments unless the command explicitly needs that cwd.
18923
19291
 
18924
19292
  # Workspace is pinned
18925
19293
 
@@ -19312,10 +19680,12 @@ export {
19312
19680
  listSessions,
19313
19681
  loadApiKey,
19314
19682
  loadBaseUrl,
19683
+ loadBraveApiKey,
19315
19684
  loadDotenv,
19316
19685
  loadExaApiKey,
19317
19686
  loadHooks,
19318
19687
  loadMetasoApiKey,
19688
+ loadOllamaApiKey,
19319
19689
  loadPerplexityApiKey,
19320
19690
  loadSessionMessages,
19321
19691
  matchesTool,