@ricsam/r5d-worker 0.0.10 → 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 CHANGED
@@ -29,14 +29,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
30
  var main_exports = {};
31
31
  __export(main_exports, {
32
- ensureVisibleGitCheckout: () => ensureVisibleGitCheckout
32
+ ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
+ isArtifactEnvPath: () => isArtifactEnvPath,
34
+ syncSessionArtifacts: () => syncSessionArtifacts
33
35
  });
34
36
  module.exports = __toCommonJS(main_exports);
35
37
  var import_node_fs = __toESM(require("node:fs"), 1);
36
- var import_node_os = __toESM(require("node:os"), 1);
37
38
  var import_node_path = __toESM(require("node:path"), 1);
38
39
  var import_node_crypto = require("node:crypto");
39
- var import_node_os2 = require("node:os");
40
+ var import_node_os = __toESM(require("node:os"), 1);
40
41
  var import_node_child_process = require("node:child_process");
41
42
  var import_git_identity = require("./git-identity.cjs");
42
43
  const import_meta = {};
@@ -45,6 +46,7 @@ const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
45
46
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
46
47
  const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
47
48
  const DEFAULT_READ_LIMIT = 2e3;
49
+ const DEFAULT_READ_MAX_BYTES = 5e4;
48
50
  const MAX_LINE_LENGTH = 2e3;
49
51
  const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
50
52
  const R5D_REMOTE_NAME = "r5d";
@@ -64,6 +66,9 @@ function defaultProjectsRoot() {
64
66
  function defaultSyncRoot() {
65
67
  return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "sync");
66
68
  }
69
+ function defaultArtifactRoot() {
70
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "artifacts");
71
+ }
67
72
  function printHelp() {
68
73
  process.stdout.write(`Usage:
69
74
  r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
@@ -77,6 +82,7 @@ Options:
77
82
  --config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
78
83
  --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
79
84
  --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
85
+ --artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
80
86
  -v, --version Show r5d-worker version
81
87
  -h, --help Show this help
82
88
  `);
@@ -193,6 +199,11 @@ function parseArgs(argv) {
193
199
  options.syncRoot = syncRoot;
194
200
  continue;
195
201
  }
202
+ const artifactRoot = readOption("--artifact-root");
203
+ if (artifactRoot !== void 0) {
204
+ options.artifactRoot = artifactRoot;
205
+ continue;
206
+ }
196
207
  rest.push(arg);
197
208
  }
198
209
  if (options.version) {
@@ -235,6 +246,170 @@ async function readResponseText(response) {
235
246
  return "";
236
247
  }
237
248
  }
