@ricsam/r5d-worker 0.0.29 → 0.0.31

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
@@ -49,6 +55,7 @@ var import_node_child_process = require("node:child_process");
49
55
  var import_cli_update = require("./cli-update.cjs");
50
56
  var import_git_identity = require("./git-identity.cjs");
51
57
  var import_heartbeat = require("./heartbeat.cjs");
58
+ var import_process_tree = require("./process-tree.cjs");
52
59
  var import_supervisor = require("./supervisor.cjs");
53
60
  const import_meta = {};
54
61
  const DEFAULT_BASE_URL = "https://r5d.dev";
@@ -705,36 +712,69 @@ function isInsideBranchPath(branchPath, filePath) {
705
712
  const relative = import_node_path.default.relative(import_node_path.default.resolve(branchPath), import_node_path.default.resolve(filePath));
706
713
  return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative);
707
714
  }
708
- function assertInsideBranch(branchPath, filePath) {
709
- if (!isInsideBranchPath(branchPath, filePath)) {
710
- 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}`);
711
725
  }
712
726
  }
713
- function resolveProjectFilePath(branchPath, virtualPath) {
714
- if (typeof virtualPath !== "string" || !virtualPath.startsWith("/") || virtualPath.includes("\0")) {
715
- 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)}`);
716
730
  }
717
- const checkoutAbsolutePath = import_node_path.default.resolve(virtualPath);
718
- if (isInsideBranchPath(branchPath, checkoutAbsolutePath)) {
719
- const checkoutVirtualPath = toVirtualPath(import_node_path.default.resolve(branchPath), checkoutAbsolutePath);
720
- if (checkoutVirtualPath !== virtualPath) {
721
- 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
+ };
722
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
+ };
723
751
  }
724
- if (virtualPath.split("/").includes("..")) {
725
- 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}`);
726
755
  }
727
- const normalized = import_node_path.default.posix.normalize(virtualPath);
728
- if (normalized.startsWith("/../")) {
729
- 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}`);
730
762
  }
731
- const repoRelativePath = normalized === "/" ? "" : normalized.slice(1);
732
- if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
733
- 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
+ );
734
776
  }
735
- const absolutePath = repoRelativePath ? import_node_path.default.resolve(branchPath, ...repoRelativePath.split("/")) : import_node_path.default.resolve(branchPath);
736
- assertInsideBranch(branchPath, absolutePath);
737
- return { absolutePath, virtualPath: repoRelativePath ? `/${repoRelativePath}` : "/", repoRelativePath };
777
+ return resolved;
738
778
  }
