@ricsam/r5d-worker 0.0.40 → 0.0.42

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,6 +29,7 @@ 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
+ editWorkerTextFile: () => editWorkerTextFile,
32
33
  ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
34
  findWorkerFiles: () => findWorkerFiles,
34
35
  githubCliEnv: () => githubCliEnv,
@@ -40,10 +41,9 @@ __export(main_exports, {
40
41
  readWorkerImageFile: () => readWorkerImageFile,
41
42
  readWorkerTextFile: () => readWorkerTextFile,
42
43
  resolveHostShell: () => resolveHostShell,
43
- resolveProjectFilePath: () => resolveProjectFilePath,
44
44
  resolveWorkerFilePath: () => resolveWorkerFilePath,
45
- syncProjectPlans: () => syncProjectPlans,
46
- syncSessionArtifacts: () => syncSessionArtifacts
45
+ syncSessionArtifacts: () => syncSessionArtifacts,
46
+ writeWorkerTextFile: () => writeWorkerTextFile
47
47
  });
48
48
  module.exports = __toCommonJS(main_exports);
49
49
  var import_node_fs = __toESM(require("node:fs"), 1);
@@ -335,15 +335,6 @@ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
335
335
  function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
336
336
  return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
337
337
  }
338
- function planApiBasePath(projectId, branchName) {
339
- return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/plans`;
340
- }
341
- function planManifestUrl(baseUrl, projectId, branchName) {
342
- return new URL(planApiBasePath(projectId, branchName), baseUrl);
343
- }
344
- function planDownloadUrl(baseUrl, projectId, branchName, filename) {
345
- return new URL(`${planApiBasePath(projectId, branchName)}/${encodeURIComponent(filename)}`, baseUrl);
346
- }
347
338
  function sha256Path(filePath) {
348
339
  return (0, import_node_crypto.createHash)("sha256").update(import_node_fs.default.readFileSync(filePath)).digest("hex");
349
340
  }
@@ -456,30 +447,6 @@ async function syncSessionArtifacts(input) {
456
447
  })
457
448
  });
458
449
  }
459
- async function fetchProjectPlanManifest(input) {
460
- return fetchRemoteFileManifest({
461
- url: planManifestUrl(input.baseUrl, input.projectId, input.branchName),
462
- token: input.token,
463
- collectionKey: "plans",
464
- label: "plan"
465
- });
466
- }
467
- async function syncProjectPlans(input) {
468
- const targetDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
469
- const plans = await fetchProjectPlanManifest(input);
470
- return syncRemoteFileSet({
471
- targetDir,
472
- files: plans,
473
- label: "Plan path",
474
- download: (plan) => downloadRemoteSyncedFile({
475
- url: planDownloadUrl(input.baseUrl, input.projectId, input.branchName, plan.filename),
476
- token: input.token,
477
- targetDir,
478
- file: plan,
479
- label: "plan"
480
- })
481
- });
482
- }
483
450
  async function resolveArtifactEnvFilePath(input) {
484
451
  if (!isArtifactEnvPath(input.filePath)) {
485
452
  return null;
@@ -539,15 +506,7 @@ async function prepareArtifactEnvForShell(input) {
539
506
  R5D_ARTIFACTS_DIR: artifactsDir
540
507
  };
541
508
  }
542
- async function preparePlanEnvForShell(input) {
543
- try {
544
- await syncProjectPlans(input);
545
- } catch (error) {
546
- process.stderr.write(
547
- `[r5d-worker] plan sync skipped for ${input.projectId}/${input.branchName}: ${error instanceof Error ? error.message : String(error)}
548
- `
549
- );
550
- }
509
+ function preparePlanEnvForShell(input) {
551
510
  const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
552
511
  import_node_fs.default.mkdirSync(plansDir, { recursive: true });
553
512
  return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
@@ -668,9 +627,6 @@ function toProjectDisplayPath(branchPath, absolutePath) {
668
627
  return relative || ".";
669
628
  }
670
629
  function assertAllowedProjectPath(repoRelativePath, inputPath) {
671
- if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
672
- throw new Error("Refusing to access .git through worker file tools");
673
- }
674
630
  if (repoRelativePath.split("/").includes("..")) {
675
631
  throw new Error(`Invalid project file path: ${inputPath}`);
676
632
  }
@@ -718,13 +674,6 @@ function resolveWorkerFilePath(branchPath, inputPath) {
718
674
  scope: "project"
719
675
  };
720
676
  }
721
- function resolveProjectFilePath(branchPath, inputPath) {
722
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
723
- if (resolved.scope === "host") {
724
- throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
725
- }
726
- return resolved;
727
- }
728
677
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
729
678
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
730
679
  }
@@ -1141,8 +1090,11 @@ async function withFileMutationQueue(key, operation) {
1141
1090
  }
1142
1091
  }
1143
1092
  }
1144
- function mutationQueueKey(projectId, branchName, filePath) {
1145
- return `${projectId}:${branchName}:${import_node_path.default.posix.normalize(filePath)}`;
1093
+ function mutationQueueKey(projectId, branchName, resolved) {
1094
+ if (resolved.scope === "host") {
1095
+ return `host:${import_node_path.default.normalize(resolved.absolutePath)}`;
1096
+ }
1097
+ return `project:${projectId}:${branchName}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
1146
1098
  }