249
+ const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
250
+ function validateArtifactSessionId(sessionId) {
251
+ if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
252
+ throw new Error(`Invalid artifact session id: ${sessionId}`);
253
+ }
254
+ }
255
+ function assertInsideRoot(rootPath, candidatePath, label) {
256
+ const relative = import_node_path.default.relative(rootPath, candidatePath);
257
+ if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
258
+ throw new Error(`${label} escapes the artifact directory`);
259
+ }
260
+ }
261
+ function sessionArtifactDir(artifactRoot, sessionId) {
262
+ validateArtifactSessionId(sessionId);
263
+ return import_node_path.default.join(artifactRoot, sessionId);
264
+ }
265
+ function artifactEnv(artifactRoot, sessionId) {
266
+ if (!sessionId) {
267
+ return {};
268
+ }
269
+ return {
270
+ R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
271
+ };
272
+ }
273
+ function isArtifactEnvPath(filePath) {
274
+ return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
275
+ }
276
+ function artifactApiBasePath(projectId, branchName, sessionId) {
277
+ return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
278
+ }
279
+ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
280
+ return new URL(artifactApiBasePath(projectId, branchName, sessionId), baseUrl);
281
+ }
282
+ function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
283
+ return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
284
+ }
285
+ function sha256Path(filePath) {
286
+ return (0, import_node_crypto.createHash)("sha256").update(import_node_fs.default.readFileSync(filePath)).digest("hex");
287
+ }
288
+ function parseSessionArtifactManifest(value) {
289
+ if (!value || typeof value !== "object" || !Array.isArray(value.artifacts)) {
290
+ throw new Error("Artifact manifest response is invalid");
291
+ }
292
+ return value.artifacts.map((entry) => {
293
+ if (!entry || typeof entry !== "object") {
294
+ throw new Error("Artifact manifest entry is invalid");
295
+ }
296
+ const artifact = entry;
297
+ if (typeof artifact.filename !== "string" || artifact.filename.includes("/") || artifact.filename.includes("\\")) {
298
+ throw new Error("Artifact manifest entry has an invalid filename");
299
+ }
300
+ if (typeof artifact.size !== "number" || !Number.isFinite(artifact.size) || artifact.size < 0) {
301
+ throw new Error(`Artifact manifest entry has an invalid size: ${artifact.filename}`);
302
+ }
303
+ if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
304
+ throw new Error(`Artifact manifest entry has an invalid sha256: ${artifact.filename}`);
305
+ }
306
+ return {
307
+ filename: artifact.filename,
308
+ size: artifact.size,
309
+ sha256: artifact.sha256.toLowerCase()
310
+ };
311
+ });
312
+ }
313
+ async function fetchSessionArtifactManifest(input) {
314
+ const response = await fetch(artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId), {
315
+ headers: {
316
+ Authorization: `Bearer ${input.token}`
317
+ }
318
+ });
319
+ if (!response.ok) {
320
+ const detail = (await readResponseText(response)).trim();
321
+ throw new Error(`Failed to fetch artifact manifest (${response.status})${detail ? `: ${detail}` : ""}`);
322
+ }
323
+ return parseSessionArtifactManifest(await response.json());
324
+ }
325
+ async function downloadSessionArtifact(input) {
326
+ const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
327
+ const targetPath = import_node_path.default.join(targetDir, input.artifact.filename);
328
+ assertInsideRoot(targetDir, targetPath, "Artifact path");
329
+ const response = await fetch(
330
+ artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, input.artifact.filename),
331
+ {
332
+ headers: {
333
+ Authorization: `Bearer ${input.token}`
334
+ }
335
+ }
336
+ );
337
+ if (!response.ok) {
338
+ const detail = (await readResponseText(response)).trim();
339
+ throw new Error(`Failed to download artifact ${input.artifact.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
340
+ }
341
+ const buffer = Buffer.from(await response.arrayBuffer());
342
+ const receivedHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
343
+ if (receivedHash !== input.artifact.sha256) {
344
+ throw new Error(`Downloaded artifact ${input.artifact.filename} failed sha256 verification`);
345
+ }
346
+ if (buffer.length !== input.artifact.size) {
347
+ throw new Error(`Downloaded artifact ${input.artifact.filename} has unexpected size`);
348
+ }
349
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
350
+ import_node_fs.default.writeFileSync(tempPath, buffer);
351
+ import_node_fs.default.renameSync(tempPath, targetPath);
352
+ }
353
+ async function syncSessionArtifacts(input) {
354
+ const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
355
+ import_node_fs.default.mkdirSync(targetDir, { recursive: true });
356
+ const artifacts = await fetchSessionArtifactManifest(input);
357
+ const manifestNames = new Set(artifacts.map((artifact) => artifact.filename));
358
+ for (const artifact of artifacts) {
359
+ const localPath = import_node_path.default.join(targetDir, artifact.filename);
360
+ assertInsideRoot(targetDir, localPath, "Artifact path");
361
+ if (import_node_fs.default.existsSync(localPath)) {
362
+ const stats = import_node_fs.default.statSync(localPath);
363
+ if (stats.isFile() && stats.size === artifact.size && sha256Path(localPath) === artifact.sha256) {
364
+ continue;
365
+ }
366
+ import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
367
+ }
368
+ await downloadSessionArtifact({ ...input, artifact });
369
+ }
370
+ for (const localName of import_node_fs.default.readdirSync(targetDir)) {
371
+ if (!manifestNames.has(localName)) {
372
+ const localPath = import_node_path.default.join(targetDir, localName);
373
+ assertInsideRoot(targetDir, localPath, "Artifact path");
374
+ import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
375
+ }
376
+ }
377
+ return targetDir;
378
+ }
379
+ async function resolveArtifactEnvFilePath(input) {
380
+ if (!isArtifactEnvPath(input.filePath)) {
381
+ return null;
382
+ }
383
+ if (!input.sessionId) {
384
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
385
+ }
386
+ if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
387
+ throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
388
+ }
389
+ const targetDir = await syncSessionArtifacts({
390
+ baseUrl: input.baseUrl,
391
+ token: input.token,
392
+ projectId: input.projectId,
393
+ branchName: input.branchName,
394
+ sessionId: input.sessionId,
395
+ artifactRoot: input.artifactRoot
396
+ });
397
+ const relativePath = import_node_path.default.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
398
+ if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
399
+ throw new Error(`Invalid artifact path: ${input.filePath}`);
400
+ }
401
+ const absolutePath = import_node_path.default.resolve(targetDir, ...relativePath.split("/"));
402
+ assertInsideRoot(targetDir, absolutePath, "Artifact path");
403
+ return {
404
+ absolutePath,
405
+ virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
406
+ };
407
+ }
408
+ function rejectArtifactWritePath(filePath) {
409
+ if (isArtifactEnvPath(filePath)) {
410
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
411
+ }
412
+ }
238
413
  async function verifyR5dctlAuth(baseUrl, token) {
239
414
  const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
240
415
  let response;
@@ -746,16 +921,36 @@ function formatLineNumberedContent(content, offset, limit) {
746
921
  const endLine = Math.min(startLine + readLimit - 1, totalLines);
747
922
  const selectedLines = allLines.slice(startLine - 1, endLine);
748
923
  const padWidth = String(endLine).length;
749
- const formattedLines = selectedLines.map((line, index) => {
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] ?? "";
750
929
  const lineNumber = startLine + index;
751
930
  const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
752
- return `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
753
- });
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;
754
947
  return {
755
- content: formattedLines.join("\n"),
948
+ content: formattedContent,
756
949
  totalLines,
757
950
  startLine,
758
- endLine
951
+ endLine: actualEndLine,
952
+ truncated,
953
+ continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
759
954
  };
