@ricsam/r5d-worker 0.0.11 → 0.0.13

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 CHANGED
@@ -31,14 +31,14 @@ var main_exports = {};
31
31
  __export(main_exports, {
32
32
  ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
33
  isArtifactEnvPath: () => isArtifactEnvPath,
34
+ prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
34
35
  syncSessionArtifacts: () => syncSessionArtifacts
35
36
  });
36
37
  module.exports = __toCommonJS(main_exports);
37
38
  var import_node_fs = __toESM(require("node:fs"), 1);
38
- var import_node_os = __toESM(require("node:os"), 1);
39
39
  var import_node_path = __toESM(require("node:path"), 1);
40
40
  var import_node_crypto = require("node:crypto");
41
- var import_node_os2 = require("node:os");
41
+ var import_node_os = __toESM(require("node:os"), 1);
42
42
  var import_node_child_process = require("node:child_process");
43
43
  var import_git_identity = require("./git-identity.cjs");
44
44
  const import_meta = {};
@@ -47,6 +47,7 @@ const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
47
47
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
48
48
  const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
49
49
  const DEFAULT_READ_LIMIT = 2e3;
50
+ const DEFAULT_READ_MAX_BYTES = 5e4;
50
51
  const MAX_LINE_LENGTH = 2e3;
51
52
  const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
52
53
  const R5D_REMOTE_NAME = "r5d";
@@ -410,6 +411,29 @@ function rejectArtifactWritePath(filePath) {
410
411
  throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
411
412
  }
412
413
  }
