svamp-cli 0.2.98 → 0.2.101
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/README.md +7 -5
- package/bin/skills/loop/IMPLEMENTATION_PROGRESS.md +49 -0
- package/bin/skills/loop/SKILL.md +99 -0
- package/bin/skills/loop/bin/channel-core.mjs +161 -0
- package/bin/skills/loop/bin/channel-server.mjs +151 -0
- package/bin/skills/loop/bin/inject-loop.mjs +41 -0
- package/bin/skills/loop/bin/loop-init.mjs +128 -0
- package/bin/skills/loop/bin/loop-status.mjs +38 -0
- package/bin/skills/loop/bin/precompact.mjs +27 -0
- package/bin/skills/loop/bin/routine-cli.mjs +121 -0
- package/bin/skills/loop/bin/routine-core.mjs +126 -0
- package/bin/skills/loop/bin/routine-runner.mjs +125 -0
- package/bin/skills/loop/bin/routine-store.mjs +49 -0
- package/bin/skills/loop/bin/state-fp.mjs +113 -0
- package/bin/skills/loop/bin/stop-gate.mjs +170 -0
- package/bin/skills/loop/routines.process.yaml +20 -0
- package/bin/skills/loop/test/test-channel-core.mjs +86 -0
- package/bin/skills/loop/test/test-loop-gate.mjs +246 -0
- package/bin/skills/loop/test/test-routine-core.mjs +54 -0
- package/bin/skills/loop/test/test-routine-engine.mjs +122 -0
- package/dist/{agentCommands-BULNvfKa.mjs → agentCommands-CAqLhLOH.mjs} +2 -2
- package/dist/{auth-BfDOBBPy.mjs → auth-CYA0e4mT.mjs} +1 -1
- package/dist/{caddy-BMbX-mFX.mjs → caddy-CuTbE3NY.mjs} +1 -14
- package/dist/cli.mjs +76 -77
- package/dist/{commands-C9DbNFz1.mjs → commands-B2uNdsyR.mjs} +2 -2
- package/dist/{commands-h2Dzb5m1.mjs → commands-Bxn_4u7d.mjs} +1 -1
- package/dist/{commands-DIhhodx8.mjs → commands-CdxEOPUt.mjs} +34 -42
- package/dist/{commands-qE4ZGLzB.mjs → commands-D-3h8H0C.mjs} +6 -6
- package/dist/{commands-FhGCsATM.mjs → commands-DRQUzw4j.mjs} +1 -1
- package/dist/{fleet-Cmma7Iu-.mjs → fleet-CNF84yJV.mjs} +1 -1
- package/dist/{frpc-BZ4l4-os.mjs → frpc-WVnBbyjf.mjs} +2 -15
- package/dist/{headlessCli-xRpI9fdk.mjs → headlessCli-DcP8eawK.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/package-DHxiXJ3N.mjs +63 -0
- package/dist/{run-DTIEcH-W.mjs → run-C7WSV8zx.mjs} +1 -1
- package/dist/{run-DxzG-3JD.mjs → run-CsMTSngP.mjs} +246 -709
- package/dist/{serveCommands-CzllIFB_.mjs → serveCommands-Can8WtLI.mjs} +5 -5
- package/dist/{serveManager-C6_Vloil.mjs → serveManager-DfETVSOb.mjs} +3 -3
- package/dist/{sideband-wPe3a3m1.mjs → sideband-C10Ni7p_.mjs} +1 -1
- package/package.json +3 -3
- package/dist/package-DD227VZO.mjs +0 -63
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic tests for routine-core (cron, next-fire, templating, validation).
|
|
3
|
+
import { parseCron, cronMatches, nextFire, renderTemplate, validateRoutine, inZone } from '../bin/routine-core.mjs';
|
|
4
|
+
|
|
5
|
+
let pass = 0, fail = 0;
|
|
6
|
+
function ok(c, m) { if (c) { pass++; console.log(` ✓ ${m}`); } else { fail++; console.log(` ✗ ${m}`); } }
|
|
7
|
+
function throws(fn, m) { try { fn(); fail++; console.log(` ✗ ${m} (did not throw)`); } catch { pass++; console.log(` ✓ ${m}`); } }
|
|
8
|
+
|
|
9
|
+
// Local-time Date helper (cron matches on local time).
|
|
10
|
+
const D = (y, mo, d, h, mi) => new Date(y, mo - 1, d, h, mi, 0, 0);
|
|
11
|
+
|
|
12
|
+
console.log('cron parsing');
|
|
13
|
+
ok(parseCron('*/5 * * * *').minute.has(5), 'parses */5 minutes');
|
|
14
|
+
throws(() => parseCron('* * * *'), 'rejects 4-field cron');
|
|
15
|
+
throws(() => parseCron('99 * * * *'), 'rejects out-of-range value');
|
|
16
|
+
|
|
17
|
+
console.log('cron matching');
|
|
18
|
+
ok(cronMatches('*/5 * * * *', D(2026, 6, 10, 9, 5)), '*/5 matches minute 5');
|
|
19
|
+
ok(!cronMatches('*/5 * * * *', D(2026, 6, 10, 9, 3)), '*/5 does not match minute 3');
|
|
20
|
+
ok(cronMatches('0 9 * * *', D(2026, 6, 10, 9, 0)), '0 9 matches 09:00');
|
|
21
|
+
ok(!cronMatches('0 9 * * *', D(2026, 6, 10, 10, 0)), '0 9 does not match 10:00');
|
|
22
|
+
// 2026-06-10 is a Wednesday (dow 3); 2026-06-13 is Saturday (dow 6)
|
|
23
|
+
ok(cronMatches('0 9 * * 1-5', D(2026, 6, 10, 9, 0)), 'weekday range matches Wednesday');
|
|
24
|
+
ok(!cronMatches('0 9 * * 1-5', D(2026, 6, 13, 9, 0)), 'weekday range excludes Saturday');
|
|
25
|
+
// dom+dow both restricted => OR semantics (match if EITHER matches)
|
|
26
|
+
ok(cronMatches('0 0 10 * 5', D(2026, 6, 10, 0, 0)), 'dom OR dow: dom=10 matches even though dow!=Fri');
|
|
27
|
+
ok(!cronMatches('0 0 13 * 5', D(2026, 6, 10, 0, 0)), 'dom OR dow: neither dom=13 nor dow=Fri matches');
|
|
28
|
+
|
|
29
|
+
console.log('next-fire');
|
|
30
|
+
{ const n = nextFire('*/5 * * * *', D(2026, 6, 10, 9, 2));
|
|
31
|
+
ok(n.getHours() === 9 && n.getMinutes() === 5, `next */5 after 09:02 is 09:05 (got ${n.getHours()}:${n.getMinutes()})`); }
|
|
32
|
+
{ const n = nextFire('0 0 * * *', D(2026, 6, 10, 9, 2));
|
|
33
|
+
ok(n.getDate() === 11 && n.getHours() === 0 && n.getMinutes() === 0, 'next daily-midnight rolls to next day 00:00'); }
|
|
34
|
+
|
|
35
|
+
console.log('timezone');
|
|
36
|
+
{ const utc = new Date('2026-06-10T12:00:00Z'); // June → EDT (UTC-4) → 08:00 New York
|
|
37
|
+
ok(inZone(utc, 'America/New_York').getHours() === 8, 'inZone converts UTC to tz wall-clock');
|
|
38
|
+
ok(cronMatches('0 8 * * *', inZone(utc, 'America/New_York')), 'cron 0 8 matches in America/New_York'); }
|
|
39
|
+
|
|
40
|
+
console.log('templating');
|
|
41
|
+
ok(renderTemplate('PR #${body.pr} by ${body.user}', { body: { pr: 42, user: 'wei' } }) === 'PR #42 by wei', 'templates body fields');
|
|
42
|
+
ok(renderTemplate('q=${query.q}', { query: { q: 'hi' } }) === 'q=hi', 'templates query fields');
|
|
43
|
+
ok(renderTemplate('missing=[${body.nope}]', { body: {} }) === 'missing=[]', 'missing var -> empty');
|
|
44
|
+
ok(renderTemplate('obj=${body.o}', { body: { o: { a: 1 } } }) === 'obj={"a":1}', 'object var -> JSON');
|
|
45
|
+
|
|
46
|
+
console.log('validation');
|
|
47
|
+
const good = { session_id: 's1', name: 'nightly', trigger: { type: 'schedule', cron: '0 2 * * *', missed: 'skip' }, action: { kind: 'message', template: 'go' }, overlap: 'queue' };
|
|
48
|
+
ok(validateRoutine(good).length === 0, 'valid routine passes');
|
|
49
|
+
ok(validateRoutine({ name: 'x', trigger: { type: 'schedule', cron: 'bad' }, action: { kind: 'message' } }).length >= 2, 'invalid routine collects errors');
|
|
50
|
+
ok(validateRoutine({ session_id: 's', name: 'w', trigger: { type: 'webhook', key: 'k' }, action: { kind: 'loop', task_template: 'do ${body.x}' } }).length === 0, 'webhook+loop routine valid');
|
|
51
|
+
ok(validateRoutine({ session_id: 's', name: 'w', trigger: { type: 'webhook', public: true }, action: { kind: 'loop', task_template: 'x' } }).some((e) => /public/.test(e)), 'public webhook + loop action rejected (unauth task injection)');
|
|
52
|
+
|
|
53
|
+
console.log(`\n${fail === 0 ? '✅' : '❌'} ${pass} passed, ${fail} failed`);
|
|
54
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Deterministic tests for routine-store + routine-runner (mock delivery, fake clock).
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { RoutineStore } from '../bin/routine-store.mjs';
|
|
7
|
+
import { RoutineRunner } from '../bin/routine-runner.mjs';
|
|
8
|
+
|
|
9
|
+
let pass = 0, fail = 0;
|
|
10
|
+
function ok(c, m) { if (c) { pass++; console.log(` ✓ ${m}`); } else { fail++; console.log(` ✗ ${m}`); } }
|
|
11
|
+
const D = (y, mo, d, h, mi) => new Date(y, mo - 1, d, h, mi, 0, 0);
|
|
12
|
+
|
|
13
|
+
const dirs = [];
|
|
14
|
+
function freshStore() { const dir = mkdtempSync(join(tmpdir(), 'routines-')); dirs.push(dir); return new RoutineStore(dir); }
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
console.log('store');
|
|
18
|
+
{ const s = freshStore();
|
|
19
|
+
const r = s.save({ session_id: 's1', name: 'hook', trigger: { type: 'webhook' }, action: { kind: 'message', template: 'hi ${body.x}' } });
|
|
20
|
+
ok(r.id?.startsWith('rt_'), 'save assigns id');
|
|
21
|
+
ok(typeof r.trigger.key === 'string' && r.trigger.key.length > 10, 'webhook routine gets a capability key');
|
|
22
|
+
ok(s.get(r.id).name === 'hook', 'get round-trips');
|
|
23
|
+
ok(s.list().length === 1, 'list returns saved routine');
|
|
24
|
+
s.setEnabled(r.id, false); ok(s.get(r.id).enabled === false, 'setEnabled persists');
|
|
25
|
+
ok(s.remove(r.id) === true && s.list().length === 0, 'remove deletes'); }
|
|
26
|
+
|
|
27
|
+
console.log('resolveAction (templating)');
|
|
28
|
+
{ const s = freshStore();
|
|
29
|
+
const runner = new RoutineRunner({ store: s, deliver: async () => {}, now: () => D(2026, 6, 10, 9, 0) });
|
|
30
|
+
const msg = s.save({ session_id: 's', name: 'm', trigger: { type: 'manual' }, action: { kind: 'message', template: 'PR ${body.pr}' } });
|
|
31
|
+
ok(runner.resolveAction(msg, { body: { pr: 7 } }).text === 'PR 7', 'message action templated');
|
|
32
|
+
const lp = s.save({ session_id: 's', name: 'l', trigger: { type: 'webhook' }, action: { kind: 'loop', task_template: 'fix ${body.issue}' } });
|
|
33
|
+
const r = runner.resolveAction(lp, { body: { issue: 'X' } });
|
|
34
|
+
ok(r.kind === 'loop' && r.task === 'fix X', 'loop action task templated'); }
|
|
35
|
+
|
|
36
|
+
console.log('schedule tick (fire once per minute)');
|
|
37
|
+
{ const s = freshStore();
|
|
38
|
+
let clock = D(2026, 6, 10, 9, 5);
|
|
39
|
+
const calls = [];
|
|
40
|
+
const runner = new RoutineRunner({ store: s, deliver: async (c) => calls.push(c), now: () => clock });
|
|
41
|
+
s.save({ session_id: 's', name: 'poll', trigger: { type: 'schedule', cron: '*/5 * * * *' }, action: { kind: 'message', template: 'tick' } });
|
|
42
|
+
await runner.tick(D(2026, 6, 10, 9, 3)); ok(calls.length === 0, 'no fire at non-matching minute 9:03');
|
|
43
|
+
await runner.tick(D(2026, 6, 10, 9, 5)); ok(calls.length === 1, 'fires at matching minute 9:05');
|
|
44
|
+
await runner.tick(D(2026, 6, 10, 9, 5)); ok(calls.length === 1, 'does not double-fire same minute');
|
|
45
|
+
await runner.tick(D(2026, 6, 10, 9, 10)); ok(calls.length === 2, 'fires again next matching minute');
|
|
46
|
+
ok(s.list()[0].last_runs.length === 2, 'run history recorded'); }
|
|
47
|
+
|
|
48
|
+
console.log('overlap policy (concurrent in-flight guard)');
|
|
49
|
+
{ const s = freshStore();
|
|
50
|
+
const calls = []; let release; const gate = new Promise((r) => { release = r; });
|
|
51
|
+
const runner = new RoutineRunner({ store: s, deliver: async () => { calls.push(1); await gate; } });
|
|
52
|
+
const r = s.save({ session_id: 's', name: 'l', overlap: 'skip', trigger: { type: 'manual' }, action: { kind: 'loop', task_template: 'go' } });
|
|
53
|
+
const p1 = runner.fire(s.get(r.id), {}, 'manual'); // enters deliver, holds active
|
|
54
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
55
|
+
const res2 = await runner.fire(s.get(r.id), {}, 'manual'); // concurrent -> skipped
|
|
56
|
+
ok(res2.skipped === 'busy' && calls.length === 1, 'overlap=skip skips a concurrent fire');
|
|
57
|
+
release(); await p1; // first delivery completes -> active cleared
|
|
58
|
+
const res3 = await runner.fire(s.get(r.id), {}, 'manual');
|
|
59
|
+
ok(res3.fired && calls.length === 2, 'fires again once the in-flight delivery completes (no permanent skip)'); }
|
|
60
|
+
|
|
61
|
+
console.log('webhook dispatch + key/method checks');
|
|
62
|
+
{ const s = freshStore();
|
|
63
|
+
const calls = [];
|
|
64
|
+
const runner = new RoutineRunner({ store: s, deliver: async (c) => calls.push(c) });
|
|
65
|
+
const r = s.save({ session_id: 's', name: 'wh', trigger: { type: 'webhook', methods: ['POST'] }, action: { kind: 'message', template: 'got ${body.v}' } });
|
|
66
|
+
const key = s.get(r.id).trigger.key;
|
|
67
|
+
ok((await runner.webhook(r.id, { key: 'wrong', method: 'POST' })).status === 401, 'bad key -> 401');
|
|
68
|
+
ok((await runner.webhook(r.id, { key, method: 'GET' })).status === 405, 'disallowed method -> 405');
|
|
69
|
+
const good = await runner.webhook(r.id, { key, method: 'POST', body: { v: 42 } });
|
|
70
|
+
ok(good.status === 200 && good.resolved.text === 'got 42', 'good webhook delivers templated message');
|
|
71
|
+
s.setEnabled(r.id, false);
|
|
72
|
+
ok((await runner.webhook(r.id, { key, method: 'POST' })).status === 409, 'disabled -> 409'); }
|
|
73
|
+
|
|
74
|
+
console.log('catch-up missed schedule');
|
|
75
|
+
{ const s = freshStore();
|
|
76
|
+
const calls = [];
|
|
77
|
+
const now = D(2026, 6, 10, 9, 30);
|
|
78
|
+
const runner = new RoutineRunner({ store: s, deliver: async (c) => calls.push(c), now: () => now });
|
|
79
|
+
s.save({ session_id: 's', name: 'daily', trigger: { type: 'schedule', cron: '0 9 * * *', missed: 'catchup', deadline_sec: 7200 }, action: { kind: 'message', template: 'good morning' } });
|
|
80
|
+
await runner.catchUp(D(2026, 6, 10, 8, 0), now); ok(calls.length === 1, 'catch-up fires the missed 09:00 run');
|
|
81
|
+
await runner.catchUp(D(2026, 6, 10, 8, 0), now); ok(calls.length === 1, 'catch-up does not re-fire once recorded'); }
|
|
82
|
+
|
|
83
|
+
console.log('daily cap');
|
|
84
|
+
{ const s = freshStore(); const calls = []; const now = D(2026, 6, 10, 9, 0);
|
|
85
|
+
const runner = new RoutineRunner({ store: s, deliver: async (c) => calls.push(c), now: () => now });
|
|
86
|
+
const r = s.save({ session_id: 's', name: 'capped', daily_cap: 2, trigger: { type: 'manual' }, action: { kind: 'message', template: 'x' } });
|
|
87
|
+
await runner.fire(s.get(r.id), {}, 'manual');
|
|
88
|
+
await runner.fire(s.get(r.id), {}, 'manual');
|
|
89
|
+
const third = await runner.fire(s.get(r.id), {}, 'manual');
|
|
90
|
+
ok(calls.length === 2, 'daily_cap=2 delivers exactly twice');
|
|
91
|
+
ok(third.skipped === 'daily_cap', 'third fire skipped by daily cap'); }
|
|
92
|
+
|
|
93
|
+
console.log('fire-time public+loop guard (#8)');
|
|
94
|
+
{ const s = freshStore(); const calls = [];
|
|
95
|
+
const runner = new RoutineRunner({ store: s, deliver: async () => calls.push(1) });
|
|
96
|
+
// hand-built (bypasses save validation, simulating a legacy/pre-guard stored routine)
|
|
97
|
+
const res = await runner.fire({ id: 'x', enabled: true, trigger: { type: 'webhook', public: true }, action: { kind: 'loop', task_template: 't' } }, {}, 'webhook');
|
|
98
|
+
ok(res.skipped?.includes('forbidden') && calls.length === 0, 'public webhook + loop action refused at fire time'); }
|
|
99
|
+
|
|
100
|
+
console.log('daily_cap under concurrency (#9 reserve-before-await)');
|
|
101
|
+
{ const s = freshStore(); const calls = []; let release; const gate = new Promise((r) => { release = r; });
|
|
102
|
+
const runner = new RoutineRunner({ store: s, deliver: async () => { calls.push(1); await gate; } });
|
|
103
|
+
const r = s.save({ session_id: 's', name: 'cap', daily_cap: 2, trigger: { type: 'manual' }, action: { kind: 'message', template: 'x' } });
|
|
104
|
+
const fires = Promise.all([0, 1, 2, 3, 4].map(() => runner.fire(s.get(r.id), {}, 'manual')));
|
|
105
|
+
await new Promise((res) => setTimeout(res, 20));
|
|
106
|
+
release(); await fires;
|
|
107
|
+
ok(calls.length === 2, `5 concurrent fires respect daily_cap=2 (delivered ${calls.length})`); }
|
|
108
|
+
|
|
109
|
+
console.log('catch-up honors timezone (#10)');
|
|
110
|
+
{ const s = freshStore();
|
|
111
|
+
const now = new Date('2026-06-10T13:30:00Z'); // 09:30 EDT
|
|
112
|
+
const calls = [];
|
|
113
|
+
const runner = new RoutineRunner({ store: s, deliver: async () => calls.push(1), now: () => now });
|
|
114
|
+
s.save({ session_id: 's', name: 'tzd', trigger: { type: 'schedule', cron: '0 9 * * *', tz: 'America/New_York', missed: 'catchup', deadline_sec: 7200 }, action: { kind: 'message', template: 'gm' } });
|
|
115
|
+
await runner.catchUp(new Date('2026-06-10T12:00:00Z'), now);
|
|
116
|
+
ok(calls.length === 1, 'catch-up fires the missed 09:00-America/New_York run (tz-aware)'); }
|
|
117
|
+
|
|
118
|
+
console.log(`\n${fail === 0 ? '✅' : '❌'} ${pass} passed, ${fail} failed`);
|
|
119
|
+
process.exit(fail === 0 ? 0 : 1);
|
|
120
|
+
} finally {
|
|
121
|
+
for (const d of dirs) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
122
|
+
}
|
|
@@ -148,7 +148,7 @@ async function sessionBroadcast(action, args) {
|
|
|
148
148
|
console.log(`Broadcast sent: ${action}`);
|
|
149
149
|
}
|
|
150
150
|
async function connectToMachineService() {
|
|
151
|
-
const { connectAndGetMachine } = await import('./commands-
|
|
151
|
+
const { connectAndGetMachine } = await import('./commands-CdxEOPUt.mjs');
|
|
152
152
|
return connectAndGetMachine();
|
|
153
153
|
}
|
|
154
154
|
async function inboxSend(targetSessionId, opts) {
|
|
@@ -165,7 +165,7 @@ async function inboxSend(targetSessionId, opts) {
|
|
|
165
165
|
}
|
|
166
166
|
const { server, machine } = await connectToMachineService();
|
|
167
167
|
try {
|
|
168
|
-
const { resolveSessionId } = await import('./commands-
|
|
168
|
+
const { resolveSessionId } = await import('./commands-CdxEOPUt.mjs');
|
|
169
169
|
const sessions = await machine.listSessions();
|
|
170
170
|
const match = resolveSessionId(sessions, targetSessionId);
|
|
171
171
|
const fullTargetId = match.sessionId;
|
|
@@ -39,19 +39,6 @@ function getCaddyDownloadUrl() {
|
|
|
39
39
|
const ext = os === "win32" ? "zip" : "tar.gz";
|
|
40
40
|
return `https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_${osStr}_${archStr}.${ext}`;
|
|
41
41
|
}
|
|
42
|
-
function isCaddyAvailable() {
|
|
43
|
-
if (existsSync(CADDY_BIN)) return true;
|
|
44
|
-
try {
|
|
45
|
-
execSync("caddy version", { stdio: "ignore" });
|
|
46
|
-
return true;
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function getCaddyPath() {
|
|
52
|
-
if (existsSync(CADDY_BIN)) return CADDY_BIN;
|
|
53
|
-
return "caddy";
|
|
54
|
-
}
|
|
55
42
|
async function ensureCaddy(log) {
|
|
56
43
|
if (existsSync(CADDY_BIN)) return CADDY_BIN;
|
|
57
44
|
const logger = log || console.log;
|
|
@@ -332,4 +319,4 @@ class CaddyManager {
|
|
|
332
319
|
}
|
|
333
320
|
}
|
|
334
321
|
|
|
335
|
-
export { CaddyManager, ensureCaddy, generateCaddyConfig
|
|
322
|
+
export { CaddyManager, ensureCaddy, generateCaddyConfig };
|