760
955
  }
761
956
  function readPngDimensions(bytes) {
@@ -822,7 +1017,51 @@ function readImageDimensions(bytes, mediaType) {
822
1017
  }
823
1018
  return dimensions;
824
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
+ }
825
1041
  async function executeReadFileOperation(input) {
1042
+ const artifact = await resolveArtifactEnvFilePath({
1043
+ filePath: input.message.filePath,
1044
+ baseUrl: input.baseUrl,
1045
+ token: input.token,
1046
+ projectId: input.projectId,
1047
+ branchName: input.message.branchName,
1048
+ sessionId: input.message.sessionId,
1049
+ artifactRoot: input.artifactRoot
1050
+ });
1051
+ if (artifact) {
1052
+ if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1053
+ throw new Error(`File not found: ${artifact.virtualPath}`);
1054
+ }
1055
+ const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1056
+ const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
1057
+ return {
1058
+ type: "read",
1059
+ kind: "text",
1060
+ file: artifact.virtualPath,
1061
+ gitBlobHash: getGitBlobHashForContent(buffer2),
1062
+ ...formatted2
1063
+ };
1064
+ }
826
1065
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
827
1066
  const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
828
1067
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
@@ -831,222 +1070,242 @@ async function executeReadFileOperation(input) {
831
1070
  const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
832
1071
  const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
833
1072
  return {
834
- type: "read_file",
1073
+ type: "read",
1074
+ kind: "text",
835
1075
  file: resolved.virtualPath,
836
1076
  gitBlobHash: getGitBlobHashForContent(buffer),
837
1077
  ...formatted
838
1078
  };
839
1079
  }
840
- async function executeCreateFileOperation(input) {
841
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
842
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
843
- if (import_node_fs.default.existsSync(resolved.absolutePath)) {
844
- throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
845
- }
846
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
847
- import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
848
- return {
849
- type: "create_file",
850
- file: resolved.virtualPath,
851
- gitBlobHash: getGitBlobHashForContent(input.message.content)
852
- };
1080
+ async function executeWriteFileOperation(input) {
1081
+ rejectArtifactWritePath(input.message.filePath);
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
+ });
853
1093
  }
