@hasna/workflows 0.1.2 → 0.1.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.
package/dist/mcp/index.js CHANGED
@@ -18,12 +18,12 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
20
  // src/lanes/types.ts
21
- import { statSync } from "fs";
21
+ import { statSync as statSync2 } from "fs";
22
22
  function binaryOnPath(binary) {
23
23
  const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
24
24
  for (const entry of pathEntries) {
25
25
  try {
26
- const stat = statSync(`${entry}/${binary}`);
26
+ const stat = statSync2(`${entry}/${binary}`);
27
27
  if (stat.isFile())
28
28
  return true;
29
29
  } catch {}
@@ -298,10 +298,12 @@ function createCursorAdapter(deps = {}) {
298
298
  try {
299
299
  const run = await withTimeout(runLocalAgent(sdk, job), timeoutMs, `cursor lane (${job.prompt.slice(0, 60)})`);
300
300
  const durationMs = Date.now() - started;
301
- const status = run?.status ?? "finished";
301
+ const status = run.status;
302
302
  const ok = status === "finished";
303
- return { ok, exitCode: ok ? 0 : 1, output: run?.result ?? "", durationMs };
303
+ return { ok, exitCode: ok ? 0 : 1, output: run.result, durationMs };
304
304
  } catch (err) {
305
+ if (err instanceof LaneAdapterShapeError)
306
+ throw err;
305
307
  const durationMs = Date.now() - started;
306
308
  return { ok: false, exitCode: 1, output: "", durationMs, error: String(err instanceof Error ? err.message : err) };
307
309
  }
@@ -322,7 +324,14 @@ async function runLocalAgent(sdk, job) {
322
324
  if (!run || typeof run !== "object") {
323
325
  throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned no Run");
324
326
  }
325
- return run;
327
+ const candidate = run;
328
+ if (typeof candidate.status !== "string" || !["finished", "error", "cancelled"].includes(candidate.status)) {
329
+ throw new LaneAdapterShapeError(`@cursor/sdk Agent.send() returned a Run without a terminal status (got ${JSON.stringify(candidate.status)}; expected finished|error|cancelled)`);
330
+ }
331
+ if (typeof candidate.result !== "string") {
332
+ throw new LaneAdapterShapeError("@cursor/sdk Agent.send() returned a Run without a string result");
333
+ }
334
+ return { status: candidate.status, result: candidate.result };
326
335
  }
327
336
  function runCursorCli(job, cliPath, started, timeoutMs) {
328
337
  try {
@@ -817,6 +826,18 @@ function validateGraph(graph) {
817
826
  if (typeof graph.version !== "string" || graph.version.trim() === "") {
818
827
  issue(issues, "version", "graph requires a version string");
819
828
  }
829
+ if (graph.memoWatch !== undefined) {
830
+ if (!Array.isArray(graph.memoWatch) || graph.memoWatch.length === 0) {
831
+ issue(issues, "memoWatch", "memoWatch must be a non-empty array of file paths or globs");
832
+ } else {
833
+ for (const entry of graph.memoWatch) {
834
+ if (typeof entry !== "string" || entry.trim() === "") {
835
+ issue(issues, "memoWatch", "each memoWatch entry must be a non-empty path or glob string");
836
+ break;
837
+ }
838
+ }
839
+ }
840
+ }
820
841
  if (!Array.isArray(graph.nodes) || graph.nodes.length === 0) {
821
842
  issue(issues, "nodes", "graph requires at least one node");
822
843
  return { ok: false, issues };
@@ -975,9 +996,6 @@ function validateNode(graph, node, issues) {
975
996
  }
976
997
  }
977
998
 
978
- // src/daemon.ts
979
- import { createHash as createHash2 } from "crypto";
980
-
981
999
  // src/secrets.ts
982
1000
  class SecretsGateError extends Error {
983
1001
  constructor(message) {
@@ -1054,6 +1072,8 @@ function assertNoSecrets(value, what) {
1054
1072
 
1055
1073
  // src/session.ts
1056
1074
  import { createHash } from "crypto";
1075
+ import { readFileSync, statSync } from "fs";
1076
+ import { isAbsolute, join } from "path";
1057
1077
  var DEFAULT_MAX_ATTEMPTS = 3;
1058
1078
  function repairTornRuns(store, wal, opts = {}) {
1059
1079
  const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
@@ -1109,6 +1129,46 @@ function recordMemo(store, graphName, nodeId, input, output) {
1109
1129
  const key = memoKey(graphName, nodeId, input);
1110
1130
  store.memoPut(key, graphName, nodeId, inputHash(input), JSON.stringify(output));
1111
1131
  }
1132
+ function memoWatchFingerprint(graph, dataDir) {
1133
+ if (!graph.memoWatch || graph.memoWatch.length === 0)
1134
+ return;
1135
+ const entries = [];
1136
+ for (const pattern of graph.memoWatch) {
1137
+ if (hasGlobMagic(pattern)) {
1138
+ const matches = [...new Bun.Glob(pattern).scanSync({ cwd: dataDir, onlyFiles: true })].sort();
1139
+ for (const match of matches) {
1140
+ const path2 = isAbsolute(match) ? match : join(dataDir, match);
1141
+ entries.push(fingerprintFile(pattern, path2));
1142
+ }
1143
+ continue;
1144
+ }
1145
+ const path = isAbsolute(pattern) ? pattern : join(dataDir, pattern);
1146
+ entries.push(fingerprintFile(pattern, path));
1147
+ }
1148
+ return entries;
1149
+ }
1150
+ function hasGlobMagic(pattern) {
1151
+ return /[*?[\]{}]/.test(pattern);
1152
+ }
1153
+ function fingerprintFile(pattern, path) {
1154
+ try {
1155
+ const stat = statSync(path);
1156
+ if (!stat.isFile()) {
1157
+ return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
1158
+ }
1159
+ const content = readFileSync(path);
1160
+ return {
1161
+ pattern,
1162
+ path,
1163
+ missing: false,
1164
+ mtimeMs: stat.mtimeMs,
1165
+ size: content.byteLength,
1166
+ sha256: createHash("sha256").update(content).digest("hex")
1167
+ };
1168
+ } catch {
1169
+ return { pattern, path, missing: true, mtimeMs: null, size: null, sha256: null };
1170
+ }
1171
+ }
1112
1172
  function findRunByIdempotencyKey(store, key) {
1113
1173
  const runs = store.listRuns({ limit: 1000 });
1114
1174
  for (const run of runs) {
@@ -1383,10 +1443,13 @@ class WorkflowsDaemon {
1383
1443
  }
1384
1444
  async executeStep(run, graph, context, wf, node) {
1385
1445
  const nodeId = node.id;
1386
- const existing = this.store.listRunNodes(run.id).find((n) => n.nodeId === nodeId);
1387
- const nodeRow = existing ?? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" });
1388
- if (node.memo) {
1389
- const memoInput = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
1446
+ const latest = this.store.getLatestRunNode(run.id, nodeId);
1447
+ const isLoopBody = graph.nodes.some((candidate) => candidate.type === "while" && candidate.body.includes(nodeId));
1448
+ const nodeRow = latest === undefined || isLoopBody && latest.status === "completed" ? this.store.createRunNode({ runId: run.id, nodeId, lane: node.lane ?? "claude" }) : latest;
1449
+ const memoWatch = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
1450
+ const memoSource = { command: node.command, prompt: node.prompt, context: stripRunState(context) };
1451
+ const memoInput = node.memo ? memoWatch !== undefined ? { ...memoSource, memoWatch } : memoSource : undefined;
1452
+ if (memoInput !== undefined) {
1390
1453
  const hit = tryMemoHit(this.store, run.graphName, nodeId, memoInput);
1391
1454
  if (hit) {
1392
1455
  const cached = JSON.parse(hit.outputJson);
@@ -1415,9 +1478,9 @@ class WorkflowsDaemon {
1415
1478
  }
1416
1479
  const output = { ok: result.ok, exitCode: result.exitCode, output: result.output, durationMs: result.durationMs };
1417
1480
  if (!result.ok) {
1418
- const retriesLeft = (node.maxRetries ?? 0) + 1 - nodeRow.attempts;
1419
- if (retriesLeft > 0) {
1420
- this.store.bumpAttemptsNode(nodeRow.id);
1481
+ const shouldRetry = nodeRow.attempts < (node.maxRetries ?? 0);
1482
+ this.store.bumpAttemptsNode(nodeRow.id);
1483
+ if (shouldRetry) {
1421
1484
  this.store.setRunNodeStatus(nodeRow.id, "pending", { error: result.error ?? `exit ${result.exitCode}` });
1422
1485
  return { kind: "advanced", nodeId };
1423
1486
  }
@@ -1438,9 +1501,13 @@ class WorkflowsDaemon {
1438
1501
  }
1439
1502
  this.store.setRunNodeStatus(nodeRow.id, "completed", { exitCode: result.exitCode, output });
1440
1503
  this.wal.append({ op: "node_finished", runId: run.id, nodeId, status: "completed", at: nowIso() });
1441
- if (node.memo) {
1442
- recordMemo(this.store, run.graphName, nodeId, { command: node.command, prompt: node.prompt, context: stripRunState(context) }, output);
1443
- this.wal.append({ op: "memo_set", key: `${run.graphName}:${nodeId}:${createHash2("sha256").update(JSON.stringify({ command: node.command, prompt: node.prompt, context: stripRunState(context) })).digest("hex")}`, at: nowIso() });
1504
+ if (memoInput !== undefined) {
1505
+ const memoInputAfter = node.memo ? memoWatchFingerprint(graph, this.store.dataDir) : undefined;
1506
+ const stable = memoWatch === undefined && memoInputAfter === undefined || memoWatch !== undefined && memoInputAfter !== undefined && JSON.stringify(memoWatch) === JSON.stringify(memoInputAfter);
1507
+ if (stable) {
1508
+ recordMemo(this.store, run.graphName, nodeId, memoInput, output);
1509
+ this.wal.append({ op: "memo_set", key: memoKey(run.graphName, nodeId, memoInput), at: nowIso() });
1510
+ }
1444
1511
  }
1445
1512
  this.recordStepResult(context, nodeId, output);
1446
1513
  return this.afterNode(run, context, wf, node.next);
@@ -1535,6 +1602,7 @@ async function runGraphToCompletion(store, wal, graph, context, options = {}) {
1535
1602
  isolatedRunId: options.isolatedRunId
1536
1603
  });
1537
1604
  const run = daemon.startRun(graph, context);
1605
+ options.onRunCreated?.(run);
1538
1606
  daemon.isolateRun(run.id);
1539
1607
  const maxCycles = options.maxCycles ?? 500;
1540
1608
  for (let cycle = 0;cycle < maxCycles; cycle++) {
@@ -1555,17 +1623,41 @@ init_lanes();
1555
1623
  // src/store.ts
1556
1624
  import { Database } from "bun:sqlite";
1557
1625
  import { mkdirSync } from "fs";
1558
- import { join } from "path";
1626
+ import { join as join2 } from "path";
1559
1627
  import { randomUUID } from "crypto";
1560
1628
  function nowIso2() {
1561
1629
  return new Date().toISOString();
1562
1630
  }
1631
+ var BUSY_TIMEOUT_MS = 3000;
1632
+ var BUSY_RETRIES = 2;
1633
+ var BUSY_RETRY_SLEEP_MS = 50;
1634
+ function isBusyError(err) {
1635
+ if (!(err instanceof Error))
1636
+ return false;
1637
+ const code = String(err.code ?? "");
1638
+ return code.includes("BUSY") || /\bdatabase is locked\b/i.test(err.message);
1639
+ }
1640
+ function retryBusy(fn) {
1641
+ let last;
1642
+ for (let attempt = 0;attempt <= BUSY_RETRIES; attempt++) {
1643
+ try {
1644
+ return fn();
1645
+ } catch (err) {
1646
+ if (!isBusyError(err))
1647
+ throw err;
1648
+ last = err;
1649
+ Bun.sleepSync(BUSY_RETRY_SLEEP_MS);
1650
+ }
1651
+ }
1652
+ throw last;
1653
+ }
1563
1654
  function openStore(dataDir) {
1564
1655
  mkdirSync(dataDir, { recursive: true });
1565
- const db = new Database(join(dataDir, "workflows.db"));
1566
- db.exec("PRAGMA journal_mode = WAL");
1567
- db.exec("PRAGMA foreign_keys = ON");
1568
- db.exec(`
1656
+ const db = new Database(join2(dataDir, "workflows.db"));
1657
+ retryBusy(() => db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`));
1658
+ retryBusy(() => db.exec("PRAGMA journal_mode = WAL"));
1659
+ retryBusy(() => db.exec("PRAGMA foreign_keys = ON"));
1660
+ retryBusy(() => db.exec(`
1569
1661
  CREATE TABLE IF NOT EXISTS runs (
1570
1662
  id TEXT PRIMARY KEY,
1571
1663
  graph_name TEXT NOT NULL,
@@ -1605,7 +1697,7 @@ function openStore(dataDir) {
1605
1697
  CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
1606
1698
  CREATE INDEX IF NOT EXISTS idx_runs_created ON runs(created_at);
1607
1699
  CREATE INDEX IF NOT EXISTS idx_run_nodes_run ON run_nodes(run_id);
1608
- `);
1700
+ `));
1609
1701
  const rowToRun = (r) => ({
1610
1702
  id: String(r.id),
1611
1703
  graphName: String(r.graph_name),
@@ -1643,31 +1735,33 @@ function openStore(dataDir) {
1643
1735
  createdAt: String(r.created_at)
1644
1736
  });
1645
1737
  return {
1738
+ dataDir,
1646
1739
  close() {
1647
1740
  db.close();
1648
1741
  },
1649
1742
  listTables() {
1650
- const rows = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
1743
+ const rows = retryBusy(() => db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all());
1651
1744
  return rows.map((r) => r.name);
1652
1745
  },
1653
1746
  createRun(input) {
1654
1747
  const id = randomUUID();
1655
1748
  const now = nowIso2();
1656
- db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
1657
- VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now);
1749
+ retryBusy(() => db.query(`INSERT INTO runs (id, graph_name, graph_version, status, attempts, context_json, created_at, updated_at)
1750
+ VALUES (?, ?, ?, 'pending', 0, ?, ?, ?)`).run(id, input.graphName, input.graphVersion, JSON.stringify(input.context ?? {}), now, now));
1658
1751
  return this.getRun(id);
1659
1752
  },
1660
1753
  getRun(id) {
1661
- const row = db.query(`SELECT * FROM runs WHERE id = ?`).get(id);
1754
+ const row = retryBusy(() => db.query(`SELECT * FROM runs WHERE id = ?`).get(id));
1662
1755
  return row ? rowToRun(row) : undefined;
1663
1756
  },
1664
1757
  listRuns(opts = {}) {
1665
1758
  const limit = Math.min(opts.limit ?? 100, 1000);
1666
1759
  if (opts.status) {
1667
- const rows2 = db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(opts.status, limit);
1760
+ const status = opts.status;
1761
+ const rows2 = retryBusy(() => db.query(`SELECT * FROM runs WHERE status = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(status, limit));
1668
1762
  return rows2.map((r) => rowToRun(r));
1669
1763
  }
1670
- const rows = db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit);
1764
+ const rows = retryBusy(() => db.query(`SELECT * FROM runs ORDER BY created_at DESC, id DESC LIMIT ?`).all(limit));
1671
1765
  return rows.map((r) => rowToRun(r));
1672
1766
  },
1673
1767
  setRunStatus(id, status, patch = {}) {
@@ -1678,28 +1772,32 @@ function openStore(dataDir) {
1678
1772
  const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing?.startedAt === null ? now : existing?.startedAt ?? null;
1679
1773
  const terminal = status === "completed" || status === "failed" || status === "cancelled" || status === "interrupted";
1680
1774
  const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing?.finishedAt === null ? now : existing?.finishedAt ?? null;
1681
- db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id);
1775
+ retryBusy(() => db.query(`UPDATE runs SET status = ?, result_json = ?, error = ?, started_at = ?, finished_at = ?, updated_at = ? WHERE id = ?`).run(status, resultJson, error, startedAt, finishedAt, now, id));
1682
1776
  },
1683
1777
  bumpAttempts(id) {
1684
- db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id);
1778
+ retryBusy(() => db.query(`UPDATE runs SET attempts = attempts + 1, updated_at = ? WHERE id = ?`).run(nowIso2(), id));
1685
1779
  },
1686
1780
  setRunContext(id, context) {
1687
- db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id);
1781
+ retryBusy(() => db.query(`UPDATE runs SET context_json = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(context), nowIso2(), id));
1688
1782
  },
1689
1783
  createRunNode(input) {
1690
1784
  const id = randomUUID();
1691
- db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
1692
- VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null);
1785
+ retryBusy(() => db.query(`INSERT INTO run_nodes (id, run_id, node_id, status, attempts, lane)
1786
+ VALUES (?, ?, ?, 'pending', 0, ?)`).run(id, input.runId, input.nodeId, input.lane ?? null));
1693
1787
  return this.getRunNode(id);
1694
1788
  },
1695
1789
  getRunNode(id) {
1696
- const row = db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id);
1790
+ const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE id = ?`).get(id));
1697
1791
  return row ? rowToNode(row) : undefined;
1698
1792
  },
1699
1793
  listRunNodes(runId) {
1700
- const rows = db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId);
1794
+ const rows = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? ORDER BY rowid ASC`).all(runId));
1701
1795
  return rows.map((r) => rowToNode(r));
1702
1796
  },
1797
+ getLatestRunNode(runId, nodeId) {
1798
+ const row = retryBusy(() => db.query(`SELECT * FROM run_nodes WHERE run_id = ? AND node_id = ? ORDER BY rowid DESC LIMIT 1`).get(runId, nodeId));
1799
+ return row ? rowToNode(row) : undefined;
1800
+ },
1703
1801
  setRunNodeStatus(id, status, patch = {}) {
1704
1802
  const now = nowIso2();
1705
1803
  const existing = this.getRunNode(id);
@@ -1711,44 +1809,44 @@ function openStore(dataDir) {
1711
1809
  const startedAt = patch.startedAt !== undefined ? patch.startedAt : status === "running" && existing.startedAt === null ? now : existing.startedAt;
1712
1810
  const terminal = status === "completed" || status === "failed" || status === "skipped";
1713
1811
  const finishedAt = patch.finishedAt !== undefined ? patch.finishedAt : terminal && existing.finishedAt === null ? now : existing.finishedAt;
1714
- db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id);
1812
+ retryBusy(() => db.query(`UPDATE run_nodes SET status = ?, exit_code = ?, output_json = ?, error = ?, started_at = ?, finished_at = ? WHERE id = ?`).run(status, exitCode, outputJson, error, startedAt, finishedAt, id));
1715
1813
  },
1716
1814
  bumpAttemptsNode(id) {
1717
- db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id);
1815
+ retryBusy(() => db.query(`UPDATE run_nodes SET attempts = attempts + 1 WHERE id = ?`).run(id));
1718
1816
  },
1719
1817
  memoPut(key, graphName, nodeId, inputHash2, outputJson) {
1720
- db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
1721
- VALUES (?, ?, ?, ?, ?, 0, ?)
1722
- ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2());
1818
+ retryBusy(() => db.query(`INSERT INTO memos (key, graph_name, node_id, input_hash, output_json, hit_count, created_at)
1819
+ VALUES (?, ?, ?, ?, ?, 0, ?)
1820
+ ON CONFLICT(key) DO UPDATE SET output_json = excluded.output_json`).run(key, graphName, nodeId, inputHash2, outputJson, nowIso2()));
1723
1821
  },
1724
1822
  memoGet(key) {
1725
- const row = db.query(`SELECT * FROM memos WHERE key = ?`).get(key);
1823
+ const row = retryBusy(() => db.query(`SELECT * FROM memos WHERE key = ?`).get(key));
1726
1824
  return row ? rowToMemo(row) : undefined;
1727
1825
  },
1728
1826
  memoHit(key) {
1729
- db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key);
1827
+ retryBusy(() => db.query(`UPDATE memos SET hit_count = hit_count + 1 WHERE key = ?`).run(key));
1730
1828
  },
1731
1829
  memoList() {
1732
- const rows = db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all();
1830
+ const rows = retryBusy(() => db.query(`SELECT * FROM memos ORDER BY created_at DESC LIMIT 1000`).all());
1733
1831
  return rows.map((r) => rowToMemo(r));
1734
1832
  },
1735
1833
  memoClear() {
1736
- db.exec(`DELETE FROM memos`);
1834
+ retryBusy(() => db.exec(`DELETE FROM memos`));
1737
1835
  }
1738
1836
  };
1739
1837
  }
1740
1838
 
1741
1839
  // src/wal.ts
1742
- import { createHash as createHash3 } from "crypto";
1743
- import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync, renameSync, truncateSync, writeFileSync } from "fs";
1744
- import { join as join2 } from "path";
1840
+ import { createHash as createHash2 } from "crypto";
1841
+ import { appendFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, truncateSync, writeFileSync } from "fs";
1842
+ import { join as join3 } from "path";
1745
1843
  var WAL_FILE_NAME = "session.wal";
1746
1844
  var SESSIONS_DIR_NAME = "sessions";
1747
1845
  var MAX_WAL_BYTES = 256 * 1024 * 1024;
1748
1846
  function checksumOf(op) {
1749
1847
  if (op === undefined || op === null)
1750
1848
  return "";
1751
- return createHash3("sha256").update(JSON.stringify(op)).digest("hex");
1849
+ return createHash2("sha256").update(JSON.stringify(op)).digest("hex");
1752
1850
  }
1753
1851
 
1754
1852
  class SessionWAL {
@@ -1757,9 +1855,9 @@ class SessionWAL {
1757
1855
  this.filePath = filePath;
1758
1856
  }
1759
1857
  static open(dataDir) {
1760
- mkdirSync2(join2(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1761
- const canonical = join2(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1762
- const legacy = join2(dataDir, WAL_FILE_NAME);
1858
+ mkdirSync2(join3(dataDir, SESSIONS_DIR_NAME), { recursive: true });
1859
+ const canonical = join3(dataDir, SESSIONS_DIR_NAME, WAL_FILE_NAME);
1860
+ const legacy = join3(dataDir, WAL_FILE_NAME);
1763
1861
  if (existsSync(legacy) && !existsSync(canonical)) {
1764
1862
  try {
1765
1863
  renameSync(legacy, canonical);
@@ -1791,7 +1889,7 @@ class SessionWAL {
1791
1889
  let repaired = false;
1792
1890
  let content;
1793
1891
  try {
1794
- content = readFileSync(this.filePath, "utf8");
1892
+ content = readFileSync2(this.filePath, "utf8");
1795
1893
  } catch {
1796
1894
  return { entries, torn, repaired, liveClaims: () => new Map };
1797
1895
  }
@@ -1974,13 +2072,10 @@ async function callWorkflowTool(service, name, args) {
1974
2072
  throw new Error(`Unknown tool: ${name}`);
1975
2073
  }
1976
2074
  }
1977
-
1978
- // src/service.ts
1979
- import { join as join3 } from "path";
1980
2075
  // package.json
1981
2076
  var package_default = {
1982
2077
  name: "@hasna/workflows",
1983
- version: "0.1.2",
2078
+ version: "0.1.4",
1984
2079
  description: "Universal graph workflow app \u2014 graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
1985
2080
  type: "module",
1986
2081
  main: "./dist/index.js",
@@ -2050,20 +2145,48 @@ var package_default = {
2050
2145
  author: "Hasna",
2051
2146
  license: "Apache-2.0",
2052
2147
  dependencies: {
2053
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
2148
+ "@anthropic-ai/claude-agent-sdk": "0.3.234",
2054
2149
  "@cursor/sdk": "^1.0.28",
2055
- "@openai/codex-sdk": "^0.147.0",
2150
+ "@hasna/paths": "0.1.0",
2151
+ "@openai/codex-sdk": "0.147.0",
2056
2152
  commander: "^13.1.0"
2057
2153
  },
2058
2154
  devDependencies: {
2059
- "@hasna/contracts": "0.14.1",
2060
- "@types/bun": "latest",
2155
+ "@hasna/contracts": "0.14.2",
2156
+ "@types/bun": "1.3.14",
2061
2157
  "@types/pg": "^8.15.6",
2062
2158
  pg: "^8.16.3",
2063
2159
  typescript: "^5.7.3"
2064
2160
  }
2065
2161
  };
2066
2162
 
2163
+ // src/paths.ts
2164
+ import { existsSync as existsSync2 } from "fs";
2165
+ import { homedir } from "os";
2166
+ import { join as join4 } from "path";
2167
+ import { dataDir } from "@hasna/paths";
2168
+ var APP = "workflows";
2169
+ var STORE_DB_FILE = "workflows.db";
2170
+ function getHomeDir(env = process.env) {
2171
+ return env["HOME"] || env["USERPROFILE"] || homedir();
2172
+ }
2173
+ function getLegacyDataDir(env = process.env) {
2174
+ return join4(getHomeDir(env), ".hasna", APP);
2175
+ }
2176
+ function getResolverDataDir(env = process.env) {
2177
+ return dataDir({ app: APP, home: getHomeDir(env), env });
2178
+ }
2179
+ function adoptResolverDataDir(resolved, env = process.env) {
2180
+ const dataOverride = env.HASNA_DATA_HOME;
2181
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
2182
+ return true;
2183
+ return existsSync2(join4(resolved, STORE_DB_FILE));
2184
+ }
2185
+ function getEffectiveDataDir(env = process.env) {
2186
+ const resolved = getResolverDataDir(env);
2187
+ return adoptResolverDataDir(resolved, env) ? resolved : getLegacyDataDir(env);
2188
+ }
2189
+
2067
2190
  // src/service.ts
2068
2191
  var DEFAULT_PORT = 8790;
2069
2192
  var DEFAULT_HOST = "127.0.0.1";
@@ -2086,11 +2209,10 @@ function envInt(names, fallback) {
2086
2209
  return Number.isFinite(parsed) ? parsed : fallback;
2087
2210
  }
2088
2211
  function resolveWorkflowsConfig(overrides = {}) {
2089
- const home = process.env.HOME ?? process.env.USERPROFILE ?? ".";
2090
2212
  return {
2091
2213
  port: overrides.port ?? envInt(["HASNA_WORKFLOWS_PORT", "WORKFLOWS_PORT"], DEFAULT_PORT),
2092
2214
  host: overrides.host ?? envString(["HASNA_WORKFLOWS_HOST", "WORKFLOWS_HOST"]) ?? DEFAULT_HOST,
2093
- dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? join3(home, ".hasna", "workflows"),
2215
+ dataDir: overrides.dataDir ?? envString(["HASNA_WORKFLOWS_DATA_DIR", "WORKFLOWS_DATA_DIR"]) ?? getEffectiveDataDir(),
2094
2216
  apiUrl: overrides.apiUrl ?? envString(["WORKFLOWS_API_URL"]),
2095
2217
  apiKey: overrides.apiKey ?? envString(["WORKFLOWS_API_KEY"])
2096
2218
  };