@ricsam/r5d-worker 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/main.cjs +243 -210
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +243 -210
- package/dist/mjs/package.json +1 -1
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -35,10 +35,9 @@ __export(main_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(main_exports);
|
|
37
37
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
38
|
-
var import_node_os = __toESM(require("node:os"), 1);
|
|
39
38
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
40
39
|
var import_node_crypto = require("node:crypto");
|
|
41
|
-
var
|
|
40
|
+
var import_node_os = __toESM(require("node:os"), 1);
|
|
42
41
|
var import_node_child_process = require("node:child_process");
|
|
43
42
|
var import_git_identity = require("./git-identity.cjs");
|
|
44
43
|
const import_meta = {};
|
|
@@ -47,6 +46,7 @@ const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
|
47
46
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
48
47
|
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
49
48
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
49
|
+
const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
50
50
|
const MAX_LINE_LENGTH = 2e3;
|
|
51
51
|
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
52
52
|
const R5D_REMOTE_NAME = "r5d";
|
|
@@ -921,16 +921,36 @@ function formatLineNumberedContent(content, offset, limit) {
|
|
|
921
921
|
const endLine = Math.min(startLine + readLimit - 1, totalLines);
|
|
922
922
|
const selectedLines = allLines.slice(startLine - 1, endLine);
|
|
923
923
|
const padWidth = String(endLine).length;
|
|
924
|
-
const formattedLines =
|
|
924
|
+
const formattedLines = [];
|
|
925
|
+
let actualEndLine = endLine;
|
|
926
|
+
let budgetTruncated = false;
|
|
927
|
+
for (let index = 0; index < selectedLines.length; index += 1) {
|
|
928
|
+
const line = selectedLines[index] ?? "";
|
|
925
929
|
const lineNumber = startLine + index;
|
|
926
930
|
const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
|
|
927
|
-
|
|
928
|
-
|
|
931
|
+
const formattedLine = `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
|
|
932
|
+
const candidateLength = formattedLines.length === 0 ? formattedLine.length : formattedLine.length + 1;
|
|
933
|
+
const currentLength = formattedLines.join("\n").length;
|
|
934
|
+
if (currentLength + candidateLength > DEFAULT_READ_MAX_BYTES) {
|
|
935
|
+
budgetTruncated = true;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
formattedLines.push(formattedLine);
|
|
939
|
+
actualEndLine = lineNumber;
|
|
940
|
+
}
|
|
941
|
+
let formattedContent = formattedLines.join("\n");
|
|
942
|
+
if (budgetTruncated) {
|
|
943
|
+
formattedContent = `${formattedContent}
|
|
944
|
+
... (truncated by byte budget)`;
|
|
945
|
+
}
|
|
946
|
+
const truncated = budgetTruncated || actualEndLine < totalLines;
|
|
929
947
|
return {
|
|
930
|
-
content:
|
|
948
|
+
content: formattedContent,
|
|
931
949
|
totalLines,
|
|
932
950
|
startLine,
|
|
933
|
-
endLine
|
|
951
|
+
endLine: actualEndLine,
|
|
952
|
+
truncated,
|
|
953
|
+
continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
|
|
934
954
|
};
|
|
935
955
|
}
|
|
936
956
|
function readPngDimensions(bytes) {
|
|
@@ -997,6 +1017,27 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
997
1017
|
}
|
|
998
1018
|
return dimensions;
|
|
999
1019
|
}
|
|
1020
|
+
const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
1021
|
+
async function withFileMutationQueue(key, operation) {
|
|
1022
|
+
const previous = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
1023
|
+
let release;
|
|
1024
|
+
const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
|
|
1025
|
+
release = resolve;
|
|
1026
|
+
}));
|
|
1027
|
+
fileMutationQueues.set(key, current);
|
|
1028
|
+
await previous.catch(() => void 0);
|
|
1029
|
+
try {
|
|
1030
|
+
return await operation();
|
|
1031
|
+
} finally {
|
|
1032
|
+
release();
|
|
1033
|
+
if (fileMutationQueues.get(key) === current) {
|
|
1034
|
+
fileMutationQueues.delete(key);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function mutationQueueKey(projectId, branchName, filePath) {
|
|
1039
|
+
return `${projectId}:${branchName}:${import_node_path.default.posix.normalize(filePath)}`;
|
|
1040
|
+
}
|
|
1000
1041
|
async function executeReadFileOperation(input) {
|
|
1001
1042
|
const artifact = await resolveArtifactEnvFilePath({
|
|
1002
1043
|
filePath: input.message.filePath,
|
|
@@ -1014,7 +1055,8 @@ async function executeReadFileOperation(input) {
|
|
|
1014
1055
|
const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
|
|
1015
1056
|
const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
|
|
1016
1057
|
return {
|
|
1017
|
-
type: "
|
|
1058
|
+
type: "read",
|
|
1059
|
+
kind: "text",
|
|
1018
1060
|
file: artifact.virtualPath,
|
|
1019
1061
|
gitBlobHash: getGitBlobHashForContent(buffer2),
|
|
1020
1062
|
...formatted2
|
|
@@ -1028,222 +1070,209 @@ async function executeReadFileOperation(input) {
|
|
|
1028
1070
|
const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
|
|
1029
1071
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
1030
1072
|
return {
|
|
1031
|
-
type: "
|
|
1073
|
+
type: "read",
|
|
1074
|
+
kind: "text",
|
|
1032
1075
|
file: resolved.virtualPath,
|
|
1033
1076
|
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
1034
1077
|
...formatted
|
|
1035
1078
|
};
|
|
1036
1079
|
}
|
|
1037
|
-
async function
|
|
1080
|
+
async function executeWriteFileOperation(input) {
|
|
1038
1081
|
rejectArtifactWritePath(input.message.filePath);
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
};
|
|
1082
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1083
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1084
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1085
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
|
|
1086
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
|
|
1087
|
+
return {
|
|
1088
|
+
type: "write",
|
|
1089
|
+
file: resolved.virtualPath,
|
|
1090
|
+
gitBlobHash: getGitBlobHashForContent(input.message.content)
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1051
1093
|
}
|
|
1052
1094
|
async function executeEditFileOperation(input) {
|
|
1053
1095
|
rejectArtifactWritePath(input.message.filePath);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1096
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1097
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1098
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1099
|
+
if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
|
|
1100
|
+
throw new Error("edit requires at least one replacement");
|
|
1101
|
+
}
|
|
1102
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
1103
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
1104
|
+
}
|
|
1105
|
+
const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
|
|
1106
|
+
const ranges = input.message.edits.map((edit, index) => {
|
|
1107
|
+
if (edit.oldText === edit.newText) {
|
|
1108
|
+
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
1109
|
+
}
|
|
1110
|
+
if (edit.oldText.length === 0) {
|
|
1111
|
+
throw new Error(`edits[${index}].oldText cannot be empty`);
|
|
1112
|
+
}
|
|
1113
|
+
const occurrences = originalContent.split(edit.oldText).length - 1;
|
|
1114
|
+
if (occurrences === 0) {
|
|
1115
|
+
throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
|
|
1116
|
+
}
|
|
1117
|
+
if (occurrences > 1) {
|
|
1118
|
+
throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
|
|
1119
|
+
}
|
|
1120
|
+
const start = originalContent.indexOf(edit.oldText);
|
|
1121
|
+
return { index, start, end: start + edit.oldText.length, edit };
|
|
1122
|
+
});
|
|
1123
|
+
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
|
|
1124
|
+
for (let index = 1; index < sortedRanges.length; index += 1) {
|
|
1125
|
+
const previous = sortedRanges[index - 1];
|
|
1126
|
+
const current = sortedRanges[index];
|
|
1127
|
+
if (current.start < previous.end) {
|
|
1128
|
+
throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
|
|
1129
|
+
}
|
|
1074
1130
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1131
|
+
let nextContent = originalContent;
|
|
1132
|
+
for (const range of [...sortedRanges].reverse()) {
|
|
1133
|
+
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
1134
|
+
}
|
|
1135
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
|
|
1136
|
+
return {
|
|
1137
|
+
type: "edit",
|
|
1138
|
+
file: resolved.virtualPath,
|
|
1139
|
+
edits: input.message.edits.length
|
|
1140
|
+
};
|
|
1141
|
+
});
|
|
1082
1142
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1143
|
+
const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
|
|
1144
|
+
function normalizeToolLimit(value, fallback, max) {
|
|
1145
|
+
if (!Number.isFinite(value) || value === void 0) return fallback;
|
|
1146
|
+
return Math.max(1, Math.min(max, Math.floor(value)));
|
|
1147
|
+
}
|
|
1148
|
+
function escapeRegex(value) {
|
|
1149
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1150
|
+
}
|
|
1151
|
+
function wildcardToRegex(pattern) {
|
|
1152
|
+
const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
1153
|
+
return new RegExp(`^${escaped}$`);
|
|
1154
|
+
}
|
|
1155
|
+
function normalizeFindPattern(pattern) {
|
|
1156
|
+
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
1157
|
+
}
|
|
1158
|
+
function toVirtualPath(branchPath, absolutePath) {
|
|
1159
|
+
const relative = import_node_path.default.relative(branchPath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
|
|
1160
|
+
return relative ? `/${relative}` : "/";
|
|
1161
|
+
}
|
|
1162
|
+
function walkProjectEntries(branchPath, startPath) {
|
|
1163
|
+
const entries = [];
|
|
1164
|
+
const visit = (absolutePath) => {
|
|
1165
|
+
const stat = import_node_fs.default.statSync(absolutePath);
|
|
1166
|
+
const virtualPath = toVirtualPath(branchPath, absolutePath);
|
|
1167
|
+
entries.push({ absolutePath, virtualPath, stat });
|
|
1168
|
+
if (!stat.isDirectory()) return;
|
|
1169
|
+
const children = import_node_fs.default.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1170
|
+
for (const child of children) {
|
|
1171
|
+
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
1172
|
+
visit(import_node_path.default.join(absolutePath, child.name));
|
|
1090
1173
|
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1174
|
+
};
|
|
1175
|
+
visit(startPath);
|
|
1176
|
+
return entries;
|
|
1093
1177
|
}
|
|
1094
|
-
function
|
|
1095
|
-
|
|
1096
|
-
let patchPath = decodePatchPath(withoutMetadata);
|
|
1097
|
-
if (patchPath === "/dev/null") {
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1100
|
-
if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
|
|
1101
|
-
patchPath = patchPath.slice(2);
|
|
1102
|
-
}
|
|
1103
|
-
rejectArtifactWritePath(patchPath);
|
|
1104
|
-
if (!patchPath || import_node_path.default.isAbsolute(patchPath)) {
|
|
1105
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
1106
|
-
}
|
|
1107
|
-
const normalized = import_node_path.default.posix.normalize(patchPath);
|
|
1108
|
-
if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
1109
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
1110
|
-
}
|
|
1111
|
-
return normalized;
|
|
1178
|
+
function isProbablyText(bytes) {
|
|
1179
|
+
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
1112
1180
|
}
|
|
1113
|
-
function
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1181
|
+
async function executeGrepOperation(input) {
|
|
1182
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1183
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1184
|
+
if (!import_node_fs.default.existsSync(start.absolutePath)) {
|
|
1185
|
+
throw new Error(`Path not found: ${start.virtualPath}`);
|
|
1186
|
+
}
|
|
1187
|
+
const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
|
|
1188
|
+
const flags = input.message.caseSensitive === false ? "i" : "";
|
|
1189
|
+
const regex = new RegExp(input.message.pattern, flags);
|
|
1190
|
+
const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
|
|
1191
|
+
const lines = [];
|
|
1192
|
+
let matchCount = 0;
|
|
1193
|
+
let truncated = false;
|
|
1194
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1195
|
+
if (!entry.stat.isFile()) continue;
|
|
1196
|
+
const repoRelative = entry.virtualPath.slice(1);
|
|
1197
|
+
if (globRegex && !globRegex.test(repoRelative)) continue;
|
|
1198
|
+
const bytes = import_node_fs.default.readFileSync(entry.absolutePath);
|
|
1199
|
+
if (!isProbablyText(bytes)) continue;
|
|
1200
|
+
const fileLines = bytes.toString("utf8").split(/\r?\n/);
|
|
1201
|
+
for (let index = 0; index < fileLines.length; index += 1) {
|
|
1202
|
+
const line = fileLines[index] ?? "";
|
|
1203
|
+
regex.lastIndex = 0;
|
|
1204
|
+
const match = regex.exec(line);
|
|
1205
|
+
if (!match) continue;
|
|
1206
|
+
matchCount += 1;
|
|
1207
|
+
if (lines.length < limit) {
|
|
1208
|
+
const column = Math.max(1, (match.index ?? 0) + 1);
|
|
1209
|
+
lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
|
|
1210
|
+
} else {
|
|
1211
|
+
truncated = true;
|
|
1212
|
+
}
|
|
1140
1213
|
}
|
|
1141
|
-
index += 1;
|
|
1142
|
-
}
|
|
1143
|
-
throw new Error("Unterminated quoted path in patch header");
|
|
1144
|
-
}
|
|
1145
|
-
function parseDiffGitPaths(line) {
|
|
1146
|
-
if (!line.startsWith("diff --git ")) {
|
|
1147
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
1148
|
-
}
|
|
1149
|
-
const rest = line.slice("diff --git ".length);
|
|
1150
|
-
const first = readDiffPathToken(rest, 0);
|
|
1151
|
-
const second = readDiffPathToken(rest, first.nextIndex);
|
|
1152
|
-
if (rest.slice(second.nextIndex).trim()) {
|
|
1153
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
1154
1214
|
}
|
|
1215
|
+
const hint = truncated ? `
|
|
1216
|
+
... truncated. Narrow path/glob or increase limit to continue.` : "";
|
|
1155
1217
|
return {
|
|
1156
|
-
|
|
1157
|
-
|
|
1218
|
+
type: "grep",
|
|
1219
|
+
content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
|
|
1220
|
+
matchCount,
|
|
1221
|
+
truncated
|
|
1158
1222
|
};
|
|
1159
1223
|
}
|
|
1160
|
-
function
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
if (line.startsWith("new file mode ")) {
|
|
1187
|
-
current.operation = "add";
|
|
1188
|
-
continue;
|
|
1189
|
-
}
|
|
1190
|
-
if (line.startsWith("deleted file mode ")) {
|
|
1191
|
-
current.operation = "delete";
|
|
1192
|
-
if (current.oldPath) {
|
|
1193
|
-
current.path = current.oldPath;
|
|
1194
|
-
delete current.oldPath;
|
|
1195
|
-
}
|
|
1196
|
-
continue;
|
|
1197
|
-
}
|
|
1198
|
-
if (line.startsWith("rename from ")) {
|
|
1199
|
-
current.operation = "move";
|
|
1200
|
-
current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
if (line.startsWith("rename to ")) {
|
|
1204
|
-
current.operation = "move";
|
|
1205
|
-
current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
if (line.startsWith("@@")) {
|
|
1209
|
-
current.hunks += 1;
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1213
|
-
current.additions += 1;
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1217
|
-
current.deletions += 1;
|
|
1224
|
+
async function executeFindOperation(input) {
|
|
1225
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1226
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1227
|
+
if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
|
|
1228
|
+
throw new Error(`Directory not found: ${start.virtualPath}`);
|
|
1229
|
+
}
|
|
1230
|
+
const pattern = normalizeFindPattern(input.message.pattern ?? "*");
|
|
1231
|
+
const matcher = wildcardToRegex(pattern);
|
|
1232
|
+
const entryType = input.message.entryType ?? "file";
|
|
1233
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1234
|
+
const matches = [];
|
|
1235
|
+
let count = 0;
|
|
1236
|
+
let truncated = false;
|
|
1237
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1238
|
+
if (entry.virtualPath === start.virtualPath) continue;
|
|
1239
|
+
const isDirectory = entry.stat.isDirectory();
|
|
1240
|
+
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
1241
|
+
if (entryType === "directory" && !isDirectory) continue;
|
|
1242
|
+
const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
|
|
1243
|
+
if (!matcher.test(import_node_path.default.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
|
|
1244
|
+
count += 1;
|
|
1245
|
+
if (matches.length < limit) {
|
|
1246
|
+
matches.push(label);
|
|
1247
|
+
} else {
|
|
1248
|
+
truncated = true;
|
|
1218
1249
|
}
|
|
1219
1250
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1251
|
+
const hint = truncated ? "\n... truncated. Narrow path/pattern or increase limit to continue." : "";
|
|
1252
|
+
return {
|
|
1253
|
+
type: "find",
|
|
1254
|
+
content: matches.length > 0 ? `${matches.join("\n")}${hint}` : "No paths found.",
|
|
1255
|
+
count,
|
|
1256
|
+
truncated
|
|
1257
|
+
};
|
|
1224
1258
|
}
|
|
1225
|
-
async function
|
|
1259
|
+
async function executeLsOperation(input) {
|
|
1226
1260
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
import_node_fs.default.rmSync(patchPath, { force: true });
|
|
1237
|
-
}
|
|
1261
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1262
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
|
|
1263
|
+
throw new Error(`Directory not found: ${resolved.virtualPath}`);
|
|
1264
|
+
}
|
|
1265
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1266
|
+
const entries = import_node_fs.default.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
1267
|
+
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1268
|
+
const truncated = entries.length > displayed.length;
|
|
1269
|
+
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
1238
1270
|
return {
|
|
1239
|
-
type: "
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
deletions: files.reduce((sum, file) => sum + file.deletions, 0),
|
|
1245
|
-
hunks: files.reduce((sum, file) => sum + file.hunks, 0)
|
|
1246
|
-
}
|
|
1271
|
+
type: "ls",
|
|
1272
|
+
path: resolved.virtualPath,
|
|
1273
|
+
content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
|
|
1274
|
+
count: entries.length,
|
|
1275
|
+
truncated
|
|
1247
1276
|
};
|
|
1248
1277
|
}
|
|
1249
1278
|
async function executeViewFileBytesOperation(input) {
|
|
@@ -1263,7 +1292,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1263
1292
|
const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
|
|
1264
1293
|
const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
|
|
1265
1294
|
if (!mediaType2) {
|
|
1266
|
-
throw new Error("
|
|
1295
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1267
1296
|
}
|
|
1268
1297
|
const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
|
|
1269
1298
|
const dimensions2 = readImageDimensions(bytes2, mediaType2);
|
|
@@ -1285,7 +1314,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1285
1314
|
const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
|
|
1286
1315
|
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1287
1316
|
if (!mediaType) {
|
|
1288
|
-
throw new Error("
|
|
1317
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1289
1318
|
}
|
|
1290
1319
|
const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
|
|
1291
1320
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
@@ -1391,14 +1420,18 @@ function pullBranch(input) {
|
|
|
1391
1420
|
}
|
|
1392
1421
|
async function executeOperation(input) {
|
|
1393
1422
|
switch (input.message.type) {
|
|
1394
|
-
case "
|
|
1423
|
+
case "read":
|
|
1395
1424
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
1396
|
-
case "
|
|
1397
|
-
return
|
|
1398
|
-
case "
|
|
1425
|
+
case "write":
|
|
1426
|
+
return executeWriteFileOperation({ ...input, message: input.message });
|
|
1427
|
+
case "edit":
|
|
1399
1428
|
return executeEditFileOperation({ ...input, message: input.message });
|
|
1400
|
-
case "
|
|
1401
|
-
return
|
|
1429
|
+
case "grep":
|
|
1430
|
+
return executeGrepOperation({ ...input, message: input.message });
|
|
1431
|
+
case "find":
|
|
1432
|
+
return executeFindOperation({ ...input, message: input.message });
|
|
1433
|
+
case "ls":
|
|
1434
|
+
return executeLsOperation({ ...input, message: input.message });
|
|
1402
1435
|
case "view_file_bytes":
|
|
1403
1436
|
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
1404
1437
|
case "sync":
|
|
@@ -1958,7 +1991,7 @@ async function startWorker(options) {
|
|
|
1958
1991
|
const hello = {
|
|
1959
1992
|
type: "hello",
|
|
1960
1993
|
hostInfo: {
|
|
1961
|
-
hostname: (0,
|
|
1994
|
+
hostname: (0, import_node_os.hostname)(),
|
|
1962
1995
|
platform: process.platform,
|
|
1963
1996
|
arch: process.arch,
|
|
1964
1997
|
pid: process.pid,
|
|
@@ -2099,7 +2132,7 @@ async function startWorker(options) {
|
|
|
2099
2132
|
}
|
|
2100
2133
|
return;
|
|
2101
2134
|
}
|
|
2102
|
-
if (message.type === "
|
|
2135
|
+
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
|
|
2103
2136
|
try {
|
|
2104
2137
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2105
2138
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/main.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { createHash
|
|
6
|
-
import { hostname } from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import os, { hostname } from "node:os";
|
|
7
6
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
8
7
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
9
8
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
@@ -11,6 +10,7 @@ const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
|
11
10
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
12
11
|
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
13
12
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
13
|
+
const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
14
14
|
const MAX_LINE_LENGTH = 2e3;
|
|
15
15
|
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
16
16
|
const R5D_REMOTE_NAME = "r5d";
|
|
@@ -885,16 +885,36 @@ function formatLineNumberedContent(content, offset, limit) {
|
|
|
885
885
|
const endLine = Math.min(startLine + readLimit - 1, totalLines);
|
|
886
886
|
const selectedLines = allLines.slice(startLine - 1, endLine);
|
|
887
887
|
const padWidth = String(endLine).length;
|
|
888
|
-
const formattedLines =
|
|
888
|
+
const formattedLines = [];
|
|
889
|
+
let actualEndLine = endLine;
|
|
890
|
+
let budgetTruncated = false;
|
|
891
|
+
for (let index = 0; index < selectedLines.length; index += 1) {
|
|
892
|
+
const line = selectedLines[index] ?? "";
|
|
889
893
|
const lineNumber = startLine + index;
|
|
890
894
|
const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
|
|
891
|
-
|
|
892
|
-
|
|
895
|
+
const formattedLine = `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
|
|
896
|
+
const candidateLength = formattedLines.length === 0 ? formattedLine.length : formattedLine.length + 1;
|
|
897
|
+
const currentLength = formattedLines.join("\n").length;
|
|
898
|
+
if (currentLength + candidateLength > DEFAULT_READ_MAX_BYTES) {
|
|
899
|
+
budgetTruncated = true;
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
formattedLines.push(formattedLine);
|
|
903
|
+
actualEndLine = lineNumber;
|
|
904
|
+
}
|
|
905
|
+
let formattedContent = formattedLines.join("\n");
|
|
906
|
+
if (budgetTruncated) {
|
|
907
|
+
formattedContent = `${formattedContent}
|
|
908
|
+
... (truncated by byte budget)`;
|
|
909
|
+
}
|
|
910
|
+
const truncated = budgetTruncated || actualEndLine < totalLines;
|
|
893
911
|
return {
|
|
894
|
-
content:
|
|
912
|
+
content: formattedContent,
|
|
895
913
|
totalLines,
|
|
896
914
|
startLine,
|
|
897
|
-
endLine
|
|
915
|
+
endLine: actualEndLine,
|
|
916
|
+
truncated,
|
|
917
|
+
continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
|
|
898
918
|
};
|
|
899
919
|
}
|
|
900
920
|
function readPngDimensions(bytes) {
|
|
@@ -961,6 +981,27 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
961
981
|
}
|
|
962
982
|
return dimensions;
|
|
963
983
|
}
|
|
984
|
+
const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
985
|
+
async function withFileMutationQueue(key, operation) {
|
|
986
|
+
const previous = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
987
|
+
let release;
|
|
988
|
+
const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
|
|
989
|
+
release = resolve;
|
|
990
|
+
}));
|
|
991
|
+
fileMutationQueues.set(key, current);
|
|
992
|
+
await previous.catch(() => void 0);
|
|
993
|
+
try {
|
|
994
|
+
return await operation();
|
|
995
|
+
} finally {
|
|
996
|
+
release();
|
|
997
|
+
if (fileMutationQueues.get(key) === current) {
|
|
998
|
+
fileMutationQueues.delete(key);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function mutationQueueKey(projectId, branchName, filePath) {
|
|
1003
|
+
return `${projectId}:${branchName}:${path.posix.normalize(filePath)}`;
|
|
1004
|
+
}
|
|
964
1005
|
async function executeReadFileOperation(input) {
|
|
965
1006
|
const artifact = await resolveArtifactEnvFilePath({
|
|
966
1007
|
filePath: input.message.filePath,
|
|
@@ -978,7 +1019,8 @@ async function executeReadFileOperation(input) {
|
|
|
978
1019
|
const buffer2 = fs.readFileSync(artifact.absolutePath);
|
|
979
1020
|
const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
|
|
980
1021
|
return {
|
|
981
|
-
type: "
|
|
1022
|
+
type: "read",
|
|
1023
|
+
kind: "text",
|
|
982
1024
|
file: artifact.virtualPath,
|
|
983
1025
|
gitBlobHash: getGitBlobHashForContent(buffer2),
|
|
984
1026
|
...formatted2
|
|
@@ -992,222 +1034,209 @@ async function executeReadFileOperation(input) {
|
|
|
992
1034
|
const buffer = fs.readFileSync(resolved.absolutePath);
|
|
993
1035
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
994
1036
|
return {
|
|
995
|
-
type: "
|
|
1037
|
+
type: "read",
|
|
1038
|
+
kind: "text",
|
|
996
1039
|
file: resolved.virtualPath,
|
|
997
1040
|
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
998
1041
|
...formatted
|
|
999
1042
|
};
|
|
1000
1043
|
}
|
|
1001
|
-
async function
|
|
1044
|
+
async function executeWriteFileOperation(input) {
|
|
1002
1045
|
rejectArtifactWritePath(input.message.filePath);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
};
|
|
1046
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1047
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1048
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1049
|
+
fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
|
|
1050
|
+
fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
|
|
1051
|
+
return {
|
|
1052
|
+
type: "write",
|
|
1053
|
+
file: resolved.virtualPath,
|
|
1054
|
+
gitBlobHash: getGitBlobHashForContent(input.message.content)
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
1015
1057
|
}
|
|
1016
1058
|
async function executeEditFileOperation(input) {
|
|
1017
1059
|
rejectArtifactWritePath(input.message.filePath);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1060
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1061
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1062
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1063
|
+
if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
|
|
1064
|
+
throw new Error("edit requires at least one replacement");
|
|
1065
|
+
}
|
|
1066
|
+
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1067
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
1068
|
+
}
|
|
1069
|
+
const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
|
|
1070
|
+
const ranges = input.message.edits.map((edit, index) => {
|
|
1071
|
+
if (edit.oldText === edit.newText) {
|
|
1072
|
+
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
1073
|
+
}
|
|
1074
|
+
if (edit.oldText.length === 0) {
|
|
1075
|
+
throw new Error(`edits[${index}].oldText cannot be empty`);
|
|
1076
|
+
}
|
|
1077
|
+
const occurrences = originalContent.split(edit.oldText).length - 1;
|
|
1078
|
+
if (occurrences === 0) {
|
|
1079
|
+
throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
|
|
1080
|
+
}
|
|
1081
|
+
if (occurrences > 1) {
|
|
1082
|
+
throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
|
|
1083
|
+
}
|
|
1084
|
+
const start = originalContent.indexOf(edit.oldText);
|
|
1085
|
+
return { index, start, end: start + edit.oldText.length, edit };
|
|
1086
|
+
});
|
|
1087
|
+
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
|
|
1088
|
+
for (let index = 1; index < sortedRanges.length; index += 1) {
|
|
1089
|
+
const previous = sortedRanges[index - 1];
|
|
1090
|
+
const current = sortedRanges[index];
|
|
1091
|
+
if (current.start < previous.end) {
|
|
1092
|
+
throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
|
|
1093
|
+
}
|
|
1038
1094
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1095
|
+
let nextContent = originalContent;
|
|
1096
|
+
for (const range of [...sortedRanges].reverse()) {
|
|
1097
|
+
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
1098
|
+
}
|
|
1099
|
+
fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
|
|
1100
|
+
return {
|
|
1101
|
+
type: "edit",
|
|
1102
|
+
file: resolved.virtualPath,
|
|
1103
|
+
edits: input.message.edits.length
|
|
1104
|
+
};
|
|
1105
|
+
});
|
|
1046
1106
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1107
|
+
const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
|
|
1108
|
+
function normalizeToolLimit(value, fallback, max) {
|
|
1109
|
+
if (!Number.isFinite(value) || value === void 0) return fallback;
|
|
1110
|
+
return Math.max(1, Math.min(max, Math.floor(value)));
|
|
1111
|
+
}
|
|
1112
|
+
function escapeRegex(value) {
|
|
1113
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1114
|
+
}
|
|
1115
|
+
function wildcardToRegex(pattern) {
|
|
1116
|
+
const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
1117
|
+
return new RegExp(`^${escaped}$`);
|
|
1118
|
+
}
|
|
1119
|
+
function normalizeFindPattern(pattern) {
|
|
1120
|
+
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
1121
|
+
}
|
|
1122
|
+
function toVirtualPath(branchPath, absolutePath) {
|
|
1123
|
+
const relative = path.relative(branchPath, absolutePath).split(path.sep).join(path.posix.sep);
|
|
1124
|
+
return relative ? `/${relative}` : "/";
|
|
1125
|
+
}
|
|
1126
|
+
function walkProjectEntries(branchPath, startPath) {
|
|
1127
|
+
const entries = [];
|
|
1128
|
+
const visit = (absolutePath) => {
|
|
1129
|
+
const stat = fs.statSync(absolutePath);
|
|
1130
|
+
const virtualPath = toVirtualPath(branchPath, absolutePath);
|
|
1131
|
+
entries.push({ absolutePath, virtualPath, stat });
|
|
1132
|
+
if (!stat.isDirectory()) return;
|
|
1133
|
+
const children = fs.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1134
|
+
for (const child of children) {
|
|
1135
|
+
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
1136
|
+
visit(path.join(absolutePath, child.name));
|
|
1054
1137
|
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1138
|
+
};
|
|
1139
|
+
visit(startPath);
|
|
1140
|
+
return entries;
|
|
1057
1141
|
}
|
|
1058
|
-
function
|
|
1059
|
-
|
|
1060
|
-
let patchPath = decodePatchPath(withoutMetadata);
|
|
1061
|
-
if (patchPath === "/dev/null") {
|
|
1062
|
-
return null;
|
|
1063
|
-
}
|
|
1064
|
-
if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
|
|
1065
|
-
patchPath = patchPath.slice(2);
|
|
1066
|
-
}
|
|
1067
|
-
rejectArtifactWritePath(patchPath);
|
|
1068
|
-
if (!patchPath || path.isAbsolute(patchPath)) {
|
|
1069
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
1070
|
-
}
|
|
1071
|
-
const normalized = path.posix.normalize(patchPath);
|
|
1072
|
-
if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
1073
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
1074
|
-
}
|
|
1075
|
-
return normalized;
|
|
1142
|
+
function isProbablyText(bytes) {
|
|
1143
|
+
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
1076
1144
|
}
|
|
1077
|
-
function
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1145
|
+
async function executeGrepOperation(input) {
|
|
1146
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1147
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1148
|
+
if (!fs.existsSync(start.absolutePath)) {
|
|
1149
|
+
throw new Error(`Path not found: ${start.virtualPath}`);
|
|
1150
|
+
}
|
|
1151
|
+
const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
|
|
1152
|
+
const flags = input.message.caseSensitive === false ? "i" : "";
|
|
1153
|
+
const regex = new RegExp(input.message.pattern, flags);
|
|
1154
|
+
const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
|
|
1155
|
+
const lines = [];
|
|
1156
|
+
let matchCount = 0;
|
|
1157
|
+
let truncated = false;
|
|
1158
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1159
|
+
if (!entry.stat.isFile()) continue;
|
|
1160
|
+
const repoRelative = entry.virtualPath.slice(1);
|
|
1161
|
+
if (globRegex && !globRegex.test(repoRelative)) continue;
|
|
1162
|
+
const bytes = fs.readFileSync(entry.absolutePath);
|
|
1163
|
+
if (!isProbablyText(bytes)) continue;
|
|
1164
|
+
const fileLines = bytes.toString("utf8").split(/\r?\n/);
|
|
1165
|
+
for (let index = 0; index < fileLines.length; index += 1) {
|
|
1166
|
+
const line = fileLines[index] ?? "";
|
|
1167
|
+
regex.lastIndex = 0;
|
|
1168
|
+
const match = regex.exec(line);
|
|
1169
|
+
if (!match) continue;
|
|
1170
|
+
matchCount += 1;
|
|
1171
|
+
if (lines.length < limit) {
|
|
1172
|
+
const column = Math.max(1, (match.index ?? 0) + 1);
|
|
1173
|
+
lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
|
|
1174
|
+
} else {
|
|
1175
|
+
truncated = true;
|
|
1176
|
+
}
|
|
1104
1177
|
}
|
|
1105
|
-
index += 1;
|
|
1106
|
-
}
|
|
1107
|
-
throw new Error("Unterminated quoted path in patch header");
|
|
1108
|
-
}
|
|
1109
|
-
function parseDiffGitPaths(line) {
|
|
1110
|
-
if (!line.startsWith("diff --git ")) {
|
|
1111
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
1112
|
-
}
|
|
1113
|
-
const rest = line.slice("diff --git ".length);
|
|
1114
|
-
const first = readDiffPathToken(rest, 0);
|
|
1115
|
-
const second = readDiffPathToken(rest, first.nextIndex);
|
|
1116
|
-
if (rest.slice(second.nextIndex).trim()) {
|
|
1117
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
1118
1178
|
}
|
|
1179
|
+
const hint = truncated ? `
|
|
1180
|
+
... truncated. Narrow path/glob or increase limit to continue.` : "";
|
|
1119
1181
|
return {
|
|
1120
|
-
|
|
1121
|
-
|
|
1182
|
+
type: "grep",
|
|
1183
|
+
content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
|
|
1184
|
+
matchCount,
|
|
1185
|
+
truncated
|
|
1122
1186
|
};
|
|
1123
1187
|
}
|
|
1124
|
-
function
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
}
|
|
1150
|
-
if (line.startsWith("new file mode ")) {
|
|
1151
|
-
current.operation = "add";
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
if (line.startsWith("deleted file mode ")) {
|
|
1155
|
-
current.operation = "delete";
|
|
1156
|
-
if (current.oldPath) {
|
|
1157
|
-
current.path = current.oldPath;
|
|
1158
|
-
delete current.oldPath;
|
|
1159
|
-
}
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
if (line.startsWith("rename from ")) {
|
|
1163
|
-
current.operation = "move";
|
|
1164
|
-
current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
if (line.startsWith("rename to ")) {
|
|
1168
|
-
current.operation = "move";
|
|
1169
|
-
current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
|
|
1170
|
-
continue;
|
|
1171
|
-
}
|
|
1172
|
-
if (line.startsWith("@@")) {
|
|
1173
|
-
current.hunks += 1;
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1177
|
-
current.additions += 1;
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1181
|
-
current.deletions += 1;
|
|
1188
|
+
async function executeFindOperation(input) {
|
|
1189
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1190
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1191
|
+
if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
|
|
1192
|
+
throw new Error(`Directory not found: ${start.virtualPath}`);
|
|
1193
|
+
}
|
|
1194
|
+
const pattern = normalizeFindPattern(input.message.pattern ?? "*");
|
|
1195
|
+
const matcher = wildcardToRegex(pattern);
|
|
1196
|
+
const entryType = input.message.entryType ?? "file";
|
|
1197
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1198
|
+
const matches = [];
|
|
1199
|
+
let count = 0;
|
|
1200
|
+
let truncated = false;
|
|
1201
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1202
|
+
if (entry.virtualPath === start.virtualPath) continue;
|
|
1203
|
+
const isDirectory = entry.stat.isDirectory();
|
|
1204
|
+
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
1205
|
+
if (entryType === "directory" && !isDirectory) continue;
|
|
1206
|
+
const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
|
|
1207
|
+
if (!matcher.test(path.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
|
|
1208
|
+
count += 1;
|
|
1209
|
+
if (matches.length < limit) {
|
|
1210
|
+
matches.push(label);
|
|
1211
|
+
} else {
|
|
1212
|
+
truncated = true;
|
|
1182
1213
|
}
|
|
1183
1214
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1215
|
+
const hint = truncated ? "\n... truncated. Narrow path/pattern or increase limit to continue." : "";
|
|
1216
|
+
return {
|
|
1217
|
+
type: "find",
|
|
1218
|
+
content: matches.length > 0 ? `${matches.join("\n")}${hint}` : "No paths found.",
|
|
1219
|
+
count,
|
|
1220
|
+
truncated
|
|
1221
|
+
};
|
|
1188
1222
|
}
|
|
1189
|
-
async function
|
|
1223
|
+
async function executeLsOperation(input) {
|
|
1190
1224
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
fs.rmSync(patchPath, { force: true });
|
|
1201
|
-
}
|
|
1225
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1226
|
+
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
|
|
1227
|
+
throw new Error(`Directory not found: ${resolved.virtualPath}`);
|
|
1228
|
+
}
|
|
1229
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1230
|
+
const entries = fs.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
1231
|
+
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1232
|
+
const truncated = entries.length > displayed.length;
|
|
1233
|
+
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
1202
1234
|
return {
|
|
1203
|
-
type: "
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
deletions: files.reduce((sum, file) => sum + file.deletions, 0),
|
|
1209
|
-
hunks: files.reduce((sum, file) => sum + file.hunks, 0)
|
|
1210
|
-
}
|
|
1235
|
+
type: "ls",
|
|
1236
|
+
path: resolved.virtualPath,
|
|
1237
|
+
content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
|
|
1238
|
+
count: entries.length,
|
|
1239
|
+
truncated
|
|
1211
1240
|
};
|
|
1212
1241
|
}
|
|
1213
1242
|
async function executeViewFileBytesOperation(input) {
|
|
@@ -1227,7 +1256,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1227
1256
|
const extension2 = path.extname(artifact.absolutePath).toLowerCase();
|
|
1228
1257
|
const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
|
|
1229
1258
|
if (!mediaType2) {
|
|
1230
|
-
throw new Error("
|
|
1259
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1231
1260
|
}
|
|
1232
1261
|
const bytes2 = fs.readFileSync(artifact.absolutePath);
|
|
1233
1262
|
const dimensions2 = readImageDimensions(bytes2, mediaType2);
|
|
@@ -1249,7 +1278,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1249
1278
|
const extension = path.extname(resolved.absolutePath).toLowerCase();
|
|
1250
1279
|
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1251
1280
|
if (!mediaType) {
|
|
1252
|
-
throw new Error("
|
|
1281
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1253
1282
|
}
|
|
1254
1283
|
const bytes = fs.readFileSync(resolved.absolutePath);
|
|
1255
1284
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
@@ -1355,14 +1384,18 @@ function pullBranch(input) {
|
|
|
1355
1384
|
}
|
|
1356
1385
|
async function executeOperation(input) {
|
|
1357
1386
|
switch (input.message.type) {
|
|
1358
|
-
case "
|
|
1387
|
+
case "read":
|
|
1359
1388
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
1360
|
-
case "
|
|
1361
|
-
return
|
|
1362
|
-
case "
|
|
1389
|
+
case "write":
|
|
1390
|
+
return executeWriteFileOperation({ ...input, message: input.message });
|
|
1391
|
+
case "edit":
|
|
1363
1392
|
return executeEditFileOperation({ ...input, message: input.message });
|
|
1364
|
-
case "
|
|
1365
|
-
return
|
|
1393
|
+
case "grep":
|
|
1394
|
+
return executeGrepOperation({ ...input, message: input.message });
|
|
1395
|
+
case "find":
|
|
1396
|
+
return executeFindOperation({ ...input, message: input.message });
|
|
1397
|
+
case "ls":
|
|
1398
|
+
return executeLsOperation({ ...input, message: input.message });
|
|
1366
1399
|
case "view_file_bytes":
|
|
1367
1400
|
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
1368
1401
|
case "sync":
|
|
@@ -2063,7 +2096,7 @@ async function startWorker(options) {
|
|
|
2063
2096
|
}
|
|
2064
2097
|
return;
|
|
2065
2098
|
}
|
|
2066
|
-
if (message.type === "
|
|
2099
|
+
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
|
|
2067
2100
|
try {
|
|
2068
2101
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2069
2102
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
package/dist/mjs/package.json
CHANGED