svamp-cli 0.1.63 → 0.1.65

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.
@@ -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 { join, resolve } from 'node:path';
4
4
  import { mkdirSync, writeFileSync, existsSync, unlinkSync, readFileSync, watch } from 'node:fs';
5
- import { c as connectToHypha, a as registerSessionService } from './run-BaMf-bAo.mjs';
5
+ import { c as connectToHypha, a as registerSessionService } from './run-DPhSmbr1.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -203,9 +203,9 @@ function createSessionScanner(opts) {
203
203
  },
204
204
  sync: readAndSync,
205
205
  cleanup() {
206
- stopped = true;
207
206
  stopWatching();
208
207
  if (currentSessionId) readAndSync();
208
+ stopped = true;
209
209
  }
210
210
  };
211
211
  }
@@ -432,11 +432,18 @@ async function runClaudeTurn(opts) {
432
432
  opts.onThinkingChange(true);
433
433
  let currentText = "";
434
434
  return new Promise((resolve) => {
435
+ let killTimer;
435
436
  const abortHandler = () => {
436
437
  try {
437
438
  child.kill("SIGTERM");
438
439
  } catch {
439
440
  }
441
+ killTimer = setTimeout(() => {
442
+ try {
443
+ child.kill("SIGKILL");
444
+ } catch {
445
+ }
446
+ }, 5e3);
440
447
  };
441
448
  if (opts.signal.aborted) {
442
449
  abortHandler();
@@ -458,18 +465,31 @@ async function runClaudeTurn(opts) {
458
465
  currentText += text;
459
466
  });
460
467
  });
468
+ rl.on("error", (err) => {
469
+ opts.log(`[remote] readline error: ${err.message}`);
470
+ });
461
471
  if (child.stderr) {
462
472
  child.stderr.on("data", (data) => {
463
473
  opts.log(`[remote:stderr] ${data.toString().trim()}`);
464
474
  });
465
475
  }
466
476
  child.on("error", (err) => {
477
+ if (killTimer) {
478
+ clearTimeout(killTimer);
479
+ killTimer = void 0;
480
+ }
481
+ rl.close();
467
482
  opts.log(`[remote] Error: ${err.message}`);
468
483
  opts.onThinkingChange(false);
469
484
  if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
470
485
  resolve("error");
471
486
  });
472
487
  child.on("exit", (code, signal) => {
488
+ if (killTimer) {
489
+ clearTimeout(killTimer);
490
+ killTimer = void 0;
491
+ }
492
+ rl.close();
473
493
  opts.log(`[remote] Exit: code=${code}, signal=${signal}`);
474
494
  opts.onThinkingChange(false);
475
495
  if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
@@ -570,18 +590,25 @@ async function loop(opts) {
570
590
  abortController.abort();
571
591
  }
572
592
  }, 500);
573
- const result = await runLocalMode({
574
- cwd: opts.cwd,
575
- claudeSessionId,
576
- onSessionFound,
577
- onMessage: opts.onMessage,
578
- onThinkingChange: opts.onThinkingChange,
579
- abort: abortController.signal,
580
- hookSettingsPath: opts.hookSettingsPath,
581
- claudeArgs: opts.claudeArgs,
582
- log
583
- });
584
- if (messageWatcher) clearInterval(messageWatcher);
593
+ let result;
594
+ try {
595
+ result = await runLocalMode({
596
+ cwd: opts.cwd,
597
+ claudeSessionId,
598
+ onSessionFound,
599
+ onMessage: opts.onMessage,
600
+ onThinkingChange: opts.onThinkingChange,
601
+ abort: abortController.signal,
602
+ hookSettingsPath: opts.hookSettingsPath,
603
+ claudeArgs: opts.claudeArgs,
604
+ log
605
+ });
606
+ } finally {
607
+ if (messageWatcher) {
608
+ clearInterval(messageWatcher);
609
+ messageWatcher = null;
610
+ }
611
+ }
585
612
  if (result.type === "switch") {
586
613
  mode = "remote";
587
614
  opts.onModeChange(mode);
@@ -664,8 +691,8 @@ async function runInteractive(options) {
664
691
  if (messageQueue.length > 0) {
665
692
  return Promise.resolve(messageQueue.shift());
666
693
  }
667
- return new Promise((resolve) => {
668
- messageWaiter = { resolve };
694
+ return new Promise((resolve2) => {
695
+ messageWaiter = { resolve: resolve2 };
669
696
  });
670
697
  }
671
698
  function hasRemoteMessage() {
@@ -706,9 +733,9 @@ async function runInteractive(options) {
706
733
  log("[hypha] Restart requested");
707
734
  return { success: false, message: "Restart not supported in interactive mode" };
708
735
  },
709
- onKillSession: () => {
736
+ onKillSession: async () => {
710
737
  log("[hypha] Kill requested");
711
- cleanup();
738
+ await cleanup();
712
739
  process.exit(0);
713
740
  },
714
741
  // File system operations — run locally since we have direct access
@@ -728,28 +755,43 @@ async function runInteractive(options) {
728
755
  return { output: err.stdout || "", error: err.message, exitCode: err.status || 1 };
729
756
  }
730
757
  },
731
- onReadFile: async (path) => {
758
+ onReadFile: async (filePath) => {
759
+ const resolvedPath = resolve(cwd, filePath);
760
+ if (resolvedPath !== resolve(cwd) && !resolvedPath.startsWith(resolve(cwd) + "/")) {
761
+ throw new Error("Path outside working directory");
762
+ }
732
763
  try {
733
764
  const { readFileSync: readFileSync2 } = await import('fs');
734
- return readFileSync2(path, "utf-8");
765
+ const buf = readFileSync2(resolvedPath);
766
+ return buf.toString("base64");
735
767
  } catch (err) {
736
768
  throw new Error(`Cannot read file: ${err.message}`);
737
769
  }
738
770
  },
739
- onWriteFile: async (path, content) => {
771
+ onWriteFile: async (filePath, content) => {
772
+ const resolvedPath = resolve(cwd, filePath);
773
+ if (resolvedPath !== resolve(cwd) && !resolvedPath.startsWith(resolve(cwd) + "/")) {
774
+ throw new Error("Path outside working directory");
775
+ }
740
776
  try {
741
- const { writeFileSync } = await import('fs');
742
- writeFileSync(path, content, "utf-8");
777
+ const { writeFileSync, mkdirSync } = await import('fs');
778
+ const { dirname: dir } = await import('path');
779
+ mkdirSync(dir(resolvedPath), { recursive: true });
780
+ writeFileSync(resolvedPath, Buffer.from(content, "base64"));
743
781
  } catch (err) {
744
782
  throw new Error(`Cannot write file: ${err.message}`);
745
783
  }
746
784
  },
747
- onListDirectory: async (path) => {
785
+ onListDirectory: async (dirPath) => {
786
+ const resolvedDir = resolve(cwd, dirPath || ".");
787
+ if (resolvedDir !== resolve(cwd) && !resolvedDir.startsWith(resolve(cwd) + "/")) {
788
+ throw new Error("Path outside working directory");
789
+ }
748
790
  const { readdirSync, statSync } = await import('fs');
749
791
  const { join: joinPath } = await import('path');
750
- return readdirSync(path).map((name) => {
792
+ return readdirSync(resolvedDir).map((name) => {
751
793
  try {
752
- const st = statSync(joinPath(path, name));
794
+ const st = statSync(joinPath(resolvedDir, name));
753
795
  return { name, type: st.isDirectory() ? "directory" : "file", size: st.size };
754
796
  } catch {
755
797
  return { name, type: "unknown", size: 0 };
@@ -803,7 +845,10 @@ async function runInteractive(options) {
803
845
  let keepAliveInterval = null;
804
846
  if (sessionService) {
805
847
  keepAliveInterval = setInterval(() => {
806
- sessionService.sendKeepAlive(false);
848
+ try {
849
+ sessionService.sendKeepAlive(false);
850
+ } catch {
851
+ }
807
852
  }, 3e4);
808
853
  }
809
854
  const cleanup = async () => {
@@ -870,11 +915,18 @@ async function runInteractive(options) {
870
915
  onModeChange: (mode) => {
871
916
  log(`Mode changed: ${mode}`);
872
917
  currentMode = mode;
918
+ if (mode === "local" && messageWaiter) {
919
+ messageWaiter = null;
920
+ }
873
921
  if (sessionService) {
874
922
  sessionService.updateAgentState({
875
923
  controlledByUser: mode === "local"
876
924
  });
877
- sessionService.updateMetadata({ ...metadata, lifecycleState: "running" });
925
+ sessionService.updateMetadata({
926
+ ...metadata,
927
+ claudeSessionId: claudeSessionId || void 0,
928
+ lifecycleState: "running"
929
+ });
878
930
  sessionService.sendKeepAlive(false, mode);
879
931
  }
880
932
  },
@@ -1,6 +1,5 @@
1
1
  import * as os from 'os';
2
- import * as http from 'http';
3
- import { requireSandboxApiEnv } from './api-Cegey1dh.mjs';
2
+ import { requireSandboxApiEnv } from './api-BRbsyqJ4.mjs';
4
3
  import { WebSocket } from 'ws';
5
4
 
6
5
  class TunnelClient {
@@ -98,6 +97,7 @@ class TunnelClient {
98
97
  } catch {
99
98
  return;
100
99
  }
100
+ if (!msg || typeof msg !== "object") return;
101
101
  switch (msg.type) {
102
102
  case "ping":
103
103
  this.send({ type: "pong" });
@@ -124,72 +124,56 @@ class TunnelClient {
124
124
  async proxyRequest(req) {
125
125
  this.requestCount++;
126
126
  const port = req.port || this.options.ports[0];
127
+ const url = `http://${this.options.localHost}:${port}${req.path}`;
128
+ const controller = new AbortController();
129
+ const timeout = setTimeout(() => controller.abort(), this.options.requestTimeout);
127
130
  const forwardHeaders = { ...req.headers };
128
- forwardHeaders["accept-encoding"] = "identity";
129
- const chunks = [];
130
- const status_holder = { status: 502, headers: {} };
131
- await new Promise((resolve, reject) => {
132
- const timeout = setTimeout(() => reject(new Error("timeout")), this.options.requestTimeout);
133
- const options = {
134
- hostname: this.options.localHost,
135
- port,
136
- path: req.path,
137
- method: req.method,
138
- headers: forwardHeaders
139
- };
140
- const httpReq = http.request(options, (res) => {
141
- status_holder.status = res.statusCode ?? 502;
142
- const rawHeaders = res.headers;
143
- for (const [key, value] of Object.entries(rawHeaders)) {
144
- if (Array.isArray(value)) {
145
- status_holder.headers[key] = value.join(", ");
146
- } else if (value !== void 0) {
147
- status_holder.headers[key] = value;
148
- }
149
- }
150
- res.on("data", (chunk) => chunks.push(chunk));
151
- res.on("end", () => {
152
- clearTimeout(timeout);
153
- resolve();
154
- });
155
- res.on("error", (e) => {
156
- clearTimeout(timeout);
157
- reject(e);
158
- });
159
- });
160
- httpReq.on("error", (e) => {
161
- clearTimeout(timeout);
162
- reject(e);
131
+ delete forwardHeaders["accept-encoding"];
132
+ delete forwardHeaders["Accept-Encoding"];
133
+ const init = {
134
+ method: req.method,
135
+ headers: forwardHeaders,
136
+ signal: controller.signal
137
+ };
138
+ if (req.body && !["GET", "HEAD"].includes(req.method.toUpperCase())) {
139
+ init.body = Buffer.from(req.body, "base64");
140
+ }
141
+ try {
142
+ const res = await fetch(url, init);
143
+ clearTimeout(timeout);
144
+ const bodyBuffer = await res.arrayBuffer();
145
+ const bodyBase64 = bodyBuffer.byteLength > 0 ? Buffer.from(bodyBuffer).toString("base64") : void 0;
146
+ const headers = {};
147
+ res.headers.forEach((value, key) => {
148
+ headers[key] = value;
163
149
  });
164
- if (req.body && !["GET", "HEAD"].includes(req.method.toUpperCase())) {
165
- httpReq.write(Buffer.from(req.body, "base64"));
166
- }
167
- httpReq.end();
168
- }).then(() => {
169
- const bodyBuffer = Buffer.concat(chunks);
170
- const bodyBase64 = bodyBuffer.length > 0 ? bodyBuffer.toString("base64") : void 0;
171
- for (const h of ["connection", "keep-alive", "transfer-encoding"]) {
172
- delete status_holder.headers[h];
173
- }
174
150
  this.send({
175
151
  type: "response",
176
152
  id: req.id,
177
- status: status_holder.status,
178
- headers: status_holder.headers,
153
+ status: res.status,
154
+ headers,
179
155
  body: bodyBase64
180
156
  });
181
- }).catch((err) => {
182
- const isTimeout = err.message === "timeout";
183
- this.send({
184
- type: "response",
185
- id: req.id,
186
- status: isTimeout ? 504 : 502,
187
- headers: { "content-type": "text/plain" },
188
- body: Buffer.from(
189
- isTimeout ? "Tunnel: request timeout" : `Tunnel: local service error: ${err.message}`
190
- ).toString("base64")
191
- });
192
- });
157
+ } catch (err) {
158
+ clearTimeout(timeout);
159
+ if (err.name === "AbortError") {
160
+ this.send({
161
+ type: "response",
162
+ id: req.id,
163
+ status: 504,
164
+ headers: { "content-type": "text/plain" },
165
+ body: Buffer.from("Tunnel: request timeout").toString("base64")
166
+ });
167
+ } else {
168
+ this.send({
169
+ type: "response",
170
+ id: req.id,
171
+ status: 502,
172
+ headers: { "content-type": "text/plain" },
173
+ body: Buffer.from(`Tunnel: local service error: ${err.message}`).toString("base64")
174
+ });
175
+ }
176
+ }
193
177
  }
194
178
  // ── WebSocket proxying ───────────────────────────────────────────────
195
179
  async handleWsOpen(msg) {
@@ -225,10 +209,14 @@ class TunnelClient {
225
209
  const localWs = this.localWebSockets.get(msg.id);
226
210
  if (!localWs || localWs.readyState !== WebSocket.OPEN) return;
227
211
  const buf = Buffer.from(msg.data, "base64");
228
- if (msg.is_text) {
229
- localWs.send(buf.toString("utf8"));
230
- } else {
231
- localWs.send(buf);
212
+ try {
213
+ if (msg.is_text) {
214
+ localWs.send(buf.toString("utf8"));
215
+ } else {
216
+ localWs.send(buf);
217
+ }
218
+ } catch (err) {
219
+ this.options.onError?.(err);
232
220
  }
233
221
  }
234
222
  handleWsClose(msg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
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": "rm -rf dist && 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-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-tunnel-proxy.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 && npx tsx test/test-supervisor.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",