@ricsam/r5d-worker 0.0.30 → 0.0.32

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
@@ -31,11 +31,17 @@ var main_exports = {};
31
31
  __export(main_exports, {
32
32
  ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
33
  findRepositorySyncBlockers: () => findRepositorySyncBlockers,
34
+ findWorkerFiles: () => findWorkerFiles,
35
+ grepWorkerFiles: () => grepWorkerFiles,
34
36
  isArtifactEnvPath: () => isArtifactEnvPath,
37
+ listWorkerDirectory: () => listWorkerDirectory,
35
38
  prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
36
39
  preparePlanEnvForShell: () => preparePlanEnvForShell,
40
+ readWorkerImageFile: () => readWorkerImageFile,
41
+ readWorkerTextFile: () => readWorkerTextFile,
37
42
  resolveHostShell: () => resolveHostShell,
38
43
  resolveProjectFilePath: () => resolveProjectFilePath,
44
+ resolveWorkerFilePath: () => resolveWorkerFilePath,
39
45
  syncManifestProjectsFromInternal: () => syncManifestProjectsFromInternal,
40
46
  syncProjectPlans: () => syncProjectPlans,
41
47
  syncSessionArtifacts: () => syncSessionArtifacts
@@ -706,36 +712,69 @@ function isInsideBranchPath(branchPath, filePath) {
706
712
  const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(filePath));
707
713
  return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
708
714
  }
709
- function assertInsideBranch(branchPath, filePath) {
710
- if (!isInsideBranchPath(branchPath, filePath)) {
711
- throw new Error("File path escapes the project branch");
715
+ function toProjectDisplayPath(branchPath, absolutePath) {
716
+ const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(absolutePath)).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
717
+ return relative || ".";
718
+ }
719
+ function assertAllowedProjectPath(repoRelativePath, inputPath) {
720
+ if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
721
+ throw new Error("Refusing to access .git through worker file tools");
722
+ }
723
+ if (repoRelativePath.split("/").includes("..")) {
724
+ throw new Error(`Invalid project file path: ${inputPath}`);
712
725
  }
713
726
  }
714
- function resolveProjectFilePath(branchPath, virtualPath) {
715
- if (typeof virtualPath !== "string" || !virtualPath.startsWith("/") || virtualPath.includes("\0")) {
716
- throw new Error(`File path must be an absolute project path starting with /. Got: ${JSON.stringify(virtualPath)}`);
727
+ function resolveWorkerFilePath(branchPath, inputPath) {
728
+ if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
729
+ throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
717
730
  }
718
- const checkoutAbsolutePath = import_node_path.default.resolve(virtualPath);
719
- if (isInsideBranchPath(branchPath, checkoutAbsolutePath)) {
720
- const checkoutVirtualPath = toVirtualPath(import_node_path.default.resolve(branchPath), checkoutAbsolutePath);
721
- if (checkoutVirtualPath !== virtualPath) {
722
- return resolveProjectFilePath(branchPath, checkoutVirtualPath);
731
+ const resolvedBranchPath = import_node_path.default.resolve(branchPath);
732
+ if (import_node_path.default.isAbsolute(inputPath)) {
733
+ const absolutePath2 = import_node_path.default.resolve(inputPath);
734
+ if (!isInsideBranchPath(resolvedBranchPath, absolutePath2)) {
735
+ return {
736
+ absolutePath: absolutePath2,
737
+ displayPath: absolutePath2,
738
+ repoRelativePath: null,
739
+ scope: "host"
740
+ };
723
741
  }
742
+ const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
743
+ const repoRelativePath2 = displayPath === "." ? "" : displayPath;
744
+ assertAllowedProjectPath(repoRelativePath2, inputPath);
745
+ return {
746
+ absolutePath: absolutePath2,
747
+ displayPath,
748
+ repoRelativePath: repoRelativePath2,
749
+ scope: "project"
750
+ };
724
751
  }
725
- if (virtualPath.split("/").includes("..")) {
726
- throw new Error(`Invalid project file path: ${virtualPath}`);
752
+ const normalizedInputPath = inputPath.replace(/\\/g, "/");
753
+ if (normalizedInputPath.split("/").includes("..")) {
754
+ throw new Error(`Invalid project file path: ${inputPath}`);
727
755
  }
728
- const normalized = import_node_path.default.posix.normalize(virtualPath);
729
- if (normalized.startsWith("/../")) {
730
- throw new Error(`Invalid project file path: ${virtualPath}`);
756
+ const normalized = import_node_path.default.posix.normalize(normalizedInputPath);
757
+ const repoRelativePath = normalized === "." ? "" : normalized.replace(/^\.\/+/, "");
758
+ assertAllowedProjectPath(repoRelativePath, inputPath);
759
+ const absolutePath = repoRelativePath ? import_node_path.default.resolve(resolvedBranchPath, ...repoRelativePath.split("/")) : resolvedBranchPath;
760
+ if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
761
+ throw new Error(`Invalid project file path: ${inputPath}`);
731
762
  }
732
- const repoRelativePath = normalized === "/" ? "" : normalized.slice(1);
733
- if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
734
- throw new Error("Refusing to access .git through worker file tools");
763
+ return {
764
+ absolutePath,
765
+ displayPath: repoRelativePath || ".",
766
+ repoRelativePath,
767
+ scope: "project"
768
+ };
769
+ }
770
+ function resolveProjectFilePath(branchPath, inputPath) {
771
+ const resolved = resolveWorkerFilePath(branchPath, inputPath);
772
+ if (resolved.scope === "host") {
773
+ throw new Error(
774
+ `Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
775
+ );
735
776
  }
736
- const absolutePath = repoRelativePath ? import_node_path.default.resolve(branchPath, ...repoRelativePath.split("/")) : import_node_path.default.resolve(branchPath);
737
- assertInsideBranch(branchPath, absolutePath);
738
- return { absolutePath, virtualPath: repoRelativePath ? `/${repoRelativePath}` : "/", repoRelativePath };
777
+ return resolved;
739
778
  }
740
779
  function resolveRemoteUrl(baseUrl, remoteUrl) {
741
780
  if (/^https?:\/\//i.test(remoteUrl)) {
@@ -1349,6 +1388,21 @@ async function withFileMutationQueue(key, operation) {
1349
1388
  function mutationQueueKey(projectId, branchName, filePath) {
1350
1389
  return `${projectId}:${branchName}:${import_node_path.default.posix.normalize(filePath)}`;
1351
1390
  }
1391
+ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1392
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1393
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1394
+ throw new Error(`File not found: ${resolved.displayPath}`);
1395
+ }
1396
+ const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
1397
+ const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
1398
+ return {
1399
+ type: "read",
1400
+ kind: "text",
1401
+ file: resolved.displayPath,
1402
+ gitBlobHash: getGitBlobHashForContent(buffer),
1403
+ ...formatted
1404
+ };
1405
+ }
1352
1406
  async function executeReadFileOperation(input) {
1353
1407
  const artifact = await resolveArtifactEnvFilePath({
1354
1408
  filePath: input.message.filePath,
@@ -1363,55 +1417,43 @@ async function executeReadFileOperation(input) {
1363
1417
  if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1364
1418
  throw new Error(`File not found: ${artifact.virtualPath}`);
1365
1419
  }
1366
- const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1367
- const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
1420
+ const buffer = import_node_fs.default.readFileSync(artifact.absolutePath);
1421
+ const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1368
1422
  return {
1369
1423
  type: "read",
1370
1424
  kind: "text",
1371
1425
  file: artifact.virtualPath,
1372
- gitBlobHash: getGitBlobHashForContent(buffer2),
1373
- ...formatted2
1426
+ gitBlobHash: getGitBlobHashForContent(buffer),
1427
+ ...formatted
1374
1428
  };
1375
1429
  }
1376
1430
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1377
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1378
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1379
- throw new Error(`File not found: ${resolved.virtualPath}`);
1380
- }
1381
- const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
1382
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1383
- return {
1384
- type: "read",
1385
- kind: "text",
1386
- file: resolved.virtualPath,
1387
- gitBlobHash: getGitBlobHashForContent(buffer),
1388
- ...formatted
1389
- };
1431
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1390
1432
  }
1391
1433
  async function executeWriteFileOperation(input) {
1392
1434
  rejectArtifactWritePath(input.message.filePath);
1393
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1394
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1395
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1435
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1436
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1437
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1396
1438
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1397
1439
  import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1398
1440
  return {
1399
1441
  type: "write",
1400
- file: resolved.virtualPath,
1442
+ file: resolved.displayPath,
1401
1443
  gitBlobHash: getGitBlobHashForContent(input.message.content)
1402
1444
  };
1403
1445
  });
1404
1446
  }
1405
1447
  async function executeEditFileOperation(input) {
1406
1448
  rejectArtifactWritePath(input.message.filePath);
1407
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1408
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1409
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1449
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1450
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1451
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1410
1452
  if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1411
1453
  throw new Error("edit requires at least one replacement");
1412
1454
  }
1413
1455
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1414
- throw new Error(`File not found: ${resolved.virtualPath}`);
1456
+ throw new Error(`File not found: ${resolved.displayPath}`);
1415
1457
  }
1416
1458
  const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
1417
1459
  const ranges = input.message.edits.map((edit, index) => {
@@ -1446,7 +1488,7 @@ async function executeEditFileOperation(input) {
1446
1488
  import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1447
1489
  return {
1448
1490
  type: "edit",
1449
- file: resolved.virtualPath,
1491
+ file: resolved.displayPath,
1450
1492
  edits: input.message.edits.length
1451
1493
  };
1452
1494
  });
@@ -1466,47 +1508,70 @@ function wildcardToRegex(pattern) {
1466
1508
  function normalizeFindPattern(pattern) {
1467
1509
  return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
1468
1510
  }
1469
- function toVirtualPath(branchPath, absolutePath) {
1470
- const relative = import_node_path.default.relative(branchPath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1471
- return relative ? `/${relative}` : "/";
1472
- }
1473
- function walkProjectEntries(branchPath, startPath) {
1511
+ function walkWorkerEntries(branchPath, start) {
1474
1512
  const entries = [];
1475
- const visit = (absolutePath) => {
1476
- const stat = import_node_fs.default.statSync(absolutePath);
1477
- const virtualPath = toVirtualPath(branchPath, absolutePath);
1478
- entries.push({ absolutePath, virtualPath, stat });
1513
+ const visitedDirectories = /* @__PURE__ */ new Set();
1514
+ const visit = (absolutePath, isRoot) => {
1515
+ let stat;
1516
+ try {
1517
+ stat = import_node_fs.default.statSync(absolutePath);
1518
+ } catch (error) {
1519
+ if (isRoot) throw error;
1520
+ return;
1521
+ }
1522
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : import_node_path.default.resolve(absolutePath);
1523
+ const hostRelativePath = import_node_path.default.relative(start.absolutePath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1524
+ const matchPath = start.scope === "project" ? displayPath : hostRelativePath || import_node_path.default.basename(start.absolutePath);
1525
+ entries.push({ absolutePath, displayPath, matchPath, stat });
1479
1526
  if (!stat.isDirectory()) return;
1480
- const children = import_node_fs.default.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1527
+ let realDirectoryPath;
1528
+ try {
1529
+ realDirectoryPath = import_node_fs.default.realpathSync(absolutePath);
1530
+ } catch (error) {
1531
+ if (isRoot) throw error;
1532
+ return;
1533
+ }
1534
+ if (visitedDirectories.has(realDirectoryPath)) return;
1535
+ visitedDirectories.add(realDirectoryPath);
1536
+ let children;
1537
+ try {
1538
+ children = import_node_fs.default.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1539
+ } catch (error) {
1540
+ if (isRoot) throw error;
1541
+ return;
1542
+ }
1481
1543
  for (const child of children) {
1482
1544
  if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
1483
- visit(import_node_path.default.join(absolutePath, child.name));
1545
+ visit(import_node_path.default.join(absolutePath, child.name), false);
1484
1546
  }
1485
1547
  };
1486
- visit(startPath);
1548
+ visit(start.absolutePath, true);
1487
1549
  return entries;
1488
1550
  }
1489
1551
  function isProbablyText(bytes) {
1490
1552
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1491
1553
  }
1492
- async function executeGrepOperation(input) {
1493
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1494
- const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1554
+ function grepWorkerFiles(branchPath, input) {
1555
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1495
1556
  if (!import_node_fs.default.existsSync(start.absolutePath)) {
1496
- throw new Error(`Path not found: ${start.virtualPath}`);
1557
+ throw new Error(`Path not found: ${start.displayPath}`);
1497
1558
  }
1498
- const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
1499
- const flags = input.message.caseSensitive === false ? "i" : "";
1500
- const regex = new RegExp(input.message.pattern, flags);
1501
- const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
1559
+ const limit = normalizeToolLimit(input.limit, 100, 1e3);
1560
+ const flags = input.caseSensitive === false ? "i" : "";
1561
+ const regex = new RegExp(input.pattern, flags);
1562
+ const globRegex = input.glob ? wildcardToRegex(input.glob) : null;
1502
1563
  const lines = [];
1503
1564
  let matchCount = 0;
1504
1565
  let truncated = false;
1505
- for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1566
+ for (const entry of walkWorkerEntries(branchPath, start)) {
1506
1567
  if (!entry.stat.isFile()) continue;
1507
- const repoRelative = entry.virtualPath.slice(1);
1508
- if (globRegex && !globRegex.test(repoRelative)) continue;
1509
- const bytes = import_node_fs.default.readFileSync(entry.absolutePath);
1568
+ if (globRegex && !globRegex.test(entry.matchPath)) continue;
1569
+ let bytes;
1570
+ try {
1571
+ bytes = import_node_fs.default.readFileSync(entry.absolutePath);
1572
+ } catch {
1573
+ continue;
1574
+ }
1510
1575
  if (!isProbablyText(bytes)) continue;
1511
1576
  const fileLines = bytes.toString("utf8").split(/\r?\n/);
1512
1577
  for (let index = 0; index < fileLines.length; index += 1) {
@@ -1517,7 +1582,7 @@ async function executeGrepOperation(input) {
1517
1582
  matchCount += 1;
1518
1583
  if (lines.length < limit) {
1519
1584
  const column = Math.max(1, (match.index ?? 0) + 1);
1520
- lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
1585
+ lines.push(`${entry.displayPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
1521
1586
  } else {
1522
1587
  truncated = true;
1523
1588
  }
@@ -1532,26 +1597,35 @@ async function executeGrepOperation(input) {
1532
1597
  truncated
1533
1598
  };
1534
1599
  }
1535
- async function executeFindOperation(input) {
1600
+ async function executeGrepOperation(input) {
1536
1601
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1537
- const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1602
+ return grepWorkerFiles(workspace.branchPath, {
1603
+ pattern: input.message.pattern,
1604
+ path: input.message.path,
1605
+ glob: input.message.glob,
1606
+ caseSensitive: input.message.caseSensitive,
1607
+ limit: input.message.limit
1608
+ });
1609
+ }
1610
+ function findWorkerFiles(branchPath, input) {
1611
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1538
1612
  if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
1539
- throw new Error(`Directory not found: ${start.virtualPath}`);
1613
+ throw new Error(`Directory not found: ${start.displayPath}`);
1540
1614
  }
1541
- const pattern = normalizeFindPattern(input.message.pattern ?? "*");
1615
+ const pattern = normalizeFindPattern(input.pattern ?? "*");
1542
1616
  const matcher = wildcardToRegex(pattern);
1543
- const entryType = input.message.entryType ?? "file";
1544
- const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1617
+ const entryType = input.entryType ?? "file";
1618
+ const limit = normalizeToolLimit(input.limit, 200, 1e3);
1545
1619
  const matches = [];
1546
1620
  let count = 0;
1547
1621
  let truncated = false;
1548
- for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1549
- if (entry.virtualPath === start.virtualPath) continue;
1622
+ for (const entry of walkWorkerEntries(branchPath, start)) {
1623
+ if (entry.absolutePath === start.absolutePath) continue;
1550
1624
  const isDirectory = entry.stat.isDirectory();
1551
1625
  if (entryType === "file" && !entry.stat.isFile()) continue;
1552
1626
  if (entryType === "directory" && !isDirectory) continue;
1553
- const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
1554
- if (!matcher.test(import_node_path.default.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
1627
+ const label = isDirectory ? `${entry.displayPath}${start.scope === "host" ? import_node_path.default.sep : "/"}` : entry.displayPath;
1628
+ if (!matcher.test(import_node_path.default.basename(entry.absolutePath)) && !matcher.test(entry.matchPath)) continue;
1555
1629
  count += 1;
1556
1630
  if (matches.length < limit) {
1557
1631
  matches.push(label);
@@ -1567,25 +1641,59 @@ async function executeFindOperation(input) {
1567
1641
  truncated
1568
1642
  };
1569
1643
  }
1570
- async function executeLsOperation(input) {
1644
+ async function executeFindOperation(input) {
1571
1645
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1572
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1646
+ return findWorkerFiles(workspace.branchPath, {
1647
+ pattern: input.message.pattern,
1648
+ path: input.message.path,
1649
+ entryType: input.message.entryType,
1650
+ limit: input.message.limit
1651
+ });
1652
+ }
1653
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1654
+ const resolved = resolveWorkerFilePath(branchPath, inputPath);
1573
1655
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1574
- throw new Error(`Directory not found: ${resolved.virtualPath}`);
1656
+ throw new Error(`Directory not found: ${resolved.displayPath}`);
1575
1657
  }
1576
- const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1658
+ const limit = normalizeToolLimit(inputLimit, 200, 1e3);
1577
1659
  const entries = import_node_fs.default.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
1578
1660
  const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
1579
1661
  const truncated = entries.length > displayed.length;
1580
1662
  const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
1581
1663
  return {
1582
1664
  type: "ls",
1583
- path: resolved.virtualPath,
1665
+ path: resolved.displayPath,
1584
1666
  content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
1585
1667
  count: entries.length,
1586
1668
  truncated
1587
1669
  };
1588
1670
  }
1671
+ async function executeLsOperation(input) {
1672
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1673
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1674
+ }
1675
+ function readWorkerImageFile(branchPath, filePath) {
1676
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1677
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1678
+ throw new Error(`File not found: ${resolved.displayPath}`);
1679
+ }
1680
+ const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
1681
+ const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1682
+ if (!mediaType) {
1683
+ throw new Error("read supports PNG and JPEG image inputs only");
1684
+ }
1685
+ const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
1686
+ const dimensions = readImageDimensions(bytes, mediaType);
1687
+ return {
1688
+ type: "view_file_bytes",
1689
+ filePath: resolved.displayPath,
1690
+ mediaType,
1691
+ base64: bytes.toString("base64"),
1692
+ fileSizeBytes: bytes.length,
1693
+ width: dimensions.width,
1694
+ height: dimensions.height
1695
+ };
1696
+ }
1589
1697
  async function executeViewFileBytesOperation(input) {
1590
1698
  const artifact = await resolveArtifactEnvFilePath({
1591
1699
  filePath: input.message.filePath,
@@ -1600,44 +1708,25 @@ async function executeViewFileBytesOperation(input) {
1600
1708
  if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1601
1709
  throw new Error(`File not found: ${artifact.virtualPath}`);
1602
1710
  }
1603
- const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1604
- const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
1605
- if (!mediaType2) {
1711
+ const extension = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1712
+ const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1713
+ if (!mediaType) {
1606
1714
  throw new Error("read supports PNG and JPEG image inputs only");
1607
1715
  }
1608
- const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1609
- const dimensions2 = readImageDimensions(bytes2, mediaType2);
1716
+ const bytes = import_node_fs.default.readFileSync(artifact.absolutePath);
1717
+ const dimensions = readImageDimensions(bytes, mediaType);
1610
1718
  return {
1611
1719
  type: "view_file_bytes",
1612
1720
  filePath: artifact.virtualPath,
1613
- mediaType: mediaType2,
1614
- base64: bytes2.toString("base64"),
1615
- fileSizeBytes: bytes2.length,
1616
- width: dimensions2.width,
1617
- height: dimensions2.height
1721
+ mediaType,
1722
+ base64: bytes.toString("base64"),
1723
+ fileSizeBytes: bytes.length,
1724
+ width: dimensions.width,
1725
+ height: dimensions.height
1618
1726
  };
1619
1727
  }
1620
1728
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1621
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1622
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1623
- throw new Error(`File not found: ${resolved.virtualPath}`);
1624
- }
1625
- const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
1626
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1627
- if (!mediaType) {
1628
- throw new Error("read supports PNG and JPEG image inputs only");
1629
- }
1630
- const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
1631
- const dimensions = readImageDimensions(bytes, mediaType);
1632
- return {
1633
- type: "view_file_bytes",
1634
- filePath: resolved.virtualPath,
1635
- mediaType,
1636
- base64: bytes.toString("base64"),
1637
- fileSizeBytes: bytes.length,
1638
- width: dimensions.width,
1639
- height: dimensions.height
1640
- };
1729
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1641
1730
  }
1642
1731
  function stagedBlobSizeBytes(context) {
1643
1732
  const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
@@ -2830,11 +2919,17 @@ if (isCliEntrypoint()) {
2830
2919
  0 && (module.exports = {
2831
2920
  ensureVisibleGitCheckout,
2832
2921
  findRepositorySyncBlockers,
2922
+ findWorkerFiles,
2923
+ grepWorkerFiles,
2833
2924
  isArtifactEnvPath,
2925
+ listWorkerDirectory,
2834
2926
  prepareArtifactEnvForShell,
2835
2927
  preparePlanEnvForShell,
2928
+ readWorkerImageFile,
2929
+ readWorkerTextFile,
2836
2930
  resolveHostShell,
2837
2931
  resolveProjectFilePath,
2932
+ resolveWorkerFilePath,
2838
2933
  syncManifestProjectsFromInternal,
2839
2934
  syncProjectPlans,
2840
2935
  syncSessionArtifacts
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -663,36 +663,69 @@ function isInsideBranchPath(branchPath, filePath) {
663
663
  const relative = path.relative(path.resolve(branchPath), path.resolve(filePath));
664
664
  return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
665
665
  }
666
- function assertInsideBranch(branchPath, filePath) {
667
- if (!isInsideBranchPath(branchPath, filePath)) {
668
- throw new Error("File path escapes the project branch");
666
+ function toProjectDisplayPath(branchPath, absolutePath) {
667
+ const relative = path.relative(path.resolve(branchPath), path.resolve(absolutePath)).split(path.sep).join(path.posix.sep);
668
+ return relative || ".";
669
+ }
670
+ 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
+ if (repoRelativePath.split("/").includes("..")) {
675
+ throw new Error(`Invalid project file path: ${inputPath}`);
669
676
  }
670
677
  }
671
- function resolveProjectFilePath(branchPath, virtualPath) {
672
- if (typeof virtualPath !== "string" || !virtualPath.startsWith("/") || virtualPath.includes("\0")) {
673
- throw new Error(`File path must be an absolute project path starting with /. Got: ${JSON.stringify(virtualPath)}`);
678
+ function resolveWorkerFilePath(branchPath, inputPath) {
679
+ if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
680
+ throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
674
681
  }
675
- const checkoutAbsolutePath = path.resolve(virtualPath);
676
- if (isInsideBranchPath(branchPath, checkoutAbsolutePath)) {
677
- const checkoutVirtualPath = toVirtualPath(path.resolve(branchPath), checkoutAbsolutePath);
678
- if (checkoutVirtualPath !== virtualPath) {
679
- return resolveProjectFilePath(branchPath, checkoutVirtualPath);
682
+ const resolvedBranchPath = path.resolve(branchPath);
683
+ if (path.isAbsolute(inputPath)) {
684
+ const absolutePath2 = path.resolve(inputPath);
685
+ if (!isInsideBranchPath(resolvedBranchPath, absolutePath2)) {
686
+ return {
687
+ absolutePath: absolutePath2,
688
+ displayPath: absolutePath2,
689
+ repoRelativePath: null,
690
+ scope: "host"
691
+ };
680
692
  }
693
+ const displayPath = toProjectDisplayPath(resolvedBranchPath, absolutePath2);
694
+ const repoRelativePath2 = displayPath === "." ? "" : displayPath;
695
+ assertAllowedProjectPath(repoRelativePath2, inputPath);
696
+ return {
697
+ absolutePath: absolutePath2,
698
+ displayPath,
699
+ repoRelativePath: repoRelativePath2,
700
+ scope: "project"
701
+ };
681
702
  }
682
- if (virtualPath.split("/").includes("..")) {
683
- throw new Error(`Invalid project file path: ${virtualPath}`);
703
+ const normalizedInputPath = inputPath.replace(/\\/g, "/");
704
+ if (normalizedInputPath.split("/").includes("..")) {
705
+ throw new Error(`Invalid project file path: ${inputPath}`);
684
706
  }
685
- const normalized = path.posix.normalize(virtualPath);
686
- if (normalized.startsWith("/../")) {
687
- throw new Error(`Invalid project file path: ${virtualPath}`);
707
+ const normalized = path.posix.normalize(normalizedInputPath);
708
+ const repoRelativePath = normalized === "." ? "" : normalized.replace(/^\.\/+/, "");
709
+ assertAllowedProjectPath(repoRelativePath, inputPath);
710
+ const absolutePath = repoRelativePath ? path.resolve(resolvedBranchPath, ...repoRelativePath.split("/")) : resolvedBranchPath;
711
+ if (!isInsideBranchPath(resolvedBranchPath, absolutePath)) {
712
+ throw new Error(`Invalid project file path: ${inputPath}`);
688
713
  }
689
- const repoRelativePath = normalized === "/" ? "" : normalized.slice(1);
690
- if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
691
- throw new Error("Refusing to access .git through worker file tools");
714
+ return {
715
+ absolutePath,
716
+ displayPath: repoRelativePath || ".",
717
+ repoRelativePath,
718
+ scope: "project"
719
+ };
720
+ }
721
+ function resolveProjectFilePath(branchPath, inputPath) {
722
+ const resolved = resolveWorkerFilePath(branchPath, inputPath);
723
+ if (resolved.scope === "host") {
724
+ throw new Error(
725
+ `Project file writes must use a checkout-relative path. Absolute host paths are read-only in file tools: ${inputPath}`
726
+ );
692
727
  }
693
- const absolutePath = repoRelativePath ? path.resolve(branchPath, ...repoRelativePath.split("/")) : path.resolve(branchPath);
694
- assertInsideBranch(branchPath, absolutePath);
695
- return { absolutePath, virtualPath: repoRelativePath ? `/${repoRelativePath}` : "/", repoRelativePath };
728
+ return resolved;
696
729
  }
697
730
  function resolveRemoteUrl(baseUrl, remoteUrl) {
698
731
  if (/^https?:\/\//i.test(remoteUrl)) {
@@ -1306,6 +1339,21 @@ async function withFileMutationQueue(key, operation) {
1306
1339
  function mutationQueueKey(projectId, branchName, filePath) {
1307
1340
  return `${projectId}:${branchName}:${path.posix.normalize(filePath)}`;
1308
1341
  }
1342
+ function readWorkerTextFile(branchPath, filePath, offset, limit) {
1343
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1344
+ if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1345
+ throw new Error(`File not found: ${resolved.displayPath}`);
1346
+ }
1347
+ const buffer = fs.readFileSync(resolved.absolutePath);
1348
+ const formatted = formatLineNumberedContent(buffer.toString("utf8"), offset, limit);
1349
+ return {
1350
+ type: "read",
1351
+ kind: "text",
1352
+ file: resolved.displayPath,
1353
+ gitBlobHash: getGitBlobHashForContent(buffer),
1354
+ ...formatted
1355
+ };
1356
+ }
1309
1357
  async function executeReadFileOperation(input) {
1310
1358
  const artifact = await resolveArtifactEnvFilePath({
1311
1359
  filePath: input.message.filePath,
@@ -1320,55 +1368,43 @@ async function executeReadFileOperation(input) {
1320
1368
  if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1321
1369
  throw new Error(`File not found: ${artifact.virtualPath}`);
1322
1370
  }
1323
- const buffer2 = fs.readFileSync(artifact.absolutePath);
1324
- const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
1371
+ const buffer = fs.readFileSync(artifact.absolutePath);
1372
+ const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1325
1373
  return {
1326
1374
  type: "read",
1327
1375
  kind: "text",
1328
1376
  file: artifact.virtualPath,
1329
- gitBlobHash: getGitBlobHashForContent(buffer2),
1330
- ...formatted2
1377
+ gitBlobHash: getGitBlobHashForContent(buffer),
1378
+ ...formatted
1331
1379
  };
1332
1380
  }
1333
1381
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1334
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1335
- if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1336
- throw new Error(`File not found: ${resolved.virtualPath}`);
1337
- }
1338
- const buffer = fs.readFileSync(resolved.absolutePath);
1339
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1340
- return {
1341
- type: "read",
1342
- kind: "text",
1343
- file: resolved.virtualPath,
1344
- gitBlobHash: getGitBlobHashForContent(buffer),
1345
- ...formatted
1346
- };
1382
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1347
1383
  }
1348
1384
  async function executeWriteFileOperation(input) {
1349
1385
  rejectArtifactWritePath(input.message.filePath);
1350
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1351
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1352
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1386
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1387
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1388
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1353
1389
  fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
1354
1390
  fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1355
1391
  return {
1356
1392
  type: "write",
1357
- file: resolved.virtualPath,
1393
+ file: resolved.displayPath,
1358
1394
  gitBlobHash: getGitBlobHashForContent(input.message.content)
1359
1395
  };
1360
1396
  });
1361
1397
  }
1362
1398
  async function executeEditFileOperation(input) {
1363
1399
  rejectArtifactWritePath(input.message.filePath);
1364
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1365
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1366
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1400
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1401
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1402
+ return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved.repoRelativePath), async () => {
1367
1403
  if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1368
1404
  throw new Error("edit requires at least one replacement");
1369
1405
  }
1370
1406
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1371
- throw new Error(`File not found: ${resolved.virtualPath}`);
1407
+ throw new Error(`File not found: ${resolved.displayPath}`);
1372
1408
  }
1373
1409
  const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
1374
1410
  const ranges = input.message.edits.map((edit, index) => {
@@ -1403,7 +1439,7 @@ async function executeEditFileOperation(input) {
1403
1439
  fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1404
1440
  return {
1405
1441
  type: "edit",
1406
- file: resolved.virtualPath,
1442
+ file: resolved.displayPath,
1407
1443
  edits: input.message.edits.length
1408
1444
  };
1409
1445
  });
@@ -1423,47 +1459,70 @@ function wildcardToRegex(pattern) {
1423
1459
  function normalizeFindPattern(pattern) {
1424
1460
  return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
1425
1461
  }
1426
- function toVirtualPath(branchPath, absolutePath) {
1427
- const relative = path.relative(branchPath, absolutePath).split(path.sep).join(path.posix.sep);
1428
- return relative ? `/${relative}` : "/";
1429
- }
1430
- function walkProjectEntries(branchPath, startPath) {
1462
+ function walkWorkerEntries(branchPath, start) {
1431
1463
  const entries = [];
1432
- const visit = (absolutePath) => {
1433
- const stat = fs.statSync(absolutePath);
1434
- const virtualPath = toVirtualPath(branchPath, absolutePath);
1435
- entries.push({ absolutePath, virtualPath, stat });
1464
+ const visitedDirectories = /* @__PURE__ */ new Set();
1465
+ const visit = (absolutePath, isRoot) => {
1466
+ let stat;
1467
+ try {
1468
+ stat = fs.statSync(absolutePath);
1469
+ } catch (error) {
1470
+ if (isRoot) throw error;
1471
+ return;
1472
+ }
1473
+ const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
1474
+ const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
1475
+ const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
1476
+ entries.push({ absolutePath, displayPath, matchPath, stat });
1436
1477
  if (!stat.isDirectory()) return;
1437
- const children = fs.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1478
+ let realDirectoryPath;
1479
+ try {
1480
+ realDirectoryPath = fs.realpathSync(absolutePath);
1481
+ } catch (error) {
1482
+ if (isRoot) throw error;
1483
+ return;
1484
+ }
1485
+ if (visitedDirectories.has(realDirectoryPath)) return;
1486
+ visitedDirectories.add(realDirectoryPath);
1487
+ let children;
1488
+ try {
1489
+ children = fs.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
1490
+ } catch (error) {
1491
+ if (isRoot) throw error;
1492
+ return;
1493
+ }
1438
1494
  for (const child of children) {
1439
1495
  if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
1440
- visit(path.join(absolutePath, child.name));
1496
+ visit(path.join(absolutePath, child.name), false);
1441
1497
  }
1442
1498
  };
1443
- visit(startPath);
1499
+ visit(start.absolutePath, true);
1444
1500
  return entries;
1445
1501
  }
1446
1502
  function isProbablyText(bytes) {
1447
1503
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1448
1504
  }
1449
- async function executeGrepOperation(input) {
1450
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1451
- const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1505
+ function grepWorkerFiles(branchPath, input) {
1506
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1452
1507
  if (!fs.existsSync(start.absolutePath)) {
1453
- throw new Error(`Path not found: ${start.virtualPath}`);
1508
+ throw new Error(`Path not found: ${start.displayPath}`);
1454
1509
  }
1455
- const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
1456
- const flags = input.message.caseSensitive === false ? "i" : "";
1457
- const regex = new RegExp(input.message.pattern, flags);
1458
- const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
1510
+ const limit = normalizeToolLimit(input.limit, 100, 1e3);
1511
+ const flags = input.caseSensitive === false ? "i" : "";
1512
+ const regex = new RegExp(input.pattern, flags);
1513
+ const globRegex = input.glob ? wildcardToRegex(input.glob) : null;
1459
1514
  const lines = [];
1460
1515
  let matchCount = 0;
1461
1516
  let truncated = false;
1462
- for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1517
+ for (const entry of walkWorkerEntries(branchPath, start)) {
1463
1518
  if (!entry.stat.isFile()) continue;
1464
- const repoRelative = entry.virtualPath.slice(1);
1465
- if (globRegex && !globRegex.test(repoRelative)) continue;
1466
- const bytes = fs.readFileSync(entry.absolutePath);
1519
+ if (globRegex && !globRegex.test(entry.matchPath)) continue;
1520
+ let bytes;
1521
+ try {
1522
+ bytes = fs.readFileSync(entry.absolutePath);
1523
+ } catch {
1524
+ continue;
1525
+ }
1467
1526
  if (!isProbablyText(bytes)) continue;
1468
1527
  const fileLines = bytes.toString("utf8").split(/\r?\n/);
1469
1528
  for (let index = 0; index < fileLines.length; index += 1) {
@@ -1474,7 +1533,7 @@ async function executeGrepOperation(input) {
1474
1533
  matchCount += 1;
1475
1534
  if (lines.length < limit) {
1476
1535
  const column = Math.max(1, (match.index ?? 0) + 1);
1477
- lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
1536
+ lines.push(`${entry.displayPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
1478
1537
  } else {
1479
1538
  truncated = true;
1480
1539
  }
@@ -1489,26 +1548,35 @@ async function executeGrepOperation(input) {
1489
1548
  truncated
1490
1549
  };
1491
1550
  }
1492
- async function executeFindOperation(input) {
1551
+ async function executeGrepOperation(input) {
1493
1552
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1494
- const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1553
+ return grepWorkerFiles(workspace.branchPath, {
1554
+ pattern: input.message.pattern,
1555
+ path: input.message.path,
1556
+ glob: input.message.glob,
1557
+ caseSensitive: input.message.caseSensitive,
1558
+ limit: input.message.limit
1559
+ });
1560
+ }
1561
+ function findWorkerFiles(branchPath, input) {
1562
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1495
1563
  if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
1496
- throw new Error(`Directory not found: ${start.virtualPath}`);
1564
+ throw new Error(`Directory not found: ${start.displayPath}`);
1497
1565
  }
1498
- const pattern = normalizeFindPattern(input.message.pattern ?? "*");
1566
+ const pattern = normalizeFindPattern(input.pattern ?? "*");
1499
1567
  const matcher = wildcardToRegex(pattern);
1500
- const entryType = input.message.entryType ?? "file";
1501
- const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1568
+ const entryType = input.entryType ?? "file";
1569
+ const limit = normalizeToolLimit(input.limit, 200, 1e3);
1502
1570
  const matches = [];
1503
1571
  let count = 0;
1504
1572
  let truncated = false;
1505
- for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1506
- if (entry.virtualPath === start.virtualPath) continue;
1573
+ for (const entry of walkWorkerEntries(branchPath, start)) {
1574
+ if (entry.absolutePath === start.absolutePath) continue;
1507
1575
  const isDirectory = entry.stat.isDirectory();
1508
1576
  if (entryType === "file" && !entry.stat.isFile()) continue;
1509
1577
  if (entryType === "directory" && !isDirectory) continue;
1510
- const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
1511
- if (!matcher.test(path.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
1578
+ const label = isDirectory ? `${entry.displayPath}${start.scope === "host" ? path.sep : "/"}` : entry.displayPath;
1579
+ if (!matcher.test(path.basename(entry.absolutePath)) && !matcher.test(entry.matchPath)) continue;
1512
1580
  count += 1;
1513
1581
  if (matches.length < limit) {
1514
1582
  matches.push(label);
@@ -1524,25 +1592,59 @@ async function executeFindOperation(input) {
1524
1592
  truncated
1525
1593
  };
1526
1594
  }
1527
- async function executeLsOperation(input) {
1595
+ async function executeFindOperation(input) {
1528
1596
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1529
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1597
+ return findWorkerFiles(workspace.branchPath, {
1598
+ pattern: input.message.pattern,
1599
+ path: input.message.path,
1600
+ entryType: input.message.entryType,
1601
+ limit: input.message.limit
1602
+ });
1603
+ }
1604
+ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
1605
+ const resolved = resolveWorkerFilePath(branchPath, inputPath);
1530
1606
  if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
1531
- throw new Error(`Directory not found: ${resolved.virtualPath}`);
1607
+ throw new Error(`Directory not found: ${resolved.displayPath}`);
1532
1608
  }
1533
- const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1609
+ const limit = normalizeToolLimit(inputLimit, 200, 1e3);
1534
1610
  const entries = fs.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
1535
1611
  const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
1536
1612
  const truncated = entries.length > displayed.length;
1537
1613
  const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
1538
1614
  return {
1539
1615
  type: "ls",
1540
- path: resolved.virtualPath,
1616
+ path: resolved.displayPath,
1541
1617
  content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
1542
1618
  count: entries.length,
1543
1619
  truncated
1544
1620
  };
1545
1621
  }
1622
+ async function executeLsOperation(input) {
1623
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1624
+ return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
1625
+ }
1626
+ function readWorkerImageFile(branchPath, filePath) {
1627
+ const resolved = resolveWorkerFilePath(branchPath, filePath);
1628
+ if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1629
+ throw new Error(`File not found: ${resolved.displayPath}`);
1630
+ }
1631
+ const extension = path.extname(resolved.absolutePath).toLowerCase();
1632
+ const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1633
+ if (!mediaType) {
1634
+ throw new Error("read supports PNG and JPEG image inputs only");
1635
+ }
1636
+ const bytes = fs.readFileSync(resolved.absolutePath);
1637
+ const dimensions = readImageDimensions(bytes, mediaType);
1638
+ return {
1639
+ type: "view_file_bytes",
1640
+ filePath: resolved.displayPath,
1641
+ mediaType,
1642
+ base64: bytes.toString("base64"),
1643
+ fileSizeBytes: bytes.length,
1644
+ width: dimensions.width,
1645
+ height: dimensions.height
1646
+ };
1647
+ }
1546
1648
  async function executeViewFileBytesOperation(input) {
1547
1649
  const artifact = await resolveArtifactEnvFilePath({
1548
1650
  filePath: input.message.filePath,
@@ -1557,44 +1659,25 @@ async function executeViewFileBytesOperation(input) {
1557
1659
  if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
1558
1660
  throw new Error(`File not found: ${artifact.virtualPath}`);
1559
1661
  }
1560
- const extension2 = path.extname(artifact.absolutePath).toLowerCase();
1561
- const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
1562
- if (!mediaType2) {
1662
+ const extension = path.extname(artifact.absolutePath).toLowerCase();
1663
+ const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1664
+ if (!mediaType) {
1563
1665
  throw new Error("read supports PNG and JPEG image inputs only");
1564
1666
  }
1565
- const bytes2 = fs.readFileSync(artifact.absolutePath);
1566
- const dimensions2 = readImageDimensions(bytes2, mediaType2);
1667
+ const bytes = fs.readFileSync(artifact.absolutePath);
1668
+ const dimensions = readImageDimensions(bytes, mediaType);
1567
1669
  return {
1568
1670
  type: "view_file_bytes",
1569
1671
  filePath: artifact.virtualPath,
1570
- mediaType: mediaType2,
1571
- base64: bytes2.toString("base64"),
1572
- fileSizeBytes: bytes2.length,
1573
- width: dimensions2.width,
1574
- height: dimensions2.height
1672
+ mediaType,
1673
+ base64: bytes.toString("base64"),
1674
+ fileSizeBytes: bytes.length,
1675
+ width: dimensions.width,
1676
+ height: dimensions.height
1575
1677
  };
1576
1678
  }
1577
1679
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1578
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1579
- if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
1580
- throw new Error(`File not found: ${resolved.virtualPath}`);
1581
- }
1582
- const extension = path.extname(resolved.absolutePath).toLowerCase();
1583
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1584
- if (!mediaType) {
1585
- throw new Error("read supports PNG and JPEG image inputs only");
1586
- }
1587
- const bytes = fs.readFileSync(resolved.absolutePath);
1588
- const dimensions = readImageDimensions(bytes, mediaType);
1589
- return {
1590
- type: "view_file_bytes",
1591
- filePath: resolved.virtualPath,
1592
- mediaType,
1593
- base64: bytes.toString("base64"),
1594
- fileSizeBytes: bytes.length,
1595
- width: dimensions.width,
1596
- height: dimensions.height
1597
- };
1680
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1598
1681
  }
1599
1682
  function stagedBlobSizeBytes(context) {
1600
1683
  const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
@@ -2786,11 +2869,17 @@ if (isCliEntrypoint()) {
2786
2869
  export {
2787
2870
  ensureVisibleGitCheckout,
2788
2871
  findRepositorySyncBlockers,
2872
+ findWorkerFiles,
2873
+ grepWorkerFiles,
2789
2874
  isArtifactEnvPath,
2875
+ listWorkerDirectory,
2790
2876
  prepareArtifactEnvForShell,
2791
2877
  preparePlanEnvForShell,
2878
+ readWorkerImageFile,
2879
+ readWorkerTextFile,
2792
2880
  resolveHostShell,
2793
2881
  resolveProjectFilePath,
2882
+ resolveWorkerFilePath,
2794
2883
  syncManifestProjectsFromInternal,
2795
2884
  syncProjectPlans,
2796
2885
  syncSessionArtifacts
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module"
5
5
  }
@@ -1,4 +1,16 @@
1
1
  #!/usr/bin/env bun
2
+ type WorkerReadFileResult = {
3
+ type: "read";
4
+ kind: "text";
5
+ file: string;
6
+ gitBlobHash?: string;
7
+ content: string;
8
+ totalLines: number;
9
+ startLine: number;
10
+ endLine: number;
11
+ truncated: boolean;
12
+ continuation?: string;
13
+ };
2
14
  type WorkerProjectManifestEntry = {
3
15
  projectId: string;
4
16
  repoSlug: string;
@@ -28,6 +40,34 @@ type WorkerRepositorySyncResult = {
28
40
  id: string;
29
41
  }>;
30
42
  };
43
+ type WorkerGrepResult = {
44
+ type: "grep";
45
+ content: string;
46
+ matchCount: number;
47
+ truncated: boolean;
48
+ };
49
+ type WorkerFindResult = {
50
+ type: "find";
51
+ content: string;
52
+ count: number;
53
+ truncated: boolean;
54
+ };
55
+ type WorkerLsResult = {
56
+ type: "ls";
57
+ path: string;
58
+ content: string;
59
+ count: number;
60
+ truncated: boolean;
61
+ };
62
+ type WorkerViewFileBytesResult = {
63
+ type: "view_file_bytes";
64
+ filePath: string;
65
+ mediaType: "image/png" | "image/jpeg";
66
+ base64: string;
67
+ fileSizeBytes: number;
68
+ width: number;
69
+ height: number;
70
+ };
31
71
  export declare function isArtifactEnvPath(filePath: string): boolean;
32
72
  export declare function syncSessionArtifacts(input: {
33
73
  baseUrl: string;
@@ -64,11 +104,21 @@ type GitAuth = {
64
104
  extraHeaderUrl: string;
65
105
  header: string;
66
106
  };
67
- export declare function resolveProjectFilePath(branchPath: string, virtualPath: string): {
107
+ type ResolvedWorkerFilePath = {
68
108
  absolutePath: string;
69
- virtualPath: string;
109
+ displayPath: string;
70
110
  repoRelativePath: string;
111
+ scope: "project";
112
+ } | {
113
+ absolutePath: string;
114
+ displayPath: string;
115
+ repoRelativePath: null;
116
+ scope: "host";
71
117
  };
118
+ export declare function resolveWorkerFilePath(branchPath: string, inputPath: string): ResolvedWorkerFilePath;
119
+ export declare function resolveProjectFilePath(branchPath: string, inputPath: string): Extract<ResolvedWorkerFilePath, {
120
+ scope: "project";
121
+ }>;
72
122
  export declare function ensureVisibleGitCheckout(input: {
73
123
  projectRoot: string;
74
124
  branchName: string;
@@ -99,6 +149,22 @@ export declare function syncManifestProjectsFromInternal(input: {
99
149
  manifests: WorkerProjectManifestEntry[];
100
150
  projectIds?: string[];
101
151
  }): Promise<WorkerRepositorySyncResult>;
152
+ export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number): WorkerReadFileResult;
153
+ export declare function grepWorkerFiles(branchPath: string, input: {
154
+ pattern: string;
155
+ path?: string;
156
+ glob?: string;
157
+ caseSensitive?: boolean;
158
+ limit?: number;
159
+ }): WorkerGrepResult;
160
+ export declare function findWorkerFiles(branchPath: string, input: {
161
+ pattern?: string;
162
+ path?: string;
163
+ entryType?: string;
164
+ limit?: number;
165
+ }): WorkerFindResult;
166
+ export declare function listWorkerDirectory(branchPath: string, inputPath?: string, inputLimit?: number): WorkerLsResult;
167
+ export declare function readWorkerImageFile(branchPath: string, filePath: string): WorkerViewFileBytesResult;
102
168
  export declare function resolveHostShell(command?: string, platform?: NodeJS.Platform): {
103
169
  file: string;
104
170
  args: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",