reasonix 0.32.0 → 0.33.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.
- package/dist/cli/chat-EIFLHBZ6.js +39 -0
- package/dist/cli/chunk-2AWTGJ2C.js +110 -0
- package/dist/cli/chunk-2AWTGJ2C.js.map +1 -0
- package/dist/cli/chunk-3Q3C4W66.js +30 -0
- package/dist/cli/chunk-3Q3C4W66.js.map +1 -0
- package/dist/cli/chunk-4DCHFFEY.js +149 -0
- package/dist/cli/chunk-4DCHFFEY.js.map +1 -0
- package/dist/cli/chunk-5X7LZJDE.js +36 -0
- package/dist/cli/chunk-5X7LZJDE.js.map +1 -0
- package/dist/cli/chunk-6TMHAK5D.js +576 -0
- package/dist/cli/chunk-6TMHAK5D.js.map +1 -0
- package/dist/cli/chunk-APPB3ZPQ.js +43 -0
- package/dist/cli/chunk-APPB3ZPQ.js.map +1 -0
- package/dist/cli/chunk-BQNUJJN7.js +42 -0
- package/dist/cli/chunk-BQNUJJN7.js.map +1 -0
- package/dist/cli/chunk-CPOV2O73.js +39 -0
- package/dist/cli/chunk-CPOV2O73.js.map +1 -0
- package/dist/cli/chunk-D5DKXIP5.js +368 -0
- package/dist/cli/chunk-D5DKXIP5.js.map +1 -0
- package/dist/cli/chunk-DFP4YSVM.js +247 -0
- package/dist/cli/chunk-DFP4YSVM.js.map +1 -0
- package/dist/cli/chunk-DULSP7JH.js +410 -0
- package/dist/cli/chunk-DULSP7JH.js.map +1 -0
- package/dist/cli/chunk-FM57FNPJ.js +46 -0
- package/dist/cli/chunk-FM57FNPJ.js.map +1 -0
- package/dist/cli/chunk-FWGEHRB7.js +54 -0
- package/dist/cli/chunk-FWGEHRB7.js.map +1 -0
- package/dist/cli/chunk-FXGQ5NHE.js +513 -0
- package/dist/cli/chunk-FXGQ5NHE.js.map +1 -0
- package/dist/cli/chunk-G3XNWSFN.js +53 -0
- package/dist/cli/chunk-G3XNWSFN.js.map +1 -0
- package/dist/cli/chunk-I6YIAK6C.js +757 -0
- package/dist/cli/chunk-I6YIAK6C.js.map +1 -0
- package/dist/cli/chunk-J5VLP23S.js +94 -0
- package/dist/cli/chunk-J5VLP23S.js.map +1 -0
- package/dist/cli/chunk-KMWKGPFZ.js +303 -0
- package/dist/cli/chunk-KMWKGPFZ.js.map +1 -0
- package/dist/cli/chunk-LVQX5KGF.js +14934 -0
- package/dist/cli/chunk-LVQX5KGF.js.map +1 -0
- package/dist/cli/chunk-MHDNZXJJ.js +48 -0
- package/dist/cli/chunk-MHDNZXJJ.js.map +1 -0
- package/dist/cli/chunk-ORM6PK57.js +140 -0
- package/dist/cli/chunk-ORM6PK57.js.map +1 -0
- package/dist/cli/chunk-Q5GRLZJF.js +99 -0
- package/dist/cli/chunk-Q5GRLZJF.js.map +1 -0
- package/dist/cli/chunk-Q6YFXW7H.js +4986 -0
- package/dist/cli/chunk-Q6YFXW7H.js.map +1 -0
- package/dist/cli/chunk-QGE6AF76.js +1467 -0
- package/dist/cli/chunk-QGE6AF76.js.map +1 -0
- package/dist/cli/chunk-RFX7TYVV.js +28 -0
- package/dist/cli/chunk-RFX7TYVV.js.map +1 -0
- package/dist/cli/chunk-RZILUXUC.js +940 -0
- package/dist/cli/chunk-RZILUXUC.js.map +1 -0
- package/dist/cli/chunk-SDE5U32Z.js +535 -0
- package/dist/cli/chunk-SDE5U32Z.js.map +1 -0
- package/dist/cli/chunk-SOZE7V7V.js +340 -0
- package/dist/cli/chunk-SOZE7V7V.js.map +1 -0
- package/dist/cli/chunk-U3V2ZQ5J.js +479 -0
- package/dist/cli/chunk-U3V2ZQ5J.js.map +1 -0
- package/dist/cli/chunk-W4LDFAZ6.js +1544 -0
- package/dist/cli/chunk-W4LDFAZ6.js.map +1 -0
- package/dist/cli/chunk-WBDE4IRI.js +208 -0
- package/dist/cli/chunk-WBDE4IRI.js.map +1 -0
- package/dist/cli/chunk-XHQIK7B6.js +189 -0
- package/dist/cli/chunk-XHQIK7B6.js.map +1 -0
- package/dist/cli/chunk-XJLZ4HKU.js +307 -0
- package/dist/cli/chunk-XJLZ4HKU.js.map +1 -0
- package/dist/cli/chunk-ZPTSJGX5.js +88 -0
- package/dist/cli/chunk-ZPTSJGX5.js.map +1 -0
- package/dist/cli/chunk-ZTLZO42A.js +231 -0
- package/dist/cli/chunk-ZTLZO42A.js.map +1 -0
- package/dist/cli/code-F4KJOE3K.js +151 -0
- package/dist/cli/code-F4KJOE3K.js.map +1 -0
- package/dist/cli/commands-JWT2MWVH.js +352 -0
- package/dist/cli/commands-JWT2MWVH.js.map +1 -0
- package/dist/cli/commit-RPZBOZS2.js +288 -0
- package/dist/cli/commit-RPZBOZS2.js.map +1 -0
- package/dist/cli/diff-NTEHCSDW.js +145 -0
- package/dist/cli/diff-NTEHCSDW.js.map +1 -0
- package/dist/cli/doctor-3TGB2NZN.js +19 -0
- package/dist/cli/doctor-3TGB2NZN.js.map +1 -0
- package/dist/cli/events-P27CX7LN.js +338 -0
- package/dist/cli/events-P27CX7LN.js.map +1 -0
- package/dist/cli/index.js +80 -33693
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-ARTNQ24O.js +266 -0
- package/dist/cli/mcp-ARTNQ24O.js.map +1 -0
- package/dist/cli/mcp-browse-HLO2ENDL.js +163 -0
- package/dist/cli/mcp-browse-HLO2ENDL.js.map +1 -0
- package/dist/cli/mcp-inspect-T2HBR22P.js +103 -0
- package/dist/cli/mcp-inspect-T2HBR22P.js.map +1 -0
- package/dist/cli/{prompt-XHICFAYN.js → prompt-V47QKSAR.js} +3 -2
- package/dist/cli/prompt-V47QKSAR.js.map +1 -0
- package/dist/cli/prune-sessions-ERL6B4G5.js +42 -0
- package/dist/cli/prune-sessions-ERL6B4G5.js.map +1 -0
- package/dist/cli/replay-TMJASRC4.js +273 -0
- package/dist/cli/replay-TMJASRC4.js.map +1 -0
- package/dist/cli/run-JMEOTQCG.js +215 -0
- package/dist/cli/run-JMEOTQCG.js.map +1 -0
- package/dist/cli/server-SYC3OVOP.js +2967 -0
- package/dist/cli/server-SYC3OVOP.js.map +1 -0
- package/dist/cli/sessions-MOJAALJI.js +102 -0
- package/dist/cli/sessions-MOJAALJI.js.map +1 -0
- package/dist/cli/setup-CCJZAWTY.js +404 -0
- package/dist/cli/setup-CCJZAWTY.js.map +1 -0
- package/dist/cli/stats-5RJCATCE.js +12 -0
- package/dist/cli/stats-5RJCATCE.js.map +1 -0
- package/dist/cli/update-4TJWRUIN.js +90 -0
- package/dist/cli/update-4TJWRUIN.js.map +1 -0
- package/dist/cli/version-3MYFE4G6.js +29 -0
- package/dist/cli/version-3MYFE4G6.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +493 -89
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-VWFJNLIK.js +0 -1031
- package/dist/cli/chunk-VWFJNLIK.js.map +0 -1
- /package/dist/cli/{prompt-XHICFAYN.js.map → chat-EIFLHBZ6.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1032,7 +1032,10 @@ var EN = {
|
|
|
1032
1032
|
restoreInfo: '\u25B8 restored "{name}" ({id}) from {when}',
|
|
1033
1033
|
restoreWrote: " \xB7 wrote back {count} file{s}",
|
|
1034
1034
|
restoreRemoved: " \xB7 removed {count} file{s} (didn't exist at checkpoint time)",
|
|
1035
|
-
restoreSkipped: " \u2717 {count} file{s} skipped:"
|
|
1035
|
+
restoreSkipped: " \u2717 {count} file{s} skipped:",
|
|
1036
|
+
cwdCodeOnly: "/cwd is only available inside `reasonix code`.",
|
|
1037
|
+
cwdUsage: "usage: /cwd <path> (current root: {current}). Re-points filesystem / shell / memory tools to <path>.",
|
|
1038
|
+
cwdUsageNoCurrent: "usage: /cwd <path> re-points the workspace root to <path>."
|
|
1036
1039
|
},
|
|
1037
1040
|
model: {
|
|
1038
1041
|
modelHint: "try deepseek-v4-flash or deepseek-v4-pro \u2014 run /models to fetch the live list",
|
|
@@ -1726,7 +1729,10 @@ var zhCN = {
|
|
|
1726
1729
|
restoreInfo: '\u25B8 \u5DF2\u6062\u590D "{name}"\uFF08{id}\uFF09\uFF0C\u6765\u81EA {when}',
|
|
1727
1730
|
restoreWrote: " \xB7 \u5199\u56DE\u4E86 {count} \u4E2A\u6587\u4EF6",
|
|
1728
1731
|
restoreRemoved: " \xB7 \u79FB\u9664\u4E86 {count} \u4E2A\u6587\u4EF6\uFF08\u68C0\u67E5\u70B9\u65F6\u4E0D\u5B58\u5728\uFF09",
|
|
1729
|
-
restoreSkipped: " \u2717 \u8DF3\u8FC7\u4E86 {count} \u4E2A\u6587\u4EF6\uFF1A"
|
|
1732
|
+
restoreSkipped: " \u2717 \u8DF3\u8FC7\u4E86 {count} \u4E2A\u6587\u4EF6\uFF1A",
|
|
1733
|
+
cwdCodeOnly: "/cwd \u4EC5\u5728 `reasonix code` \u4E2D\u53EF\u7528\u3002",
|
|
1734
|
+
cwdUsage: "\u7528\u6CD5\uFF1A/cwd <path> \uFF08\u5F53\u524D\u6839\u76EE\u5F55\uFF1A{current}\uFF09\u3002\u91CD\u65B0\u6307\u5411 filesystem / shell / memory \u5DE5\u5177\u5230 <path>\u3002",
|
|
1735
|
+
cwdUsageNoCurrent: "\u7528\u6CD5\uFF1A/cwd <path> \u5C06\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\u5207\u6362\u5230 <path>\u3002"
|
|
1730
1736
|
},
|
|
1731
1737
|
model: {
|
|
1732
1738
|
modelHint: "\u5C1D\u8BD5 deepseek-v4-flash \u6216 deepseek-v4-pro \u2014 \u8FD0\u884C /models \u83B7\u53D6\u5B9E\u65F6\u5217\u8868",
|
|
@@ -4873,15 +4879,25 @@ function rankPickerCandidates(files, query, limitOrOpts) {
|
|
|
4873
4879
|
for (const e of entries) {
|
|
4874
4880
|
const lower = e.path.toLowerCase();
|
|
4875
4881
|
const hit = lower.indexOf(needle);
|
|
4876
|
-
if (hit
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
+
if (hit >= 0) {
|
|
4883
|
+
const slash = lower.lastIndexOf("/");
|
|
4884
|
+
const base = slash >= 0 ? lower.slice(slash + 1) : lower;
|
|
4885
|
+
let cls = 2;
|
|
4886
|
+
if (base.startsWith(needle)) cls = 0;
|
|
4887
|
+
else if (lower.startsWith(needle)) cls = 1;
|
|
4888
|
+
scored.push({
|
|
4889
|
+
path: e.path,
|
|
4890
|
+
score: cls * 1e4 + Math.min(hit, 9999),
|
|
4891
|
+
mtimeMs: e.mtimeMs,
|
|
4892
|
+
recent: recent.has(e.path)
|
|
4893
|
+
});
|
|
4894
|
+
continue;
|
|
4895
|
+
}
|
|
4896
|
+
const fuzzy = fuzzySubseqScore(needle, lower);
|
|
4897
|
+
if (fuzzy === null) continue;
|
|
4882
4898
|
scored.push({
|
|
4883
4899
|
path: e.path,
|
|
4884
|
-
score:
|
|
4900
|
+
score: 3e4 + fuzzy,
|
|
4885
4901
|
mtimeMs: e.mtimeMs,
|
|
4886
4902
|
recent: recent.has(e.path)
|
|
4887
4903
|
});
|
|
@@ -4893,11 +4909,33 @@ function rankPickerCandidates(files, query, limitOrOpts) {
|
|
|
4893
4909
|
});
|
|
4894
4910
|
return scored.slice(0, limit).map((s) => s.path);
|
|
4895
4911
|
}
|
|
4912
|
+
function fuzzySubseqScore(needle, target) {
|
|
4913
|
+
if (needle.length === 0) return 0;
|
|
4914
|
+
const slashIdx = target.lastIndexOf("/");
|
|
4915
|
+
const basenameStart = slashIdx >= 0 ? slashIdx + 1 : 0;
|
|
4916
|
+
let qi = 0;
|
|
4917
|
+
let lastMatchIdx = -2;
|
|
4918
|
+
let consecutive = 0;
|
|
4919
|
+
let basenameMatches = 0;
|
|
4920
|
+
let totalGap = 0;
|
|
4921
|
+
for (let ti = 0; ti < target.length && qi < needle.length; ti++) {
|
|
4922
|
+
if (target[ti] !== needle[qi]) continue;
|
|
4923
|
+
if (ti === lastMatchIdx + 1) consecutive++;
|
|
4924
|
+
else if (lastMatchIdx >= 0) totalGap += ti - lastMatchIdx - 1;
|
|
4925
|
+
if (ti >= basenameStart) basenameMatches++;
|
|
4926
|
+
lastMatchIdx = ti;
|
|
4927
|
+
qi++;
|
|
4928
|
+
}
|
|
4929
|
+
if (qi < needle.length) return null;
|
|
4930
|
+
const quality = Math.max(0, totalGap - consecutive * 10 - basenameMatches * 5);
|
|
4931
|
+
const lengthPenalty = Math.floor(target.length / 4);
|
|
4932
|
+
return quality + lengthPenalty;
|
|
4933
|
+
}
|
|
4896
4934
|
var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
|
|
4897
4935
|
function expandAtMentions(text, rootDir, opts = {}) {
|
|
4898
4936
|
const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
|
|
4899
4937
|
const maxDirEntries = Math.max(1, opts.maxDirEntries ?? DEFAULT_AT_DIR_MAX_ENTRIES);
|
|
4900
|
-
const
|
|
4938
|
+
const fs5 = opts.fs ?? defaultFs;
|
|
4901
4939
|
const root = resolve(rootDir);
|
|
4902
4940
|
const seen = /* @__PURE__ */ new Map();
|
|
4903
4941
|
const expansions = [];
|
|
@@ -4910,7 +4948,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
|
|
|
4910
4948
|
if (!cleaned) continue;
|
|
4911
4949
|
const token = `@${cleaned}`;
|
|
4912
4950
|
if (seen.has(token)) continue;
|
|
4913
|
-
const expansion = resolveMention(cleaned, root, maxBytes, maxDirEntries,
|
|
4951
|
+
const expansion = resolveMention(cleaned, root, maxBytes, maxDirEntries, fs5, dirListings);
|
|
4914
4952
|
seen.set(token, expansion);
|
|
4915
4953
|
expansions.push(expansion);
|
|
4916
4954
|
}
|
|
@@ -4927,7 +4965,7 @@ ${files.join("\n")}
|
|
|
4927
4965
|
`<directory path="${ex.path}" entries="${ex.entries ?? files.length}"${truncAttr}>${body}</directory>`
|
|
4928
4966
|
);
|
|
4929
4967
|
} else if (ex.ok) {
|
|
4930
|
-
const content = readSafe(root, ex.path,
|
|
4968
|
+
const content = readSafe(root, ex.path, fs5);
|
|
4931
4969
|
blocks.push(`<file path="${ex.path}">
|
|
4932
4970
|
${content}
|
|
4933
4971
|
</file>`);
|
|
@@ -4941,7 +4979,7 @@ ${content}
|
|
|
4941
4979
|
${blocks.join("\n\n")}`;
|
|
4942
4980
|
return { text: augmented, expansions };
|
|
4943
4981
|
}
|
|
4944
|
-
function resolveMention(rawPath, root, maxBytes, maxDirEntries,
|
|
4982
|
+
function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs5, dirListings) {
|
|
4945
4983
|
if (isAbsolute(rawPath)) {
|
|
4946
4984
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
|
|
4947
4985
|
}
|
|
@@ -4950,18 +4988,18 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs4, dirListings
|
|
|
4950
4988
|
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
4951
4989
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
|
|
4952
4990
|
}
|
|
4953
|
-
if (!
|
|
4991
|
+
if (!fs5.exists(resolved)) {
|
|
4954
4992
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
|
|
4955
4993
|
}
|
|
4956
|
-
if (
|
|
4957
|
-
const size =
|
|
4994
|
+
if (fs5.isFile(resolved)) {
|
|
4995
|
+
const size = fs5.size(resolved);
|
|
4958
4996
|
if (size > maxBytes) {
|
|
4959
4997
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
|
|
4960
4998
|
}
|
|
4961
4999
|
return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
|
|
4962
5000
|
}
|
|
4963
|
-
if (
|
|
4964
|
-
const { files, truncated } =
|
|
5001
|
+
if (fs5.isDir?.(resolved) && fs5.listDir) {
|
|
5002
|
+
const { files, truncated } = fs5.listDir(resolved, root, maxDirEntries);
|
|
4965
5003
|
dirListings.set(rawPath, files);
|
|
4966
5004
|
return {
|
|
4967
5005
|
token: `@${rawPath}`,
|
|
@@ -4974,10 +5012,10 @@ function resolveMention(rawPath, root, maxBytes, maxDirEntries, fs4, dirListings
|
|
|
4974
5012
|
}
|
|
4975
5013
|
return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
|
|
4976
5014
|
}
|
|
4977
|
-
function readSafe(root, rawPath,
|
|
5015
|
+
function readSafe(root, rawPath, fs5) {
|
|
4978
5016
|
const resolved = resolve(root, rawPath);
|
|
4979
5017
|
try {
|
|
4980
|
-
return
|
|
5018
|
+
return fs5.read(resolved);
|
|
4981
5019
|
} catch {
|
|
4982
5020
|
return "(read failed)";
|
|
4983
5021
|
}
|
|
@@ -5790,9 +5828,9 @@ function applyMemoryStack(basePrompt, rootDir) {
|
|
|
5790
5828
|
}
|
|
5791
5829
|
|
|
5792
5830
|
// src/tools/filesystem.ts
|
|
5793
|
-
import { promises as
|
|
5794
|
-
import * as
|
|
5795
|
-
import
|
|
5831
|
+
import { promises as fs4 } from "fs";
|
|
5832
|
+
import * as pathMod4 from "path";
|
|
5833
|
+
import picomatch3 from "picomatch";
|
|
5796
5834
|
|
|
5797
5835
|
// src/tools/fs/edit.ts
|
|
5798
5836
|
import { promises as fs } from "fs";
|
|
@@ -5827,6 +5865,83 @@ async function applyEdit(rootDir, abs, args) {
|
|
|
5827
5865
|
return `${header}
|
|
5828
5866
|
${diff}`;
|
|
5829
5867
|
}
|
|
5868
|
+
async function applyMultiEdit(rootDir, edits) {
|
|
5869
|
+
if (edits.length === 0) {
|
|
5870
|
+
throw new Error("multi_edit: edits must contain at least one entry");
|
|
5871
|
+
}
|
|
5872
|
+
const filesByPath = /* @__PURE__ */ new Map();
|
|
5873
|
+
for (let i = 0; i < edits.length; i++) {
|
|
5874
|
+
const e = edits[i];
|
|
5875
|
+
if (typeof e.abs !== "string" || e.abs.length === 0) {
|
|
5876
|
+
throw new Error(`multi_edit: edit #${i + 1} requires a string \`path\` (no edits applied)`);
|
|
5877
|
+
}
|
|
5878
|
+
if (typeof e.search !== "string") {
|
|
5879
|
+
throw new Error(`multi_edit: edit #${i + 1} requires a string \`search\` (no edits applied)`);
|
|
5880
|
+
}
|
|
5881
|
+
if (typeof e.replace !== "string") {
|
|
5882
|
+
throw new Error(
|
|
5883
|
+
`multi_edit: edit #${i + 1} requires a string \`replace\` (no edits applied)`
|
|
5884
|
+
);
|
|
5885
|
+
}
|
|
5886
|
+
const rel = displayRel(rootDir, e.abs);
|
|
5887
|
+
if (e.search.length === 0) {
|
|
5888
|
+
throw new Error(
|
|
5889
|
+
`multi_edit: edit #${i + 1} (${rel}) search cannot be empty (no edits applied)`
|
|
5890
|
+
);
|
|
5891
|
+
}
|
|
5892
|
+
let state = filesByPath.get(e.abs);
|
|
5893
|
+
if (!state) {
|
|
5894
|
+
let before;
|
|
5895
|
+
try {
|
|
5896
|
+
before = await fs.readFile(e.abs, "utf8");
|
|
5897
|
+
} catch (err) {
|
|
5898
|
+
throw new Error(
|
|
5899
|
+
`multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
|
|
5900
|
+
);
|
|
5901
|
+
}
|
|
5902
|
+
const le = before.includes("\r\n") ? "\r\n" : "\n";
|
|
5903
|
+
state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
|
|
5904
|
+
filesByPath.set(e.abs, state);
|
|
5905
|
+
}
|
|
5906
|
+
const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
|
|
5907
|
+
const adaptedReplace = e.replace.replace(/\r?\n/g, state.le);
|
|
5908
|
+
const firstIdx = state.buf.indexOf(adaptedSearch);
|
|
5909
|
+
if (firstIdx < 0) {
|
|
5910
|
+
throw new Error(
|
|
5911
|
+
`multi_edit: edit #${i + 1} search text not found in ${rel} \u2014 no edits applied (multi_edit is atomic)`
|
|
5912
|
+
);
|
|
5913
|
+
}
|
|
5914
|
+
const nextIdx = state.buf.indexOf(adaptedSearch, firstIdx + 1);
|
|
5915
|
+
if (nextIdx >= 0) {
|
|
5916
|
+
throw new Error(
|
|
5917
|
+
`multi_edit: edit #${i + 1} search text appears multiple times in ${rel} \u2014 include more context to disambiguate (no edits applied)`
|
|
5918
|
+
);
|
|
5919
|
+
}
|
|
5920
|
+
const startLine = state.buf.slice(0, firstIdx).split(/\r?\n/).length;
|
|
5921
|
+
state.buf = state.buf.slice(0, firstIdx) + adaptedReplace + state.buf.slice(firstIdx + adaptedSearch.length);
|
|
5922
|
+
state.hunks.push(`# ${rel}
|
|
5923
|
+
${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
|
|
5924
|
+
state.deltaChars += adaptedReplace.length - adaptedSearch.length;
|
|
5925
|
+
state.touched++;
|
|
5926
|
+
}
|
|
5927
|
+
for (const [abs, state] of filesByPath) {
|
|
5928
|
+
await fs.writeFile(abs, state.buf, "utf8");
|
|
5929
|
+
}
|
|
5930
|
+
const fileCount = filesByPath.size;
|
|
5931
|
+
const editCount = edits.length;
|
|
5932
|
+
let totalDelta = 0;
|
|
5933
|
+
const allHunks = [];
|
|
5934
|
+
for (const state of filesByPath.values()) {
|
|
5935
|
+
totalDelta += state.deltaChars;
|
|
5936
|
+
allHunks.push(...state.hunks);
|
|
5937
|
+
}
|
|
5938
|
+
const sign = totalDelta >= 0 ? "+" : "";
|
|
5939
|
+
const editNoun = editCount === 1 ? "edit" : "edits";
|
|
5940
|
+
const fileNoun = fileCount === 1 ? "file" : "files";
|
|
5941
|
+
const header = `multi_edit: applied ${editCount} ${editNoun} across ${fileCount} ${fileNoun} (${sign}${totalDelta} chars)`;
|
|
5942
|
+
return `${header}
|
|
5943
|
+
${allHunks.join("\n")}`;
|
|
5944
|
+
}
|
|
5830
5945
|
function renderEditDiff(search, replace, startLine) {
|
|
5831
5946
|
const a = search.split(/\r?\n/);
|
|
5832
5947
|
const b = replace.split(/\r?\n/);
|
|
@@ -5873,15 +5988,78 @@ function lineDiff(a, b) {
|
|
|
5873
5988
|
return out;
|
|
5874
5989
|
}
|
|
5875
5990
|
|
|
5876
|
-
// src/tools/fs/
|
|
5991
|
+
// src/tools/fs/glob.ts
|
|
5877
5992
|
import { promises as fs2 } from "fs";
|
|
5878
5993
|
import * as pathMod2 from "path";
|
|
5994
|
+
import picomatch2 from "picomatch";
|
|
5995
|
+
function displayRel2(rootDir, full) {
|
|
5996
|
+
return pathMod2.relative(rootDir, full).replaceAll("\\", "/");
|
|
5997
|
+
}
|
|
5998
|
+
async function globFiles(ctx, startAbs, args) {
|
|
5999
|
+
if (args.signal?.aborted) {
|
|
6000
|
+
throw new DOMException("glob aborted by user", "AbortError");
|
|
6001
|
+
}
|
|
6002
|
+
const includeDeps = args.include_deps === true;
|
|
6003
|
+
const sortBy = args.sort_by ?? "mtime";
|
|
6004
|
+
const limit = Math.max(1, Math.min(1e3, Math.floor(args.limit ?? 200)));
|
|
6005
|
+
const isMatch = picomatch2(args.pattern, { dot: true, nocase: true });
|
|
6006
|
+
const hits = [];
|
|
6007
|
+
const walk2 = async (dir) => {
|
|
6008
|
+
if (args.signal?.aborted) {
|
|
6009
|
+
throw new DOMException("glob aborted by user", "AbortError");
|
|
6010
|
+
}
|
|
6011
|
+
let entries;
|
|
6012
|
+
try {
|
|
6013
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
6014
|
+
} catch {
|
|
6015
|
+
return;
|
|
6016
|
+
}
|
|
6017
|
+
for (const e of entries) {
|
|
6018
|
+
const full = pathMod2.join(dir, e.name);
|
|
6019
|
+
if (e.isDirectory()) {
|
|
6020
|
+
if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
|
|
6021
|
+
await walk2(full);
|
|
6022
|
+
continue;
|
|
6023
|
+
}
|
|
6024
|
+
if (!e.isFile() && !e.isSymbolicLink()) continue;
|
|
6025
|
+
const rel = displayRel2(ctx.rootDir, full);
|
|
6026
|
+
if (!isMatch(rel)) continue;
|
|
6027
|
+
let mtimeMs = 0;
|
|
6028
|
+
if (sortBy === "mtime") {
|
|
6029
|
+
try {
|
|
6030
|
+
const st = await fs2.stat(full);
|
|
6031
|
+
mtimeMs = st.mtimeMs;
|
|
6032
|
+
} catch {
|
|
6033
|
+
continue;
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
hits.push({ rel, mtimeMs });
|
|
6037
|
+
}
|
|
6038
|
+
};
|
|
6039
|
+
await walk2(startAbs);
|
|
6040
|
+
if (hits.length === 0) return "(no matches)";
|
|
6041
|
+
if (sortBy === "mtime") hits.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
6042
|
+
else hits.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
6043
|
+
const truncated = hits.length > limit;
|
|
6044
|
+
const shown = hits.slice(0, limit);
|
|
6045
|
+
const lines = shown.map((h) => h.rel);
|
|
6046
|
+
if (truncated) {
|
|
6047
|
+
lines.push(
|
|
6048
|
+
`[\u2026 ${hits.length - limit} more matches \u2014 refine pattern or raise limit (max 1000) \u2026]`
|
|
6049
|
+
);
|
|
6050
|
+
}
|
|
6051
|
+
return lines.join("\n");
|
|
6052
|
+
}
|
|
6053
|
+
|
|
6054
|
+
// src/tools/fs/search.ts
|
|
6055
|
+
import { promises as fs3 } from "fs";
|
|
6056
|
+
import * as pathMod3 from "path";
|
|
5879
6057
|
function throwIfAborted(signal) {
|
|
5880
6058
|
if (!signal?.aborted) return;
|
|
5881
6059
|
throw new DOMException("search aborted by user", "AbortError");
|
|
5882
6060
|
}
|
|
5883
|
-
function
|
|
5884
|
-
return
|
|
6061
|
+
function displayRel3(rootDir, full) {
|
|
6062
|
+
return pathMod3.relative(rootDir, full).replaceAll("\\", "/");
|
|
5885
6063
|
}
|
|
5886
6064
|
async function searchFiles(ctx, startAbs, args) {
|
|
5887
6065
|
throwIfAborted(args.signal);
|
|
@@ -5899,17 +6077,17 @@ async function searchFiles(ctx, startAbs, args) {
|
|
|
5899
6077
|
throwIfAborted(args.signal);
|
|
5900
6078
|
let entries;
|
|
5901
6079
|
try {
|
|
5902
|
-
entries = await
|
|
6080
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
5903
6081
|
} catch {
|
|
5904
6082
|
return;
|
|
5905
6083
|
}
|
|
5906
6084
|
for (const e of entries) {
|
|
5907
6085
|
throwIfAborted(args.signal);
|
|
5908
|
-
const full =
|
|
6086
|
+
const full = pathMod3.join(dir, e.name);
|
|
5909
6087
|
const lower = e.name.toLowerCase();
|
|
5910
6088
|
const hit = re ? re.test(e.name) : lower.includes(needle);
|
|
5911
6089
|
if (hit) {
|
|
5912
|
-
const rel =
|
|
6090
|
+
const rel = displayRel3(ctx.rootDir, full);
|
|
5913
6091
|
if (totalBytes + rel.length + 1 > ctx.maxListBytes) {
|
|
5914
6092
|
matches.push("[\u2026 search truncated \u2014 refine pattern \u2026]");
|
|
5915
6093
|
return;
|
|
@@ -5930,6 +6108,7 @@ async function searchContent(ctx, startAbs, args) {
|
|
|
5930
6108
|
throwIfAborted(args.signal);
|
|
5931
6109
|
const caseSensitive = args.case_sensitive === true;
|
|
5932
6110
|
const includeDeps = args.include_deps === true;
|
|
6111
|
+
const ctxLines = Math.max(0, Math.min(20, Math.floor(args.context ?? 0)));
|
|
5933
6112
|
let re = null;
|
|
5934
6113
|
try {
|
|
5935
6114
|
re = new RegExp(args.pattern, caseSensitive ? "" : "i");
|
|
@@ -5941,12 +6120,22 @@ async function searchContent(ctx, startAbs, args) {
|
|
|
5941
6120
|
let totalBytes = 0;
|
|
5942
6121
|
let scanned = 0;
|
|
5943
6122
|
let truncated = false;
|
|
6123
|
+
const pushLine = (out) => {
|
|
6124
|
+
if (totalBytes + out.length + 1 > ctx.maxListBytes) {
|
|
6125
|
+
matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
6126
|
+
truncated = true;
|
|
6127
|
+
return false;
|
|
6128
|
+
}
|
|
6129
|
+
matches.push(out);
|
|
6130
|
+
totalBytes += out.length + 1;
|
|
6131
|
+
return true;
|
|
6132
|
+
};
|
|
5944
6133
|
const walk2 = async (dir) => {
|
|
5945
6134
|
if (truncated) return;
|
|
5946
6135
|
throwIfAborted(args.signal);
|
|
5947
6136
|
let entries;
|
|
5948
6137
|
try {
|
|
5949
|
-
entries = await
|
|
6138
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
5950
6139
|
} catch {
|
|
5951
6140
|
return;
|
|
5952
6141
|
}
|
|
@@ -5955,16 +6144,16 @@ async function searchContent(ctx, startAbs, args) {
|
|
|
5955
6144
|
throwIfAborted(args.signal);
|
|
5956
6145
|
if (e.isDirectory()) {
|
|
5957
6146
|
if (!includeDeps && ctx.skipDirNames.has(e.name)) continue;
|
|
5958
|
-
await walk2(
|
|
6147
|
+
await walk2(pathMod3.join(dir, e.name));
|
|
5959
6148
|
continue;
|
|
5960
6149
|
}
|
|
5961
6150
|
if (!e.isFile()) continue;
|
|
5962
|
-
const full =
|
|
5963
|
-
if (ctx.nameMatch && !ctx.nameMatch(e.name,
|
|
6151
|
+
const full = pathMod3.join(dir, e.name);
|
|
6152
|
+
if (ctx.nameMatch && !ctx.nameMatch(e.name, displayRel3(ctx.rootDir, full))) continue;
|
|
5964
6153
|
if (ctx.isBinaryByName(e.name)) continue;
|
|
5965
6154
|
let fh;
|
|
5966
6155
|
try {
|
|
5967
|
-
fh = await
|
|
6156
|
+
fh = await fs3.open(full, "r");
|
|
5968
6157
|
} catch {
|
|
5969
6158
|
continue;
|
|
5970
6159
|
}
|
|
@@ -5987,25 +6176,45 @@ async function searchContent(ctx, startAbs, args) {
|
|
|
5987
6176
|
const firstNul = raw.indexOf(0);
|
|
5988
6177
|
if (firstNul !== -1 && firstNul < 8 * 1024) continue;
|
|
5989
6178
|
const text = raw.toString("utf8");
|
|
5990
|
-
const rel =
|
|
6179
|
+
const rel = displayRel3(ctx.rootDir, full);
|
|
5991
6180
|
const lines = text.split(/\r?\n/);
|
|
6181
|
+
const hits = [];
|
|
5992
6182
|
for (let li = 0; li < lines.length; li++) {
|
|
5993
6183
|
throwIfAborted(args.signal);
|
|
5994
6184
|
const line = lines[li];
|
|
5995
6185
|
const lineForCheck = caseSensitive ? line : line.toLowerCase();
|
|
5996
6186
|
const hit = re ? re.test(line) : lineForCheck.includes(needle);
|
|
5997
|
-
if (
|
|
5998
|
-
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
5999
|
-
const out = `${rel}:${li + 1}: ${display}`;
|
|
6000
|
-
if (totalBytes + out.length + 1 > ctx.maxListBytes) {
|
|
6001
|
-
matches.push(`[\u2026 truncated at ${ctx.maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
|
|
6002
|
-
truncated = true;
|
|
6003
|
-
return;
|
|
6004
|
-
}
|
|
6005
|
-
matches.push(out);
|
|
6006
|
-
totalBytes += out.length + 1;
|
|
6187
|
+
if (hit) hits.push(li);
|
|
6007
6188
|
}
|
|
6008
6189
|
scanned++;
|
|
6190
|
+
if (hits.length === 0) continue;
|
|
6191
|
+
if (ctxLines === 0) {
|
|
6192
|
+
for (const li of hits) {
|
|
6193
|
+
if (truncated) return;
|
|
6194
|
+
const line = lines[li];
|
|
6195
|
+
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
6196
|
+
if (!pushLine(`${rel}:${li + 1}: ${display}`)) return;
|
|
6197
|
+
}
|
|
6198
|
+
continue;
|
|
6199
|
+
}
|
|
6200
|
+
const hitSet = new Set(hits);
|
|
6201
|
+
let prevWindowEnd = -2;
|
|
6202
|
+
for (const li of hits) {
|
|
6203
|
+
if (truncated) return;
|
|
6204
|
+
const winStart = Math.max(0, li - ctxLines);
|
|
6205
|
+
const winEnd = Math.min(lines.length - 1, li + ctxLines);
|
|
6206
|
+
if (winStart > prevWindowEnd + 1 && prevWindowEnd >= 0) {
|
|
6207
|
+
if (!pushLine("--")) return;
|
|
6208
|
+
}
|
|
6209
|
+
const realStart = winStart > prevWindowEnd + 1 ? winStart : prevWindowEnd + 1;
|
|
6210
|
+
for (let i = realStart; i <= winEnd; i++) {
|
|
6211
|
+
const line = lines[i];
|
|
6212
|
+
const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
|
|
6213
|
+
const sep2 = hitSet.has(i) ? ":" : "-";
|
|
6214
|
+
if (!pushLine(`${rel}:${i + 1}${sep2} ${display}`)) return;
|
|
6215
|
+
}
|
|
6216
|
+
prevWindowEnd = winEnd;
|
|
6217
|
+
}
|
|
6009
6218
|
}
|
|
6010
6219
|
};
|
|
6011
6220
|
await walk2(startAbs);
|
|
@@ -6023,8 +6232,8 @@ var AUTO_PREVIEW_HEAD_LINES = 80;
|
|
|
6023
6232
|
var AUTO_PREVIEW_TAIL_LINES = 40;
|
|
6024
6233
|
var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
|
|
6025
6234
|
var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
|
|
6026
|
-
function
|
|
6027
|
-
return
|
|
6235
|
+
function displayRel4(rootDir, full) {
|
|
6236
|
+
return pathMod4.relative(rootDir, full).replaceAll("\\", "/");
|
|
6028
6237
|
}
|
|
6029
6238
|
var GLOB_METACHARS = /[*?{[]/;
|
|
6030
6239
|
function compileNameFilter(filter) {
|
|
@@ -6034,7 +6243,7 @@ function compileNameFilter(filter) {
|
|
|
6034
6243
|
return (name) => name.toLowerCase().includes(needle);
|
|
6035
6244
|
}
|
|
6036
6245
|
const matchPath = filter.includes("/");
|
|
6037
|
-
const isMatch =
|
|
6246
|
+
const isMatch = picomatch3(filter, { dot: true, nocase: true });
|
|
6038
6247
|
return matchPath ? (_n, rel) => isMatch(rel) : (name) => isMatch(name);
|
|
6039
6248
|
}
|
|
6040
6249
|
function isLikelyBinaryByName(name) {
|
|
@@ -6043,7 +6252,7 @@ function isLikelyBinaryByName(name) {
|
|
|
6043
6252
|
return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
6044
6253
|
}
|
|
6045
6254
|
function registerFilesystemTools(registry, opts) {
|
|
6046
|
-
const rootDir =
|
|
6255
|
+
const rootDir = pathMod4.resolve(opts.rootDir);
|
|
6047
6256
|
const allowWriting = opts.allowWriting !== false;
|
|
6048
6257
|
const maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
6049
6258
|
const maxListBytes = opts.maxListBytes ?? DEFAULT_MAX_LIST_BYTES;
|
|
@@ -6056,10 +6265,10 @@ function registerFilesystemTools(registry, opts) {
|
|
|
6056
6265
|
normalized = normalized.slice(1);
|
|
6057
6266
|
}
|
|
6058
6267
|
if (normalized.length === 0) normalized = ".";
|
|
6059
|
-
const resolved =
|
|
6060
|
-
const normRoot =
|
|
6061
|
-
const rel =
|
|
6062
|
-
if (rel.startsWith("..") ||
|
|
6268
|
+
const resolved = pathMod4.resolve(rootDir, normalized);
|
|
6269
|
+
const normRoot = pathMod4.resolve(rootDir);
|
|
6270
|
+
const rel = pathMod4.relative(normRoot, resolved);
|
|
6271
|
+
if (rel.startsWith("..") || pathMod4.isAbsolute(rel)) {
|
|
6063
6272
|
throw new Error(
|
|
6064
6273
|
`path escapes sandbox root (${normRoot}): ${raw} \u2014 workspace is pinned at launch; quit and relaunch with \`reasonix code --dir <path>\` to work in a different folder`
|
|
6065
6274
|
);
|
|
@@ -6091,7 +6300,7 @@ When none of these is given AND the file is longer than ${DEFAULT_AUTO_PREVIEW_L
|
|
|
6091
6300
|
},
|
|
6092
6301
|
fn: async (args) => {
|
|
6093
6302
|
const abs = safePath(args.path);
|
|
6094
|
-
const fh = await
|
|
6303
|
+
const fh = await fs4.open(abs, "r");
|
|
6095
6304
|
let raw;
|
|
6096
6305
|
try {
|
|
6097
6306
|
const stat2 = await fh.stat();
|
|
@@ -6165,7 +6374,7 @@ ${slice.join("\n")}`;
|
|
|
6165
6374
|
},
|
|
6166
6375
|
fn: async (args) => {
|
|
6167
6376
|
const abs = safePath(args.path ?? ".");
|
|
6168
|
-
const entries = await
|
|
6377
|
+
const entries = await fs4.readdir(abs, { withFileTypes: true });
|
|
6169
6378
|
const lines = [];
|
|
6170
6379
|
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6171
6380
|
lines.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
@@ -6209,7 +6418,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6209
6418
|
if (depth > maxDepth) return;
|
|
6210
6419
|
let entries;
|
|
6211
6420
|
try {
|
|
6212
|
-
entries = await
|
|
6421
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
6213
6422
|
} catch {
|
|
6214
6423
|
return;
|
|
6215
6424
|
}
|
|
@@ -6244,7 +6453,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6244
6453
|
lines.push(line);
|
|
6245
6454
|
emitted++;
|
|
6246
6455
|
if (e.isDirectory() && !skip) {
|
|
6247
|
-
await walk2(
|
|
6456
|
+
await walk2(pathMod4.join(dir, e.name), depth + 1);
|
|
6248
6457
|
}
|
|
6249
6458
|
}
|
|
6250
6459
|
};
|
|
@@ -6305,6 +6514,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6305
6514
|
include_deps: {
|
|
6306
6515
|
type: "boolean",
|
|
6307
6516
|
description: "When true, also search inside node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
|
|
6517
|
+
},
|
|
6518
|
+
context: {
|
|
6519
|
+
type: "integer",
|
|
6520
|
+
description: "Lines of context to show around each match (both before and after). Default 0 (just the matching line). Capped at 20. Output uses ripgrep style: `:` after the line number on the matching line, `-` on context lines, `--` separating non-adjacent windows."
|
|
6308
6521
|
}
|
|
6309
6522
|
},
|
|
6310
6523
|
required: ["pattern"]
|
|
@@ -6321,6 +6534,43 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6321
6534
|
{ ...args, signal: toolCtx?.signal }
|
|
6322
6535
|
)
|
|
6323
6536
|
});
|
|
6537
|
+
registry.register({
|
|
6538
|
+
name: "glob",
|
|
6539
|
+
parallelSafe: true,
|
|
6540
|
+
description: "List files matching a glob pattern, sorted by mtime (most-recently-modified first) by default. Use this for 'what changed lately', 'find all *.test.ts', 'all configs under src/'. Glob syntax matches the cross-tool standard: `*` (any chars in one segment), `**` (any segments), `?` (one char), `{a,b}` (alternation). Pattern matches against the path RELATIVE to the search root (e.g. 'src/**/*.ts' from project root). Skips node_modules / .git / dist / build / etc by default. Default limit 200; raise via `limit` (max 1000). Different from `search_files` (substring on basename) and `search_content` (matches inside file contents).",
|
|
6541
|
+
readOnly: true,
|
|
6542
|
+
parameters: {
|
|
6543
|
+
type: "object",
|
|
6544
|
+
properties: {
|
|
6545
|
+
pattern: {
|
|
6546
|
+
type: "string",
|
|
6547
|
+
description: "Glob pattern, e.g. 'src/**/*.ts', '**/*.{md,mdx}', 'tests/*.test.ts'."
|
|
6548
|
+
},
|
|
6549
|
+
path: {
|
|
6550
|
+
type: "string",
|
|
6551
|
+
description: "Base directory to walk (default: sandbox root). The pattern matches relative to this path."
|
|
6552
|
+
},
|
|
6553
|
+
sort_by: {
|
|
6554
|
+
type: "string",
|
|
6555
|
+
enum: ["mtime", "name"],
|
|
6556
|
+
description: "Sort order. 'mtime' (default) shows most-recently-modified first \u2014 useful for 'what did I change today'. 'name' is alphabetical."
|
|
6557
|
+
},
|
|
6558
|
+
include_deps: {
|
|
6559
|
+
type: "boolean",
|
|
6560
|
+
description: "When true, also walk node_modules / .git / dist / build / etc. Off by default."
|
|
6561
|
+
},
|
|
6562
|
+
limit: {
|
|
6563
|
+
type: "integer",
|
|
6564
|
+
description: "Cap on returned matches. Default 200; clamped to [1, 1000]."
|
|
6565
|
+
}
|
|
6566
|
+
},
|
|
6567
|
+
required: ["pattern"]
|
|
6568
|
+
},
|
|
6569
|
+
fn: async (args, toolCtx) => globFiles({ rootDir, skipDirNames: SKIP_DIR_NAMES }, safePath(args.path ?? "."), {
|
|
6570
|
+
...args,
|
|
6571
|
+
signal: toolCtx?.signal
|
|
6572
|
+
})
|
|
6573
|
+
});
|
|
6324
6574
|
registry.register({
|
|
6325
6575
|
name: "get_file_info",
|
|
6326
6576
|
parallelSafe: true,
|
|
@@ -6335,7 +6585,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6335
6585
|
},
|
|
6336
6586
|
fn: async (args) => {
|
|
6337
6587
|
const abs = safePath(args.path);
|
|
6338
|
-
const st = await
|
|
6588
|
+
const st = await fs4.lstat(abs);
|
|
6339
6589
|
const type = st.isDirectory() ? "directory" : st.isSymbolicLink() ? "symlink" : "file";
|
|
6340
6590
|
return JSON.stringify({
|
|
6341
6591
|
type,
|
|
@@ -6358,9 +6608,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6358
6608
|
},
|
|
6359
6609
|
fn: async (args) => {
|
|
6360
6610
|
const abs = safePath(args.path);
|
|
6361
|
-
await
|
|
6362
|
-
await
|
|
6363
|
-
return `wrote ${args.content.length} chars to ${
|
|
6611
|
+
await fs4.mkdir(pathMod4.dirname(abs), { recursive: true });
|
|
6612
|
+
await fs4.writeFile(abs, args.content, "utf8");
|
|
6613
|
+
return `wrote ${args.content.length} chars to ${displayRel4(rootDir, abs)}`;
|
|
6364
6614
|
}
|
|
6365
6615
|
});
|
|
6366
6616
|
registry.register({
|
|
@@ -6377,6 +6627,43 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6377
6627
|
},
|
|
6378
6628
|
fn: async (args) => applyEdit(rootDir, safePath(args.path), args)
|
|
6379
6629
|
});
|
|
6630
|
+
registry.register({
|
|
6631
|
+
name: "multi_edit",
|
|
6632
|
+
description: "Apply N SEARCH/REPLACE edits across ONE OR MORE files in a single atomic call. Edits run sequentially in array order; for edits that touch the same file, a later edit can match text inserted by an earlier one. If ANY edit fails (search not found, ambiguous match, empty search, file unreadable), NO files are written \u2014 atomic at the validation layer. Same per-edit rules as edit_file: `search` is exact text (whitespace sensitive, no regex) and must be unique in its target file at the moment that edit applies. Use this for renames spanning multiple files, cross-file refactors, or any batch where you'd otherwise loop edit_file.",
|
|
6633
|
+
parameters: {
|
|
6634
|
+
type: "object",
|
|
6635
|
+
properties: {
|
|
6636
|
+
edits: {
|
|
6637
|
+
type: "array",
|
|
6638
|
+
description: "Edits to apply in order. Length \u2265 1. Each edit names its own target file.",
|
|
6639
|
+
items: {
|
|
6640
|
+
type: "object",
|
|
6641
|
+
properties: {
|
|
6642
|
+
path: {
|
|
6643
|
+
type: "string",
|
|
6644
|
+
description: "File the edit targets (sandbox-relative or absolute)."
|
|
6645
|
+
},
|
|
6646
|
+
search: {
|
|
6647
|
+
type: "string",
|
|
6648
|
+
description: "Exact text to find (must be unique in the file)."
|
|
6649
|
+
},
|
|
6650
|
+
replace: { type: "string", description: "Text to substitute in place of `search`." }
|
|
6651
|
+
},
|
|
6652
|
+
required: ["path", "search", "replace"]
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
},
|
|
6656
|
+
required: ["edits"]
|
|
6657
|
+
},
|
|
6658
|
+
fn: async (args) => {
|
|
6659
|
+
const resolved = (args.edits ?? []).map((e) => ({
|
|
6660
|
+
abs: safePath(e?.path),
|
|
6661
|
+
search: e?.search,
|
|
6662
|
+
replace: e?.replace
|
|
6663
|
+
}));
|
|
6664
|
+
return applyMultiEdit(rootDir, resolved);
|
|
6665
|
+
}
|
|
6666
|
+
});
|
|
6380
6667
|
registry.register({
|
|
6381
6668
|
name: "create_directory",
|
|
6382
6669
|
description: "Create a directory (and any missing parents) under the sandbox root.",
|
|
@@ -6387,8 +6674,8 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6387
6674
|
},
|
|
6388
6675
|
fn: async (args) => {
|
|
6389
6676
|
const abs = safePath(args.path);
|
|
6390
|
-
await
|
|
6391
|
-
return `created ${
|
|
6677
|
+
await fs4.mkdir(abs, { recursive: true });
|
|
6678
|
+
return `created ${displayRel4(rootDir, abs)}/`;
|
|
6392
6679
|
}
|
|
6393
6680
|
});
|
|
6394
6681
|
registry.register({
|
|
@@ -6405,9 +6692,9 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
|
|
|
6405
6692
|
fn: async (args) => {
|
|
6406
6693
|
const src = safePath(args.source);
|
|
6407
6694
|
const dst = safePath(args.destination);
|
|
6408
|
-
await
|
|
6409
|
-
await
|
|
6410
|
-
return `moved ${
|
|
6695
|
+
await fs4.mkdir(pathMod4.dirname(dst), { recursive: true });
|
|
6696
|
+
await fs4.rename(src, dst);
|
|
6697
|
+
return `moved ${displayRel4(rootDir, src)} \u2192 ${displayRel4(rootDir, dst)}`;
|
|
6411
6698
|
}
|
|
6412
6699
|
});
|
|
6413
6700
|
return registry;
|
|
@@ -6886,6 +7173,108 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
6886
7173
|
return registry;
|
|
6887
7174
|
}
|
|
6888
7175
|
|
|
7176
|
+
// src/tools/todo.ts
|
|
7177
|
+
var DESCRIPTION = 'In-session task tracker for multi-step work. NOT a plan \u2014 no approval gate, no checkpoint pauses, doesn\'t touch any files. The tool replaces the entire todo list every call (set semantics, NOT append). Pass the FULL list every time.\n\nWhen to use:\n\u2022 The task has 3+ distinct steps and you want to keep them straight as you work.\n\u2022 The user gave you a multi-part request ("do A, then B, then C").\n\u2022 You\'re partway through a long task and want to record where you are so a future you doesn\'t lose the thread.\n\nWhen NOT to use:\n\u2022 One-shot edits, single-question answers, single-tool tasks.\n\u2022 User-facing approval gates \u2192 that\'s `submit_plan`.\n\u2022 Branching choices \u2192 that\'s `ask_choice`.\n\nRules:\n\u2022 Exactly ONE todo may have status:"in_progress" at a time (or zero \u2014 between steps).\n\u2022 Mark a todo "completed" the moment it\'s actually done \u2014 don\'t batch.\n\u2022 Each todo: `content` (imperative, e.g. "Add tests"), `activeForm` (gerund shown while running, e.g. "Adding tests"), `status`.\n\u2022 Empty `todos:[]` is allowed \u2014 it clears the list when work is fully done.';
|
|
7178
|
+
function validateTodos(raw) {
|
|
7179
|
+
if (!Array.isArray(raw)) {
|
|
7180
|
+
throw new Error("todo_write: `todos` must be an array");
|
|
7181
|
+
}
|
|
7182
|
+
const out = [];
|
|
7183
|
+
let inProgressCount = 0;
|
|
7184
|
+
for (let i = 0; i < raw.length; i++) {
|
|
7185
|
+
const entry = raw[i];
|
|
7186
|
+
if (!entry || typeof entry !== "object") {
|
|
7187
|
+
throw new Error(`todo_write: todo #${i + 1} must be an object`);
|
|
7188
|
+
}
|
|
7189
|
+
const e = entry;
|
|
7190
|
+
const content = typeof e.content === "string" ? e.content.trim() : "";
|
|
7191
|
+
const activeForm = typeof e.activeForm === "string" ? e.activeForm.trim() : "";
|
|
7192
|
+
const status = e.status;
|
|
7193
|
+
if (!content) {
|
|
7194
|
+
throw new Error(`todo_write: todo #${i + 1} \`content\` must be a non-empty string`);
|
|
7195
|
+
}
|
|
7196
|
+
if (!activeForm) {
|
|
7197
|
+
throw new Error(`todo_write: todo #${i + 1} \`activeForm\` must be a non-empty string`);
|
|
7198
|
+
}
|
|
7199
|
+
if (status !== "pending" && status !== "in_progress" && status !== "completed") {
|
|
7200
|
+
throw new Error(
|
|
7201
|
+
`todo_write: todo #${i + 1} \`status\` must be one of pending|in_progress|completed (got ${JSON.stringify(status)})`
|
|
7202
|
+
);
|
|
7203
|
+
}
|
|
7204
|
+
if (status === "in_progress") {
|
|
7205
|
+
inProgressCount++;
|
|
7206
|
+
if (inProgressCount > 1) {
|
|
7207
|
+
throw new Error(
|
|
7208
|
+
"todo_write: at most one todo may be in_progress at a time \u2014 mark the previous one completed first"
|
|
7209
|
+
);
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
out.push({ content, status, activeForm });
|
|
7213
|
+
}
|
|
7214
|
+
return out;
|
|
7215
|
+
}
|
|
7216
|
+
function renderTodos(todos) {
|
|
7217
|
+
if (todos.length === 0) return "todos cleared (0 items)";
|
|
7218
|
+
let done = 0;
|
|
7219
|
+
let inProgress = 0;
|
|
7220
|
+
let pending = 0;
|
|
7221
|
+
for (const t2 of todos) {
|
|
7222
|
+
if (t2.status === "completed") done++;
|
|
7223
|
+
else if (t2.status === "in_progress") inProgress++;
|
|
7224
|
+
else pending++;
|
|
7225
|
+
}
|
|
7226
|
+
const header = `todos updated \xB7 ${done} done \xB7 ${inProgress} in progress \xB7 ${pending} pending`;
|
|
7227
|
+
const lines = todos.map((t2) => {
|
|
7228
|
+
if (t2.status === "completed") return `[x] ${t2.content}`;
|
|
7229
|
+
if (t2.status === "in_progress") return `[>] ${t2.activeForm}`;
|
|
7230
|
+
return `[ ] ${t2.content}`;
|
|
7231
|
+
});
|
|
7232
|
+
return `${header}
|
|
7233
|
+
${lines.join("\n")}`;
|
|
7234
|
+
}
|
|
7235
|
+
function registerTodoTool(registry, opts = {}) {
|
|
7236
|
+
registry.register({
|
|
7237
|
+
name: "todo_write",
|
|
7238
|
+
description: DESCRIPTION,
|
|
7239
|
+
readOnly: true,
|
|
7240
|
+
parameters: {
|
|
7241
|
+
type: "object",
|
|
7242
|
+
properties: {
|
|
7243
|
+
todos: {
|
|
7244
|
+
type: "array",
|
|
7245
|
+
description: "The COMPLETE new todo list. Replaces whatever was there before. Pass [] to clear.",
|
|
7246
|
+
items: {
|
|
7247
|
+
type: "object",
|
|
7248
|
+
properties: {
|
|
7249
|
+
content: {
|
|
7250
|
+
type: "string",
|
|
7251
|
+
description: 'Imperative step description, e.g. "Add tests for parser".'
|
|
7252
|
+
},
|
|
7253
|
+
status: {
|
|
7254
|
+
type: "string",
|
|
7255
|
+
enum: ["pending", "in_progress", "completed"],
|
|
7256
|
+
description: "Current state. Exactly one item may be in_progress."
|
|
7257
|
+
},
|
|
7258
|
+
activeForm: {
|
|
7259
|
+
type: "string",
|
|
7260
|
+
description: 'Gerund form shown while in_progress, e.g. "Adding tests for parser".'
|
|
7261
|
+
}
|
|
7262
|
+
},
|
|
7263
|
+
required: ["content", "status", "activeForm"]
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
},
|
|
7267
|
+
required: ["todos"]
|
|
7268
|
+
},
|
|
7269
|
+
fn: async (args) => {
|
|
7270
|
+
const todos = validateTodos(args?.todos);
|
|
7271
|
+
opts.onTodosUpdated?.(todos);
|
|
7272
|
+
return renderTodos(todos);
|
|
7273
|
+
}
|
|
7274
|
+
});
|
|
7275
|
+
return registry;
|
|
7276
|
+
}
|
|
7277
|
+
|
|
6889
7278
|
// src/tools/subagent-types.ts
|
|
6890
7279
|
var EXPLORE_SYSTEM = `You are an exploration subagent. Wide-net read-only investigation; return one distilled answer.
|
|
6891
7280
|
|
|
@@ -7255,11 +7644,11 @@ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
|
|
|
7255
7644
|
}
|
|
7256
7645
|
|
|
7257
7646
|
// src/tools/shell.ts
|
|
7258
|
-
import * as
|
|
7647
|
+
import * as pathMod8 from "path";
|
|
7259
7648
|
|
|
7260
7649
|
// src/tools/jobs.ts
|
|
7261
7650
|
import { spawn as spawn2 } from "child_process";
|
|
7262
|
-
import * as
|
|
7651
|
+
import * as pathMod5 from "path";
|
|
7263
7652
|
function killProcessTree(pid, signal) {
|
|
7264
7653
|
if (process.platform === "win32") {
|
|
7265
7654
|
const args = ["/pid", String(pid), "/T"];
|
|
@@ -7319,7 +7708,7 @@ var JobRegistry = class {
|
|
|
7319
7708
|
const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
|
|
7320
7709
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
7321
7710
|
const spawnOpts = {
|
|
7322
|
-
cwd:
|
|
7711
|
+
cwd: pathMod5.resolve(opts.cwd),
|
|
7323
7712
|
shell: false,
|
|
7324
7713
|
windowsHide: true,
|
|
7325
7714
|
env: process.env,
|
|
@@ -7605,12 +7994,12 @@ function latestOutputSince(before, after) {
|
|
|
7605
7994
|
// src/tools/shell/exec.ts
|
|
7606
7995
|
import { spawn as spawn4, spawnSync } from "child_process";
|
|
7607
7996
|
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
7608
|
-
import * as
|
|
7997
|
+
import * as pathMod7 from "path";
|
|
7609
7998
|
|
|
7610
7999
|
// src/tools/shell-chain.ts
|
|
7611
8000
|
import { spawn as spawn3 } from "child_process";
|
|
7612
8001
|
import { closeSync, openSync } from "fs";
|
|
7613
|
-
import * as
|
|
8002
|
+
import * as pathMod6 from "path";
|
|
7614
8003
|
var UnsupportedSyntaxError = class extends Error {
|
|
7615
8004
|
constructor(detail) {
|
|
7616
8005
|
super(`run_command: ${detail}`);
|
|
@@ -7877,7 +8266,7 @@ function openRedirects(redirects, cwd) {
|
|
|
7877
8266
|
let bothFd = null;
|
|
7878
8267
|
const toClose = [];
|
|
7879
8268
|
const open = (target, flags) => {
|
|
7880
|
-
const resolved =
|
|
8269
|
+
const resolved = pathMod6.resolve(cwd, target);
|
|
7881
8270
|
const fd = openSync(resolved, flags);
|
|
7882
8271
|
toClose.push(fd);
|
|
7883
8272
|
return fd;
|
|
@@ -8373,16 +8762,16 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
8373
8762
|
const platform = opts.platform ?? process.platform;
|
|
8374
8763
|
if (platform !== "win32") return cmd;
|
|
8375
8764
|
if (!cmd) return cmd;
|
|
8376
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
8377
|
-
if (
|
|
8765
|
+
if (cmd.includes("/") || cmd.includes("\\") || pathMod7.isAbsolute(cmd)) return cmd;
|
|
8766
|
+
if (pathMod7.extname(cmd)) return cmd;
|
|
8378
8767
|
const env = opts.env ?? process.env;
|
|
8379
8768
|
const pathExt = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
|
|
8380
|
-
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" :
|
|
8769
|
+
const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod7.delimiter);
|
|
8381
8770
|
const pathDirs = (env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
8382
8771
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
8383
8772
|
for (const dir of pathDirs) {
|
|
8384
8773
|
for (const ext of pathExt) {
|
|
8385
|
-
const full =
|
|
8774
|
+
const full = pathMod7.win32.join(dir, cmd + ext);
|
|
8386
8775
|
if (isFile(full)) return full;
|
|
8387
8776
|
}
|
|
8388
8777
|
}
|
|
@@ -8452,8 +8841,8 @@ function withUtf8Codepage(cmdline) {
|
|
|
8452
8841
|
function isBareWindowsName(s) {
|
|
8453
8842
|
if (!s) return false;
|
|
8454
8843
|
if (s.includes("/") || s.includes("\\")) return false;
|
|
8455
|
-
if (
|
|
8456
|
-
if (
|
|
8844
|
+
if (pathMod7.isAbsolute(s)) return false;
|
|
8845
|
+
if (pathMod7.extname(s)) return false;
|
|
8457
8846
|
return true;
|
|
8458
8847
|
}
|
|
8459
8848
|
function quoteForCmdExe(arg) {
|
|
@@ -8474,7 +8863,7 @@ var NeedsConfirmationError = class extends Error {
|
|
|
8474
8863
|
}
|
|
8475
8864
|
};
|
|
8476
8865
|
function registerShellTools(registry, opts) {
|
|
8477
|
-
const rootDir =
|
|
8866
|
+
const rootDir = pathMod8.resolve(opts.rootDir);
|
|
8478
8867
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
8479
8868
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
8480
8869
|
const jobs = opts.jobs ?? new JobRegistry();
|
|
@@ -9519,7 +9908,7 @@ function truncate(s, n) {
|
|
|
9519
9908
|
// src/version.ts
|
|
9520
9909
|
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
|
|
9521
9910
|
import { homedir as homedir6 } from "os";
|
|
9522
|
-
import { dirname as dirname6, join as
|
|
9911
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
9523
9912
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9524
9913
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
9525
9914
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -9528,7 +9917,7 @@ function readPackageVersion() {
|
|
|
9528
9917
|
try {
|
|
9529
9918
|
let dir = dirname6(fileURLToPath2(import.meta.url));
|
|
9530
9919
|
for (let i = 0; i < 6; i++) {
|
|
9531
|
-
const p =
|
|
9920
|
+
const p = join12(dir, "package.json");
|
|
9532
9921
|
if (existsSync9(p)) {
|
|
9533
9922
|
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
9534
9923
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
@@ -9545,7 +9934,7 @@ function readPackageVersion() {
|
|
|
9545
9934
|
}
|
|
9546
9935
|
var VERSION = readPackageVersion();
|
|
9547
9936
|
function cachePath(homeDirOverride) {
|
|
9548
|
-
return
|
|
9937
|
+
return join12(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
|
|
9549
9938
|
}
|
|
9550
9939
|
function readCache(homeDirOverride) {
|
|
9551
9940
|
try {
|
|
@@ -10539,8 +10928,8 @@ function lineEndingOf(text) {
|
|
|
10539
10928
|
|
|
10540
10929
|
// src/code/prompt.ts
|
|
10541
10930
|
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
10542
|
-
import { join as
|
|
10543
|
-
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, list_directory, directory_tree, search_files, search_content, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell.
|
|
10931
|
+
import { join as join13 } from "path";
|
|
10932
|
+
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, edit_file, multi_edit, list_directory, directory_tree, search_files, search_content, glob, get_file_info) rooted at the user's working directory, plus run_command / run_background for shell, plus \`todo_write\` for in-session multi-step tracking.
|
|
10544
10933
|
|
|
10545
10934
|
# Cite or shut up \u2014 non-negotiable
|
|
10546
10935
|
|
|
@@ -10588,10 +10977,23 @@ Skip it when one option is clearly correct (just do it, or submit_plan) or a fre
|
|
|
10588
10977
|
|
|
10589
10978
|
Each option: short stable id (A/B/C), one-line title, optional summary. \`allowCustom: true\` when their real answer might not fit. Max 6. A ~1-sentence lead-in before the call is fine ("I see three directions \u2014 letting you pick"); don't repeat the options in it. After the call, STOP.
|
|
10590
10979
|
|
|
10980
|
+
# When to track multi-step intent (todo_write)
|
|
10981
|
+
|
|
10982
|
+
\`todo_write\` is a lightweight in-session task tracker \u2014 NOT a plan. No approval gate, no checkpoint pauses, doesn't touch files. Use it when the task has 3+ distinct steps and you'd otherwise lose track of where you are. Each call REPLACES the entire list (set semantics). Exactly one item may be \`in_progress\` at a time \u2014 flip it to \`completed\` the moment that step's done, before starting the next.
|
|
10983
|
+
|
|
10984
|
+
Use it for:
|
|
10985
|
+
- Multi-part user requests ("do A, then B, then C") \u2014 record the parts so you don't drop one.
|
|
10986
|
+
- Long refactors where you've finished step 2 of 5 and want a visible record.
|
|
10987
|
+
- Any moment where you'd otherwise enumerate "1. ... 2. ... 3. ..." in prose \u2014 the tool is strictly better, the UI shows progress live.
|
|
10988
|
+
|
|
10989
|
+
Skip it for: one-shot edits, single-question answers, anything that fits in one tool call. Don't \`todo_write\` and \`submit_plan\` for the same work \u2014 \`submit_plan\` is for tasks that need a review gate; \`todo_write\` is for personal bookkeeping after the user has already given you the green light.
|
|
10990
|
+
|
|
10991
|
+
Call shape: \`{ todos: [{ content, activeForm, status }, ...] }\` \u2014 \`content\` is imperative ("Add tests"), \`activeForm\` is gerund ("Adding tests") shown while \`in_progress\`. Pass the FULL list every call, not a delta. Pass \`todos: []\` to clear when work's done.
|
|
10992
|
+
|
|
10591
10993
|
# Plan mode (/plan)
|
|
10592
10994
|
|
|
10593
10995
|
The user can ALSO enter "plan mode" via /plan, which is a stronger, explicit constraint:
|
|
10594
|
-
- Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
|
|
10996
|
+
- Write tools (edit_file, multi_edit, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch \u2014 you'll get a tool result like "unavailable in plan mode". Don't retry them.
|
|
10595
10997
|
- Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work \u2014 use them to investigate.
|
|
10596
10998
|
- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.
|
|
10597
10999
|
|
|
@@ -10660,6 +11062,7 @@ Rules:
|
|
|
10660
11062
|
>>>>>>> REPLACE
|
|
10661
11063
|
- Do NOT use write_file to change existing files \u2014 the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).
|
|
10662
11064
|
- Paths are relative to the working directory. Don't use absolute paths.
|
|
11065
|
+
- For multi-site changes \u2014 same file or across files \u2014 prefer \`multi_edit\` over N \`edit_file\` calls. Shape: \`{ edits: [{ path, search, replace }, ...] }\`. All edits validate before any file is written; any failure \u2192 ALL files untouched. Per-file edits run in array order, so a later edit can match text inserted by an earlier one.
|
|
10663
11066
|
|
|
10664
11067
|
# Trust what you already know
|
|
10665
11068
|
|
|
@@ -10669,7 +11072,7 @@ Before exploring the filesystem to answer a factual question, check whether the
|
|
|
10669
11072
|
|
|
10670
11073
|
- Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.
|
|
10671
11074
|
- Prefer \`search_files\` over \`list_directory\` when you know roughly what you're looking for \u2014 it saves context and avoids enumerating huge trees. Note: \`search_files\` matches file NAMES; for searching file CONTENTS use \`search_content\`.
|
|
10672
|
-
- Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
|
|
11075
|
+
- Available exploration tools: \`read_file\`, \`list_directory\`, \`directory_tree\`, \`search_files\` (filename match), \`glob\` (mtime-sorted glob \u2014 use for "what changed lately", "all *.ts under src/"), \`search_content\` (content grep \u2014 use for "where is X called", "find all references to Y"; pass \`context:N\` for grep -C N around hits), \`get_file_info\`. Don't call \`grep\` or other tools that aren't in this list \u2014 they don't exist as functions.
|
|
10673
11076
|
|
|
10674
11077
|
# Path conventions
|
|
10675
11078
|
|
|
@@ -10742,7 +11145,7 @@ If \`semantic_search\` returns nothing useful (low scores, off-topic), THEN fall
|
|
|
10742
11145
|
function codeSystemPrompt(rootDir, opts = {}) {
|
|
10743
11146
|
const base = opts.hasSemanticSearch ? `${CODE_SYSTEM_PROMPT}${SEMANTIC_SEARCH_ROUTING}` : CODE_SYSTEM_PROMPT;
|
|
10744
11147
|
const withMemory = applyMemoryStack(base, rootDir);
|
|
10745
|
-
const gitignorePath =
|
|
11148
|
+
const gitignorePath = join13(rootDir, ".gitignore");
|
|
10746
11149
|
let result = withMemory;
|
|
10747
11150
|
if (existsSync11(gitignorePath)) {
|
|
10748
11151
|
let content;
|
|
@@ -10793,9 +11196,9 @@ import {
|
|
|
10793
11196
|
writeFileSync as writeFileSync7
|
|
10794
11197
|
} from "fs";
|
|
10795
11198
|
import { homedir as homedir7 } from "os";
|
|
10796
|
-
import { dirname as dirname8, join as
|
|
11199
|
+
import { dirname as dirname8, join as join14 } from "path";
|
|
10797
11200
|
function defaultUsageLogPath(homeDirOverride) {
|
|
10798
|
-
return
|
|
11201
|
+
return join14(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
|
|
10799
11202
|
}
|
|
10800
11203
|
var USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
10801
11204
|
var USAGE_RETENTION_DAYS = 365;
|
|
@@ -11112,6 +11515,7 @@ export {
|
|
|
11112
11515
|
registerPlanTool,
|
|
11113
11516
|
registerShellTools,
|
|
11114
11517
|
registerSubagentTool,
|
|
11518
|
+
registerTodoTool,
|
|
11115
11519
|
registerWebTools,
|
|
11116
11520
|
renderMarkdown as renderDiffMarkdown,
|
|
11117
11521
|
renderSummaryTable as renderDiffSummary,
|