reasonix 0.37.0 → 0.39.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 (98) hide show
  1. package/README.md +1 -0
  2. package/README.zh-CN.md +1 -0
  3. package/dist/cli/{chat-7257YAPG.js → chat-QCY6CH7O.js} +17 -17
  4. package/dist/cli/{chunk-XQIFIB3U.js → chunk-4D662BWT.js} +2 -2
  5. package/dist/cli/{chunk-SEFXUF24.js → chunk-5ZCRXN7S.js} +160 -29
  6. package/dist/cli/chunk-5ZCRXN7S.js.map +1 -0
  7. package/dist/cli/{chunk-XOIDSPMQ.js → chunk-6DR4F3MC.js} +20 -6
  8. package/dist/cli/chunk-6DR4F3MC.js.map +1 -0
  9. package/dist/cli/{chunk-RFX7TYVV.js → chunk-7G3SESEU.js} +15 -2
  10. package/dist/cli/chunk-7G3SESEU.js.map +1 -0
  11. package/dist/cli/chunk-AFFZF3MW.js +36 -0
  12. package/dist/cli/chunk-AFFZF3MW.js.map +1 -0
  13. package/dist/cli/{chunk-GKZJXYMY.js → chunk-AJGLCSZS.js} +709 -21
  14. package/dist/cli/chunk-AJGLCSZS.js.map +1 -0
  15. package/dist/cli/{chunk-JGZKTAOH.js → chunk-AKDDHHE6.js} +2 -2
  16. package/dist/cli/{chunk-APPB3ZPQ.js → chunk-BQR5TTNY.js} +10 -7
  17. package/dist/cli/chunk-BQR5TTNY.js.map +1 -0
  18. package/dist/cli/{chunk-YER7WCHF.js → chunk-DDA76P44.js} +25 -11
  19. package/dist/cli/chunk-DDA76P44.js.map +1 -0
  20. package/dist/cli/{chunk-T52GAWPP.js → chunk-NLV2YORE.js} +2 -2
  21. package/dist/cli/{chunk-S4GF3HPO.js → chunk-NTVW2TWO.js} +6 -4
  22. package/dist/cli/chunk-NTVW2TWO.js.map +1 -0
  23. package/dist/cli/{chunk-UNMYFZPZ.js → chunk-SJNIIH5W.js} +112 -112
  24. package/dist/cli/chunk-SJNIIH5W.js.map +1 -0
  25. package/dist/cli/{chunk-ZJR4QLXB.js → chunk-SUZRC4NC.js} +2 -2
  26. package/dist/cli/{chunk-BHLHOS5Y.js → chunk-SWLIVNTP.js} +328 -6
  27. package/dist/cli/chunk-SWLIVNTP.js.map +1 -0
  28. package/dist/cli/{chunk-JULZ7JTO.js → chunk-TGO7X47P.js} +97 -16
  29. package/dist/cli/chunk-TGO7X47P.js.map +1 -0
  30. package/dist/cli/{chunk-VF57YX2M.js → chunk-TPDWAMG6.js} +29 -1
  31. package/dist/cli/chunk-TPDWAMG6.js.map +1 -0
  32. package/dist/cli/{chunk-MSKUP6PD.js → chunk-TPK2CHWR.js} +1320 -934
  33. package/dist/cli/chunk-TPK2CHWR.js.map +1 -0
  34. package/dist/cli/{chunk-4Q3GRJIU.js → chunk-V5D77TFD.js} +2 -2
  35. package/dist/cli/{code-64EG5IU2.js → code-3BBVXXY6.js} +28 -20
  36. package/dist/cli/code-3BBVXXY6.js.map +1 -0
  37. package/dist/cli/{commands-FE2UDFBC.js → commands-PJMHSP3Z.js} +3 -4
  38. package/dist/cli/{commands-FE2UDFBC.js.map → commands-PJMHSP3Z.js.map} +1 -1
  39. package/dist/cli/{commit-3IAGB22T.js → commit-R6SC44W5.js} +2 -3
  40. package/dist/cli/{commit-3IAGB22T.js.map → commit-R6SC44W5.js.map} +1 -1
  41. package/dist/cli/{diff-NTEHCSDW.js → diff-LXBBKOZA.js} +17 -9
  42. package/dist/cli/diff-LXBBKOZA.js.map +1 -0
  43. package/dist/cli/{doctor-BW5HSQDW.js → doctor-ZBUEBRXP.js} +9 -7
  44. package/dist/cli/index.js +57 -33
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/{mcp-2RDEQST6.js → mcp-RABKZDX4.js} +12 -4
  47. package/dist/cli/mcp-RABKZDX4.js.map +1 -0
  48. package/dist/cli/{mcp-browse-VM5GLRBQ.js → mcp-browse-H6O73SHN.js} +2 -3
  49. package/dist/cli/{mcp-browse-VM5GLRBQ.js.map → mcp-browse-H6O73SHN.js.map} +1 -1
  50. package/dist/cli/{mcp-inspect-CWSVCZUQ.js → mcp-inspect-XWBO52H6.js} +9 -7
  51. package/dist/cli/mcp-inspect-XWBO52H6.js.map +1 -0
  52. package/dist/cli/{prompt-KGIUONO3.js → prompt-CZSOFYK6.js} +3 -3
  53. package/dist/cli/{replay-D7RT2DR7.js → replay-TWTUIUUB.js} +17 -13
  54. package/dist/cli/replay-TWTUIUUB.js.map +1 -0
  55. package/dist/cli/{run-RWCOA32G.js → run-RWBLIICY.js} +15 -15
  56. package/dist/cli/run-RWBLIICY.js.map +1 -0
  57. package/dist/cli/{server-6ZW4TQUP.js → server-EPU4QONU.js} +21 -19
  58. package/dist/cli/server-EPU4QONU.js.map +1 -0
  59. package/dist/cli/{sessions-5ISNWFMU.js → sessions-TWUFHOUX.js} +9 -10
  60. package/dist/cli/{sessions-5ISNWFMU.js.map → sessions-TWUFHOUX.js.map} +1 -1
  61. package/dist/cli/{setup-HJG23NKJ.js → setup-WHXXHIZV.js} +61 -16
  62. package/dist/cli/setup-WHXXHIZV.js.map +1 -0
  63. package/dist/cli/{version-BXAN7Q4V.js → version-RAMBOIYL.js} +9 -10
  64. package/dist/cli/{version-BXAN7Q4V.js.map → version-RAMBOIYL.js.map} +1 -1
  65. package/dist/index.d.ts +22 -2
  66. package/dist/index.js +927 -58
  67. package/dist/index.js.map +1 -1
  68. package/package.json +4 -1
  69. package/dist/cli/chunk-APPB3ZPQ.js.map +0 -1
  70. package/dist/cli/chunk-BHLHOS5Y.js.map +0 -1
  71. package/dist/cli/chunk-GKZJXYMY.js.map +0 -1
  72. package/dist/cli/chunk-JULZ7JTO.js.map +0 -1
  73. package/dist/cli/chunk-MSKUP6PD.js.map +0 -1
  74. package/dist/cli/chunk-RFX7TYVV.js.map +0 -1
  75. package/dist/cli/chunk-S4GF3HPO.js.map +0 -1
  76. package/dist/cli/chunk-SEFXUF24.js.map +0 -1
  77. package/dist/cli/chunk-UNMYFZPZ.js.map +0 -1
  78. package/dist/cli/chunk-VF57YX2M.js.map +0 -1
  79. package/dist/cli/chunk-WUI3P4RA.js +0 -319
  80. package/dist/cli/chunk-WUI3P4RA.js.map +0 -1
  81. package/dist/cli/chunk-XOIDSPMQ.js.map +0 -1
  82. package/dist/cli/chunk-YER7WCHF.js.map +0 -1
  83. package/dist/cli/code-64EG5IU2.js.map +0 -1
  84. package/dist/cli/diff-NTEHCSDW.js.map +0 -1
  85. package/dist/cli/mcp-2RDEQST6.js.map +0 -1
  86. package/dist/cli/mcp-inspect-CWSVCZUQ.js.map +0 -1
  87. package/dist/cli/replay-D7RT2DR7.js.map +0 -1
  88. package/dist/cli/run-RWCOA32G.js.map +0 -1
  89. package/dist/cli/server-6ZW4TQUP.js.map +0 -1
  90. package/dist/cli/setup-HJG23NKJ.js.map +0 -1
  91. /package/dist/cli/{chat-7257YAPG.js.map → chat-QCY6CH7O.js.map} +0 -0
  92. /package/dist/cli/{chunk-XQIFIB3U.js.map → chunk-4D662BWT.js.map} +0 -0
  93. /package/dist/cli/{chunk-JGZKTAOH.js.map → chunk-AKDDHHE6.js.map} +0 -0
  94. /package/dist/cli/{chunk-T52GAWPP.js.map → chunk-NLV2YORE.js.map} +0 -0
  95. /package/dist/cli/{chunk-ZJR4QLXB.js.map → chunk-SUZRC4NC.js.map} +0 -0
  96. /package/dist/cli/{chunk-4Q3GRJIU.js.map → chunk-V5D77TFD.js.map} +0 -0
  97. /package/dist/cli/{doctor-BW5HSQDW.js.map → doctor-ZBUEBRXP.js.map} +0 -0
  98. /package/dist/cli/{prompt-KGIUONO3.js.map → prompt-CZSOFYK6.js.map} +0 -0
