svamp-cli 0.1.48 → 0.1.49
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/cli.mjs +80 -36
- package/dist/commands-6EyqaoCp.mjs +507 -0
- package/dist/commands-Cw2Od6mc.mjs +1683 -0
- package/dist/commands-D1brd9fB.mjs +1741 -0
- package/dist/commands-DHnFOhQC.mjs +1741 -0
- package/dist/commands-DWira-Cz.mjs +1741 -0
- package/dist/commands-DlPBC5p0.mjs +514 -0
- package/dist/commands-DwveR96q.mjs +1683 -0
- package/dist/commands-HrBaGV-C.mjs +1683 -0
- package/dist/commands-Wng0OuNY.mjs +1683 -0
- package/dist/index.mjs +1 -1
- package/dist/package-BYUO-39f.mjs +60 -0
- package/dist/run-B9ND6srh.mjs +6154 -0
- package/dist/run-BicITYWX.mjs +6138 -0
- package/dist/run-BxTdRjCG.mjs +1051 -0
- package/dist/run-CE4H8ZiN.mjs +6273 -0
- package/dist/run-CtJRxaFC.mjs +1051 -0
- package/dist/run-D1PFrNZB.mjs +6273 -0
- package/dist/run-DWdtp6VD.mjs +6136 -0
- package/dist/run-Dd9XkswU.mjs +1051 -0
- package/dist/run-YFYpyThQ.mjs +1051 -0
- package/dist/run-coIDvBK_.mjs +6127 -0
- package/dist/run-wpUutZ9C.mjs +1051 -0
- package/dist/run-yTjJ7noq.mjs +1051 -0
- package/dist/tunnel-BXEroHJF.mjs +299 -0
- package/package.json +5 -3
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import { r as requireSandboxApiEnv } from './commands-DlPBC5p0.mjs';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
class TunnelClient {
|
|
6
|
+
ws = null;
|
|
7
|
+
options;
|
|
8
|
+
env;
|
|
9
|
+
sandboxId;
|
|
10
|
+
reconnectAttempts = 0;
|
|
11
|
+
maxReconnectAttempts = 20;
|
|
12
|
+
destroyed = false;
|
|
13
|
+
pingInterval = null;
|
|
14
|
+
requestCount = 0;
|
|
15
|
+
localWebSockets = /* @__PURE__ */ new Map();
|
|
16
|
+
// request_id → local WS connection
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = {
|
|
19
|
+
localHost: "localhost",
|
|
20
|
+
requestTimeout: 3e4,
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
this.env = options.env || requireSandboxApiEnv();
|
|
24
|
+
this.sandboxId = options.sandboxId || this.env.sandboxId || `local-${os.hostname()}-${process.pid}`;
|
|
25
|
+
}
|
|
26
|
+
/** Build the WebSocket URL for the tunnel endpoint. */
|
|
27
|
+
buildWsUrl() {
|
|
28
|
+
const baseUrl = this.env.apiUrl.replace(/\/+$/, "");
|
|
29
|
+
const wsBase = baseUrl.replace(/^http/, "ws");
|
|
30
|
+
const ns = this.env.namespace || "_tunnel";
|
|
31
|
+
const params = new URLSearchParams({
|
|
32
|
+
token: this.env.apiKey,
|
|
33
|
+
sandbox_id: this.sandboxId
|
|
34
|
+
});
|
|
35
|
+
return `${wsBase}/services/${ns}/${this.options.name}/tunnel?${params}`;
|
|
36
|
+
}
|
|
37
|
+
/** Connect to the tunnel endpoint. */
|
|
38
|
+
async connect() {
|
|
39
|
+
if (this.destroyed) return;
|
|
40
|
+
const url = this.buildWsUrl();
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
try {
|
|
43
|
+
this.ws = new WebSocket(url);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
reject(new Error(`Failed to create WebSocket: ${err.message}`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.ws.on("open", () => {
|
|
49
|
+
this.reconnectAttempts = 0;
|
|
50
|
+
this.startPingInterval();
|
|
51
|
+
this.options.onConnect?.();
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
this.ws.on("message", (data) => {
|
|
55
|
+
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
56
|
+
this.handleMessage(raw);
|
|
57
|
+
});
|
|
58
|
+
this.ws.on("close", () => {
|
|
59
|
+
this.stopPingInterval();
|
|
60
|
+
this.cleanupLocalWebSockets();
|
|
61
|
+
this.options.onDisconnect?.();
|
|
62
|
+
if (!this.destroyed) {
|
|
63
|
+
this.scheduleReconnect();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
this.ws.on("error", (err) => {
|
|
67
|
+
this.options.onError?.(err);
|
|
68
|
+
if (this.reconnectAttempts === 0) {
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Disconnect and stop reconnecting. */
|
|
75
|
+
destroy() {
|
|
76
|
+
this.destroyed = true;
|
|
77
|
+
this.stopPingInterval();
|
|
78
|
+
this.cleanupLocalWebSockets();
|
|
79
|
+
if (this.ws) {
|
|
80
|
+
this.ws.close();
|
|
81
|
+
this.ws = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Number of HTTP requests proxied. */
|
|
85
|
+
get totalRequests() {
|
|
86
|
+
return this.requestCount;
|
|
87
|
+
}
|
|
88
|
+
/** Number of active WebSocket connections being proxied. */
|
|
89
|
+
get activeWebSockets() {
|
|
90
|
+
return this.localWebSockets.size;
|
|
91
|
+
}
|
|
92
|
+
// ── Message handling ────────────────────────────────────────────────
|
|
93
|
+
handleMessage(raw) {
|
|
94
|
+
let msg;
|
|
95
|
+
try {
|
|
96
|
+
msg = JSON.parse(raw);
|
|
97
|
+
} catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
switch (msg.type) {
|
|
101
|
+
case "ping":
|
|
102
|
+
this.send({ type: "pong" });
|
|
103
|
+
break;
|
|
104
|
+
case "request":
|
|
105
|
+
this.options.onRequest?.(msg);
|
|
106
|
+
this.proxyRequest(msg).catch((err) => {
|
|
107
|
+
this.options.onError?.(err);
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
case "ws_open":
|
|
111
|
+
this.handleWsOpen(msg).catch((err) => {
|
|
112
|
+
this.options.onError?.(err);
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
case "ws_data":
|
|
116
|
+
this.handleWsData(msg);
|
|
117
|
+
break;
|
|
118
|
+
case "ws_close":
|
|
119
|
+
this.handleWsClose(msg);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async proxyRequest(req) {
|
|
124
|
+
this.requestCount++;
|
|
125
|
+
const port = req.port || this.options.ports[0];
|
|
126
|
+
const url = `http://${this.options.localHost}:${port}${req.path}`;
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timeout = setTimeout(() => controller.abort(), this.options.requestTimeout);
|
|
129
|
+
const init = {
|
|
130
|
+
method: req.method,
|
|
131
|
+
headers: req.headers,
|
|
132
|
+
signal: controller.signal
|
|
133
|
+
};
|
|
134
|
+
if (req.body && !["GET", "HEAD"].includes(req.method.toUpperCase())) {
|
|
135
|
+
init.body = Buffer.from(req.body, "base64");
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(url, init);
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
const bodyBuffer = await res.arrayBuffer();
|
|
141
|
+
const bodyBase64 = bodyBuffer.byteLength > 0 ? Buffer.from(bodyBuffer).toString("base64") : void 0;
|
|
142
|
+
const headers = {};
|
|
143
|
+
res.headers.forEach((value, key) => {
|
|
144
|
+
headers[key] = value;
|
|
145
|
+
});
|
|
146
|
+
this.send({
|
|
147
|
+
type: "response",
|
|
148
|
+
id: req.id,
|
|
149
|
+
status: res.status,
|
|
150
|
+
headers,
|
|
151
|
+
body: bodyBase64
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
if (err.name === "AbortError") {
|
|
156
|
+
this.send({
|
|
157
|
+
type: "response",
|
|
158
|
+
id: req.id,
|
|
159
|
+
status: 504,
|
|
160
|
+
headers: { "content-type": "text/plain" },
|
|
161
|
+
body: Buffer.from("Tunnel: request timeout").toString("base64")
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
this.send({
|
|
165
|
+
type: "response",
|
|
166
|
+
id: req.id,
|
|
167
|
+
status: 502,
|
|
168
|
+
headers: { "content-type": "text/plain" },
|
|
169
|
+
body: Buffer.from(`Tunnel: local service error: ${err.message}`).toString("base64")
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ── WebSocket proxying ───────────────────────────────────────────────
|
|
175
|
+
async handleWsOpen(msg) {
|
|
176
|
+
const port = msg.port || this.options.ports[0];
|
|
177
|
+
const localUrl = `ws://${this.options.localHost}:${port}${msg.path}`;
|
|
178
|
+
try {
|
|
179
|
+
const localWs = new WebSocket(localUrl, {
|
|
180
|
+
headers: msg.headers
|
|
181
|
+
});
|
|
182
|
+
this.localWebSockets.set(msg.id, localWs);
|
|
183
|
+
localWs.on("message", (data, isBinary) => {
|
|
184
|
+
const encoded = typeof data === "string" ? Buffer.from(data).toString("base64") : (data instanceof Buffer ? data : Buffer.from(data)).toString("base64");
|
|
185
|
+
this.send({
|
|
186
|
+
type: "ws_data",
|
|
187
|
+
id: msg.id,
|
|
188
|
+
data: encoded,
|
|
189
|
+
is_text: !isBinary
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
localWs.on("close", (code) => {
|
|
193
|
+
this.localWebSockets.delete(msg.id);
|
|
194
|
+
this.send({ type: "ws_close", id: msg.id, code: code || 1e3 });
|
|
195
|
+
});
|
|
196
|
+
localWs.on("error", () => {
|
|
197
|
+
this.localWebSockets.delete(msg.id);
|
|
198
|
+
this.send({ type: "ws_close", id: msg.id, code: 1011 });
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
this.send({ type: "ws_close", id: msg.id, code: 1011 });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
handleWsData(msg) {
|
|
205
|
+
const localWs = this.localWebSockets.get(msg.id);
|
|
206
|
+
if (!localWs || localWs.readyState !== WebSocket.OPEN) return;
|
|
207
|
+
const buf = Buffer.from(msg.data, "base64");
|
|
208
|
+
if (msg.is_text) {
|
|
209
|
+
localWs.send(buf.toString("utf8"));
|
|
210
|
+
} else {
|
|
211
|
+
localWs.send(buf);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
handleWsClose(msg) {
|
|
215
|
+
const localWs = this.localWebSockets.get(msg.id);
|
|
216
|
+
if (localWs) {
|
|
217
|
+
this.localWebSockets.delete(msg.id);
|
|
218
|
+
localWs.close(msg.code || 1e3);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
cleanupLocalWebSockets() {
|
|
222
|
+
for (const [id, ws] of this.localWebSockets) {
|
|
223
|
+
ws.close(1001);
|
|
224
|
+
}
|
|
225
|
+
this.localWebSockets.clear();
|
|
226
|
+
}
|
|
227
|
+
// ── WebSocket helpers ───────────────────────────────────────────────
|
|
228
|
+
send(msg) {
|
|
229
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
230
|
+
this.ws.send(JSON.stringify(msg));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
startPingInterval() {
|
|
234
|
+
this.stopPingInterval();
|
|
235
|
+
this.pingInterval = setInterval(() => {
|
|
236
|
+
this.send({ type: "ping" });
|
|
237
|
+
}, 3e4);
|
|
238
|
+
}
|
|
239
|
+
stopPingInterval() {
|
|
240
|
+
if (this.pingInterval) {
|
|
241
|
+
clearInterval(this.pingInterval);
|
|
242
|
+
this.pingInterval = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
scheduleReconnect() {
|
|
246
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
247
|
+
this.options.onError?.(new Error(
|
|
248
|
+
`Tunnel disconnected: max reconnect attempts (${this.maxReconnectAttempts}) reached`
|
|
249
|
+
));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.reconnectAttempts++;
|
|
253
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
254
|
+
setTimeout(() => {
|
|
255
|
+
if (!this.destroyed) {
|
|
256
|
+
this.connect().catch((err) => {
|
|
257
|
+
this.options.onError?.(err);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}, delay);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function runTunnel(name, ports) {
|
|
264
|
+
const portList = ports.join(", ");
|
|
265
|
+
const client = new TunnelClient({
|
|
266
|
+
name,
|
|
267
|
+
ports,
|
|
268
|
+
onConnect: () => {
|
|
269
|
+
console.log(`Tunnel connected: ${name} \u2192 localhost:[${portList}]`);
|
|
270
|
+
},
|
|
271
|
+
onDisconnect: () => {
|
|
272
|
+
console.log("Tunnel disconnected, reconnecting...");
|
|
273
|
+
},
|
|
274
|
+
onRequest: (req) => {
|
|
275
|
+
console.log(` ${req.method} :${req.port || ports[0]}${req.path}`);
|
|
276
|
+
},
|
|
277
|
+
onError: (err) => {
|
|
278
|
+
console.error(`Tunnel error: ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
const cleanup = () => {
|
|
282
|
+
console.log("\nTunnel shutting down...");
|
|
283
|
+
client.destroy();
|
|
284
|
+
process.exit(0);
|
|
285
|
+
};
|
|
286
|
+
process.on("SIGINT", cleanup);
|
|
287
|
+
process.on("SIGTERM", cleanup);
|
|
288
|
+
try {
|
|
289
|
+
await client.connect();
|
|
290
|
+
console.log(`Tunnel is active. Press Ctrl+C to stop.`);
|
|
291
|
+
await new Promise(() => {
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error(`Failed to establish tunnel: ${err.message}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export { TunnelClient, runTunnel };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svamp-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
4
4
|
"description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
|
|
5
5
|
"author": "Amun AI AB",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc --noEmit && pkgroll",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"test": "npx tsx test/test-authorize.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-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-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-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-machine-list-directory.mjs",
|
|
23
|
+
"test": "npx tsx test/test-authorize.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-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-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-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",
|
|
24
24
|
"test:hypha": "node --no-warnings test/test-hypha-service.mjs",
|
|
25
25
|
"dev": "tsx src/cli.ts",
|
|
26
26
|
"dev:daemon": "tsx src/cli.ts daemon start-sync",
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
32
|
-
"hypha-rpc": "0.21.
|
|
32
|
+
"hypha-rpc": "0.21.29",
|
|
33
|
+
"ws": "^8.18.0",
|
|
33
34
|
"zod": "^3.24.4"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/node": ">=20",
|
|
38
|
+
"@types/ws": "^8.5.14",
|
|
37
39
|
"pkgroll": "^2.14.2",
|
|
38
40
|
"tsx": "^4.20.6",
|
|
39
41
|
"typescript": "5.9.3"
|