@vellumai/cli 0.1.10 → 0.1.12

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.
@@ -1,6 +1,17 @@
1
- import { loadAllAssistants } from "../lib/assistant-config";
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ import {
6
+ findAssistantByName,
7
+ loadAllAssistants,
8
+ type AssistantEntry,
9
+ } from "../lib/assistant-config";
2
10
  import { checkHealth } from "../lib/health-check";
3
11
  import { withStatusEmoji } from "../lib/status-emoji";
12
+ import { execOutput } from "../lib/step-runner";
13
+
14
+ // ── Table formatting helpers ────────────────────────────────────
4
15
 
5
16
  interface TableRow {
6
17
  name: string;
@@ -32,7 +43,207 @@ function formatRow(r: TableRow, colWidths: ColWidths): string {
32
43
  return ` ${pad(r.name, colWidths.name)} ${pad(r.status, colWidths.status)} ${r.info}`;
33
44
  }
34
45
 
35
- export async function ps(): Promise<void> {
46
+ function printTable(rows: TableRow[]): void {
47
+ const colWidths = computeColWidths(rows);
48
+ const headers: TableRow = { name: "PROCESS", status: "STATUS", info: "INFO" };
49
+ console.log(formatRow(headers, colWidths));
50
+ const sep = ` ${"-".repeat(colWidths.name)} ${"-".repeat(colWidths.status)} ${"-".repeat(colWidths.info)}`;
51
+ console.log(sep);
52
+ for (const row of rows) {
53
+ console.log(formatRow(row, colWidths));
54
+ }
55
+ }
56
+
57
+ // ── Remote process listing via SSH ──────────────────────────────
58
+
59
+ const SSH_OPTS = [
60
+ "-o", "StrictHostKeyChecking=no",
61
+ "-o", "UserKnownHostsFile=/dev/null",
62
+ "-o", "ConnectTimeout=10",
63
+ "-o", "LogLevel=ERROR",
64
+ ];
65
+
66
+ const REMOTE_PS_CMD = [
67
+ // List vellum-related processes: daemon, gateway, qdrant, and any bun children
68
+ "ps ax -o pid=,ppid=,args=",
69
+ "| grep -E 'vellum|gateway|qdrant|openclaw'",
70
+ "| grep -v grep",
71
+ ].join(" ");
72
+
73
+ interface RemoteProcess {
74
+ pid: string;
75
+ ppid: string;
76
+ command: string;
77
+ }
78
+
79
+ function classifyProcess(command: string): string {
80
+ if (/qdrant/.test(command)) return "qdrant";
81
+ if (/gateway/.test(command)) return "gateway";
82
+ if (/openclaw/.test(command)) return "openclaw-adapter";
83
+ if (/daemon\s+(start|restart)/.test(command)) return "daemon";
84
+ if (/vellum/.test(command)) return "vellum";
85
+ return "unknown";
86
+ }
87
+
88
+ function parseRemotePs(output: string): RemoteProcess[] {
89
+ return output
90
+ .trim()
91
+ .split("\n")
92
+ .filter((line) => line.trim().length > 0)
93
+ .map((line) => {
94
+ const trimmed = line.trim();
95
+ const parts = trimmed.split(/\s+/);
96
+ const pid = parts[0];
97
+ const ppid = parts[1];
98
+ const command = parts.slice(2).join(" ");
99
+ return { pid, ppid, command };
100
+ });
101
+ }
102
+
103
+ function extractHostFromUrl(url: string): string {
104
+ try {
105
+ const parsed = new URL(url);
106
+ return parsed.hostname;
107
+ } catch {
108
+ return url.replace(/^https?:\/\//, "").split(":")[0];
109
+ }
110
+ }
111
+
112
+ function resolveCloud(entry: AssistantEntry): string {
113
+ if (entry.cloud) return entry.cloud;
114
+ if (entry.project) return "gcp";
115
+ if (entry.sshUser) return "custom";
116
+ return "local";
117
+ }
118
+
119
+ async function getRemoteProcessesGcp(
120
+ entry: AssistantEntry,
121
+ ): Promise<string> {
122
+ return execOutput("gcloud", [
123
+ "compute",
124
+ "ssh",
125
+ `${entry.sshUser ?? entry.assistantId}@${entry.assistantId}`,
126
+ `--zone=${entry.zone}`,
127
+ `--project=${entry.project}`,
128
+ `--command=${REMOTE_PS_CMD}`,
129
+ "--ssh-flag=-o StrictHostKeyChecking=no",
130
+ "--ssh-flag=-o UserKnownHostsFile=/dev/null",
131
+ "--ssh-flag=-o ConnectTimeout=10",
132
+ "--ssh-flag=-o LogLevel=ERROR",
133
+ ]);
134
+ }
135
+
136
+ async function getRemoteProcessesCustom(
137
+ entry: AssistantEntry,
138
+ ): Promise<string> {
139
+ const host = extractHostFromUrl(entry.runtimeUrl);
140
+ const sshUser = entry.sshUser ?? "root";
141
+ return execOutput("ssh", [
142
+ ...SSH_OPTS,
143
+ `${sshUser}@${host}`,
144
+ REMOTE_PS_CMD,
145
+ ]);
146
+ }
147
+
148
+ async function getLocalProcesses(): Promise<TableRow[]> {
149
+ const rows: TableRow[] = [];
150
+ const vellumDir = join(homedir(), ".vellum");
151
+
152
+ // Check daemon PID
153
+ const pidFile = join(vellumDir, "vellum.pid");
154
+ if (existsSync(pidFile)) {
155
+ const pid = readFileSync(pidFile, "utf-8").trim();
156
+ let status = "running";
157
+ try {
158
+ process.kill(parseInt(pid, 10), 0);
159
+ } catch {
160
+ status = "not running";
161
+ }
162
+ rows.push({ name: "daemon", status: withStatusEmoji(status), info: `PID ${pid}` });
163
+ } else {
164
+ rows.push({ name: "daemon", status: withStatusEmoji("not running"), info: "no PID file" });
165
+ }
166
+
167
+ // Check qdrant PID
168
+ const qdrantPidFile = join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid");
169
+ if (existsSync(qdrantPidFile)) {
170
+ const pid = readFileSync(qdrantPidFile, "utf-8").trim();
171
+ let status = "running";
172
+ try {
173
+ process.kill(parseInt(pid, 10), 0);
174
+ } catch {
175
+ status = "not running";
176
+ }
177
+ rows.push({ name: "qdrant", status: withStatusEmoji(status), info: `PID ${pid} | port 6333` });
178
+ } else {
179
+ rows.push({ name: "qdrant", status: withStatusEmoji("not running"), info: "no PID file" });
180
+ }
181
+
182
+ // Check gateway via ps
183
+ try {
184
+ const output = await execOutput("ps", ["ax", "-o", "pid=,command="]);
185
+ const gatewayLines = output
186
+ .split("\n")
187
+ .filter((l) => /gateway\/src\/index\.ts/.test(l) && !l.includes("grep"));
188
+ if (gatewayLines.length > 0) {
189
+ const trimmed = gatewayLines[0].trim();
190
+ const pid = trimmed.split(/\s+/)[0];
191
+ rows.push({ name: "gateway", status: withStatusEmoji("running"), info: `PID ${pid} | port 7830` });
192
+ } else {
193
+ rows.push({ name: "gateway", status: withStatusEmoji("not running"), info: "" });
194
+ }
195
+ } catch {
196
+ rows.push({ name: "gateway", status: withStatusEmoji("unknown"), info: "" });
197
+ }
198
+
199
+ return rows;
200
+ }
201
+
202
+ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
203
+ const cloud = resolveCloud(entry);
204
+
205
+ console.log(`Processes for ${entry.assistantId} (${cloud}):\n`);
206
+
207
+ if (cloud === "local") {
208
+ const rows = await getLocalProcesses();
209
+ printTable(rows);
210
+ return;
211
+ }
212
+
213
+ let output: string;
214
+ try {
215
+ if (cloud === "gcp") {
216
+ output = await getRemoteProcessesGcp(entry);
217
+ } else if (cloud === "custom") {
218
+ output = await getRemoteProcessesCustom(entry);
219
+ } else {
220
+ console.error(`Unsupported cloud type '${cloud}' for process listing.`);
221
+ process.exit(1);
222
+ }
223
+ } catch (error) {
224
+ console.error(`Failed to list processes: ${error instanceof Error ? error.message : error}`);
225
+ process.exit(1);
226
+ }
227
+
228
+ const procs = parseRemotePs(output);
229
+
230
+ if (procs.length === 0) {
231
+ console.log("No vellum processes found on the remote instance.");
232
+ return;
233
+ }
234
+
235
+ const rows: TableRow[] = procs.map((p) => ({
236
+ name: classifyProcess(p.command),
237
+ status: withStatusEmoji("running"),
238
+ info: `PID ${p.pid} | ${p.command.slice(0, 80)}`,
239
+ }));
240
+
241
+ printTable(rows);
242
+ }
243
+
244
+ // ── List all assistants (no arg) ────────────────────────────────
245
+
246
+ async function listAllAssistants(): Promise<void> {
36
247
  const assistants = loadAllAssistants();
37
248
 
38
249
  if (assistants.length === 0) {
@@ -90,3 +301,22 @@ export async function ps(): Promise<void> {
90
301
  }),
91
302
  );
92
303
  }
304
+
305
+ // ── Entry point ─────────────────────────────────────────────────
306
+
307
+ export async function ps(): Promise<void> {
308
+ const assistantId = process.argv[3];
309
+
310
+ if (!assistantId) {
311
+ await listAllAssistants();
312
+ return;
313
+ }
314
+
315
+ const entry = findAssistantByName(assistantId);
316
+ if (!entry) {
317
+ console.error(`No assistant found with name '${assistantId}'.`);
318
+ process.exit(1);
319
+ }
320
+
321
+ await showAssistantProcesses(entry);
322
+ }
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, readFileSync, rmSync, unlinkSync } from "fs";
2
+ import { rmSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
5
 
@@ -7,6 +7,7 @@ import { findAssistantByName, removeAssistantEntry } from "../lib/assistant-conf
7
7
  import type { AssistantEntry } from "../lib/assistant-config";
8
8
  import { retireInstance as retireAwsInstance } from "../lib/aws";
9
9
  import { retireInstance as retireGcpInstance } from "../lib/gcp";
10
+ import { stopProcessByPidFile } from "../lib/process";
10
11
  import { exec } from "../lib/step-runner";
11
12
 
12
13
  function resolveCloud(entry: AssistantEntry): string {
@@ -37,39 +38,17 @@ async function retireLocal(): Promise<void> {
37
38
  const vellumDir = join(homedir(), ".vellum");
38
39
  const isDesktopApp = !!process.env.VELLUM_DESKTOP_APP;
39
40
 
40
- // Stop daemon via PID file (works for both desktop app and standalone)
41
- const pidFile = join(vellumDir, "vellum.pid");
41
+ // Stop daemon via PID file
42
+ const daemonPidFile = join(vellumDir, "vellum.pid");
42
43
  const socketFile = join(vellumDir, "vellum.sock");
44
+ await stopProcessByPidFile(daemonPidFile, "daemon", [socketFile]);
43
45
 
44
- if (existsSync(pidFile)) {
45
- try {
46
- const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
47
- if (!isNaN(pid)) {
48
- try {
49
- process.kill(pid, 0); // Check if alive
50
- process.kill(pid, "SIGTERM");
51
- const deadline = Date.now() + 2000;
52
- while (Date.now() < deadline) {
53
- try {
54
- process.kill(pid, 0);
55
- await new Promise((r) => setTimeout(r, 100));
56
- } catch {
57
- break;
58
- }
59
- }
60
- try {
61
- process.kill(pid, 0);
62
- process.kill(pid, "SIGKILL");
63
- } catch {}
64
- } catch {}
65
- }
66
- } catch {}
67
- try { unlinkSync(pidFile); } catch {}
68
- try { unlinkSync(socketFile); } catch {}
69
- }
46
+ // Stop gateway via PID file
47
+ const gatewayPidFile = join(vellumDir, "gateway.pid");
48
+ await stopProcessByPidFile(gatewayPidFile, "gateway");
70
49
 
71
50
  if (!isDesktopApp) {
72
- // Non-desktop: also stop daemon via bunx (fallback) and kill gateway
51
+ // Non-desktop: also stop daemon via bunx (fallback)
73
52
  try {
74
53
  const child = spawn("bunx", ["vellum", "daemon", "stop"], {
75
54
  stdio: "inherit",
@@ -81,17 +60,6 @@ async function retireLocal(): Promise<void> {
81
60
  });
82
61
  } catch {}
83
62
 
84
- try {
85
- const killGateway = spawn("pkill", ["-f", "gateway/src/index.ts"], {
86
- stdio: "ignore",
87
- });
88
-
89
- await new Promise<void>((resolve) => {
90
- killGateway.on("close", () => resolve());
91
- killGateway.on("error", () => resolve());
92
- });
93
- } catch {}
94
-
95
63
  // Only delete ~/.vellum in non-desktop mode
96
64
  rmSync(vellumDir, { recursive: true, force: true });
97
65
  }
@@ -1,63 +1,35 @@
1
- import { existsSync, readFileSync, unlinkSync } from "fs";
2
1
  import { homedir } from "os";
3
2
  import { join } from "path";
4
3
 
5
- export async function sleep(): Promise<void> {
6
- const vellumDir = join(homedir(), ".vellum");
7
- const pidFile = join(vellumDir, "vellum.pid");
8
- const socketFile = join(vellumDir, "vellum.sock");
9
-
10
- if (!existsSync(pidFile)) {
11
- console.log("No daemon PID file found — nothing to stop.");
12
- process.exit(0);
13
- }
14
-
15
- const pidStr = readFileSync(pidFile, "utf-8").trim();
16
- const pid = parseInt(pidStr, 10);
17
-
18
- if (isNaN(pid)) {
19
- console.log("Invalid PID file contents — cleaning up.");
20
- try { unlinkSync(pidFile); } catch {}
21
- try { unlinkSync(socketFile); } catch {}
22
- process.exit(0);
23
- }
4
+ import { loadAllAssistants } from "../lib/assistant-config";
5
+ import { stopProcessByPidFile } from "../lib/process";
24
6
 
25
- // Check if process is alive
26
- try {
27
- process.kill(pid, 0);
28
- } catch {
29
- console.log(`Daemon process ${pid} is not running cleaning up stale files.`);
30
- try { unlinkSync(pidFile); } catch {}
31
- try { unlinkSync(socketFile); } catch {}
32
- process.exit(0);
7
+ export async function sleep(): Promise<void> {
8
+ const assistants = loadAllAssistants();
9
+ const hasLocal = assistants.some((a) => a.cloud === "local");
10
+ if (!hasLocal) {
11
+ console.error("Error: No local assistant found in lock file. Run 'vellum hatch local' first.");
12
+ process.exit(1);
33
13
  }
34
14
 
35
- console.log(`Stopping daemon (pid ${pid})...`);
36
- process.kill(pid, "SIGTERM");
37
-
38
- // Wait up to 2s for graceful exit
39
- const deadline = Date.now() + 2000;
40
- while (Date.now() < deadline) {
41
- try {
42
- process.kill(pid, 0);
43
- await new Promise((r) => setTimeout(r, 100));
44
- } catch {
45
- break; // Process exited
46
- }
15
+ const vellumDir = join(homedir(), ".vellum");
16
+ const daemonPidFile = join(vellumDir, "vellum.pid");
17
+ const socketFile = join(vellumDir, "vellum.sock");
18
+ const gatewayPidFile = join(vellumDir, "gateway.pid");
19
+
20
+ // Stop daemon
21
+ const daemonStopped = await stopProcessByPidFile(daemonPidFile, "daemon", [socketFile]);
22
+ if (!daemonStopped) {
23
+ console.log("Daemon is not running.");
24
+ } else {
25
+ console.log("Daemon stopped.");
47
26
  }
48
27
 
49
- // Force kill if still alive
50
- try {
51
- process.kill(pid, 0);
52
- console.log("Daemon did not exit after SIGTERM, sending SIGKILL...");
53
- process.kill(pid, "SIGKILL");
54
- } catch {
55
- // Already dead
28
+ // Stop gateway
29
+ const gatewayStopped = await stopProcessByPidFile(gatewayPidFile, "gateway");
30
+ if (!gatewayStopped) {
31
+ console.log("Gateway is not running.");
32
+ } else {
33
+ console.log("Gateway stopped.");
56
34
  }
57
-
58
- // Clean up PID and socket files
59
- try { unlinkSync(pidFile); } catch {}
60
- try { unlinkSync(socketFile); } catch {}
61
-
62
- console.log("Daemon stopped.");
63
35
  }
@@ -2,9 +2,18 @@ import { existsSync, readFileSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
4
 
5
- import { startLocalDaemon, startGateway } from "./hatch";
5
+ import { loadAllAssistants } from "../lib/assistant-config";
6
+ import { isProcessAlive } from "../lib/process";
7
+ import { startLocalDaemon, startGateway } from "../lib/local";
6
8
 
7
9
  export async function wake(): Promise<void> {
10
+ const assistants = loadAllAssistants();
11
+ const hasLocal = assistants.some((a) => a.cloud === "local");
12
+ if (!hasLocal) {
13
+ console.error("Error: No local assistant found in lock file. Run 'vellum hatch local' first.");
14
+ process.exit(1);
15
+ }
16
+
8
17
  const vellumDir = join(homedir(), ".vellum");
9
18
  const pidFile = join(vellumDir, "vellum.pid");
10
19
 
@@ -30,7 +39,13 @@ export async function wake(): Promise<void> {
30
39
 
31
40
  // Start gateway (non-desktop only)
32
41
  if (!process.env.VELLUM_DESKTOP_APP) {
33
- await startGateway();
42
+ const gatewayPidFile = join(vellumDir, "gateway.pid");
43
+ const { alive, pid } = isProcessAlive(gatewayPidFile);
44
+ if (alive) {
45
+ console.log(`Gateway already running (pid ${pid}).`);
46
+ } else {
47
+ await startGateway();
48
+ }
34
49
  }
35
50
 
36
51
  console.log("✅ Wake complete.");