@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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "type": "commonjs"
5
5
  }
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, randomUUID } from "node:crypto";
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";
@@ -374,6 +374,29 @@ function rejectArtifactWritePath(filePath) {
374
374
  throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
375
375
  }
376
376
  }
377
+ async function prepareArtifactEnvForShell(input) {
378
+ if (!input.sessionId) {
379
+ return {};
380
+ }
381
+ try {
382
+ await syncSessionArtifacts({
383
+ baseUrl: input.baseUrl,
384
+ token: input.token,
385
+ projectId: input.projectId,
386
+ branchName: input.branchName,
387
+ sessionId: input.sessionId,
388
+ artifactRoot: input.artifactRoot
389
+ });
390
+ } catch (error) {
391
+ process.stderr.write(`[r5d-worker] artifact sync skipped for ${input.sessionId}: ${error instanceof Error ? error.message : String(error)}
392
+ `);
393
+ }
394
+ const artifactsDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
395
+ fs.mkdirSync(artifactsDir, { recursive: true });
396
+ return {
397
+ R5D_ARTIFACTS_DIR: artifactsDir
398
+ };
399
+ }
377
400
  async function verifyR5dctlAuth(baseUrl, token) {
378
401
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
379
402
  let response;
@@ -885,16 +908,36 @@ function formatLineNumberedContent(content, offset, limit) {
885
908
  const endLine = Math.min(startLine + readLimit - 1, totalLines);
886
909
  const selectedLines = allLines.slice(startLine - 1, endLine);
887
910
  const padWidth = String(endLine).length;
888
- const formattedLines = selectedLines.map((line, index) => {
911
+ const formattedLines = [];
912
+ let actualEndLine = endLine;
913
+ let budgetTruncated = false;
914
+ for (let index = 0; index < selectedLines.length; index += 1) {
915
+ const line = selectedLines[index] ?? "";
889
916
  const lineNumber = startLine + index;
890
917
  const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
891
- return `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
892
- });
918
+ const formattedLine = `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
919
+ const candidateLength = formattedLines.length === 0 ? formattedLine.length : formattedLine.length + 1;
920
+ const currentLength = formattedLines.join("\n").length;
921
+ if (currentLength + candidateLength > DEFAULT_READ_MAX_BYTES) {
922
+ budgetTruncated = true;
923
+ break;
924
+ }
925
+ formattedLines.push(formattedLine);
926
+ actualEndLine = lineNumber;
927
+ }
928
+ let formattedContent = formattedLines.join("\n");
929
+ if (budgetTruncated) {
930
+ formattedContent = `${formattedContent}
931
+ ... (truncated by byte budget)`;
932
+ }
933
+ const truncated = budgetTruncated || actualEndLine < totalLines;
893
934
  return {
894
- content: formattedLines.join("\n"),
935
+ content: formattedContent,
895
936
  totalLines,
896
937
  startLine,
897
- endLine
938
+ endLine: actualEndLine,
939
+ truncated,
940
+ continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
898
941
  };
899
942
  }
900
943
  function readPngDimensions(bytes) {
@@ -961,6 +1004,27 @@ function readImageDimensions(bytes, mediaType) {
961
1004
  }
962
1005
  return dimensions;
963
1006
  }
1007
+ const fileMutationQueues = /* @__PURE__ */ new Map();
1008
+ async function withFileMutationQueue(key, operation) {
1009
+ const previous = fileMutationQueues.get(key) ?? Promise.resolve();
1010
+ let release;
1011
+ const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
1012
+ release = resolve;
1013
+ }));
1014
+ fileMutationQueues.set(key, current);
1015
+ await previous.catch(() => void 0);
1016
+ try {
1017
+ return await operation();
1018
+ } finally {
1019
+ release();
1020
+ if (fileMutationQueues.get(key) === current) {
1021
+ fileMutationQueues.delete(key);
1022
+ }
1023
+ }
1024
+ }
1025
+ function mutationQueueKey(projectId, branchName, filePath) {
1026
+ return `${projectId}:${branchName}:${path.posix.normalize(filePath)}`;
1027
+ }
964
1028
  async function executeReadFileOperation(input) {
965
1029
  const artifact = await resolveArtifactEnvFilePath({
966
1030
  filePath: input.message.filePath,
@@ -978,7 +1042,8 @@ async function executeReadFileOperation(input) {
978
1042
  const buffer2 = fs.readFileSync(artifact.absolutePath);
979
1043
  const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
980
1044
  return {
981
- type: "read_file",
1045
+ type: "read",
1046
+ kind: "text",
982
1047
  file: artifact.virtualPath,
983
1048
  gitBlobHash: getGitBlobHashForContent(buffer2),
984
1049
  ...formatted2
@@ -992,222 +1057,209 @@ async function executeReadFileOperation(input) {
992
1057
  const buffer = fs.readFileSync(resolved.absolutePath);
993
1058
  const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
994
1059
  return {
995
- type: "read_file",
1060
+ type: "read",
1061
+ kind: "text",
996
1062
  file: resolved.virtualPath,
997
1063
  gitBlobHash: getGitBlobHashForContent(buffer),
998
1064
  ...formatted
999
1065
  };
1000
1066
  }
1001
- async function executeCreateFileOperation(input) {
1067
+ async function executeWriteFileOperation(input) {
1002
1068
  rejectArtifactWritePath(input.message.filePath);
1003
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1004
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1005
- if (fs.existsSync(resolved.absolutePath)) {
1006
- throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
1007
- }
1008
- fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1009
- fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1010
- return {
1011
- type: "create_file",
1012
- file: resolved.virtualPath,
1013
- gitBlobHash: getGitBlobHashForContent(input.message.content)
1014
- };
1069
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1070
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1071
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1072
+ fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1073
+ fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1074
+ return {
1075
+ type: "write",
1076
+ file: resolved.virtualPath,
1077
+ gitBlobHash: getGitBlobHashForContent(input.message.content)
1078
+ };
1079
+ });
1015
1080
  }
1016
1081
  async function executeEditFileOperation(input) {
1017
1082
  rejectArtifactWritePath(input.message.filePath);
1018
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1019
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1020
- if (input.message.oldString === input.message.newString) {
1021
- throw new Error("old_string and new_string must be different");
1022
- }
1023
- if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1024
- throw new Error(`File not found: ${resolved.virtualPath}`);
1025
- }
1026
- const content = fs.readFileSync(resolved.absolutePath, "utf8");
1027
- if (!content.includes(input.message.oldString)) {
1028
- throw new Error(
1029
- `Could not find old_string: "${input.message.oldString.slice(0, 100)}${input.message.oldString.length > 100 ? "..." : ""}"`
1030
- );
1031
- }
1032
- if (!input.message.replaceAll) {
1033
- const occurrences = content.split(input.message.oldString).length - 1;
1034
- if (occurrences > 1) {
1035
- throw new Error(
1036
- `old_string is not unique in the file (found ${occurrences} occurrences). Provide more context or use replace_all: true.`
1037
- );
1083
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1084
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1085
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1086
+ if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1087
+ throw new Error("edit requires at least one replacement");
1088
+ }
1089
+ if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1090
+ throw new Error(`File not found: ${resolved.virtualPath}`);
1091
+ }
1092
+ const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
1093
+ const ranges = input.message.edits.map((edit, index) => {
1094
+ if (edit.oldText === edit.newText) {
1095
+ throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
1096
+ }
1097
+ if (edit.oldText.length === 0) {
1098
+ throw new Error(`edits[${index}].oldText cannot be empty`);
1099
+ }
1100
+ const occurrences = originalContent.split(edit.oldText).length - 1;
1101
+ if (occurrences === 0) {
1102
+ throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1103
+ }
1104
+ if (occurrences > 1) {
1105
+ throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
1106
+ }
1107
+ const start = originalContent.indexOf(edit.oldText);
1108
+ return { index, start, end: start + edit.oldText.length, edit };
1109
+ });
1110
+ const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
1111
+ for (let index = 1; index < sortedRanges.length; index += 1) {
1112
+ const previous = sortedRanges[index - 1];
1113
+ const current = sortedRanges[index];
1114
+ if (current.start < previous.end) {
1115
+ throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
1116
+ }
1038
1117
  }
1039
- }
1040
- const nextContent = input.message.replaceAll ? content.split(input.message.oldString).join(input.message.newString) : content.replace(input.message.oldString, input.message.newString);
1041
- fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1042
- return {
1043
- type: "edit_file",
1044
- file: resolved.virtualPath
1045
- };
1118
+ let nextContent = originalContent;
1119
+ for (const range of [...sortedRanges].reverse()) {
1120
+ nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
1121
+ }
1122
+ fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1123
+ return {
1124
+ type: "edit",
1125
+ file: resolved.virtualPath,
1126
+ edits: input.message.edits.length
1127
+ };
1128
+ });
1046
1129
  }
