svamp-cli 0.1.6 → 0.1.7

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-C2z4Zl9E.mjs';
1
+ import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-aevHxpEl.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -74,10 +74,13 @@ async function main() {
74
74
  }
75
75
  } else if (subcommand === "agent") {
76
76
  await handleAgentCommand();
77
+ } else if (subcommand === "session") {
78
+ await handleSessionCommand();
77
79
  } else if (subcommand === "--help" || subcommand === "-h" || !subcommand) {
78
80
  printHelp();
79
81
  } else if (subcommand === "--version" || subcommand === "-v") {
80
- console.log("svamp version: 0.1.0");
82
+ const pkg = await import('./package-DD4JHOe_.mjs').catch(() => ({ default: { version: "unknown" } }));
83
+ console.log(`svamp version: ${pkg.default.version}`);
81
84
  } else {
82
85
  console.error(`Unknown command: ${subcommand}`);
83
86
  printHelp();
@@ -91,7 +94,7 @@ async function handleAgentCommand() {
91
94
  return;
92
95
  }
93
96
  if (agentArgs[0] === "list") {
94
- const { KNOWN_ACP_AGENTS } = await import('./run-C2z4Zl9E.mjs').then(function (n) { return n.f; });
97
+ const { KNOWN_ACP_AGENTS } = await import('./run-aevHxpEl.mjs').then(function (n) { return n.f; });
95
98
  console.log("Known ACP agents:");
96
99
  for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
97
100
  console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")}`);
@@ -100,10 +103,10 @@ async function handleAgentCommand() {
100
103
  console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
101
104
  return;
102
105
  }
103
- const { resolveAcpAgentConfig } = await import('./run-C2z4Zl9E.mjs').then(function (n) { return n.f; });
104
- const { AcpBackend } = await import('./run-C2z4Zl9E.mjs').then(function (n) { return n.e; });
105
- const { GeminiTransport } = await import('./run-C2z4Zl9E.mjs').then(function (n) { return n.G; });
106
- const { DefaultTransport } = await import('./run-C2z4Zl9E.mjs').then(function (n) { return n.D; });
106
+ const { resolveAcpAgentConfig } = await import('./run-aevHxpEl.mjs').then(function (n) { return n.f; });
107
+ const { AcpBackend } = await import('./run-aevHxpEl.mjs').then(function (n) { return n.e; });
108
+ const { GeminiTransport } = await import('./run-aevHxpEl.mjs').then(function (n) { return n.G; });
109
+ const { DefaultTransport } = await import('./run-aevHxpEl.mjs').then(function (n) { return n.D; });
107
110
  let cwd = process.cwd();
108
111
  const filteredArgs = [];
109
112
  for (let i = 0; i < agentArgs.length; i++) {
@@ -187,7 +190,8 @@ Agent stopped: ${msg.detail || ""}`);
187
190
  console.log(`Session started: ${result.sessionId}`);
188
191
  process.stdout.write("> ");
189
192
  } catch (err) {
190
- console.error(`Failed to start ${config.agentName}: ${err.message}`);
193
+ const errMsg = err?.message || (typeof err === "object" ? JSON.stringify(err) : String(err));
194
+ console.error(`Failed to start ${config.agentName}: ${errMsg}`);
191
195
  process.exit(1);
192
196
  }
193
197
  const readline = await import('readline');
@@ -224,6 +228,62 @@ Agent stopped: ${msg.detail || ""}`);
224
228
  process.exit(0);
225
229
  });
226
230
  }