1147
1099
  function readWorkerTextFile(branchPath, filePath, offset, limit) {
1148
1100
  const resolved = resolveWorkerFilePath(branchPath, filePath);
@@ -1159,6 +1111,63 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1159
1111
  ...formatted
1160
1112
  };
1161
1113
  }
1114
+ function writeWorkerTextFile(branchPath, filePath, content) {
1115
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1116
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1117
+ import_node_fs.default.writeFileSync(resolved.absolutePath, content, "utf8");
1118
+ return {
1119
+ type: "write",
1120
+ file: resolved.displayPath,
1121
+ gitBlobHash: getGitBlobHashForContent(content)
1122
+ };
1123
+ }
1124
+ function editWorkerTextFile(branchPath, filePath, edits) {
1125
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1126
+ if (!Array.isArray(edits) || edits.length === 0) {
1127
+ throw new Error("edit requires at least one replacement");
1128
+ }
1129
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1130
+ throw new Error(`File not found: ${resolved.displayPath}`);
1131
+ }
1132
+ const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
1133
+ const ranges = edits.map((edit, index) => {
1134
+ if (edit.oldText === edit.newText) {
1135
+ throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
1136
+ }
1137
+ if (edit.oldText.length === 0) {
1138
+ throw new Error(`edits[${index}].oldText cannot be empty`);
1139
+ }
1140
+ const occurrences = originalContent.split(edit.oldText).length - 1;
1141
+ if (occurrences === 0) {
1142
+ throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1143
+ }
1144
+ if (occurrences > 1) {
1145
+ throw new Error(
1146
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1147
+ );
1148
+ }
1149
+ const start = originalContent.indexOf(edit.oldText);
1150
+ return { index, start, end: start + edit.oldText.length, edit };
1151
+ });
1152
+ const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
1153
+ for (let index = 1; index < sortedRanges.length; index += 1) {
1154
+ const previous = sortedRanges[index - 1];
1155
+ const current = sortedRanges[index];
1156
+ if (current.start < previous.end) {
1157
+ throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
1158
+ }
1159
+ }
1160
+ let nextContent = originalContent;
1161
+ for (const range of [...sortedRanges].reverse()) {
1162
+ nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
1163
+ }
1164
+ import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1165
+ return {
1166
+ type: "edit",
1167
+ file: resolved.displayPath,
1168
+ edits: edits.length
1169
+ };
1170
+ }
1162
1171
  async function executeReadFileOperation(input) {
1163
1172
  const artifact = await resolveArtifactEnvFilePath({
1164
1173
  filePath: input.message.filePath,
@@ -1189,66 +1198,17 @@ async function executeReadFileOperation(input) {
1189
1198
  async function executeWriteFileOperation(input) {
1190
1199
  rejectArtifactWritePath(input.message.filePath);
1191
1200
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1192
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1193
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1194
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1195
- import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1196
- return {
1197
- type: "write",
1198
- file: resolved.displayPath,
1199
- gitBlobHash: getGitBlobHashForContent(input.message.content)
1200
- };
1201
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1202
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1203
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1201
1204
  });
1202
1205
  }
1203
1206
  async function executeEditFileOperation(input) {
1204
1207
  rejectArtifactWritePath(input.message.filePath);
1205
1208
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1206
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1207
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1208
- if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1209
- throw new Error("edit requires at least one replacement");
1210
- }
1211
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1212
- throw new Error(`File not found: ${resolved.displayPath}`);
1213
- }
1214
- const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
1215
- const ranges = input.message.edits.map((edit, index) => {
1216
- if (edit.oldText === edit.newText) {
1217
- throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
1218
- }
1219
- if (edit.oldText.length === 0) {
1220
- throw new Error(`edits[${index}].oldText cannot be empty`);
1221
- }
1222
- const occurrences = originalContent.split(edit.oldText).length - 1;
1223
- if (occurrences === 0) {
1224
- throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1225
- }
1226
- if (occurrences > 1) {
1227
- throw new Error(
1228
- `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1229
- );
1230
- }
1231
- const start = originalContent.indexOf(edit.oldText);
1232
- return { index, start, end: start + edit.oldText.length, edit };
1233
- });
1234
- const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
1235
- for (let index = 1; index < sortedRanges.length; index += 1) {
1236
- const previous = sortedRanges[index - 1];
1237
- const current = sortedRanges[index];
1238
- if (current.start < previous.end) {
1239
- throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
1240
- }
1241
- }
1242
- let nextContent = originalContent;
1243
- for (const range of [...sortedRanges].reverse()) {
1244
- nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
1245
- }
1246
- import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1247
- return {
1248
- type: "edit",
1249
- file: resolved.displayPath,
1250
- edits: input.message.edits.length
1251
- };
1209
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1210
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1211
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1252
1212
  });
1253
1213
  }
1254
1214
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1525,9 +1485,7 @@ async function executeCommand(input) {
1525
1485
  sessionId: input.message.sessionId,
1526
1486
  artifactRoot: input.artifactRoot
1527
1487
  });
1528
- const planProcessEnv = await preparePlanEnvForShell({
1529
- baseUrl: input.baseUrl,
1530
- token: input.token,
1488
+ const planProcessEnv = preparePlanEnvForShell({
1531
1489
  projectId: input.projectId,
1532
1490
  branchName: input.message.branchName,
1533
1491
  planRoot: input.planRoot,
@@ -1636,9 +1594,7 @@ async function executeStreamingCommand(input) {
1636
1594
  sessionId: input.message.sessionId,
1637
1595
  artifactRoot: input.artifactRoot
1638
1596
  });
1639
- const planProcessEnv = await preparePlanEnvForShell({
1640
- baseUrl: input.baseUrl,
1641
- token: input.token,
1597
+ const planProcessEnv = preparePlanEnvForShell({
1642
1598
  projectId: input.projectId,
1643
1599
  branchName: input.message.branchName,
1644
1600
  planRoot: input.planRoot,
@@ -1982,9 +1938,7 @@ async function openPty(input) {
1982
1938
  branchName: input.message.branchName,
1983
1939
  manifest: input.manifest
1984
1940
  });
1985
- const planProcessEnv = await preparePlanEnvForShell({
1986
- baseUrl: input.baseUrl,
1987
- token: input.token,
1941
+ const planProcessEnv = preparePlanEnvForShell({
1988
1942
  projectId: input.projectId,
1989
1943
  branchName: input.message.branchName,
1990
1944
  planRoot: input.planRoot,
@@ -2180,19 +2134,6 @@ async function startWorker(options) {
2180
2134
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2181
2135
  workspaceSyncRequestsInFlight += 1;
2182
2136
  try {
2183
- if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
2184
- for (const manifest of manifestByProjectId.values()) {
2185
- for (const branchName of manifest.branches) {
2186
- await syncProjectPlans({
2187
- baseUrl,
2188
- token,
2189
- projectId: manifest.projectId,
2190
- branchName,
2191
- planRoot
2192
- });
2193
- }
2194
- }
2195
- }
2196
2137
  const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2197
2138
  const result = await workspaceSyncSingleFlight.run(
2198
2139
  workspaceSyncInput(trigger, {
@@ -2673,6 +2614,7 @@ if (isCliEntrypoint()) {
2673
2614
  }
2674
2615
  // Annotate the CommonJS export names for ESM import in node:
2675
2616
  0 && (module.exports = {
2617
+ editWorkerTextFile,
2676
2618
  ensureVisibleGitCheckout,
2677
2619
  findWorkerFiles,
2678
2620
  githubCliEnv,
@@ -2684,8 +2626,7 @@ if (isCliEntrypoint()) {
2684
2626
  readWorkerImageFile,
2685
2627
  readWorkerTextFile,
2686
2628
  resolveHostShell,
2687
- resolveProjectFilePath,
2688
2629
  resolveWorkerFilePath,
2689
- syncProjectPlans,
2690
- syncSessionArtifacts
2630
+ syncSessionArtifacts,
2631
+ writeWorkerTextFile
2691
2632
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -299,15 +299,6 @@ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
299
299
  function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
300
300
  return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
301
301
  }
302
- function planApiBasePath(projectId, branchName) {
303
- return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/plans`;
304
- }
305
- function planManifestUrl(baseUrl, projectId, branchName) {
306
- return new URL(planApiBasePath(projectId, branchName), baseUrl);
307
- }
308
- function planDownloadUrl(baseUrl, projectId, branchName, filename) {
309
- return new URL(`${planApiBasePath(projectId, branchName)}/${encodeURIComponent(filename)}`, baseUrl);
310
- }
311
302
  function sha256Path(filePath) {
312
303
  return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
313
304
  }
@@ -420,30 +411,6 @@ async function syncSessionArtifacts(input) {
420
411
  })
421
412
  });
422
413
  }