1047
- function decodePatchPath(rawPath) {
1048
- const trimmed = rawPath.trim();
1049
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
1050
- try {
1051
- return JSON.parse(trimmed);
1052
- } catch {
1053
- return trimmed.slice(1, -1);
1130
+ const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
1131
+ function normalizeToolLimit(value, fallback, max) {
1132
+ if (!Number.isFinite(value) || value === void 0) return fallback;
1133
+ return Math.max(1, Math.min(max, Math.floor(value)));
1134
+ }
1135
+ function escapeRegex(value) {
1136
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1137
+ }
1138
+ function wildcardToRegex(pattern) {
1139
+ const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
1140
+ return new RegExp(`^${escaped}$`);
1141
+ }
1142
+ function normalizeFindPattern(pattern) {
1143
+ return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
1144
+ }
1145
+ function toVirtualPath(branchPath, absolutePath) {
1146
+ const relative = path.relative(branchPath, absolutePath).split(path.sep).join(path.posix.sep);
1147
+ return relative ? `/${relative}` : "/";
1148
+ }
1149
+ function walkProjectEntries(branchPath, startPath) {
1150
+ const entries = [];
1151
+ const visit = (absolutePath) => {
1152
+ const stat = fs.statSync(absolutePath);
1153
+ const virtualPath = toVirtualPath(branchPath, absolutePath);
1154
+ entries.push({ absolutePath, virtualPath, stat });
1155
+ if (!stat.isDirectory()) return;
1156
+ const children = fs.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1157
+ for (const child of children) {
1158
+ if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
1159
+ visit(path.join(absolutePath, child.name));
1054
1160
  }
1055
- }
1056
- return trimmed;
1161
+ };
1162
+ visit(startPath);
1163
+ return entries;
1057
1164
  }
1058
- function normalizePatchPath(rawPath) {
1059
- const withoutMetadata = rawPath.split(" ")[0] ?? rawPath;
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;
1165
+ function isProbablyText(bytes) {
1166
+ return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1076
1167
  }
1077
- function readDiffPathToken(value, startIndex) {
1078
- let index = startIndex;
1079
- while (index < value.length && /\s/.test(value[index] ?? "")) {
1080
- index += 1;
1081
- }
1082
- if (index >= value.length) {
1083
- throw new Error("Missing path in patch header");
1084
- }
1085
- if (value[index] !== '"') {
1086
- const tokenStart2 = index;
1087
- while (index < value.length && !/\s/.test(value[index] ?? "")) {
1088
- index += 1;
1089
- }
1090
- return { token: value.slice(tokenStart2, index), nextIndex: index };
1091
- }
1092
- const tokenStart = index;
1093
- index += 1;
1094
- let escaped = false;
1095
- while (index < value.length) {
1096
- const character = value[index];
1097
- if (escaped) {
1098
- escaped = false;
1099
- } else if (character === "\\") {
1100
- escaped = true;
1101
- } else if (character === '"') {
1102
- index += 1;
1103
- return { token: value.slice(tokenStart, index), nextIndex: index };
1168
+ async function executeGrepOperation(input) {
1169
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1170
+ const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1171
+ if (!fs.existsSync(start.absolutePath)) {
1172
+ throw new Error(`Path not found: ${start.virtualPath}`);
1173
+ }
1174
+ const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
1175
+ const flags = input.message.caseSensitive === false ? "i" : "";
1176
+ const regex = new RegExp(input.message.pattern, flags);
1177
+ const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
1178
+ const lines = [];
1179
+ let matchCount = 0;
1180
+ let truncated = false;
1181
+ for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1182
+ if (!entry.stat.isFile()) continue;
1183
+ const repoRelative = entry.virtualPath.slice(1);
1184
+ if (globRegex && !globRegex.test(repoRelative)) continue;
1185
+ const bytes = fs.readFileSync(entry.absolutePath);
1186
+ if (!isProbablyText(bytes)) continue;
1187
+ const fileLines = bytes.toString("utf8").split(/\r?\n/);
1188
+ for (let index = 0; index < fileLines.length; index += 1) {
1189
+ const line = fileLines[index] ?? "";
1190
+ regex.lastIndex = 0;
1191
+ const match = regex.exec(line);
1192
+ if (!match) continue;
1193
+ matchCount += 1;
1194
+ if (lines.length < limit) {
1195
+ const column = Math.max(1, (match.index ?? 0) + 1);
1196
+ lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
1197
+ } else {
1198
+ truncated = true;
1199
+ }
1104
1200
  }
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
1201
  }
1202
+ const hint = truncated ? `
1203
+ ... truncated. Narrow path/glob or increase limit to continue.` : "";
1119
1204
  return {
1120
- oldPath: normalizePatchPath(first.token),
1121
- newPath: normalizePatchPath(second.token)
1205
+ type: "grep",
1206
+ content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
1207
+ matchCount,
1208
+ truncated
1122
1209
  };
1123
1210
  }
1124
- function summarizePatch(patch) {
1125
- if (!patch.trim()) {
1126
- throw new Error("Patch cannot be empty");
1127
- }
1128
- const files = [];
1129
- let current;
1130
- for (const line of patch.split(/\r?\n/)) {
1131
- if (line.startsWith("diff --git ")) {
1132
- const { oldPath, newPath } = parseDiffGitPaths(line);
1133
- if (!oldPath && !newPath) {
1134
- throw new Error(`Invalid patch header: ${line}`);
1135
- }
1136
- current = {
1137
- path: newPath ?? oldPath,
1138
- ...oldPath && newPath && oldPath !== newPath ? { oldPath } : {},
1139
- operation: oldPath && newPath && oldPath !== newPath ? "move" : "update",
1140
- additions: 0,
1141
- deletions: 0,
1142
- hunks: 0
1143
- };
1144
- files.push(current);
1145
- continue;
1146
- }
1147
- if (!current) {
1148
- continue;
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;
1211
+ async function executeFindOperation(input) {
1212
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1213
+ const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1214
+ if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
1215
+ throw new Error(`Directory not found: ${start.virtualPath}`);
1216
+ }
1217
+ const pattern = normalizeFindPattern(input.message.pattern ?? "*");
1218
+ const matcher = wildcardToRegex(pattern);
1219
+ const entryType = input.message.entryType ?? "file";
1220
+ const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1221
+ const matches = [];
1222
+ let count = 0;
1223
+ let truncated = false;
1224
+ for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1225
+ if (entry.virtualPath === start.virtualPath) continue;
1226
+ const isDirectory = entry.stat.isDirectory();
1227
+ if (entryType === "file" && !entry.stat.isFile()) continue;
1228
+ if (entryType === "directory" && !isDirectory) continue;
1229
+ const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
1230
+ if (!matcher.test(path.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
1231
+ count += 1;
1232
+ if (matches.length < limit) {
1233
+ matches.push(label);
1234
+ } else {
1235
+ truncated = true;
1182
1236
  }
1183
1237
  }
1184
- if (files.length === 0) {
1185
- throw new Error("Patch must contain at least one diff --git file header");
1186
- }
1187
- return files;
1238
+ const hint = truncated ? "\n... truncated. Narrow path/pattern or increase limit to continue." : "";
1239
+ return {
1240
+ type: "find",
1241
+ content: matches.length > 0 ? `${matches.join("\n")}${hint}` : "No paths found.",
1242
+ count,
1243
+ truncated
1244
+ };
1188
1245
  }
1189
- async function executeApplyPatchOperation(input) {
1246
+ async function executeLsOperation(input) {
1190
1247
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1191
- const files = summarizePatch(input.message.patch);
1192
- const patchPath = path.join(os.tmpdir(), `r5d-apply-patch-${randomUUID()}.patch`);
1193
- try {
1194
- fs.writeFileSync(patchPath, input.message.patch, "utf8");
1195
- runInternalGit(workspace.internalGit, ["apply", "--check", "--whitespace=nowarn", patchPath]);
1196
- runInternalGit(workspace.internalGit, ["apply", "--whitespace=nowarn", patchPath]);
1197
- } catch (error) {
1198
- throw new Error(`Patch did not apply: ${error instanceof Error ? error.message : String(error)}`);
1199
- } finally {
1200
- fs.rmSync(patchPath, { force: true });
1201
- }
1248
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1249
+ if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
1250
+ throw new Error(`Directory not found: ${resolved.virtualPath}`);
1251
+ }
1252
+ const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1253
+ 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));
1254
+ const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
1255
+ const truncated = entries.length > displayed.length;
1256
+ const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
1202
1257
  return {
1203
- type: "apply_patch",
1204
- files,
1205
- summary: {
1206
- filesChanged: files.length,
1207
- additions: files.reduce((sum, file) => sum + file.additions, 0),
1208
- deletions: files.reduce((sum, file) => sum + file.deletions, 0),
1209
- hunks: files.reduce((sum, file) => sum + file.hunks, 0)
1210
- }
1258
+ type: "ls",
1259
+ path: resolved.virtualPath,
1260
+ content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
1261
+ count: entries.length,
1262
+ truncated
1211
1263
  };
1212
1264
  }
1213
1265
  async function executeViewFileBytesOperation(input) {
@@ -1227,7 +1279,7 @@ async function executeViewFileBytesOperation(input) {
1227
1279
  const extension2 = path.extname(artifact.absolutePath).toLowerCase();
1228
1280
  const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
1229
1281
  if (!mediaType2) {
1230
- throw new Error("view_image supports PNG and JPEG files only");
1282
+ throw new Error("read supports PNG and JPEG image inputs only");
1231
1283
  }
1232
1284
  const bytes2 = fs.readFileSync(artifact.absolutePath);
1233
1285
  const dimensions2 = readImageDimensions(bytes2, mediaType2);
@@ -1249,7 +1301,7 @@ async function executeViewFileBytesOperation(input) {
1249
1301
  const extension = path.extname(resolved.absolutePath).toLowerCase();
1250
1302
  const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1251
1303
  if (!mediaType) {
1252
- throw new Error("view_image supports PNG and JPEG files only");
1304
+ throw new Error("read supports PNG and JPEG image inputs only");
1253
1305
  }
1254
1306
  const bytes = fs.readFileSync(resolved.absolutePath);
1255
1307
  const dimensions = readImageDimensions(bytes, mediaType);
@@ -1355,14 +1407,18 @@ function pullBranch(input) {
1355
1407
  }
1356
1408
  async function executeOperation(input) {
1357
1409
  switch (input.message.type) {
1358
- case "read_file":
1410
+ case "read":
1359
1411
  return executeReadFileOperation({ ...input, message: input.message });
1360
- case "create_file":
1361
- return executeCreateFileOperation({ ...input, message: input.message });
1362
- case "edit_file":
1412
+ case "write":
1413
+ return executeWriteFileOperation({ ...input, message: input.message });
1414
+ case "edit":
1363
1415
  return executeEditFileOperation({ ...input, message: input.message });
1364
- case "apply_patch":
1365
- return executeApplyPatchOperation({ ...input, message: input.message });
1416
+ case "grep":
1417
+ return executeGrepOperation({ ...input, message: input.message });
1418
+ case "find":
1419
+ return executeFindOperation({ ...input, message: input.message });
1420
+ case "ls":
1421
+ return executeLsOperation({ ...input, message: input.message });
1366
1422
  case "view_file_bytes":
1367
1423
  return executeViewFileBytesOperation({ ...input, message: input.message });
1368
1424
  case "sync":
@@ -1417,18 +1473,14 @@ async function executeCommand(input) {
1417
1473
  branchName: input.message.branchName,
1418
1474
  manifest: input.manifest
1419
1475
  });
1420
- let artifactProcessEnv = {};
1421
- if (input.message.sessionId) {
1422
- await syncSessionArtifacts({
1423
- baseUrl: input.baseUrl,
1424
- token: input.token,
1425
- projectId: input.projectId,
1426
- branchName: input.message.branchName,
1427
- sessionId: input.message.sessionId,
1428
- artifactRoot: input.artifactRoot
1429
- });
1430
- artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
1431
- }
1476
+ const artifactProcessEnv = await prepareArtifactEnvForShell({
1477
+ baseUrl: input.baseUrl,
1478
+ token: input.token,
1479
+ projectId: input.projectId,
1480
+ branchName: input.message.branchName,
1481
+ sessionId: input.message.sessionId,
1482
+ artifactRoot: input.artifactRoot
1483
+ });
1432
1484
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1433
1485
  const subprocess = Bun.spawn(input.message.argv, {
1434
1486
  cwd,
@@ -1502,7 +1554,7 @@ async function executeStreamingCommand(input) {
1502
1554
  branchName: input.message.branchName,
1503
1555
  manifest: input.manifest
1504
1556
  });
1505
- await syncSessionArtifacts({
1557
+ const artifactProcessEnv = await prepareArtifactEnvForShell({
1506
1558
  baseUrl: input.baseUrl,
1507
1559
  token: input.token,
1508
1560
  projectId: input.projectId,
@@ -1510,7 +1562,6 @@ async function executeStreamingCommand(input) {
1510
1562
  sessionId: input.message.sessionId,
1511
1563
  artifactRoot: input.artifactRoot
1512
1564
  });
1513
- const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
1514
1565
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1515
1566
  const subprocess = Bun.spawn(input.message.argv, {
1516
1567
  cwd,
@@ -2063,7 +2114,7 @@ async function startWorker(options) {
2063
2114
  }
2064
2115
  return;
2065
2116
  }
2066
- 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") {
2117
+ 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
2118
  try {
2068
2119
  const manifest = manifestByProjectId.get(message.projectId);
2069
2120
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -2148,5 +2199,6 @@ if (isCliEntrypoint()) {
2148
2199
  export {
2149
2200
  ensureVisibleGitCheckout,
2150
2201
  isArtifactEnvPath,
2202
+ prepareArtifactEnvForShell,
2151
2203
  syncSessionArtifacts
2152
2204
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "type": "module"
5
5
  }