svamp-cli 0.2.85 → 0.2.86

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/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-MQKWgYFS.mjs';
1
+ export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-rWXfNd7e.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -10,9 +10,9 @@ import 'node:fs';
10
10
  import 'util';
11
11
  import 'node:crypto';
12
12
  import 'node:path';
13
+ import 'node:os';
13
14
  import 'node:child_process';
14
15
  import '@agentclientprotocol/sdk';
15
- import 'node:os';
16
16
  import '@modelcontextprotocol/sdk/client/index.js';
17
17
  import '@modelcontextprotocol/sdk/client/stdio.js';
18
18
  import '@modelcontextprotocol/sdk/types.js';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.85";
2
+ var version = "0.2.86";
3
3
  var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
4
  var author = "Amun AI AB";
5
5
  var license = "SEE LICENSE IN LICENSE";
@@ -19,7 +19,7 @@ var exports$1 = {
19
19
  var scripts = {
20
20
  build: "rm -rf dist bin/skills && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && tsc --noEmit && pkgroll",
21
21
  typecheck: "tsc --noEmit",
22
- test: "npx tsx test/test-context-window.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs",
22
+ test: "npx tsx test/test-context-window.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-routine.mjs && npx tsx test/test-routine-rpc.mjs",
23
23
  "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
24
  dev: "tsx src/cli.ts",
25
25
  "dev:daemon": "tsx src/cli.ts daemon start-sync",
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
2
2
  import os from 'node:os';
3
3
  import { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
5
- import { c as connectToHypha, a as registerSessionService, D as generateHookSettings } from './run-MQKWgYFS.mjs';
5
+ import { c as connectToHypha, a as registerSessionService, E as generateHookSettings } from './run-rWXfNd7e.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -1,17 +1,17 @@
1
1
  import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os, { homedir as homedir$1 } from 'os';
2
2
  import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
3
- import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync, copyFileSync, unlinkSync as unlinkSync$1, watch, rmdirSync, readdirSync } from 'fs';
3
+ import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, copyFileSync, unlinkSync as unlinkSync$1, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
4
  import path__default, { join, dirname, resolve, basename } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
- import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync, unlinkSync } from 'node:fs';
8
+ import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
9
  import { promisify } from 'util';
10
- import { randomUUID, createHash } from 'node:crypto';
10
+ import { randomBytes, randomUUID, createHash } from 'node:crypto';
11
11
  import { join as join$1 } from 'node:path';
12
+ import os$1, { homedir, platform } from 'node:os';
12
13
  import { spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
13
14
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
14
- import os$1, { homedir, platform } from 'node:os';
15
15
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
16
16
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
17
17
  import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -1393,7 +1393,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1393
1393
  const tunnels = handlers.tunnels;
1394
1394
  if (!tunnels) throw new Error("Tunnel management not available");
1395
1395
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
1396
- const { FrpcTunnel } = await import('./frpc-D3LiH7GX.mjs');
1396
+ const { FrpcTunnel } = await import('./frpc-BfBqHN33.mjs');
1397
1397
  const tunnel = new FrpcTunnel({
1398
1398
  name: params.name,
1399
1399
  ports: params.ports,
@@ -1609,6 +1609,262 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1609
1609
  };
1610
1610
  }
1611
1611
 
1612
+ const FIELD_RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]];
1613
+ function parseField(token, [min, max]) {
1614
+ const set = /* @__PURE__ */ new Set();
1615
+ for (const part of token.split(",")) {
1616
+ let m;
1617
+ if (part === "*") {
1618
+ for (let i = min; i <= max; i++) set.add(i);
1619
+ } else if (m = part.match(/^\*\/(\d+)$/)) {
1620
+ const s = +m[1];
1621
+ for (let i = min; i <= max; i += s) set.add(i);
1622
+ } else if (m = part.match(/^(\d+)-(\d+)\/(\d+)$/)) {
1623
+ for (let i = +m[1]; i <= +m[2]; i += +m[3]) set.add(i);
1624
+ } else if (m = part.match(/^(\d+)-(\d+)$/)) {
1625
+ for (let i = +m[1]; i <= +m[2]; i++) set.add(i);
1626
+ } else if (m = part.match(/^(\d+)$/)) {
1627
+ set.add(+m[1]);
1628
+ } else throw new Error(`invalid cron field: "${token}"`);
1629
+ }
1630
+ for (const v of set) if (v < min || v > max) throw new Error(`cron value ${v} out of range [${min},${max}]`);
1631
+ return set;
1632
+ }
1633
+ function parseCron(expr) {
1634
+ const fields = String(expr).trim().split(/\s+/);
1635
+ if (fields.length !== 5) throw new Error(`cron must have 5 fields, got ${fields.length}: "${expr}"`);
1636
+ const [minute, hour, dom, month, dow] = fields.map((f, i) => parseField(f, FIELD_RANGES[i]));
1637
+ return { minute, hour, dom, month, dow, domRestricted: fields[2] !== "*", dowRestricted: fields[4] !== "*" };
1638
+ }
1639
+ function cronMatches(expr, date) {
1640
+ const c = typeof expr === "string" ? parseCron(expr) : expr;
1641
+ if (!c.minute.has(date.getMinutes())) return false;
1642
+ if (!c.hour.has(date.getHours())) return false;
1643
+ if (!c.month.has(date.getMonth() + 1)) return false;
1644
+ const domOk = c.dom.has(date.getDate());
1645
+ const dowOk = c.dow.has(date.getDay());
1646
+ if (c.domRestricted && c.dowRestricted) return domOk || dowOk;
1647
+ return domOk && dowOk;
1648
+ }
1649
+ function nextFire(expr, from) {
1650
+ const c = parseCron(expr);
1651
+ const d = new Date(from.getTime());
1652
+ d.setSeconds(0, 0);
1653
+ d.setMinutes(d.getMinutes() + 1);
1654
+ for (let i = 0; i < 366 * 24 * 60; i++) {
1655
+ if (cronMatches(c, d)) return new Date(d.getTime());
1656
+ d.setMinutes(d.getMinutes() + 1);
1657
+ }
1658
+ return null;
1659
+ }
1660
+ function resolvePath(ctx, path) {
1661
+ return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], ctx);
1662
+ }
1663
+ function renderTemplate(template, ctx) {
1664
+ return String(template).replace(/\$\{([\w.$]+)\}/g, (_m, p) => {
1665
+ const v = resolvePath(ctx, p);
1666
+ return v == null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
1667
+ });
1668
+ }
1669
+ const TRIGGER_TYPES = ["manual", "schedule", "webhook", "api"];
1670
+ const ACTION_KINDS = ["message", "loop"];
1671
+ const OVERLAP = ["queue", "skip", "replace"];
1672
+ function validateRoutine(r) {
1673
+ const errs = [];
1674
+ if (!r || typeof r !== "object") return ["routine must be an object"];
1675
+ if (!r.session_id) errs.push("session_id required");
1676
+ if (!r.name) errs.push("name required");
1677
+ const t = r.trigger;
1678
+ if (!t || !TRIGGER_TYPES.includes(t.type)) errs.push(`trigger.type must be one of ${TRIGGER_TYPES.join("|")}`);
1679
+ if (t?.type === "schedule") {
1680
+ try {
1681
+ parseCron(t.cron);
1682
+ } catch (e) {
1683
+ errs.push(`trigger.cron: ${e.message}`);
1684
+ }
1685
+ if (t.missed && !["catchup", "skip"].includes(t.missed)) errs.push("trigger.missed must be catchup|skip");
1686
+ }
1687
+ const a = r.action;
1688
+ if (!a || !ACTION_KINDS.includes(a.kind)) errs.push(`action.kind must be one of ${ACTION_KINDS.join("|")}`);
1689
+ if (a?.kind === "message" && !a.template) errs.push("action.template required for message action");
1690
+ if (a?.kind === "loop" && !a.loop && !a.task_template) errs.push("action.loop or action.task_template required for loop action");
1691
+ if (r.overlap && !OVERLAP.includes(r.overlap)) errs.push(`overlap must be one of ${OVERLAP.join("|")}`);
1692
+ return errs;
1693
+ }
1694
+
1695
+ function defaultRoutinesDir() {
1696
+ return process.env.SVAMP_ROUTINES_DIR || join$1(homedir(), ".svamp", "routines");
1697
+ }
1698
+ const genId = () => "rt_" + randomBytes(5).toString("hex");
1699
+ const genKey = () => randomBytes(18).toString("base64url");
1700
+ class RoutineStore {
1701
+ dir;
1702
+ constructor(dir = defaultRoutinesDir()) {
1703
+ this.dir = dir;
1704
+ mkdirSync$1(dir, { recursive: true });
1705
+ }
1706
+ _path(id) {
1707
+ return join$1(this.dir, `${id}.json`);
1708
+ }
1709
+ list(sessionId) {
1710
+ const all = readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
1711
+ try {
1712
+ return JSON.parse(readFileSync(join$1(this.dir, f), "utf8"));
1713
+ } catch {
1714
+ return null;
1715
+ }
1716
+ }).filter((r) => !!r);
1717
+ return sessionId ? all.filter((r) => r.session_id === sessionId) : all;
1718
+ }
1719
+ get(id) {
1720
+ try {
1721
+ return JSON.parse(readFileSync(this._path(id), "utf8"));
1722
+ } catch {
1723
+ return null;
1724
+ }
1725
+ }
1726
+ save(routine) {
1727
+ const r = { overlap: "queue", enabled: true, last_runs: [], ...routine };
1728
+ if (!r.id) r.id = genId();
1729
+ if ((r.trigger?.type === "webhook" || r.trigger?.type === "api") && !r.trigger.key) r.trigger.key = genKey();
1730
+ const errs = validateRoutine(r);
1731
+ if (errs.length) throw new Error("invalid routine: " + errs.join("; "));
1732
+ const tmp = this._path(r.id) + ".tmp";
1733
+ writeFileSync$1(tmp, JSON.stringify(r, null, 2));
1734
+ renameSync$1(tmp, this._path(r.id));
1735
+ return r;
1736
+ }
1737
+ remove(id) {
1738
+ const p = this._path(id);
1739
+ if (existsSync(p)) {
1740
+ rmSync(p);
1741
+ return true;
1742
+ }
1743
+ return false;
1744
+ }
1745
+ setEnabled(id, enabled) {
1746
+ const r = this.get(id);
1747
+ if (!r) return null;
1748
+ r.enabled = enabled;
1749
+ return this.save(r);
1750
+ }
1751
+ recordRun(id, entry) {
1752
+ const r = this.get(id);
1753
+ if (!r) return null;
1754
+ r.last_runs = [{ firedAt: (/* @__PURE__ */ new Date()).toISOString(), via: "manual", delivered: "message", outcome: "ok", ...entry }, ...r.last_runs || []].slice(0, 20);
1755
+ return this.save(r);
1756
+ }
1757
+ }
1758
+
1759
+ const minuteKey = (d) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${d.getHours()}-${d.getMinutes()}`;
1760
+ class RoutineRunner {
1761
+ store;
1762
+ deliver;
1763
+ now;
1764
+ onReplace;
1765
+ log;
1766
+ active = /* @__PURE__ */ new Set();
1767
+ _firedMinute = /* @__PURE__ */ new Map();
1768
+ constructor(o) {
1769
+ this.store = o.store;
1770
+ this.deliver = o.deliver;
1771
+ this.now = o.now || (() => /* @__PURE__ */ new Date());
1772
+ this.onReplace = o.onReplace;
1773
+ this.log = o.log || (() => {
1774
+ });
1775
+ }
1776
+ resolveAction(routine, payload = {}) {
1777
+ const ctx = { ...payload, now: this.now().toISOString(), routine: { id: routine.id, name: routine.name } };
1778
+ const a = routine.action;
1779
+ if (a.kind === "message") return { kind: "message", text: renderTemplate(a.template || "", ctx) };
1780
+ const task = renderTemplate(a.task_template || a.loop?.task || "", ctx);
1781
+ return { kind: "loop", task, loop: a.loop };
1782
+ }
1783
+ markDone(id) {
1784
+ this.active.delete(id);
1785
+ }
1786
+ _deliveredToday(routine) {
1787
+ const today = this.now().toDateString();
1788
+ return (routine.last_runs || []).filter((r) => r.outcome === "delivered" && new Date(r.firedAt).toDateString() === today).length;
1789
+ }
1790
+ async fire(routine, payload = {}, via = "manual") {
1791
+ if (!routine.enabled) return { skipped: "disabled" };
1792
+ if (routine.daily_cap && this._deliveredToday(this.store.get(routine.id) || routine) >= routine.daily_cap) {
1793
+ this.store.recordRun(routine.id, { via, delivered: routine.action.kind, outcome: "skipped (daily cap)" });
1794
+ return { skipped: "daily_cap" };
1795
+ }
1796
+ if (this.active.has(routine.id)) {
1797
+ if (routine.overlap === "skip") {
1798
+ this.store.recordRun(routine.id, { via, delivered: routine.action.kind, outcome: "skipped (busy)" });
1799
+ return { skipped: "busy" };
1800
+ }
1801
+ if (routine.overlap === "replace") {
1802
+ try {
1803
+ this.onReplace?.(routine.id);
1804
+ } catch {
1805
+ }
1806
+ this.active.delete(routine.id);
1807
+ }
1808
+ }
1809
+ const resolved = this.resolveAction(routine, payload);
1810
+ let outcome = "delivered";
1811
+ try {
1812
+ await this.deliver({ routine, action: routine.action, resolved, payload, via });
1813
+ if (resolved.kind === "loop") this.active.add(routine.id);
1814
+ } catch (e) {
1815
+ outcome = "error: " + (e?.message || e);
1816
+ }
1817
+ const r = this.store.get(routine.id) || routine;
1818
+ r.last_fired_at = this.now().toISOString();
1819
+ r.last_runs = [{ firedAt: r.last_fired_at, via, delivered: resolved.kind, outcome }, ...r.last_runs || []].slice(0, 20);
1820
+ this.store.save(r);
1821
+ return { fired: true, via, resolved, outcome };
1822
+ }
1823
+ async tick(date = this.now()) {
1824
+ const results = [];
1825
+ for (const r of this.store.list()) {
1826
+ if (!r.enabled || r.trigger?.type !== "schedule") continue;
1827
+ if (!cronMatches(r.trigger.cron, date)) continue;
1828
+ const mk = minuteKey(date);
1829
+ if (this._firedMinute.get(r.id) === mk) continue;
1830
+ this._firedMinute.set(r.id, mk);
1831
+ results.push({ id: r.id, ...await this.fire(r, { tick: date.toISOString() }, "schedule") });
1832
+ }
1833
+ return results;
1834
+ }
1835
+ async webhook(id, opts = {}) {
1836
+ const { key, method = "POST", body = {}, query = {} } = opts;
1837
+ const r = this.store.get(id);
1838
+ if (!r) return { status: 404, error: "routine not found" };
1839
+ if (r.trigger?.type !== "webhook" && r.trigger?.type !== "api") return { status: 400, error: "not a webhook routine" };
1840
+ if (!r.enabled) return { status: 409, error: "routine disabled" };
1841
+ if (!r.trigger.public && r.trigger.key && key !== r.trigger.key) return { status: 401, error: "bad key" };
1842
+ const methods = r.trigger.methods || ["GET", "POST"];
1843
+ if (!methods.includes(method)) return { status: 405, error: "method not allowed" };
1844
+ const res = await this.fire(r, { body, query }, r.trigger.type);
1845
+ return { status: 200, ...res };
1846
+ }
1847
+ async runNow(id, payload = {}) {
1848
+ const r = this.store.get(id);
1849
+ if (!r) return { error: "not found" };
1850
+ return this.fire(r, payload, "manual");
1851
+ }
1852
+ async catchUp(sinceDate, nowDate = this.now()) {
1853
+ const results = [];
1854
+ for (const r of this.store.list()) {
1855
+ if (!r.enabled || r.trigger?.type !== "schedule" || r.trigger.missed !== "catchup") continue;
1856
+ const deadlineMs = (r.trigger.deadline_sec || 3600) * 1e3;
1857
+ const since = new Date(Math.max(sinceDate.getTime(), nowDate.getTime() - deadlineMs));
1858
+ const due = nextFire(r.trigger.cron, since);
1859
+ if (due && due <= nowDate) {
1860
+ const last = r.last_fired_at ? new Date(r.last_fired_at) : /* @__PURE__ */ new Date(0);
1861
+ if (last < due) results.push({ id: r.id, ...await this.fire(r, { catchup: due.toISOString() }, "schedule") });
1862
+ }
1863
+ }
1864
+ return results;
1865
+ }
1866
+ }
1867
+
1612
1868
  function isStructuredMessage(msg) {
1613
1869
  return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId);
1614
1870
  }
@@ -1821,6 +2077,13 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1821
2077
  });
1822
2078
  return msg;
1823
2079
  };
2080
+ const routineStore = new RoutineStore();
2081
+ const syncRoutinesToMetadata = () => {
2082
+ metadata.routines = routineStore.list(sessionId);
2083
+ metadataVersion++;
2084
+ notifyListeners({ type: "update-session", sessionId, metadata: { value: metadata, version: metadataVersion } });
2085
+ callbacks.onMetadataUpdate?.(metadata);
2086
+ };
1824
2087
  const rpcHandlers = {
1825
2088
  // ── Messages ──
1826
2089
  getMessages: async (afterSeq, limit, context) => {
@@ -1948,6 +2211,49 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1948
2211
  callbacks.onUpdateConfig?.(patch);
1949
2212
  return { success: true };
1950
2213
  },
2214
+ // ── Routines (session-scoped triggers -> message/loop). CRUD + metadata
2215
+ // sync; actual schedule/webhook firing is handled by the supervised
2216
+ // routine server (`svamp routine serve`) reading the shared store. ──
2217
+ listRoutines: async (context) => {
2218
+ authorizeRequest(context, metadata.sharing, "view");
2219
+ return { routines: routineStore.list(sessionId) };
2220
+ },
2221
+ saveRoutine: async (routine, context) => {
2222
+ authorizeRequest(context, metadata.sharing, "admin");
2223
+ try {
2224
+ const saved = routineStore.save({ ...routine, session_id: sessionId });
2225
+ syncRoutinesToMetadata();
2226
+ return { success: true, routine: saved };
2227
+ } catch (e) {
2228
+ return { success: false, error: e?.message || String(e) };
2229
+ }
2230
+ },
2231
+ removeRoutine: async (id, context) => {
2232
+ authorizeRequest(context, metadata.sharing, "admin");
2233
+ const r = routineStore.get(id);
2234
+ if (r && r.session_id !== sessionId) return { success: false };
2235
+ const ok = routineStore.remove(id);
2236
+ syncRoutinesToMetadata();
2237
+ return { success: ok };
2238
+ },
2239
+ setRoutineEnabled: async (id, enabled, context) => {
2240
+ authorizeRequest(context, metadata.sharing, "admin");
2241
+ const r = routineStore.get(id);
2242
+ if (r && r.session_id !== sessionId) return { success: false };
2243
+ routineStore.setEnabled(id, enabled);
2244
+ syncRoutinesToMetadata();
2245
+ return { success: true };
2246
+ },
2247
+ runRoutineNow: async (id, context) => {
2248
+ authorizeRequest(context, metadata.sharing, "interact");
2249
+ const r = routineStore.get(id);
2250
+ if (!r || r.session_id !== sessionId) return { error: "not found" };
2251
+ const resolved = new RoutineRunner({ store: routineStore, deliver: async () => {
2252
+ } }).resolveAction(r, {});
2253
+ routineStore.recordRun(id, { via: "manual", delivered: resolved.kind, outcome: "resolved" });
2254
+ syncRoutinesToMetadata();
2255
+ return { success: true, resolved };
2256
+ },
1951
2257
  // ── Agent State ──
1952
2258
  getAgentState: async (context) => {
1953
2259
  authorizeRequest(context, metadata.sharing, "view");
@@ -5556,11 +5862,16 @@ var claudeAuth = /*#__PURE__*/Object.freeze({
5556
5862
  updateEnvFile: updateEnvFile
5557
5863
  });
5558
5864
 
5559
- const SVAMP_HOME$2 = process.env.SVAMP_HOME || join(homedir$1(), ".svamp");
5560
- const CACHE_FILE = join(SVAMP_HOME$2, "instance-config.json");
5865
+ function svampHome() {
5866
+ return process.env.SVAMP_HOME || join(homedir$1(), ".svamp");
5867
+ }
5868
+ function cacheFile() {
5869
+ return join(svampHome(), "instance-config.json");
5870
+ }
5561
5871
  const CONFIG_FILENAME = "svamp.json";
5562
5872
  let _config = null;
5563
5873
  let _loaded = false;
5874
+ let _cacheChecked = false;
5564
5875
  function stripSlash$1(u) {
5565
5876
  return u.replace(/\/+$/, "");
5566
5877
  }
@@ -5588,8 +5899,8 @@ async function fetchConfig(anchor) {
5588
5899
  }
5589
5900
  function readCache() {
5590
5901
  try {
5591
- if (existsSync$1(CACHE_FILE)) {
5592
- const data = JSON.parse(readFileSync$1(CACHE_FILE, "utf-8"));
5902
+ if (existsSync$1(cacheFile())) {
5903
+ const data = JSON.parse(readFileSync$1(cacheFile(), "utf-8"));
5593
5904
  if (data && typeof data === "object" && !Array.isArray(data)) return data;
5594
5905
  }
5595
5906
  } catch {
@@ -5598,8 +5909,8 @@ function readCache() {
5598
5909
  }
5599
5910
  function writeCache(cfg) {
5600
5911
  try {
5601
- mkdirSync(SVAMP_HOME$2, { recursive: true });
5602
- writeFileSync(CACHE_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
5912
+ mkdirSync(svampHome(), { recursive: true });
5913
+ writeFileSync(cacheFile(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
5603
5914
  } catch {
5604
5915
  }
5605
5916
  }
@@ -5618,38 +5929,43 @@ async function loadInstanceConfig(opts = {}) {
5618
5929
  applyConfig(fetched);
5619
5930
  writeCache(fetched);
5620
5931
  _loaded = true;
5932
+ _cacheChecked = true;
5621
5933
  return _config;
5622
5934
  }
5623
5935
  }
5624
5936
  const cached = readCache();
5625
5937
  if (cached) applyConfig(cached);
5626
5938
  _loaded = true;
5939
+ _cacheChecked = true;
5627
5940
  return _config;
5628
5941
  }
5629
5942
  function getLoadedConfig() {
5630
- if (!_loaded) {
5943
+ if (!_loaded && !_cacheChecked) {
5631
5944
  const cached = readCache();
5632
5945
  if (cached) applyConfig(cached);
5633
- _loaded = true;
5946
+ _cacheChecked = true;
5634
5947
  }
5635
5948
  return _config;
5636
5949
  }
5637
5950
  function clearInstanceConfigCache() {
5638
5951
  _config = null;
5639
5952
  _loaded = false;
5953
+ _cacheChecked = false;
5640
5954
  try {
5641
- if (existsSync$1(CACHE_FILE)) rmSync(CACHE_FILE);
5955
+ if (existsSync$1(cacheFile())) rmSync$1(cacheFile());
5642
5956
  } catch {
5643
5957
  }
5644
5958
  }
5645
5959
  function __setConfigForTest(cfg) {
5646
5960
  _config = cfg;
5647
5961
  _loaded = true;
5962
+ _cacheChecked = true;
5648
5963
  if (cfg) applyConfig(cfg);
5649
5964
  }
5650
5965
  function __resetForTest() {
5651
5966
  _config = null;
5652
5967
  _loaded = false;
5968
+ _cacheChecked = false;
5653
5969
  }
5654
5970
 
5655
5971
  var instanceConfig = /*#__PURE__*/Object.freeze({
@@ -7337,7 +7653,7 @@ function installBundledSkill(name) {
7337
7653
  mkdirSync(dst, { recursive: true });
7338
7654
  function copyDir(s, d) {
7339
7655
  mkdirSync(d, { recursive: true });
7340
- for (const entry of readdirSync(s, { withFileTypes: true })) {
7656
+ for (const entry of readdirSync$1(s, { withFileTypes: true })) {
7341
7657
  const sp = join(s, entry.name);
7342
7658
  const dp = join(d, entry.name);
7343
7659
  if (entry.isDirectory()) copyDir(sp, dp);
@@ -8301,7 +8617,7 @@ async function startDaemon(options) {
8301
8617
  }
8302
8618
  }
8303
8619
  try {
8304
- await loadInstanceConfig();
8620
+ await loadInstanceConfig({ force: true });
8305
8621
  } catch (err) {
8306
8622
  logger.log(`[config] Instance config load failed (continuing with env): ${err?.message || err}`);
8307
8623
  }
@@ -8369,7 +8685,7 @@ async function startDaemon(options) {
8369
8685
  const list = loadExposedTunnels().filter((t) => t.name !== name);
8370
8686
  saveExposedTunnels(list);
8371
8687
  }
8372
- const { ServeManager } = await import('./serveManager-j2Z-hrHL.mjs');
8688
+ const { ServeManager } = await import('./serveManager-BgnooFJZ.mjs');
8373
8689
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
8374
8690
  ensureAutoInstalledSkills(logger).catch(() => {
8375
8691
  });
@@ -10942,7 +11258,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
10942
11258
  const specs = loadExposedTunnels();
10943
11259
  if (specs.length === 0) return;
10944
11260
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
10945
- const { FrpcTunnel } = await import('./frpc-D3LiH7GX.mjs');
11261
+ const { FrpcTunnel } = await import('./frpc-BfBqHN33.mjs');
10946
11262
  for (const spec of specs) {
10947
11263
  if (tunnels.has(spec.name)) continue;
10948
11264
  try {
@@ -11689,4 +12005,4 @@ var run = /*#__PURE__*/Object.freeze({
11689
12005
  stopDaemon: stopDaemon
11690
12006
  });
11691
12007
 
11692
- export { mergeSecurityContexts as A, buildSessionShareUrl as B, buildMachineShareUrl as C, generateHookSettings as D, DefaultTransport$1 as E, acpBackend as F, acpAgentConfig as G, codexMcpBackend as H, GeminiTransport$1 as I, claudeAuth as J, instanceConfig as K, api as L, run as M, ServeAuth as S, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, getHyphaServerUrl as i, hasCookieToken as j, getSkillsServer as k, getSkillsWorkspaceName as l, getSkillsCollectionName as m, fetchWithTimeout as n, searchSkills as o, parseFrontmatter as p, SKILLS_DIR as q, registerMachineService as r, startDaemon as s, getSkillInfo as t, downloadSkillFile as u, listSkillFiles as v, normalizeAllowedUser as w, loadSecurityContextConfig as x, resolveSecurityContext as y, buildSecurityContextFromFlags as z };
12008
+ export { buildSecurityContextFromFlags as A, mergeSecurityContexts as B, buildSessionShareUrl as C, buildMachineShareUrl as D, generateHookSettings as E, DefaultTransport$1 as F, acpBackend as G, acpAgentConfig as H, codexMcpBackend as I, GeminiTransport$1 as J, claudeAuth as K, instanceConfig as L, api as M, run as N, RoutineStore as R, ServeAuth as S, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, getFrpsSubdomainHost as e, getFrpsServerPort as f, getHyphaServerUrl$1 as g, getFrpsServerAddr as h, getHyphaServerUrl as i, hasCookieToken as j, RoutineRunner as k, getSkillsServer as l, getSkillsWorkspaceName as m, getSkillsCollectionName as n, fetchWithTimeout as o, parseFrontmatter as p, searchSkills as q, registerMachineService as r, startDaemon as s, SKILLS_DIR as t, getSkillInfo as u, downloadSkillFile as v, listSkillFiles as w, normalizeAllowedUser as x, loadSecurityContextConfig as y, resolveSecurityContext as z };
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-B75aXb6C.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-Dj7Be8dw.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
@@ -93,7 +93,7 @@ async function serveAdd(args, machineId) {
93
93
  }
94
94
  }
95
95
  async function serveApply(args, machineId) {
96
- const { connectAndGetMachine } = await import('./commands-B75aXb6C.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-Dj7Be8dw.mjs');
97
97
  const fs = await import('fs');
98
98
  const yaml = await import('yaml');
99
99
  const file = positionalArgs(args)[0];
@@ -182,7 +182,7 @@ async function serveApply(args, machineId) {
182
182
  }
183
183
  }
184
184
  async function serveRemove(args, machineId) {
185
- const { connectAndGetMachine } = await import('./commands-B75aXb6C.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-Dj7Be8dw.mjs');
186
186
  const pos = positionalArgs(args);
187
187
  const name = pos[0];
188
188
  if (!name) {
@@ -202,7 +202,7 @@ async function serveRemove(args, machineId) {
202
202
  }
203
203
  }
204
204
  async function serveList(args, machineId) {
205
- const { connectAndGetMachine } = await import('./commands-B75aXb6C.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-Dj7Be8dw.mjs');
206
206
  const all = hasFlag(args, "--all", "-a");
207
207
  const json = hasFlag(args, "--json");
208
208
  const sessionId = getFlag(args, "--session");
@@ -235,7 +235,7 @@ async function serveList(args, machineId) {
235
235
  }
236
236
  }
237
237
  async function serveInfo(machineId) {
238
- const { connectAndGetMachine } = await import('./commands-B75aXb6C.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-Dj7Be8dw.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();
@@ -4,7 +4,7 @@ import * as fs from 'fs';
4
4
  import * as http from 'http';
5
5
  import * as net from 'net';
6
6
  import * as path from 'path';
7
- import { i as getHyphaServerUrl, S as ServeAuth, j as hasCookieToken } from './run-MQKWgYFS.mjs';
7
+ import { i as getHyphaServerUrl, S as ServeAuth, j as hasCookieToken } from './run-rWXfNd7e.mjs';
8
8
  import 'os';
9
9
  import 'fs/promises';
10
10
  import 'url';
@@ -12,9 +12,9 @@ import 'node:fs';
12
12
  import 'util';
13
13
  import 'node:crypto';
14
14
  import 'node:path';
15
+ import 'node:os';
15
16
  import 'node:child_process';
16
17
  import '@agentclientprotocol/sdk';
17
- import 'node:os';
18
18
  import '@modelcontextprotocol/sdk/client/index.js';
19
19
  import '@modelcontextprotocol/sdk/client/stdio.js';
20
20
  import '@modelcontextprotocol/sdk/types.js';
@@ -712,7 +712,7 @@ class ServeManager {
712
712
  const mount = this.mounts.get(mountName);
713
713
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, `static-${subdomainSafe}-${mount.linkToken}`]]) : void 0;
714
714
  try {
715
- const { FrpcTunnel } = await import('./frpc-D3LiH7GX.mjs');
715
+ const { FrpcTunnel } = await import('./frpc-BfBqHN33.mjs');
716
716
  let tunnel;
717
717
  tunnel = new FrpcTunnel({
718
718
  name: tunnelName,