@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/cjs/main.cjs CHANGED
@@ -282,7 +282,25 @@ async function readResponseText(response) {
282
282
  }
283
283
  }
284
284
  const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
285
+ const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
286
+ const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
285
287
  const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
288
+ const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
289
+ function parseBuiltInToolPath(inputPath) {
290
+ const normalized = inputPath.replace(/\\/g, "/");
291
+ const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
292
+ if (!match) {
293
+ return null;
294
+ }
295
+ const ref = `$${match[1] ?? match[2]}`;
296
+ if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
297
+ throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
298
+ }
299
+ return {
300
+ ref,
301
+ relativePath: match[3] ?? ""
302
+ };
303
+ }
286
304
  function validateArtifactSessionId(sessionId) {
287
305
  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)) {
288
306
  throw new Error(`Invalid artifact session id: ${sessionId}`);
@@ -291,7 +309,7 @@ function validateArtifactSessionId(sessionId) {
291
309
  function assertInsideRoot(rootPath, candidatePath, label) {
292
310
  const relative = import_node_path.default.relative(rootPath, candidatePath);
293
311
  if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
294
- throw new Error(`${label} escapes sync directory`);
312
+ throw new Error(`${label} escapes its allowed root`);
295
313
  }
296
314
  }
297
315
  function sessionArtifactDir(artifactRoot, sessionId) {
@@ -324,7 +342,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
324
342
  return env;
325
343
  }
326
344
  function isArtifactEnvPath(filePath) {
327
- return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
345
+ try {
346
+ return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
347
+ } catch {
348
+ return false;
349
+ }
328
350
  }
329
351
  function artifactApiBasePath(projectId, branchName, sessionId) {
330
352
  return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
@@ -458,40 +480,45 @@ async function syncSessionArtifacts(input) {
458
480
  void queued.then(release, release);
459
481
  return queued;
460
482
  }
461
- async function resolveArtifactEnvFilePath(input) {
462
- if (!isArtifactEnvPath(input.filePath)) {
463
- return null;
483
+ async function prepareBuiltInToolPaths(input) {
484
+ const parsed = parseBuiltInToolPath(input.filePath);
485
+ if (!parsed) {
486
+ return void 0;
464
487
  }
465
- if (!input.sessionId) {
466
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
488
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
489
+ if (input.access === "write") {
490
+ throw new Error(
491
+ `${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
492
+ );
493
+ }
494
+ if (!input.sessionId) {
495
+ throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
496
+ }
497
+ return {
498
+ artifactsDir: await syncSessionArtifacts({
499
+ baseUrl: input.baseUrl,
500
+ token: input.token,
501
+ projectId: input.projectId,
502
+ branchName: input.branchName,
503
+ sessionId: input.sessionId,
504
+ artifactRoot: input.artifactRoot
505
+ })
506
+ };
467
507
  }
468
- if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
469
- throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
508
+ const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
509
+ import_node_fs.default.mkdirSync(plansDir, { recursive: true });
510
+ if (parsed.ref === R5D_PLANS_DIR_REF) {
511
+ return { plansDir };
470
512
  }
471
- const targetDir = await syncSessionArtifacts({
472
- baseUrl: input.baseUrl,
473
- token: input.token,
474
- projectId: input.projectId,
475
- branchName: input.branchName,
476
- sessionId: input.sessionId,
477
- artifactRoot: input.artifactRoot
478
- });
479
- const relativePath = import_node_path.default.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
480
- if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
481
- throw new Error(`Invalid artifact path: ${input.filePath}`);
513
+ if (!input.activePlanId) {
514
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
482
515
  }
483
- const absolutePath = import_node_path.default.resolve(targetDir, ...relativePath.split("/"));
484
- assertInsideRoot(targetDir, absolutePath, "Artifact path");
516
+ validatePlanId(input.activePlanId);
485
517
  return {
486
- absolutePath,
487
- virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
518
+ plansDir,
519
+ activePlanFile: import_node_path.default.join(plansDir, `${input.activePlanId}.plan.md`)
488
520
  };
489
521
  }
490
- function rejectArtifactWritePath(filePath) {
491
- if (isArtifactEnvPath(filePath)) {
492
- throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
493
- }
494
- }
495
522
  async function prepareArtifactEnvForShell(input) {
496
523
  if (!input.sessionId) {
497
524
  return {};
@@ -642,10 +669,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
642
669
  throw new Error(`Invalid project file path: ${inputPath}`);
643
670
  }
644
671
  }
645
- function resolveWorkerFilePath(branchPath, inputPath) {
672
+ function assertPathInsideRealRoot(rootPath, candidatePath, label) {
673
+ const resolvedRoot = import_node_path.default.resolve(rootPath);
674
+ const resolvedCandidate = import_node_path.default.resolve(candidatePath);
675
+ assertInsideRoot(resolvedRoot, resolvedCandidate, label);
676
+ let existingPath = resolvedCandidate;
677
+ while (!import_node_fs.default.existsSync(existingPath)) {
678
+ const parentPath = import_node_path.default.dirname(existingPath);
679
+ if (parentPath === existingPath) {
680
+ throw new Error(`${label} root does not exist: ${rootPath}`);
681
+ }
682
+ existingPath = parentPath;
683
+ }
684
+ const realRoot = import_node_fs.default.realpathSync(resolvedRoot);
685
+ const realExistingPath = import_node_fs.default.realpathSync(existingPath);
686
+ assertInsideRoot(realRoot, realExistingPath, label);
687
+ }
688
+ function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
689
+ if (parsed.relativePath.split("/").includes("..")) {
690
+ throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
691
+ }
692
+ let rootPath;
693
+ let absolutePath;
694
+ if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
695
+ rootPath = builtInPaths?.artifactsDir;
696
+ } else if (parsed.ref === R5D_PLANS_DIR_REF) {
697
+ rootPath = builtInPaths?.plansDir;
698
+ } else {
699
+ if (parsed.relativePath) {
700
+ throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
701
+ }
702
+ rootPath = builtInPaths?.plansDir;
703
+ absolutePath = builtInPaths?.activePlanFile;
704
+ }
705
+ if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
706
+ const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
707
+ throw new Error(`${parsed.ref} paths require ${requirement}`);
708
+ }
709
+ const normalizedPath = parsed.relativePath ? import_node_path.default.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
710
+ const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
711
+ absolutePath ??= normalizedRelativePath ? import_node_path.default.resolve(rootPath, ...normalizedRelativePath.split("/")) : import_node_path.default.resolve(rootPath);
712
+ assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
713
+ return {
714
+ absolutePath,
715
+ displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
716
+ repoRelativePath: null,
717
+ scope: "virtual",
718
+ virtualRootPath: import_node_path.default.resolve(rootPath)
719
+ };
720
+ }
721
+ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
646
722
  if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
647
723
  throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
648
724
  }
725
+ const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
726
+ if (parsedBuiltInPath) {
727
+ return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
728
+ }
649
729
  const resolvedBranchPath = import_node_path.default.resolve(branchPath);
650
730
  if (import_node_path.default.isAbsolute(inputPath)) {
651
731
  const absolutePath2 = import_node_path.default.resolve(inputPath);
@@ -1102,13 +1182,13 @@ async function withFileMutationQueue(key, operation) {
1102
1182
  }
1103
1183
  }
1104
1184
  function mutationQueueKey(projectId, branchName, resolved) {
1105
- if (resolved.scope === "host") {
1106
- return `host:${import_node_path.default.normalize(resolved.absolutePath)}`;
1185
+ if (resolved.scope !== "project") {
1186
+ return `${resolved.scope}:${import_node_path.default.normalize(resolved.absolutePath)}`;
1107
1187
  }
1108
1188
  return `project:${projectId}:${branchName}:${import_node_path.default.posix.normalize(resolved.repoRelativePath)}`;
1109
1189
  }
1110
- function readWorkerTextFile(branchPath, filePath, offset, limit) {
1111
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1190
+ function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
1191
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1112
1192
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1113
1193
  throw new Error(`File not found: ${resolved.displayPath}`);
1114
1194
  }
@@ -1122,8 +1202,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1122
1202
  ...formatted
1123
1203
  };
1124
1204
  }
1125
- function writeWorkerTextFile(branchPath, filePath, content) {
1126
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1205
+ function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
1206
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1127
1207
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1128
1208
  import_node_fs.default.writeFileSync(resolved.absolutePath, content, "utf8");
1129
1209
  return {
@@ -1132,8 +1212,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
1132
1212
  gitBlobHash: getGitBlobHashForContent(content)
1133
1213
  };
1134
1214
  }
1135
- function editWorkerTextFile(branchPath, filePath, edits) {
1136
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1215
+ function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
1216
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1137
1217
  if (!Array.isArray(edits) || edits.length === 0) {
1138
1218
  throw new Error("edit requires at least one replacement");
1139
1219
  }
@@ -1180,46 +1260,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
1180
1260
  };
1181
1261
  }
1182
1262
  async function executeReadFileOperation(input) {
1183
- const artifact = await resolveArtifactEnvFilePath({
1263
+ const builtInPaths = await prepareBuiltInToolPaths({
1184
1264
  filePath: input.message.filePath,
1185
1265
  baseUrl: input.baseUrl,
1186
1266
  token: input.token,
1187
1267
  projectId: input.projectId,
1188
1268
  branchName: input.message.branchName,
1189
1269
  sessionId: input.message.sessionId,
1190
- artifactRoot: input.artifactRoot
1270
+ activePlanId: input.message.activePlanId,
1271
+ artifactRoot: input.artifactRoot,
1272
+ planRoot: input.planRoot,
1273
+ access: "read"
1191
1274
  });
1192
- if (artifact) {
1193
- if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1194
- throw new Error(`File not found: ${artifact.virtualPath}`);
1195
- }
1196
- const buffer = import_node_fs.default.readFileSync(artifact.absolutePath);
1197
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1198
- return {
1199
- type: "read",
1200
- kind: "text",
1201
- file: artifact.virtualPath,
1202
- gitBlobHash: getGitBlobHashForContent(buffer),
1203
- ...formatted
1204
- };
1205
- }
1206
1275
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1207
- return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1276
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
1208
1277
  }
1209
1278
  async function executeWriteFileOperation(input) {
1210
- rejectArtifactWritePath(input.message.filePath);
1279
+ const builtInPaths = await prepareBuiltInToolPaths({
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
+ activePlanId: input.message.activePlanId,
1287
+ artifactRoot: input.artifactRoot,
1288
+ planRoot: input.planRoot,
1289
+ access: "write"
1290
+ });
1211
1291
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1212
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1292
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1213
1293
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1214
- return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
1294
+ return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
1215
1295
  });
1216
1296
  }
1217
1297
  async function executeEditFileOperation(input) {
1218
- rejectArtifactWritePath(input.message.filePath);
1298
+ const builtInPaths = await prepareBuiltInToolPaths({
1299
+ filePath: input.message.filePath,
1300
+ baseUrl: input.baseUrl,
1301
+ token: input.token,
1302
+ projectId: input.projectId,
1303
+ branchName: input.message.branchName,
1304
+ sessionId: input.message.sessionId,
1305
+ activePlanId: input.message.activePlanId,
1306
+ artifactRoot: input.artifactRoot,
1307
+ planRoot: input.planRoot,
1308
+ access: "write"
1309
+ });
1219
1310
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1220
- const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
1311
+ const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
1221
1312
  return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
1222
- return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
1313
+ return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
1223
1314
  });
1224
1315
  }
1225
1316
  const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
@@ -1240,16 +1331,20 @@ function normalizeFindPattern(pattern) {
1240
1331
  function walkWorkerEntries(branchPath, start) {
1241
1332
  const entries = [];
1242
1333
  const visitedDirectories = /* @__PURE__ */ new Set();
1334
+ const realVirtualRoot = start.scope === "virtual" ? import_node_fs.default.realpathSync(start.virtualRootPath) : null;
1243
1335
  const visit = (absolutePath, isRoot) => {
1244
1336
  let stat;
1245
1337
  try {
1246
1338
  stat = import_node_fs.default.statSync(absolutePath);
1339
+ if (realVirtualRoot) {
1340
+ assertInsideRoot(realVirtualRoot, import_node_fs.default.realpathSync(absolutePath), `${start.displayPath} path`);
1341
+ }
1247
1342
  } catch (error) {
1248
1343
  if (isRoot) throw error;
1249
1344
  return;
1250
1345
  }
1251
- const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : import_node_path.default.resolve(absolutePath);
1252
1346
  const hostRelativePath = import_node_path.default.relative(start.absolutePath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1347
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : import_node_path.default.resolve(absolutePath);
1253
1348
  const matchPath = start.scope === "project" ? displayPath : hostRelativePath || import_node_path.default.basename(start.absolutePath);
1254
1349
  entries.push({ absolutePath, displayPath, matchPath, stat });
1255
1350
  if (!stat.isDirectory()) return;
@@ -1280,8 +1375,8 @@ function walkWorkerEntries(branchPath, start) {
1280
1375
  function isProbablyText(bytes) {
1281
1376
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1282
1377
  }
1283
- function grepWorkerFiles(branchPath, input) {
1284
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1378
+ function grepWorkerFiles(branchPath, input, builtInPaths) {
1379
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1285
1380
  if (!import_node_fs.default.existsSync(start.absolutePath)) {
1286
1381
  throw new Error(`Path not found: ${start.displayPath}`);
1287
1382
  }
@@ -1327,17 +1422,34 @@ function grepWorkerFiles(branchPath, input) {
1327
1422
  };
1328
1423
  }
1329
1424
  async function executeGrepOperation(input) {
1330
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1331
- return grepWorkerFiles(workspace.branchPath, {
1332
- pattern: input.message.pattern,
1333
- path: input.message.path,
1334
- glob: input.message.glob,
1335
- caseSensitive: input.message.caseSensitive,
1336
- limit: input.message.limit
1425
+ const toolPath = input.message.path ?? ".";
1426
+ const builtInPaths = await prepareBuiltInToolPaths({
1427
+ filePath: toolPath,
1428
+ baseUrl: input.baseUrl,
1429
+ token: input.token,
1430
+ projectId: input.projectId,
1431
+ branchName: input.message.branchName,
1432
+ sessionId: input.message.sessionId,
1433
+ activePlanId: input.message.activePlanId,
1434
+ artifactRoot: input.artifactRoot,
1435
+ planRoot: input.planRoot,
1436
+ access: "read"
1337
1437
  });
1438
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1439
+ return grepWorkerFiles(
1440
+ workspace.branchPath,
1441
+ {
1442
+ pattern: input.message.pattern,
1443
+ path: input.message.path,
1444
+ glob: input.message.glob,
1445
+ caseSensitive: input.message.caseSensitive,
1446
+ limit: input.message.limit
1447
+ },
1448
+ builtInPaths
1449
+ );
1338
1450
  }
1339
- function findWorkerFiles(branchPath, input) {
1340
- const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1451
+ function findWorkerFiles(branchPath, input, builtInPaths) {
1452
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
1341
1453
  if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
1342
1454
  throw new Error(`Directory not found: ${start.displayPath}`);
1343
1455
  }
@@ -1371,16 +1483,33 @@ function findWorkerFiles(branchPath, input) {
1371
1483
  };
1372
1484
  }
1373
1485
  async function executeFindOperation(input) {
1374
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1375
- return findWorkerFiles(workspace.branchPath, {
1376
- pattern: input.message.pattern,
1377
- path: input.message.path,
1378
- entryType: input.message.entryType,
1379
- limit: input.message.limit
1486
+ const toolPath = input.message.path ?? ".";
1487
+ const builtInPaths = await prepareBuiltInToolPaths({
1488
+ filePath: toolPath,
1489
+ baseUrl: input.baseUrl,
1490
+ token: input.token,
1491
+ projectId: input.projectId,
1492
+ branchName: input.message.branchName,
1493
+ sessionId: input.message.sessionId,
1494
+ activePlanId: input.message.activePlanId,
1495
+ artifactRoot: input.artifactRoot,
1496
+ planRoot: input.planRoot,
1497
+ access: "read"
1380
1498
  });
1499
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1500
+ return findWorkerFiles(
1501
+ workspace.branchPath,
1502
+ {
1503
+ pattern: input.message.pattern,
1504
+ path: input.message.path,
1505
+ entryType: input.message.entryType,
1506
+ limit: input.message.limit
1507
+ },
1508
+ builtInPaths
1509
+ );
1381
1510
  }
1382
- function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1383
- const resolved = resolveWorkerFilePath(branchPath, inputPath);
1511
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
1512
+ const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
1384
1513
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1385
1514
  throw new Error(`Directory not found: ${resolved.displayPath}`);
1386
1515
  }
@@ -1398,11 +1527,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1398
1527
  };
1399
1528
  }
1400
1529
  async function executeLsOperation(input) {
1530
+ const toolPath = input.message.path ?? ".";
1531
+ const builtInPaths = await prepareBuiltInToolPaths({
1532
+ filePath: toolPath,
1533
+ baseUrl: input.baseUrl,
1534
+ token: input.token,
1535
+ projectId: input.projectId,
1536
+ branchName: input.message.branchName,
1537
+ sessionId: input.message.sessionId,
1538
+ activePlanId: input.message.activePlanId,
1539
+ artifactRoot: input.artifactRoot,
1540
+ planRoot: input.planRoot,
1541
+ access: "read"
1542
+ });
1401
1543
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1402
- return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1544
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
1403
1545
  }
1404
- function readWorkerImageFile(branchPath, filePath) {
1405
- const resolved = resolveWorkerFilePath(branchPath, filePath);
1546
+ function readWorkerImageFile(branchPath, filePath, builtInPaths) {
1547
+ const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
1406
1548
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1407
1549
  throw new Error(`File not found: ${resolved.displayPath}`);
1408
1550
  }
@@ -1424,38 +1566,20 @@ function readWorkerImageFile(branchPath, filePath) {
1424
1566
  };
1425
1567
  }
1426
1568
  async function executeViewFileBytesOperation(input) {
1427
- const artifact = await resolveArtifactEnvFilePath({
1569
+ const builtInPaths = await prepareBuiltInToolPaths({
1428
1570
  filePath: input.message.filePath,
1429
1571
  baseUrl: input.baseUrl,
1430
1572
  token: input.token,
1431
1573
  projectId: input.projectId,
1432
1574
  branchName: input.message.branchName,
1433
1575
  sessionId: input.message.sessionId,
1434
- artifactRoot: input.artifactRoot
1576
+ activePlanId: input.message.activePlanId,
1577
+ artifactRoot: input.artifactRoot,
1578
+ planRoot: input.planRoot,
1579
+ access: "read"
1435
1580
  });
1436
- if (artifact) {
1437
- if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1438
- throw new Error(`File not found: ${artifact.virtualPath}`);
1439
- }
1440
- const extension = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1441
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1442
- if (!mediaType) {
1443
- throw new Error("read supports PNG and JPEG image inputs only");
1444
- }
1445
- const bytes = import_node_fs.default.readFileSync(artifact.absolutePath);
1446
- const dimensions = readImageDimensions(bytes, mediaType);
1447
- return {
1448
- type: "view_file_bytes",
1449
- filePath: artifact.virtualPath,
1450
- mediaType,
1451
- base64: bytes.toString("base64"),
1452
- fileSizeBytes: bytes.length,
1453
- width: dimensions.width,
1454
- height: dimensions.height
1455
- };
1456
- }
1457
1581
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1458
- return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1582
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
1459
1583
  }
1460
1584
  async function executeOperation(input) {
1461
1585
  switch (input.message.type) {
@@ -2094,6 +2218,7 @@ async function startWorker(options) {
2094
2218
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2095
2219
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2096
2220
  const manifestByProjectId = /* @__PURE__ */ new Map();
2221
+ const pendingManifestCheckouts = /* @__PURE__ */ new Map();
2097
2222
  const workspaceSyncSingleFlight = new import_workspace_sync.WorkspaceSyncSingleFlight();
2098
2223
  let workspaceRemoteUrl = null;
2099
2224
  let activeWorkspaceIncidentId = null;
@@ -2122,37 +2247,64 @@ async function startWorker(options) {
2122
2247
  const sendWorkspaceSyncResult = (requestId, result) => {
2123
2248
  sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
2124
2249
  };
2125
- const ensureAllVisibleWorkspaceCheckouts = () => {
2250
+ const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
2251
+ const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
2252
+ (manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
2253
+ );
2254
+ const ensureVisibleWorkspaceCheckouts = (targets) => {
2126
2255
  const created = [];
2127
- for (const manifest of manifestByProjectId.values()) {
2128
- for (const branchName of manifest.branches) {
2129
- const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId), branchName);
2130
- const existed = import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"));
2131
- const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2132
- ensureVisibleGitCheckout({
2133
- projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
2134
- branchName,
2135
- githubRemoteUrl,
2136
- githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2137
- githubAuthHeader: manifest.repoAuthHeader,
2138
- defaultBranch: manifest.defaultBranch || "main",
2139
- reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2140
- });
2141
- if (!existed) created.push({ projectId: manifest.projectId, branchName });
2256
+ for (const target of targets) {
2257
+ const manifest = manifestByProjectId.get(target.projectId);
2258
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2259
+ if (!manifest || !manifest.branches.includes(target.branchName)) {
2260
+ pendingManifestCheckouts.delete(key);
2261
+ continue;
2142
2262
  }
2263
+ const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
2264
+ const branchPath = import_node_path.default.join(projectRoot, target.branchName);
2265
+ const existed = hasNormalVisibleGitDir(branchPath);
2266
+ const githubRemoteUrl = githubRemoteUrlFor(baseUrl, manifest.projectId, manifest);
2267
+ ensureVisibleGitCheckout({
2268
+ projectRoot,
2269
+ branchName: target.branchName,
2270
+ githubRemoteUrl,
2271
+ githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
2272
+ githubAuthHeader: manifest.repoAuthHeader,
2273
+ defaultBranch: manifest.defaultBranch || "main",
2274
+ reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
2275
+ });
2276
+ if (!existed) created.push(target);
2143
2277
  }
2144
2278
  return created;
2145
2279
  };
2146
2280
  const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
2147
2281
  workspaceSyncRequestsInFlight += 1;
2148
2282
  try {
2149
- const newVisibleCheckouts = trigger.type === "connect" ? ensureAllVisibleWorkspaceCheckouts() : [];
2150
- const result = await workspaceSyncSingleFlight.run(
2151
- workspaceSyncInput(trigger, {
2283
+ let pendingTargets = [];
2284
+ const result = await workspaceSyncSingleFlight.runPrepared(() => {
2285
+ const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
2286
+ let checkoutTargets = [];
2287
+ if (shouldEnsureCheckouts) {
2288
+ checkoutTargets = trigger.type === "connect" ? allManifestCheckouts() : [...pendingManifestCheckouts.values()];
2289
+ }
2290
+ pendingTargets = shouldEnsureCheckouts ? [...pendingManifestCheckouts.values()] : [];
2291
+ const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets);
2292
+ const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
2293
+ for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
2294
+ newVisibleCheckoutByKey.set(manifestCheckoutKey(target.projectId, target.branchName), target);
2295
+ }
2296
+ const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
2297
+ return workspaceSyncInput(trigger, {
2152
2298
  ...overrides,
2153
2299
  ...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
2154
- })
2155
- );
2300
+ });
2301
+ });
2302
+ if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
2303
+ for (const target of pendingTargets) {
2304
+ const key = manifestCheckoutKey(target.projectId, target.branchName);
2305
+ if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
2306
+ }
2307
+ }
2156
2308
  previousPeriodicFingerprint = null;
2157
2309
  sendWorkspaceSyncResult(requestId, result);
2158
2310
  return result;
@@ -2245,9 +2397,26 @@ async function startWorker(options) {
2245
2397
  configureGitHubAuth(message.githubCredential);
2246
2398
  visibleGitIdentity = message.gitIdentity;
2247
2399
  workspaceRemoteUrl = message.workspaceRemoteUrl;
2400
+ const previousCheckoutKeys = new Set(
2401
+ allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2402
+ );
2248
2403
  const migratedProjectIds = (0, import_managed_paths.migrateLegacyProjectRoots)(projectsRoot, message.projects);
2249
2404
  manifestByProjectId.clear();
2250
2405
  for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
2406
+ const currentCheckouts = allManifestCheckouts();
2407
+ const currentCheckoutKeys = new Set(
2408
+ currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
2409
+ );
2410
+ for (const key of pendingManifestCheckouts.keys()) {
2411
+ if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
2412
+ }
2413
+ for (const checkout of currentCheckouts) {
2414
+ const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
2415
+ if (previousCheckoutKeys.has(key)) continue;
2416
+ const branchPath = import_node_path.default.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
2417
+ if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
2418
+ }
2419
+ previousPeriodicFingerprint = null;
2251
2420
  process.stdout.write(
2252
2421
  `[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
2253
2422
  `
@@ -2258,8 +2427,15 @@ async function startWorker(options) {
2258
2427
  if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
2259
2428
  periodicWorkspaceScanInFlight = true;
2260
2429
  void (async () => {
2261
- const input = workspaceSyncInput({ type: "periodic" });
2262
- const fingerprint = await workspaceSyncSingleFlight.fingerprint(input);
2430
+ if (pendingManifestCheckouts.size > 0) {
2431
+ previousPeriodicFingerprint = null;
2432
+ await runRequestedWorkspaceSync(crypto.randomUUID(), {
2433
+ type: "periodic",
2434
+ detail: "workspace manifest additions"
2435
+ });
2436
+ return;
2437
+ }
2438
+ const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
2263
2439
  if (fingerprint === emptyFingerprint) {
2264
2440
  previousPeriodicFingerprint = null;
2265
2441
  return;
@@ -2526,6 +2702,7 @@ async function startWorker(options) {
2526
2702
  projectRoot,
2527
2703
  syncRoot,
2528
2704
  artifactRoot,
2705
+ planRoot,
2529
2706
  manifest
2530
2707
  });
2531
2708
  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": "commonjs"
5
5
  }