@ricsam/r5d-worker 0.0.45 → 0.0.46

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/mjs/main.mjs CHANGED
@@ -246,7 +246,25 @@ async function readResponseText(response) {
246
246
  }
247
247
  }
248
248
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
249
+ const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
250
+ const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
249
251
  const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
252
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
253
+ function parseBuiltInToolPath(inputPath) {
254
+ const normalized = inputPath.replace(/\\/g, "/");
255
+ const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
256
+ if (!match) {
257
+ return null;
258
+ }
259
+ const ref = `$${match[1] ?? match[2]}`;
260
+ if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
261
+ throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
262
+ }
263
+ return {
264
+ ref,
265
+ relativePath: match[3] ?? ""
266
+ };
267
+ }
250
268
  function validateArtifactSessionId(sessionId) {
251
269
  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
270
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -255,7 +273,7 @@ function validateArtifactSessionId(sessionId) {
255
273
  function assertInsideRoot(rootPath, candidatePath, label) {
256
274
  const relative = path.relative(rootPath, candidatePath);
257
275
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
258
- throw new Error(`${label} escapes sync directory`);
276
+ throw new Error(`${label} escapes its allowed root`);
259
277
  }
260
278
  }
261
279
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -288,7 +306,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
288
306
  return env;
289
307
  }
290
308
  function isArtifactEnvPath(filePath) {
291
- return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
309
+ try {
310
+ return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
311
+ } catch {
312
+ return false;
313
+ }
292
314
  }