423
- async function fetchProjectPlanManifest(input) {
424
- return fetchRemoteFileManifest({
425
- url: planManifestUrl(input.baseUrl, input.projectId, input.branchName),
426
- token: input.token,
427
- collectionKey: "plans",
428
- label: "plan"
429
- });
430
- }
431
- async function syncProjectPlans(input) {
432
- const targetDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
433
- const plans = await fetchProjectPlanManifest(input);
434
- return syncRemoteFileSet({
435
- targetDir,
436
- files: plans,
437
- label: "Plan path",
438
- download: (plan) => downloadRemoteSyncedFile({
439
- url: planDownloadUrl(input.baseUrl, input.projectId, input.branchName, plan.filename),
440
- token: input.token,
441
- targetDir,
442
- file: plan,
443
- label: "plan"
444
- })
445
- });
446
- }
447
414
  async function resolveArtifactEnvFilePath(input) {
448
415
  if (!isArtifactEnvPath(input.filePath)) {
449
416
  return null;
@@ -503,15 +470,7 @@ async function prepareArtifactEnvForShell(input) {
503
470
  R5D_ARTIFACTS_DIR: artifactsDir
504
471
  };
505
472
  }
506
- async function preparePlanEnvForShell(input) {
507
- try {
508
- await syncProjectPlans(input);
509
- } catch (error) {
510
- process.stderr.write(
511
- `[r5d-worker] plan sync skipped for ${input.projectId}/${input.branchName}: ${error instanceof Error ? error.message : String(error)}
512
- `
513
- );
514
- }
473
+ function preparePlanEnvForShell(input) {
515
474
  const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
516
475
  fs.mkdirSync(plansDir, { recursive: true });
517
476
  return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
@@ -632,9 +591,6 @@ function toProjectDisplayPath(branchPath, absolutePath) {
632
591
  return relative || ".";
633
592
  }
634
593
  function assertAllowedProjectPath(repoRelativePath, inputPath) {
635
- if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
636
- throw new Error("Refusing to access .git through worker file tools");
637
- }
638
594
  if (repoRelativePath.split("/").includes("..")) {
639
595
  throw new Error(`Invalid project file path: ${inputPath}`);
640
596
  }
@@ -682,13 +638,6 @@ function resolveWorkerFilePath(branchPath, inputPath) {
682
638
  scope: "project"
683
639
  };
684
640
  }
685
- function resolveProjectFilePath(branchPath, inputPath) {
686
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
687
- if (resolved.scope === "host") {
688
- throw new Error(`Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`);
689
- }
690
- return resolved;
691
- }
692
641
  function fallbackInternalRemoteUrl(baseUrl, projectId) {
693
642
  return new URL(`/git/${projectId}.git`, baseUrl).toString();
694
643
  }
@@ -1105,8 +1054,11 @@ async function withFileMutationQueue(key, operation) {
1105
1054
  }
1106
1055
  }
1107
1056
  }
