codemini-cli 0.6.3 → 0.6.4
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/codemini-web/dist/assets/{AboutDialog-jgqGjQgl.js → AboutDialog-MRopwNIL.js} +2 -2
- package/codemini-web/dist/assets/CodeWikiPanel-UpK5xGE3.js +1 -0
- package/codemini-web/dist/assets/ConfigDialog-CNl28wsj.js +1 -0
- package/codemini-web/dist/assets/GitDiffDialog-gSysUg2J.js +3 -0
- package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-DFUmo3Kl.js} +3 -3
- package/codemini-web/dist/assets/MessageBubble-CGnnViv0.js +12 -0
- package/codemini-web/dist/assets/PatchDiff-B8rwvEg5.js +230 -0
- package/codemini-web/dist/assets/ProjectSelector-BF59M1zb.js +1 -0
- package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-CQTjbSiw.js} +4 -4
- package/codemini-web/dist/assets/SoulDialog-BLjUGqqB.js +1 -0
- package/codemini-web/dist/assets/chevron-right--85xg7qk.js +1 -0
- package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-6uELoidu.js} +6 -6
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-gb1UMBZ5.js} +1 -1
- package/codemini-web/dist/assets/index-1xqD0R5t.css +2 -0
- package/codemini-web/dist/assets/index-CDXQGwPs.js +65 -0
- package/codemini-web/dist/assets/{input-CNQgbKe6.js → input-Ca8O_061.js} +1 -1
- package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-ROliF8Yd.js +1 -0
- package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BhT11Ztp.js} +1 -1
- package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-D7R5Lth6.js} +1 -1
- package/codemini-web/dist/assets/select-DBvcHBzs.js +1 -0
- package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-BfNZcWfX.js} +1 -1
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +325 -296
- package/codemini-web/server.js +310 -243
- package/package.json +1 -1
- package/src/core/agent-loop.js +188 -97
- package/src/core/chat-runtime.js +674 -571
- package/src/core/config-store.js +11 -3
- package/src/core/git-oplog-change-tracker.js +387 -0
- package/src/core/non-git-backup.js +116 -0
- package/src/core/paths.js +123 -123
- package/src/core/session-store.js +148 -99
- package/src/core/tools.js +499 -456
- package/src/tui/chat-app.js +196 -56
- package/codemini-web/dist/assets/CodeWikiPanel-EPuoerNv.js +0 -1
- package/codemini-web/dist/assets/ConfigDialog-B5IGZCc9.js +0 -1
- package/codemini-web/dist/assets/GitDiffDialog-Bb_Tw5ZK.js +0 -222
- package/codemini-web/dist/assets/MessageBubble-wUff4GP4.js +0 -6
- package/codemini-web/dist/assets/ProjectSelector-C0leTf6f.js +0 -1
- package/codemini-web/dist/assets/SoulDialog-XDTEGWvH.js +0 -1
- package/codemini-web/dist/assets/chevron-right-Dbzw7YzA.js +0 -1
- package/codemini-web/dist/assets/index-D0EGtNPr.js +0 -65
- package/codemini-web/dist/assets/index-wOUf3WkN.css +0 -2
- package/codemini-web/dist/assets/lib-BOngVP_M.js +0 -11
- package/codemini-web/dist/assets/lib-DrOTTm_N.js +0 -1
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-DrBu5KyC.js +0 -1
- package/codemini-web/dist/assets/select-BZXfigic.js +0 -1
package/src/core/tools.js
CHANGED
|
@@ -30,13 +30,13 @@ import {
|
|
|
30
30
|
sanitizeTextForModel,
|
|
31
31
|
summarizeRunOutput
|
|
32
32
|
} from './tool-output.js';
|
|
33
|
-
import {
|
|
34
|
-
normalizeFilePathValue,
|
|
35
|
-
normalizePathArgs,
|
|
36
|
-
parseInlineRangePath,
|
|
37
|
-
normalizePatternArgs,
|
|
38
|
-
normalizeReadArgs,
|
|
39
|
-
normalizeWebFetchArgs,
|
|
33
|
+
import {
|
|
34
|
+
normalizeFilePathValue,
|
|
35
|
+
normalizePathArgs,
|
|
36
|
+
parseInlineRangePath,
|
|
37
|
+
normalizePatternArgs,
|
|
38
|
+
normalizeReadArgs,
|
|
39
|
+
normalizeWebFetchArgs,
|
|
40
40
|
normalizeWebSearchArgs,
|
|
41
41
|
normalizeWriteArgs
|
|
42
42
|
} from './tool-args.js';
|
|
@@ -150,64 +150,64 @@ function trimLinePreview(line, maxLen = 180) {
|
|
|
150
150
|
return `${text.slice(0, maxLen - 3)}...`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function splitLines(text) {
|
|
154
|
-
return String(text || '').split('\n');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function buildDiffPreview(beforeContent, afterContent) {
|
|
158
|
-
const beforeLines = splitLines(beforeContent);
|
|
159
|
-
const afterLines = splitLines(afterContent);
|
|
160
|
-
let prefix = 0;
|
|
161
|
-
while (
|
|
162
|
-
prefix < beforeLines.length &&
|
|
163
|
-
prefix < afterLines.length &&
|
|
164
|
-
beforeLines[prefix] === afterLines[prefix]
|
|
165
|
-
) {
|
|
166
|
-
prefix += 1;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
let beforeEnd = beforeLines.length - 1;
|
|
170
|
-
let afterEnd = afterLines.length - 1;
|
|
171
|
-
while (
|
|
172
|
-
beforeEnd >= prefix &&
|
|
173
|
-
afterEnd >= prefix &&
|
|
174
|
-
beforeLines[beforeEnd] === afterLines[afterEnd]
|
|
175
|
-
) {
|
|
176
|
-
beforeEnd -= 1;
|
|
177
|
-
afterEnd -= 1;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const lines = [];
|
|
181
|
-
for (let i = prefix; i <= beforeEnd; i += 1) {
|
|
182
|
-
lines.push(`-${i + 1}| ${beforeLines[i]}`);
|
|
183
|
-
}
|
|
184
|
-
for (let i = prefix; i <= afterEnd; i += 1) {
|
|
185
|
-
lines.push(`+${i + 1}| ${afterLines[i]}`);
|
|
186
|
-
}
|
|
187
|
-
return lines.join('\n');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function clampNumber(value, min, max, fallback) {
|
|
191
|
-
const num = Number(value);
|
|
192
|
-
if (!Number.isFinite(num)) return fallback;
|
|
153
|
+
function splitLines(text) {
|
|
154
|
+
return String(text || '').split('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildDiffPreview(beforeContent, afterContent) {
|
|
158
|
+
const beforeLines = splitLines(beforeContent);
|
|
159
|
+
const afterLines = splitLines(afterContent);
|
|
160
|
+
let prefix = 0;
|
|
161
|
+
while (
|
|
162
|
+
prefix < beforeLines.length &&
|
|
163
|
+
prefix < afterLines.length &&
|
|
164
|
+
beforeLines[prefix] === afterLines[prefix]
|
|
165
|
+
) {
|
|
166
|
+
prefix += 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let beforeEnd = beforeLines.length - 1;
|
|
170
|
+
let afterEnd = afterLines.length - 1;
|
|
171
|
+
while (
|
|
172
|
+
beforeEnd >= prefix &&
|
|
173
|
+
afterEnd >= prefix &&
|
|
174
|
+
beforeLines[beforeEnd] === afterLines[afterEnd]
|
|
175
|
+
) {
|
|
176
|
+
beforeEnd -= 1;
|
|
177
|
+
afterEnd -= 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const lines = [];
|
|
181
|
+
for (let i = prefix; i <= beforeEnd; i += 1) {
|
|
182
|
+
lines.push(`-${i + 1}| ${beforeLines[i]}`);
|
|
183
|
+
}
|
|
184
|
+
for (let i = prefix; i <= afterEnd; i += 1) {
|
|
185
|
+
lines.push(`+${i + 1}| ${afterLines[i]}`);
|
|
186
|
+
}
|
|
187
|
+
return lines.join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function clampNumber(value, min, max, fallback) {
|
|
191
|
+
const num = Number(value);
|
|
192
|
+
if (!Number.isFinite(num)) return fallback;
|
|
193
193
|
return Math.min(max, Math.max(min, num));
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function normalizeWhitespace(value) {
|
|
197
|
-
return String(value || '')
|
|
198
|
-
.replace(/\s+/g, ' ')
|
|
199
|
-
.trim();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function semanticBoolean(value, fallback = false) {
|
|
203
|
-
if (typeof value === 'boolean') return value;
|
|
204
|
-
if (typeof value === 'number') return value !== 0;
|
|
205
|
-
const text = String(value ?? '').trim().toLowerCase();
|
|
206
|
-
if (!text) return fallback;
|
|
207
|
-
if (['true', '1', 'yes', 'y', 'on'].includes(text)) return true;
|
|
208
|
-
if (['false', '0', 'no', 'n', 'off'].includes(text)) return false;
|
|
209
|
-
return Boolean(value);
|
|
210
|
-
}
|
|
196
|
+
function normalizeWhitespace(value) {
|
|
197
|
+
return String(value || '')
|
|
198
|
+
.replace(/\s+/g, ' ')
|
|
199
|
+
.trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function semanticBoolean(value, fallback = false) {
|
|
203
|
+
if (typeof value === 'boolean') return value;
|
|
204
|
+
if (typeof value === 'number') return value !== 0;
|
|
205
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
206
|
+
if (!text) return fallback;
|
|
207
|
+
if (['true', '1', 'yes', 'y', 'on'].includes(text)) return true;
|
|
208
|
+
if (['false', '0', 'no', 'n', 'off'].includes(text)) return false;
|
|
209
|
+
return Boolean(value);
|
|
210
|
+
}
|
|
211
211
|
|
|
212
212
|
function trimPreview(value, maxLen = 300) {
|
|
213
213
|
const text = normalizeWhitespace(value);
|
|
@@ -900,7 +900,7 @@ async function getFileState(root, relativePath, config = {}) {
|
|
|
900
900
|
};
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
-
async function readFile(root, args, config = {}) {
|
|
903
|
+
async function readFile(root, args, config = {}) {
|
|
904
904
|
const normalizedArgs = normalizeReadArgs(args);
|
|
905
905
|
const target = await resolveInWorkspace(root, normalizedArgs?.path, config);
|
|
906
906
|
const stat = await fs.stat(target);
|
|
@@ -979,21 +979,21 @@ async function readFile(root, args, config = {}) {
|
|
|
979
979
|
truncated,
|
|
980
980
|
content,
|
|
981
981
|
...(enclosing ? { enclosing_symbol: enclosing.name, enclosing_kind: enclosing.kind, enclosing_line: enclosing.start_line } : {})
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
async function writeFile(root, args, config = {}) {
|
|
986
|
-
const normalizedArgs = normalizeWriteArgs(args);
|
|
987
|
-
const rawPath = String(normalizedArgs?.path || '').trim();
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function writeFile(root, args, config = {}) {
|
|
986
|
+
const normalizedArgs = normalizeWriteArgs(args);
|
|
987
|
+
const rawPath = String(normalizedArgs?.path || '').trim();
|
|
988
988
|
if (!rawPath) {
|
|
989
989
|
throw new Error('write requires a file path like weather/WeatherForecast.js');
|
|
990
990
|
}
|
|
991
|
-
if (rawPath === '.' || rawPath === './') {
|
|
992
|
-
throw new Error('write requires a file path, not the workspace root');
|
|
993
|
-
}
|
|
994
|
-
if (normalizedArgs?.content == null) {
|
|
995
|
-
throw new Error('write requires content. For existing files, use edit with old_text/new_text or pass content with full_file_rewrite=true.');
|
|
996
|
-
}
|
|
991
|
+
if (rawPath === '.' || rawPath === './') {
|
|
992
|
+
throw new Error('write requires a file path, not the workspace root');
|
|
993
|
+
}
|
|
994
|
+
if (normalizedArgs?.content == null) {
|
|
995
|
+
throw new Error('write requires content. For existing files, use edit with old_text/new_text or pass content with full_file_rewrite=true.');
|
|
996
|
+
}
|
|
997
997
|
const target = await resolveInWorkspace(root, rawPath, config);
|
|
998
998
|
try {
|
|
999
999
|
const stat = await fs.stat(target);
|
|
@@ -1007,33 +1007,33 @@ async function writeFile(root, args, config = {}) {
|
|
|
1007
1007
|
let existed = true;
|
|
1008
1008
|
try {
|
|
1009
1009
|
before = await fs.readFile(target, 'utf8');
|
|
1010
|
-
} catch {
|
|
1011
|
-
existed = false;
|
|
1012
|
-
}
|
|
1013
|
-
const nextContent = String(normalizedArgs.content ?? '');
|
|
1014
|
-
if (existed && before === nextContent && !normalizedArgs?.append) {
|
|
1015
|
-
return {
|
|
1016
|
-
ok: true,
|
|
1017
|
-
path: rawPath,
|
|
1018
|
-
action: 'unchanged',
|
|
1019
|
-
changed_line: 1,
|
|
1020
|
-
diff_preview: '',
|
|
1021
|
-
lines_added: 0,
|
|
1022
|
-
lines_removed: 0
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
if (existed && !normalizedArgs?.append && !normalizedArgs?.full_file_rewrite) {
|
|
1026
|
-
throw new Error(
|
|
1027
|
-
`write target exists: ${rawPath}. Use edit for source changes, append=true to append, or full_file_rewrite=true to replace the whole file.`
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
1031
|
-
if (normalizedArgs?.append) {
|
|
1032
|
-
await fs.appendFile(target, nextContent, 'utf8');
|
|
1033
|
-
} else {
|
|
1034
|
-
await fs.writeFile(target, nextContent, 'utf8');
|
|
1035
|
-
}
|
|
1036
|
-
const after = normalizedArgs?.append ? `${before}${nextContent}` : nextContent;
|
|
1010
|
+
} catch {
|
|
1011
|
+
existed = false;
|
|
1012
|
+
}
|
|
1013
|
+
const nextContent = String(normalizedArgs.content ?? '');
|
|
1014
|
+
if (existed && before === nextContent && !normalizedArgs?.append) {
|
|
1015
|
+
return {
|
|
1016
|
+
ok: true,
|
|
1017
|
+
path: rawPath,
|
|
1018
|
+
action: 'unchanged',
|
|
1019
|
+
changed_line: 1,
|
|
1020
|
+
diff_preview: '',
|
|
1021
|
+
lines_added: 0,
|
|
1022
|
+
lines_removed: 0
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
if (existed && !normalizedArgs?.append && !normalizedArgs?.full_file_rewrite) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`write target exists: ${rawPath}. Use edit for source changes, append=true to append, or full_file_rewrite=true to replace the whole file.`
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
1031
|
+
if (normalizedArgs?.append) {
|
|
1032
|
+
await fs.appendFile(target, nextContent, 'utf8');
|
|
1033
|
+
} else {
|
|
1034
|
+
await fs.writeFile(target, nextContent, 'utf8');
|
|
1035
|
+
}
|
|
1036
|
+
const after = normalizedArgs?.append ? `${before}${nextContent}` : nextContent;
|
|
1037
1037
|
const beforeLines = splitLines(before);
|
|
1038
1038
|
const afterLines = splitLines(after);
|
|
1039
1039
|
let changeLine = 0;
|
|
@@ -1044,17 +1044,17 @@ async function writeFile(root, args, config = {}) {
|
|
|
1044
1044
|
break;
|
|
1045
1045
|
}
|
|
1046
1046
|
}
|
|
1047
|
-
const changed = countChangedLines(before, after);
|
|
1048
|
-
return {
|
|
1049
|
-
ok: true,
|
|
1050
|
-
path: rawPath,
|
|
1051
|
-
action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
|
|
1052
|
-
changed_line: changeLine || Math.max(1, afterLines.length),
|
|
1053
|
-
diff_preview: buildDiffPreview(before, after),
|
|
1054
|
-
lines_added: changed.added,
|
|
1055
|
-
lines_removed: changed.removed
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1047
|
+
const changed = countChangedLines(before, after);
|
|
1048
|
+
return {
|
|
1049
|
+
ok: true,
|
|
1050
|
+
path: rawPath,
|
|
1051
|
+
action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
|
|
1052
|
+
changed_line: changeLine || Math.max(1, afterLines.length),
|
|
1053
|
+
diff_preview: buildDiffPreview(before, after),
|
|
1054
|
+
lines_added: changed.added,
|
|
1055
|
+
lines_removed: changed.removed
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
1058
|
|
|
1059
1059
|
async function prepareDeleteTarget(root, args, config = {}) {
|
|
1060
1060
|
const normalizedArgs = normalizePathArgs(args, ['file', 'file_path', 'target', 'directory', 'dir']);
|
|
@@ -1452,8 +1452,8 @@ async function stopBackgroundTask(_root, args) {
|
|
|
1452
1452
|
return { ...snapshotBackgroundTask(task), stopped: true };
|
|
1453
1453
|
}
|
|
1454
1454
|
|
|
1455
|
-
async function builtinGrep(root, args, config = {}) {
|
|
1456
|
-
const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd', 'file_path', 'file']);
|
|
1455
|
+
async function builtinGrep(root, args, config = {}) {
|
|
1456
|
+
const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd', 'file_path', 'file']);
|
|
1457
1457
|
const pattern = String(normalizedArgs?.pattern || '').trim();
|
|
1458
1458
|
if (!pattern) throw new Error('grep requires pattern');
|
|
1459
1459
|
const maxResults = Math.max(1, Math.min(200, Number(normalizedArgs?.max_results || 50)));
|
|
@@ -1487,8 +1487,8 @@ async function builtinGrep(root, args, config = {}) {
|
|
|
1487
1487
|
return { pattern, matches, truncated: false };
|
|
1488
1488
|
}
|
|
1489
1489
|
|
|
1490
|
-
async function builtinGlob(root, args, config = {}) {
|
|
1491
|
-
const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd', 'file_path', 'file']);
|
|
1490
|
+
async function builtinGlob(root, args, config = {}) {
|
|
1491
|
+
const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd', 'file_path', 'file']);
|
|
1492
1492
|
const pattern = String(normalizedArgs?.pattern || '').trim();
|
|
1493
1493
|
if (!pattern) throw new Error('glob requires pattern');
|
|
1494
1494
|
const maxResults = Math.max(1, Math.min(500, Number(normalizedArgs?.max_results || 200)));
|
|
@@ -1508,8 +1508,8 @@ async function builtinGlob(root, args, config = {}) {
|
|
|
1508
1508
|
};
|
|
1509
1509
|
}
|
|
1510
1510
|
|
|
1511
|
-
async function builtinList(root, args, config = {}) {
|
|
1512
|
-
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'file_path', 'file', 'target']);
|
|
1511
|
+
async function builtinList(root, args, config = {}) {
|
|
1512
|
+
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'file_path', 'file', 'target']);
|
|
1513
1513
|
const relativePath = String(normalizedArgs?.path || '.').trim() || '.';
|
|
1514
1514
|
const target = await resolveInWorkspace(root, relativePath, config);
|
|
1515
1515
|
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
@@ -1649,10 +1649,10 @@ function countChangedLines(beforeContent, afterContent) {
|
|
|
1649
1649
|
return { added: n - lcsLen, removed: m - lcsLen };
|
|
1650
1650
|
}
|
|
1651
1651
|
|
|
1652
|
-
function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
|
|
1653
|
-
const diffPreview = buildDiffPreview(beforeContent, afterContent);
|
|
1654
|
-
const changed = countChangedLines(beforeContent, afterContent);
|
|
1655
|
-
return {
|
|
1652
|
+
function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
|
|
1653
|
+
const diffPreview = buildDiffPreview(beforeContent, afterContent);
|
|
1654
|
+
const changed = countChangedLines(beforeContent, afterContent);
|
|
1655
|
+
return {
|
|
1656
1656
|
ok: true,
|
|
1657
1657
|
path: pathText,
|
|
1658
1658
|
action,
|
|
@@ -1662,26 +1662,26 @@ function editResult(pathText, action, beforeContent, afterContent, changedLine =
|
|
|
1662
1662
|
lines_added: changed.added,
|
|
1663
1663
|
lines_removed: changed.removed
|
|
1664
1664
|
};
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function lineRangeToOffsets(content, startLineRaw, endLineRaw) {
|
|
1668
|
-
const lines = splitLines(content);
|
|
1669
|
-
const totalLines = lines.length;
|
|
1670
|
-
const startLine = Math.max(1, Math.min(totalLines, Number(startLineRaw) || 1));
|
|
1671
|
-
const endLine = Math.max(startLine, Math.min(totalLines, Number(endLineRaw) || startLine));
|
|
1672
|
-
let startOffset = 0;
|
|
1673
|
-
for (let i = 1; i < startLine; i += 1) {
|
|
1674
|
-
startOffset += lines[i - 1].length + 1;
|
|
1675
|
-
}
|
|
1676
|
-
let endOffset = startOffset;
|
|
1677
|
-
for (let i = startLine; i <= endLine; i += 1) {
|
|
1678
|
-
endOffset += lines[i - 1].length;
|
|
1679
|
-
if (i < endLine) endOffset += 1;
|
|
1680
|
-
}
|
|
1681
|
-
return { startLine, endLine, startOffset, endOffset };
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
async function replaceBlock(root, args, config = {}) {
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function lineRangeToOffsets(content, startLineRaw, endLineRaw) {
|
|
1668
|
+
const lines = splitLines(content);
|
|
1669
|
+
const totalLines = lines.length;
|
|
1670
|
+
const startLine = Math.max(1, Math.min(totalLines, Number(startLineRaw) || 1));
|
|
1671
|
+
const endLine = Math.max(startLine, Math.min(totalLines, Number(endLineRaw) || startLine));
|
|
1672
|
+
let startOffset = 0;
|
|
1673
|
+
for (let i = 1; i < startLine; i += 1) {
|
|
1674
|
+
startOffset += lines[i - 1].length + 1;
|
|
1675
|
+
}
|
|
1676
|
+
let endOffset = startOffset;
|
|
1677
|
+
for (let i = startLine; i <= endLine; i += 1) {
|
|
1678
|
+
endOffset += lines[i - 1].length;
|
|
1679
|
+
if (i < endLine) endOffset += 1;
|
|
1680
|
+
}
|
|
1681
|
+
return { startLine, endLine, startOffset, endOffset };
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
async function replaceBlock(root, args, config = {}) {
|
|
1685
1685
|
const relativePath = String(args?.path || '').trim();
|
|
1686
1686
|
const newContent = String(args?.new_content || args?.content || '');
|
|
1687
1687
|
const target = args?.target || {};
|
|
@@ -1700,67 +1700,67 @@ async function replaceBlock(root, args, config = {}) {
|
|
|
1700
1700
|
return editResult(relativePath, 'replace_block', state.content, afterContent, resolved.start_line);
|
|
1701
1701
|
}
|
|
1702
1702
|
|
|
1703
|
-
async function replaceText(root, args, config = {}) {
|
|
1704
|
-
const relativePath = String(args?.path || '').trim();
|
|
1705
|
-
const oldText = String(args?.old_text || '');
|
|
1706
|
-
const newText = String(args?.new_text || '');
|
|
1707
|
-
const replaceAll = semanticBoolean(args?.replace_all ?? args?.replaceAll);
|
|
1708
|
-
const state = await getFileState(root, relativePath, config);
|
|
1709
|
-
if (!oldText) {
|
|
1710
|
-
throw new Error('replace_text requires old_text');
|
|
1711
|
-
}
|
|
1712
|
-
const rangeStart = Number(args?.start_line || args?.line);
|
|
1713
|
-
const rangeEnd = Number(args?.end_line || args?.line);
|
|
1714
|
-
const hasRange = Number.isFinite(rangeStart) && rangeStart > 0;
|
|
1715
|
-
const range = hasRange
|
|
1716
|
-
? lineRangeToOffsets(state.content, rangeStart, Number.isFinite(rangeEnd) && rangeEnd >= rangeStart ? rangeEnd : rangeStart)
|
|
1717
|
-
: null;
|
|
1718
|
-
const searchContent = range ? state.content.slice(range.startOffset, range.endOffset) : state.content;
|
|
1719
|
-
const occurrences = searchContent.split(oldText).length - 1;
|
|
1720
|
-
if (occurrences !== 1) {
|
|
1721
|
-
if (replaceAll && occurrences > 0) {
|
|
1722
|
-
const replaced = searchContent.replaceAll(oldText, newText);
|
|
1723
|
-
const afterContent = range
|
|
1724
|
-
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1725
|
-
: state.content.replaceAll(oldText, newText);
|
|
1726
|
-
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1727
|
-
const changedLine = range
|
|
1728
|
-
? range.startLine + splitLines(searchContent.slice(0, searchContent.indexOf(oldText))).length - 1
|
|
1729
|
-
: splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1730
|
-
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1731
|
-
}
|
|
1732
|
-
const baseLine = hasRange ? range.startLine : 1;
|
|
1733
|
-
const baseOffset = hasRange ? range.startOffset : 0;
|
|
1734
|
-
const lineDetails = [];
|
|
1735
|
-
let searchPos = 0;
|
|
1736
|
-
while (true) {
|
|
1737
|
-
const pos = searchContent.indexOf(oldText, searchPos);
|
|
1738
|
-
if (pos === -1) break;
|
|
1739
|
-
const lineNum = baseLine + splitLines(searchContent.slice(0, pos)).length - 1;
|
|
1740
|
-
const globalPos = baseOffset + pos;
|
|
1741
|
-
const lStart = state.content.lastIndexOf('\n', globalPos) + 1;
|
|
1742
|
-
const lEnd = state.content.indexOf('\n', globalPos);
|
|
1743
|
-
const lineText = state.content.slice(lStart, lEnd >= 0 ? lEnd : void 0).trim();
|
|
1744
|
-
lineDetails.push(` Line ${lineNum}: ${lineText}`);
|
|
1745
|
-
searchPos = pos + oldText.length;
|
|
1746
|
-
}
|
|
1747
|
-
const lineHint = lineDetails.length > 0 ? `\n${lineDetails.join('\n')}\n` : ' ';
|
|
1748
|
-
throw new Error(
|
|
1749
|
-
occurrences === 0
|
|
1750
|
-
? 'replace_text old_text not found'
|
|
1751
|
-
: `replace_text old_text not unique; found ${occurrences} occurrences:${lineHint}Use path:"${relativePath}:N-M" to narrow the range, set replace_all=true, or provide more unique old_text`
|
|
1752
|
-
);
|
|
1753
|
-
}
|
|
1754
|
-
const replaced = searchContent.replace(oldText, newText);
|
|
1755
|
-
const afterContent = range
|
|
1756
|
-
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1757
|
-
: state.content.replace(oldText, newText);
|
|
1758
|
-
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1759
|
-
const changedLine = range
|
|
1760
|
-
? range.startLine + splitLines(searchContent.slice(0, searchContent.indexOf(oldText))).length - 1
|
|
1761
|
-
: splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1762
|
-
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1763
|
-
}
|
|
1703
|
+
async function replaceText(root, args, config = {}) {
|
|
1704
|
+
const relativePath = String(args?.path || '').trim();
|
|
1705
|
+
const oldText = String(args?.old_text || '');
|
|
1706
|
+
const newText = String(args?.new_text || '');
|
|
1707
|
+
const replaceAll = semanticBoolean(args?.replace_all ?? args?.replaceAll);
|
|
1708
|
+
const state = await getFileState(root, relativePath, config);
|
|
1709
|
+
if (!oldText) {
|
|
1710
|
+
throw new Error('replace_text requires old_text');
|
|
1711
|
+
}
|
|
1712
|
+
const rangeStart = Number(args?.start_line || args?.line);
|
|
1713
|
+
const rangeEnd = Number(args?.end_line || args?.line);
|
|
1714
|
+
const hasRange = Number.isFinite(rangeStart) && rangeStart > 0;
|
|
1715
|
+
const range = hasRange
|
|
1716
|
+
? lineRangeToOffsets(state.content, rangeStart, Number.isFinite(rangeEnd) && rangeEnd >= rangeStart ? rangeEnd : rangeStart)
|
|
1717
|
+
: null;
|
|
1718
|
+
const searchContent = range ? state.content.slice(range.startOffset, range.endOffset) : state.content;
|
|
1719
|
+
const occurrences = searchContent.split(oldText).length - 1;
|
|
1720
|
+
if (occurrences !== 1) {
|
|
1721
|
+
if (replaceAll && occurrences > 0) {
|
|
1722
|
+
const replaced = searchContent.replaceAll(oldText, newText);
|
|
1723
|
+
const afterContent = range
|
|
1724
|
+
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1725
|
+
: state.content.replaceAll(oldText, newText);
|
|
1726
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1727
|
+
const changedLine = range
|
|
1728
|
+
? range.startLine + splitLines(searchContent.slice(0, searchContent.indexOf(oldText))).length - 1
|
|
1729
|
+
: splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1730
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1731
|
+
}
|
|
1732
|
+
const baseLine = hasRange ? range.startLine : 1;
|
|
1733
|
+
const baseOffset = hasRange ? range.startOffset : 0;
|
|
1734
|
+
const lineDetails = [];
|
|
1735
|
+
let searchPos = 0;
|
|
1736
|
+
while (true) {
|
|
1737
|
+
const pos = searchContent.indexOf(oldText, searchPos);
|
|
1738
|
+
if (pos === -1) break;
|
|
1739
|
+
const lineNum = baseLine + splitLines(searchContent.slice(0, pos)).length - 1;
|
|
1740
|
+
const globalPos = baseOffset + pos;
|
|
1741
|
+
const lStart = state.content.lastIndexOf('\n', globalPos) + 1;
|
|
1742
|
+
const lEnd = state.content.indexOf('\n', globalPos);
|
|
1743
|
+
const lineText = state.content.slice(lStart, lEnd >= 0 ? lEnd : void 0).trim();
|
|
1744
|
+
lineDetails.push(` Line ${lineNum}: ${lineText}`);
|
|
1745
|
+
searchPos = pos + oldText.length;
|
|
1746
|
+
}
|
|
1747
|
+
const lineHint = lineDetails.length > 0 ? `\n${lineDetails.join('\n')}\n` : ' ';
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
occurrences === 0
|
|
1750
|
+
? 'replace_text old_text not found'
|
|
1751
|
+
: `replace_text old_text not unique; found ${occurrences} occurrences:${lineHint}Use path:"${relativePath}:N-M" to narrow the range, set replace_all=true, or provide more unique old_text`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const replaced = searchContent.replace(oldText, newText);
|
|
1755
|
+
const afterContent = range
|
|
1756
|
+
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1757
|
+
: state.content.replace(oldText, newText);
|
|
1758
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1759
|
+
const changedLine = range
|
|
1760
|
+
? range.startLine + splitLines(searchContent.slice(0, searchContent.indexOf(oldText))).length - 1
|
|
1761
|
+
: splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1762
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1763
|
+
}
|
|
1764
1764
|
|
|
1765
1765
|
async function insertRelative(root, args, mode, config = {}) {
|
|
1766
1766
|
const relativePath = String(args?.path || '').trim();
|
|
@@ -1807,62 +1807,62 @@ async function openTarget(root, args, config = {}) {
|
|
|
1807
1807
|
};
|
|
1808
1808
|
}
|
|
1809
1809
|
|
|
1810
|
-
function normalizeEditTargetArgs(args = {}) {
|
|
1811
|
-
const rawFile = String(args?.file || args?.path || args?.file_path || '').trim();
|
|
1812
|
-
const inlineRange = parseInlineRangePath(rawFile);
|
|
1813
|
-
const file = normalizeFilePathValue(rawFile, { stripInlineRange: true }).trim();
|
|
1814
|
-
const nestedEdit = args?.edit && typeof args.edit === 'object' ? args.edit : null;
|
|
1815
|
-
const startLine = args?.start_line ?? args?.line ?? inlineRange?.start_line;
|
|
1816
|
-
const endLine = args?.end_line ?? inlineRange?.end_line ?? args?.line;
|
|
1817
|
-
if (nestedEdit) {
|
|
1818
|
-
const normalizedEdit = { ...nestedEdit };
|
|
1819
|
-
if (normalizedEdit.new_content == null && normalizedEdit.content != null) {
|
|
1820
|
-
normalizedEdit.new_content = normalizedEdit.content;
|
|
1821
|
-
}
|
|
1822
|
-
if (normalizedEdit.old_text == null && normalizedEdit.old_string != null) {
|
|
1823
|
-
normalizedEdit.old_text = normalizedEdit.old_string;
|
|
1824
|
-
}
|
|
1825
|
-
if (normalizedEdit.new_text == null && normalizedEdit.content != null && normalizedEdit.old_text != null) {
|
|
1826
|
-
normalizedEdit.new_text = normalizedEdit.content;
|
|
1827
|
-
}
|
|
1828
|
-
if (normalizedEdit.new_text == null && normalizedEdit.new_string != null) {
|
|
1829
|
-
normalizedEdit.new_text = normalizedEdit.new_string;
|
|
1830
|
-
}
|
|
1831
|
-
return {
|
|
1832
|
-
path: file,
|
|
1833
|
-
file,
|
|
1834
|
-
start_line: startLine,
|
|
1835
|
-
end_line: endLine,
|
|
1836
|
-
ast_target: normalizedEdit.ast_target ?? args?.ast_target,
|
|
1837
|
-
edit: normalizedEdit
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1810
|
+
function normalizeEditTargetArgs(args = {}) {
|
|
1811
|
+
const rawFile = String(args?.file || args?.path || args?.file_path || '').trim();
|
|
1812
|
+
const inlineRange = parseInlineRangePath(rawFile);
|
|
1813
|
+
const file = normalizeFilePathValue(rawFile, { stripInlineRange: true }).trim();
|
|
1814
|
+
const nestedEdit = args?.edit && typeof args.edit === 'object' ? args.edit : null;
|
|
1815
|
+
const startLine = args?.start_line ?? args?.line ?? inlineRange?.start_line;
|
|
1816
|
+
const endLine = args?.end_line ?? inlineRange?.end_line ?? args?.line;
|
|
1817
|
+
if (nestedEdit) {
|
|
1818
|
+
const normalizedEdit = { ...nestedEdit };
|
|
1819
|
+
if (normalizedEdit.new_content == null && normalizedEdit.content != null) {
|
|
1820
|
+
normalizedEdit.new_content = normalizedEdit.content;
|
|
1821
|
+
}
|
|
1822
|
+
if (normalizedEdit.old_text == null && normalizedEdit.old_string != null) {
|
|
1823
|
+
normalizedEdit.old_text = normalizedEdit.old_string;
|
|
1824
|
+
}
|
|
1825
|
+
if (normalizedEdit.new_text == null && normalizedEdit.content != null && normalizedEdit.old_text != null) {
|
|
1826
|
+
normalizedEdit.new_text = normalizedEdit.content;
|
|
1827
|
+
}
|
|
1828
|
+
if (normalizedEdit.new_text == null && normalizedEdit.new_string != null) {
|
|
1829
|
+
normalizedEdit.new_text = normalizedEdit.new_string;
|
|
1830
|
+
}
|
|
1831
|
+
return {
|
|
1832
|
+
path: file,
|
|
1833
|
+
file,
|
|
1834
|
+
start_line: startLine,
|
|
1835
|
+
end_line: endLine,
|
|
1836
|
+
ast_target: normalizedEdit.ast_target ?? args?.ast_target,
|
|
1837
|
+
edit: normalizedEdit
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
1840
|
const topLevelOldText = args?.old_text ?? args?.old_string;
|
|
1841
1841
|
const topLevelContent = args?.content;
|
|
1842
|
-
return {
|
|
1843
|
-
path: file,
|
|
1844
|
-
file,
|
|
1845
|
-
start_line: startLine,
|
|
1846
|
-
end_line: endLine,
|
|
1847
|
-
ast_target: args?.ast_target,
|
|
1848
|
-
edit: {
|
|
1849
|
-
kind: args?.kind,
|
|
1850
|
-
target: args?.target,
|
|
1842
|
+
return {
|
|
1843
|
+
path: file,
|
|
1844
|
+
file,
|
|
1845
|
+
start_line: startLine,
|
|
1846
|
+
end_line: endLine,
|
|
1847
|
+
ast_target: args?.ast_target,
|
|
1848
|
+
edit: {
|
|
1849
|
+
kind: args?.kind,
|
|
1850
|
+
target: args?.target,
|
|
1851
1851
|
new_content: args?.new_content ?? args?.content,
|
|
1852
1852
|
old_text: args?.old_text,
|
|
1853
1853
|
new_text: args?.new_text ?? (topLevelOldText != null && topLevelContent != null ? topLevelContent : undefined),
|
|
1854
|
-
old_string: args?.old_string,
|
|
1855
|
-
new_string: args?.new_string,
|
|
1856
|
-
anchor_text: args?.anchor_text,
|
|
1857
|
-
content: args?.content,
|
|
1858
|
-
replace_all: args?.replace_all ?? args?.replaceAll
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1854
|
+
old_string: args?.old_string,
|
|
1855
|
+
new_string: args?.new_string,
|
|
1856
|
+
anchor_text: args?.anchor_text,
|
|
1857
|
+
content: args?.content,
|
|
1858
|
+
replace_all: args?.replace_all ?? args?.replaceAll
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
1862
|
|
|
1863
1863
|
async function editTarget(root, args, config = {}) {
|
|
1864
|
-
const normalized = normalizeEditTargetArgs(args);
|
|
1865
|
-
const file = normalized.file || normalizeFilePathValue(args?.recent_file || '', { stripInlineRange: true }).trim();
|
|
1864
|
+
const normalized = normalizeEditTargetArgs(args);
|
|
1865
|
+
const file = normalized.file || normalizeFilePathValue(args?.recent_file || '', { stripInlineRange: true }).trim();
|
|
1866
1866
|
const astTarget = normalized.ast_target;
|
|
1867
1867
|
const edit = normalized.edit || {};
|
|
1868
1868
|
let kind = String(edit.kind || '').trim();
|
|
@@ -1872,8 +1872,8 @@ async function editTarget(root, args, config = {}) {
|
|
|
1872
1872
|
if (edit.new_text == null && edit.new_string != null) {
|
|
1873
1873
|
edit.new_text = edit.new_string;
|
|
1874
1874
|
}
|
|
1875
|
-
const hasContent = edit.new_content != null || edit.content != null;
|
|
1876
|
-
const hasExplicitRewrite = edit.kind === 'rewrite_file' || args?.kind === 'rewrite_file';
|
|
1875
|
+
const hasContent = edit.new_content != null || edit.content != null;
|
|
1876
|
+
const hasExplicitRewrite = edit.kind === 'rewrite_file' || args?.kind === 'rewrite_file';
|
|
1877
1877
|
const hasTargetHint = Boolean(edit.symbol || args?.symbol || edit.line || args?.line || edit.target);
|
|
1878
1878
|
if (!kind) {
|
|
1879
1879
|
if (hasContent && hasTargetHint) {
|
|
@@ -1882,23 +1882,23 @@ async function editTarget(root, args, config = {}) {
|
|
|
1882
1882
|
kind = 'replace_text';
|
|
1883
1883
|
} else if ((edit.anchor_text != null || edit.target_text != null) && (edit.content != null || edit.new_content != null)) {
|
|
1884
1884
|
kind = String(edit.position || edit.mode || args?.position || '').trim() === 'after' ? 'insert_after' : 'insert_before';
|
|
1885
|
-
} else if (hasContent && hasExplicitRewrite) {
|
|
1886
|
-
kind = 'rewrite_file';
|
|
1887
|
-
}
|
|
1885
|
+
} else if (hasContent && hasExplicitRewrite) {
|
|
1886
|
+
kind = 'rewrite_file';
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (!file || !kind) {
|
|
1890
|
+
const recentFile = String(args?.recent_file || '').trim();
|
|
1891
|
+
const rawArgs = typeof args?._raw === 'string' && args._raw.trim() ? ` Raw tool arguments: ${args._raw.trim()}.` : '';
|
|
1892
|
+
const missing = !file
|
|
1893
|
+
? 'file path'
|
|
1894
|
+
: edit.old_text != null && edit.new_text == null && edit.content == null
|
|
1895
|
+
? 'new_text'
|
|
1896
|
+
: 'edit operation';
|
|
1897
|
+
const hint = recentFile
|
|
1898
|
+
? ` If you meant the recently read file ${recentFile}, use edit with {file:"${recentFile}", old_text:"...", new_text:"..."} for a text replacement, or {file:"${recentFile}", kind:"rewrite_file", new_content:"..."} for a full rewrite.`
|
|
1899
|
+
: ' Use edit with {file:"path", old_text:"...", new_text:"..."} for a text replacement, or {file:"path", kind:"rewrite_file", new_content:"..."} for a full rewrite.';
|
|
1900
|
+
throw new Error(`edit requires ${missing}.${rawArgs}${hint}`);
|
|
1888
1901
|
}
|
|
1889
|
-
if (!file || !kind) {
|
|
1890
|
-
const recentFile = String(args?.recent_file || '').trim();
|
|
1891
|
-
const rawArgs = typeof args?._raw === 'string' && args._raw.trim() ? ` Raw tool arguments: ${args._raw.trim()}.` : '';
|
|
1892
|
-
const missing = !file
|
|
1893
|
-
? 'file path'
|
|
1894
|
-
: edit.old_text != null && edit.new_text == null && edit.content == null
|
|
1895
|
-
? 'new_text'
|
|
1896
|
-
: 'edit operation';
|
|
1897
|
-
const hint = recentFile
|
|
1898
|
-
? ` If you meant the recently read file ${recentFile}, use edit with {file:"${recentFile}", old_text:"...", new_text:"..."} for a text replacement, or {file:"${recentFile}", kind:"rewrite_file", new_content:"..."} for a full rewrite.`
|
|
1899
|
-
: ' Use edit with {file:"path", old_text:"...", new_text:"..."} for a text replacement, or {file:"path", kind:"rewrite_file", new_content:"..."} for a full rewrite.';
|
|
1900
|
-
throw new Error(`edit requires ${missing}.${rawArgs}${hint}`);
|
|
1901
|
-
}
|
|
1902
1902
|
if (astTarget) {
|
|
1903
1903
|
if (kind !== 'replace_block') {
|
|
1904
1904
|
throw new Error('AST-scoped edit only supports replace_block');
|
|
@@ -1942,16 +1942,16 @@ async function editTarget(root, args, config = {}) {
|
|
|
1942
1942
|
}, config);
|
|
1943
1943
|
}
|
|
1944
1944
|
}
|
|
1945
|
-
if (kind === 'replace_text') {
|
|
1946
|
-
return replaceText(root, {
|
|
1947
|
-
path: file,
|
|
1948
|
-
old_text: edit.old_text,
|
|
1949
|
-
new_text: edit.new_text,
|
|
1950
|
-
replace_all: edit.replace_all ?? args?.replace_all ?? args?.replaceAll,
|
|
1951
|
-
start_line: edit.start_line ?? normalized.start_line,
|
|
1952
|
-
end_line: edit.end_line ?? normalized.end_line
|
|
1953
|
-
}, config);
|
|
1954
|
-
}
|
|
1945
|
+
if (kind === 'replace_text') {
|
|
1946
|
+
return replaceText(root, {
|
|
1947
|
+
path: file,
|
|
1948
|
+
old_text: edit.old_text,
|
|
1949
|
+
new_text: edit.new_text,
|
|
1950
|
+
replace_all: edit.replace_all ?? args?.replace_all ?? args?.replaceAll,
|
|
1951
|
+
start_line: edit.start_line ?? normalized.start_line,
|
|
1952
|
+
end_line: edit.end_line ?? normalized.end_line
|
|
1953
|
+
}, config);
|
|
1954
|
+
}
|
|
1955
1955
|
if (kind === 'insert_before') {
|
|
1956
1956
|
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_before', config);
|
|
1957
1957
|
}
|
|
@@ -1968,19 +1968,19 @@ async function editTarget(root, args, config = {}) {
|
|
|
1968
1968
|
throw new Error(`edit does not support kind: ${kind}`);
|
|
1969
1969
|
}
|
|
1970
1970
|
|
|
1971
|
-
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate, fffAdapter }) {
|
|
1971
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate, fffAdapter, backupManager }) {
|
|
1972
1972
|
const emitSystemTool = (event) => {
|
|
1973
1973
|
if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
|
|
1974
1974
|
};
|
|
1975
|
-
const astSelectionCache = new Map();
|
|
1976
|
-
let lastAstTarget = null;
|
|
1977
|
-
let lastReadPath = '';
|
|
1978
|
-
let lastReadRange = null;
|
|
1979
|
-
const rememberAstSelection = (filePath, astTarget) => {
|
|
1980
|
-
const key = normalizePath(filePath).trim();
|
|
1981
|
-
if (!key || !astTarget) return;
|
|
1982
|
-
lastAstTarget = astTarget;
|
|
1983
|
-
astSelectionCache.set(key, astTarget);
|
|
1975
|
+
const astSelectionCache = new Map();
|
|
1976
|
+
let lastAstTarget = null;
|
|
1977
|
+
let lastReadPath = '';
|
|
1978
|
+
let lastReadRange = null;
|
|
1979
|
+
const rememberAstSelection = (filePath, astTarget) => {
|
|
1980
|
+
const key = normalizePath(filePath).trim();
|
|
1981
|
+
if (!key || !astTarget) return;
|
|
1982
|
+
lastAstTarget = astTarget;
|
|
1983
|
+
astSelectionCache.set(key, astTarget);
|
|
1984
1984
|
};
|
|
1985
1985
|
const hasExplicitBlockHints = (args = {}) =>
|
|
1986
1986
|
Boolean(
|
|
@@ -1993,15 +1993,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1993
1993
|
args?.edit?.line ||
|
|
1994
1994
|
args?.edit?.target
|
|
1995
1995
|
);
|
|
1996
|
-
const resolveCachedAstTarget = (args = {}, { requireAstScope = false } = {}) => {
|
|
1997
|
-
const file = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.ast_target?.path || '', { stripInlineRange: true }).trim();
|
|
1998
|
-
if (args?.ast_target) return args.ast_target;
|
|
1999
|
-
if (file) {
|
|
2000
|
-
if (requireAstScope && hasExplicitBlockHints(args)) return null;
|
|
2001
|
-
return astSelectionCache.get(file) || null;
|
|
2002
|
-
}
|
|
2003
|
-
return lastAstTarget || null;
|
|
2004
|
-
};
|
|
1996
|
+
const resolveCachedAstTarget = (args = {}, { requireAstScope = false } = {}) => {
|
|
1997
|
+
const file = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.ast_target?.path || '', { stripInlineRange: true }).trim();
|
|
1998
|
+
if (args?.ast_target) return args.ast_target;
|
|
1999
|
+
if (file) {
|
|
2000
|
+
if (requireAstScope && hasExplicitBlockHints(args)) return null;
|
|
2001
|
+
return astSelectionCache.get(file) || null;
|
|
2002
|
+
}
|
|
2003
|
+
return lastAstTarget || null;
|
|
2004
|
+
};
|
|
2005
2005
|
const ensureProjectIndex = async () => {
|
|
2006
2006
|
const eventId = `project-index:${Date.now()}`;
|
|
2007
2007
|
const name = 'project_index(.codemini/project-map.json,.codemini/file-index.json)';
|
|
@@ -2053,17 +2053,17 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2053
2053
|
{
|
|
2054
2054
|
type: 'function',
|
|
2055
2055
|
function: {
|
|
2056
|
-
name: 'read',
|
|
2057
|
-
description:
|
|
2058
|
-
'Inspect code or text files. Use {path} for normal reads; file_path/file are accepted aliases. Use start_line/end_line or path:"src/app.ts:10-40" for ranges. Normal code reads include enclosing symbol metadata when available; read with query returns the matched AST node and ast_target.',
|
|
2059
|
-
parameters: {
|
|
2060
|
-
type: 'object',
|
|
2061
|
-
properties: {
|
|
2062
|
-
path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
|
|
2063
|
-
file_path: { type: 'string', description: 'Alias for path' },
|
|
2064
|
-
file: { type: 'string', description: 'Alias for path' },
|
|
2065
|
-
start_line: { type: 'number', description: '1-based start line' },
|
|
2066
|
-
end_line: { type: 'number', description: 'Inclusive end line' },
|
|
2056
|
+
name: 'read',
|
|
2057
|
+
description:
|
|
2058
|
+
'Inspect code or text files. Use {path} for normal reads; file_path/file are accepted aliases. Use start_line/end_line or path:"src/app.ts:10-40" for ranges. Normal code reads include enclosing symbol metadata when available; read with query returns the matched AST node and ast_target.',
|
|
2059
|
+
parameters: {
|
|
2060
|
+
type: 'object',
|
|
2061
|
+
properties: {
|
|
2062
|
+
path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
|
|
2063
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2064
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2065
|
+
start_line: { type: 'number', description: '1-based start line' },
|
|
2066
|
+
end_line: { type: 'number', description: 'Inclusive end line' },
|
|
2067
2067
|
max_chars: { type: 'number', description: 'Max chars to return' },
|
|
2068
2068
|
ast_target: { type: 'object', description: 'AST target from ast_query or a prior AST selection. When provided, read returns that node instead of a line window.' },
|
|
2069
2069
|
query: { type: 'string', description: 'Optional Tree-sitter query to run inline before reading the first matched AST node. Use with path for one-shot function/class/method reads.' },
|
|
@@ -2082,10 +2082,10 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2082
2082
|
'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
|
|
2083
2083
|
parameters: {
|
|
2084
2084
|
type: 'object',
|
|
2085
|
-
properties: {
|
|
2086
|
-
pattern: { type: 'string', description: 'Search pattern' },
|
|
2087
|
-
path: { type: 'string', description: 'Directory or file to search. file_path/file/dir/directory/cwd are accepted aliases.' },
|
|
2088
|
-
regex: { type: 'boolean', description: 'Treat pattern as regex' },
|
|
2085
|
+
properties: {
|
|
2086
|
+
pattern: { type: 'string', description: 'Search pattern' },
|
|
2087
|
+
path: { type: 'string', description: 'Directory or file to search. file_path/file/dir/directory/cwd are accepted aliases.' },
|
|
2088
|
+
regex: { type: 'boolean', description: 'Treat pattern as regex' },
|
|
2089
2089
|
case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
|
|
2090
2090
|
max_results: { type: 'number', description: 'Max matches to return' },
|
|
2091
2091
|
language: { type: 'string', description: 'Filter by language' },
|
|
@@ -2102,9 +2102,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2102
2102
|
description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
|
|
2103
2103
|
parameters: {
|
|
2104
2104
|
type: 'object',
|
|
2105
|
-
properties: {
|
|
2106
|
-
path: { type: 'string', description: 'Directory path to list. file_path/file/dir/directory are accepted aliases.' },
|
|
2107
|
-
include_hidden: { type: 'boolean', description: 'Include dotfiles' }
|
|
2105
|
+
properties: {
|
|
2106
|
+
path: { type: 'string', description: 'Directory path to list. file_path/file/dir/directory are accepted aliases.' },
|
|
2107
|
+
include_hidden: { type: 'boolean', description: 'Include dotfiles' }
|
|
2108
2108
|
}
|
|
2109
2109
|
}
|
|
2110
2110
|
}
|
|
@@ -2112,9 +2112,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2112
2112
|
{
|
|
2113
2113
|
type: 'function',
|
|
2114
2114
|
function: {
|
|
2115
|
-
name: 'query_project_index',
|
|
2116
|
-
description:
|
|
2117
|
-
'Query the lightweight project index before broad file reads. Returns relevant files plus Symbol Graph summaries: symbol_id, type, range, signature, calls, called_by, imports, writes, and emits.',
|
|
2115
|
+
name: 'query_project_index',
|
|
2116
|
+
description:
|
|
2117
|
+
'Query the lightweight project index before broad file reads. Returns relevant files plus Symbol Graph summaries: symbol_id, type, range, signature, calls, called_by, imports, writes, and emits.',
|
|
2118
2118
|
parameters: {
|
|
2119
2119
|
type: 'object',
|
|
2120
2120
|
properties: {
|
|
@@ -2130,24 +2130,24 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2130
2130
|
{
|
|
2131
2131
|
type: 'function',
|
|
2132
2132
|
function: {
|
|
2133
|
-
name: 'edit',
|
|
2134
|
-
description:
|
|
2135
|
-
'Edit existing files. Prefer {path, old_text, new_text}; old_string/new_string and file_path/file are accepted aliases. If old_text is repeated, use path:"file:10-30" or rely on the most recent read range. Set replace_all=true to replace every match. Advanced kind/ast_target edits are still supported.',
|
|
2136
|
-
parameters: {
|
|
2137
|
-
type: 'object',
|
|
2138
|
-
properties: {
|
|
2139
|
-
path: { type: 'string', description: 'File path to edit. Inline ranges like src/app.js:10-30 are accepted.' },
|
|
2140
|
-
file_path: { type: 'string', description: 'Alias for path' },
|
|
2141
|
-
file: { type: 'string', description: 'Alias for path' },
|
|
2142
|
-
new_content: { type: 'string', description: 'Replacement content' },
|
|
2143
|
-
old_text: { type: 'string', description: 'Exact text to replace' },
|
|
2144
|
-
new_text: { type: 'string', description: 'Replacement text' },
|
|
2145
|
-
old_string: { type: 'string', description: 'Alias for old_text' },
|
|
2146
|
-
new_string: { type: 'string', description: 'Alias for new_text' },
|
|
2147
|
-
replace_all: { type: 'boolean', description: 'Replace all matching old_text occurrences' },
|
|
2148
|
-
start_line: { type: 'number', description: 'Optional range start for disambiguating old_text' },
|
|
2149
|
-
end_line: { type: 'number', description: 'Optional range end for disambiguating old_text' },
|
|
2150
|
-
anchor_text: { type: 'string', description: 'Anchor text for inserts' },
|
|
2133
|
+
name: 'edit',
|
|
2134
|
+
description:
|
|
2135
|
+
'Edit existing files. Prefer {path, old_text, new_text}; old_string/new_string and file_path/file are accepted aliases. If old_text is repeated, use path:"file:10-30" or rely on the most recent read range. Set replace_all=true to replace every match. Advanced kind/ast_target edits are still supported.',
|
|
2136
|
+
parameters: {
|
|
2137
|
+
type: 'object',
|
|
2138
|
+
properties: {
|
|
2139
|
+
path: { type: 'string', description: 'File path to edit. Inline ranges like src/app.js:10-30 are accepted.' },
|
|
2140
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2141
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2142
|
+
new_content: { type: 'string', description: 'Replacement content' },
|
|
2143
|
+
old_text: { type: 'string', description: 'Exact text to replace' },
|
|
2144
|
+
new_text: { type: 'string', description: 'Replacement text' },
|
|
2145
|
+
old_string: { type: 'string', description: 'Alias for old_text' },
|
|
2146
|
+
new_string: { type: 'string', description: 'Alias for new_text' },
|
|
2147
|
+
replace_all: { type: 'boolean', description: 'Replace all matching old_text occurrences' },
|
|
2148
|
+
start_line: { type: 'number', description: 'Optional range start for disambiguating old_text' },
|
|
2149
|
+
end_line: { type: 'number', description: 'Optional range end for disambiguating old_text' },
|
|
2150
|
+
anchor_text: { type: 'string', description: 'Anchor text for inserts' },
|
|
2151
2151
|
content: { type: 'string', description: 'Content to insert or append' },
|
|
2152
2152
|
position: { type: 'string', description: 'before or after' },
|
|
2153
2153
|
kind: { type: 'string', description: 'replace_block, replace_text, insert_before, insert_after, or rewrite_file' },
|
|
@@ -2157,23 +2157,23 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2157
2157
|
line: { type: 'number', description: 'Line to target' },
|
|
2158
2158
|
edit: { type: 'object', description: 'Structured edit input' }
|
|
2159
2159
|
},
|
|
2160
|
-
required: ['path', 'content']
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
},
|
|
2160
|
+
required: ['path', 'content']
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
},
|
|
2164
2164
|
{
|
|
2165
2165
|
type: 'function',
|
|
2166
2166
|
function: {
|
|
2167
|
-
name: 'write',
|
|
2168
|
-
description:
|
|
2169
|
-
'Create a new file, append to a file, or perform an explicit whole-file rewrite. Always include path and content; file_path/file are accepted aliases. For existing files, prefer edit after reading the relevant range. Overwriting an existing file requires full_file_rewrite=true.',
|
|
2170
|
-
parameters: {
|
|
2171
|
-
type: 'object',
|
|
2172
|
-
properties: {
|
|
2173
|
-
path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
|
|
2174
|
-
file_path: { type: 'string', description: 'Alias for path' },
|
|
2175
|
-
file: { type: 'string', description: 'Alias for path' },
|
|
2176
|
-
content: { type: 'string', description: 'Content to write' },
|
|
2167
|
+
name: 'write',
|
|
2168
|
+
description:
|
|
2169
|
+
'Create a new file, append to a file, or perform an explicit whole-file rewrite. Always include path and content; file_path/file are accepted aliases. For existing files, prefer edit after reading the relevant range. Overwriting an existing file requires full_file_rewrite=true.',
|
|
2170
|
+
parameters: {
|
|
2171
|
+
type: 'object',
|
|
2172
|
+
properties: {
|
|
2173
|
+
path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
|
|
2174
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2175
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2176
|
+
content: { type: 'string', description: 'Content to write' },
|
|
2177
2177
|
append: { type: 'boolean', description: 'Append instead of overwrite' },
|
|
2178
2178
|
full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
|
|
2179
2179
|
},
|
|
@@ -2189,12 +2189,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2189
2189
|
'Delete a file or directory inside the workspace. Missing targets fail. Workspace escape attempts are rejected.',
|
|
2190
2190
|
parameters: {
|
|
2191
2191
|
type: 'object',
|
|
2192
|
-
properties: {
|
|
2193
|
-
path: { type: 'string', description: 'File or directory path to delete. file_path/file/target are accepted aliases.' },
|
|
2194
|
-
file_path: { type: 'string', description: 'Alias for path' },
|
|
2195
|
-
file: { type: 'string', description: 'Alias for path' },
|
|
2196
|
-
target: { type: 'string', description: 'Alias for path' }
|
|
2197
|
-
},
|
|
2192
|
+
properties: {
|
|
2193
|
+
path: { type: 'string', description: 'File or directory path to delete. file_path/file/target are accepted aliases.' },
|
|
2194
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2195
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2196
|
+
target: { type: 'string', description: 'Alias for path' }
|
|
2197
|
+
},
|
|
2198
2198
|
required: ['path']
|
|
2199
2199
|
}
|
|
2200
2200
|
}
|
|
@@ -2562,7 +2562,36 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2562
2562
|
};
|
|
2563
2563
|
|
|
2564
2564
|
const definitions = [...primaryDefinitions];
|
|
2565
|
-
const activeFffAdapter = fffAdapter || createFffAdapter({ workspaceRoot, config });
|
|
2565
|
+
const activeFffAdapter = fffAdapter || createFffAdapter({ workspaceRoot, config });
|
|
2566
|
+
async function backupNonGitPathOnce(rawPath) {
|
|
2567
|
+
if (!backupManager || typeof backupManager.backupOnce !== 'function') return null;
|
|
2568
|
+
const normalized = normalizeFilePathValue(rawPath || '', { stripInlineRange: true }).trim();
|
|
2569
|
+
if (!normalized) return null;
|
|
2570
|
+
try {
|
|
2571
|
+
const backup = await backupManager.backupOnce(normalized);
|
|
2572
|
+
return backup?.ok ? backup : null;
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
return {
|
|
2575
|
+
ok: false,
|
|
2576
|
+
path: normalized,
|
|
2577
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
function attachBackup(result, backup) {
|
|
2582
|
+
if (!backup || !result || typeof result !== 'object') return result;
|
|
2583
|
+
return {
|
|
2584
|
+
...result,
|
|
2585
|
+
non_git_backup: true,
|
|
2586
|
+
backupPath: backup.backupPath || '',
|
|
2587
|
+
backupRelativePath: backup.backupRelativePath || '',
|
|
2588
|
+
backupCreated: backup.created === true,
|
|
2589
|
+
backupReused: backup.reused === true,
|
|
2590
|
+
backupSkipped: backup.skipped === true || (!backup.backupPath && backup.existed === true),
|
|
2591
|
+
backupError: backup.error || '',
|
|
2592
|
+
backupReason: backup.reason || ''
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2566
2595
|
let fffConnected = false;
|
|
2567
2596
|
|
|
2568
2597
|
async function ensureFffConnected() {
|
|
@@ -2595,8 +2624,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2595
2624
|
return builtinGlob(workspaceRoot, args, config);
|
|
2596
2625
|
}
|
|
2597
2626
|
|
|
2598
|
-
async function list(args) {
|
|
2599
|
-
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'file_path', 'file', 'target']);
|
|
2627
|
+
async function list(args) {
|
|
2628
|
+
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'file_path', 'file', 'target']);
|
|
2600
2629
|
if (!resolvesOutsideRoot(workspaceRoot, normalizedArgs?.path || '.') && activeFffAdapter?.list) {
|
|
2601
2630
|
try {
|
|
2602
2631
|
await ensureFffConnected();
|
|
@@ -2617,15 +2646,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2617
2646
|
...args,
|
|
2618
2647
|
path: args?.path || directAstTarget?.path,
|
|
2619
2648
|
ast_target: directAstTarget
|
|
2620
|
-
});
|
|
2621
|
-
if (directAstTarget?.path) rememberAstSelection(directAstTarget.path, directAstTarget);
|
|
2622
|
-
const readPath = normalizePath(result?.path || directAstTarget?.path || '').trim();
|
|
2623
|
-
if (readPath) {
|
|
2624
|
-
lastReadPath = readPath;
|
|
2625
|
-
lastReadRange = null;
|
|
2626
|
-
}
|
|
2627
|
-
return { ...result, ast_target: directAstTarget };
|
|
2628
|
-
}
|
|
2649
|
+
});
|
|
2650
|
+
if (directAstTarget?.path) rememberAstSelection(directAstTarget.path, directAstTarget);
|
|
2651
|
+
const readPath = normalizePath(result?.path || directAstTarget?.path || '').trim();
|
|
2652
|
+
if (readPath) {
|
|
2653
|
+
lastReadPath = readPath;
|
|
2654
|
+
lastReadRange = null;
|
|
2655
|
+
}
|
|
2656
|
+
return { ...result, ast_target: directAstTarget };
|
|
2657
|
+
}
|
|
2629
2658
|
|
|
2630
2659
|
if (inlineQuery) {
|
|
2631
2660
|
const queryResult = await queryAst(workspaceRoot, args);
|
|
@@ -2646,30 +2675,30 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2646
2675
|
path: firstTarget.path,
|
|
2647
2676
|
ast_target: firstTarget
|
|
2648
2677
|
});
|
|
2649
|
-
const readPath = normalizePath(result?.path || firstTarget?.path || '').trim();
|
|
2650
|
-
if (readPath) {
|
|
2651
|
-
lastReadPath = readPath;
|
|
2652
|
-
lastReadRange = null;
|
|
2653
|
-
}
|
|
2654
|
-
return {
|
|
2655
|
-
path: result.path,
|
|
2656
|
-
language: result.language,
|
|
2657
|
-
node: result.node,
|
|
2658
|
-
content: result.content,
|
|
2659
|
-
ast_target: firstTarget,
|
|
2660
|
-
symbol: {
|
|
2661
|
-
symbol_id: `${result.path}#${firstTarget.name || firstTarget.node_type || `${result.node.start_line}-${result.node.end_line}`}`,
|
|
2662
|
-
type: result.node.node_type,
|
|
2663
|
-
file: result.path,
|
|
2664
|
-
range: {
|
|
2665
|
-
start_line: result.node.start_line,
|
|
2666
|
-
end_line: result.node.end_line
|
|
2667
|
-
}
|
|
2668
|
-
},
|
|
2669
|
-
query: inlineQuery,
|
|
2670
|
-
capture_name: String(args?.capture_name || '').trim() || undefined,
|
|
2671
|
-
matches: queryResult.matches.length
|
|
2672
|
-
};
|
|
2678
|
+
const readPath = normalizePath(result?.path || firstTarget?.path || '').trim();
|
|
2679
|
+
if (readPath) {
|
|
2680
|
+
lastReadPath = readPath;
|
|
2681
|
+
lastReadRange = null;
|
|
2682
|
+
}
|
|
2683
|
+
return {
|
|
2684
|
+
path: result.path,
|
|
2685
|
+
language: result.language,
|
|
2686
|
+
node: result.node,
|
|
2687
|
+
content: result.content,
|
|
2688
|
+
ast_target: firstTarget,
|
|
2689
|
+
symbol: {
|
|
2690
|
+
symbol_id: `${result.path}#${firstTarget.name || firstTarget.node_type || `${result.node.start_line}-${result.node.end_line}`}`,
|
|
2691
|
+
type: result.node.node_type,
|
|
2692
|
+
file: result.path,
|
|
2693
|
+
range: {
|
|
2694
|
+
start_line: result.node.start_line,
|
|
2695
|
+
end_line: result.node.end_line
|
|
2696
|
+
}
|
|
2697
|
+
},
|
|
2698
|
+
query: inlineQuery,
|
|
2699
|
+
capture_name: String(args?.capture_name || '').trim() || undefined,
|
|
2700
|
+
matches: queryResult.matches.length
|
|
2701
|
+
};
|
|
2673
2702
|
}
|
|
2674
2703
|
|
|
2675
2704
|
const result = await readFile(workspaceRoot, {
|
|
@@ -2680,14 +2709,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2680
2709
|
? args.max_chars
|
|
2681
2710
|
: config.context?.read_file_max_chars ?? 24000
|
|
2682
2711
|
}, config);
|
|
2683
|
-
const readPath = normalizePath(result?.path || args?.path || '').trim();
|
|
2684
|
-
if (readPath) {
|
|
2685
|
-
lastReadPath = readPath;
|
|
2686
|
-
lastReadRange = result?.phase === 'content'
|
|
2687
|
-
? { path: readPath, start_line: result.start_line, end_line: result.end_line }
|
|
2688
|
-
: null;
|
|
2689
|
-
}
|
|
2690
|
-
return result;
|
|
2712
|
+
const readPath = normalizePath(result?.path || args?.path || '').trim();
|
|
2713
|
+
if (readPath) {
|
|
2714
|
+
lastReadPath = readPath;
|
|
2715
|
+
lastReadRange = result?.phase === 'content'
|
|
2716
|
+
? { path: readPath, start_line: result.start_line, end_line: result.end_line }
|
|
2717
|
+
: null;
|
|
2718
|
+
}
|
|
2719
|
+
return result;
|
|
2691
2720
|
},
|
|
2692
2721
|
query_project_index: async (args) => {
|
|
2693
2722
|
await ensureProjectIndex();
|
|
@@ -2713,19 +2742,20 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2713
2742
|
edit: async (args) => {
|
|
2714
2743
|
await ensureProjectIndex();
|
|
2715
2744
|
const normalizedKind = String(args?.edit?.kind || args?.kind || '').trim();
|
|
2716
|
-
const hasReplaceTextArgs = args?.edit?.old_text != null || args?.old_text != null || args?.old_string != null;
|
|
2717
|
-
const astTarget = hasReplaceTextArgs || (normalizedKind && normalizedKind !== 'replace_block')
|
|
2718
|
-
? null
|
|
2719
|
-
: resolveCachedAstTarget(args, { requireAstScope: normalizedKind === 'replace_block' });
|
|
2720
|
-
const editPath = normalizeFilePathValue(args?.path || args?.file || args?.file_path || '', { stripInlineRange: true }).trim();
|
|
2721
|
-
const shouldUseRecentReadRange =
|
|
2722
|
-
editPath &&
|
|
2723
|
-
lastReadRange?.path === editPath &&
|
|
2724
|
-
!Number.isFinite(Number(args?.start_line || args?.line || args?.edit?.start_line)) &&
|
|
2725
|
-
!Number.isFinite(Number(args?.end_line || args?.edit?.end_line));
|
|
2726
|
-
const rangeArgs = shouldUseRecentReadRange
|
|
2727
|
-
? { start_line: lastReadRange.start_line, end_line: lastReadRange.end_line }
|
|
2728
|
-
: {};
|
|
2745
|
+
const hasReplaceTextArgs = args?.edit?.old_text != null || args?.old_text != null || args?.old_string != null;
|
|
2746
|
+
const astTarget = hasReplaceTextArgs || (normalizedKind && normalizedKind !== 'replace_block')
|
|
2747
|
+
? null
|
|
2748
|
+
: resolveCachedAstTarget(args, { requireAstScope: normalizedKind === 'replace_block' });
|
|
2749
|
+
const editPath = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.ast_target?.path || args?.edit?.target?.path || '', { stripInlineRange: true }).trim();
|
|
2750
|
+
const shouldUseRecentReadRange =
|
|
2751
|
+
editPath &&
|
|
2752
|
+
lastReadRange?.path === editPath &&
|
|
2753
|
+
!Number.isFinite(Number(args?.start_line || args?.line || args?.edit?.start_line)) &&
|
|
2754
|
+
!Number.isFinite(Number(args?.end_line || args?.edit?.end_line));
|
|
2755
|
+
const rangeArgs = shouldUseRecentReadRange
|
|
2756
|
+
? { start_line: lastReadRange.start_line, end_line: lastReadRange.end_line }
|
|
2757
|
+
: {};
|
|
2758
|
+
const backup = await backupNonGitPathOnce(editPath || astTarget?.path);
|
|
2729
2759
|
const result = await editTarget(
|
|
2730
2760
|
workspaceRoot,
|
|
2731
2761
|
astTarget
|
|
@@ -2733,21 +2763,25 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2733
2763
|
: { ...args, ...rangeArgs, recent_file: lastReadPath },
|
|
2734
2764
|
config
|
|
2735
2765
|
);
|
|
2736
|
-
if (result?.path) await refreshProjectFile(result.path);
|
|
2737
|
-
return result;
|
|
2738
|
-
},
|
|
2739
|
-
write: async (args) => {
|
|
2740
|
-
await ensureProjectIndex();
|
|
2741
|
-
const
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2766
|
+
if (result?.path) await refreshProjectFile(result.path);
|
|
2767
|
+
return attachBackup(result, backup);
|
|
2768
|
+
},
|
|
2769
|
+
write: async (args) => {
|
|
2770
|
+
await ensureProjectIndex();
|
|
2771
|
+
const writePath = normalizeFilePathValue(args?.path || args?.file || args?.file_path || '', { stripInlineRange: true }).trim();
|
|
2772
|
+
const backup = await backupNonGitPathOnce(writePath);
|
|
2773
|
+
const result = await writeFile(workspaceRoot, args, config);
|
|
2774
|
+
if (result?.path) await refreshProjectFile(result.path);
|
|
2775
|
+
return attachBackup(result, backup);
|
|
2776
|
+
},
|
|
2777
|
+
delete: Object.assign(async (args) => {
|
|
2778
|
+
await ensureProjectIndex();
|
|
2779
|
+
const deletePathValue = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.target || '', { stripInlineRange: true }).trim();
|
|
2780
|
+
const backup = await backupNonGitPathOnce(deletePathValue);
|
|
2781
|
+
const result = await deletePath(workspaceRoot, args, config);
|
|
2782
|
+
if (result?.path) await refreshProjectFile(result.path);
|
|
2783
|
+
return attachBackup(result, backup);
|
|
2784
|
+
}, {
|
|
2751
2785
|
prepareApproval: async (args) => {
|
|
2752
2786
|
const target = await prepareDeleteTarget(workspaceRoot, args, config);
|
|
2753
2787
|
return {
|
|
@@ -3029,41 +3063,50 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
3029
3063
|
return lines.join('\n');
|
|
3030
3064
|
},
|
|
3031
3065
|
|
|
3032
|
-
edit(result) {
|
|
3033
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
3034
|
-
const p = result.path || '';
|
|
3035
|
-
const action = result.action || '';
|
|
3036
|
-
const line = result.changed_line || 0;
|
|
3037
|
-
const
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3066
|
+
edit(result) {
|
|
3067
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
3068
|
+
const p = result.path || '';
|
|
3069
|
+
const action = result.action || '';
|
|
3070
|
+
const line = result.changed_line || 0;
|
|
3071
|
+
const backup = result.backupPath
|
|
3072
|
+
? `\nbackup: ${result.backupPath}${result.backupReused ? ' (reused)' : ''}`
|
|
3073
|
+
: '';
|
|
3074
|
+
const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}${backup}`;
|
|
3075
|
+
const diffPreview = result.diff_preview || '';
|
|
3076
|
+
if (diffPreview) {
|
|
3077
|
+
const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
|
|
3041
3078
|
return `${summary}\n${trimmed}`;
|
|
3042
3079
|
}
|
|
3043
3080
|
return summary + (result.ok !== false ? '' : ` [FAILED: ${result.error || 'unknown'}]`);
|
|
3044
3081
|
},
|
|
3045
3082
|
|
|
3046
|
-
write(result) {
|
|
3047
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
3048
|
-
const p = result.path || '';
|
|
3049
|
-
const action = result.action || 'write';
|
|
3050
|
-
const line = result.changed_line || 0;
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3083
|
+
write(result) {
|
|
3084
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
3085
|
+
const p = result.path || '';
|
|
3086
|
+
const action = result.action || 'write';
|
|
3087
|
+
const line = result.changed_line || 0;
|
|
3088
|
+
const backup = result.backupPath
|
|
3089
|
+
? `\nbackup: ${result.backupPath}${result.backupReused ? ' (reused)' : ''}`
|
|
3090
|
+
: '';
|
|
3091
|
+
const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}${backup}`;
|
|
3092
|
+
const diffPreview = result.diff_preview || '';
|
|
3093
|
+
if (diffPreview) {
|
|
3094
|
+
const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
|
|
3055
3095
|
return `${summary}\n${trimmed}`;
|
|
3056
3096
|
}
|
|
3057
3097
|
return summary;
|
|
3058
3098
|
},
|
|
3059
3099
|
|
|
3060
|
-
delete(result) {
|
|
3061
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
3062
|
-
if (result.ok === false) return JSON.stringify(result);
|
|
3063
|
-
const kind = result.type || 'item';
|
|
3064
|
-
const target = result.path || '';
|
|
3065
|
-
|
|
3066
|
-
|
|
3100
|
+
delete(result) {
|
|
3101
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
3102
|
+
if (result.ok === false) return JSON.stringify(result);
|
|
3103
|
+
const kind = result.type || 'item';
|
|
3104
|
+
const target = result.path || '';
|
|
3105
|
+
const backup = result.backupPath
|
|
3106
|
+
? `\nbackup: ${result.backupPath}${result.backupReused ? ' (reused)' : ''}`
|
|
3107
|
+
: '';
|
|
3108
|
+
return `[delete: ${kind}] deleted ${target}${backup}`;
|
|
3109
|
+
},
|
|
3067
3110
|
|
|
3068
3111
|
run(result) {
|
|
3069
3112
|
if (!result || typeof result !== 'object') return String(result);
|