@hasna/loops 0.3.3 → 0.3.4

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.
@@ -347,6 +347,7 @@ function rowToLoop(row) {
347
347
  status: row.status,
348
348
  schedule: JSON.parse(row.schedule_json),
349
349
  target: JSON.parse(row.target_json),
350
+ machine: row.machine_json ? JSON.parse(row.machine_json) : undefined,
350
351
  nextRunAt: row.next_run_at ?? undefined,
351
352
  retryScheduledFor: row.retry_scheduled_for ?? undefined,
352
353
  catchUp: row.catch_up,
@@ -489,6 +490,7 @@ class Store {
489
490
  status TEXT NOT NULL,
490
491
  schedule_json TEXT NOT NULL,
491
492
  target_json TEXT NOT NULL,
493
+ machine_json TEXT,
492
494
  next_run_at TEXT,
493
495
  retry_scheduled_for TEXT,
494
496
  catch_up TEXT NOT NULL,
@@ -610,10 +612,14 @@ class Store {
610
612
  );
611
613
  CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
612
614
  `);
615
+ try {
616
+ this.db.query("ALTER TABLE loops ADD COLUMN machine_json TEXT").run();
617
+ } catch {}
613
618
  try {
614
619
  this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN pid INTEGER").run();
615
620
  } catch {}
616
621
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
622
+ this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
617
623
  }
618
624
  assertDaemonLeaseFence(opts = {}, now = nowIso()) {
619
625
  if (!opts.daemonLeaseId)
@@ -631,6 +637,7 @@ class Store {
631
637
  status: "active",
632
638
  schedule: input.schedule,
633
639
  target: input.target,
640
+ machine: input.machine,
634
641
  nextRunAt: initialNextRun(input.schedule, from),
635
642
  catchUp: input.catchUp ?? "latest",
636
643
  catchUpLimit: input.catchUpLimit ?? 50,
@@ -642,9 +649,9 @@ class Store {
642
649
  createdAt: now,
643
650
  updatedAt: now
644
651
  };
645
- this.db.query(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, next_run_at, retry_scheduled_for,
652
+ this.db.query(`INSERT INTO loops (id, name, description, status, schedule_json, target_json, machine_json, next_run_at, retry_scheduled_for,
646
653
  catch_up, catch_up_limit, overlap, max_attempts, retry_delay_ms, lease_ms, expires_at, created_at, updated_at)
647
- VALUES ($id, $name, $description, $status, $schedule, $target, $nextRun, NULL, $catchUp, $catchUpLimit,
654
+ VALUES ($id, $name, $description, $status, $schedule, $target, $machine, $nextRun, NULL, $catchUp, $catchUpLimit,
648
655
  $overlap, $maxAttempts, $retryDelay, $leaseMs, $expiresAt, $created, $updated)`).run({
649
656
  $id: loop.id,
650
657
  $name: loop.name,
@@ -652,6 +659,7 @@ class Store {
652
659
  $status: loop.status,
653
660
  $schedule: JSON.stringify(loop.schedule),
654
661
  $target: JSON.stringify(loop.target),
662
+ $machine: loop.machine ? JSON.stringify(loop.machine) : null,
655
663
  $nextRun: loop.nextRunAt ?? null,
656
664
  $catchUp: loop.catchUp,
657
665
  $catchUpLimit: loop.catchUpLimit,
@@ -1510,8 +1518,9 @@ import { hostname as hostname2 } from "os";
1510
1518
  import { spawn as spawn2 } from "child_process";
1511
1519
 
1512
1520
  // src/lib/executor.ts
1513
- import { spawn } from "child_process";
1521
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
1514
1522
  import { once } from "events";
1523
+ import { resolveMachineCommand } from "@hasna/machines/consumer";
1515
1524
 
1516
1525
  // src/lib/accounts.ts
1517
1526
  import { spawnSync } from "child_process";
@@ -1668,6 +1677,59 @@ function commandNotFoundMessage(command, env = process.env) {
1668
1677
  return `Executable not found in PATH: ${command}. Effective PATH=${env.PATH || "(empty)"}`;
1669
1678
  }
1670
1679
 
1680
+ // src/lib/machines.ts
1681
+ import {
1682
+ discoverMachineTopology,
1683
+ resolveMachineRoute
1684
+ } from "@hasna/machines/consumer";
1685
+ function compact(value) {
1686
+ const text = value?.trim();
1687
+ return text ? text : undefined;
1688
+ }
1689
+ function entryToSummary(entry, topology) {
1690
+ return {
1691
+ id: entry.machine_id,
1692
+ hostname: compact(entry.hostname),
1693
+ platform: compact(entry.platform),
1694
+ user: compact(entry.user),
1695
+ workspacePath: compact(entry.workspace_path),
1696
+ route: entry.ssh.route,
1697
+ local: entry.machine_id === topology.local_machine_id || entry.ssh.route === "local",
1698
+ heartbeatStatus: entry.heartbeat_status,
1699
+ tailscaleOnline: entry.tailscale.online,
1700
+ tags: entry.tags
1701
+ };
1702
+ }
1703
+ function machineFromRoute(route, topology) {
1704
+ if (!route.ok || !route.machine_id) {
1705
+ throw new Error(`OpenMachines route not found for machine: ${route.requested_machine_id}`);
1706
+ }
1707
+ const entry = topology.machines.find((machine) => machine.machine_id === route.machine_id);
1708
+ return {
1709
+ id: route.machine_id,
1710
+ requestedId: route.requested_machine_id !== route.machine_id ? route.requested_machine_id : undefined,
1711
+ route: route.route,
1712
+ local: route.local,
1713
+ confidence: route.confidence,
1714
+ workspacePath: compact(entry?.workspace_path),
1715
+ resolvedAt: route.generated_at,
1716
+ packageVersion: route.package.version,
1717
+ warnings: route.warnings.length ? route.warnings : undefined
1718
+ };
1719
+ }
1720
+ function listOpenMachines() {
1721
+ const topology = discoverMachineTopology();
1722
+ return topology.machines.map((entry) => entryToSummary(entry, topology));
1723
+ }
1724
+ function resolveLoopMachine(machineId) {
1725
+ const topology = discoverMachineTopology();
1726
+ const route = resolveMachineRoute(machineId, { topology });
1727
+ return machineFromRoute(route, topology);
1728
+ }
1729
+ function refreshLoopMachine(machine) {
1730
+ return resolveLoopMachine(machine.id);
1731
+ }
1732
+
1671
1733
  // src/lib/executor.ts
1672
1734
  var DEFAULT_TIMEOUT_MS = 30 * 60000;
1673
1735
  var DEFAULT_MAX_OUTPUT_BYTES = 256 * 1024;
@@ -1688,6 +1750,23 @@ var AUTH_ENV_KEYS = [
1688
1750
  "XDG_STATE_HOME",
1689
1751
  "XDG_CACHE_HOME"
1690
1752
  ];
1753
+ var TRANSPORT_ENV_KEYS = new Set([
1754
+ "BUN_INSTALL",
1755
+ "HOME",
1756
+ "LANG",
1757
+ "LANGUAGE",
1758
+ "LOGNAME",
1759
+ "PATH",
1760
+ "SHELL",
1761
+ "SSH_AGENT_PID",
1762
+ "SSH_AUTH_SOCK",
1763
+ "TERM",
1764
+ "TMP",
1765
+ "TMPDIR",
1766
+ "TEMP",
1767
+ "USER",
1768
+ "XDG_RUNTIME_DIR"
1769
+ ]);
1691
1770
  function appendBounded(current, chunk, maxBytes) {
1692
1771
  const next = current + chunk.toString("utf8");
1693
1772
  if (Buffer.byteLength(next, "utf8") <= maxBytes)
@@ -1714,6 +1793,29 @@ function killProcessGroup(pid) {
1714
1793
  }
1715
1794
  }, 2000).unref();
1716
1795
  }
1796
+ function shellQuote(value) {
1797
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1798
+ }
1799
+ function metadataEnv(metadata) {
1800
+ const env = {};
1801
+ if (metadata.loopId)
1802
+ env.LOOPS_LOOP_ID = metadata.loopId;
1803
+ if (metadata.loopName)
1804
+ env.LOOPS_LOOP_NAME = metadata.loopName;
1805
+ if (metadata.runId)
1806
+ env.LOOPS_RUN_ID = metadata.runId;
1807
+ if (metadata.scheduledFor)
1808
+ env.LOOPS_SCHEDULED_FOR = metadata.scheduledFor;
1809
+ if (metadata.workflowId)
1810
+ env.LOOPS_WORKFLOW_ID = metadata.workflowId;
1811
+ if (metadata.workflowName)
1812
+ env.LOOPS_WORKFLOW_NAME = metadata.workflowName;
1813
+ if (metadata.workflowRunId)
1814
+ env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
1815
+ if (metadata.workflowStepId)
1816
+ env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
1817
+ return env;
1818
+ }
1717
1819
  function providerCommand(provider) {
1718
1820
  switch (provider) {
1719
1821
  case "claude":
@@ -1835,26 +1937,213 @@ function executionEnv(spec, metadata, opts) {
1835
1937
  }
1836
1938
  Object.assign(env, spec.env ?? {});
1837
1939
  env.PATH = normalizeExecutionPath(env);
1838
- if (metadata.loopId)
1839
- env.LOOPS_LOOP_ID = metadata.loopId;
1840
- if (metadata.loopName)
1841
- env.LOOPS_LOOP_NAME = metadata.loopName;
1842
- if (metadata.runId)
1843
- env.LOOPS_RUN_ID = metadata.runId;
1844
- if (metadata.scheduledFor)
1845
- env.LOOPS_SCHEDULED_FOR = metadata.scheduledFor;
1846
- if (metadata.workflowId)
1847
- env.LOOPS_WORKFLOW_ID = metadata.workflowId;
1848
- if (metadata.workflowName)
1849
- env.LOOPS_WORKFLOW_NAME = metadata.workflowName;
1850
- if (metadata.workflowRunId)
1851
- env.LOOPS_WORKFLOW_RUN_ID = metadata.workflowRunId;
1852
- if (metadata.workflowStepId)
1853
- env.LOOPS_WORKFLOW_STEP_ID = metadata.workflowStepId;
1940
+ Object.assign(env, metadataEnv(metadata));
1854
1941
  return env;
1855
1942
  }
1943
+ function resolvedMachine(opts) {
1944
+ if (!opts.machine)
1945
+ return;
1946
+ return (opts.machineResolver ?? refreshLoopMachine)(opts.machine);
1947
+ }
1948
+ function commandForShell(spec) {
1949
+ if (!spec.args.length)
1950
+ return spec.command;
1951
+ return [spec.command, ...spec.args.map(shellQuote)].join(" ");
1952
+ }
1953
+ function hereDoc(value) {
1954
+ let delimiter2 = `__OPENLOOPS_STDIN_${Math.random().toString(36).slice(2).toUpperCase()}__`;
1955
+ while (value.split(/\r?\n/).includes(delimiter2)) {
1956
+ delimiter2 = `__OPENLOOPS_STDIN_${Math.random().toString(36).slice(2).toUpperCase()}__`;
1957
+ }
1958
+ return [`cat > "$__OPENLOOPS_STDIN" <<'${delimiter2}'`, value, delimiter2];
1959
+ }
1960
+ function remoteBootstrapLines(spec, metadata) {
1961
+ const lines = [
1962
+ "set -e",
1963
+ 'export PATH="$HOME/.local/bin:$HOME/.bun/bin:$HOME/.cargo/bin:$HOME/.npm-global/bin:$HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin${PATH:+:$PATH}"'
1964
+ ];
1965
+ if (spec.cwd)
1966
+ lines.push(`cd ${shellQuote(spec.cwd)}`);
1967
+ if (spec.account) {
1968
+ if (!spec.accountTool)
1969
+ throw new Error("account.tool is required when no provider tool can be inferred");
1970
+ lines.push("if ! command -v accounts >/dev/null 2>&1; then echo 'accounts CLI is not available on remote machine' >&2; exit 127; fi", `unset ${AUTH_ENV_KEYS.join(" ")}`, `eval "$(accounts env ${shellQuote(spec.account.profile)} --tool ${shellQuote(spec.accountTool)})"`, `export LOOPS_ACCOUNT_PROFILE=${shellQuote(spec.account.profile)}`, `export LOOPS_ACCOUNT_TOOL=${shellQuote(spec.accountTool)}`);
1971
+ }
1972
+ for (const [key, value] of Object.entries({ ...metadataEnv(metadata), ...spec.env ?? {} })) {
1973
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
1974
+ continue;
1975
+ lines.push(`export ${key}=${shellQuote(value)}`);
1976
+ }
1977
+ return lines;
1978
+ }
1979
+ function remoteScript(spec, metadata) {
1980
+ const lines = remoteBootstrapLines(spec, metadata);
1981
+ let stdinRedirect = "";
1982
+ if (spec.stdin !== undefined) {
1983
+ lines.push('__OPENLOOPS_STDIN="$(mktemp -t openloops-stdin.XXXXXX)"', `trap 'rm -f "$__OPENLOOPS_STDIN"' EXIT`);
1984
+ lines.push(...hereDoc(spec.stdin));
1985
+ stdinRedirect = ' < "$__OPENLOOPS_STDIN"';
1986
+ }
1987
+ const invocation = spec.shell ? `sh -lc ${shellQuote(commandForShell(spec))}${stdinRedirect}` : `${[spec.command, ...spec.args].map(shellQuote).join(" ")}${stdinRedirect}`;
1988
+ lines.push(invocation);
1989
+ return `${lines.join(`
1990
+ `)}
1991
+ `;
1992
+ }
1993
+ function remotePreflightScript(spec, metadata) {
1994
+ return [
1995
+ ...remoteBootstrapLines(spec, metadata),
1996
+ "command -v bash >/dev/null 2>&1",
1997
+ `command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
1998
+ ].join(`
1999
+ `);
2000
+ }
2001
+ function transportEnv(opts) {
2002
+ const source = opts.env ?? process.env;
2003
+ const env = {};
2004
+ for (const [key, value] of Object.entries(source)) {
2005
+ if (value === undefined)
2006
+ continue;
2007
+ if (TRANSPORT_ENV_KEYS.has(key) || key.startsWith("LC_"))
2008
+ env[key] = value;
2009
+ }
2010
+ env.PATH = normalizeExecutionPath(env);
2011
+ return env;
2012
+ }
2013
+ function preflightRemoteSpec(spec, machine, metadata, opts) {
2014
+ const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
2015
+ const result = spawnSync2(plan.command, plan.args, {
2016
+ encoding: "utf8",
2017
+ env: transportEnv(opts),
2018
+ input: remotePreflightScript(spec, metadata),
2019
+ stdio: ["pipe", "pipe", "pipe"],
2020
+ timeout: 15000
2021
+ });
2022
+ if (result.error)
2023
+ throw new Error(`remote preflight failed on ${machine.id}: ${result.error.message}`);
2024
+ if ((result.status ?? 1) !== 0) {
2025
+ const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
2026
+ throw new Error(`remote preflight failed on ${machine.id}${detail ? `: ${detail}` : ""}`);
2027
+ }
2028
+ }
2029
+ async function executeRemoteSpec(spec, machine, metadata, opts) {
2030
+ const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
2031
+ const startedAt = nowIso();
2032
+ let stdout = "";
2033
+ let stderr = "";
2034
+ let timedOut = false;
2035
+ let exitCode;
2036
+ let error;
2037
+ let plan;
2038
+ let script;
2039
+ try {
2040
+ plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
2041
+ script = remoteScript(spec, metadata);
2042
+ } catch (err) {
2043
+ return {
2044
+ status: "failed",
2045
+ stdout: "",
2046
+ stderr: "",
2047
+ error: err instanceof Error ? err.message : String(err),
2048
+ startedAt,
2049
+ finishedAt: nowIso(),
2050
+ durationMs: 0
2051
+ };
2052
+ }
2053
+ const child = spawn(plan.command, plan.args, {
2054
+ env: transportEnv(opts),
2055
+ detached: true,
2056
+ stdio: ["pipe", "pipe", "pipe"]
2057
+ });
2058
+ if (child.pid)
2059
+ opts.onSpawn?.(child.pid);
2060
+ child.stdin?.on("error", (err) => {
2061
+ if (err.code !== "EPIPE")
2062
+ error = err.message;
2063
+ });
2064
+ child.stdin?.end(script);
2065
+ const abortHandler = () => {
2066
+ error = "cancelled";
2067
+ if (child.pid)
2068
+ killProcessGroup(child.pid);
2069
+ };
2070
+ if (opts.signal?.aborted)
2071
+ abortHandler();
2072
+ opts.signal?.addEventListener("abort", abortHandler, { once: true });
2073
+ child.stdout?.on("data", (chunk) => {
2074
+ stdout = appendBounded(stdout, chunk, maxOutputBytes);
2075
+ });
2076
+ child.stderr?.on("data", (chunk) => {
2077
+ stderr = appendBounded(stderr, chunk, maxOutputBytes);
2078
+ });
2079
+ const timer = setTimeout(() => {
2080
+ timedOut = true;
2081
+ if (child.pid)
2082
+ killProcessGroup(child.pid);
2083
+ }, spec.timeoutMs);
2084
+ timer.unref();
2085
+ try {
2086
+ const [code, signal] = await once(child, "exit");
2087
+ if (typeof code === "number")
2088
+ exitCode = code;
2089
+ if (signal)
2090
+ error = `terminated by ${signal}`;
2091
+ } catch (err) {
2092
+ error = err instanceof Error ? err.message : String(err);
2093
+ } finally {
2094
+ clearTimeout(timer);
2095
+ opts.signal?.removeEventListener("abort", abortHandler);
2096
+ }
2097
+ const finishedAt = nowIso();
2098
+ const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
2099
+ if (timedOut) {
2100
+ return {
2101
+ status: "timed_out",
2102
+ exitCode,
2103
+ stdout,
2104
+ stderr,
2105
+ error: `timed out after ${spec.timeoutMs}ms`,
2106
+ pid: child.pid,
2107
+ startedAt,
2108
+ finishedAt,
2109
+ durationMs
2110
+ };
2111
+ }
2112
+ if (error || exitCode !== 0) {
2113
+ return {
2114
+ status: "failed",
2115
+ exitCode,
2116
+ stdout,
2117
+ stderr,
2118
+ error: error ?? `remote process on ${machine.id} exited with code ${exitCode ?? "unknown"}`,
2119
+ pid: child.pid,
2120
+ startedAt,
2121
+ finishedAt,
2122
+ durationMs
2123
+ };
2124
+ }
2125
+ return {
2126
+ status: "succeeded",
2127
+ exitCode,
2128
+ stdout,
2129
+ stderr,
2130
+ pid: child.pid,
2131
+ startedAt,
2132
+ finishedAt,
2133
+ durationMs
2134
+ };
2135
+ }
1856
2136
  function preflightTarget(target, metadata = {}, opts = {}) {
1857
2137
  const spec = commandSpec(target);
2138
+ const machine = resolvedMachine(opts);
2139
+ if (machine && !machine.local) {
2140
+ preflightRemoteSpec(spec, machine, metadata, opts);
2141
+ return {
2142
+ command: spec.command,
2143
+ accountProfile: spec.account?.profile,
2144
+ accountTool: spec.accountTool
2145
+ };
2146
+ }
1858
2147
  const env = executionEnv(spec, metadata, opts);
1859
2148
  if (!spec.shell && !executableExists(spec.command, env)) {
1860
2149
  throw new Error(commandNotFoundMessage(spec.command, env));
@@ -1867,6 +2156,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
1867
2156
  }
1868
2157
  async function executeTarget(target, metadata = {}, opts = {}) {
1869
2158
  const spec = commandSpec(target);
2159
+ const machine = resolvedMachine(opts);
2160
+ if (machine && !machine.local)
2161
+ return executeRemoteSpec(spec, machine, metadata, opts);
1870
2162
  const maxOutputBytes = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
1871
2163
  const startedAt = nowIso();
1872
2164
  let stdout = "";
@@ -1982,7 +2274,7 @@ async function executeLoop(loop, run, opts = {}) {
1982
2274
  loopName: loop.name,
1983
2275
  runId: run.id,
1984
2276
  scheduledFor: run.scheduledFor
1985
- }, opts);
2277
+ }, { ...opts, machine: opts.machine ?? loop.machine });
1986
2278
  }