854
1094
  async function executeEditFileOperation(input) {
855
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
856
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
857
- if (input.message.oldString === input.message.newString) {
858
- throw new Error("old_string and new_string must be different");
859
- }
860
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
861
- throw new Error(`File not found: ${resolved.virtualPath}`);
862
- }
863
- const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
864
- if (!content.includes(input.message.oldString)) {
865
- throw new Error(
866
- `Could not find old_string: "${input.message.oldString.slice(0, 100)}${input.message.oldString.length > 100 ? "..." : ""}"`
867
- );
868
- }
869
- if (!input.message.replaceAll) {
870
- const occurrences = content.split(input.message.oldString).length - 1;
871
- if (occurrences > 1) {
872
- throw new Error(
873
- `old_string is not unique in the file (found ${occurrences} occurrences). Provide more context or use replace_all: true.`
874
- );
1095
+ rejectArtifactWritePath(input.message.filePath);
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
+ }
875
1130
  }
876
- }
877
- const nextContent = input.message.replaceAll ? content.split(input.message.oldString).join(input.message.newString) : content.replace(input.message.oldString, input.message.newString);
878
- import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
879
- return {
880
- type: "edit_file",
881
- file: resolved.virtualPath
882
- };
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
+ });
883
1142
  }
884
- function decodePatchPath(rawPath) {
885
- const trimmed = rawPath.trim();
886
- if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
887
- try {
888
- return JSON.parse(trimmed);
889
- } catch {
890
- return trimmed.slice(1, -1);
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));
891
1173
  }
892
- }
893
- return trimmed;
1174
+ };
1175
+ visit(startPath);
1176
+ return entries;
894
1177
  }
895
- function normalizePatchPath(rawPath) {
896
- const withoutMetadata = rawPath.split(" ")[0] ?? rawPath;
897
- let patchPath = decodePatchPath(withoutMetadata);
898
- if (patchPath === "/dev/null") {
899
- return null;
900
- }
901
- if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
902
- patchPath = patchPath.slice(2);
903
- }
904
- if (!patchPath || import_node_path.default.isAbsolute(patchPath)) {
905
- throw new Error(`Invalid patch path: ${rawPath}`);
906
- }
907
- const normalized = import_node_path.default.posix.normalize(patchPath);
908
- if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
909
- throw new Error(`Invalid patch path: ${rawPath}`);
910
- }
911
- return normalized;
1178
+ function isProbablyText(bytes) {
1179
+ return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
912
1180
  }
913
- function readDiffPathToken(value, startIndex) {
914
- let index = startIndex;
915
- while (index < value.length && /\s/.test(value[index] ?? "")) {
916
- index += 1;
917
- }
918
- if (index >= value.length) {
919
- throw new Error("Missing path in patch header");
920
- }
921
- if (value[index] !== '"') {
922
- const tokenStart2 = index;
923
- while (index < value.length && !/\s/.test(value[index] ?? "")) {
924
- index += 1;
925
- }
926
- return { token: value.slice(tokenStart2, index), nextIndex: index };
927
- }
928
- const tokenStart = index;
929
- index += 1;
930
- let escaped = false;
931
- while (index < value.length) {
932
- const character = value[index];
933
- if (escaped) {
934
- escaped = false;
935
- } else if (character === "\\") {
936
- escaped = true;
937
- } else if (character === '"') {
938
- index += 1;
939
- return { token: value.slice(tokenStart, index), nextIndex: index };
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
+ }
940
1213
  }
941
- index += 1;
942
- }
943
- throw new Error("Unterminated quoted path in patch header");
944
- }
945
- function parseDiffGitPaths(line) {
946
- if (!line.startsWith("diff --git ")) {
947
- throw new Error(`Invalid patch header: ${line}`);
948
- }
949
- const rest = line.slice("diff --git ".length);
950
- const first = readDiffPathToken(rest, 0);
951
- const second = readDiffPathToken(rest, first.nextIndex);
952
- if (rest.slice(second.nextIndex).trim()) {
953
- throw new Error(`Invalid patch header: ${line}`);
954
1214
  }
1215
+ const hint = truncated ? `
1216
+ ... truncated. Narrow path/glob or increase limit to continue.` : "";
955
1217
  return {
956
- oldPath: normalizePatchPath(first.token),
957
- newPath: normalizePatchPath(second.token)
1218
+ type: "grep",
1219
+ content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
1220
+ matchCount,
1221
+ truncated
958
1222
  };
959
1223
  }