739
779
  function resolveRemoteUrl(baseUrl, remoteUrl) {
740
780
  if (/^https?:\/\//i.test(remoteUrl)) {
@@ -1348,6 +1388,21 @@ async function withFileMutationQueue(key, operation) {
1348
1388
  function mutationQueueKey(projectId, branchName, filePath) {
1349
1389
  return `${projectId}:${branchName}:${import_node_path.default.posix.normalize(filePath)}`;
1350
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
+ }
1351
1406
  async function executeReadFileOperation(input) {
1352
1407
  const artifact = await resolveArtifactEnvFilePath({
1353
1408
  filePath: input.message.filePath,
@@ -1362,55 +1417,43 @@ async function executeReadFileOperation(input) {
1362
1417
  if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1363
1418
  throw new Error(`File not found: ${artifact.virtualPath}`);
1364
1419
  }
1365
- const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1366
- 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);
1367
1422
  return {
1368
1423
  type: "read",
1369
1424
  kind: "text",
1370
1425
  file: artifact.virtualPath,
1371
- gitBlobHash: getGitBlobHashForContent(buffer2),
1372
- ...formatted2
1426
+ gitBlobHash: getGitBlobHashForContent(buffer),
1427
+ ...formatted
1373
1428
  };
1374
1429
  }
1375
1430
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1376
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1377
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1378
- throw new Error(`File not found: ${resolved.virtualPath}`);
1379
- }
1380
- const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
1381
- const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
1382
- return {
1383
- type: "read",
1384
- kind: "text",
1385
- file: resolved.virtualPath,
1386
- gitBlobHash: getGitBlobHashForContent(buffer),
1387
- ...formatted
1388
- };
1431
+ return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
1389
1432
  }
1390
1433
  async function executeWriteFileOperation(input) {
1391
1434
  rejectArtifactWritePath(input.message.filePath);
1392
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1393
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1394
- 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 () => {
1395
1438
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
1396
1439
  import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
1397
1440
  return {
1398
1441
  type: "write",
1399
- file: resolved.virtualPath,
1442
+ file: resolved.displayPath,
1400
1443
  gitBlobHash: getGitBlobHashForContent(input.message.content)
1401
1444
  };
1402
1445
  });
1403
1446
  }
1404
1447
  async function executeEditFileOperation(input) {
1405
1448
  rejectArtifactWritePath(input.message.filePath);
1406
- return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
1407
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1408
- 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 () => {
1409
1452
  if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
1410
1453
  throw new Error("edit requires at least one replacement");
1411
1454
  }
1412
1455
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1413
- throw new Error(`File not found: ${resolved.virtualPath}`);
1456
+ throw new Error(`File not found: ${resolved.displayPath}`);
1414
1457
  }
1415
1458
  const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
1416
1459
  const ranges = input.message.edits.map((edit, index) => {
@@ -1445,7 +1488,7 @@ async function executeEditFileOperation(input) {
1445
1488
  import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
1446
1489
  return {
1447
1490
  type: "edit",
1448
- file: resolved.virtualPath,
1491
+ file: resolved.displayPath,
1449
1492
  edits: input.message.edits.length
1450
1493
  };
1451
1494
  });
@@ -1465,47 +1508,70 @@ function wildcardToRegex(pattern) {
1465
1508
  function normalizeFindPattern(pattern) {
1466
1509
  return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
1467
1510
  }
1468
- function toVirtualPath(branchPath, absolutePath) {
1469
- const relative = import_node_path.default.relative(branchPath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
1470
- return relative ? `/${relative}` : "/";
1471
- }
1472
- function walkProjectEntries(branchPath, startPath) {
1511
+ function walkWorkerEntries(branchPath, start) {
1473
1512
  const entries = [];
1474
- const visit = (absolutePath) => {
1475
- const stat = import_node_fs.default.statSync(absolutePath);
1476
- const virtualPath = toVirtualPath(branchPath, absolutePath);
1477
- 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 });
1478
1526
  if (!stat.isDirectory()) return;
1479
- 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
+ }
1480
1543
  for (const child of children) {
1481
1544
  if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
1482
- visit(import_node_path.default.join(absolutePath, child.name));
1545
+ visit(import_node_path.default.join(absolutePath, child.name), false);
1483
1546
  }
1484
1547
  };
1485
- visit(startPath);
1548
+ visit(start.absolutePath, true);
1486
1549
  return entries;
1487
1550
  }
1488
1551
  function isProbablyText(bytes) {
1489
1552
  return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
1490
1553
  }
1491
- async function executeGrepOperation(input) {
1492
- const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1493
- const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
1554
+ function grepWorkerFiles(branchPath, input) {
1555
+ const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
1494
1556
  if (!import_node_fs.default.existsSync(start.absolutePath)) {
1495
- throw new Error(`Path not found: ${start.virtualPath}`);
1557
+ throw new Error(`Path not found: ${start.displayPath}`);
1496
1558
  }
1497
- const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
1498
- const flags = input.message.caseSensitive === false ? "i" : "";
1499
- const regex = new RegExp(input.message.pattern, flags);
1500
- 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;
1501
1563
  const lines = [];
1502
1564
  let matchCount = 0;
1503
1565
  let truncated = false;
1504
- for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1566
+ for (const entry of walkWorkerEntries(branchPath, start)) {
1505
1567
  if (!entry.stat.isFile()) continue;
1506
- const repoRelative = entry.virtualPath.slice(1);
1507
- if (globRegex && !globRegex.test(repoRelative)) continue;
1508
- 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
+ }
1509
1575
  if (!isProbablyText(bytes)) continue;
1510
1576
  const fileLines = bytes.toString("utf8").split(/\r?\n/);
1511
1577
  for (let index = 0; index < fileLines.length; index += 1) {
@@ -1516,7 +1582,7 @@ async function executeGrepOperation(input) {
1516
1582
  matchCount += 1;
1517
1583
  if (lines.length < limit) {
1518
1584
  const column = Math.max(1, (match.index ?? 0) + 1);
1519
- 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)}`);
1520
1586
  } else {
1521
1587
  truncated = true;
1522
1588
  }
@@ -1531,26 +1597,35 @@ async function executeGrepOperation(input) {
1531
1597
  truncated
1532
1598
  };
1533
1599
  }
1534
- async function executeFindOperation(input) {
1600
+ async function executeGrepOperation(input) {
1535
1601
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1536
- 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 ?? ".");
1537
1612
  if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
1538
- throw new Error(`Directory not found: ${start.virtualPath}`);
1613
+ throw new Error(`Directory not found: ${start.displayPath}`);
1539
1614
  }
1540
- const pattern = normalizeFindPattern(input.message.pattern ?? "*");
1615
+ const pattern = normalizeFindPattern(input.pattern ?? "*");
1541
1616
  const matcher = wildcardToRegex(pattern);
1542
- const entryType = input.message.entryType ?? "file";
1543
- const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1617
+ const entryType = input.entryType ?? "file";
1618
+ const limit = normalizeToolLimit(input.limit, 200, 1e3);
1544
1619
  const matches = [];
1545
1620
  let count = 0;
1546
1621
  let truncated = false;
1547
- for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
1548
- if (entry.virtualPath === start.virtualPath) continue;
1622
+ for (const entry of walkWorkerEntries(branchPath, start)) {
1623
+ if (entry.absolutePath === start.absolutePath) continue;
1549
1624
  const isDirectory = entry.stat.isDirectory();
1550
1625
  if (entryType === "file" && !entry.stat.isFile()) continue;
1551
1626
  if (entryType === "directory" && !isDirectory) continue;
1552
- const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
1553
- 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;
1554
1629
  count += 1;
1555
1630
  if (matches.length < limit) {
1556
1631
  matches.push(label);
@@ -1566,25 +1641,59 @@ async function executeFindOperation(input) {
1566
1641
  truncated
1567
1642
  };
1568
1643
  }
1569
- async function executeLsOperation(input) {
1644
+ async function executeFindOperation(input) {
1570
1645
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1571
- 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);
1572
1655
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
1573
- throw new Error(`Directory not found: ${resolved.virtualPath}`);
1656
+ throw new Error(`Directory not found: ${resolved.displayPath}`);
1574
1657
  }
1575
- const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
1658
+ const limit = normalizeToolLimit(inputLimit, 200, 1e3);
1576
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));
1577
1660
  const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
1578
1661
  const truncated = entries.length > displayed.length;
1579
1662
  const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
1580
1663
  return {
1581
1664
  type: "ls",
1582
- path: resolved.virtualPath,
1665
+ path: resolved.displayPath,
1583
1666
  content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
1584
1667
  count: entries.length,
1585
1668
  truncated
1586
1669
  };
1587
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
+ }
1588
1697
  async function executeViewFileBytesOperation(input) {
1589
1698
  const artifact = await resolveArtifactEnvFilePath({
1590
1699
  filePath: input.message.filePath,
@@ -1599,44 +1708,25 @@ async function executeViewFileBytesOperation(input) {
1599
1708
  if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
1600
1709
  throw new Error(`File not found: ${artifact.virtualPath}`);
1601
1710
  }
1602
- const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
1603
- const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
1604
- 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) {
1605
1714
  throw new Error("read supports PNG and JPEG image inputs only");
1606
1715
  }
1607
- const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
1608
- const dimensions2 = readImageDimensions(bytes2, mediaType2);
1716
+ const bytes = import_node_fs.default.readFileSync(artifact.absolutePath);
1717
+ const dimensions = readImageDimensions(bytes, mediaType);
1609
1718
  return {
1610
1719
  type: "view_file_bytes",
1611
1720
  filePath: artifact.virtualPath,
1612
- mediaType: mediaType2,
1613
- base64: bytes2.toString("base64"),
1614
- fileSizeBytes: bytes2.length,
1615
- width: dimensions2.width,
1616
- height: dimensions2.height
1721
+ mediaType,
1722
+ base64: bytes.toString("base64"),
1723
+ fileSizeBytes: bytes.length,
1724
+ width: dimensions.width,
1725
+ height: dimensions.height
1617
1726
  };
1618
1727
  }
1619
1728
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1620
- const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
1621
- if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
1622
- throw new Error(`File not found: ${resolved.virtualPath}`);
1623
- }
1624
- const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
1625
- const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
1626
- if (!mediaType) {
1627
- throw new Error("read supports PNG and JPEG image inputs only");
1628
- }
1629
- const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
1630
- const dimensions = readImageDimensions(bytes, mediaType);
1631
- return {
1632
- type: "view_file_bytes",
1633
- filePath: resolved.virtualPath,
1634
- mediaType,
1635
- base64: bytes.toString("base64"),
1636
- fileSizeBytes: bytes.length,
1637
- width: dimensions.width,
1638
- height: dimensions.height
1639
- };
1729
+ return readWorkerImageFile(workspace.branchPath, input.message.filePath);
1640
1730
  }
1641
1731
  function stagedBlobSizeBytes(context) {
1642
1732
  const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
@@ -1817,6 +1907,7 @@ async function executeCommand(input) {
1817
1907
  cwd,
1818
1908
  stdout: "pipe",
1819
1909
  stderr: "pipe",
1910
+ detached: true,
1820
1911
  env: {
1821
1912
  ...process.env,
1822
1913
  ...containerRegistryEnv(),
@@ -1838,7 +1929,12 @@ async function executeCommand(input) {
1838
1929
  });
1839
1930
  if (input.message.timeoutMs) {
1840
1931
  timeout = setTimeout(() => {
1841
- subprocess.kill("SIGTERM");
1932
+ void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
1933
+ process.stderr.write(
1934
+ `[r5d-worker] failed to terminate timed-out command ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
1935
+ `
1936
+ );
1937
+ });
1842
1938
  }, input.message.timeoutMs);
1843
1939
  }
1844
1940
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -1874,6 +1970,8 @@ async function executeCommand(input) {
1874
1970
  async function executeStreamingCommand(input) {
1875
1971
  const startedAt = Date.now();
1876
1972
  let started = false;
1973
+ let timedOut = false;
1974
+ let timeout;
1877
1975
  try {
1878
1976
  if (cancelledProcessRuns.delete(input.message.runId)) {
1879
1977
  sendWorkerMessage(input.ws, {
@@ -1914,6 +2012,7 @@ async function executeStreamingCommand(input) {
1914
2012
  cwd,
1915
2013
  stdout: "pipe",
1916
2014
  stderr: "pipe",
2015
+ detached: true,
1917
2016
  env: {
1918
2017
  ...process.env,
1919
2018
  ...containerRegistryEnv(),
@@ -1940,6 +2039,17 @@ async function executeStreamingCommand(input) {
1940
2039
  requestId: input.message.requestId,
1941
2040
  runId: input.message.runId
1942
2041
  });
2042
+ if (input.message.timeoutMs) {
2043
+ timeout = setTimeout(() => {
2044
+ timedOut = true;
2045
+ void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
2046
+ process.stderr.write(
2047
+ `[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
2048
+ `
2049
+ );
2050
+ });
2051
+ }, input.message.timeoutMs);
2052
+ }
1943
2053
  const [exitCode] = await Promise.all([
1944
2054
  subprocess.exited,
1945
2055
  streamCommandOutput(subprocess.stdout, (data) => {
@@ -1963,7 +2073,8 @@ async function executeStreamingCommand(input) {
1963
2073
  type: "exec_exit",
1964
2074
  runId: input.message.runId,
1965
2075
  exitCode,
1966
- durationMs: Date.now() - startedAt
2076
+ durationMs: Date.now() - startedAt,
2077
+ ...timedOut ? { timedOut: true } : {}
1967
2078
  });
1968
2079
  } catch (error) {
1969
2080
  const message = error instanceof Error ? error.message : String(error);
@@ -1983,6 +2094,9 @@ async function executeStreamingCommand(input) {
1983
2094
  });
1984
2095
  }
1985
2096
  } finally {
2097
+ if (timeout) {
2098
+ clearTimeout(timeout);
2099
+ }
1986
2100
  activeProcesses.delete(input.message.runId);
1987
2101
  cancelledProcessRuns.delete(input.message.runId);
1988
2102
  }
@@ -2545,18 +2659,27 @@ async function startWorker(options) {
2545
2659
  }
2546
2660
  if (message.type === "cancel") {
2547
2661
  const active = activeProcesses.get(message.runId);
2662
+ let cancelled = true;
2663
+ let cancelMessage;
2548
2664
  if (active) {
2549
- active.process.kill("SIGTERM");
2665
+ try {
2666
+ await (0, import_process_tree.terminateProcessTree)(active.process);
2667
+ cancelMessage = `Stopped ${active.command}`;
2668
+ } catch (error) {
2669
+ cancelled = false;
2670
+ cancelMessage = `Failed to stop ${active.command}: ${error instanceof Error ? error.message : String(error)}`;
2671
+ }
2550
2672
  } else {
2551
2673
  cancelledProcessRuns.add(message.runId);
2674
+ cancelMessage = "Cancellation queued before command start";
2552
2675
  }
2553
2676
  ws.send(
2554
2677
  JSON.stringify({
2555
2678
  type: "cancel_result",
2556
2679
  requestId: message.requestId,
2557
2680
  runId: message.runId,
2558
- cancelled: true,
2559
- message: active ? `Sent SIGTERM to ${active.command}` : "Cancellation queued before command start"
2681
+ cancelled,
2682
+ message: cancelMessage
2560
2683
  })
2561
2684
  );
2562
2685
  return;
@@ -2796,11 +2919,17 @@ if (isCliEntrypoint()) {
2796
2919
  0 && (module.exports = {
2797
2920
  ensureVisibleGitCheckout,
2798
2921
  findRepositorySyncBlockers,
2922
+ findWorkerFiles,
2923
+ grepWorkerFiles,
2799
2924
  isArtifactEnvPath,
2925
+ listWorkerDirectory,
2800
2926
  prepareArtifactEnvForShell,
2801
2927
  preparePlanEnvForShell,
2928
+ readWorkerImageFile,
2929
+ readWorkerTextFile,
2802
2930
  resolveHostShell,
2803
2931
  resolveProjectFilePath,
2932
+ resolveWorkerFilePath,
2804
2933
  syncManifestProjectsFromInternal,
2805
2934
  syncProjectPlans,
2806
2935
  syncSessionArtifacts
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "type": "commonjs"
5
5
  }