svamp-cli 0.1.87 → 0.1.88

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,263 @@
1
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
6
+ function getConfigPath(sessionId) {
7
+ const cwd = process.cwd();
8
+ return join(cwd, ".svamp", sessionId, "config.json");
9
+ }
10
+ function readConfig(configPath) {
11
+ try {
12
+ if (existsSync(configPath)) return JSON.parse(readFileSync(configPath, "utf-8"));
13
+ } catch {
14
+ }
15
+ return {};
16
+ }
17
+ function writeConfig(configPath, config) {
18
+ mkdirSync(dirname(configPath), { recursive: true });
19
+ const tmp = configPath + ".tmp";
20
+ writeFileSync(tmp, JSON.stringify(config, null, 2));
21
+ renameSync(tmp, configPath);
22
+ }
23
+ async function connectAndEmit(event) {
24
+ const ENV_FILE = join(SVAMP_HOME, ".env");
25
+ if (existsSync(ENV_FILE)) {
26
+ const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
27
+ for (const line of lines) {
28
+ const m = line.match(/^([A-Z_]+)=(.*)/);
29
+ if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, "");
30
+ }
31
+ }
32
+ const serverUrl = process.env.HYPHA_SERVER_URL;
33
+ const token = process.env.HYPHA_TOKEN;
34
+ if (!serverUrl || !token) {
35
+ console.error('No Hypha credentials. Run "svamp login" first.');
36
+ process.exit(1);
37
+ }
38
+ const origLog = console.log;
39
+ const origWarn = console.warn;
40
+ const origInfo = console.info;
41
+ console.log = () => {
42
+ };
43
+ console.warn = () => {
44
+ };
45
+ console.info = () => {
46
+ };
47
+ let server;
48
+ try {
49
+ const mod = await import('hypha-rpc');
50
+ const connectToServer = mod.connectToServer || mod.default?.connectToServer;
51
+ server = await connectToServer({ server_url: serverUrl, token, name: "svamp-agent-emit" });
52
+ } catch (err) {
53
+ console.log = origLog;
54
+ console.warn = origWarn;
55
+ console.info = origInfo;
56
+ console.error(`Failed to connect to Hypha: ${err.message}`);
57
+ process.exit(1);
58
+ }
59
+ console.log = origLog;
60
+ console.warn = origWarn;
61
+ console.info = origInfo;
62
+ await server.emit({ ...event, to: "*" });
63
+ await server.disconnect();
64
+ }
65
+ async function sessionSetTitle(title) {
66
+ const sessionId = process.env.SVAMP_SESSION_ID;
67
+ if (!sessionId) {
68
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
69
+ process.exit(1);
70
+ }
71
+ const configPath = getConfigPath(sessionId);
72
+ const config = readConfig(configPath);
73
+ config.title = title.trim();
74
+ writeConfig(configPath, config);
75
+ console.log(`Session title set: "${title.trim()}"`);
76
+ }
77
+ async function sessionSetLink(url, label) {
78
+ const sessionId = process.env.SVAMP_SESSION_ID;
79
+ if (!sessionId) {
80
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
81
+ process.exit(1);
82
+ }
83
+ const resolvedLabel = label?.trim() || (() => {
84
+ try {
85
+ return new URL(url).hostname;
86
+ } catch {
87
+ return "View";
88
+ }
89
+ })();
90
+ const configPath = getConfigPath(sessionId);
91
+ const config = readConfig(configPath);
92
+ config.session_link = { url: url.trim(), label: resolvedLabel };
93
+ writeConfig(configPath, config);
94
+ console.log(`Session link set: "${resolvedLabel}" \u2192 ${url.trim()}`);
95
+ }
96
+ async function sessionNotify(message, level = "info") {
97
+ const sessionId = process.env.SVAMP_SESSION_ID;
98
+ if (!sessionId) {
99
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
100
+ process.exit(1);
101
+ }
102
+ await connectAndEmit({
103
+ type: "svamp:session-notify",
104
+ data: { sessionId, message, level, timestamp: Date.now() }
105
+ });
106
+ console.log(`Notification sent [${level}]: ${message}`);
107
+ }
108
+ async function sessionBroadcast(action, args) {
109
+ const sessionId = process.env.SVAMP_SESSION_ID;
110
+ if (!sessionId) {
111
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
112
+ process.exit(1);
113
+ }
114
+ let payload = { action };
115
+ if (action === "open-canvas") {
116
+ const url = args[0];
117
+ if (!url) {
118
+ console.error("Usage: svamp session broadcast open-canvas <url> [label]");
119
+ process.exit(1);
120
+ }
121
+ const label = args[1] || (() => {
122
+ try {
123
+ return new URL(url).hostname;
124
+ } catch {
125
+ return "View";
126
+ }
127
+ })();
128
+ payload = { action, url, label };
129
+ } else if (action === "close-canvas") ; else if (action === "toast") {
130
+ const message = args[0];
131
+ if (!message) {
132
+ console.error("Usage: svamp session broadcast toast <message>");
133
+ process.exit(1);
134
+ }
135
+ payload = { action, message };
136
+ } else {
137
+ console.error(`Unknown broadcast action: ${action}`);
138
+ console.error("Available actions: open-canvas, close-canvas, toast");
139
+ process.exit(1);
140
+ }
141
+ await connectAndEmit({
142
+ type: "svamp:session-broadcast",
143
+ data: { sessionId, ...payload }
144
+ });
145
+ console.log(`Broadcast sent: ${action}`);
146
+ }
147
+ async function connectToMachineService() {
148
+ const { connectAndGetMachine } = await import('./commands-Cf89hwcf.mjs');
149
+ return connectAndGetMachine();
150
+ }
151
+ async function inboxSend(targetSessionId, opts) {
152
+ const sessionId = process.env.SVAMP_SESSION_ID;
153
+ if (!sessionId) {
154
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
155
+ process.exit(1);
156
+ }
157
+ const body = opts?.body || "";
158
+ if (!body) {
159
+ console.error("Message body is required.");
160
+ process.exit(1);
161
+ }
162
+ const { server, machine } = await connectToMachineService();
163
+ try {
164
+ const { resolveSessionId } = await import('./commands-Cf89hwcf.mjs');
165
+ const sessions = await machine.listSessions();
166
+ const match = resolveSessionId(sessions, targetSessionId);
167
+ const fullTargetId = match.sessionId;
168
+ const { randomUUID } = await import('node:crypto');
169
+ const message = {
170
+ messageId: randomUUID(),
171
+ body,
172
+ timestamp: Date.now(),
173
+ read: false,
174
+ from: `agent:${sessionId}`,
175
+ fromSession: sessionId,
176
+ to: fullTargetId,
177
+ subject: opts?.subject,
178
+ urgency: opts?.urgency || "normal"
179
+ };
180
+ const result = await machine.sessionRPC(fullTargetId, "sendInboxMessage", { message });
181
+ console.log(`Inbox message sent to ${fullTargetId.slice(0, 8)} (id: ${result.messageId.slice(0, 8)})`);
182
+ } finally {
183
+ await server.disconnect();
184
+ }
185
+ }
186
+ async function inboxList(opts) {
187
+ const sessionId = process.env.SVAMP_SESSION_ID;
188
+ if (!sessionId) {
189
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
190
+ process.exit(1);
191
+ }
192
+ const { server, machine } = await connectToMachineService();
193
+ try {
194
+ const result = await machine.sessionRPC(sessionId, "getInbox", { opts: { unread: opts?.unread, limit: opts?.limit } });
195
+ const messages = result.messages;
196
+ if (opts?.json) {
197
+ console.log(JSON.stringify({ messages }, null, 2));
198
+ return;
199
+ }
200
+ if (messages.length === 0) {
201
+ console.log("Inbox is empty.");
202
+ return;
203
+ }
204
+ for (const msg of messages) {
205
+ const status = msg.read ? " " : "\u25CF";
206
+ const from = msg.from ? ` from ${msg.from}` : "";
207
+ const subject = msg.subject ? ` \u2014 ${msg.subject}` : "";
208
+ const urgencyTag = msg.urgency === "urgent" ? " [URGENT]" : "";
209
+ const preview = msg.body.length > 100 ? msg.body.slice(0, 97) + "..." : msg.body;
210
+ console.log(`${status} ${msg.messageId.slice(0, 8)}${urgencyTag}${from}${subject}: ${preview}`);
211
+ }
212
+ } finally {
213
+ await server.disconnect();
214
+ }
215
+ }
216
+ async function inboxReply(messageId, body) {
217
+ const sessionId = process.env.SVAMP_SESSION_ID;
218
+ if (!sessionId) {
219
+ console.error("SVAMP_SESSION_ID not set. This command must be run inside a Svamp session.");
220
+ process.exit(1);
221
+ }
222
+ const { server, machine } = await connectToMachineService();
223
+ try {
224
+ const result = await machine.sessionRPC(sessionId, "getInbox", {});
225
+ const original = result.messages.find((m) => m.messageId === messageId || m.messageId.startsWith(messageId));
226
+ if (!original) {
227
+ console.error(`Message ${messageId} not found in inbox.`);
228
+ process.exit(1);
229
+ }
230
+ if (!original.fromSession) {
231
+ console.error("Cannot reply: original message has no fromSession.");
232
+ process.exit(1);
233
+ }
234
+ const { randomUUID } = await import('node:crypto');
235
+ const reply = {
236
+ messageId: randomUUID(),
237
+ body,
238
+ timestamp: Date.now(),
239
+ read: false,
240
+ from: `agent:${sessionId}`,
241
+ fromSession: sessionId,
242
+ to: original.fromSession,
243
+ subject: original.subject ? `Re: ${original.subject}` : void 0,
244
+ urgency: "normal",
245
+ replyTo: original.messageId,
246
+ threadId: original.threadId || original.messageId
247
+ };
248
+ const sendResult = await machine.sessionRPC(original.fromSession, "sendInboxMessage", { message: reply });
249
+ console.log(`Reply sent to ${original.fromSession.slice(0, 8)} (id: ${sendResult.messageId.slice(0, 8)})`);
250
+ } finally {
251
+ await server.disconnect();
252
+ }
253
+ }
254
+ async function machineNotify(message, level = "info") {
255
+ const machineId = process.env.SVAMP_MACHINE_ID;
256
+ await connectAndEmit({
257
+ type: "svamp:machine-notify",
258
+ data: { machineId, message, level, timestamp: Date.now() }
259
+ });
260
+ console.log(`Machine notification sent [${level}]: ${message}`);
261
+ }
262
+
263
+ export { inboxList, inboxReply, inboxSend, machineNotify, sessionBroadcast, sessionNotify, sessionSetLink, sessionSetTitle };
@@ -0,0 +1,147 @@
1
+ function getSandboxEnv() {
2
+ return {
3
+ apiUrl: process.env.SANDBOX_API_URL || "https://agent-sandbox.aicell.io",
4
+ apiKey: process.env.SANDBOX_API_KEY || process.env.HYPHA_TOKEN || "",
5
+ namespace: process.env.SANDBOX_NAMESPACE || process.env.HYPHA_WORKSPACE || "",
6
+ sandboxId: process.env.SANDBOX_ID || ""
7
+ };
8
+ }
9
+ function requireSandboxEnv() {
10
+ const env = getSandboxEnv();
11
+ if (!env.apiKey) {
12
+ throw new Error(
13
+ 'No API credentials found.\nRun "svamp login" to authenticate with Hypha, or set SANDBOX_API_KEY directly.'
14
+ );
15
+ }
16
+ if (!env.namespace) {
17
+ throw new Error(
18
+ 'No namespace/workspace found.\nRun "svamp login" to set HYPHA_WORKSPACE, or set SANDBOX_NAMESPACE directly.'
19
+ );
20
+ }
21
+ return env;
22
+ }
23
+ function requireSandboxApiEnv() {
24
+ const env = getSandboxEnv();
25
+ if (!env.apiKey) {
26
+ throw new Error(
27
+ 'No API credentials found.\nRun "svamp login" to authenticate with Hypha, or set SANDBOX_API_KEY directly.'
28
+ );
29
+ }
30
+ return env;
31
+ }
32
+ async function sandboxFetch(env, path, init, timeoutMs = 3e4) {
33
+ const url = `${env.apiUrl.replace(/\/+$/, "")}${path}`;
34
+ const headers = {
35
+ "Authorization": `Bearer ${env.apiKey}`,
36
+ "Content-Type": "application/json",
37
+ ...init?.headers || {}
38
+ };
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
41
+ let res;
42
+ try {
43
+ res = await fetch(url, { ...init, headers, signal: controller.signal });
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
47
+ if (!res.ok) {
48
+ const body = await res.text().catch(() => "");
49
+ let detail = body;
50
+ try {
51
+ detail = JSON.parse(body).detail || body;
52
+ } catch {
53
+ }
54
+ throw new Error(`${res.status} ${res.statusText}: ${detail}`);
55
+ }
56
+ return res;
57
+ }
58
+ async function createServiceGroup(name, ports, options) {
59
+ const env = requireSandboxEnv();
60
+ const body = {};
61
+ if (ports.length === 1 && options?.subdomain) {
62
+ body.port = ports[0];
63
+ body.subdomain = options.subdomain;
64
+ } else {
65
+ body.ports = ports.map((p) => ({ port: p }));
66
+ }
67
+ if (options?.healthPath) {
68
+ body.health_path = options.healthPath;
69
+ if (options.healthInterval) {
70
+ body.health_interval = options.healthInterval;
71
+ }
72
+ }
73
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}`, {
74
+ method: "POST",
75
+ body: JSON.stringify(body)
76
+ });
77
+ return res.json();
78
+ }
79
+ async function listServiceGroups() {
80
+ const env = requireSandboxEnv();
81
+ const res = await sandboxFetch(env, `/services/${env.namespace}`);
82
+ return res.json();
83
+ }
84
+ async function getServiceGroup(name) {
85
+ const env = requireSandboxEnv();
86
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}`);
87
+ return res.json();
88
+ }
89
+ async function deleteServiceGroup(name) {
90
+ const env = requireSandboxEnv();
91
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}`, {
92
+ method: "DELETE"
93
+ });
94
+ return res.json();
95
+ }
96
+ async function addPort(name, port, subdomain) {
97
+ const env = requireSandboxEnv();
98
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/ports`, {
99
+ method: "POST",
100
+ body: JSON.stringify({ port, subdomain })
101
+ });
102
+ return res.json();
103
+ }
104
+ async function removePort(name, port) {
105
+ const env = requireSandboxEnv();
106
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/ports/${port}`, {
107
+ method: "DELETE"
108
+ });
109
+ return res.json();
110
+ }
111
+ async function renameSubdomain(name, port, subdomain) {
112
+ const env = requireSandboxEnv();
113
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/subdomain`, {
114
+ method: "PUT",
115
+ body: JSON.stringify({ port, subdomain })
116
+ });
117
+ return res.json();
118
+ }
119
+ async function addBackend(name, sandboxId) {
120
+ const env = requireSandboxEnv();
121
+ const sid = sandboxId || env.sandboxId;
122
+ if (!sid) {
123
+ throw new Error(
124
+ "No sandbox ID provided and SANDBOX_ID is not set.\nUse --sandbox-id <id> to specify which sandbox to add."
125
+ );
126
+ }
127
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/backends`, {
128
+ method: "POST",
129
+ body: JSON.stringify({ sandbox_id: sid })
130
+ });
131
+ return res.json();
132
+ }
133
+ async function removeBackend(name, sandboxId) {
134
+ const env = requireSandboxEnv();
135
+ const sid = sandboxId || env.sandboxId;
136
+ if (!sid) {
137
+ throw new Error(
138
+ "No sandbox ID provided and SANDBOX_ID is not set.\nUse --sandbox-id <id> to specify which sandbox to remove."
139
+ );
140
+ }
141
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/backends/${sid}`, {
142
+ method: "DELETE"
143
+ });
144
+ return res.json();
145
+ }
146
+
147
+ export { addBackend, addPort, createServiceGroup, deleteServiceGroup, getSandboxEnv, getServiceGroup, listServiceGroups, removeBackend, removePort, renameSubdomain, requireSandboxApiEnv, requireSandboxEnv };