svamp-cli 0.2.225 → 0.2.227

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.
@@ -0,0 +1,78 @@
1
+ const LAUNCHD_LABEL = "io.hypha.svamp.daemon";
2
+ const SYSTEMD_UNIT = "svamp-daemon.service";
3
+ async function ensureSupervisorViaServiceManager() {
4
+ const os = await import('os');
5
+ const { join } = await import('path');
6
+ const fs = await import('fs');
7
+ const { execSync } = await import('child_process');
8
+ if (process.platform === "darwin") {
9
+ const plistPath = join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
10
+ if (!fs.existsSync(plistPath)) return false;
11
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
12
+ try {
13
+ execSync(`launchctl bootstrap gui/${uid} "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
14
+ } catch {
15
+ }
16
+ try {
17
+ execSync(`launchctl enable gui/${uid}/${LAUNCHD_LABEL} 2>/dev/null`, { stdio: "ignore" });
18
+ } catch {
19
+ }
20
+ try {
21
+ execSync(`launchctl kickstart -k gui/${uid}/${LAUNCHD_LABEL}`, { stdio: "ignore" });
22
+ return true;
23
+ } catch {
24
+ try {
25
+ execSync(`launchctl load "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ }
32
+ if (process.platform === "linux") {
33
+ const unitPath = join(os.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
34
+ if (!fs.existsSync(unitPath)) return false;
35
+ try {
36
+ execSync(`systemctl --user start ${SYSTEMD_UNIT}`, { stdio: "ignore" });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ async function stopViaServiceManager() {
45
+ const os = await import('os');
46
+ const { join } = await import('path');
47
+ const fs = await import('fs');
48
+ const { execSync } = await import('child_process');
49
+ if (process.platform === "darwin") {
50
+ const plistPath = join(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
51
+ if (!fs.existsSync(plistPath)) return false;
52
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
53
+ try {
54
+ execSync(`launchctl bootout gui/${uid}/${LAUNCHD_LABEL} 2>/dev/null`, { stdio: "ignore" });
55
+ return true;
56
+ } catch {
57
+ try {
58
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ }
65
+ if (process.platform === "linux") {
66
+ const unitPath = join(os.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
67
+ if (!fs.existsSync(unitPath)) return false;
68
+ try {
69
+ execSync(`systemctl --user stop ${SYSTEMD_UNIT}`, { stdio: "ignore" });
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+
78
+ export { LAUNCHD_LABEL, SYSTEMD_UNIT, ensureSupervisorViaServiceManager, stopViaServiceManager };
@@ -0,0 +1,80 @@
1
+ import { R as READ_ONLY_TOOLS, B as loadMachineContext, C as buildMachineInstructions, D as machineToolsForRole, E as buildMachineTools } from './run-LEPH8aka.mjs';
2
+ import 'node:child_process';
3
+ import 'os';
4
+ import 'fs/promises';
5
+ import 'fs';
6
+ import 'path';
7
+ import 'url';
8
+ import 'child_process';
9
+ import 'crypto';
10
+ import 'node:crypto';
11
+ import 'node:fs';
12
+ import 'util';
13
+ import 'node:path';
14
+ import 'node:events';
15
+ import 'node:os';
16
+ import '@agentclientprotocol/sdk';
17
+ import '@modelcontextprotocol/sdk/client/index.js';
18
+ import '@modelcontextprotocol/sdk/client/stdio.js';
19
+ import '@modelcontextprotocol/sdk/types.js';
20
+ import 'zod';
21
+ import 'node:fs/promises';
22
+ import 'node:util';
23
+
24
+ function toolsForRole(role) {
25
+ if (role === "admin") return [...READ_ONLY_TOOLS, "run_bash", "send_to_session"];
26
+ if (role === "interact") return [...READ_ONLY_TOOLS, "send_to_session"];
27
+ return [...READ_ONLY_TOOLS];
28
+ }
29
+ function toRealtimeTools(tools) {
30
+ return tools.map((t) => ({ type: "function", name: t.name, description: t.description, parameters: t.parameters }));
31
+ }
32
+ function buildVoiceSessionUpdate(instructions, tools, opts) {
33
+ return {
34
+ type: "session.update",
35
+ session: {
36
+ type: "realtime",
37
+ instructions,
38
+ tools: toRealtimeTools(tools),
39
+ tool_choice: "auto",
40
+ ...opts?.voice ? { audio: { output: { voice: opts.voice } } } : {}
41
+ }
42
+ };
43
+ }
44
+ async function handleRealtimeEvent(ev, tools, send, nameByCallId) {
45
+ if (ev?.type === "response.output_item.added" && ev.item?.type === "function_call") {
46
+ if (nameByCallId && ev.item.call_id) nameByCallId.set(ev.item.call_id, ev.item.name);
47
+ return null;
48
+ }
49
+ if (!ev || ev.type !== "response.function_call_arguments.done") return null;
50
+ const callId = ev.call_id;
51
+ const name = ev.name || nameByCallId && nameByCallId.get(callId) || "";
52
+ let args = {};
53
+ try {
54
+ args = ev.arguments ? JSON.parse(ev.arguments) : {};
55
+ } catch {
56
+ }
57
+ const tool = tools.find((t) => t.name === name);
58
+ let output;
59
+ if (!tool) {
60
+ output = `error: tool "${name}" is not available to this caller`;
61
+ } else {
62
+ try {
63
+ output = await tool.run(args);
64
+ } catch (e) {
65
+ output = `error: ${e?.message || e}`;
66
+ }
67
+ }
68
+ send({ type: "conversation.item.create", item: { type: "function_call_output", call_id: callId, output } });
69
+ send({ type: "response.create" });
70
+ return { name, output };
71
+ }
72
+ async function initMachineVoiceSession(init) {
73
+ const ctx = await loadMachineContext(init.deps);
74
+ const instructions = buildMachineInstructions(ctx);
75
+ const allow = new Set(machineToolsForRole(init.role));
76
+ const granted = buildMachineTools(init.deps, ctx.skills).filter((t) => allow.has(t.name));
77
+ return { tools: granted, sessionUpdate: buildVoiceSessionUpdate(instructions, granted, { voice: init.voice }) };
78
+ }
79
+
80
+ export { buildVoiceSessionUpdate, handleRealtimeEvent, initMachineVoiceSession, toRealtimeTools, toolsForRole };
@@ -0,0 +1,148 @@
1
+ import { existsSync, unlinkSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parse, stringify } from 'yaml';
4
+
5
+ function isWorkflowEnabled(wf) {
6
+ return wf.enabled !== false;
7
+ }
8
+ function workflowSteps(wf) {
9
+ return Object.values(wf.jobs || {}).flatMap((j) => j?.steps || []);
10
+ }
11
+ function workflowCrons(wf) {
12
+ return (wf.on?.schedule || []).map((s) => s?.cron).filter((c) => !!c);
13
+ }
14
+ function workflowsDir(projectRoot) {
15
+ return join(projectRoot, ".svamp", "workflows");
16
+ }
17
+ function workflowPath(projectRoot, name) {
18
+ return join(workflowsDir(projectRoot), `${name}.yaml`);
19
+ }
20
+ function normalizeOn(on) {
21
+ if (!on || typeof on !== "object") {
22
+ return void 0;
23
+ }
24
+ const out = {};
25
+ if (Array.isArray(on.schedule)) {
26
+ const sched = on.schedule.map((s) => ({ cron: String(s?.cron ?? s ?? "") })).filter((s) => s.cron);
27
+ if (sched.length) out.schedule = sched;
28
+ } else if (typeof on.schedule === "string" && on.schedule.trim()) {
29
+ out.schedule = [{ cron: on.schedule.trim() }];
30
+ }
31
+ if ("workflow_dispatch" in on || on.dispatch === true) out.workflow_dispatch = true;
32
+ if (typeof on.channel === "string" && on.channel) out.channel = on.channel;
33
+ if (Array.isArray(on.issue) && on.issue.length) out.issue = on.issue.map(String);
34
+ return Object.keys(out).length ? out : void 0;
35
+ }
36
+ function normalizeJobs(jobs) {
37
+ if (Array.isArray(jobs)) {
38
+ const steps = jobs.map((j) => ({ run: String(j?.run ?? "") })).filter((s) => s.run);
39
+ return steps.length ? { run: { steps } } : {};
40
+ }
41
+ if (jobs && typeof jobs === "object") {
42
+ const out = {};
43
+ for (const [id, job] of Object.entries(jobs)) {
44
+ const rawSteps = Array.isArray(job?.steps) ? job.steps : job?.run ? [{ run: job.run }] : [];
45
+ const steps = rawSteps.map((s) => ({ run: String(s?.run ?? ""), ...s?.name ? { name: String(s.name) } : {} })).filter((s) => s.run);
46
+ if (steps.length) out[String(id)] = { steps };
47
+ }
48
+ return out;
49
+ }
50
+ return {};
51
+ }
52
+ function parseWorkflow(content) {
53
+ try {
54
+ const o = parse(content);
55
+ if (!o || typeof o !== "object" || !o.name) return null;
56
+ return {
57
+ name: String(o.name),
58
+ on: normalizeOn(o.on),
59
+ jobs: normalizeJobs(o.jobs),
60
+ session: o.session ?? null,
61
+ // Only `enabled: false` disables; anything else (absent/true) is active.
62
+ ...o.enabled === false ? { enabled: false } : {}
63
+ };
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function serializeWorkflow(wf) {
69
+ const clean = { name: wf.name };
70
+ if (wf.session) clean.session = wf.session;
71
+ if (wf.enabled === false) clean.enabled = false;
72
+ const on = {};
73
+ if (wf.on?.schedule?.length) on.schedule = wf.on.schedule.map((s) => ({ cron: s.cron }));
74
+ if (wf.on?.workflow_dispatch) on.workflow_dispatch = {};
75
+ if (wf.on?.channel) on.channel = wf.on.channel;
76
+ if (wf.on?.issue?.length) on.issue = wf.on.issue;
77
+ if (Object.keys(on).length) clean.on = on;
78
+ const jobs = {};
79
+ for (const [id, job] of Object.entries(wf.jobs || {})) {
80
+ jobs[id] = { steps: (job.steps || []).map((s) => s.name ? { name: s.name, run: s.run } : { run: s.run }) };
81
+ }
82
+ clean.jobs = Object.keys(jobs).length ? jobs : { run: { steps: [] } };
83
+ return stringify(clean);
84
+ }
85
+ function listWorkflows(projectRoot) {
86
+ const dir = workflowsDir(projectRoot);
87
+ if (!existsSync(dir)) return [];
88
+ const out = [];
89
+ for (const name of readdirSync(dir)) {
90
+ if (!name.endsWith(".yaml") && !name.endsWith(".yml")) continue;
91
+ const p = join(dir, name);
92
+ try {
93
+ const wf = parseWorkflow(readFileSync(p, "utf-8"));
94
+ if (wf) {
95
+ try {
96
+ const st = statSync(p);
97
+ wf.created = new Date(st.birthtimeMs || st.mtimeMs).toISOString();
98
+ } catch {
99
+ }
100
+ out.push(wf);
101
+ }
102
+ } catch {
103
+ }
104
+ }
105
+ return out.sort((a, b) => a.name.localeCompare(b.name));
106
+ }
107
+ function getWorkflow(projectRoot, name) {
108
+ const p = workflowPath(projectRoot, name);
109
+ if (!existsSync(p)) return null;
110
+ try {
111
+ return parseWorkflow(readFileSync(p, "utf-8"));
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ function rawWorkflow(projectRoot, name) {
117
+ const p = workflowPath(projectRoot, name);
118
+ return existsSync(p) ? readFileSync(p, "utf-8") : null;
119
+ }
120
+ function saveWorkflow(projectRoot, wf) {
121
+ const dir = workflowsDir(projectRoot);
122
+ mkdirSync(dir, { recursive: true });
123
+ const path = workflowPath(projectRoot, wf.name);
124
+ const tmp = `${path}.tmp-${process.pid}`;
125
+ writeFileSync(tmp, serializeWorkflow(wf));
126
+ renameSync(tmp, path);
127
+ }
128
+ function setWorkflowEnabled(projectRoot, name, enabled) {
129
+ const wf = getWorkflow(projectRoot, name);
130
+ if (!wf) return null;
131
+ const next = { ...wf };
132
+ if (enabled) delete next.enabled;
133
+ else next.enabled = false;
134
+ saveWorkflow(projectRoot, next);
135
+ return next;
136
+ }
137
+ function removeWorkflow(projectRoot, name) {
138
+ const p = workflowPath(projectRoot, name);
139
+ if (!existsSync(p)) return false;
140
+ try {
141
+ unlinkSync(p);
142
+ return true;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ export { saveWorkflow as a, rawWorkflow as b, workflowSteps as c, workflowsDir as d, getWorkflow as g, isWorkflowEnabled as i, listWorkflows as l, removeWorkflow as r, setWorkflowEnabled as s, workflowCrons as w };
@@ -0,0 +1,159 @@
1
+ import { existsSync, readFileSync, unlinkSync, openSync, writeSync, closeSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+
4
+ function acquireSupervisorLock(pidFile, pid = process.pid) {
5
+ try {
6
+ const fd = openSync(pidFile, "wx");
7
+ try {
8
+ writeSync(fd, String(pid));
9
+ } finally {
10
+ closeSync(fd);
11
+ }
12
+ return { acquired: true };
13
+ } catch (err) {
14
+ if (err?.code !== "EEXIST") throw err;
15
+ }
16
+ let existingPid = 0;
17
+ try {
18
+ existingPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
19
+ } catch {
20
+ }
21
+ if (!existingPid || Number.isNaN(existingPid) || existingPid <= 0) {
22
+ try {
23
+ unlinkSync(pidFile);
24
+ } catch {
25
+ }
26
+ return { acquired: false, staleCleaned: true };
27
+ }
28
+ if (isPidAlive(existingPid)) {
29
+ return { acquired: false, heldBy: existingPid };
30
+ }
31
+ try {
32
+ unlinkSync(pidFile);
33
+ } catch {
34
+ }
35
+ return { acquired: false, staleCleaned: true };
36
+ }
37
+ function acquireSupervisorLockWithRetry(pidFile, pid = process.pid) {
38
+ let result = acquireSupervisorLock(pidFile, pid);
39
+ if (!result.acquired && result.staleCleaned) {
40
+ result = acquireSupervisorLock(pidFile, pid);
41
+ }
42
+ return { acquired: result.acquired, heldBy: result.heldBy };
43
+ }
44
+ function releaseSupervisorLock(pidFile, pid = process.pid) {
45
+ try {
46
+ if (!existsSync(pidFile)) return;
47
+ const content = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
48
+ if (content === pid) unlinkSync(pidFile);
49
+ } catch {
50
+ }
51
+ }
52
+ function isPidAlive(pid) {
53
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch (err) {
58
+ return err?.code === "EPERM";
59
+ }
60
+ }
61
+ function findOrphanedSyncPids(currentSupervisorPid = process.pid) {
62
+ if (process.platform === "win32") return [];
63
+ const pids = [];
64
+ try {
65
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
66
+ const pgrepArgs = uid !== void 0 ? ["-u", String(uid), "-af", "svamp daemon start-sync"] : ["-af", "svamp daemon start-sync"];
67
+ const output = execFileSync("pgrep", pgrepArgs, {
68
+ encoding: "utf-8",
69
+ timeout: 5e3
70
+ });
71
+ for (const line of output.split("\n")) {
72
+ const trimmed = line.trim();
73
+ if (!trimmed) continue;
74
+ const match = trimmed.match(/^(\d+)\s/);
75
+ if (!match) continue;
76
+ const pid = parseInt(match[1], 10);
77
+ if (Number.isNaN(pid) || pid <= 0) continue;
78
+ if (pid === process.pid) continue;
79
+ const ppid = getPpid(pid);
80
+ if (ppid === currentSupervisorPid) continue;
81
+ pids.push(pid);
82
+ }
83
+ } catch (err) {
84
+ if (err?.status !== 1) ;
85
+ }
86
+ return pids;
87
+ }
88
+ function getPpid(pid) {
89
+ if (process.platform === "win32") return 0;
90
+ try {
91
+ const out = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
92
+ encoding: "utf-8",
93
+ timeout: 2e3
94
+ }).trim();
95
+ const ppid = parseInt(out, 10);
96
+ return Number.isNaN(ppid) ? 0 : ppid;
97
+ } catch {
98
+ return 0;
99
+ }
100
+ }
101
+ async function killOrphanedSyncs(pids, opts = {}) {
102
+ if (pids.length === 0) return;
103
+ const log = opts.log || (() => {
104
+ });
105
+ const gracePeriodMs = opts.gracePeriodMs ?? 3e3;
106
+ log(`Killing ${pids.length} orphan sync daemon(s): ${pids.join(", ")}`);
107
+ for (const pid of pids) {
108
+ try {
109
+ process.kill(pid, "SIGTERM");
110
+ } catch {
111
+ }
112
+ }
113
+ const pollStep = 100;
114
+ const steps = Math.max(1, Math.ceil(gracePeriodMs / pollStep));
115
+ for (let i = 0; i < steps; i++) {
116
+ await new Promise((r) => setTimeout(r, pollStep));
117
+ if (pids.every((p) => !isPidAlive(p))) {
118
+ log("All orphan daemons exited cleanly");
119
+ return;
120
+ }
121
+ }
122
+ const survivors = pids.filter(isPidAlive);
123
+ if (survivors.length > 0) {
124
+ log(`Force-killing survivors: ${survivors.join(", ")}`);
125
+ for (const pid of survivors) {
126
+ try {
127
+ process.kill(pid, "SIGKILL");
128
+ } catch {
129
+ }
130
+ }
131
+ }
132
+ }
133
+ function watchParentLiveness(opts) {
134
+ if (process.env.SVAMP_SUPERVISED !== "1") {
135
+ return () => {
136
+ };
137
+ }
138
+ const supervisorPid = process.ppid;
139
+ if (!supervisorPid || supervisorPid === 1) {
140
+ return () => {
141
+ };
142
+ }
143
+ const intervalMs = opts.intervalMs ?? 5e3;
144
+ let fired = false;
145
+ const timer = setInterval(() => {
146
+ if (fired) return;
147
+ if (!isPidAlive(supervisorPid)) {
148
+ fired = true;
149
+ clearInterval(timer);
150
+ opts.onParentDeath();
151
+ }
152
+ }, intervalMs);
153
+ timer.unref?.();
154
+ return () => {
155
+ clearInterval(timer);
156
+ };
157
+ }
158
+
159
+ export { acquireSupervisorLock, acquireSupervisorLockWithRetry, findOrphanedSyncPids, getPpid, isPidAlive, killOrphanedSyncs, releaseSupervisorLock, watchParentLiveness };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.225",
3
+ "version": "0.2.227",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",