293
315
  function artifactApiBasePath(projectId, branchName, sessionId) {
294
316
  return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
@@ -422,40 +444,45 @@ async function syncSessionArtifacts(input) {
422
444
  void queued.then(release, release);
423
445
  return queued;
424
446
  }
425
- async function resolveArtifactEnvFilePath(input) {
426
- if (!isArtifactEnvPath(input.filePath)) {
427
- return null;
447
+ async function prepareBuiltInToolPaths(input) {
448
+ const parsed = parseBuiltInToolPath(input.filePath);
449
+ if (!parsed) {
450
+ return void 0;
428
451
  }
429
- if (!input.sessionId) {
430
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
452
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
453
+ if (input.access === "write") {
454
+ throw new Error(
455
+ `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
456
+ );
457
+ }
458
+ if (!input.sessionId) {
459
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
460
+ }
461
+ return {
462
+ artifactsDir: await syncSessionArtifacts({
463
+ baseUrl: input.baseUrl,
464
+ token: input.token,
465
+ projectId: input.projectId,
466
+ branchName: input.branchName,
467
+ sessionId: input.sessionId,
468
+ artifactRoot: input.artifactRoot
469
+ })
470
+ };
431
471
  }
432
- if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
433
- throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
472
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
473
+ fs.mkdirSync(plansDir, { recursive: true });
474
+ if (parsed.ref === R5D_PLANS_DIR_REF) {
475
+ return { plansDir };
434
476
  }
435
- const targetDir = await syncSessionArtifacts({
436
- baseUrl: input.baseUrl,
437
- token: input.token,
438
- projectId: input.projectId,
439
- branchName: input.branchName,
440
- sessionId: input.sessionId,
441
- artifactRoot: input.artifactRoot
442
- });
443
- const relativePath = path.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
444
- if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
445
- throw new Error(`Invalid artifact path: ${input.filePath}`);
477
+ if (!input.activePlanId) {
478
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
446
479
  }
447
- const absolutePath = path.resolve(targetDir, ...relativePath.split("/"));
448
- assertInsideRoot(targetDir, absolutePath, "Artifact path");
480
+ validatePlanId(input.activePlanId);
449
481
  return {
450
- absolutePath,
451
- virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
482
+ plansDir,
483
+ activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
452
484
  };
453
485
  }
454
- function rejectArtifactWritePath(filePath) {
455
- if (isArtifactEnvPath(filePath)) {
456
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
457
- }
458
- }
459
486
  async function prepareArtifactEnvForShell(input) {
460
487
  if (!input.sessionId) {
461
488
  return {};
@@ -606,10 +633,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
606
633
  throw new Error(`Invalid project file path: ${inputPath}`);
607
634
  }
608
635
  }
609
- function resolveWorkerFilePath(branchPath, inputPath) {
636
+ function assertPathInsideRealRoot(rootPath, candidatePath, label) {
637
+ const resolvedRoot = path.resolve(rootPath);
638
+ const resolvedCandidate = path.resolve(candidatePath);
639
+ assertInsideRoot(resolvedRoot, resolvedCandidate, label);
640
+ let existingPath = resolvedCandidate;
641
+ while (!fs.existsSync(existingPath)) {
642
+ const parentPath = path.dirname(existingPath);
643
+ if (parentPath === existingPath) {
644
+ throw new Error(`${label} root does not exist: ${rootPath}`);
645
+ }
646
+ existingPath = parentPath;
647
+ }
648
+ const realRoot = fs.realpathSync(resolvedRoot);
649
+ const realExistingPath = fs.realpathSync(existingPath);
650
+ assertInsideRoot(realRoot, realExistingPath, label);
651
+ }
652
+ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
653
+ if (parsed.relativePath.split("/").includes("..")) {
654
+ throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
655
+ }
656
+ let rootPath;
657
+ let absolutePath;
658
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
659
+ rootPath = builtInPaths?.artifactsDir;
660
+ } else if (parsed.ref === R5D_PLANS_DIR_REF) {
661
+ rootPath = builtInPaths?.plansDir;
662
+ } else {
663
+ if (parsed.relativePath) {
664
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
665
+ }
666
+ rootPath = builtInPaths?.plansDir;
667
+ absolutePath = builtInPaths?.activePlanFile;
668
+ }
669
+ if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
670
+ const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
671
+ throw new Error(`${parsed.ref} paths require ${requirement}`);
672
+ }
673
+ const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
674
+ const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
675
+ absolutePath ??= normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
676
+ assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
677
+ return {
678
+ absolutePath,
679
+ displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
680
+ repoRelativePath: null,
681
+ scope: "virtual",
682
+ virtualRootPath: path.resolve(rootPath)
683
+ };
684
+ }
685
+ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
610
686
  if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
611
687
  throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
612
688
  }
689
+ const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
690
+ if (parsedBuiltInPath) {
691
+ return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
692
+ }
613
693
  const resolvedBranchPath = path.resolve(branchPath);
614
694
  if (path.isAbsolute(inputPath)) {
615
695
  const absolutePath2 = path.resolve(inputPath);
@@ -1066,13 +1146,13 @@ async function withFileMutationQueue(key, operation) {
1066
1146
  }
1067
1147
  }
1068
1148
  function mutationQueueKey(projectId, branchName, resolved) {
1069
- if (resolved.scope === "host") {
1070
- return `host:${path.normalize(resolved.absolutePath)}`;
1149
+ if (resolved.scope !== "project") {
1150
+ return `${resolved.scope}:${path.normalize(resolved.absolutePath)}`;
1071
1151
  }
1072
1152
  return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
1073
1153
  }
1074
- function readWorkerTextFile(branchPath, filePath, offset, limit) {
1075
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1154
+ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1155
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1076
1156
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1077
1157
  throw new Error(`File not found: ${resolved.displayPath}`);
1078
1158
  }
@@ -1086,8 +1166,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1086
1166
  ...formatted
1087
1167
  };
1088
1168
  }
1089
- function writeWorkerTextFile(branchPath, filePath, content) {
1090
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1169
+ function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
1170
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1091
1171
  fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1092
1172
  fs.writeFileSync(resolved.absolutePath, content, "utf8");
1093
1173
  return {
@@ -1096,8 +1176,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
1096
1176
  gitBlobHash: getGitBlobHashForContent(content)
1097
1177
  };
1098
1178
  }
1099
- function editWorkerTextFile(branchPath, filePath, edits) {
1100
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1179
+ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1180
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1101
1181
  if (!Array.isArray(edits) || edits.length === 0) {
1102
1182
  throw new Error("edit requires at least one replacement");
1103
1183
  }
@@ -1144,46 +1224,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
1144
1224
  };
1145
1225
  }
1146
1226
  async function executeReadFileOperation(input) {
1147
- const artifact = await resolveArtifactEnvFilePath({
1227
+ const builtInPaths = await prepareBuiltInToolPaths({
1148
1228
  filePath: input.message.filePath,
1149
1229
  baseUrl: input.baseUrl,
1150
1230
  token: input.token,
1151
1231
  projectId: input.projectId,
1152
1232
  branchName: input.message.branchName,
1153
1233
  sessionId: input.message.sessionId,
1154
- artifactRoot: input.artifactRoot
1234
+ activePlanId: input.message.activePlanId,
1235
+ artifactRoot: input.artifactRoot,
1236
+ planRoot: input.planRoot,
1237
+ access: "read"
1155
1238
  });
1156
- if (artifact) {
1157
- if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1158
- throw new Error(`File not found: ${artifact.virtualPath}`);
1159
- }
1160
- const buffer = fs.readFileSync(artifact.absolutePath);
1161
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1162
- return {
1163
- type: "read",
1164
- kind: "text",
1165
- file: artifact.virtualPath,
1166
- gitBlobHash: getGitBlobHashForContent(buffer),
1167
- ...formatted
1168
- };
1169
- }
1170
1239
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1171
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1240
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1172
1241
  }
1173
1242
  async function executeWriteFileOperation(input) {
1174
- rejectArtifactWritePath(input.message.filePath);
1243
+ const builtInPaths = await prepareBuiltInToolPaths({
1244
+ filePath: input.message.filePath,
1245
+ baseUrl: input.baseUrl,
1246
+ token: input.token,
1247
+ projectId: input.projectId,
1248
+ branchName: input.message.branchName,
1249
+ sessionId: input.message.sessionId,
1250
+ activePlanId: input.message.activePlanId,
1251
+ artifactRoot: input.artifactRoot,
1252
+ planRoot: input.planRoot,
1253
+ access: "write"
1254
+ });
1175
1255
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1176
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1256
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1177
1257
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1178
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1258
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1179
1259
  });
1180
1260
  }
1181
1261
  async function executeEditFileOperation(input) {
1182
- rejectArtifactWritePath(input.message.filePath);
1262
+ const builtInPaths = await prepareBuiltInToolPaths({
1263
+ filePath: input.message.filePath,
1264
+ baseUrl: input.baseUrl,
1265
+ token: input.token,
1266
+ projectId: input.projectId,
1267
+ branchName: input.message.branchName,
1268
+ sessionId: input.message.sessionId,
1269
+ activePlanId: input.message.activePlanId,
1270
+ artifactRoot: input.artifactRoot,
1271
+ planRoot: input.planRoot,
1272
+ access: "write"
1273
+ });
1183
1274
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1184
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1275
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1185
1276
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1186
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1277
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1187
1278
  });
1188
1279
  }
1189
1280
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1204,16 +1295,20 @@ function normalizeFindPattern(pattern) {
1204
1295
  function walkWorkerEntries(branchPath, start) {
1205
1296
  const entries = [];
1206
1297
  const visitedDirectories = /* @__PURE__ */ new Set();
1298
+ const realVirtualRoot = start.scope === "virtual" ? fs.realpathSync(start.virtualRootPath) : null;
1207
1299
  const visit = (absolutePath, isRoot) => {
1208
1300
  let stat;
1209
1301
  try {
1210
1302
  stat = fs.statSync(absolutePath);
1303
+ if (realVirtualRoot) {
1304
+ assertInsideRoot(realVirtualRoot, fs.realpathSync(absolutePath), `${start.displayPath} path`);
1305
+ }
1211
1306
  } catch (error) {
1212
1307
  if (isRoot) throw error;
1213
1308
  return;
1214
1309
  }
1215
- const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
1216
1310
  const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
1311
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : path.resolve(absolutePath);
1217
1312
  const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
1218
1313
  entries.push({ absolutePath, displayPath, matchPath, stat });
1219
1314
  if (!stat.isDirectory()) return;
@@ -1244,8 +1339,8 @@ function walkWorkerEntries(branchPath, start) {
1244
1339
  function isProbablyText(bytes) {
1245
1340
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1246
1341
  }
1247
- function grepWorkerFiles(branchPath, input) {
1248
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1342
+ function grepWorkerFiles(branchPath, input, builtInPaths) {
1343
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1249
1344
  if (!fs.existsSync(start.absolutePath)) {
1250
1345
  throw new Error(`Path not found: ${start.displayPath}`);
1251
1346
  }
@@ -1291,17 +1386,34 @@ function grepWorkerFiles(branchPath, input) {
1291
1386
  };
1292
1387
  }
1293
1388
  async function executeGrepOperation(input) {
1294
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1295
- return grepWorkerFiles(workspace.branchPath, {
1296
- pattern: input.message.pattern,
1297
- path: input.message.path,
1298
- glob: input.message.glob,
1299
- caseSensitive: input.message.caseSensitive,
1300
- limit: input.message.limit
1389
+ const toolPath = input.message.path ?? ".";
1390
+ const builtInPaths = await prepareBuiltInToolPaths({
1391
+ filePath: toolPath,
1392
+ baseUrl: input.baseUrl,
1393
+ token: input.token,
1394
+ projectId: input.projectId,
1395
+ branchName: input.message.branchName,
1396
+ sessionId: input.message.sessionId,
1397
+ activePlanId: input.message.activePlanId,
1398
+ artifactRoot: input.artifactRoot,
1399
+ planRoot: input.planRoot,
1400
+ access: "read"
1301
1401
  });
1402
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1403
+ return grepWorkerFiles(
1404
+ workspace.branchPath,
1405
+ {
1406
+ pattern: input.message.pattern,
1407
+ path: input.message.path,
1408
+ glob: input.message.glob,
1409
+ caseSensitive: input.message.caseSensitive,
1410
+ limit: input.message.limit
1411
+ },
1412
+ builtInPaths
1413
+ );
1302
1414
  }
1303
- function findWorkerFiles(branchPath, input) {
1304
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1415
+ function findWorkerFiles(branchPath, input, builtInPaths) {
1416
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1305
1417
  if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
1306
1418
  throw new Error(`Directory not found: ${start.displayPath}`);
1307
1419
  }
@@ -1335,16 +1447,33 @@ function findWorkerFiles(branchPath, input) {
1335
1447
  };
1336
1448
  }
1337
1449
  async function executeFindOperation(input) {
1338
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1339
- return findWorkerFiles(workspace.branchPath, {
1340
- pattern: input.message.pattern,
1341
- path: input.message.path,
1342
- entryType: input.message.entryType,
1343
- limit: input.message.limit
1450
+ const toolPath = input.message.path ?? ".";
1451
+ const builtInPaths = await prepareBuiltInToolPaths({
1452
+ filePath: toolPath,
1453
+ baseUrl: input.baseUrl,
1454
+ token: input.token,
1455
+ projectId: input.projectId,
1456
+ branchName: input.message.branchName,
1457
+ sessionId: input.message.sessionId,
1458
+ activePlanId: input.message.activePlanId,
1459
+ artifactRoot: input.artifactRoot,
1460
+ planRoot: input.planRoot,
1461
+ access: "read"
1344
1462
  });
1463
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1464
+ return findWorkerFiles(
1465
+ workspace.branchPath,
1466
+ {
1467
+ pattern: input.message.pattern,
1468
+ path: input.message.path,
1469
+ entryType: input.message.entryType,
1470
+ limit: input.message.limit
1471
+ },
1472
+ builtInPaths
1473
+ );
1345
1474
  }
1346
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1347
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
1475
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
1476
+ const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
1348
1477
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
1349
1478
  throw new Error(`Directory not found: ${resolved.displayPath}`);
1350
1479
  }
@@ -1362,11 +1491,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1362
1491
  };
1363
1492
  }
1364
1493
  async function executeLsOperation(input) {
1494
+ const toolPath = input.message.path ?? ".";
1495
+ const builtInPaths = await prepareBuiltInToolPaths({
1496
+ filePath: toolPath,
1497
+ baseUrl: input.baseUrl,
1498
+ token: input.token,
1499
+ projectId: input.projectId,
1500
+ branchName: input.message.branchName,
1501
+ sessionId: input.message.sessionId,
1502
+ activePlanId: input.message.activePlanId,
1503
+ artifactRoot: input.artifactRoot,
1504
+ planRoot: input.planRoot,
1505
+ access: "read"
1506
+ });
1365
1507
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1366
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1508
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1367
1509
  }
1368
- function readWorkerImageFile(branchPath, filePath) {
1369
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1510
+ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1511
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1370
1512
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1371
1513
  throw new Error(`File not found: ${resolved.displayPath}`);
1372
1514
  }
@@ -1388,38 +1530,20 @@ function readWorkerImageFile(branchPath, filePath) {
1388
1530
  };
1389
1531
  }
1390
1532
  async function executeViewFileBytesOperation(input) {
1391
- const artifact = await resolveArtifactEnvFilePath({
1533
+ const builtInPaths = await prepareBuiltInToolPaths({
1392
1534
  filePath: input.message.filePath,
1393
1535
  baseUrl: input.baseUrl,
1394
1536
  token: input.token,
1395
1537
  projectId: input.projectId,
1396
1538
  branchName: input.message.branchName,
1397
1539
  sessionId: input.message.sessionId,
1398
- artifactRoot: input.artifactRoot
1540
+ activePlanId: input.message.activePlanId,
1541
+ artifactRoot: input.artifactRoot,
1542
+ planRoot: input.planRoot,
1543
+ access: "read"
1399
1544
  });
1400
- if (artifact) {
1401
- if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1402
- throw new Error(`File not found: ${artifact.virtualPath}`);
1403
- }
1404
- const extension = path.extname(artifact.absolutePath).toLowerCase();
1405
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1406
- if (!mediaType) {
1407
- throw new Error("read supports PNG and JPEG image inputs only");
1408
- }
1409
- const bytes = fs.readFileSync(artifact.absolutePath);
1410
- const dimensions = readImageDimensions(bytes, mediaType);
1411
- return {
1412
- type: "view_file_bytes",
1413
- filePath: artifact.virtualPath,
1414
- mediaType,
1415
- base64: bytes.toString("base64"),
1416
- fileSizeBytes: bytes.length,
1417
- width: dimensions.width,
1418
- height: dimensions.height
1419
- };
1420
- }
1421
1545
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1422
- return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1546
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1423
1547
  }
1424
1548
  async function executeOperation(input) {
1425
1549
  switch (input.message.type) {
@@ -2058,6 +2182,7 @@ async function startWorker(options) {
2058
2182
  fs.mkdirSync(artifactRoot, { recursive: true });
2059
2183
  fs.mkdirSync(planRoot, { recursive: true });
2060
2184
  const manifestByProjectId = /* @__PURE__ */ new Map();
2185
+ const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2061
2186
  const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
2062
2187
  let workspaceRemoteUrl = null;
2063
2188
  let activeWorkspaceIncidentId = null;
@@ -2086,37 +2211,64 @@ async function startWorker(options) {
2086
2211
  const sendWorkspaceSyncResult = (requestId, result) => {
2087
2212
  sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2088
2213
  };
2089
- const ensureAllVisibleWorkspaceCheckouts = () => {
2214
+ const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2215
+ const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2216
+ (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2217
+ );
2218
+ const ensureVisibleWorkspaceCheckouts = (targets) => {
2090
2219
  const created = [];
2091
- for (const manifest of manifestByProjectId.values()) {
2092
- for (const branchName of manifest.branches) {
2093
- const branchPath = path.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2094
- const existed = fs.existsSync(path.join(branchPath, ".git"));
2095
- const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2096
- ensureVisibleGitCheckout({
2097
- projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2098
- branchName,
2099
- githubRemoteUrl,
2100
- githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2101
- githubAuthHeader: manifest.repoAuthHeader,
2102
- defaultBranch: manifest.defaultBranch || "main",
2103
- reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2104
- });
2105
- if (!existed) created.push({ projectId: manifest.projectId, branchName });
2220
+ for (const target of targets) {
2221
+ const manifest = manifestByProjectId.get(target.projectId);
2222
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2223
+ if (!manifest || !manifest.branches.includes(target.branchName)) {
2224
+ pendingManifestCheckouts.delete(key);
2225
+ continue;
2106
2226
  }
2227
+ const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2228
+ const branchPath = path.join(projectRoot, target.branchName);
2229
+ const existed = hasNormalVisibleGitDir(branchPath);
2230
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2231
+ ensureVisibleGitCheckout({
2232
+ projectRoot,
2233
+ branchName: target.branchName,
2234
+ githubRemoteUrl,
2235
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2236
+ githubAuthHeader: manifest.repoAuthHeader,
2237
+ defaultBranch: manifest.defaultBranch || "main",
2238
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2239
+ });
2240
+ if (!existed) created.push(target);
2107
2241
  }
2108
2242
  return created;
2109
2243
  };
2110
2244
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2111
2245
  workspaceSyncRequestsInFlight += 1;
2112
2246
  try {
2113
- const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2114
- const result = await workspaceSyncSingleFlight.run(
2115
- workspaceSyncInput(trigger, {
2247
+ let pendingTargets = [];
2248
+ const result = await workspaceSyncSingleFlight.runPrepared(() => {
2249
+ const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2250
+ let checkoutTargets = [];
2251
+ if (shouldEnsureCheckouts) {
2252
+ checkoutTargets = trigger.type === "connect" ? allManifestCheckouts() : [...pendingManifestCheckouts.values()];
2253
+ }
2254
+ pendingTargets = shouldEnsureCheckouts ? [...pendingManifestCheckouts.values()] : [];
2255
+ const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets);
2256
+ const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2257
+ for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2258
+ newVisibleCheckoutByKey.set(manifestCheckoutKey(target.projectId, target.branchName), target);
2259
+ }
2260
+ const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2261
+ return workspaceSyncInput(trigger, {
2116
2262
  ...overrides,
2117
2263
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2118
- })
2119
- );
2264
+ });
2265
+ });
2266
+ if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
2267
+ for (const target of pendingTargets) {
2268
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2269
+ if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2270
+ }
2271
+ }
2120
2272
  previousPeriodicFingerprint = null;
2121
2273
  sendWorkspaceSyncResult(requestId, result);
2122
2274
  return result;
@@ -2209,9 +2361,26 @@ async function startWorker(options) {
2209
2361
  configureGitHubAuth(message.githubCredential);
2210
2362
  visibleGitIdentity = message.gitIdentity;
2211
2363
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2364
+ const previousCheckoutKeys = new Set(
2365
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2366
+ );
2212
2367
  const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
2213
2368
  manifestByProjectId.clear();
2214
2369
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2370
+ const currentCheckouts = allManifestCheckouts();
2371
+ const currentCheckoutKeys = new Set(
2372
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2373
+ );
2374
+ for (const key of pendingManifestCheckouts.keys()) {
2375
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2376
+ }
2377
+ for (const checkout of currentCheckouts) {
2378
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2379
+ if (previousCheckoutKeys.has(key)) continue;
2380
+ const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2381
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2382
+ }
2383
+ previousPeriodicFingerprint = null;
2215
2384
  process.stdout.write(
2216
2385
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2217
2386
  `
@@ -2222,8 +2391,15 @@ async function startWorker(options) {
2222
2391
  if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2223
2392
  periodicWorkspaceScanInFlight = true;
2224
2393
  void (async () => {
2225
- const input = workspaceSyncInput({ type: "periodic" });
2226
- const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2394
+ if (pendingManifestCheckouts.size > 0) {
2395
+ previousPeriodicFingerprint = null;
2396
+ await runRequestedWorkspaceSync(crypto.randomUUID(), {
2397
+ type: "periodic",
2398
+ detail: "workspace manifest additions"
2399
+ });
2400
+ return;
2401
+ }
2402
+ const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
2227
2403
  if (fingerprint === emptyFingerprint) {
2228
2404
  previousPeriodicFingerprint = null;
2229
2405
  return;
@@ -2490,6 +2666,7 @@ async function startWorker(options) {
2490
2666
  projectRoot,
2491
2667
  syncRoot,
2492
2668
  artifactRoot,
2669
+ planRoot,
2493
2670
  manifest
2494
2671
  });
2495
2672
  ws.send(
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.45",
3
+ "version": "0.0.46",
4
4
  "type": "module"
5
5
  }