package/README.md CHANGED
@@ -162,6 +162,7 @@ For live cache-hit rates, costs, and methodology, see [`benchmarks/`](./benchmar
162
162
  ## Documentation
163
163
 
164
164
  - [**Architecture**](./docs/ARCHITECTURE.md) — three pillars: cache-first loop, tool-call repair, cost control
165
+ - [**CLI Reference**](./docs/CLI-REFERENCE.md) — every shell subcommand, every slash command, every keybinding
165
166
  - [**Benchmarks**](./benchmarks/) — τ-bench-lite harness, transcripts, cost methodology
166
167
  - [**Website**](https://esengine.github.io/DeepSeek-Reasonix/) — getting started, dashboard mockup, TUI mockup
167
168
  - [**Contributing**](./CONTRIBUTING.md) — comment policy, error-handling rules, library-over-hand-rolled
package/README.zh-CN.md CHANGED
@@ -157,6 +157,7 @@ npx reasonix code --dir /path/to/project
157
157
  ## 文档
158
158
 
159
159
  - [**架构**](./docs/ARCHITECTURE.md) —— 四大支柱、缓存优先循环、思维提取、脚手架
160
+ - [**CLI 参考**](./docs/CLI-REFERENCE.md) —— 每个 shell 子命令、每个 slash 命令、每个快捷键
160
161
  - [**基准测试**](./benchmarks/) —— τ-bench-lite harness、transcript、成本方法论
161
162
  - [**官方网站**](https://esengine.github.io/DeepSeek-Reasonix/) —— 入门、Dashboard 设计稿、TUI 设计稿
162
163
  - [**贡献指南**](./CONTRIBUTING.md) —— 注释规则、错误处理、用现成库不手写
@@ -1,42 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  chatCommand
4
- } from "./chunk-MSKUP6PD.js";
4
+ } from "./chunk-TPK2CHWR.js";
5
5
  import "./chunk-BQNUJJN7.js";
6
- import "./chunk-RFX7TYVV.js";
6
+ import "./chunk-7G3SESEU.js";
7
7
  import "./chunk-MRLXEMZ7.js";
8
8
  import "./chunk-CPOV2O73.js";
9
- import "./chunk-SEFXUF24.js";
10
- import "./chunk-UNMYFZPZ.js";
9
+ import "./chunk-5ZCRXN7S.js";
10
+ import "./chunk-SJNIIH5W.js";
11
11
  import "./chunk-XJLZ4HKU.js";
12
12
  import "./chunk-XHQIK7B6.js";
13
- import "./chunk-YER7WCHF.js";
14
- import "./chunk-T52GAWPP.js";
15
- import "./chunk-ZJR4QLXB.js";
13
+ import "./chunk-DDA76P44.js";
14
+ import "./chunk-NLV2YORE.js";
15
+ import "./chunk-SUZRC4NC.js";
16
16
  import "./chunk-MHDNZXJJ.js";
17
- import "./chunk-JULZ7JTO.js";
17
+ import "./chunk-TGO7X47P.js";
18
+ import "./chunk-AFFZF3MW.js";
18
19
  import "./chunk-DAEAAVDF.js";
19
20
  import "./chunk-KMWKGPFZ.js";
20
21
  import "./chunk-3Q3C4W66.js";
21
22
  import "./chunk-4DCHFFEY.js";
22
23
  import "./chunk-WJ3YX4PZ.js";
23
- import "./chunk-VF57YX2M.js";
24
+ import "./chunk-TPDWAMG6.js";
24
25
  import "./chunk-SOZE7V7V.js";
25
26
  import "./chunk-6NMWJSES.js";
26
- import "./chunk-S4GF3HPO.js";
27
- import "./chunk-XOIDSPMQ.js";
27
+ import "./chunk-NTVW2TWO.js";
28
+ import "./chunk-6DR4F3MC.js";
28
29
  import "./chunk-FM57FNPJ.js";
29
- import "./chunk-XQIFIB3U.js";
30
- import "./chunk-JGZKTAOH.js";
30
+ import "./chunk-4D662BWT.js";
31
+ import "./chunk-AKDDHHE6.js";
31
32
  import "./chunk-5X7LZJDE.js";
32
33
  import "./chunk-6CXT5JRM.js";
33
- import "./chunk-GKZJXYMY.js";
34
- import "./chunk-BHLHOS5Y.js";
35
- import "./chunk-WUI3P4RA.js";
34
+ import "./chunk-AJGLCSZS.js";
35
+ import "./chunk-SWLIVNTP.js";
36
36
  import "./chunk-ZTLZO42A.js";
37
37
  import "./chunk-ORM6PK57.js";
38
38
  import "./chunk-CRPQUBP6.js";
39
39
  export {
40
40
  chatCommand
41
41
  };
42
- //# sourceMappingURL=chat-7257YAPG.js.map
42
+ //# sourceMappingURL=chat-QCY6CH7O.js.map
@@ -7,7 +7,7 @@ import {
7
7
  compileFilters,
8
8
  defaultIndexConfig,
9
9
  resolveSemanticEmbeddingConfig
10
- } from "./chunk-BHLHOS5Y.js";
10
+ } from "./chunk-SWLIVNTP.js";
11
11
 