1108
- function mutationQueueKey(projectId, branchName, filePath) {
1109
- return `${projectId}:${branchName}:${path.posix.normalize(filePath)}`;
1057
+ function mutationQueueKey(projectId, branchName, resolved) {
1058
+ if (resolved.scope === "host") {
1059
+ return `host:${path.normalize(resolved.absolutePath)}`;
1060
+ }
1061
+ return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
1110
1062
  }
1111
1063
  function readWorkerTextFile(branchPath, filePath, offset, limit) {
1112
1064
  const resolved = resolveWorkerFilePath(branchPath, filePath);
@@ -1123,6 +1075,63 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1123
1075
  ...formatted
1124
1076
  };
1125
1077
  }
1078
+ function writeWorkerTextFile(branchPath, filePath, content) {
1079
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1080
+ fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1081
+ fs.writeFileSync(resolved.absolutePath, content, "utf8");
1082
+ return {
1083
+ type: "write",
1084
+ file: resolved.displayPath,
1085
+ gitBlobHash: getGitBlobHashForContent(content)
1086
+ };
1087
+ }
1088
+ function editWorkerTextFile(branchPath, filePath, edits) {
1089
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1090
+ if (!Array.isArray(edits) || edits.length === 0) {
1091
+ throw new Error("edit requires at least one replacement");
1092
+ }
1093
+ if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1094
+ throw new Error(`File not found: ${resolved.displayPath}`);
1095
+ }
1096
+ const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
1097
+ const ranges = edits.map((edit, index) => {
1098
+ if (edit.oldText === edit.newText) {
1099
+ throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
1100
+ }
1101
+ if (edit.oldText.length === 0) {
1102
+ throw new Error(`edits[${index}].oldText cannot be empty`);
1103
+ }
1104
+ const occurrences = originalContent.split(edit.oldText).length - 1;
1105
+ if (occurrences === 0) {
1106
+ throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1107
+ }
1108
+ if (occurrences > 1) {
1109
+ throw new Error(
1110
+ `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1111
+ );
1112
+ }
1113
+ const start = originalContent.indexOf(edit.oldText);
1114
+ return { index, start, end: start + edit.oldText.length, edit };
1115
+ });
1116
+ const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
1117
+ for (let index = 1; index < sortedRanges.length; index += 1) {
1118
+ const previous = sortedRanges[index - 1];
1119
+ const current = sortedRanges[index];
1120
+ if (current.start < previous.end) {
1121
+ throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
1122
+ }
1123
+ }
1124
+ let nextContent = originalContent;
1125
+ for (const range of [...sortedRanges].reverse()) {
1126
+ nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
1127
+ }
1128
+ fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1129
+ return {
1130
+ type: "edit",
1131
+ file: resolved.displayPath,
1132
+ edits: edits.length
1133
+ };
1134
+ }
1126
1135
  async function executeReadFileOperation(input) {
1127
1136
  const artifact = await resolveArtifactEnvFilePath({
1128
1137
  filePath: input.message.filePath,
@@ -1153,66 +1162,17 @@ async function executeReadFileOperation(input) {
1153
1162
  async function executeWriteFileOperation(input) {
1154
1163
  rejectArtifactWritePath(input.message.filePath);
1155
1164
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1156
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1157
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1158
- fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1159
- fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1160
- return {
1161
- type: "write",
1162
- file: resolved.displayPath,
1163
- gitBlobHash: getGitBlobHashForContent(input.message.content)
1164
- };
1165
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1166
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1167
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1165
1168
  });
1166
1169
  }
1167
1170
  async function executeEditFileOperation(input) {
1168
1171
  rejectArtifactWritePath(input.message.filePath);
1169
1172
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1170
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1171
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1172
- if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1173
- throw new Error("edit requires at least one replacement");
1174
- }
1175
- if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1176
- throw new Error(`File not found: ${resolved.displayPath}`);
1177
- }
1178
- const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
1179
- const ranges = input.message.edits.map((edit, index) => {
1180
- if (edit.oldText === edit.newText) {
1181
- throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
1182
- }
1183
- if (edit.oldText.length === 0) {
1184
- throw new Error(`edits[${index}].oldText cannot be empty`);
1185
- }
1186
- const occurrences = originalContent.split(edit.oldText).length - 1;
1187
- if (occurrences === 0) {
1188
- throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
1189
- }
1190
- if (occurrences > 1) {
1191
- throw new Error(
1192
- `edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`
1193
- );
1194
- }
1195
- const start = originalContent.indexOf(edit.oldText);
1196
- return { index, start, end: start + edit.oldText.length, edit };
1197
- });
1198
- const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
1199
- for (let index = 1; index < sortedRanges.length; index += 1) {
1200
- const previous = sortedRanges[index - 1];
1201
- const current = sortedRanges[index];
1202
- if (current.start < previous.end) {
1203
- throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
1204
- }
1205
- }
1206
- let nextContent = originalContent;
1207
- for (const range of [...sortedRanges].reverse()) {
1208
- nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
1209
- }
1210
- fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1211
- return {
1212
- type: "edit",
1213
- file: resolved.displayPath,
1214
- edits: input.message.edits.length
1215
- };
1173
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1174
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1175
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1216
1176
  });
1217
1177
  }
1218
1178
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1489,9 +1449,7 @@ async function executeCommand(input) {
1489
1449
  sessionId: input.message.sessionId,
1490
1450
  artifactRoot: input.artifactRoot
1491
1451
  });
1492
- const planProcessEnv = await preparePlanEnvForShell({
1493
- baseUrl: input.baseUrl,
1494
- token: input.token,
1452
+ const planProcessEnv = preparePlanEnvForShell({
1495
1453
  projectId: input.projectId,
1496
1454
  branchName: input.message.branchName,
1497
1455
  planRoot: input.planRoot,
@@ -1600,9 +1558,7 @@ async function executeStreamingCommand(input) {
1600
1558
  sessionId: input.message.sessionId,
1601
1559
  artifactRoot: input.artifactRoot
1602
1560
  });
1603
- const planProcessEnv = await preparePlanEnvForShell({
1604
- baseUrl: input.baseUrl,
1605
- token: input.token,
1561
+ const planProcessEnv = preparePlanEnvForShell({
1606
1562
  projectId: input.projectId,
1607
1563
  branchName: input.message.branchName,
1608
1564
  planRoot: input.planRoot,
@@ -1946,9 +1902,7 @@ async function openPty(input) {
1946
1902
  branchName: input.message.branchName,
1947
1903
  manifest: input.manifest
1948
1904
  });
1949
- const planProcessEnv = await preparePlanEnvForShell({
1950
- baseUrl: input.baseUrl,
1951
- token: input.token,
1905
+ const planProcessEnv = preparePlanEnvForShell({
1952
1906
  projectId: input.projectId,
1953
1907
  branchName: input.message.branchName,
1954
1908
  planRoot: input.planRoot,
@@ -2144,19 +2098,6 @@ async function startWorker(options) {
2144
2098
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2145
2099
  workspaceSyncRequestsInFlight += 1;
2146
2100
  try {
2147
- if (trigger.type === "plan_write" || trigger.type === "task_status" || trigger.type === "connect" || trigger.type === "inbound_head" || trigger.type === "manual") {
2148
- for (const manifest of manifestByProjectId.values()) {
2149
- for (const branchName of manifest.branches) {
2150
- await syncProjectPlans({
2151
- baseUrl,
2152
- token,
2153
- projectId: manifest.projectId,
2154
- branchName,
2155
- planRoot
2156
- });
2157
- }
2158
- }
2159
- }
2160
2101
  const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2161
2102
  const result = await workspaceSyncSingleFlight.run(
2162
2103
  workspaceSyncInput(trigger, {
@@ -2636,6 +2577,7 @@ if (isCliEntrypoint()) {
2636
2577
  });
2637
2578
  }
2638
2579
  export {
2580
+ editWorkerTextFile,
2639
2581
  ensureVisibleGitCheckout,
2640
2582
  findWorkerFiles,
2641
2583
  githubCliEnv,
@@ -2647,8 +2589,7 @@ export {
2647
2589
  readWorkerImageFile,
2648
2590
  readWorkerTextFile,
2649
2591
  resolveHostShell,
2650
- resolveProjectFilePath,
2651
2592
  resolveWorkerFilePath,
2652
- syncProjectPlans,
2653
- syncSessionArtifacts
2593
+ syncSessionArtifacts,
2594
+ writeWorkerTextFile
2654
2595
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "type": "module"
5
5
  }
@@ -11,6 +11,16 @@ type WorkerReadFileResult = {
11
11
  truncated: boolean;
12
12
  continuation?: string;
13
13
  };
14
+ type WorkerWriteFileResult = {
15
+ type: "write";
16
+ file: string;
17
+ gitBlobHash?: string;
18
+ };
19
+ type WorkerEditFileResult = {
20
+ type: "edit";
21
+ file: string;
22
+ edits: number;
23
+ };
14
24
  type WorkerGrepResult = {
15
25
  type: "grep";
16
26
  content: string;
@@ -48,13 +58,6 @@ export declare function syncSessionArtifacts(input: {
48
58
  sessionId: string;
49
59
  artifactRoot: string;
50
60
  }): Promise<string>;
51
- export declare function syncProjectPlans(input: {
52
- baseUrl: string;
53
- token: string;
54
- projectId: string;
55
- branchName: string;
56
- planRoot: string;
57
- }): Promise<string>;
58
61
  export declare function prepareArtifactEnvForShell(input: {
59
62
  baseUrl: string;
60
63
  token: string;
@@ -64,13 +67,11 @@ export declare function prepareArtifactEnvForShell(input: {
64
67
  artifactRoot: string;
65
68
  }): Promise<Record<string, string>>;
66
69
  export declare function preparePlanEnvForShell(input: {
67
- baseUrl: string;
68
- token: string;
69
70
  projectId: string;
70
71
  branchName: string;
71
72
  planRoot: string;
72
73
  activePlanId?: string;
73
- }): Promise<Record<string, string>>;
74
+ }): Record<string, string>;
74
75
  type GitAuth = {
75
76
  extraHeaderUrl: string;
76
77
  header: string;
@@ -87,9 +88,6 @@ type ResolvedWorkerFilePath = {
87
88
  scope: "host";
88
89
  };
89
90
  export declare function resolveWorkerFilePath(branchPath: string, inputPath: string): ResolvedWorkerFilePath;
90
- export declare function resolveProjectFilePath(branchPath: string, inputPath: string): Extract<ResolvedWorkerFilePath, {
91
- scope: "project";
92
- }>;
93
91
  export declare function githubCliEnv(token: string | null | undefined): Record<string, string>;
94
92
  export declare function ensureVisibleGitCheckout(input: {
95
93
  projectRoot: string;
@@ -101,6 +99,11 @@ export declare function ensureVisibleGitCheckout(input: {
101
99
  reconcileExistingOrigin?: boolean;
102
100
  }): string;
103
101
  export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number): WorkerReadFileResult;
102
+ export declare function writeWorkerTextFile(branchPath: string, filePath: string, content: string): WorkerWriteFileResult;
103
+ export declare function editWorkerTextFile(branchPath: string, filePath: string, edits: Array<{
104
+ oldText: string;
105
+ newText: string;
106
+ }>): WorkerEditFileResult;
104
107
  export declare function grepWorkerFiles(branchPath: string, input: {
105
108
  pattern: string;
106
109
  path?: string;
@@ -12,7 +12,7 @@ export type WorkspaceProjectManifestEntry = {
12
12
  branches: string[];
13
13
  };
14
14
  export type WorkspaceSyncTrigger = {
15
- type: "connect" | "inbound_head" | "write" | "edit" | "plan_write" | "task_status" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
15
+ type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
16
16
  sessionId?: string;
17
17
  processRunId?: string;
18
18
  toolCallId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",