codemini-cli 0.6.3 → 0.6.5
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-BUp8EzDg.js} +2 -2
- package/codemini-web/dist/assets/CodeWikiPanel-Fp0VKdzo.js +1 -0
- package/codemini-web/dist/assets/ConfigDialog-DIpj779O.js +1 -0
- package/codemini-web/dist/assets/GitDiffDialog-ZLEuX8Qm.js +3 -0
- package/codemini-web/dist/assets/{MemoryDialog-BhxQgG0I.js → MemoryDialog-D2YbENVd.js} +3 -3
- package/codemini-web/dist/assets/MessageBubble-BIgpZsLn.js +12 -0
- package/codemini-web/dist/assets/PatchDiff-CvKNaHsw.js +230 -0
- package/codemini-web/dist/assets/ProjectSelector-DXIep3lE.js +1 -0
- package/codemini-web/dist/assets/{SkillDialog-DxS43NpR.js → SkillDialog-DjPF-XBx.js} +4 -4
- package/codemini-web/dist/assets/SoulDialog-BfIoKETs.js +1 -0
- package/codemini-web/dist/assets/chevron-right-CfNZHlyU.js +1 -0
- package/codemini-web/dist/assets/{chunk-BO2N2NFS-Budy_hfO.js → chunk-BO2N2NFS-DMUdjM9q.js} +6 -6
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CQS1PAvD.js → highlighted-body-OFNGDK62-8ch0jz7Z.js} +1 -1
- package/codemini-web/dist/assets/index-BhMtCC8_.js +65 -0
- package/codemini-web/dist/assets/index-DRXwJ-n_.css +2 -0
- package/codemini-web/dist/assets/input-CYpdNDlR.js +1 -0
- package/codemini-web/dist/assets/lib-BXWizt13.js +1 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-KBEtMEB9.js +1 -0
- package/codemini-web/dist/assets/{pencil-Ce_LFiEh.js → pencil-BdA2cEeE.js} +1 -1
- package/codemini-web/dist/assets/{refresh-cw-BKL-AZu5.js → refresh-cw-CJGgUGiS.js} +1 -1
- package/codemini-web/dist/assets/select-BLOccU1M.js +1 -0
- package/codemini-web/dist/assets/{trash-2-KmAlCwXd.js → trash-2-CQzNOch5.js} +1 -1
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +332 -296
- package/codemini-web/server.js +319 -243
- package/package.json +1 -1
- package/src/core/agent-loop.js +188 -100
- package/src/core/chat-runtime.js +676 -571
- package/src/core/config-store.js +9 -3
- package/src/core/git-oplog-change-tracker.js +468 -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 +555 -434
- 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/input-CNQgbKe6.js +0 -1
- 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,28 +1662,80 @@ function editResult(pathText, action, beforeContent, afterContent, changedLine =
|
|
|
1662
1662
|
lines_added: changed.added,
|
|
1663
1663
|
lines_removed: changed.removed
|
|
1664
1664
|
};
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
1667
|
function lineRangeToOffsets(content, startLineRaw, endLineRaw) {
|
|
1668
1668
|
const lines = splitLines(content);
|
|
1669
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;
|
|
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
|
+
function normalizeNewlinesWithMap(text) {
|
|
1685
|
+
const source = String(text || '');
|
|
1686
|
+
const chars = [];
|
|
1687
|
+
const indexMap = [];
|
|
1688
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1689
|
+
const ch = source[i];
|
|
1690
|
+
if (ch === '\r') {
|
|
1691
|
+
chars.push('\n');
|
|
1692
|
+
indexMap.push(i);
|
|
1693
|
+
if (source[i + 1] === '\n') i += 1;
|
|
1694
|
+
continue;
|
|
1695
|
+
}
|
|
1696
|
+
chars.push(ch);
|
|
1697
|
+
indexMap.push(i);
|
|
1675
1698
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1699
|
+
return { text: chars.join(''), indexMap };
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function detectEol(text) {
|
|
1703
|
+
const sample = String(text || '');
|
|
1704
|
+
const crlf = (sample.match(/\r\n/g) || []).length;
|
|
1705
|
+
const loneLf = (sample.match(/(?<!\r)\n/g) || []).length;
|
|
1706
|
+
const loneCr = (sample.match(/\r(?!\n)/g) || []).length;
|
|
1707
|
+
if (crlf >= loneLf && crlf >= loneCr && crlf > 0) return '\r\n';
|
|
1708
|
+
if (loneCr > loneLf && loneCr > 0) return '\r';
|
|
1709
|
+
return '\n';
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function applyEol(text, eol) {
|
|
1713
|
+
return String(text || '').replace(/\r\n|\r|\n/g, eol || '\n');
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function findLineEndingEquivalentMatches(content, oldText) {
|
|
1717
|
+
const normalizedOld = normalizeNewlinesWithMap(oldText).text;
|
|
1718
|
+
if (!normalizedOld) return [];
|
|
1719
|
+
const normalizedContent = normalizeNewlinesWithMap(content);
|
|
1720
|
+
const matches = [];
|
|
1721
|
+
let pos = 0;
|
|
1722
|
+
while (true) {
|
|
1723
|
+
const found = normalizedContent.text.indexOf(normalizedOld, pos);
|
|
1724
|
+
if (found === -1) break;
|
|
1725
|
+
const start = normalizedContent.indexMap[found] ?? 0;
|
|
1726
|
+
const endNorm = found + normalizedOld.length;
|
|
1727
|
+
const end = endNorm >= normalizedContent.text.length
|
|
1728
|
+
? String(content || '').length
|
|
1729
|
+
: normalizedContent.indexMap[endNorm];
|
|
1730
|
+
matches.push({ start, end });
|
|
1731
|
+
pos = found + Math.max(1, normalizedOld.length);
|
|
1680
1732
|
}
|
|
1681
|
-
return
|
|
1733
|
+
return matches;
|
|
1682
1734
|
}
|
|
1683
1735
|
|
|
1684
1736
|
async function replaceBlock(root, args, config = {}) {
|
|
1685
|
-
const relativePath = String(args?.path || '').trim();
|
|
1686
|
-
const newContent = String(args?.new_content || args?.content || '');
|
|
1737
|
+
const relativePath = String(args?.path || '').trim();
|
|
1738
|
+
const newContent = String(args?.new_content || args?.content || '');
|
|
1687
1739
|
const target = args?.target || {};
|
|
1688
1740
|
const state = await getFileState(root, relativePath, config);
|
|
1689
1741
|
const resolved = resolveReplaceBlockTarget(state, target);
|
|
@@ -1700,67 +1752,93 @@ async function replaceBlock(root, args, config = {}) {
|
|
|
1700
1752
|
return editResult(relativePath, 'replace_block', state.content, afterContent, resolved.start_line);
|
|
1701
1753
|
}
|
|
1702
1754
|
|
|
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;
|
|
1755
|
+
async function replaceText(root, args, config = {}) {
|
|
1756
|
+
const relativePath = String(args?.path || '').trim();
|
|
1757
|
+
const oldText = String(args?.old_text || '');
|
|
1758
|
+
const newText = String(args?.new_text || '');
|
|
1759
|
+
const replaceAll = semanticBoolean(args?.replace_all ?? args?.replaceAll);
|
|
1760
|
+
const state = await getFileState(root, relativePath, config);
|
|
1761
|
+
if (!oldText) {
|
|
1762
|
+
throw new Error('replace_text requires old_text');
|
|
1763
|
+
}
|
|
1764
|
+
const rangeStart = Number(args?.start_line || args?.line);
|
|
1765
|
+
const rangeEnd = Number(args?.end_line || args?.line);
|
|
1766
|
+
const hasRange = Number.isFinite(rangeStart) && rangeStart > 0;
|
|
1715
1767
|
const range = hasRange
|
|
1716
1768
|
? lineRangeToOffsets(state.content, rangeStart, Number.isFinite(rangeEnd) && rangeEnd >= rangeStart ? rangeEnd : rangeStart)
|
|
1717
1769
|
: null;
|
|
1718
1770
|
const searchContent = range ? state.content.slice(range.startOffset, range.endOffset) : state.content;
|
|
1719
1771
|
const occurrences = searchContent.split(oldText).length - 1;
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1772
|
+
let newlineMatches = null;
|
|
1773
|
+
if (occurrences === 0 && /[\r\n]/.test(oldText)) {
|
|
1774
|
+
newlineMatches = findLineEndingEquivalentMatches(searchContent, oldText);
|
|
1775
|
+
if ((replaceAll && newlineMatches.length > 0) || newlineMatches.length === 1) {
|
|
1776
|
+
let cursor = 0;
|
|
1777
|
+
let replaced = '';
|
|
1778
|
+
for (const match of newlineMatches) {
|
|
1779
|
+
const originalMatch = searchContent.slice(match.start, match.end);
|
|
1780
|
+
replaced += searchContent.slice(cursor, match.start);
|
|
1781
|
+
replaced += applyEol(newText, detectEol(originalMatch));
|
|
1782
|
+
cursor = match.end;
|
|
1783
|
+
if (!replaceAll) break;
|
|
1784
|
+
}
|
|
1785
|
+
replaced += searchContent.slice(cursor);
|
|
1723
1786
|
const afterContent = range
|
|
1724
1787
|
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1725
|
-
:
|
|
1788
|
+
: replaced;
|
|
1726
1789
|
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1790
|
+
const first = newlineMatches[0];
|
|
1727
1791
|
const changedLine = range
|
|
1728
|
-
? range.startLine + splitLines(searchContent.slice(0,
|
|
1729
|
-
: splitLines(state.content.slice(0,
|
|
1792
|
+
? range.startLine + splitLines(searchContent.slice(0, first.start)).length - 1
|
|
1793
|
+
: splitLines(state.content.slice(0, first.start)).length;
|
|
1730
1794
|
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1731
1795
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1796
|
+
}
|
|
1797
|
+
if (occurrences !== 1) {
|
|
1798
|
+
if (replaceAll && occurrences > 0) {
|
|
1799
|
+
const replaced = searchContent.replaceAll(oldText, newText);
|
|
1800
|
+
const afterContent = range
|
|
1801
|
+
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1802
|
+
: state.content.replaceAll(oldText, newText);
|
|
1803
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1804
|
+
const changedLine = range
|
|
1805
|
+
? range.startLine + splitLines(searchContent.slice(0, searchContent.indexOf(oldText))).length - 1
|
|
1806
|
+
: splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1807
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1808
|
+
}
|
|
1809
|
+
const baseLine = hasRange ? range.startLine : 1;
|
|
1810
|
+
const baseOffset = hasRange ? range.startOffset : 0;
|
|
1811
|
+
const lineDetails = [];
|
|
1812
|
+
let searchPos = 0;
|
|
1813
|
+
while (true) {
|
|
1814
|
+
const pos = searchContent.indexOf(oldText, searchPos);
|
|
1815
|
+
if (pos === -1) break;
|
|
1816
|
+
const lineNum = baseLine + splitLines(searchContent.slice(0, pos)).length - 1;
|
|
1817
|
+
const globalPos = baseOffset + pos;
|
|
1818
|
+
const lStart = state.content.lastIndexOf('\n', globalPos) + 1;
|
|
1819
|
+
const lEnd = state.content.indexOf('\n', globalPos);
|
|
1820
|
+
const lineText = state.content.slice(lStart, lEnd >= 0 ? lEnd : void 0).trim();
|
|
1821
|
+
lineDetails.push(` Line ${lineNum}: ${lineText}`);
|
|
1822
|
+
searchPos = pos + oldText.length;
|
|
1823
|
+
}
|
|
1747
1824
|
const lineHint = lineDetails.length > 0 ? `\n${lineDetails.join('\n')}\n` : ' ';
|
|
1825
|
+
const effectiveOccurrences = newlineMatches?.length || occurrences;
|
|
1748
1826
|
throw new Error(
|
|
1749
|
-
|
|
1827
|
+
effectiveOccurrences === 0
|
|
1750
1828
|
? 'replace_text old_text not found'
|
|
1751
|
-
: `replace_text old_text not unique; found ${
|
|
1829
|
+
: `replace_text old_text not unique; found ${effectiveOccurrences} occurrences:${lineHint}Use path:"${relativePath}:N-M" to narrow the range, set replace_all=true, or provide more unique old_text`
|
|
1752
1830
|
);
|
|
1753
1831
|
}
|
|
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
|
-
}
|
|
1832
|
+
const replaced = searchContent.replace(oldText, newText);
|
|
1833
|
+
const afterContent = range
|
|
1834
|
+
? `${state.content.slice(0, range.startOffset)}${replaced}${state.content.slice(range.endOffset)}`
|
|
1835
|
+
: state.content.replace(oldText, newText);
|
|
1836
|
+
await fs.writeFile(state.target, afterContent, 'utf8');
|
|
1837
|
+
const changedLine = range
|
|
1838
|
+
? range.startLine + splitLines(searchContent.slice(0, searchContent.indexOf(oldText))).length - 1
|
|
1839
|
+
: splitLines(state.content.slice(0, state.content.indexOf(oldText))).length;
|
|
1840
|
+
return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
|
|
1841
|
+
}
|
|
1764
1842
|
|
|
1765
1843
|
async function insertRelative(root, args, mode, config = {}) {
|
|
1766
1844
|
const relativePath = String(args?.path || '').trim();
|
|
@@ -1807,62 +1885,62 @@ async function openTarget(root, args, config = {}) {
|
|
|
1807
1885
|
};
|
|
1808
1886
|
}
|
|
1809
1887
|
|
|
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
|
-
}
|
|
1888
|
+
function normalizeEditTargetArgs(args = {}) {
|
|
1889
|
+
const rawFile = String(args?.file || args?.path || args?.file_path || '').trim();
|
|
1890
|
+
const inlineRange = parseInlineRangePath(rawFile);
|
|
1891
|
+
const file = normalizeFilePathValue(rawFile, { stripInlineRange: true }).trim();
|
|
1892
|
+
const nestedEdit = args?.edit && typeof args.edit === 'object' ? args.edit : null;
|
|
1893
|
+
const startLine = args?.start_line ?? args?.line ?? inlineRange?.start_line;
|
|
1894
|
+
const endLine = args?.end_line ?? inlineRange?.end_line ?? args?.line;
|
|
1895
|
+
if (nestedEdit) {
|
|
1896
|
+
const normalizedEdit = { ...nestedEdit };
|
|
1897
|
+
if (normalizedEdit.new_content == null && normalizedEdit.content != null) {
|
|
1898
|
+
normalizedEdit.new_content = normalizedEdit.content;
|
|
1899
|
+
}
|
|
1900
|
+
if (normalizedEdit.old_text == null && normalizedEdit.old_string != null) {
|
|
1901
|
+
normalizedEdit.old_text = normalizedEdit.old_string;
|
|
1902
|
+
}
|
|
1903
|
+
if (normalizedEdit.new_text == null && normalizedEdit.content != null && normalizedEdit.old_text != null) {
|
|
1904
|
+
normalizedEdit.new_text = normalizedEdit.content;
|
|
1905
|
+
}
|
|
1906
|
+
if (normalizedEdit.new_text == null && normalizedEdit.new_string != null) {
|
|
1907
|
+
normalizedEdit.new_text = normalizedEdit.new_string;
|
|
1908
|
+
}
|
|
1909
|
+
return {
|
|
1910
|
+
path: file,
|
|
1911
|
+
file,
|
|
1912
|
+
start_line: startLine,
|
|
1913
|
+
end_line: endLine,
|
|
1914
|
+
ast_target: normalizedEdit.ast_target ?? args?.ast_target,
|
|
1915
|
+
edit: normalizedEdit
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1840
1918
|
const topLevelOldText = args?.old_text ?? args?.old_string;
|
|
1841
1919
|
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,
|
|
1920
|
+
return {
|
|
1921
|
+
path: file,
|
|
1922
|
+
file,
|
|
1923
|
+
start_line: startLine,
|
|
1924
|
+
end_line: endLine,
|
|
1925
|
+
ast_target: args?.ast_target,
|
|
1926
|
+
edit: {
|
|
1927
|
+
kind: args?.kind,
|
|
1928
|
+
target: args?.target,
|
|
1851
1929
|
new_content: args?.new_content ?? args?.content,
|
|
1852
1930
|
old_text: args?.old_text,
|
|
1853
1931
|
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
|
-
}
|
|
1932
|
+
old_string: args?.old_string,
|
|
1933
|
+
new_string: args?.new_string,
|
|
1934
|
+
anchor_text: args?.anchor_text,
|
|
1935
|
+
content: args?.content,
|
|
1936
|
+
replace_all: args?.replace_all ?? args?.replaceAll
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1862
1940
|
|
|
1863
1941
|
async function editTarget(root, args, config = {}) {
|
|
1864
|
-
const normalized = normalizeEditTargetArgs(args);
|
|
1865
|
-
const file = normalized.file || normalizeFilePathValue(args?.recent_file || '', { stripInlineRange: true }).trim();
|
|
1942
|
+
const normalized = normalizeEditTargetArgs(args);
|
|
1943
|
+
const file = normalized.file || normalizeFilePathValue(args?.recent_file || '', { stripInlineRange: true }).trim();
|
|
1866
1944
|
const astTarget = normalized.ast_target;
|
|
1867
1945
|
const edit = normalized.edit || {};
|
|
1868
1946
|
let kind = String(edit.kind || '').trim();
|
|
@@ -1872,8 +1950,8 @@ async function editTarget(root, args, config = {}) {
|
|
|
1872
1950
|
if (edit.new_text == null && edit.new_string != null) {
|
|
1873
1951
|
edit.new_text = edit.new_string;
|
|
1874
1952
|
}
|
|
1875
|
-
const hasContent = edit.new_content != null || edit.content != null;
|
|
1876
|
-
const hasExplicitRewrite = edit.kind === 'rewrite_file' || args?.kind === 'rewrite_file';
|
|
1953
|
+
const hasContent = edit.new_content != null || edit.content != null;
|
|
1954
|
+
const hasExplicitRewrite = edit.kind === 'rewrite_file' || args?.kind === 'rewrite_file';
|
|
1877
1955
|
const hasTargetHint = Boolean(edit.symbol || args?.symbol || edit.line || args?.line || edit.target);
|
|
1878
1956
|
if (!kind) {
|
|
1879
1957
|
if (hasContent && hasTargetHint) {
|
|
@@ -1882,23 +1960,23 @@ async function editTarget(root, args, config = {}) {
|
|
|
1882
1960
|
kind = 'replace_text';
|
|
1883
1961
|
} else if ((edit.anchor_text != null || edit.target_text != null) && (edit.content != null || edit.new_content != null)) {
|
|
1884
1962
|
kind = String(edit.position || edit.mode || args?.position || '').trim() === 'after' ? 'insert_after' : 'insert_before';
|
|
1885
|
-
} else if (hasContent && hasExplicitRewrite) {
|
|
1886
|
-
kind = 'rewrite_file';
|
|
1887
|
-
}
|
|
1963
|
+
} else if (hasContent && hasExplicitRewrite) {
|
|
1964
|
+
kind = 'rewrite_file';
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (!file || !kind) {
|
|
1968
|
+
const recentFile = String(args?.recent_file || '').trim();
|
|
1969
|
+
const rawArgs = typeof args?._raw === 'string' && args._raw.trim() ? ` Raw tool arguments: ${args._raw.trim()}.` : '';
|
|
1970
|
+
const missing = !file
|
|
1971
|
+
? 'file path'
|
|
1972
|
+
: edit.old_text != null && edit.new_text == null && edit.content == null
|
|
1973
|
+
? 'new_text'
|
|
1974
|
+
: 'edit operation';
|
|
1975
|
+
const hint = recentFile
|
|
1976
|
+
? ` 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.`
|
|
1977
|
+
: ' 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.';
|
|
1978
|
+
throw new Error(`edit requires ${missing}.${rawArgs}${hint}`);
|
|
1888
1979
|
}
|
|
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
1980
|
if (astTarget) {
|
|
1903
1981
|
if (kind !== 'replace_block') {
|
|
1904
1982
|
throw new Error('AST-scoped edit only supports replace_block');
|
|
@@ -1942,16 +2020,16 @@ async function editTarget(root, args, config = {}) {
|
|
|
1942
2020
|
}, config);
|
|
1943
2021
|
}
|
|
1944
2022
|
}
|
|
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
|
-
}
|
|
2023
|
+
if (kind === 'replace_text') {
|
|
2024
|
+
return replaceText(root, {
|
|
2025
|
+
path: file,
|
|
2026
|
+
old_text: edit.old_text,
|
|
2027
|
+
new_text: edit.new_text,
|
|
2028
|
+
replace_all: edit.replace_all ?? args?.replace_all ?? args?.replaceAll,
|
|
2029
|
+
start_line: edit.start_line ?? normalized.start_line,
|
|
2030
|
+
end_line: edit.end_line ?? normalized.end_line
|
|
2031
|
+
}, config);
|
|
2032
|
+
}
|
|
1955
2033
|
if (kind === 'insert_before') {
|
|
1956
2034
|
return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_before', config);
|
|
1957
2035
|
}
|
|
@@ -1968,19 +2046,19 @@ async function editTarget(root, args, config = {}) {
|
|
|
1968
2046
|
throw new Error(`edit does not support kind: ${kind}`);
|
|
1969
2047
|
}
|
|
1970
2048
|
|
|
1971
|
-
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate, fffAdapter }) {
|
|
2049
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate, fffAdapter, backupManager }) {
|
|
1972
2050
|
const emitSystemTool = (event) => {
|
|
1973
2051
|
if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
|
|
1974
2052
|
};
|
|
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);
|
|
2053
|
+
const astSelectionCache = new Map();
|
|
2054
|
+
let lastAstTarget = null;
|
|
2055
|
+
let lastReadPath = '';
|
|
2056
|
+
let lastReadRange = null;
|
|
2057
|
+
const rememberAstSelection = (filePath, astTarget) => {
|
|
2058
|
+
const key = normalizePath(filePath).trim();
|
|
2059
|
+
if (!key || !astTarget) return;
|
|
2060
|
+
lastAstTarget = astTarget;
|
|
2061
|
+
astSelectionCache.set(key, astTarget);
|
|
1984
2062
|
};
|
|
1985
2063
|
const hasExplicitBlockHints = (args = {}) =>
|
|
1986
2064
|
Boolean(
|
|
@@ -1993,15 +2071,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1993
2071
|
args?.edit?.line ||
|
|
1994
2072
|
args?.edit?.target
|
|
1995
2073
|
);
|
|
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
|
-
};
|
|
2074
|
+
const resolveCachedAstTarget = (args = {}, { requireAstScope = false } = {}) => {
|
|
2075
|
+
const file = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.ast_target?.path || '', { stripInlineRange: true }).trim();
|
|
2076
|
+
if (args?.ast_target) return args.ast_target;
|
|
2077
|
+
if (file) {
|
|
2078
|
+
if (requireAstScope && hasExplicitBlockHints(args)) return null;
|
|
2079
|
+
return astSelectionCache.get(file) || null;
|
|
2080
|
+
}
|
|
2081
|
+
return lastAstTarget || null;
|
|
2082
|
+
};
|
|
2005
2083
|
const ensureProjectIndex = async () => {
|
|
2006
2084
|
const eventId = `project-index:${Date.now()}`;
|
|
2007
2085
|
const name = 'project_index(.codemini/project-map.json,.codemini/file-index.json)';
|
|
@@ -2053,17 +2131,17 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2053
2131
|
{
|
|
2054
2132
|
type: 'function',
|
|
2055
2133
|
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' },
|
|
2134
|
+
name: 'read',
|
|
2135
|
+
description:
|
|
2136
|
+
'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.',
|
|
2137
|
+
parameters: {
|
|
2138
|
+
type: 'object',
|
|
2139
|
+
properties: {
|
|
2140
|
+
path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
|
|
2141
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2142
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2143
|
+
start_line: { type: 'number', description: '1-based start line' },
|
|
2144
|
+
end_line: { type: 'number', description: 'Inclusive end line' },
|
|
2067
2145
|
max_chars: { type: 'number', description: 'Max chars to return' },
|
|
2068
2146
|
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
2147
|
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 +2160,10 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2082
2160
|
'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
2161
|
parameters: {
|
|
2084
2162
|
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' },
|
|
2163
|
+
properties: {
|
|
2164
|
+
pattern: { type: 'string', description: 'Search pattern' },
|
|
2165
|
+
path: { type: 'string', description: 'Directory or file to search. file_path/file/dir/directory/cwd are accepted aliases.' },
|
|
2166
|
+
regex: { type: 'boolean', description: 'Treat pattern as regex' },
|
|
2089
2167
|
case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
|
|
2090
2168
|
max_results: { type: 'number', description: 'Max matches to return' },
|
|
2091
2169
|
language: { type: 'string', description: 'Filter by language' },
|
|
@@ -2102,9 +2180,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2102
2180
|
description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
|
|
2103
2181
|
parameters: {
|
|
2104
2182
|
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' }
|
|
2183
|
+
properties: {
|
|
2184
|
+
path: { type: 'string', description: 'Directory path to list. file_path/file/dir/directory are accepted aliases.' },
|
|
2185
|
+
include_hidden: { type: 'boolean', description: 'Include dotfiles' }
|
|
2108
2186
|
}
|
|
2109
2187
|
}
|
|
2110
2188
|
}
|
|
@@ -2112,9 +2190,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2112
2190
|
{
|
|
2113
2191
|
type: 'function',
|
|
2114
2192
|
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.',
|
|
2193
|
+
name: 'query_project_index',
|
|
2194
|
+
description:
|
|
2195
|
+
'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
2196
|
parameters: {
|
|
2119
2197
|
type: 'object',
|
|
2120
2198
|
properties: {
|
|
@@ -2130,24 +2208,24 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2130
2208
|
{
|
|
2131
2209
|
type: 'function',
|
|
2132
2210
|
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' },
|
|
2211
|
+
name: 'edit',
|
|
2212
|
+
description:
|
|
2213
|
+
'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.',
|
|
2214
|
+
parameters: {
|
|
2215
|
+
type: 'object',
|
|
2216
|
+
properties: {
|
|
2217
|
+
path: { type: 'string', description: 'File path to edit. Inline ranges like src/app.js:10-30 are accepted.' },
|
|
2218
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2219
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2220
|
+
new_content: { type: 'string', description: 'Replacement content' },
|
|
2221
|
+
old_text: { type: 'string', description: 'Exact text to replace' },
|
|
2222
|
+
new_text: { type: 'string', description: 'Replacement text' },
|
|
2223
|
+
old_string: { type: 'string', description: 'Alias for old_text' },
|
|
2224
|
+
new_string: { type: 'string', description: 'Alias for new_text' },
|
|
2225
|
+
replace_all: { type: 'boolean', description: 'Replace all matching old_text occurrences' },
|
|
2226
|
+
start_line: { type: 'number', description: 'Optional range start for disambiguating old_text' },
|
|
2227
|
+
end_line: { type: 'number', description: 'Optional range end for disambiguating old_text' },
|
|
2228
|
+
anchor_text: { type: 'string', description: 'Anchor text for inserts' },
|
|
2151
2229
|
content: { type: 'string', description: 'Content to insert or append' },
|
|
2152
2230
|
position: { type: 'string', description: 'before or after' },
|
|
2153
2231
|
kind: { type: 'string', description: 'replace_block, replace_text, insert_before, insert_after, or rewrite_file' },
|
|
@@ -2157,23 +2235,23 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2157
2235
|
line: { type: 'number', description: 'Line to target' },
|
|
2158
2236
|
edit: { type: 'object', description: 'Structured edit input' }
|
|
2159
2237
|
},
|
|
2160
|
-
required: ['path', 'content']
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
},
|
|
2238
|
+
required: ['path', 'content']
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2164
2242
|
{
|
|
2165
2243
|
type: 'function',
|
|
2166
2244
|
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' },
|
|
2245
|
+
name: 'write',
|
|
2246
|
+
description:
|
|
2247
|
+
'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.',
|
|
2248
|
+
parameters: {
|
|
2249
|
+
type: 'object',
|
|
2250
|
+
properties: {
|
|
2251
|
+
path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
|
|
2252
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2253
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2254
|
+
content: { type: 'string', description: 'Content to write' },
|
|
2177
2255
|
append: { type: 'boolean', description: 'Append instead of overwrite' },
|
|
2178
2256
|
full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
|
|
2179
2257
|
},
|
|
@@ -2189,12 +2267,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2189
2267
|
'Delete a file or directory inside the workspace. Missing targets fail. Workspace escape attempts are rejected.',
|
|
2190
2268
|
parameters: {
|
|
2191
2269
|
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
|
-
},
|
|
2270
|
+
properties: {
|
|
2271
|
+
path: { type: 'string', description: 'File or directory path to delete. file_path/file/target are accepted aliases.' },
|
|
2272
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
2273
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
2274
|
+
target: { type: 'string', description: 'Alias for path' }
|
|
2275
|
+
},
|
|
2198
2276
|
required: ['path']
|
|
2199
2277
|
}
|
|
2200
2278
|
}
|
|
@@ -2562,7 +2640,36 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2562
2640
|
};
|
|
2563
2641
|
|
|
2564
2642
|
const definitions = [...primaryDefinitions];
|
|
2565
|
-
const activeFffAdapter = fffAdapter || createFffAdapter({ workspaceRoot, config });
|
|
2643
|
+
const activeFffAdapter = fffAdapter || createFffAdapter({ workspaceRoot, config });
|
|
2644
|
+
async function backupNonGitPathOnce(rawPath) {
|
|
2645
|
+
if (!backupManager || typeof backupManager.backupOnce !== 'function') return null;
|
|
2646
|
+
const normalized = normalizeFilePathValue(rawPath || '', { stripInlineRange: true }).trim();
|
|
2647
|
+
if (!normalized) return null;
|
|
2648
|
+
try {
|
|
2649
|
+
const backup = await backupManager.backupOnce(normalized);
|
|
2650
|
+
return backup?.ok ? backup : null;
|
|
2651
|
+
} catch (error) {
|
|
2652
|
+
return {
|
|
2653
|
+
ok: false,
|
|
2654
|
+
path: normalized,
|
|
2655
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
function attachBackup(result, backup) {
|
|
2660
|
+
if (!backup || !result || typeof result !== 'object') return result;
|
|
2661
|
+
return {
|
|
2662
|
+
...result,
|
|
2663
|
+
non_git_backup: true,
|
|
2664
|
+
backupPath: backup.backupPath || '',
|
|
2665
|
+
backupRelativePath: backup.backupRelativePath || '',
|
|
2666
|
+
backupCreated: backup.created === true,
|
|
2667
|
+
backupReused: backup.reused === true,
|
|
2668
|
+
backupSkipped: backup.skipped === true || (!backup.backupPath && backup.existed === true),
|
|
2669
|
+
backupError: backup.error || '',
|
|
2670
|
+
backupReason: backup.reason || ''
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2566
2673
|
let fffConnected = false;
|
|
2567
2674
|
|
|
2568
2675
|
async function ensureFffConnected() {
|
|
@@ -2595,8 +2702,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2595
2702
|
return builtinGlob(workspaceRoot, args, config);
|
|
2596
2703
|
}
|
|
2597
2704
|
|
|
2598
|
-
async function list(args) {
|
|
2599
|
-
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'file_path', 'file', 'target']);
|
|
2705
|
+
async function list(args) {
|
|
2706
|
+
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'file_path', 'file', 'target']);
|
|
2600
2707
|
if (!resolvesOutsideRoot(workspaceRoot, normalizedArgs?.path || '.') && activeFffAdapter?.list) {
|
|
2601
2708
|
try {
|
|
2602
2709
|
await ensureFffConnected();
|
|
@@ -2617,15 +2724,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2617
2724
|
...args,
|
|
2618
2725
|
path: args?.path || directAstTarget?.path,
|
|
2619
2726
|
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
|
-
}
|
|
2727
|
+
});
|
|
2728
|
+
if (directAstTarget?.path) rememberAstSelection(directAstTarget.path, directAstTarget);
|
|
2729
|
+
const readPath = normalizePath(result?.path || directAstTarget?.path || '').trim();
|
|
2730
|
+
if (readPath) {
|
|
2731
|
+
lastReadPath = readPath;
|
|
2732
|
+
lastReadRange = null;
|
|
2733
|
+
}
|
|
2734
|
+
return { ...result, ast_target: directAstTarget };
|
|
2735
|
+
}
|
|
2629
2736
|
|
|
2630
2737
|
if (inlineQuery) {
|
|
2631
2738
|
const queryResult = await queryAst(workspaceRoot, args);
|
|
@@ -2646,30 +2753,30 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2646
2753
|
path: firstTarget.path,
|
|
2647
2754
|
ast_target: firstTarget
|
|
2648
2755
|
});
|
|
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
|
-
};
|
|
2756
|
+
const readPath = normalizePath(result?.path || firstTarget?.path || '').trim();
|
|
2757
|
+
if (readPath) {
|
|
2758
|
+
lastReadPath = readPath;
|
|
2759
|
+
lastReadRange = null;
|
|
2760
|
+
}
|
|
2761
|
+
return {
|
|
2762
|
+
path: result.path,
|
|
2763
|
+
language: result.language,
|
|
2764
|
+
node: result.node,
|
|
2765
|
+
content: result.content,
|
|
2766
|
+
ast_target: firstTarget,
|
|
2767
|
+
symbol: {
|
|
2768
|
+
symbol_id: `${result.path}#${firstTarget.name || firstTarget.node_type || `${result.node.start_line}-${result.node.end_line}`}`,
|
|
2769
|
+
type: result.node.node_type,
|
|
2770
|
+
file: result.path,
|
|
2771
|
+
range: {
|
|
2772
|
+
start_line: result.node.start_line,
|
|
2773
|
+
end_line: result.node.end_line
|
|
2774
|
+
}
|
|
2775
|
+
},
|
|
2776
|
+
query: inlineQuery,
|
|
2777
|
+
capture_name: String(args?.capture_name || '').trim() || undefined,
|
|
2778
|
+
matches: queryResult.matches.length
|
|
2779
|
+
};
|
|
2673
2780
|
}
|
|
2674
2781
|
|
|
2675
2782
|
const result = await readFile(workspaceRoot, {
|
|
@@ -2680,14 +2787,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2680
2787
|
? args.max_chars
|
|
2681
2788
|
: config.context?.read_file_max_chars ?? 24000
|
|
2682
2789
|
}, 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;
|
|
2790
|
+
const readPath = normalizePath(result?.path || args?.path || '').trim();
|
|
2791
|
+
if (readPath) {
|
|
2792
|
+
lastReadPath = readPath;
|
|
2793
|
+
lastReadRange = result?.phase === 'content'
|
|
2794
|
+
? { path: readPath, start_line: result.start_line, end_line: result.end_line }
|
|
2795
|
+
: null;
|
|
2796
|
+
}
|
|
2797
|
+
return result;
|
|
2691
2798
|
},
|
|
2692
2799
|
query_project_index: async (args) => {
|
|
2693
2800
|
await ensureProjectIndex();
|
|
@@ -2713,19 +2820,20 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2713
2820
|
edit: async (args) => {
|
|
2714
2821
|
await ensureProjectIndex();
|
|
2715
2822
|
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
|
-
: {};
|
|
2823
|
+
const hasReplaceTextArgs = args?.edit?.old_text != null || args?.old_text != null || args?.old_string != null;
|
|
2824
|
+
const astTarget = hasReplaceTextArgs || (normalizedKind && normalizedKind !== 'replace_block')
|
|
2825
|
+
? null
|
|
2826
|
+
: resolveCachedAstTarget(args, { requireAstScope: normalizedKind === 'replace_block' });
|
|
2827
|
+
const editPath = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.ast_target?.path || args?.edit?.target?.path || '', { stripInlineRange: true }).trim();
|
|
2828
|
+
const shouldUseRecentReadRange =
|
|
2829
|
+
editPath &&
|
|
2830
|
+
lastReadRange?.path === editPath &&
|
|
2831
|
+
!Number.isFinite(Number(args?.start_line || args?.line || args?.edit?.start_line)) &&
|
|
2832
|
+
!Number.isFinite(Number(args?.end_line || args?.edit?.end_line));
|
|
2833
|
+
const rangeArgs = shouldUseRecentReadRange
|
|
2834
|
+
? { start_line: lastReadRange.start_line, end_line: lastReadRange.end_line }
|
|
2835
|
+
: {};
|
|
2836
|
+
const backup = await backupNonGitPathOnce(editPath || astTarget?.path);
|
|
2729
2837
|
const result = await editTarget(
|
|
2730
2838
|
workspaceRoot,
|
|
2731
2839
|
astTarget
|
|
@@ -2733,21 +2841,25 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2733
2841
|
: { ...args, ...rangeArgs, recent_file: lastReadPath },
|
|
2734
2842
|
config
|
|
2735
2843
|
);
|
|
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
|
-
|
|
2844
|
+
if (result?.path) await refreshProjectFile(result.path);
|
|
2845
|
+
return attachBackup(result, backup);
|
|
2846
|
+
},
|
|
2847
|
+
write: async (args) => {
|
|
2848
|
+
await ensureProjectIndex();
|
|
2849
|
+
const writePath = normalizeFilePathValue(args?.path || args?.file || args?.file_path || '', { stripInlineRange: true }).trim();
|
|
2850
|
+
const backup = await backupNonGitPathOnce(writePath);
|
|
2851
|
+
const result = await writeFile(workspaceRoot, args, config);
|
|
2852
|
+
if (result?.path) await refreshProjectFile(result.path);
|
|
2853
|
+
return attachBackup(result, backup);
|
|
2854
|
+
},
|
|
2855
|
+
delete: Object.assign(async (args) => {
|
|
2856
|
+
await ensureProjectIndex();
|
|
2857
|
+
const deletePathValue = normalizeFilePathValue(args?.path || args?.file || args?.file_path || args?.target || '', { stripInlineRange: true }).trim();
|
|
2858
|
+
const backup = await backupNonGitPathOnce(deletePathValue);
|
|
2859
|
+
const result = await deletePath(workspaceRoot, args, config);
|
|
2860
|
+
if (result?.path) await refreshProjectFile(result.path);
|
|
2861
|
+
return attachBackup(result, backup);
|
|
2862
|
+
}, {
|
|
2751
2863
|
prepareApproval: async (args) => {
|
|
2752
2864
|
const target = await prepareDeleteTarget(workspaceRoot, args, config);
|
|
2753
2865
|
return {
|
|
@@ -3029,41 +3141,50 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
3029
3141
|
return lines.join('\n');
|
|
3030
3142
|
},
|
|
3031
3143
|
|
|
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
|
-
|
|
3144
|
+
edit(result) {
|
|
3145
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
3146
|
+
const p = result.path || '';
|
|
3147
|
+
const action = result.action || '';
|
|
3148
|
+
const line = result.changed_line || 0;
|
|
3149
|
+
const backup = result.backupPath
|
|
3150
|
+
? `\nbackup: ${result.backupPath}${result.backupReused ? ' (reused)' : ''}`
|
|
3151
|
+
: '';
|
|
3152
|
+
const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}${backup}`;
|
|
3153
|
+
const diffPreview = result.diff_preview || '';
|
|
3154
|
+
if (diffPreview) {
|
|
3155
|
+
const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
|
|
3041
3156
|
return `${summary}\n${trimmed}`;
|
|
3042
3157
|
}
|
|
3043
3158
|
return summary + (result.ok !== false ? '' : ` [FAILED: ${result.error || 'unknown'}]`);
|
|
3044
3159
|
},
|
|
3045
3160
|
|
|
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
|
-
|
|
3161
|
+
write(result) {
|
|
3162
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
3163
|
+
const p = result.path || '';
|
|
3164
|
+
const action = result.action || 'write';
|
|
3165
|
+
const line = result.changed_line || 0;
|
|
3166
|
+
const backup = result.backupPath
|
|
3167
|
+
? `\nbackup: ${result.backupPath}${result.backupReused ? ' (reused)' : ''}`
|
|
3168
|
+
: '';
|
|
3169
|
+
const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}${backup}`;
|
|
3170
|
+
const diffPreview = result.diff_preview || '';
|
|
3171
|
+
if (diffPreview) {
|
|
3172
|
+
const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
|
|
3055
3173
|
return `${summary}\n${trimmed}`;
|
|
3056
3174
|
}
|
|
3057
3175
|
return summary;
|
|
3058
3176
|
},
|
|
3059
3177
|
|
|
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
|
-
|
|
3178
|
+
delete(result) {
|
|
3179
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
3180
|
+
if (result.ok === false) return JSON.stringify(result);
|
|
3181
|
+
const kind = result.type || 'item';
|
|
3182
|
+
const target = result.path || '';
|
|
3183
|
+
const backup = result.backupPath
|
|
3184
|
+
? `\nbackup: ${result.backupPath}${result.backupReused ? ' (reused)' : ''}`
|
|
3185
|
+
: '';
|
|
3186
|
+
return `[delete: ${kind}] deleted ${target}${backup}`;
|
|
3187
|
+
},
|
|
3067
3188
|
|
|
3068
3189
|
run(result) {
|
|
3069
3190
|
if (!result || typeof result !== 'object') return String(result);
|