414
+ async function prepareArtifactEnvForShell(input) {
415
+ if (!input.sessionId) {
416
+ return {};
417
+ }
418
+ try {
419
+ await syncSessionArtifacts({
420
+ baseUrl: input.baseUrl,
421
+ token: input.token,
422
+ projectId: input.projectId,
423
+ branchName: input.branchName,
424
+ sessionId: input.sessionId,
425
+ artifactRoot: input.artifactRoot
426
+ });
427
+ } catch (error) {
428
+ process.stderr.write(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
429
+ `);
430
+ }
431
+ const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
432
+ import_node_fs.default.mkdirSync(artifactsDir, { recursive: true });
433
+ return {
434
+ R5D_ARTIFACTS_DIR: artifactsDir
435
+ };
436
+ }
413
437
  async function verifyR5dctlAuth(baseUrl, token) {
414
438
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
415
439
  let response;
@@ -921,16 +945,36 @@ function formatLineNumberedContent(content, offset, limit) {
921
945
  const endLine = Math.min(startLine + readLimit - 1, totalLines);
922
946
  const selectedLines = allLines.slice(startLine - 1, endLine);
923
947
  const padWidth = String(endLine).length;
924
- const formattedLines = selectedLines.map((line, index) => {
948
+ const formattedLines = [];
949
+ let actualEndLine = endLine;
950
+ let budgetTruncated = false;
951
+ for (let index = 0; index < selectedLines.length; index += 1) {
952
+ const line = selectedLines[index] ?? "";
925
953
  const lineNumber = startLine + index;
926
954
  const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
927
- return `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
928
- });
955
+ const formattedLine = `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
956
+ const candidateLength = formattedLines.length === 0 ? formattedLine.length : formattedLine.length + 1;
957
+ const currentLength = formattedLines.join("\n").length;
958
+ if (currentLength + candidateLength > DEFAULT_READ_MAX_BYTES) {
959
+ budgetTruncated = true;
960
+ break;
961
+ }
962
+ formattedLines.push(formattedLine);
963
+ actualEndLine = lineNumber;
964
+ }
965
+ let formattedContent = formattedLines.join("\n");
966
+ if (budgetTruncated) {
967
+ formattedContent = `${formattedContent}
968
+ ... (truncated by byte budget)`;
969
+ }
970
+ const truncated = budgetTruncated || actualEndLine < totalLines;
929
971
  return {
930
- content: formattedLines.join("\n"),
972
+ content: formattedContent,
931
973
  totalLines,
932
974
  startLine,
933
- endLine
975
+ endLine: actualEndLine,
976
+ truncated,
977
+ continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
934
978
  };
935
979
  }
936
980
  function readPngDimensions(bytes) {
@@ -997,6 +1041,27 @@ function readImageDimensions(bytes, mediaType) {
997
1041
  }
998
1042
  return dimensions;
999
1043
  }
1044
+ const fileMutationQueues = /* @__PURE__ */ new Map();
1045
+ async function withFileMutationQueue(key, operation) {
1046
+ const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1047
+ let release;
1048
+ const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1049
+ release = resolve;
1050
+ }));
1051
+ fileMutationQueues.set(key, current);
1052
+ await previous.catch(() => void 0);
1053
+ try {
1054
+ return await operation();
1055
+ } finally {
1056
+ release();
1057
+ if (fileMutationQueues.get(key) === current) {
1058
+ fileMutationQueues.delete(key);
1059
+ }
1060
+ }
1061
+ }
1062
+ function mutationQueueKey(projectId, branchName, filePath) {
1063
+ return `${projectId}:${branchName}:${import_node_path.default.posix.normalize(filePath)}`;
1064
+ }
1000
1065
  async function executeReadFileOperation(input) {
1001
1066
  const artifact = await resolveArtifactEnvFilePath({
1002
1067
  filePath: input.message.filePath,
@@ -1014,7 +1079,8 @@ async function executeReadFileOperation(input) {
1014
1079
  const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1015
1080
  const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
1016
1081
  return {
1017
- type: "read_file",
1082
+ type: "read",
1083
+ kind: "text",
1018
1084
  file: artifact.virtualPath,
1019
1085
  gitBlobHash: getGitBlobHashForContent(buffer2),
1020
1086
  ...formatted2
@@ -1028,222 +1094,209 @@ async function executeReadFileOperation(input) {
1028
1094
  const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
1029
1095
  const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1030
1096
  return {
1031
- type: "read_file",
1097
+ type: "read",
1098
+ kind: "text",
1032
1099
  file: resolved.virtualPath,
1033
1100
  gitBlobHash: getGitBlobHashForContent(buffer),
1034
1101
  ...formatted
1035
1102
  };
1036
1103
  }
1037
- async function executeCreateFileOperation(input) {
1104
+ async function executeWriteFileOperation(input) {
1038
1105
  rejectArtifactWritePath(input.message.filePath);
1039
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1040
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1041
- if (import_node_fs.default.existsSync(resolved.absolutePath)) {
1042
- throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
1043
- }
1044
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1045
- import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1046
- return {
1047
- type: "create_file",
1048
- file: resolved.virtualPath,
1049
- gitBlobHash: getGitBlobHashForContent(input.message.content)
1050
- };
1106
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1107
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1108
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1109
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1110
+ import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1111
+ return {
1112
+ type: "write",
1113
+ file: resolved.virtualPath,
1114
+ gitBlobHash: getGitBlobHashForContent(input.message.content)
1115
+ };
1116
+ });
1051
1117
  }
1052
1118
  async function executeEditFileOperation(input) {
1053
1119
  rejectArtifactWritePath(input.message.filePath);
1054
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1055
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1056
- if (input.message.oldString === input.message.newString) {
1057
- throw new Error("old_string and new_string must be different");
1058
- }
1059
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1060
- throw new Error(`File not found: ${resolved.virtualPath}`);
1061
- }
1062
- const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
1063
- if (!content.includes(input.message.oldString)) {
1064
- throw new Error(
1065
- `Could not find old_string: "${input.message.oldString.slice(0, 100)}${input.message.oldString.length > 100 ? "..." : ""}"`
1066
- );
1067
- }
1068
- if (!input.message.replaceAll) {
1069
- const occurrences = content.split(input.message.oldString).length - 1;
1070
- if (occurrences > 1) {
1071
- throw new Error(
1072
- `old_string is not unique in the file (found ${occurrences} occurrences). Provide more context or use replace_all: true.`
1073
- );
1120
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1121
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1122
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1123
+ if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1124
+ throw new Error("edit requires at least one replacement");
1125
+ }
1126
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1127
+ throw new Error(`File not found: ${resolved.virtualPath}`);
1128
+ }
1129
+ const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
1130
+ const ranges = input.message.edits.map((edit, index) => {
1131
+ if (edit.oldText === edit.newText) {
1132
+ throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
1133
+ }
1134
+ if (edit.oldText.length === 0) {
1135
+ throw new Error(`edits[${index}].oldText cannot be empty`);
1136
+ }
1137
+ const occurrences = originalContent.split(edit.oldText).length - 1;
1138
+ if (occurrences === 0) {
1139
+ throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1140
+ }
1141
+ if (occurrences > 1) {
1142
+ throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1143
+ }
1144
+ const start = originalContent.indexOf(edit.oldText);
1145
+ return { index, start, end: start + edit.oldText.length, edit };
1146
+ });
1147
+ const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
1148
+ for (let index = 1; index < sortedRanges.length; index += 1) {
1149
+ const previous = sortedRanges[index - 1];
1150
+ const current = sortedRanges[index];
1151
+ if (current.start < previous.end) {
1152
+ throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
1153
+ }
1074
1154
  }
1075
- }
1076
- const nextContent = input.message.replaceAll ? content.split(input.message.oldString).join(input.message.newString) : content.replace(input.message.oldString, input.message.newString);
1077
- import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1078
- return {
1079
- type: "edit_file",
1080
- file: resolved.virtualPath
1081
- };
1155
+ let nextContent = originalContent;
1156
+ for (const range of [...sortedRanges].reverse()) {
1157
+ nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
1158
+ }
1159
+ import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1160
+ return {
1161
+ type: "edit",
1162
+ file: resolved.virtualPath,
1163
+ edits: input.message.edits.length
1164
+ };
1165
+ });
1082
1166
  }
1083
- function decodePatchPath(rawPath) {
1084
- const trimmed = rawPath.trim();
1085
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
1086
- try {
1087
- return JSON.parse(trimmed);
1088
- } catch {
1089
- return trimmed.slice(1, -1);
1167
+ const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
1168
+ function normalizeToolLimit(value, fallback, max) {
1169
+ if (!Number.isFinite(value) || value === void 0) return fallback;
1170
+ return Math.max(1, Math.min(max, Math.floor(value)));
1171
+ }
1172
+ function escapeRegex(value) {
1173
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1174
+ }
1175
+ function wildcardToRegex(pattern) {
1176
+ const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
1177
+ return new RegExp(`^${escaped}$`);
1178
+ }
1179
+ function normalizeFindPattern(pattern) {
1180
+ return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
1181
+ }
1182
+ function toVirtualPath(branchPath, absolutePath) {
1183
+ const relative = import_node_path.default.relative(branchPath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1184
+ return relative ? `/${relative}` : "/";
1185
+ }
1186
+ function walkProjectEntries(branchPath, startPath) {
1187
+ const entries = [];
1188
+ const visit = (absolutePath) => {
1189
+ const stat = import_node_fs.default.statSync(absolutePath);
1190
+ const virtualPath = toVirtualPath(branchPath, absolutePath);
1191
+ entries.push({ absolutePath, virtualPath, stat });
1192
+ if (!stat.isDirectory()) return;
1193
+ const children = import_node_fs.default.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1194
+ for (const child of children) {
1195
+ if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
1196
+ visit(import_node_path.default.join(absolutePath, child.name));
1090
1197
  }
1091
- }
1092
- return trimmed;
1198
+ };
1199
+ visit(startPath);
1200
+ return entries;
1093
1201
  }
1094
- function normalizePatchPath(rawPath) {
1095
- const withoutMetadata = rawPath.split(" ")[0] ?? rawPath;
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;
1202
+ function isProbablyText(bytes) {
1203
+ return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1112
1204
  }
1113
- function readDiffPathToken(value, startIndex) {
1114
- let index = startIndex;
1115
- while (index < value.length && /\s/.test(value[index] ?? "")) {
1116
- index += 1;
1117
- }
1118
- if (index >= value.length) {
1119
- throw new Error("Missing path in patch header");
1120
- }
1121
- if (value[index] !== '"') {
1122
- const tokenStart2 = index;
1123
- while (index < value.length && !/\s/.test(value[index] ?? "")) {
1124
- index += 1;
1125
- }
1126
- return { token: value.slice(tokenStart2, index), nextIndex: index };
1127
- }
1128
- const tokenStart = index;
1129
- index += 1;
1130
- let escaped = false;
1131
- while (index < value.length) {
1132
- const character = value[index];
1133
- if (escaped) {
1134
- escaped = false;
1135
- } else if (character === "\\") {
1136
- escaped = true;
1137
- } else if (character === '"') {
1138
- index += 1;
1139
- return { token: value.slice(tokenStart, index), nextIndex: index };
1205
+ async function executeGrepOperation(input) {
1206
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1207
+ const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1208
+ if (!import_node_fs.default.existsSync(start.absolutePath)) {
1209
+ throw new Error(`Path not found: ${start.virtualPath}`);
1210
+ }
1211
+ const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
1212
+ const flags = input.message.caseSensitive === false ? "i" : "";
1213
+ const regex = new RegExp(input.message.pattern, flags);
1214
+ const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
1215
+ const lines = [];
1216
+ let matchCount = 0;
1217
+ let truncated = false;
1218
+ for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1219
+ if (!entry.stat.isFile()) continue;
1220
+ const repoRelative = entry.virtualPath.slice(1);
1221
+ if (globRegex && !globRegex.test(repoRelative)) continue;
1222
+ const bytes = import_node_fs.default.readFileSync(entry.absolutePath);
1223
+ if (!isProbablyText(bytes)) continue;
1224
+ const fileLines = bytes.toString("utf8").split(/\r?\n/);
1225
+ for (let index = 0; index < fileLines.length; index += 1) {
1226
+ const line = fileLines[index] ?? "";
1227
+ regex.lastIndex = 0;
1228
+ const match = regex.exec(line);
1229
+ if (!match) continue;
1230
+ matchCount += 1;
1231
+ if (lines.length < limit) {
1232
+ const column = Math.max(1, (match.index ?? 0) + 1);
1233
+ lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
1234
+ } else {
1235
+ truncated = true;
1236
+ }
1140
1237
  }
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
1238
  }
1239
+ const hint = truncated ? `
1240
+ ... truncated. Narrow path/glob or increase limit to continue.` : "";
1155
1241
  return {
1156
- oldPath: normalizePatchPath(first.token),
1157
- newPath: normalizePatchPath(second.token)
1242
+ type: "grep",
1243
+ content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
1244
+ matchCount,
1245
+ truncated
1158
1246
  };
1159
1247
  }
1160
- function summarizePatch(patch) {
1161
- if (!patch.trim()) {
1162
- throw new Error("Patch cannot be empty");
1163
- }
1164
- const files = [];
1165
- let current;
1166
- for (const line of patch.split(/\r?\n/)) {
1167
- if (line.startsWith("diff --git ")) {
1168
- const { oldPath, newPath } = parseDiffGitPaths(line);
1169
- if (!oldPath && !newPath) {
1170
- throw new Error(`Invalid patch header: ${line}`);
1171
- }
1172
- current = {
1173
- path: newPath ?? oldPath,
1174
- ...oldPath && newPath && oldPath !== newPath ? { oldPath } : {},
1175
- operation: oldPath && newPath && oldPath !== newPath ? "move" : "update",
1176
- additions: 0,
1177
- deletions: 0,
1178
- hunks: 0
1179
- };
1180
- files.push(current);
1181
- continue;
1182
- }
1183
- if (!current) {
1184
- continue;
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;
1248
+ async function executeFindOperation(input) {
1249
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1250
+ const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1251
+ if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
1252
+ throw new Error(`Directory not found: ${start.virtualPath}`);
1253
+ }
1254
+ const pattern = normalizeFindPattern(input.message.pattern ?? "*");
1255
+ const matcher = wildcardToRegex(pattern);
1256
+ const entryType = input.message.entryType ?? "file";
1257
+ const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1258
+ const matches = [];
1259
+ let count = 0;
1260
+ let truncated = false;
1261
+ for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1262
+ if (entry.virtualPath === start.virtualPath) continue;
1263
+ const isDirectory = entry.stat.isDirectory();
1264
+ if (entryType === "file" && !entry.stat.isFile()) continue;
1265
+ if (entryType === "directory" && !isDirectory) continue;
1266
+ const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
1267
+ if (!matcher.test(import_node_path.default.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
1268
+ count += 1;
1269
+ if (matches.length < limit) {
1270
+ matches.push(label);
1271
+ } else {
1272
+ truncated = true;
1218
1273
  }
1219
1274
  }
1220
- if (files.length === 0) {
1221
- throw new Error("Patch must contain at least one diff --git file header");
1222
- }
1223
- return files;
1275
+ const hint = truncated ? "\n... truncated. Narrow path/pattern or increase limit to continue." : "";
1276
+ return {
1277
+ type: "find",
1278
+ content: matches.length > 0 ? `${matches.join("\n")}${hint}` : "No paths found.",
1279
+ count,
1280
+ truncated
1281
+ };
1224
1282
  }
1225
- async function executeApplyPatchOperation(input) {
1283
+ async function executeLsOperation(input) {
1226
1284
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1227
- const files = summarizePatch(input.message.patch);
1228
- const patchPath = import_node_path.default.join(import_node_os.default.tmpdir(), `r5d-apply-patch-${(0, import_node_crypto.randomUUID)()}.patch`);
1229
- try {
1230
- import_node_fs.default.writeFileSync(patchPath, input.message.patch, "utf8");
1231
- runInternalGit(workspace.internalGit, ["apply", "--check", "--whitespace=nowarn", patchPath]);
1232
- runInternalGit(workspace.internalGit, ["apply", "--whitespace=nowarn", patchPath]);
1233
- } catch (error) {
1234
- throw new Error(`Patch did not apply: ${error instanceof Error ? error.message : String(error)}`);
1235
- } finally {
1236
- import_node_fs.default.rmSync(patchPath, { force: true });
1237
- }
1285
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1286
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1287
+ throw new Error(`Directory not found: ${resolved.virtualPath}`);
1288
+ }
1289
+ const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1290
+ 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));
1291
+ const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
1292
+ const truncated = entries.length > displayed.length;
1293
+ const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
1238
1294
  return {
1239
- type: "apply_patch",
1240
- files,
1241
- summary: {
1242
- filesChanged: files.length,
1243
- additions: files.reduce((sum, file) => sum + file.additions, 0),
1244
- deletions: files.reduce((sum, file) => sum + file.deletions, 0),
1245
- hunks: files.reduce((sum, file) => sum + file.hunks, 0)
1246
- }
1295
+ type: "ls",
1296
+ path: resolved.virtualPath,
1297
+ content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
1298
+ count: entries.length,
1299
+ truncated
1247
1300
  };
1248
1301
  }
1249
1302
  async function executeViewFileBytesOperation(input) {
@@ -1263,7 +1316,7 @@ async function executeViewFileBytesOperation(input) {
1263
1316
  const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1264
1317
  const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
1265
1318
  if (!mediaType2) {
1266
- throw new Error("view_image supports PNG and JPEG files only");
1319
+ throw new Error("read supports PNG and JPEG image inputs only");
1267
1320
  }
1268
1321
  const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1269
1322
  const dimensions2 = readImageDimensions(bytes2, mediaType2);
@@ -1285,7 +1338,7 @@ async function executeViewFileBytesOperation(input) {
1285
1338
  const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
1286
1339
  const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1287
1340
  if (!mediaType) {
1288
- throw new Error("view_image supports PNG and JPEG files only");
1341
+ throw new Error("read supports PNG and JPEG image inputs only");
1289
1342
  }
1290
1343
  const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
1291
1344
  const dimensions = readImageDimensions(bytes, mediaType);
@@ -1391,14 +1444,18 @@ function pullBranch(input) {
1391
1444
  }
1392
1445
  async function executeOperation(input) {
1393
1446
  switch (input.message.type) {
1394
- case "read_file":
1447
+ case "read":
1395
1448
  return executeReadFileOperation({ ...input, message: input.message });
1396
- case "create_file":
1397
- return executeCreateFileOperation({ ...input, message: input.message });
1398
- case "edit_file":
1449
+ case "write":
1450
+ return executeWriteFileOperation({ ...input, message: input.message });
1451
+ case "edit":
1399
1452
  return executeEditFileOperation({ ...input, message: input.message });
1400
- case "apply_patch":
1401
- return executeApplyPatchOperation({ ...input, message: input.message });
1453
+ case "grep":
1454
+ return executeGrepOperation({ ...input, message: input.message });
1455
+ case "find":
1456
+ return executeFindOperation({ ...input, message: input.message });
1457
+ case "ls":
1458
+ return executeLsOperation({ ...input, message: input.message });
1402
1459
  case "view_file_bytes":
1403
1460
  return executeViewFileBytesOperation({ ...input, message: input.message });
1404
1461
  case "sync":
@@ -1453,18 +1510,14 @@ async function executeCommand(input) {
1453
1510
  branchName: input.message.branchName,
1454
1511
  manifest: input.manifest
1455
1512
  });
1456
- let artifactProcessEnv = {};
1457
- if (input.message.sessionId) {
1458
- await syncSessionArtifacts({
1459
- baseUrl: input.baseUrl,
1460
- token: input.token,
1461
- projectId: input.projectId,
1462
- branchName: input.message.branchName,
1463
- sessionId: input.message.sessionId,
1464
- artifactRoot: input.artifactRoot
1465
- });
1466
- artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
1467
- }
1513
+ const artifactProcessEnv = await prepareArtifactEnvForShell({
1514
+ baseUrl: input.baseUrl,
1515
+ token: input.token,
1516
+ projectId: input.projectId,
1517
+ branchName: input.message.branchName,
1518
+ sessionId: input.message.sessionId,
1519
+ artifactRoot: input.artifactRoot
1520
+ });
1468
1521
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1469
1522
  const subprocess = Bun.spawn(input.message.argv, {
1470
1523
  cwd,
@@ -1538,7 +1591,7 @@ async function executeStreamingCommand(input) {
1538
1591
  branchName: input.message.branchName,
1539
1592
  manifest: input.manifest
1540
1593
  });
1541
- await syncSessionArtifacts({
1594
+ const artifactProcessEnv = await prepareArtifactEnvForShell({
1542
1595
  baseUrl: input.baseUrl,
1543
1596
  token: input.token,
1544
1597
  projectId: input.projectId,
@@ -1546,7 +1599,6 @@ async function executeStreamingCommand(input) {
1546
1599
  sessionId: input.message.sessionId,
1547
1600
  artifactRoot: input.artifactRoot
1548
1601
  });
1549
- const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
1550
1602
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1551
1603
  const subprocess = Bun.spawn(input.message.argv, {
1552
1604
  cwd,
@@ -1958,7 +2010,7 @@ async function startWorker(options) {
1958
2010
  const hello = {
1959
2011
  type: "hello",
1960
2012
  hostInfo: {
1961
- hostname: (0, import_node_os2.hostname)(),
2013
+ hostname: (0, import_node_os.hostname)(),
1962
2014
  platform: process.platform,
1963
2015
  arch: process.arch,
1964
2016
  pid: process.pid,
@@ -2099,7 +2151,7 @@ async function startWorker(options) {
2099
2151
  }
2100
2152
  return;
2101
2153
  }
2102
- if (message.type === "read_file" || message.type === "create_file" || message.type === "edit_file" || message.type === "apply_patch" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2154
+ 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
2155
  try {
2104
2156
  const manifest = manifestByProjectId.get(message.projectId);
2105
2157
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -2185,5 +2237,6 @@ if (isCliEntrypoint()) {
2185
2237
  0 && (module.exports = {
2186
2238
  ensureVisibleGitCheckout,
2187
2239
  isArtifactEnvPath,
2240
+ prepareArtifactEnvForShell,
2188
2241
  syncSessionArtifacts
2189
2242
  });