12
12
  // src/index/semantic/builder.ts
13
13
  import { promises as fs3 } from "fs";
@@ -948,4 +948,4 @@ export {
948
948
  startOllamaDaemon,
949
949
  pullOllamaModel
950
950
  };
951
- //# sourceMappingURL=chunk-XQIFIB3U.js.map
951
+ //# sourceMappingURL=chunk-4D662BWT.js.map
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  MemoryStore,
4
4
  sanitizeMemoryName
5
- } from "./chunk-YER7WCHF.js";
5
+ } from "./chunk-DDA76P44.js";
6
6
  import {
7
7
  countTokens,
8
8
  estimateConversationTokens,
@@ -13,15 +13,15 @@ import {
13
13
  } from "./chunk-KMWKGPFZ.js";
14
14
  import {
15
15
  pauseGate
16
- } from "./chunk-S4GF3HPO.js";
16
+ } from "./chunk-NTVW2TWO.js";
17
17
  import {
18
18
  NEGATIVE_CLAIM_RULE,
19
19
  TUI_FORMATTING_RULES
20
- } from "./chunk-XOIDSPMQ.js";
20
+ } from "./chunk-6DR4F3MC.js";
21
21
  import {
22
22
  formatHookOutcomeMessage,
23
23
  runHooks
24
- } from "./chunk-JGZKTAOH.js";
24
+ } from "./chunk-AKDDHHE6.js";
25
25
  import {
26
26
  ignoredByLayers,
27
27
  loadGitignoreAt,
@@ -36,12 +36,12 @@ import {
36
36
  } from "./chunk-6CXT5JRM.js";
37
37
  import {
38
38
  t
39
- } from "./chunk-GKZJXYMY.js";
39
+ } from "./chunk-AJGLCSZS.js";
40
40
  import {
41
41
  DEFAULT_INDEX_EXCLUDES,
42
42
  webSearchEndpoint,
43
43
  webSearchEngine
44
- } from "./chunk-BHLHOS5Y.js";
44
+ } from "./chunk-SWLIVNTP.js";
45
45
  import {
46
46
  DEEPSEEK_CONTEXT_TOKENS,
47
47
  DEFAULT_CONTEXT_TOKENS,
@@ -287,6 +287,8 @@ var ToolRegistry = class {
287
287
  _interceptor = null;
288
288
  _auditListener = null;
289
289
  _resultAugmenter = null;
290
+ /** Per-tool fingerprint of the last call that failed schema validation. Cleared by any successful validation for that tool. */
291
+ _lastMalformed = /* @__PURE__ */ new Map();
290
292
  constructor(opts = {}) {
291
293
  this._autoFlatten = opts.autoFlatten !== false;
292
294
  }
@@ -309,6 +311,10 @@ var ToolRegistry = class {
309
311
  setResultAugmenter(fn) {
310
312
  this._resultAugmenter = fn;
311
313
  }
314
+ /** True when an augmenter is already wired — lets late-installing callers skip clobbering an earlier one. */
315
+ get hasResultAugmenter() {
316
+ return this._resultAugmenter !== null;
317
+ }
312
318
  register(def) {
313
319
  if (!def.name) throw new Error("tool requires a name");
314
320
  const internal = { ...def };
@@ -357,17 +363,29 @@ var ToolRegistry = class {
357
363
  if (!tool) {
358
364
  return JSON.stringify({ error: `unknown tool: ${name}` });
359
365
  }
366
+ const fingerprint = fingerprintArgs(argumentsRaw);
360
367
  let args;
361
368
  try {
362
369
  args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
363
370
  } catch (err) {
364
- return JSON.stringify({
365
- error: `invalid tool arguments JSON: ${err.message}`
366
- });
371
+ return this._noteMalformed(
372
+ name,
373
+ fingerprint,
374
+ `invalid tool arguments JSON: ${err.message}`
375
+ );
367
376
  }
368
377
  if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
369
378
  args = nestArguments(args);
370
379
  }
380
+ const missing = tool.parameters ? missingRequiredParam(tool.parameters, args) : null;
381
+ if (missing) {
382
+ return this._noteMalformed(
383
+ name,
384
+ fingerprint,
385
+ `missing required parameter "${missing}". Retry with all required parameters filled.`
386
+ );
387
+ }
388
+ this._lastMalformed.delete(name);
371
389
  if (this._planMode && !isReadOnlyCall(tool, args)) {
372
390
  return JSON.stringify({
373
391
  error: `${name}: unavailable in plan mode \u2014 this is a read-only exploration phase. Use read_file / list_directory / search_files / directory_tree / web_search / allowlisted shell commands to investigate. Call submit_plan with your proposed plan when you're ready for the user's review.`,
@@ -423,6 +441,18 @@ var ToolRegistry = class {
423
441
  }
424
442
  return finalResult;
425
443
  }
444
+ /** Records the failed call's fingerprint; on the 2nd consecutive identical malformed call to the same tool, returns a sharper error that tells the model to stop retrying. */
445
+ _noteMalformed(name, fingerprint, detail) {
446
+ const prev = this._lastMalformed.get(name);
447
+ this._lastMalformed.set(name, fingerprint);
448
+ if (prev === fingerprint) {
449
+ return JSON.stringify({
450
+ error: `${name}: same call just failed validation (${detail}) \u2014 DO NOT retry with identical args. Either fix the call (read the schema in the tool spec) or pick a different tool.`,
451
+ consecutiveMalformed: true
452
+ });
453
+ }
454
+ return JSON.stringify({ error: `${name}: ${detail}` });
455
+ }
426
456
  };
427
457
  function isReadOnlyCall(tool, args) {
428
458
  if (tool.readOnlyCheck) {
@@ -440,6 +470,22 @@ function hasDotKey(obj) {
440
470
  }
441
471
  return false;
442
472
  }
473
+ function fingerprintArgs(argumentsRaw) {
474
+ if (typeof argumentsRaw === "string") return argumentsRaw;
475
+ try {
476
+ return JSON.stringify(argumentsRaw);
477
+ } catch {
478
+ return "";
479
+ }
480
+ }
481
+ function missingRequiredParam(schema, args) {
482
+ const required = schema.required;
483
+ if (!required || required.length === 0) return null;
484
+ for (const key of required) {
485
+ if (args[key] === void 0) return key;
486
+ }
487
+ return null;
488
+ }
443
489
 
444
490
  // src/memory/runtime.ts
445
491
  import { createHash } from "crypto";
@@ -1375,6 +1421,7 @@ function signature(call) {
1375
1421
 
1376
1422
  // src/loop.ts
1377
1423
  var ESCALATION_MODEL = "deepseek-v4-pro";
1424
+ var PARENT_BUDGET_WARN_THRESHOLD = 5;
1378
1425
  var CacheFirstLoop = class {
1379
1426
  client;
1380
1427
  prefix;
@@ -1411,6 +1458,7 @@ var CacheFirstLoop = class {
1411
1458
  _turnFailures = new TurnFailureTracker();
1412
1459
  _turnSelfCorrected = false;
1413
1460
  _foldedThisTurn = false;
1461
+ _toolDispatchesThisStep = 0;
1414
1462
  context;
1415
1463
  /** Subscribe API so UI hooks can derive `running` from finally-guaranteed insertions. */
1416
1464
  get inflight() {
@@ -1465,6 +1513,23 @@ var CacheFirstLoop = class {
1465
1513
  stormThreshold: parsePositiveIntEnv(process.env.REASONIX_STORM_THRESHOLD),
1466
1514
  stormWindow: parsePositiveIntEnv(process.env.REASONIX_STORM_WINDOW)
1467
1515
  });
1516
+ if (!this.tools.hasResultAugmenter) {
1517
+ this.tools.setResultAugmenter((_name, _args, result) => {
1518
+ this._toolDispatchesThisStep++;
1519
+ const remaining = this.maxToolIters - this._toolDispatchesThisStep;
1520
+ if (remaining <= 0) {
1521
+ return `${result}
1522
+
1523
+ [budget: 0 of ${this.maxToolIters} tool calls left this turn \u2014 finalize NOW; the next iter forces a summary]`;
1524
+ }
1525
+ if (remaining <= PARENT_BUDGET_WARN_THRESHOLD) {
1526
+ return `${result}
1527
+
1528
+ [budget: ${remaining} of ${this.maxToolIters} tool calls left this turn \u2014 wrap up soon]`;
1529
+ }
1530
+ return result;
1531
+ });
1532
+ }
1468
1533
  this.sessionName = opts.session ?? null;
1469
1534
  if (this.sessionName) {
1470
1535
  const prior = loadSessionMessages(this.sessionName);
@@ -1718,6 +1783,7 @@ ${reason}`
1718
1783
  this._turnSelfCorrected = false;
1719
1784
  this._escalateThisTurn = false;
1720
1785
  this._foldedThisTurn = false;
1786
+ this._toolDispatchesThisStep = 0;
1721
1787
  let armedConsumed = false;
1722
1788
  if (this._proArmedForNextTurn) {
1723
1789
  this._escalateThisTurn = true;
@@ -3129,6 +3195,74 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3129
3195
  return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
3130
3196
  }
3131
3197
  });
3198
+ registry.register({
3199
+ name: "delete_file",
3200
+ description: "Delete one file under the sandbox root. Refuses directories \u2014 use delete_directory for those. Errors if the path doesn't exist.",
3201
+ parameters: {
3202
+ type: "object",
3203
+ properties: { path: { type: "string" } },
3204
+ required: ["path"]
3205
+ },
3206
+ fn: async (args) => {
3207
+ const abs = safePath(args.path);
3208
+ const st = await fs4.lstat(abs);
3209
+ if (st.isDirectory()) {
3210
+ throw new Error(
3211
+ `delete_file: ${args.path} is a directory \u2014 use delete_directory to remove it`
3212
+ );
3213
+ }
3214
+ await fs4.unlink(abs);
3215
+ return `deleted ${displayRel4(rootDir, abs)}`;
3216
+ }
3217
+ });
3218
+ registry.register({
3219
+ name: "delete_directory",
3220
+ description: "Recursively delete a directory under the sandbox root. Pass `recursive:false` to refuse non-empty directories. Errors if the path doesn't exist.",
3221
+ parameters: {
3222
+ type: "object",
3223
+ properties: {
3224
+ path: { type: "string" },
3225
+ recursive: {
3226
+ type: "boolean",
3227
+ description: "When true (default) deletes the directory and all its contents. When false, only removes empty directories \u2014 non-empty refuses with an error."
3228
+ }
3229
+ },
3230
+ required: ["path"]
3231
+ },
3232
+ fn: async (args) => {
3233
+ const abs = safePath(args.path);
3234
+ const st = await fs4.lstat(abs);
3235
+ if (!st.isDirectory()) {
3236
+ throw new Error(`delete_directory: ${args.path} is a file \u2014 use delete_file to remove it`);
3237
+ }
3238
+ const recursive = args.recursive !== false;
3239
+ if (recursive) {
3240
+ await fs4.rm(abs, { recursive: true, force: false });
3241
+ } else {
3242
+ await fs4.rmdir(abs);
3243
+ }
3244
+ return `deleted ${displayRel4(rootDir, abs)}/${recursive ? " (recursive)" : ""}`;
3245
+ }
3246
+ });
3247
+ registry.register({
3248
+ name: "copy_file",
3249
+ description: "Copy a file or directory under the sandbox root. Both source and destination resolve under the sandbox. Parent directories of the destination are created as needed. Refuses to overwrite an existing destination \u2014 delete it first if you want to replace it.",
3250
+ parameters: {
3251
+ type: "object",
3252
+ properties: {
3253
+ source: { type: "string" },
3254
+ destination: { type: "string" }
3255
+ },
3256
+ required: ["source", "destination"]
3257
+ },
3258
+ fn: async (args) => {
3259
+ const src = safePath(args.source);
3260
+ const dst = safePath(args.destination);
3261
+ await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
3262
+ await fs4.cp(src, dst, { recursive: true, force: false, errorOnExist: true });
3263
+ return `copied ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
3264
+ }
3265
+ });
3132
3266
  return registry;
3133
3267
  }
3134
3268
 
@@ -3343,7 +3477,7 @@ function registerChoiceTool(registry, opts = {}) {
3343
3477
 
3344
3478
  // src/tools/plan-core.ts
3345
3479
  var SUBMIT_PLAN_DESCRIPTION = "Submit ONE concrete plan you've already decided on. Use this for tasks that warrant a review gate \u2014 multi-file refactors, architecture changes, anything that would be expensive or confusing to undo. Skip it for small fixes (one-line typo, obvious bug with a clear fix) \u2014 just make the change. The user will either approve (you then implement it), ask for refinement, or cancel. If the user has already enabled /plan mode, writes are blocked at dispatch and you MUST use this. CRITICAL: do NOT use submit_plan to present alternative routes (A/B/C, option 1/2/3) for the user to pick from \u2014 the picker only exposes approve/refine/cancel, so a menu plan strands the user with no way to choose. For branching decisions, call `ask_choice` instead; only call submit_plan once the user has picked a direction and you have a single actionable plan. Write the plan as markdown with a one-line summary, a bulleted list of files to touch and what will change, and any risks or open questions. STRONGLY PREFERRED: pass `steps` \u2014 an array of {id, title, action, risk?} \u2014 so the UI renders a structured step list above the approval picker and tracks per-step progress. Use risk='high' for steps that touch prod data / break public APIs / are hard to undo; 'med' for non-trivial but reversible (multi-file edits, schema tweaks); 'low' for safe local work. After each step, call `mark_step_complete` so the user sees progress ticks.";
3346
- var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one step of the approved plan as done. Call this after finishing each step, then immediately continue with the NEXT step \u2014 do not stop or wait for the user. The TUI updates the plan card's progress in place. After the FINAL step, write a brief reply summarizing what was done and end the turn. Pass the `stepId` from the plan's steps array, a short `result` (what you did), and optional `notes` for anything surprising (errors, scope changes, follow-ups). This tool doesn't change any files. Don't call it if the plan didn't include structured steps, and don't invent ids that weren't in the original plan.";
3480
+ var MARK_STEP_COMPLETE_DESCRIPTION = "Mark one step of the approved plan as done. MANDATORY: call this exactly once after finishing each step, before starting the next one \u2014 skipping it leaves the user staring at `0/N done` on the resume banner even when the work is finished, and they have no way to know which steps actually ran. The TUI updates the plan card's progress in place; the count is persisted to disk so it survives session resume. After the FINAL step, write a brief reply summarizing what was done and end the turn. Pass the `stepId` from the plan's steps array, a short `result` (what you did), and optional `notes` for anything surprising (errors, scope changes, follow-ups). This tool doesn't change any files. Don't call it if the plan didn't include structured steps, and don't invent ids that weren't in the original plan. If you only realized at the end that you skipped marking steps, mark them then \u2014 late is still better than never.";
3347
3481
  var REVISE_PLAN_DESCRIPTION = "Surgically replace the REMAINING steps of an in-flight plan. Call this when the user has given feedback at a checkpoint that warrants a structured plan change \u2014 skip a step, swap two steps, add a new step, change risk, etc. Pass: `reason` (one sentence why), `remainingSteps` (the new tail of the plan, replacing whatever steps haven't been done yet), and optional `summary` (updated one-line plan summary). Done steps are NEVER touched \u2014 keep them out of `remainingSteps`. The TUI shows a diff (removed in red, kept in gray, added in green) and the user accepts or rejects. Don't call this for trivial mid-step adjustments \u2014 just keep executing. Don't call submit_plan for revisions either \u2014 that resets the whole plan including completed steps. Use submit_plan only when the entire approach has changed; use revise_plan when the tail needs editing.";
3348
3482
  var STEP_ITEM_SCHEMA = {
3349
3483
  type: "object",
@@ -3667,16 +3801,19 @@ async function searchMojeek(query, opts = {}) {
3667
3801
  signal: opts.signal,
3668
3802
  redirect: "follow"
3669
3803
  });
3670
- if (!resp.ok) throw new Error(`web_search ${resp.status}`);
3804
+ if (!resp.ok) throw new Error(t("webErrors.status", { status: resp.status }));
3671
3805
  const html = await resp.text();
3672
3806
  const results = parseMojeekResults(html).slice(0, topK);
3673
3807
  if (results.length === 0) {
3674
3808
  if (/no results found|did not match any documents/i.test(html)) return [];
3675
3809
  if (/captcha|verify you are human|access denied|forbidden/i.test(html)) {
3676
- throw new Error("web_search: Mojeek anti-bot page \u2014 rate-limited or blocked");
3810
+ throw new Error(t("webErrors.mojeekBlocked"));
3677
3811
  }
3678
3812
  throw new Error(
3679
- `web_search: 0 results but response doesn't look like a real empty page (${html.length} chars, first 120: ${html.slice(0, 120).replace(/\s+/g, " ")})`
3813
+ t("webErrors.mojeekNoResults", {
3814
+ chars: html.length,
3815
+ preview: html.slice(0, 120).replace(/\s+/g, " ")
3816
+ })
3680
3817
  );
3681
3818
  }
3682
3819
  return results;
@@ -3686,10 +3823,10 @@ function normalizeSearxngEndpoint(raw) {
3686
3823
  try {
3687
3824
  url = new URL(raw.includes("://") ? raw : `http://${raw}`);
3688
3825
  } catch {
3689
- throw new Error(`web_search: invalid SearXNG endpoint "${raw}"`);
3826
+ throw new Error(t("webErrors.invalidEndpoint", { endpoint: raw }));
3690
3827
  }
3691
3828
  if (url.protocol !== "http:" && url.protocol !== "https:") {
3692
- throw new Error(`web_search: SearXNG endpoint must be http(s), got ${url.protocol}`);
3829
+ throw new Error(t("webErrors.endpointMustBeHttp", { protocol: url.protocol }));
3693
3830
  }
3694
3831
  return url.origin;
3695
3832
  }
@@ -3709,19 +3846,17 @@ async function searchSearxng(query, opts = {}) {
3709
3846
  } catch (err) {
3710
3847
  if (err instanceof TypeError && err.message.includes("fetch")) {
3711
3848
  throw new Error(
3712
- `web_search: Cannot reach SearXNG server at ${opts.endpoint ?? "http://localhost:8080"}. Please install SearXNG (https://github.com/searxng/searxng) and start it (e.g. \`docker run -d -p 8080:8080 searxng/searxng\`), or switch to the default engine with /search-engine mojeek.`
3849
+ t("webErrors.cannotReach", { endpoint: opts.endpoint ?? "http://localhost:8080" })
3713
3850
  );
3714
3851
  }
3715
3852
  throw err;
3716
3853
  }
3717
- if (!resp.ok) throw new Error(`web_search ${resp.status}`);
3854
+ if (!resp.ok) throw new Error(t("webErrors.status", { status: resp.status }));
3718
3855
  const html = await resp.text();
3719
3856
  const results = parseSearxngHtmlResults(html).slice(0, topK);
3720
3857
  if (results.length === 0) {
3721
3858
  if (/no results found|did not match any documents/i.test(html)) return [];
3722
- throw new Error(
3723
- `web_search: 0 results but SearXNG response doesn't look like an empty results page (${html.length} chars)`
3724
- );
3859
+ throw new Error(t("webErrors.searxngNoResults", { chars: html.length }));
3725
3860
  }
3726
3861
  return results;
3727
3862
  }
@@ -3815,13 +3950,11 @@ async function webFetch(url, opts = {}) {
3815
3950
  clearTimeout(timer);
3816
3951
  opts.signal?.removeEventListener("abort", cancel);
3817
3952
  }
3818
- if (!resp.ok) throw new Error(`web_fetch ${resp.status} for ${url}`);
3953
+ if (!resp.ok) throw new Error(t("webErrors.fetchStatus", { status: resp.status, url }));
3819
3954
  const contentType = resp.headers.get("content-type") ?? "";
3820
3955
  const declaredLen = Number(resp.headers.get("content-length") ?? "");
3821
3956
  if (Number.isFinite(declaredLen) && declaredLen > FETCH_MAX_BYTES) {
3822
- throw new Error(
3823
- `web_fetch refused: content-length ${declaredLen} bytes exceeds ${FETCH_MAX_BYTES}-byte cap (${url})`
3824
- );
3957
+ throw new Error(t("webErrors.fetchTooLarge", { len: declaredLen, cap: FETCH_MAX_BYTES, url }));
3825
3958
  }
3826
3959
  const raw = await readBodyCapped(resp, FETCH_MAX_BYTES);
3827
3960
  const title = extractTitle(raw);
@@ -3848,9 +3981,7 @@ async function readBodyCapped(resp, maxBytes) {
3848
3981
  await reader.cancel();
3849
3982
  } catch {
3850
3983
  }
3851
- throw new Error(
3852
- `web_fetch refused: response body exceeded ${maxBytes}-byte cap (${total} bytes seen)`
3853
- );
3984
+ throw new Error(t("webErrors.fetchBodyTooLarge", { cap: maxBytes, seen: total }));
3854
3985
  }
3855
3986
  out += decoder.decode(value, { stream: true });
3856
3987
  }
@@ -3978,7 +4109,7 @@ function registerWebTools(registry, opts = {}) {
3978
4109
  },
3979
4110
  fn: async (args, ctx) => {
3980
4111
  if (!/^https?:\/\//i.test(args.url)) {
3981
- throw new Error("web_fetch: url must start with http:// or https://");
4112
+ throw new Error(t("webErrors.fetchInvalidUrl"));
3982
4113
  }
3983
4114
  const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
3984
4115
  const header = page.title ? `${page.title}
@@ -5077,4 +5208,4 @@ export {
5077
5208
  snapshotBeforeEdits,
5078
5209
  restoreSnapshots
5079
5210
  };
5080
- //# sourceMappingURL=chunk-SEFXUF24.js.map
5211
+ //# sourceMappingURL=chunk-5ZCRXN7S.js.map