svamp-cli 0.1.52 → 0.1.53

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 ADDED
@@ -0,0 +1,193 @@
1
+ # svamp-cli
2
+
3
+ AI workspace daemon and CLI for [Hypha Cloud](https://hypha.aicell.io). Run AI agents locally with cloud sync, manage sessions, share with teammates, and orchestrate tasks.
4
+
5
+ **Svamp** (Swedish for "mushroom") is the interactive layer of Hypha Cloud — where teams and AI agents collaborate in real time.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g svamp-cli
11
+ ```
12
+
13
+ Requires **Node.js >= 22** (for native WebSocket support).
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Login to Hypha Cloud
19
+ svamp login
20
+
21
+ # Start interactive Claude session (synced to web app)
22
+ svamp
23
+
24
+ # Or start the daemon for background sessions
25
+ svamp daemon start
26
+
27
+ # Spawn a session
28
+ svamp session spawn claude -d ~/my-project
29
+
30
+ # Send a message
31
+ svamp session send <session-id> "Fix the failing tests"
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ Credentials are stored in `~/.svamp/.env`:
37
+
38
+ ```
39
+ HYPHA_SERVER_URL=https://hypha.aicell.io
40
+ HYPHA_TOKEN=<your-token>
41
+ HYPHA_WORKSPACE=<your-workspace>
42
+ ```
43
+
44
+ ## Commands
45
+
46
+ ### Interactive Mode
47
+
48
+ ```bash
49
+ svamp # Start Claude in terminal with cloud sync
50
+ svamp start [-d <path>] # Same, with explicit directory
51
+ ```
52
+
53
+ When you run `svamp` with no arguments, Claude starts in your terminal with full interactive access. Your session is synced to Hypha Cloud and visible in the web app. When a message arrives from the web app, svamp switches to remote mode automatically. Press Space-Space to return to local mode.
54
+
55
+ ### Login
56
+
57
+ ```bash
58
+ svamp login [server-url] # Login via browser OAuth
59
+ ```
60
+
61
+ ### Daemon Management
62
+
63
+ ```bash
64
+ svamp daemon start # Start daemon (detached)
65
+ svamp daemon stop # Stop (sessions preserved for auto-restore)
66
+ svamp daemon stop --cleanup # Stop and mark all sessions as stopped
67
+ svamp daemon restart # Restart seamlessly
68
+ svamp daemon status # Show daemon status
69
+ svamp daemon install # Install as system service (launchd/systemd)
70
+ svamp daemon uninstall # Remove system service
71
+ ```
72
+
73
+ ### Session Management
74
+
75
+ All session commands support `--machine <id>` / `-m <id>` to target a specific machine.
76
+
77
+ ```bash
78
+ svamp session list [--active] [--json]
79
+ svamp session machines # List discoverable machines
80
+ svamp session spawn <agent> [-d <path>] [--message <msg>] [--wait]
81
+ svamp session stop <id>
82
+ svamp session info <id> [--json]
83
+ svamp session send <id> <message> [--wait] [--timeout N]
84
+ svamp session wait <id> [--timeout N]
85
+ svamp session messages <id> [--last N] [--json]
86
+ svamp session attach <id> # Interactive terminal attach
87
+ svamp session approve <id> # Approve pending permission
88
+ svamp session deny <id> # Deny pending permission
89
+ ```
90
+
91
+ #### Session Sharing
92
+
93
+ ```bash
94
+ svamp session share <id> --list
95
+ svamp session share <id> --add <email>[:<role>] # Roles: view, interact, admin
96
+ svamp session share <id> --remove <email>
97
+ ```
98
+
99
+ #### Isolation & Security Flags (on spawn)
100
+
101
+ ```bash
102
+ svamp session spawn claude -d <path> --isolate
103
+ svamp session spawn claude -d <path> --share alice@example.com:admin
104
+ svamp session spawn claude -d <path> --security-context ./context.json
105
+ svamp session spawn claude -d <path> --deny-network
106
+ svamp session spawn claude -d <path> --deny-read /etc --allow-write /tmp/work
107
+ svamp session spawn claude -d <path> --allow-domain api.anthropic.com
108
+ ```
109
+
110
+ #### Ralph Loop (Iterative Task Automation)
111
+
112
+ ```bash
113
+ svamp session ralph-start <id> "<task>" [--promise DONE] [--max 10] [--cooldown 1]
114
+ svamp session ralph-cancel <id>
115
+ svamp session ralph-status <id>
116
+ ```
117
+
118
+ The Ralph Loop enables agents to iterate on tasks with verifiable completion. Each iteration the agent works toward a goal and signals completion via `<promise>DONE</promise>`.
119
+
120
+ ### Machine Management
121
+
122
+ ```bash
123
+ svamp machine share --list
124
+ svamp machine share --add <email>[:<role>]
125
+ svamp machine share --remove <email>
126
+ svamp machine share --config <path> # Apply security context config
127
+ svamp machine share --show-config
128
+ ```
129
+
130
+ ### Skills Marketplace
131
+
132
+ ```bash
133
+ svamp skills find <query> [--json] # Search marketplace
134
+ svamp skills install <name> [--force] # Install to ~/.claude/skills/<name>/
135
+ svamp skills list # List installed skills
136
+ svamp skills remove <name> # Remove skill
137
+ svamp skills publish <path> # Publish to marketplace
138
+ ```
139
+
140
+ ### Service Exposure (Cloud HTTP Services)
141
+
142
+ Expose HTTP services from cloud sandboxes or local machines to stable external URLs.
143
+
144
+ ```bash
145
+ svamp service expose <name> --port <port> # Create + join (auto-detect cloud/tunnel)
146
+ svamp service create <name> --port <port>
147
+ svamp service list [--json]
148
+ svamp service info <name> [--json]
149
+ svamp service delete <name>
150
+ svamp service tunnel <name> --port <port> # Tunnel local ports
151
+ ```
152
+
153
+ ### Local Agent Sessions
154
+
155
+ ```bash
156
+ svamp agent list # List known agents (ACP + MCP)
157
+ svamp agent <name> # Start local agent session (gemini, codex)
158
+ svamp agent -- <cmd> [args] # Start custom ACP agent
159
+ ```
160
+
161
+ ## Agent Backends
162
+
163
+ | Agent | Protocol | Transport |
164
+ |-------|----------|-----------|
165
+ | Claude | Native | CLI subprocess |
166
+ | Codex | MCP | STDIO (`codex mcp-server`) |
167
+ | Gemini | ACP | STDIO (`gemini --experimental-acp`) |
168
+
169
+ ## Security & Isolation
170
+
171
+ Sessions can be isolated using OS-level sandboxes:
172
+
173
+ | Method | Platform | Description |
174
+ |--------|----------|-------------|
175
+ | nono | macOS/Linux | Kernel-enforced capability sandbox (preferred) |
176
+ | Docker | Any | Container-based isolation |
177
+ | Podman | Any | Rootless container fallback |
178
+
179
+ Security contexts define per-user filesystem and network rules. See `--security-context` flag.
180
+
181
+ ## Development
182
+
183
+ ```bash
184
+ yarn install
185
+ yarn workspace svamp-cli build
186
+ yarn workspace svamp-cli test # Run unit tests (235+ tests)
187
+ yarn workspace svamp-cli test:e2e # Run E2E session tests
188
+ yarn workspace svamp-cli test:hypha # Run Hypha service integration tests
189
+ ```
190
+
191
+ ## License
192
+
193
+ See LICENSE in the repository root.
package/bin/svamp.mjs CHANGED
@@ -7,12 +7,10 @@ import { existsSync, readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { homedir } from 'os';
9
9
 
10
- // Simple .env loader — load from SVAMP_HOME or ~/.svamp/
11
- const envDir = process.env.SVAMP_HOME || join(homedir(), '.svamp');
12
- const envFile = join(envDir, '.env');
13
-
14
- if (existsSync(envFile)) {
15
- const lines = readFileSync(envFile, 'utf-8').split('\n');
10
+ // Simple .env loader — load from SVAMP_HOME or ~/.svamp/, fallback to ~/.hypha/.env
11
+ function loadEnvFile(path) {
12
+ if (!existsSync(path)) return false;
13
+ const lines = readFileSync(path, 'utf-8').split('\n');
16
14
  for (const line of lines) {
17
15
  const trimmed = line.trim();
18
16
  if (!trimmed || trimmed.startsWith('#')) continue;
@@ -20,10 +18,21 @@ if (existsSync(envFile)) {
20
18
  if (eqIdx === -1) continue;
21
19
  const key = trimmed.slice(0, eqIdx).trim();
22
20
  const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
23
- if (!process.env[key]) {
21
+ // HYPHA_* vars are managed by `svamp login` — .env is source of truth,
22
+ // always apply them even if already in the shell environment.
23
+ // Other vars only set if not already present.
24
+ if (key.startsWith('HYPHA_') || !process.env[key]) {
24
25
  process.env[key] = value;
25
26
  }
26
27
  }
28
+ return true;
29
+ }
30
+
31
+ const svampEnv = join(process.env.SVAMP_HOME || join(homedir(), '.svamp'), '.env');
32
+ if (!loadEnvFile(svampEnv)) {
33
+ // Fallback: load from ~/.hypha/.env (shared with hypha-cli)
34
+ const hyphaEnv = join(process.env.HYPHA_HOME || join(homedir(), '.hypha'), '.env');
35
+ loadEnvFile(hyphaEnv);
27
36
  }
28
37
 
29
38
  // Import and run the CLI
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { b as stopDaemon, s as startDaemon, d as daemonStatus } from './run-BnnUavlu.mjs';
1
+ import { b as stopDaemon, s as startDaemon, d as daemonStatus } from './run-CBhm4Jop.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -106,14 +106,14 @@ async function main() {
106
106
  } else if (subcommand === "skills") {
107
107
  await handleSkillsCommand();
108
108
  } else if (subcommand === "service" || subcommand === "svc") {
109
- const { handleServiceCommand } = await import('./commands-DwY2B7KW.mjs').then(function (n) { return n.c; });
109
+ const { handleServiceCommand } = await import('./commands-ZuFXrcot.mjs').then(function (n) { return n.c; });
110
110
  await handleServiceCommand();
111
111
  } else if (subcommand === "--help" || subcommand === "-h") {
112
112
  printHelp();
113
113
  } else if (!subcommand || subcommand === "start") {
114
114
  await handleInteractiveCommand();
115
115
  } else if (subcommand === "--version" || subcommand === "-v") {
116
- const pkg = await import('./package-D7EUXtnk.mjs').catch(() => ({ default: { version: "unknown" } }));
116
+ const pkg = await import('./package-DbeynOln.mjs').catch(() => ({ default: { version: "unknown" } }));
117
117
  console.log(`svamp version: ${pkg.default.version}`);
118
118
  } else {
119
119
  console.error(`Unknown command: ${subcommand}`);
@@ -122,7 +122,7 @@ async function main() {
122
122
  }
123
123
  }
124
124
  async function handleInteractiveCommand() {
125
- const { runInteractive } = await import('./run-Bw6aGHLA.mjs');
125
+ const { runInteractive } = await import('./run-Y0b60UYS.mjs');
126
126
  const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
127
127
  let directory = process.cwd();
128
128
  let resumeSessionId;
@@ -167,7 +167,7 @@ async function handleAgentCommand() {
167
167
  return;
168
168
  }
169
169
  if (agentArgs[0] === "list") {
170
- const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-BnnUavlu.mjs').then(function (n) { return n.i; });
170
+ const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-CBhm4Jop.mjs').then(function (n) { return n.i; });
171
171
  console.log("Known agents:");
172
172
  for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
173
173
  console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
@@ -179,7 +179,7 @@ async function handleAgentCommand() {
179
179
  console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
180
180
  return;
181
181
  }
182
- const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-BnnUavlu.mjs').then(function (n) { return n.i; });
182
+ const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-CBhm4Jop.mjs').then(function (n) { return n.i; });
183
183
  let cwd = process.cwd();
184
184
  const filteredArgs = [];
185
185
  for (let i = 0; i < agentArgs.length; i++) {
@@ -203,12 +203,12 @@ async function handleAgentCommand() {
203
203
  console.log(`Starting ${config.agentName} agent in ${cwd}...`);
204
204
  let backend;
205
205
  if (KNOWN_MCP_AGENTS[config.agentName]) {
206
- const { CodexMcpBackend } = await import('./run-BnnUavlu.mjs').then(function (n) { return n.j; });
206
+ const { CodexMcpBackend } = await import('./run-CBhm4Jop.mjs').then(function (n) { return n.j; });
207
207
  backend = new CodexMcpBackend({ cwd, log: logFn });
208
208
  } else {
209
- const { AcpBackend } = await import('./run-BnnUavlu.mjs').then(function (n) { return n.h; });
210
- const { GeminiTransport } = await import('./run-BnnUavlu.mjs').then(function (n) { return n.G; });
211
- const { DefaultTransport } = await import('./run-BnnUavlu.mjs').then(function (n) { return n.D; });
209
+ const { AcpBackend } = await import('./run-CBhm4Jop.mjs').then(function (n) { return n.h; });
210
+ const { GeminiTransport } = await import('./run-CBhm4Jop.mjs').then(function (n) { return n.G; });
211
+ const { DefaultTransport } = await import('./run-CBhm4Jop.mjs').then(function (n) { return n.D; });
212
212
  const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
213
213
  backend = new AcpBackend({
214
214
  agentName: config.agentName,
@@ -326,7 +326,7 @@ async function handleSessionCommand() {
326
326
  printSessionHelp();
327
327
  return;
328
328
  }
329
- const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionQueueAdd, sessionQueueList, sessionQueueClear } = await import('./commands-CI_BVphs.mjs');
329
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionQueueAdd, sessionQueueList, sessionQueueClear } = await import('./commands-VGt5ofDo.mjs');
330
330
  const parseFlagStr = (flag, shortFlag) => {
331
331
  for (let i = 1; i < sessionArgs.length; i++) {
332
332
  if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
@@ -386,7 +386,7 @@ async function handleSessionCommand() {
386
386
  allowDomain.push(sessionArgs[++i]);
387
387
  }
388
388
  }
389
- const { parseShareArg } = await import('./commands-CI_BVphs.mjs');
389
+ const { parseShareArg } = await import('./commands-VGt5ofDo.mjs');
390
390
  const shareEntries = share.map((s) => parseShareArg(s));
391
391
  await sessionSpawn(agent, dir, targetMachineId, {
392
392
  message,
@@ -470,7 +470,7 @@ async function handleSessionCommand() {
470
470
  console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
471
471
  process.exit(1);
472
472
  }
473
- const { sessionApprove } = await import('./commands-CI_BVphs.mjs');
473
+ const { sessionApprove } = await import('./commands-VGt5ofDo.mjs');
474
474
  const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
475
475
  await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
476
476
  json: hasFlag("--json")
@@ -480,7 +480,7 @@ async function handleSessionCommand() {
480
480
  console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
481
481
  process.exit(1);
482
482
  }
483
- const { sessionDeny } = await import('./commands-CI_BVphs.mjs');
483
+ const { sessionDeny } = await import('./commands-VGt5ofDo.mjs');
484
484
  const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
485
485
  await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
486
486
  json: hasFlag("--json")
@@ -549,7 +549,7 @@ async function handleMachineCommand() {
549
549
  return;
550
550
  }
551
551
  if (machineSubcommand === "share") {
552
- const { machineShare } = await import('./commands-CI_BVphs.mjs');
552
+ const { machineShare } = await import('./commands-VGt5ofDo.mjs');
553
553
  let machineId;
554
554
  const shareArgs = [];
555
555
  for (let i = 1; i < machineArgs.length; i++) {
@@ -579,7 +579,7 @@ async function handleMachineCommand() {
579
579
  }
580
580
  await machineShare(machineId, { add, remove, list, configPath, showConfig });
581
581
  } else if (machineSubcommand === "exec") {
582
- const { machineExec } = await import('./commands-CI_BVphs.mjs');
582
+ const { machineExec } = await import('./commands-VGt5ofDo.mjs');
583
583
  let machineId;
584
584
  let cwd;
585
585
  const cmdParts = [];
@@ -599,7 +599,7 @@ async function handleMachineCommand() {
599
599
  }
600
600
  await machineExec(machineId, command, cwd);
601
601
  } else if (machineSubcommand === "info") {
602
- const { machineInfo } = await import('./commands-CI_BVphs.mjs');
602
+ const { machineInfo } = await import('./commands-VGt5ofDo.mjs');
603
603
  let machineId;
604
604
  for (let i = 1; i < machineArgs.length; i++) {
605
605
  if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
@@ -608,7 +608,7 @@ async function handleMachineCommand() {
608
608
  }
609
609
  await machineInfo(machineId);
610
610
  } else if (machineSubcommand === "ls") {
611
- const { machineLs } = await import('./commands-CI_BVphs.mjs');
611
+ const { machineLs } = await import('./commands-VGt5ofDo.mjs');
612
612
  let machineId;
613
613
  let showHidden = false;
614
614
  let path;
@@ -738,7 +738,7 @@ Please open this URL in your browser:
738
738
  }
739
739
  envLines.push(`HYPHA_SERVER_URL=${serverUrl}`);
740
740
  envLines.push(`HYPHA_TOKEN=${longLivedToken}`);
741
- envLines.push(`HYPHA_WORKSPACE=${userId}`);
741
+ envLines.push(`HYPHA_WORKSPACE=${workspace}`);
742
742
  while (envLines.length > 0 && envLines[envLines.length - 1].trim() === "") {
743
743
  envLines.pop();
744
744
  }