960
- function summarizePatch(patch) {
961
- if (!patch.trim()) {
962
- throw new Error("Patch cannot be empty");
963
- }
964
- const files = [];
965
- let current;
966
- for (const line of patch.split(/\r?\n/)) {
967
- if (line.startsWith("diff --git ")) {
968
- const { oldPath, newPath } = parseDiffGitPaths(line);
969
- if (!oldPath && !newPath) {
970
- throw new Error(`Invalid patch header: ${line}`);
971
- }
972
- current = {
973
- path: newPath ?? oldPath,
974
- ...oldPath && newPath && oldPath !== newPath ? { oldPath } : {},
975
- operation: oldPath && newPath && oldPath !== newPath ? "move" : "update",
976
- additions: 0,
977
- deletions: 0,
978
- hunks: 0
979
- };
980
- files.push(current);
981
- continue;
982
- }
983
- if (!current) {
984
- continue;
985
- }
986
- if (line.startsWith("new file mode ")) {
987
- current.operation = "add";
988
- continue;
989
- }
990
- if (line.startsWith("deleted file mode ")) {
991
- current.operation = "delete";
992
- if (current.oldPath) {
993
- current.path = current.oldPath;
994
- delete current.oldPath;
995
- }
996
- continue;
997
- }
998
- if (line.startsWith("rename from ")) {
999
- current.operation = "move";
1000
- current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
1001
- continue;
1002
- }
1003
- if (line.startsWith("rename to ")) {
1004
- current.operation = "move";
1005
- current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
1006
- continue;
1007
- }
1008
- if (line.startsWith("@@")) {
1009
- current.hunks += 1;
1010
- continue;
1011
- }
1012
- if (line.startsWith("+") && !line.startsWith("+++")) {
1013
- current.additions += 1;
1014
- continue;
1015
- }
1016
- if (line.startsWith("-") && !line.startsWith("---")) {
1017
- 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;
1018
1249
  }
1019
1250
  }
1020
- if (files.length === 0) {
1021
- throw new Error("Patch must contain at least one diff --git file header");
1022
- }
1023
- return files;
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
+ };
1024
1258
  }
