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.
@@ -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
- description: 'Read a text file. Returns its content (truncated if huge).',
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
- description: 'Write (or overwrite) a text file with content. Creates parent directories. PREFER edit_file for changing existing files — write_file is for NEW files or full rewrites only.',
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. Usage: read the file first; pass oldString (exact text including indentation, WITHOUT line-number prefixes) and newString. FAILS if oldString is not found ("oldString not found in content") or found multiple times ("Found multiple matches...") — provide more surrounding lines to make it unique, or set replace_all:true to replace every instance. ALWAYS prefer this tool over write_file for existing files; NEVER reformat unrelated code.',
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: 'Exact text to replace (must match file content verbatim)' },
954
- new_string: { type: 'string', description: 'Replacement text (can be empty to delete)' },
955
- replace_all: { type: 'boolean', description: 'Replace every occurrence instead of failing on multiple matches' },
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
- const abs = safeResolve(String(args.path || ''), cwd);
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
- if (!oldS) return JSON.stringify({ ok: false, error: 'old_string boş olamaz' });
1109
- if (!fs.existsSync(abs)) return JSON.stringify({ ok: false, error: 'dosya bulunamadı: ' + abs });
1110
- const src = fs.readFileSync(abs, 'utf8');
1111
- const count = src.split(oldS).length - 1;
1112
- if (count === 0) {
1113
- return JSON.stringify({
1114
- ok: false,
1115
- error: 'oldString not found in content — dosyayı read_file ile tekrar oku; satır numarası ön ekini (ör. "1: ") EKLEME, içerik birebir olmalı',
1116
- });
1117
- }
1118
- if (count > 1 && !args.replace_all && !args.replaceAll) {
1119
- return JSON.stringify({
1120
- ok: false,
1121
- error: `Found multiple matches for oldString (${count} kez). Daha fazla bağlam satırı ekleyerek benzersiz yap ya da replace_all:true ver`,
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, String(args.content ?? ''), 'utf8');
1194
- return JSON.stringify({ ok: true, path: abs, bytes: Buffer.byteLength(String(args.content ?? '')) });
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,