1987
2279
 
1988
2280
  // src/lib/workflow-runner.ts
@@ -2084,6 +2376,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
2084
2376
  try {
2085
2377
  result = await executeTarget(targetWithStepAccount(step), metadata, {
2086
2378
  ...opts,
2379
+ machine: opts.machine ?? opts.loop?.machine,
2087
2380
  signal: controller.signal,
2088
2381
  onSpawn: (pid) => {
2089
2382
  opts.beforePersist?.();
@@ -2683,7 +2976,7 @@ async function startDaemon(opts) {
2683
2976
 
2684
2977
  // src/daemon/install.ts
2685
2978
  import { chmodSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
2686
- import { spawnSync as spawnSync2 } from "child_process";
2979
+ import { spawnSync as spawnSync3 } from "child_process";
2687
2980
  import { dirname as dirname3 } from "path";
2688
2981
  function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
2689
2982
  const command = [execPath, cliEntry, ...args].join(" ");
@@ -2753,7 +3046,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
2753
3046
  function enableStartup(result) {
2754
3047
  const commands = result.platform === "linux" ? ["systemctl --user daemon-reload", "systemctl --user enable --now loops-daemon.service"] : result.platform === "darwin" ? [`launchctl load -w ${result.path}`] : [];
2755
3048
  return commands.map((command) => {
2756
- const run = spawnSync2("sh", ["-c", command], {
3049
+ const run = spawnSync3("sh", ["-c", command], {
2757
3050
  encoding: "utf8",
2758
3051
  stdio: ["ignore", "pipe", "pipe"]
2759
3052
  });
@@ -2768,7 +3061,7 @@ function enableStartup(result) {
2768
3061
 
2769
3062
  // src/daemon/index.ts
2770
3063
  var program = new Command;
2771
- program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.3");
3064
+ program.name("loops-daemon").description("OpenLoops daemon helper").version("0.3.4");
2772
3065
  program.command("run").option("--interval-ms <ms>", "tick interval", (value) => Number(value)).action(async (opts) => runDaemon({ intervalMs: opts.intervalMs }));
2773
3066
  program.command("start").action(async () => {
2774
3067
  const result = await startDaemon({ cliEntry: process.argv[1] ?? "loops-daemon", args: ["run"] });
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { LoopsClientOptions } from "./sdk/index.js";
4
4
  export { Store } from "./lib/store.js";
5
5
  export { parseDuration, parseCron, nextCronRun, initialNextRun, computeNextAfter } from "./lib/schedule.js";
6
6
  export { executeLoop, executeTarget, preflightTarget } from "./lib/executor.js";
7
+ export { listOpenMachines, refreshLoopMachine, resolveLoopMachine } from "./lib/machines.js";
7
8
  export { tick } from "./lib/scheduler.js";
8
9
  export { executeWorkflow, executeLoopTarget, preflightWorkflow } from "./lib/workflow-runner.js";
9
10
  export { workflowExecutionOrder, workflowBodyFromJson } from "./lib/workflow-spec.js";