1025
- async function executeApplyPatchOperation(input) {
1259
+ async function executeLsOperation(input) {
1026
1260
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1027
- const files = summarizePatch(input.message.patch);
1028
- const patchPath = import_node_path.default.join(import_node_os.default.tmpdir(), `r5d-apply-patch-${(0, import_node_crypto.randomUUID)()}.patch`);
1029
- try {
1030
- import_node_fs.default.writeFileSync(patchPath, input.message.patch, "utf8");
1031
- runInternalGit(workspace.internalGit, ["apply", "--check", "--whitespace=nowarn", patchPath]);
1032
- runInternalGit(workspace.internalGit, ["apply", "--whitespace=nowarn", patchPath]);
1033
- } catch (error) {
1034
- throw new Error(`Patch did not apply: ${error instanceof Error ? error.message : String(error)}`);
1035
- } finally {
1036
- import_node_fs.default.rmSync(patchPath, { force: true });
1037
- }
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." : "";
1038
1270
  return {
1039
- type: "apply_patch",
1040
- files,
1041
- summary: {
1042
- filesChanged: files.length,
1043
- additions: files.reduce((sum, file) => sum + file.additions, 0),
1044
- deletions: files.reduce((sum, file) => sum + file.deletions, 0),
1045
- hunks: files.reduce((sum, file) => sum + file.hunks, 0)
1046
- }
1271
+ type: "ls",
1272
+ path: resolved.virtualPath,
1273
+ content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
1274
+ count: entries.length,
1275
+ truncated
1047
1276
  };
1048
1277
  }
1049
1278
  async function executeViewFileBytesOperation(input) {
1279
+ const artifact = await resolveArtifactEnvFilePath({
1280
+ filePath: input.message.filePath,
1281
+ baseUrl: input.baseUrl,
1282
+ token: input.token,
1283
+ projectId: input.projectId,
1284
+ branchName: input.message.branchName,
1285
+ sessionId: input.message.sessionId,
1286
+ artifactRoot: input.artifactRoot
1287
+ });
1288
+ if (artifact) {
1289
+ if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1290
+ throw new Error(`File not found: ${artifact.virtualPath}`);
1291
+ }
1292
+ const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1293
+ const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
1294
+ if (!mediaType2) {
1295
+ throw new Error("read supports PNG and JPEG image inputs only");
1296
+ }
1297
+ const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1298
+ const dimensions2 = readImageDimensions(bytes2, mediaType2);
1299
+ return {
1300
+ type: "view_file_bytes",
1301
+ filePath: artifact.virtualPath,
1302
+ mediaType: mediaType2,
1303
+ base64: bytes2.toString("base64"),
1304
+ fileSizeBytes: bytes2.length,
1305
+ width: dimensions2.width,
1306
+ height: dimensions2.height
1307
+ };
1308
+ }
1050
1309
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1051
1310
  const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1052
1311
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
@@ -1055,7 +1314,7 @@ async function executeViewFileBytesOperation(input) {
1055
1314
  const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
1056
1315
  const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1057
1316
  if (!mediaType) {
1058
- throw new Error("view_image supports PNG and JPEG files only");
1317
+ throw new Error("read supports PNG and JPEG image inputs only");
1059
1318
  }
1060
1319
  const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
1061
1320
  const dimensions = readImageDimensions(bytes, mediaType);
@@ -1161,14 +1420,18 @@ function pullBranch(input) {
1161
1420
  }
1162
1421
  async function executeOperation(input) {
1163
1422
  switch (input.message.type) {
1164
- case "read_file":
1423
+ case "read":
1165
1424
  return executeReadFileOperation({ ...input, message: input.message });
1166
- case "create_file":
1167
- return executeCreateFileOperation({ ...input, message: input.message });
1168
- case "edit_file":
1425
+ case "write":
1426
+ return executeWriteFileOperation({ ...input, message: input.message });
1427
+ case "edit":
1169
1428
  return executeEditFileOperation({ ...input, message: input.message });
1170
- case "apply_patch":
1171
- return executeApplyPatchOperation({ ...input, message: input.message });
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 });
1172
1435
  case "view_file_bytes":
1173
1436
  return executeViewFileBytesOperation({ ...input, message: input.message });
1174
1437
  case "sync":
@@ -1223,6 +1486,18 @@ async function executeCommand(input) {
1223
1486
  branchName: input.message.branchName,
1224
1487
  manifest: input.manifest
1225
1488
  });
1489
+ let artifactProcessEnv = {};
1490
+ if (input.message.sessionId) {
1491
+ await syncSessionArtifacts({
1492
+ baseUrl: input.baseUrl,
1493
+ token: input.token,
1494
+ projectId: input.projectId,
1495
+ branchName: input.message.branchName,
1496
+ sessionId: input.message.sessionId,
1497
+ artifactRoot: input.artifactRoot
1498
+ });
1499
+ artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
1500
+ }
1226
1501
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1227
1502
  const subprocess = Bun.spawn(input.message.argv, {
1228
1503
  cwd,
@@ -1232,7 +1507,8 @@ async function executeCommand(input) {
1232
1507
  ...process.env,
1233
1508
  ...containerRegistryEnv(),
1234
1509
  ...input.message.env ?? {},
1235
- ...internalGitProcessEnv(workspace, input.message.env)
1510
+ ...internalGitProcessEnv(workspace, input.message.env),
1511
+ ...artifactProcessEnv
1236
1512
  }
1237
1513
  });
1238
1514
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1295,6 +1571,15 @@ async function executeStreamingCommand(input) {
1295
1571
  branchName: input.message.branchName,
1296
1572
  manifest: input.manifest
1297
1573
  });
