svamp-cli 0.1.23 → 0.1.28

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,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Svamp Agent CLI entry point
4
+ // Loads .env from ~/.svamp/.env if present, then runs the agent CLI
5
+
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { homedir } from 'os';
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');
16
+ for (const line of lines) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed || trimmed.startsWith('#')) continue;
19
+ const eqIdx = trimmed.indexOf('=');
20
+ if (eqIdx === -1) continue;
21
+ const key = trimmed.slice(0, eqIdx).trim();
22
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
23
+ if (!process.env[key]) {
24
+ process.env[key] = value;
25
+ }
26
+ }
27
+ }
28
+
29
+ // Import and run the agent CLI
30
+ import('../dist/agent-cli.mjs').catch((err) => {
31
+ console.error('Failed to load compiled agent CLI. Try running: yarn workspace svamp-cli build');
32
+ console.error(err.message);
33
+ process.exit(1);
34
+ });
@@ -0,0 +1,453 @@
1
+ import { Command } from 'commander';
2
+ import { connectAndGetMachine, resolveSessionId } from './commands-BOeSil-P.mjs';
3
+ import 'node:fs';
4
+ import 'node:path';
5
+ import 'node:os';
6
+ import './hyphaClient-DLkclazm.mjs';
7
+
8
+ function formatIsoTime(ts) {
9
+ if (!ts) return "-";
10
+ const date = new Date(ts);
11
+ if (Number.isNaN(date.getTime())) return "-";
12
+ return date.toISOString();
13
+ }
14
+ function toMarkdownInline(value) {
15
+ const escaped = value.replace(/`/g, "\\`");
16
+ return `\`${escaped}\``;
17
+ }
18
+ function normalizeCodeBlockText(value) {
19
+ const text = value.trim().length > 0 ? value : "(empty)";
20
+ return text.replace(/```/g, "``\\`");
21
+ }
22
+ function formatSessionTable(sessions) {
23
+ if (sessions.length === 0) {
24
+ return "## Sessions\n\n- Total: 0\n- Items: none";
25
+ }
26
+ const sections = sessions.map((s, index) => {
27
+ const name = s.name || "-";
28
+ const path = s.path || s.directory || "-";
29
+ const status = s.active ? "active" : "inactive";
30
+ return [
31
+ `### Session ${index + 1}`,
32
+ `- ID: ${toMarkdownInline(s.sessionId)}`,
33
+ `- Agent: ${s.flavor || "claude"}`,
34
+ `- Name: ${name}`,
35
+ `- Path: ${path}`,
36
+ `- Status: ${status}`
37
+ ].join("\n");
38
+ });
39
+ return `## Sessions
40
+
41
+ - Total: ${sessions.length}
42
+
43
+ ${sections.join("\n\n")}`;
44
+ }
45
+ function formatSessionStatus(data) {
46
+ const lines = [
47
+ "## Session Status",
48
+ "",
49
+ `- Session ID: ${toMarkdownInline(data.sessionId)}`,
50
+ `- Agent: ${data.flavor}`
51
+ ];
52
+ if (data.name) lines.push(`- Name: ${data.name}`);
53
+ if (data.summary) lines.push(`- Summary: ${data.summary}`);
54
+ if (data.path) lines.push(`- Path: ${data.path}`);
55
+ if (data.host) lines.push(`- Host: ${data.host}`);
56
+ if (data.lifecycleState) lines.push(`- Lifecycle: ${data.lifecycleState}`);
57
+ lines.push(`- Active: ${data.active ? "yes" : "no"}`);
58
+ lines.push(`- Thinking: ${data.thinking ? "yes" : "no"}`);
59
+ lines.push(`- Agent Status: ${data.active ? "busy" : "idle"}`);
60
+ if (data.startedBy) lines.push(`- Started By: ${data.startedBy}`);
61
+ if (data.claudeSessionId) lines.push(`- Claude Session: ${data.claudeSessionId}`);
62
+ if (data.sessionLink) lines.push(`- Link: ${data.sessionLink}`);
63
+ return lines.join("\n");
64
+ }
65
+ function formatMessageHistory(messages) {
66
+ if (messages.length === 0) {
67
+ return "## Message History\n\n- Count: 0\n- Items: none";
68
+ }
69
+ const sections = messages.map((msg, index) => {
70
+ return [
71
+ `### Message ${index + 1}`,
72
+ `- ID: ${toMarkdownInline(msg.id)}`,
73
+ `- Time: ${formatIsoTime(msg.createdAt)}`,
74
+ `- Role: ${msg.role}`,
75
+ "- Text:",
76
+ "```text",
77
+ normalizeCodeBlockText(msg.text),
78
+ "```"
79
+ ].join("\n");
80
+ });
81
+ return `## Message History
82
+
83
+ - Count: ${messages.length}
84
+
85
+ ${sections.join("\n\n")}`;
86
+ }
87
+ function formatJson(data) {
88
+ return JSON.stringify(data, null, 2);
89
+ }
90
+
91
+ async function resolveSessionByPath(machine, server, path) {
92
+ const sessions = await machine.listSessions();
93
+ const active = sessions.filter((s) => s.active);
94
+ let matches = active.filter((s) => s.directory === path);
95
+ if (matches.length === 0) {
96
+ for (const s of active) {
97
+ try {
98
+ const svc = await server.getService(`svamp-session-${s.sessionId}`);
99
+ const { metadata } = await svc.getMetadata();
100
+ if (metadata?.path === path) {
101
+ matches.push(s);
102
+ }
103
+ } catch {
104
+ }
105
+ }
106
+ }
107
+ if (matches.length === 0) {
108
+ throw new Error(`No active session found for path "${path}"`);
109
+ }
110
+ return matches[0];
111
+ }
112
+ function extractMessageText(msg) {
113
+ const content = msg.content;
114
+ if (!content) return null;
115
+ const role = content.role || "unknown";
116
+ let text = "";
117
+ if (role === "user") {
118
+ const data = content.content;
119
+ if (typeof data === "string") {
120
+ try {
121
+ const parsed = JSON.parse(data);
122
+ text = parsed?.text || parsed?.content?.text || data;
123
+ } catch {
124
+ text = data;
125
+ }
126
+ } else if (data?.text) {
127
+ text = data.text;
128
+ } else if (data?.type === "text") {
129
+ text = data.text || "";
130
+ } else {
131
+ text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
132
+ }
133
+ } else if (role === "agent" || role === "assistant") {
134
+ const data = content.content?.data || content.content;
135
+ if (!data) return null;
136
+ if (data.type === "assistant" && Array.isArray(data.content)) {
137
+ const parts = [];
138
+ for (const block of data.content) {
139
+ if (block.type === "text" && block.text) {
140
+ parts.push(block.text);
141
+ } else if (block.type === "tool_use") {
142
+ parts.push(`[tool: ${block.name}]`);
143
+ }
144
+ }
145
+ text = parts.join("\n");
146
+ } else if (data.type === "result") {
147
+ text = data.result || "";
148
+ } else if (data.type === "output") {
149
+ const inner = data.data;
150
+ if (inner?.type === "assistant" && Array.isArray(inner.content)) {
151
+ const parts = [];
152
+ for (const block of inner.content) {
153
+ if (block.type === "text" && block.text) {
154
+ parts.push(block.text);
155
+ } else if (block.type === "tool_use") {
156
+ parts.push(`[tool: ${block.name}]`);
157
+ }
158
+ }
159
+ text = parts.join("\n");
160
+ } else if (inner?.type === "result") {
161
+ text = inner.result || "";
162
+ }
163
+ }
164
+ } else if (role === "session") {
165
+ text = "[session event]";
166
+ }
167
+ return {
168
+ id: msg.id || "",
169
+ seq: msg.seq || 0,
170
+ role,
171
+ text,
172
+ createdAt: msg.createdAt || 0
173
+ };
174
+ }
175
+ async function waitForIdle(server, sessionId, timeoutMs) {
176
+ const svc = await server.getService(`svamp-session-${sessionId}`);
177
+ const pollInterval = 2e3;
178
+ const deadline = Date.now() + timeoutMs;
179
+ while (Date.now() < deadline) {
180
+ const activity = await svc.getActivityState();
181
+ if (activity && !activity.active) {
182
+ return;
183
+ }
184
+ await new Promise((r) => setTimeout(r, pollInterval));
185
+ }
186
+ throw new Error("Timeout waiting for agent to become idle");
187
+ }
188
+ async function waitForBusyThenIdle(server, sessionId, timeoutMs = 3e5, busyTimeoutMs = 1e4) {
189
+ const svc = await server.getService(`svamp-session-${sessionId}`);
190
+ const pollInterval = 2e3;
191
+ const deadline = Date.now() + timeoutMs;
192
+ const busyDeadline = Date.now() + busyTimeoutMs;
193
+ let sawBusy = false;
194
+ while (Date.now() < deadline) {
195
+ const activity = await svc.getActivityState();
196
+ const isActive = activity?.active === true;
197
+ if (isActive) {
198
+ sawBusy = true;
199
+ }
200
+ if (sawBusy && !isActive) {
201
+ return;
202
+ }
203
+ if (!sawBusy && Date.now() > busyDeadline) {
204
+ return;
205
+ }
206
+ await new Promise((r) => setTimeout(r, pollInterval));
207
+ }
208
+ throw new Error("Timeout waiting for agent to become idle");
209
+ }
210
+ const program = new Command();
211
+ program.name("svamp-agent").description("CLI client for controlling Svamp sessions remotely").version("0.1.0");
212
+ program.command("list").description("List all sessions").option("--active", "Show only active sessions").option("--json", "Output as JSON").action(async (opts) => {
213
+ const { server, machine } = await connectAndGetMachine();
214
+ try {
215
+ const sessions = await machine.listSessions();
216
+ const filtered = opts.active ? sessions.filter((s) => s.active) : sessions;
217
+ const enriched = [];
218
+ for (const s of filtered) {
219
+ let flavor = "claude";
220
+ let name = "";
221
+ let path = s.directory || "";
222
+ let host = "";
223
+ if (s.metadata) {
224
+ flavor = s.metadata.flavor || "claude";
225
+ name = s.metadata.name || "";
226
+ }
227
+ if (s.active) {
228
+ try {
229
+ const svc = await server.getService(`svamp-session-${s.sessionId}`);
230
+ const { metadata } = await svc.getMetadata();
231
+ flavor = metadata?.flavor || flavor;
232
+ name = metadata?.name || name;
233
+ path = metadata?.path || path;
234
+ host = metadata?.host || "";
235
+ } catch {
236
+ }
237
+ }
238
+ enriched.push({ ...s, flavor, name, path, host });
239
+ }
240
+ if (opts.json) {
241
+ console.log(formatJson(enriched.map((s) => ({
242
+ sessionId: s.sessionId,
243
+ agent: s.flavor,
244
+ name: s.name,
245
+ path: s.path,
246
+ host: s.host,
247
+ active: s.active,
248
+ directory: s.directory
249
+ }))));
250
+ } else {
251
+ console.log(formatSessionTable(enriched));
252
+ }
253
+ } finally {
254
+ await server.disconnect();
255
+ }
256
+ });
257
+ program.command("status").description("Get live session state").argument("<session-id>", "Session ID or prefix").option("--json", "Output as JSON").action(async (sessionId, opts) => {
258
+ const { server, machine } = await connectAndGetMachine();
259
+ try {
260
+ const sessions = await machine.listSessions();
261
+ const match = resolveSessionId(sessions, sessionId);
262
+ const fullId = match.sessionId;
263
+ let metadata = {};
264
+ let activity = {};
265
+ try {
266
+ const svc = await server.getService(`svamp-session-${fullId}`);
267
+ const metaResult = await svc.getMetadata();
268
+ metadata = metaResult.metadata || {};
269
+ activity = await svc.getActivityState();
270
+ } catch {
271
+ }
272
+ const statusData = {
273
+ sessionId: fullId,
274
+ flavor: metadata.flavor || "claude",
275
+ name: metadata.name || "",
276
+ path: metadata.path || match.directory || "",
277
+ host: metadata.host || "",
278
+ lifecycleState: metadata.lifecycleState || "unknown",
279
+ active: activity.active ?? false,
280
+ thinking: activity.thinking ?? false,
281
+ startedBy: metadata.startedBy || match.startedBy || "",
282
+ summary: metadata.summary?.text || void 0,
283
+ claudeSessionId: metadata.claudeSessionId || void 0,
284
+ sessionLink: metadata.sessionLink?.url || void 0
285
+ };
286
+ if (opts.json) {
287
+ console.log(formatJson(statusData));
288
+ } else {
289
+ console.log(formatSessionStatus(statusData));
290
+ }
291
+ } finally {
292
+ await server.disconnect();
293
+ }
294
+ });
295
+ program.command("create").description("Create a new session").option("--agent <agent>", "Agent type (claude, gemini, opencode)", "claude").option("--directory <path>", "Working directory path").option("--json", "Output as JSON").action(async (opts) => {
296
+ const { server, machine } = await connectAndGetMachine();
297
+ try {
298
+ const directory = opts.directory ?? process.cwd();
299
+ const result = await machine.spawnSession({
300
+ directory,
301
+ agent: opts.agent
302
+ });
303
+ if (result.type === "success") {
304
+ if (opts.json) {
305
+ console.log(formatJson({
306
+ sessionId: result.sessionId,
307
+ agent: opts.agent,
308
+ directory
309
+ }));
310
+ } else {
311
+ console.log([
312
+ "## Session Created",
313
+ "",
314
+ `- Session ID: \`${result.sessionId}\``,
315
+ `- Agent: ${opts.agent}`,
316
+ `- Directory: ${directory}`,
317
+ ...result.message ? [`- Note: ${result.message}`] : []
318
+ ].join("\n"));
319
+ }
320
+ } else if (result.type === "requestToApproveDirectoryCreation") {
321
+ throw new Error(`Directory ${result.directory} does not exist. Create it first or use an existing directory.`);
322
+ } else {
323
+ throw new Error(result.errorMessage || "Unknown error creating session");
324
+ }
325
+ } finally {
326
+ await server.disconnect();
327
+ }
328
+ });
329
+ program.command("send").description("Send a message to a session").argument("<session-id>", "Session ID or prefix (ignored when --by-path is used)").argument("<message>", "Message text").option("--wait", "Wait for agent to become idle").option("--by-path <path>", "Resolve session by working directory path (active sessions only)").option("--json", "Output as JSON").action(async (sessionId, message, opts) => {
330
+ const { server, machine } = await connectAndGetMachine();
331
+ try {
332
+ let match;
333
+ if (opts.byPath) {
334
+ match = await resolveSessionByPath(machine, server, opts.byPath);
335
+ } else {
336
+ const sessions = await machine.listSessions();
337
+ match = resolveSessionId(sessions, sessionId);
338
+ }
339
+ const fullId = match.sessionId;
340
+ const svc = await server.getService(`svamp-session-${fullId}`);
341
+ const result = await svc.sendMessage(
342
+ JSON.stringify({
343
+ role: "user",
344
+ content: { type: "text", text: message },
345
+ meta: { sentFrom: "svamp-agent" }
346
+ })
347
+ );
348
+ if (opts.wait) {
349
+ await waitForBusyThenIdle(server, fullId);
350
+ }
351
+ if (opts.json) {
352
+ console.log(formatJson({
353
+ sessionId: fullId,
354
+ message,
355
+ sent: true,
356
+ seq: result.seq,
357
+ waited: !!opts.wait
358
+ }));
359
+ } else {
360
+ console.log([
361
+ "## Message Sent",
362
+ "",
363
+ `- Session ID: \`${fullId}\``,
364
+ `- Sequence: ${result.seq}`,
365
+ ...opts.wait ? [`- Waited For Idle: yes`] : []
366
+ ].join("\n"));
367
+ }
368
+ } finally {
369
+ await server.disconnect();
370
+ }
371
+ });
372
+ program.command("history").description("Read message history").argument("<session-id>", "Session ID or prefix").option("--limit <n>", "Limit number of messages", (v) => {
373
+ const n = parseInt(v, 10);
374
+ if (isNaN(n) || n <= 0) throw new Error("--limit must be a positive integer");
375
+ return n;
376
+ }).option("--after <seq>", "Only show messages after this sequence number", (v) => {
377
+ const n = parseInt(v, 10);
378
+ if (isNaN(n) || n < 0) throw new Error("--after must be a non-negative integer");
379
+ return n;
380
+ }).option("--json", "Output as JSON").action(async (sessionId, opts) => {
381
+ const { server, machine } = await connectAndGetMachine();
382
+ try {
383
+ const sessions = await machine.listSessions();
384
+ const match = resolveSessionId(sessions, sessionId);
385
+ const fullId = match.sessionId;
386
+ const svc = await server.getService(`svamp-session-${fullId}`);
387
+ const afterSeq = opts.after ?? 0;
388
+ const apiLimit = opts.limit ?? 500;
389
+ const { messages } = await svc.getMessages(afterSeq, apiLimit);
390
+ let formatted = [];
391
+ for (const msg of messages) {
392
+ const extracted = extractMessageText(msg);
393
+ if (extracted) {
394
+ formatted.push(extracted);
395
+ }
396
+ }
397
+ formatted.sort((a, b) => a.createdAt - b.createdAt);
398
+ if (opts.json) {
399
+ console.log(formatJson(formatted));
400
+ } else {
401
+ console.log(formatMessageHistory(formatted));
402
+ }
403
+ } finally {
404
+ await server.disconnect();
405
+ }
406
+ });
407
+ program.command("stop").description("Stop a session").argument("<session-id>", "Session ID or prefix").action(async (sessionId) => {
408
+ const { server, machine } = await connectAndGetMachine();
409
+ try {
410
+ const sessions = await machine.listSessions();
411
+ const match = resolveSessionId(sessions, sessionId);
412
+ const success = await machine.stopSession(match.sessionId);
413
+ if (success) {
414
+ console.log([
415
+ "## Session Stopped",
416
+ "",
417
+ `- Session ID: \`${match.sessionId}\``
418
+ ].join("\n"));
419
+ } else {
420
+ throw new Error("Failed to stop session (not found on daemon)");
421
+ }
422
+ } finally {
423
+ await server.disconnect();
424
+ }
425
+ });
426
+ program.command("wait").description("Wait for agent to become idle").argument("<session-id>", "Session ID or prefix").option("--timeout <seconds>", "Timeout in seconds", (v) => {
427
+ const n = parseInt(v, 10);
428
+ if (isNaN(n) || n <= 0) throw new Error("--timeout must be a positive integer");
429
+ return n;
430
+ }, 300).action(async (sessionId, opts) => {
431
+ const { server, machine } = await connectAndGetMachine();
432
+ try {
433
+ const sessions = await machine.listSessions();
434
+ const match = resolveSessionId(sessions, sessionId);
435
+ const fullId = match.sessionId;
436
+ await waitForIdle(server, fullId, opts.timeout * 1e3);
437
+ console.log([
438
+ "## Session Idle",
439
+ "",
440
+ `- Session ID: \`${fullId}\``
441
+ ].join("\n"));
442
+ } catch (err) {
443
+ const msg = err instanceof Error ? err.message : String(err);
444
+ console.error(msg);
445
+ process.exitCode = 1;
446
+ } finally {
447
+ await server.disconnect();
448
+ }
449
+ });
450
+ program.parseAsync(process.argv).catch((err) => {
451
+ console.error(err instanceof Error ? err.message : String(err));
452
+ process.exitCode = 1;
453
+ });