231
+ async function handleSessionCommand() {
232
+ const sessionArgs = args.slice(1);
233
+ const sessionSubcommand = sessionArgs[0];
234
+ if (!sessionSubcommand || sessionSubcommand === "--help" || sessionSubcommand === "-h") {
235
+ printSessionHelp();
236
+ return;
237
+ }
238
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach } = await import('./commands-p36abIvL.mjs');
239
+ if (sessionSubcommand === "list" || sessionSubcommand === "ls") {
240
+ await sessionList();
241
+ } else if (sessionSubcommand === "spawn") {
242
+ const agent = sessionArgs[1] || "claude";
243
+ let dir = process.cwd();
244
+ for (let i = 1; i < sessionArgs.length; i++) {
245
+ if ((sessionArgs[i] === "-d" || sessionArgs[i] === "--directory") && i + 1 < sessionArgs.length) {
246
+ dir = sessionArgs[i + 1];
247
+ i++;
248
+ }
249
+ }
250
+ await sessionSpawn(agent, dir);
251
+ } else if (sessionSubcommand === "stop") {
252
+ if (!sessionArgs[1]) {
253
+ console.error("Usage: svamp session stop <session-id>");
254
+ process.exit(1);
255
+ }
256
+ await sessionStop(sessionArgs[1]);
257
+ } else if (sessionSubcommand === "info") {
258
+ if (!sessionArgs[1]) {
259
+ console.error("Usage: svamp session info <session-id>");
260
+ process.exit(1);
261
+ }
262
+ await sessionInfo(sessionArgs[1]);
263
+ } else if (sessionSubcommand === "messages" || sessionSubcommand === "msgs") {
264
+ if (!sessionArgs[1]) {
265
+ console.error("Usage: svamp session messages <session-id> [--last N]");
266
+ process.exit(1);
267
+ }
268
+ let last;
269
+ const lastIdx = sessionArgs.indexOf("--last");
270
+ if (lastIdx >= 0 && sessionArgs[lastIdx + 1]) {
271
+ last = parseInt(sessionArgs[lastIdx + 1], 10);
272
+ }
273
+ await sessionMessages(sessionArgs[1], last);
274
+ } else if (sessionSubcommand === "attach") {
275
+ if (!sessionArgs[1]) {
276
+ console.error("Usage: svamp session attach <session-id>");
277
+ process.exit(1);
278
+ }
279
+ await sessionAttach(sessionArgs[1]);
280
+ } else {
281
+ console.error(`Unknown session command: ${sessionSubcommand}`);
282
+ printSessionHelp();
283
+ process.exit(1);
284
+ }
285
+ process.exit(0);
286
+ }
227
287
  async function loginToHypha() {
228
288
  const serverUrl = args[1] || process.env.HYPHA_SERVER_URL;
229
289
  if (!serverUrl) {
@@ -313,15 +373,19 @@ function printHelp() {
313
373
  svamp \u2014 Svamp CLI with Hypha transport
314
374
 
315
375
  Usage:
316
- svamp login [url] Login to Hypha (opens browser, stores token)
317
- svamp daemon start Start the daemon (detached)
318
- svamp daemon stop Stop the daemon
319
- svamp daemon status Show daemon status
320
- svamp agent list List known ACP agents
321
- svamp agent <name> Start interactive ACP agent session
322
- svamp agent -- <cmd> Start custom ACP agent
323
- svamp --version Show version
324
- svamp --help Show this help
376
+ svamp login [url] Login to Hypha (opens browser, stores token)
377
+ svamp daemon start Start the daemon (detached)
378
+ svamp daemon stop Stop the daemon
379
+ svamp daemon status Show daemon status
380
+ svamp session list List active daemon sessions
381
+ svamp session spawn Spawn a new session on the daemon
382
+ svamp session attach <id> Attach to a session (interactive)
383
+ svamp session --help Show all session commands
384
+ svamp agent list List known ACP agents
385
+ svamp agent <name> Start local ACP agent session
386
+ svamp agent -- <cmd> Start custom ACP agent
387
+ svamp --version Show version
388
+ svamp --help Show this help
325
389
 
326
390
  Environment variables:
327
391
  HYPHA_SERVER_URL Hypha server URL (required for daemon)
@@ -341,6 +405,37 @@ Usage:
341
405
  svamp daemon status Show daemon status
342
406
  `);
343
407
  }
408
+ function printSessionHelp() {
409
+ console.log(`
410
+ svamp session \u2014 Manage daemon sessions (Claude, Gemini, OpenCode)
411
+
412
+ Usage:
413
+ svamp session list List active sessions
414
+ svamp session spawn <agent> [-d <path>] Spawn a new session
415
+ svamp session stop <id> Stop a session
416
+ svamp session info <id> Show session metadata
417
+ svamp session messages <id> [--last N] Dump recent messages
418
+ svamp session attach <id> Attach to session (interactive)
419
+
420
+ Agents: claude (default), gemini, opencode
421
+
422
+ Session IDs can be abbreviated (prefix match, like Docker).
423
+
424
+ Attach commands:
425
+ /quit, /detach Detach (session keeps running)
426
+ /abort, /cancel Cancel current agent turn
427
+ /kill Stop the session
428
+ /info Show session status
429
+
430
+ Examples:
431
+ svamp session list
432
+ svamp session spawn claude -d ~/projects/myapp
433
+ svamp session spawn gemini
434
+ svamp session info abc12345
435
+ svamp session messages abc12345 --last 10
436
+ svamp session attach abc12345
437
+ `);
438
+ }
344
439
  function printAgentHelp() {
345
440
  console.log(`
346
441
  svamp agent \u2014 Interactive ACP agent sessions
@@ -0,0 +1,476 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import os from 'node:os';
4
+ import { c as connectToHypha } from './run-aevHxpEl.mjs';
5
+ import 'os';
6
+ import 'fs/promises';
7
+ import 'fs';
8
+ import 'path';
9
+ import 'url';
10
+ import 'child_process';
11
+ import 'crypto';
12
+ import 'node:crypto';
13
+ import '@modelcontextprotocol/sdk/server/mcp.js';
14
+ import 'node:http';
15
+ import '@modelcontextprotocol/sdk/server/streamableHttp.js';
16
+ import 'zod';
17
+ import 'node:child_process';
18
+ import '@agentclientprotocol/sdk';
19
+
20
+ const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
21
+ const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
22
+ const ENV_FILE = join(SVAMP_HOME, ".env");
23
+ function loadDotEnv() {
24
+ if (!existsSync(ENV_FILE)) return;
25
+ const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
26
+ for (const line of lines) {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith("#")) continue;
29
+ const eqIdx = trimmed.indexOf("=");
30
+ if (eqIdx === -1) continue;
31
+ const key = trimmed.slice(0, eqIdx).trim();
32
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
33
+ if (!process.env[key]) {
34
+ process.env[key] = value;
35
+ }
36
+ }
37
+ }
38
+ function readDaemonState() {
39
+ if (!existsSync(DAEMON_STATE_FILE)) return null;
40
+ try {
41
+ return JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function isDaemonAlive(state) {
47
+ try {
48
+ process.kill(state.pid, 0);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ async function connectAndGetMachine() {
55
+ loadDotEnv();
56
+ const state = readDaemonState();
57
+ if (!state || !isDaemonAlive(state)) {
58
+ console.error('Daemon is not running. Start it with "svamp daemon start".');
59
+ process.exit(1);
60
+ }
61
+ const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
62
+ const token = process.env.HYPHA_TOKEN;
63
+ if (!serverUrl) {
64
+ console.error('No Hypha server URL. Run "svamp login <url>" first.');
65
+ process.exit(1);
66
+ }
67
+ const origLog = console.log;
68
+ const origWarn = console.warn;
69
+ const origInfo = console.info;
70
+ const stdoutWrite = process.stdout.write.bind(process.stdout);
71
+ console.log = () => {
72
+ };
73
+ console.warn = () => {
74
+ };
75
+ console.info = () => {
76
+ };
77
+ process.stdout.write = (chunk, ...args) => {
78
+ if (typeof chunk === "string" && (chunk.includes("WebSocket") || chunk.includes("Connection established") || chunk.includes("registering service") || chunk.includes("Subscribing") || chunk.includes("subscribed") || chunk.includes("Cleaning up") || chunk.includes("disconnected"))) {
79
+ return true;
80
+ }
81
+ return stdoutWrite(chunk, ...args);
82
+ };
83
+ const restoreConsole = () => {
84
+ console.log = origLog;
85
+ console.warn = origWarn;
86
+ console.info = origInfo;
87
+ process.stdout.write = stdoutWrite;
88
+ };
89
+ let server;
90
+ try {
91
+ server = await connectToHypha({
92
+ serverUrl,
93
+ token,
94
+ name: "svamp-session-cli"
95
+ });
96
+ } catch (err) {
97
+ restoreConsole();
98
+ console.error(`Failed to connect to Hypha: ${err.message}`);
99
+ process.exit(1);
100
+ }
101
+ let machine;
102
+ if (state.machineId) {
103
+ try {
104
+ machine = await server.getService(`svamp-machine-${state.machineId}`);
105
+ } catch {
106
+ }
107
+ }
108
+ if (!machine) {
109
+ try {
110
+ const services = await server.listServices();
111
+ const machineServices = services.filter((svc) => {
112
+ const sid = svc.id || svc.name || "";
113
+ return sid.includes("svamp-machine-");
114
+ });
115
+ if (machineServices.length === 0) {
116
+ restoreConsole();
117
+ console.error("No machine service found. Is the daemon registered on Hypha?");
118
+ await server.disconnect();
119
+ process.exit(1);
120
+ }
121
+ const svcId = machineServices[0].id || machineServices[0].name;
122
+ machine = await server.getService(svcId);
123
+ } catch (err) {
124
+ restoreConsole();
125
+ console.error(`Failed to discover machine service: ${err.message}`);
126
+ await server.disconnect();
127
+ process.exit(1);
128
+ }
129
+ }
130
+ restoreConsole();
131
+ return { server, machine };
132
+ }
133
+ function resolveSessionId(sessions, partial) {
134
+ const exact = sessions.find((s) => s.sessionId === partial);
135
+ if (exact) return exact;
136
+ const matches = sessions.filter((s) => s.sessionId.startsWith(partial));
137
+ if (matches.length === 1) return matches[0];
138
+ if (matches.length === 0) {
139
+ console.error(`No session found matching: ${partial}`);
140
+ console.error('Run "svamp session list" to see active sessions.');
141
+ process.exit(1);
142
+ }
143
+ console.error(`Ambiguous session ID "${partial}". Matches:`);
144
+ for (const s of matches) {
145
+ console.error(` ${s.sessionId}`);
146
+ }
147
+ process.exit(1);
148
+ }
149
+ function truncate(str, max) {
150
+ if (str.length <= max) return str;
151
+ return "..." + str.slice(str.length - max + 3);
152
+ }
153
+ function renderMessage(msg) {
154
+ const content = msg.content;
155
+ if (!content) return;
156
+ const role = content.role;
157
+ if (role === "user") {
158
+ const data = content.content;
159
+ let text;
160
+ if (typeof data === "string") {
161
+ try {
162
+ const parsed = JSON.parse(data);
163
+ text = parsed?.text || parsed?.content?.text || data;
164
+ } catch {
165
+ text = data;
166
+ }
167
+ } else if (data?.text) {
168
+ text = data.text;
169
+ } else if (data?.type === "text") {
170
+ text = data.text || "";
171
+ } else {
172
+ text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
173
+ }
174
+ console.log(`\x1B[36m[user]\x1B[0m ${text}`);
175
+ } else if (role === "agent" || role === "assistant") {
176
+ const data = content.content?.data || content.content;
177
+ if (!data) return;
178
+ if (data.type === "assistant" && Array.isArray(data.content)) {
179
+ for (const block of data.content) {
180
+ if (block.type === "text" && block.text) {
181
+ process.stdout.write(block.text);
182
+ if (!block.text.endsWith("\n")) process.stdout.write("\n");
183
+ } else if (block.type === "tool_use") {
184
+ const argsStr = JSON.stringify(block.input || {}).slice(0, 120);
185
+ console.log(`\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})`);
186
+ } else if (block.type === "tool_result") {
187
+ const resultStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
188
+ console.log(`\x1B[90m[result]\x1B[0m ${resultStr.slice(0, 200)}${resultStr.length > 200 ? "..." : ""}`);
189
+ } else if (block.type === "thinking") {
190
+ const text = block.thinking || block.text || "";
191
+ if (text) console.log(`\x1B[90m[thinking] ${text.slice(0, 200)}\x1B[0m`);
192
+ }
193
+ }
194
+ } else if (data.type === "result") {
195
+ if (data.result) console.log(`\x1B[32m[done]\x1B[0m ${data.result}`);
196
+ } else if (data.type === "output") {
197
+ const inner = data.data;
198
+ if (inner?.type === "assistant" && Array.isArray(inner.content)) {
199
+ for (const block of inner.content) {
200
+ if (block.type === "text" && block.text) {
201
+ process.stdout.write(block.text);
202
+ if (!block.text.endsWith("\n")) process.stdout.write("\n");
203
+ } else if (block.type === "tool_use") {
204
+ const argsStr = JSON.stringify(block.input || {}).slice(0, 120);
205
+ console.log(`\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})`);
206
+ } else if (block.type === "tool_result") {
207
+ const resultStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
208
+ console.log(`\x1B[90m[result]\x1B[0m ${resultStr.slice(0, 200)}${resultStr.length > 200 ? "..." : ""}`);
209
+ }
210
+ }
211
+ } else if (inner?.type === "result") {
212
+ if (inner.result) console.log(`\x1B[32m[done]\x1B[0m ${inner.result}`);
213
+ }
214
+ }
215
+ } else if (role === "session") {
216
+ const data = content.content?.data;
217
+ if (data?.type === "system" && data?.subtype === "init") {
218
+ console.log(`\x1B[90m[session init]\x1B[0m`);
219
+ }
220
+ }
221
+ }
222
+ async function sessionList() {
223
+ const { server, machine } = await connectAndGetMachine();
224
+ try {
225
+ const sessions = await machine.listSessions();
226
+ if (sessions.length === 0) {
227
+ console.log("No active sessions.");
228
+ return;
229
+ }
230
+ const enriched = [];
231
+ for (const s of sessions) {
232
+ let flavor = "claude";
233
+ let name = "";
234
+ if (s.metadata) {
235
+ flavor = s.metadata.flavor || "claude";
236
+ name = s.metadata.name || "";
237
+ }
238
+ if (!name && s.active) {
239
+ try {
240
+ const svc = await server.getService(`svamp-session-${s.sessionId}`);
241
+ const { metadata } = await svc.getMetadata();
242
+ flavor = metadata?.flavor || flavor;
243
+ name = metadata?.name || "";
244
+ } catch {
245
+ }
246
+ }
247
+ enriched.push({ ...s, flavor, name });
248
+ }
249
+ const header = `${"ID".padEnd(10)} ${"AGENT".padEnd(10)} ${"STATUS".padEnd(9)} ${"NAME".padEnd(25)} ${"DIRECTORY".padEnd(35)}`;
250
+ console.log(header);
251
+ console.log("-".repeat(header.length));
252
+ for (const s of enriched) {
253
+ const id = s.sessionId.slice(0, 8);
254
+ const agent = (s.flavor || "claude").padEnd(10);
255
+ const status = s.active ? "\x1B[32mactive\x1B[0m " : "\x1B[90minactive\x1B[0m";
256
+ const name = truncate(s.name || "-", 25).padEnd(25);
257
+ const dir = truncate(s.directory || "-", 33).padEnd(35);
258
+ console.log(`${id.padEnd(10)} ${agent} ${status} ${name} ${dir}`);
259
+ }
260
+ } finally {
261
+ await server.disconnect();
262
+ }
263
+ }
264
+ async function sessionSpawn(agent, directory) {
265
+ const { server, machine } = await connectAndGetMachine();
266
+ try {
267
+ console.log(`Spawning ${agent} session in ${directory}...`);
268
+ const result = await machine.spawnSession({
269
+ directory,
270
+ agent
271
+ });
272
+ if (result.type === "success") {
273
+ console.log(`Session started: ${result.sessionId}`);
274
+ if (result.message) console.log(` ${result.message}`);
275
+ } else if (result.type === "requestToApproveDirectoryCreation") {
276
+ console.error(`Directory ${result.directory} does not exist. Create it first or use an existing directory.`);
277
+ process.exit(1);
278
+ } else {
279
+ console.error(`Failed: ${result.errorMessage || "Unknown error"}`);
280
+ process.exit(1);
281
+ }
282
+ } finally {
283
+ await server.disconnect();
284
+ }
285
+ }
286
+ async function sessionStop(sessionId) {
287
+ const { server, machine } = await connectAndGetMachine();
288
+ try {
289
+ const sessions = await machine.listSessions();
290
+ const match = resolveSessionId(sessions, sessionId);
291
+ const success = await machine.stopSession(match.sessionId);
292
+ if (success) {
293
+ console.log(`Session ${match.sessionId.slice(0, 8)} stopped.`);
294
+ } else {
295
+ console.error("Failed to stop session (not found on daemon).");
296
+ process.exit(1);
297
+ }
298
+ } finally {
299
+ await server.disconnect();
300
+ }
301
+ }
302
+ async function sessionInfo(sessionId) {
303
+ const { server, machine } = await connectAndGetMachine();
304
+ try {
305
+ const sessions = await machine.listSessions();
306
+ const match = resolveSessionId(sessions, sessionId);
307
+ const fullId = match.sessionId;
308
+ let metadata = {};
309
+ let activity = {};
310
+ try {
311
+ const svc = await server.getService(`svamp-session-${fullId}`);
312
+ const metaResult = await svc.getMetadata();
313
+ metadata = metaResult.metadata || {};
314
+ activity = await svc.getActivityState();
315
+ } catch {
316
+ }
317
+ console.log(`Session: ${fullId}`);
318
+ console.log(`Agent: ${metadata.flavor || "claude"}`);
319
+ console.log(`Name: ${metadata.name || "(unnamed)"}`);
320
+ console.log(`Directory: ${metadata.path || match.directory || "-"}`);
321
+ console.log(`Host: ${metadata.host || "-"}`);
322
+ console.log(`State: ${metadata.lifecycleState || "unknown"}`);
323
+ console.log(`Active: ${activity.active ?? "-"}`);
324
+ console.log(`Thinking: ${activity.thinking ?? "-"}`);
325
+ console.log(`Started by: ${metadata.startedBy || match.startedBy || "-"}`);
326
+ if (metadata.summary?.text) {
327
+ console.log(`Summary: ${metadata.summary.text}`);
328
+ }
329
+ if (metadata.claudeSessionId) {
330
+ console.log(`Claude ID: ${metadata.claudeSessionId}`);
331
+ }
332
+ if (metadata.sessionLink?.url) {
333
+ console.log(`Link: ${metadata.sessionLink.url}`);
334
+ }
335
+ } finally {
336
+ await server.disconnect();
337
+ }
338
+ }
339
+ async function sessionMessages(sessionId, last) {
340
+ const { server, machine } = await connectAndGetMachine();
341
+ try {
342
+ const sessions = await machine.listSessions();
343
+ const match = resolveSessionId(sessions, sessionId);
344
+ const fullId = match.sessionId;
345
+ const svc = await server.getService(`svamp-session-${fullId}`);
346
+ const { messages } = await svc.getMessages(0, 1e3);
347
+ const toShow = last ? messages.slice(-last) : messages;
348
+ if (toShow.length === 0) {
349
+ console.log("No messages yet.");
350
+ return;
351
+ }
352
+ for (const msg of toShow) {
353
+ renderMessage(msg);
354
+ }
355
+ } finally {
356
+ await server.disconnect();
357
+ }
358
+ }
359
+ async function sessionAttach(sessionId) {
360
+ const { server, machine } = await connectAndGetMachine();
361
+ const sessions = await machine.listSessions();
362
+ const match = resolveSessionId(sessions, sessionId);
363
+ const fullId = match.sessionId;
364
+ let svc;
365
+ try {
366
+ svc = await server.getService(`svamp-session-${fullId}`);
367
+ } catch (err) {
368
+ console.error(`Could not find session service: ${err.message}`);
369
+ await server.disconnect();
370
+ process.exit(1);
371
+ }
372
+ const { metadata } = await svc.getMetadata();
373
+ const flavor = metadata?.flavor || "claude";
374
+ const name = metadata?.name || fullId.slice(0, 8);
375
+ console.log(`Attached to ${flavor} session "${name}". Commands: /quit /abort /kill
376
+ `);
377
+ const seenMessageIds = /* @__PURE__ */ new Set();
378
+ let replayDone = false;
379
+ await svc.registerListener({
380
+ onUpdate: (update) => {
381
+ if (update.type === "new-message") {
382
+ const msg = update.message;
383
+ if (!msg?.id) return;
384
+ if (seenMessageIds.has(msg.id)) return;
385
+ seenMessageIds.add(msg.id);
386
+ if (!replayDone) return;
387
+ renderMessage(msg);
388
+ } else if (update.type === "activity") {
389
+ if (!replayDone) return;
390
+ if (update.thinking) {
391
+ process.stdout.write("\x1B[90m[thinking...]\x1B[0m\r");
392
+ } else if (!update.active) {
393
+ process.stdout.write("\n> ");
394
+ }
395
+ } else if (update.type === "update-session") ;
396
+ }
397
+ });
398
+ await new Promise((r) => setTimeout(r, 500));
399
+ replayDone = true;
400
+ console.log(`\x1B[90m(${seenMessageIds.size} messages in history)\x1B[0m`);
401
+ process.stdout.write("> ");
402
+ const readline = await import('readline');
403
+ const rl = readline.createInterface({
404
+ input: process.stdin,
405
+ output: process.stdout,
406
+ terminal: true
407
+ });
408
+ rl.on("line", async (line) => {
409
+ const trimmed = line.trim();
410
+ if (!trimmed) {
411
+ process.stdout.write("> ");
412
+ return;
413
+ }
414
+ if (trimmed === "/quit" || trimmed === "/detach") {
415
+ console.log("Detaching (session continues running)...");
416
+ rl.close();
417
+ await server.disconnect();
418
+ process.exit(0);
419
+ }
420
+ if (trimmed === "/abort" || trimmed === "/cancel") {
421
+ try {
422
+ await svc.abort();
423
+ console.log("Abort sent.");
424
+ } catch (err) {
425
+ console.error(`Abort failed: ${err.message}`);
426
+ }
427
+ process.stdout.write("> ");
428
+ return;
429
+ }
430
+ if (trimmed === "/kill") {
431
+ try {
432
+ await svc.killSession();
433
+ console.log("Session killed.");
434
+ } catch (err) {
435
+ console.error(`Kill failed: ${err.message}`);
436
+ }
437
+ rl.close();
438
+ await server.disconnect();
439
+ process.exit(0);
440
+ }
441
+ if (trimmed === "/info") {
442
+ try {
443
+ const { metadata: m } = await svc.getMetadata();
444
+ const act = await svc.getActivityState();
445
+ console.log(` Agent: ${m?.flavor || "claude"}, State: ${m?.lifecycleState || "?"}, Active: ${act?.active}, Thinking: ${act?.thinking}`);
446
+ } catch (err) {
447
+ console.error(`Info failed: ${err.message}`);
448
+ }
449
+ process.stdout.write("> ");
450
+ return;
451
+ }
452
+ try {
453
+ await svc.sendMessage(
454
+ JSON.stringify({
455
+ role: "user",
456
+ content: { type: "text", text: trimmed }
457
+ })
458
+ );
459
+ } catch (err) {
460
+ console.error(`Send failed: ${err.message}`);
461
+ process.stdout.write("> ");
462
+ }
463
+ });
464
+ rl.on("close", async () => {
465
+ await server.disconnect();
466
+ process.exit(0);
467
+ });
468
+ process.on("SIGINT", async () => {
469
+ console.log("\nDetaching (session continues running)...");
470
+ rl.close();
471
+ await server.disconnect();
472
+ process.exit(0);
473
+ });
474
+ }
475
+
476
+ export { sessionAttach, sessionInfo, sessionList, sessionMessages, sessionSpawn, sessionStop };