1574
+ await syncSessionArtifacts({
1575
+ baseUrl: input.baseUrl,
1576
+ token: input.token,
1577
+ projectId: input.projectId,
1578
+ branchName: input.message.branchName,
1579
+ sessionId: input.message.sessionId,
1580
+ artifactRoot: input.artifactRoot
1581
+ });
1582
+ const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
1298
1583
  const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1299
1584
  const subprocess = Bun.spawn(input.message.argv, {
1300
1585
  cwd,
@@ -1304,7 +1589,8 @@ async function executeStreamingCommand(input) {
1304
1589
  ...process.env,
1305
1590
  ...containerRegistryEnv(),
1306
1591
  ...input.message.env ?? {},
1307
- ...internalGitProcessEnv(workspace, input.message.env)
1592
+ ...internalGitProcessEnv(workspace, input.message.env),
1593
+ ...artifactProcessEnv
1308
1594
  }
1309
1595
  });
1310
1596
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1679,11 +1965,14 @@ async function startWorker(options) {
1679
1965
  validateLabel(label);
1680
1966
  const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
1681
1967
  const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
1968
+ const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
1682
1969
  process.stdout.write(`[r5d-worker] label: ${label}
1683
1970
  `);
1684
1971
  process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
1685
1972
  `);
1686
1973
  process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
1974
+ `);
1975
+ process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
1687
1976
  `);
1688
1977
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
1689
1978
  `);
@@ -1691,6 +1980,7 @@ async function startWorker(options) {
1691
1980
  await verifyR5dctlAuth(baseUrl, token);
1692
1981
  import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
1693
1982
  import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
1983
+ import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
1694
1984
  const manifestByProjectId = /* @__PURE__ */ new Map();
1695
1985
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
1696
1986
  headers: {
@@ -1701,12 +1991,13 @@ async function startWorker(options) {
1701
1991
  const hello = {
1702
1992
  type: "hello",
1703
1993
  hostInfo: {
1704
- hostname: (0, import_node_os2.hostname)(),
1994
+ hostname: (0, import_node_os.hostname)(),
1705
1995
  platform: process.platform,
1706
1996
  arch: process.arch,
1707
1997
  pid: process.pid,
1708
1998
  version: getWorkerVersion(),
1709
- projectRoot: projectsRoot
1999
+ projectRoot: projectsRoot,
2000
+ artifactRoot
1710
2001
  }
1711
2002
  };
1712
2003
  ws.send(JSON.stringify(hello));
@@ -1803,7 +2094,7 @@ async function startWorker(options) {
1803
2094
  const result = await withBranchLock(
1804
2095
  message.projectId,
1805
2096
  message.branchName,
1806
- () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
2097
+ () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
1807
2098
  );
1808
2099
  ws.send(JSON.stringify(result));
1809
2100
  return;
@@ -1822,7 +2113,7 @@ async function startWorker(options) {
1822
2113
  void withBranchLock(
1823
2114
  message.projectId,
1824
2115
  message.branchName,
1825
- () => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
2116
+ () => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
1826
2117
  ).catch((error) => {
1827
2118
  sendWorkerMessage(ws, {
1828
2119
  type: "exec_start_error",
@@ -1841,14 +2132,14 @@ async function startWorker(options) {
1841
2132
  }
1842
2133
  return;
1843
2134
  }
1844
- 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") {
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") {
1845
2136
  try {
1846
2137
  const manifest = manifestByProjectId.get(message.projectId);
1847
2138
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
1848
2139
  const result = await withBranchLock(
1849
2140
  message.projectId,
1850
2141
  message.branchName,
1851
- () => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
2142
+ () => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
1852
2143
  );
1853
2144
  ws.send(
1854
2145
  JSON.stringify({
@@ -1925,5 +2216,7 @@ if (isCliEntrypoint()) {
1925
2216
  }
1926
2217
  // Annotate the CommonJS export names for ESM import in node:
1927
2218
  0 && (module.exports = {
1928
- ensureVisibleGitCheckout
2219
+ ensureVisibleGitCheckout,
2220
+ isArtifactEnvPath,
2221
+ syncSessionArtifacts
1929
2222
  });