beast-agent 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/agentdefs.js +122 -0
- package/src/agent/engine.js +176 -22
- package/src/agent/skills.js +1 -0
- package/src/agent/tools.js +527 -35
- package/src/main.js +41 -0
- package/src/preload.js +1 -0
- package/src/renderer/index.html +7 -1
- package/src/renderer/renderer.js +291 -13
- package/src/renderer/style.css +369 -254
package/src/agent/tools.js
CHANGED
|
@@ -894,6 +894,414 @@ async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, signal } = {}) {
|
|
|
894
894
|
};
|
|
895
895
|
}
|
|
896
896
|
|
|
897
|
+
/* ================= opencode edit.ts BİREBİR PORT =================
|
|
898
|
+
Kaynak: opencode-dev/packages/opencode/src/tool/edit.ts
|
|
899
|
+
9 aşamalı replacer zinciri: Simple → LineTrimmed → BlockAnchor →
|
|
900
|
+
WhitespaceNormalized → IndentationFlexible → EscapeNormalized →
|
|
901
|
+
TrimmedBoundary → ContextAware → MultiOccurrence.
|
|
902
|
+
Modelin old_string'i ufak girinti/boşluk farkıyla tutturamadığında zincir
|
|
903
|
+
akıllı eşleşme bulur — "dosyayı baştan oku" döngüsü kökten kırılır. */
|
|
904
|
+
|
|
905
|
+
function normalizeLineEndings(text) {
|
|
906
|
+
return text.replaceAll('\r\n', '\n');
|
|
907
|
+
}
|
|
908
|
+
function detectLineEnding(text) {
|
|
909
|
+
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
910
|
+
}
|
|
911
|
+
function convertToLineEnding(text, ending) {
|
|
912
|
+
if (ending === '\n') return text;
|
|
913
|
+
return text.replaceAll('\n', '\r\n');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/* blok-anchor fallback benzerlik eşikleri (edit.ts:220-221) */
|
|
917
|
+
const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.65;
|
|
918
|
+
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.65;
|
|
919
|
+
|
|
920
|
+
function levenshtein(a, b) {
|
|
921
|
+
if (a === '' || b === '') return Math.max(a.length, b.length);
|
|
922
|
+
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
923
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
|
|
924
|
+
);
|
|
925
|
+
for (let i = 1; i <= a.length; i++) {
|
|
926
|
+
for (let j = 1; j <= b.length; j++) {
|
|
927
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
928
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return matrix[a.length][b.length];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const SimpleReplacer = function* (_content, find) {
|
|
935
|
+
yield find;
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const LineTrimmedReplacer = function* (content, find) {
|
|
939
|
+
const originalLines = content.split('\n');
|
|
940
|
+
const searchLines = find.split('\n');
|
|
941
|
+
if (searchLines[searchLines.length - 1] === '') searchLines.pop();
|
|
942
|
+
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
943
|
+
let matches = true;
|
|
944
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
945
|
+
if (originalLines[i + j].trim() !== searchLines[j].trim()) {
|
|
946
|
+
matches = false;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (matches) {
|
|
951
|
+
let matchStartIndex = 0;
|
|
952
|
+
for (let k = 0; k < i; k++) matchStartIndex += originalLines[k].length + 1;
|
|
953
|
+
let matchEndIndex = matchStartIndex;
|
|
954
|
+
for (let k = 0; k < searchLines.length; k++) {
|
|
955
|
+
matchEndIndex += originalLines[i + k].length;
|
|
956
|
+
if (k < searchLines.length - 1) matchEndIndex += 1;
|
|
957
|
+
}
|
|
958
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
const BlockAnchorReplacer = function* (content, find) {
|
|
964
|
+
const originalLines = content.split('\n');
|
|
965
|
+
const searchLines = find.split('\n');
|
|
966
|
+
if (searchLines.length < 3) return;
|
|
967
|
+
if (searchLines[searchLines.length - 1] === '') searchLines.pop();
|
|
968
|
+
const firstLineSearch = searchLines[0].trim();
|
|
969
|
+
const lastLineSearch = searchLines[searchLines.length - 1].trim();
|
|
970
|
+
const searchBlockSize = searchLines.length;
|
|
971
|
+
const maxLineDelta = Math.max(1, Math.floor(searchBlockSize * 0.25));
|
|
972
|
+
const candidates = [];
|
|
973
|
+
for (let i = 0; i < originalLines.length; i++) {
|
|
974
|
+
if (originalLines[i].trim() !== firstLineSearch) continue;
|
|
975
|
+
for (let j = i + 2; j < originalLines.length; j++) {
|
|
976
|
+
if (originalLines[j].trim() === lastLineSearch) {
|
|
977
|
+
const actualBlockSize = j - i + 1;
|
|
978
|
+
if (Math.abs(actualBlockSize - searchBlockSize) <= maxLineDelta) {
|
|
979
|
+
candidates.push({ startLine: i, endLine: j });
|
|
980
|
+
}
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (candidates.length === 0) return;
|
|
986
|
+
if (candidates.length === 1) {
|
|
987
|
+
const { startLine, endLine } = candidates[0];
|
|
988
|
+
const actualBlockSize = endLine - startLine + 1;
|
|
989
|
+
let similarity = 0;
|
|
990
|
+
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2);
|
|
991
|
+
if (linesToCheck > 0) {
|
|
992
|
+
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
993
|
+
const originalLine = originalLines[startLine + j].trim();
|
|
994
|
+
const searchLine = searchLines[j].trim();
|
|
995
|
+
const maxLen = Math.max(originalLine.length, searchLine.length);
|
|
996
|
+
if (maxLen === 0) continue;
|
|
997
|
+
const distance = levenshtein(originalLine, searchLine);
|
|
998
|
+
similarity += (1 - distance / maxLen) / linesToCheck;
|
|
999
|
+
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) break;
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
similarity = 1.0;
|
|
1003
|
+
}
|
|
1004
|
+
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
|
|
1005
|
+
let matchStartIndex = 0;
|
|
1006
|
+
for (let k = 0; k < startLine; k++) matchStartIndex += originalLines[k].length + 1;
|
|
1007
|
+
let matchEndIndex = matchStartIndex;
|
|
1008
|
+
for (let k = startLine; k <= endLine; k++) {
|
|
1009
|
+
matchEndIndex += originalLines[k].length;
|
|
1010
|
+
if (k < endLine) matchEndIndex += 1;
|
|
1011
|
+
}
|
|
1012
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
let bestMatch = null;
|
|
1017
|
+
let maxSimilarity = -1;
|
|
1018
|
+
for (const candidate of candidates) {
|
|
1019
|
+
const { startLine, endLine } = candidate;
|
|
1020
|
+
const actualBlockSize = endLine - startLine + 1;
|
|
1021
|
+
let similarity = 0;
|
|
1022
|
+
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2);
|
|
1023
|
+
if (linesToCheck > 0) {
|
|
1024
|
+
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
1025
|
+
const originalLine = originalLines[startLine + j].trim();
|
|
1026
|
+
const searchLine = searchLines[j].trim();
|
|
1027
|
+
const maxLen = Math.max(originalLine.length, searchLine.length);
|
|
1028
|
+
if (maxLen === 0) continue;
|
|
1029
|
+
const distance = levenshtein(originalLine, searchLine);
|
|
1030
|
+
similarity += 1 - distance / maxLen;
|
|
1031
|
+
}
|
|
1032
|
+
similarity /= linesToCheck;
|
|
1033
|
+
} else {
|
|
1034
|
+
similarity = 1.0;
|
|
1035
|
+
}
|
|
1036
|
+
if (similarity > maxSimilarity) {
|
|
1037
|
+
maxSimilarity = similarity;
|
|
1038
|
+
bestMatch = candidate;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
|
|
1042
|
+
const { startLine, endLine } = bestMatch;
|
|
1043
|
+
let matchStartIndex = 0;
|
|
1044
|
+
for (let k = 0; k < startLine; k++) matchStartIndex += originalLines[k].length + 1;
|
|
1045
|
+
let matchEndIndex = matchStartIndex;
|
|
1046
|
+
for (let k = startLine; k <= endLine; k++) {
|
|
1047
|
+
matchEndIndex += originalLines[k].length;
|
|
1048
|
+
if (k < endLine) matchEndIndex += 1;
|
|
1049
|
+
}
|
|
1050
|
+
yield content.substring(matchStartIndex, matchEndIndex);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const WhitespaceNormalizedReplacer = function* (content, find) {
|
|
1055
|
+
const normalizeWhitespace = (text) => text.replace(/\s+/g, ' ').trim();
|
|
1056
|
+
const normalizedFind = normalizeWhitespace(find);
|
|
1057
|
+
const lines = content.split('\n');
|
|
1058
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1059
|
+
const line = lines[i];
|
|
1060
|
+
if (normalizeWhitespace(line) === normalizedFind) {
|
|
1061
|
+
yield line;
|
|
1062
|
+
} else {
|
|
1063
|
+
const normalizedLine = normalizeWhitespace(line);
|
|
1064
|
+
if (normalizedLine.includes(normalizedFind)) {
|
|
1065
|
+
const words = find.trim().split(/\s+/);
|
|
1066
|
+
if (words.length > 0) {
|
|
1067
|
+
const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('\\s+');
|
|
1068
|
+
try {
|
|
1069
|
+
const regex = new RegExp(pattern);
|
|
1070
|
+
const match = line.match(regex);
|
|
1071
|
+
if (match) yield match[0];
|
|
1072
|
+
} catch {}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const findLines = find.split('\n');
|
|
1078
|
+
if (findLines.length > 1) {
|
|
1079
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1080
|
+
const block = lines.slice(i, i + findLines.length);
|
|
1081
|
+
if (normalizeWhitespace(block.join('\n')) === normalizedFind) {
|
|
1082
|
+
yield block.join('\n');
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const IndentationFlexibleReplacer = function* (content, find) {
|
|
1089
|
+
const removeIndentation = (text) => {
|
|
1090
|
+
const lines = text.split('\n');
|
|
1091
|
+
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
|
|
1092
|
+
if (nonEmptyLines.length === 0) return text;
|
|
1093
|
+
const minIndent = Math.min(
|
|
1094
|
+
...nonEmptyLines.map((line) => {
|
|
1095
|
+
const match = line.match(/^(\s*)/);
|
|
1096
|
+
return match ? match[1].length : 0;
|
|
1097
|
+
})
|
|
1098
|
+
);
|
|
1099
|
+
return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join('\n');
|
|
1100
|
+
};
|
|
1101
|
+
const normalizedFind = removeIndentation(find);
|
|
1102
|
+
const contentLines = content.split('\n');
|
|
1103
|
+
const findLines = find.split('\n');
|
|
1104
|
+
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
1105
|
+
const block = contentLines.slice(i, i + findLines.length).join('\n');
|
|
1106
|
+
if (removeIndentation(block) === normalizedFind) yield block;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const EscapeNormalizedReplacer = function* (content, find) {
|
|
1111
|
+
const unescapeString = (str) =>
|
|
1112
|
+
str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
|
1113
|
+
switch (capturedChar) {
|
|
1114
|
+
case 'n': return '\n';
|
|
1115
|
+
case 't': return '\t';
|
|
1116
|
+
case 'r': return '\r';
|
|
1117
|
+
case "'": return "'";
|
|
1118
|
+
case '"': return '"';
|
|
1119
|
+
case '`': return '`';
|
|
1120
|
+
case '\\': return '\\';
|
|
1121
|
+
case '\n': return '\n';
|
|
1122
|
+
case '$': return '$';
|
|
1123
|
+
default: return match;
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
const unescapedFind = unescapeString(find);
|
|
1127
|
+
if (content.includes(unescapedFind)) yield unescapedFind;
|
|
1128
|
+
const lines = content.split('\n');
|
|
1129
|
+
const findLines = unescapedFind.split('\n');
|
|
1130
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1131
|
+
const block = lines.slice(i, i + findLines.length).join('\n');
|
|
1132
|
+
if (unescapeString(block) === unescapedFind) yield block;
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
const MultiOccurrenceReplacer = function* (content, find) {
|
|
1137
|
+
let startIndex = 0;
|
|
1138
|
+
while (true) {
|
|
1139
|
+
const index = content.indexOf(find, startIndex);
|
|
1140
|
+
if (index === -1) break;
|
|
1141
|
+
yield find;
|
|
1142
|
+
startIndex = index + find.length;
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const TrimmedBoundaryReplacer = function* (content, find) {
|
|
1147
|
+
const trimmedFind = find.trim();
|
|
1148
|
+
if (trimmedFind === find) return;
|
|
1149
|
+
if (content.includes(trimmedFind)) yield trimmedFind;
|
|
1150
|
+
const lines = content.split('\n');
|
|
1151
|
+
const findLines = find.split('\n');
|
|
1152
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1153
|
+
const block = lines.slice(i, i + findLines.length).join('\n');
|
|
1154
|
+
if (block.trim() === trimmedFind) yield block;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const ContextAwareReplacer = function* (content, find) {
|
|
1159
|
+
const findLines = find.split('\n');
|
|
1160
|
+
if (findLines.length < 3) return;
|
|
1161
|
+
if (findLines[findLines.length - 1] === '') findLines.pop();
|
|
1162
|
+
const contentLines = content.split('\n');
|
|
1163
|
+
const firstLine = findLines[0].trim();
|
|
1164
|
+
const lastLine = findLines[findLines.length - 1].trim();
|
|
1165
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
1166
|
+
if (contentLines[i].trim() !== firstLine) continue;
|
|
1167
|
+
for (let j = i + 2; j < contentLines.length; j++) {
|
|
1168
|
+
if (contentLines[j].trim() === lastLine) {
|
|
1169
|
+
const blockLines = contentLines.slice(i, j + 1);
|
|
1170
|
+
const block = blockLines.join('\n');
|
|
1171
|
+
if (blockLines.length === findLines.length) {
|
|
1172
|
+
let matchingLines = 0;
|
|
1173
|
+
let totalNonEmptyLines = 0;
|
|
1174
|
+
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
1175
|
+
const blockLine = blockLines[k].trim();
|
|
1176
|
+
const findLine = findLines[k].trim();
|
|
1177
|
+
if (blockLine.length > 0 || findLine.length > 0) {
|
|
1178
|
+
totalNonEmptyLines++;
|
|
1179
|
+
if (blockLine === findLine) matchingLines++;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
|
|
1183
|
+
yield block;
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
function isDisproportionateMatch(search, oldString) {
|
|
1194
|
+
const oldLines = oldString.split('\n').length;
|
|
1195
|
+
const searchLines = search.split('\n').length;
|
|
1196
|
+
if (searchLines >= Math.max(oldLines + 3, oldLines * 2)) return true;
|
|
1197
|
+
if (oldLines === 1) return false;
|
|
1198
|
+
return search.trim().length > Math.max(oldString.trim().length + 500, oldString.trim().length * 4);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/* edit.ts:682-729 replace() — zinciri sırayla dener; tek eşleşme şart,
|
|
1202
|
+
replaceAll'de tümünü değiştirir; hata mesajları BİREBİR opencode */
|
|
1203
|
+
function ocReplace(content, oldString, newString, replaceAll = false) {
|
|
1204
|
+
if (oldString === newString) {
|
|
1205
|
+
throw new Error('No changes to apply: oldString and newString are identical.');
|
|
1206
|
+
}
|
|
1207
|
+
if (oldString === '') {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
'oldString cannot be empty when editing an existing file. Provide the exact text to replace, or use write_file for an intentional full-file replacement.'
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
let notFound = true;
|
|
1213
|
+
for (const replacer of [
|
|
1214
|
+
SimpleReplacer,
|
|
1215
|
+
LineTrimmedReplacer,
|
|
1216
|
+
BlockAnchorReplacer,
|
|
1217
|
+
WhitespaceNormalizedReplacer,
|
|
1218
|
+
IndentationFlexibleReplacer,
|
|
1219
|
+
EscapeNormalizedReplacer,
|
|
1220
|
+
TrimmedBoundaryReplacer,
|
|
1221
|
+
ContextAwareReplacer,
|
|
1222
|
+
MultiOccurrenceReplacer,
|
|
1223
|
+
]) {
|
|
1224
|
+
for (const search of replacer(content, oldString)) {
|
|
1225
|
+
const index = content.indexOf(search);
|
|
1226
|
+
if (index === -1) continue;
|
|
1227
|
+
notFound = false;
|
|
1228
|
+
if (isDisproportionateMatch(search, oldString)) {
|
|
1229
|
+
throw new Error(
|
|
1230
|
+
'Refusing replacement because the matched span is much larger than oldString. Re-read the file and provide the full exact oldString for the intended replacement.'
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
if (replaceAll) {
|
|
1234
|
+
return { result: content.replaceAll(search, newString), replacements: content.split(search).length - 1 };
|
|
1235
|
+
}
|
|
1236
|
+
const lastIndex = content.lastIndexOf(search);
|
|
1237
|
+
if (index !== lastIndex) continue;
|
|
1238
|
+
return { result: content.substring(0, index) + newString + content.substring(index + search.length), replacements: 1 };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (notFound) {
|
|
1242
|
+
throw new Error(
|
|
1243
|
+
'Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.'
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
throw new Error('Found multiple matches for oldString. Provide more surrounding context to make the match unique.');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/* ---------- opencode diff portu (npm "diff" paketinin diffLines karşılığı) ----------
|
|
1250
|
+
LCS tabanlı satır diff'i: additions/deletions sayımı (edit.ts:175-180) ve
|
|
1251
|
+
UI split-diff görünümü için kırpılmış bölge (trimDiff mantığı). */
|
|
1252
|
+
function diffLineCounts(aText, bText) {
|
|
1253
|
+
/* bos metin = 0 satir (opencode npm diffLines davranisi) */
|
|
1254
|
+
const a = aText ? String(aText).split('\n') : [];
|
|
1255
|
+
const b = bText ? String(bText).split('\n') : [];
|
|
1256
|
+
let p = 0;
|
|
1257
|
+
while (p < a.length && p < b.length && a[p] === b[p]) p++;
|
|
1258
|
+
let ea = a.length - 1;
|
|
1259
|
+
let eb = b.length - 1;
|
|
1260
|
+
while (ea >= p && eb >= p && a[ea] === b[eb]) { ea--; eb--; }
|
|
1261
|
+
const midA = a.slice(p, ea + 1);
|
|
1262
|
+
const midB = b.slice(p, eb + 1);
|
|
1263
|
+
const n = midA.length;
|
|
1264
|
+
const m = midB.length;
|
|
1265
|
+
if (!n && !m) return { additions: 0, deletions: 0 };
|
|
1266
|
+
if (n * m > 400000 || n > 1500 || m > 1500) return { additions: m, deletions: n };
|
|
1267
|
+
const w = m + 1;
|
|
1268
|
+
const dp = new Int32Array((n + 1) * w);
|
|
1269
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
1270
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
1271
|
+
dp[i * w + j] =
|
|
1272
|
+
midA[i] === midB[j]
|
|
1273
|
+
? dp[(i + 1) * w + j + 1] + 1
|
|
1274
|
+
: Math.max(dp[(i + 1) * w + j], dp[i * w + j + 1]);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const matched = dp[0];
|
|
1278
|
+
return { additions: m - matched, deletions: n - matched };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const DIFF_REGION_CTX = 3; /* değişiklik etrafında kaç bağlam satırı gösterilir */
|
|
1282
|
+
const DIFF_REGION_CAP = 3500; /* UI'ye giden bölge başına karakter tavanı */
|
|
1283
|
+
|
|
1284
|
+
function capDiffText(s) {
|
|
1285
|
+
const t = String(s || '');
|
|
1286
|
+
return t.length > DIFF_REGION_CAP ? t.slice(0, DIFF_REGION_CAP) + '\n…' : t;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/* Değişen bölgeyi ±DIFF_REGION_CTX bağlam satırıyla kırpar (opencode trimDiff
|
|
1290
|
+
mantığı — tüm dosya yerine yalnız değişen kısım UI'ye gider). */
|
|
1291
|
+
function diffRegion(before, after) {
|
|
1292
|
+
const a = String(before || '').split('\n');
|
|
1293
|
+
const b = String(after || '').split('\n');
|
|
1294
|
+
let p = 0;
|
|
1295
|
+
while (p < a.length && p < b.length && a[p] === b[p]) p++;
|
|
1296
|
+
let ea = a.length - 1;
|
|
1297
|
+
let eb = b.length - 1;
|
|
1298
|
+
while (ea >= p && eb >= p && a[ea] === b[eb]) { ea--; eb--; }
|
|
1299
|
+
const start = Math.max(0, p - DIFF_REGION_CTX);
|
|
1300
|
+
const beforeRegion = a.slice(start, Math.min(a.length, ea + 1 + DIFF_REGION_CTX)).join('\n');
|
|
1301
|
+
const afterRegion = b.slice(start, Math.min(b.length, eb + 1 + DIFF_REGION_CTX)).join('\n');
|
|
1302
|
+
return { before: capDiffText(beforeRegion), after: capDiffText(afterRegion), startLine: start + 1 };
|
|
1303
|
+
}
|
|
1304
|
+
|
|
897
1305
|
const definitions = [
|
|
898
1306
|
{
|
|
899
1307
|
type: 'function',
|
|
@@ -915,11 +1323,25 @@ const definitions = [
|
|
|
915
1323
|
type: 'function',
|
|
916
1324
|
function: {
|
|
917
1325
|
name: 'read_file',
|
|
918
|
-
|
|
1326
|
+
/* opencode read.txt BİREBİR port (parametre adı path olarak kaldı) */
|
|
1327
|
+
description:
|
|
1328
|
+
'Read a file or directory from the local filesystem. If the path does not exist, an error is returned.\n\n' +
|
|
1329
|
+
'Usage:\n' +
|
|
1330
|
+
'- By default, this tool returns up to 2000 lines from the start of the file.\n' +
|
|
1331
|
+
'- The offset parameter is the line number to start reading from (1-indexed).\n' +
|
|
1332
|
+
'- To read later sections, call this tool again with a larger offset — NEVER re-read from the start of the file.\n' +
|
|
1333
|
+
'- Use the grep tool to find specific content in large files or files with long lines.\n' +
|
|
1334
|
+
'- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.\n' +
|
|
1335
|
+
'- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\\n", you will receive "1: foo\\n".\n' +
|
|
1336
|
+
'- Any line longer than 2000 characters is truncated.\n' +
|
|
1337
|
+
'- Call this tool in parallel when you know there are multiple files you want to read.\n' +
|
|
1338
|
+
'- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.',
|
|
919
1339
|
parameters: {
|
|
920
1340
|
type: 'object',
|
|
921
1341
|
properties: {
|
|
922
|
-
path: { type: 'string' },
|
|
1342
|
+
path: { type: 'string', description: 'The path to the file to read' },
|
|
1343
|
+
offset: { type: 'number', description: 'The line number to start reading from (1-indexed)' },
|
|
1344
|
+
limit: { type: 'number', description: 'The maximum number of lines to read (defaults to 2000)' },
|
|
923
1345
|
},
|
|
924
1346
|
required: ['path'],
|
|
925
1347
|
},
|
|
@@ -929,12 +1351,19 @@ const definitions = [
|
|
|
929
1351
|
type: 'function',
|
|
930
1352
|
function: {
|
|
931
1353
|
name: 'write_file',
|
|
932
|
-
|
|
1354
|
+
/* opencode write.txt BİREBİR port */
|
|
1355
|
+
description:
|
|
1356
|
+
'Writes a file to the local filesystem.\n\n' +
|
|
1357
|
+
'Usage:\n' +
|
|
1358
|
+
'- This tool will overwrite the existing file if there is one at the provided path.\n' +
|
|
1359
|
+
'- If this is an existing file, you MUST use the read_file tool first to read the file\'s contents. This tool will fail if you did not read the file first.\n' +
|
|
1360
|
+
'- ALWAYS prefer editing existing files in the codebase with edit_file. NEVER write new files unless explicitly required.\n' +
|
|
1361
|
+
'- The result includes additions/deletions counts — the change is APPLIED to disk immediately; do NOT read the file again to verify.',
|
|
933
1362
|
parameters: {
|
|
934
1363
|
type: 'object',
|
|
935
1364
|
properties: {
|
|
936
|
-
path: { type: 'string' },
|
|
937
|
-
content: { type: 'string' },
|
|
1365
|
+
path: { type: 'string', description: 'The path to the file to write' },
|
|
1366
|
+
content: { type: 'string', description: 'The content to write to the file' },
|
|
938
1367
|
},
|
|
939
1368
|
required: ['path', 'content'],
|
|
940
1369
|
},
|
|
@@ -944,15 +1373,24 @@ const definitions = [
|
|
|
944
1373
|
type: 'function',
|
|
945
1374
|
function: {
|
|
946
1375
|
name: 'edit_file',
|
|
1376
|
+
/* opencode edit.txt BİREBİR port (parametre adları snake_case kaldı) */
|
|
947
1377
|
description:
|
|
948
|
-
'Performs exact string replacements in files
|
|
1378
|
+
'Performs exact string replacements in files.\n\n' +
|
|
1379
|
+
'Usage:\n' +
|
|
1380
|
+
'- You must use your read_file tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.\n' +
|
|
1381
|
+
'- When editing text from read_file output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.\n' +
|
|
1382
|
+
'- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n' +
|
|
1383
|
+
'- The edit will FAIL if `old_string` is not found in the file with an error "oldString not found in content".\n' +
|
|
1384
|
+
'- The edit will FAIL if `old_string` is found multiple times in the file with an error "Found multiple matches for oldString. Provide more surrounding lines in old_string to identify the correct match." Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.\n' +
|
|
1385
|
+
'- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.\n' +
|
|
1386
|
+
'- The result includes additions/deletions counts — the change is APPLIED to disk immediately; do NOT read the file again to verify.',
|
|
949
1387
|
parameters: {
|
|
950
1388
|
type: 'object',
|
|
951
1389
|
properties: {
|
|
952
|
-
path: { type: 'string' },
|
|
953
|
-
old_string: { type: 'string', description: '
|
|
954
|
-
new_string: { type: 'string', description: '
|
|
955
|
-
replace_all: { type: 'boolean', description: 'Replace
|
|
1390
|
+
path: { type: 'string', description: 'The path to the file to modify' },
|
|
1391
|
+
old_string: { type: 'string', description: 'The text to replace' },
|
|
1392
|
+
new_string: { type: 'string', description: 'The text to replace it with (must be different from old_string)' },
|
|
1393
|
+
replace_all: { type: 'boolean', description: 'Replace all occurrences of old_string (default false)' },
|
|
956
1394
|
},
|
|
957
1395
|
required: ['path', 'old_string', 'new_string'],
|
|
958
1396
|
},
|
|
@@ -1102,32 +1540,54 @@ async function exec(name, args, ctx) {
|
|
|
1102
1540
|
});
|
|
1103
1541
|
}
|
|
1104
1542
|
case 'edit_file': {
|
|
1105
|
-
|
|
1543
|
+
/* opencode edit.ts execute BİREBİR port — replacer zinciri + satır sonu
|
|
1544
|
+
normalizasyonu + diff metadata (additions/deletions + UI diffView) */
|
|
1545
|
+
const filePath = safeResolve(String(args.path || args.filePath || ''), cwd);
|
|
1106
1546
|
const oldS = String(args.old_string ?? args.oldString ?? '');
|
|
1107
1547
|
const newS = String(args.new_string ?? args.newString ?? '');
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1548
|
+
try {
|
|
1549
|
+
if (oldS === newS) {
|
|
1550
|
+
throw new Error('No changes to apply: oldString and newString are identical.');
|
|
1551
|
+
}
|
|
1552
|
+
/* boş oldString = YENİ dosya oluşturma yolu (edit.ts:90-121) */
|
|
1553
|
+
if (oldS === '') {
|
|
1554
|
+
if (fs.existsSync(filePath)) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
'oldString cannot be empty when editing an existing file. Provide the exact text to replace, or use write_file for an intentional full-file replacement.'
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1560
|
+
fs.writeFileSync(filePath, newS, 'utf8');
|
|
1561
|
+
const counts = diffLineCounts('', newS);
|
|
1562
|
+
const out = { ok: true, path: filePath, note: 'Edit applied successfully.', replacements: 1, ...counts };
|
|
1563
|
+
if (ctx.wantDiff) out.diffView = { path: filePath, before: '', after: capDiffText(newS), startLine: 1, ...counts };
|
|
1564
|
+
return JSON.stringify(out);
|
|
1565
|
+
}
|
|
1566
|
+
if (!fs.existsSync(filePath)) throw new Error(`File ${filePath} not found`);
|
|
1567
|
+
if (fs.statSync(filePath).isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`);
|
|
1568
|
+
const contentOld = fs.readFileSync(filePath, 'utf8');
|
|
1569
|
+
/* satır sonu stili dosyadan alınır, old/new buna çevrilir (edit.ts:129-131) */
|
|
1570
|
+
const ending = detectLineEnding(contentOld);
|
|
1571
|
+
const oldN = convertToLineEnding(normalizeLineEndings(oldS), ending);
|
|
1572
|
+
const newN = convertToLineEnding(normalizeLineEndings(newS), ending);
|
|
1573
|
+
const rep = ocReplace(contentOld, oldN, newN, !!(args.replace_all || args.replaceAll));
|
|
1574
|
+
const contentNew = rep.result;
|
|
1575
|
+
fs.writeFileSync(filePath, contentNew, 'utf8');
|
|
1576
|
+
const counts = diffLineCounts(contentOld, contentNew);
|
|
1577
|
+
const out = {
|
|
1578
|
+
ok: true,
|
|
1579
|
+
path: filePath,
|
|
1580
|
+
note: 'Edit applied successfully.',
|
|
1581
|
+
replacements: rep.replacements,
|
|
1582
|
+
...counts,
|
|
1583
|
+
};
|
|
1584
|
+
if (ctx.wantDiff) {
|
|
1585
|
+
out.diffView = { path: filePath, ...diffRegion(contentOld, contentNew), ...counts };
|
|
1586
|
+
}
|
|
1587
|
+
return JSON.stringify(out);
|
|
1588
|
+
} catch (e) {
|
|
1589
|
+
return JSON.stringify({ ok: false, error: String((e && e.message) || e) });
|
|
1123
1590
|
}
|
|
1124
|
-
const out = args.replace_all || args.replaceAll ? src.split(oldS).join(newS) : src.replace(oldS, newS);
|
|
1125
|
-
fs.writeFileSync(abs, out, 'utf8');
|
|
1126
|
-
return JSON.stringify({
|
|
1127
|
-
ok: true,
|
|
1128
|
-
path: abs,
|
|
1129
|
-
replacements: args.replace_all || args.replaceAll ? count : 1,
|
|
1130
|
-
});
|
|
1131
1591
|
}
|
|
1132
1592
|
case 'read_file': {
|
|
1133
1593
|
const abs = safeResolve(String(args.path || ''), cwd);
|
|
@@ -1184,14 +1644,31 @@ async function exec(name, args, ctx) {
|
|
|
1184
1644
|
totalLines: allLines.length,
|
|
1185
1645
|
offset,
|
|
1186
1646
|
truncated,
|
|
1647
|
+
/* opencode read.ts çıktı işaretleri portu: model devamını offset ile
|
|
1648
|
+
okur — BAŞTAN okuma döngüsü kırılır */
|
|
1649
|
+
...(truncated
|
|
1650
|
+
? { note: `(Devam ediyor — toplam ${allLines.length} satır. Kalan bölüm için offset:${offset + slice.length} ile oku; dosyayı BAŞTAN okuma.)` }
|
|
1651
|
+
: { note: `(End of file - total ${allLines.length} lines)` }),
|
|
1187
1652
|
content,
|
|
1188
1653
|
});
|
|
1189
1654
|
}
|
|
1190
1655
|
case 'write_file': {
|
|
1656
|
+
/* opencode write.ts port: üzerine yazmadan önce eski içerik alınır,
|
|
1657
|
+
sonuçta additions/deletions + UI diffView döner */
|
|
1191
1658
|
const abs = safeResolve(String(args.path || ''), cwd);
|
|
1659
|
+
const content = String(args.content ?? '');
|
|
1660
|
+
const existed = fs.existsSync(abs);
|
|
1661
|
+
const before = existed && !fs.statSync(abs).isDirectory() ? fs.readFileSync(abs, 'utf8') : '';
|
|
1192
1662
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
1193
|
-
fs.writeFileSync(abs,
|
|
1194
|
-
|
|
1663
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
1664
|
+
const counts = diffLineCounts(before, content);
|
|
1665
|
+
const out = { ok: true, path: abs, bytes: Buffer.byteLength(content), ...counts };
|
|
1666
|
+
if (ctx.wantDiff) {
|
|
1667
|
+
out.diffView = existed
|
|
1668
|
+
? { path: abs, ...diffRegion(before, content), ...counts }
|
|
1669
|
+
: { path: abs, before: '', after: capDiffText(content), startLine: 1, ...counts };
|
|
1670
|
+
}
|
|
1671
|
+
return JSON.stringify(out);
|
|
1195
1672
|
}
|
|
1196
1673
|
case 'list_dir': {
|
|
1197
1674
|
const abs = args.path ? safeResolve(String(args.path), cwd) : cwd;
|
|
@@ -1477,6 +1954,21 @@ async function tinyfishSearch(query, maxResults, signal) {
|
|
|
1477
1954
|
module.exports = {
|
|
1478
1955
|
definitions,
|
|
1479
1956
|
exec,
|
|
1957
|
+
/* opencode edit.ts portu (test + dış kullanım) */
|
|
1958
|
+
ocReplace,
|
|
1959
|
+
diffLineCounts,
|
|
1960
|
+
diffRegion,
|
|
1961
|
+
ocReplacers: {
|
|
1962
|
+
SimpleReplacer,
|
|
1963
|
+
LineTrimmedReplacer,
|
|
1964
|
+
BlockAnchorReplacer,
|
|
1965
|
+
WhitespaceNormalizedReplacer,
|
|
1966
|
+
IndentationFlexibleReplacer,
|
|
1967
|
+
EscapeNormalizedReplacer,
|
|
1968
|
+
TrimmedBoundaryReplacer,
|
|
1969
|
+
ContextAwareReplacer,
|
|
1970
|
+
MultiOccurrenceReplacer,
|
|
1971
|
+
},
|
|
1480
1972
|
runCommand,
|
|
1481
1973
|
runShellCommand,
|
|
1482
1974
|
runBashCommand,
|