@vellumai/cli 0.3.11 → 0.3.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,5 +1,5 @@
1
1
  import { randomBytes } from "crypto";
2
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
3
3
  import { homedir, tmpdir, userInfo } from "os";
4
4
  import { join } from "path";
5
5
 
@@ -29,13 +29,14 @@ export type { PollResult, WatchHatchingResult } from "../lib/gcp";
29
29
 
30
30
  const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
31
31
 
32
- async function resolveInstallScriptPath(): Promise<string | null> {
33
- const sourcePath = join(import.meta.dir, "..", "adapters", "install.sh");
34
- if (existsSync(sourcePath)) {
35
- return sourcePath;
36
- }
37
- console.warn("⚠️ Install script not found at", sourcePath, "(expected in compiled binary)");
38
- return null;
32
+ // Embedded install script bun --compile doesn't bundle non-JS assets,
33
+ // so we inline it to ensure it's available in the compiled binary.
34
+ import INSTALL_SCRIPT_CONTENT from "../adapters/install.sh" with { type: "text" };
35
+
36
+ function resolveInstallScriptPath(): string {
37
+ const tmpPath = join(tmpdir(), `vellum-install-${process.pid}.sh`);
38
+ writeFileSync(tmpPath, INSTALL_SCRIPT_CONTENT, { mode: 0o755 });
39
+ return tmpPath;
39
40
  }
40
41
  const HATCH_TIMEOUT_MS: Record<Species, number> = {
41
42
  vellum: 2 * 60 * 1000,
@@ -51,8 +52,8 @@ function desktopLog(msg: string): void {
51
52
  process.stdout.write(msg + "\n");
52
53
  }
53
54
 
54
- function buildTimestampRedirect(): string {
55
- return `exec > >(while IFS= read -r line; do printf '[%s] %s\\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$line"; done > /var/log/startup-script.log) 2>&1`;
55
+ function buildTimestampRedirect(logPath: string): string {
56
+ return `exec > >(while IFS= read -r line; do printf '[%s] %s\\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$line"; done > ${logPath}) 2>&1`;
56
57
  }
57
58
 
58
59
  function buildUserSetup(sshUser: string): string {
@@ -82,7 +83,9 @@ export async function buildStartupScript(
82
83
  cloud: RemoteHost,
83
84
  ): Promise<string> {
84
85
  const platformUrl = process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
85
- const timestampRedirect = buildTimestampRedirect();
86
+ const logPath = cloud === "custom" ? "/tmp/vellum-startup.log" : "/var/log/startup-script.log";
87
+ const errorPath = cloud === "custom" ? "/tmp/vellum-startup-error" : "/var/log/startup-error";
88
+ const timestampRedirect = buildTimestampRedirect(logPath);
86
89
  const userSetup = buildUserSetup(sshUser);
87
90
  const ownershipFixup = buildOwnershipFixup();
88
91
 
@@ -102,7 +105,7 @@ set -e
102
105
 
103
106
  ${timestampRedirect}
104
107
 
105
- trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > /var/log/startup-error; echo "Last 20 log lines:" >> /var/log/startup-error; tail -20 /var/log/startup-script.log >> /var/log/startup-error 2>/dev/null || true; fi' EXIT
108
+ trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > ${errorPath}; echo "Last 20 log lines:" >> ${errorPath}; tail -20 ${logPath} >> ${errorPath} 2>/dev/null || true; fi' EXIT
106
109
  ${userSetup}
107
110
  ANTHROPIC_API_KEY=${anthropicApiKey}
108
111
  GATEWAY_RUNTIME_PROXY_ENABLED=true
@@ -401,13 +404,31 @@ function watchHatchingDesktop(
401
404
  }
402
405
 
403
406
  function buildSshArgs(host: string): string[] {
404
- return [
405
- host,
407
+ const args: string[] = [host];
408
+ const keyPath = process.env.VELLUM_SSH_KEY_PATH;
409
+ if (keyPath) {
410
+ args.push("-i", keyPath);
411
+ }
412
+ args.push(
406
413
  "-o", "StrictHostKeyChecking=no",
407
414
  "-o", "UserKnownHostsFile=/dev/null",
408
415
  "-o", "ConnectTimeout=10",
409
416
  "-o", "LogLevel=ERROR",
410
- ];
417
+ );
418
+ return args;
419
+ }
420
+
421
+ function buildScpArgs(keyPath?: string): string[] {
422
+ const args: string[] = [];
423
+ if (keyPath) {
424
+ args.push("-i", keyPath);
425
+ }
426
+ args.push(
427
+ "-o", "StrictHostKeyChecking=no",
428
+ "-o", "UserKnownHostsFile=/dev/null",
429
+ "-o", "LogLevel=ERROR",
430
+ );
431
+ return args;
411
432
  }
412
433
 
413
434
  function extractHostname(host: string): string {
@@ -454,27 +475,22 @@ async function hatchCustom(
454
475
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
455
476
  writeFileSync(startupScriptPath, startupScript);
456
477
 
478
+ const sshKeyPath = process.env.VELLUM_SSH_KEY_PATH;
479
+
480
+ const installScriptPath = resolveInstallScriptPath();
481
+
457
482
  try {
458
- const installScriptPath = await resolveInstallScriptPath();
459
- if (installScriptPath) {
460
- console.log("📋 Uploading install script to instance...");
461
- await exec("scp", [
462
- "-o", "StrictHostKeyChecking=no",
463
- "-o", "UserKnownHostsFile=/dev/null",
464
- "-o", "LogLevel=ERROR",
465
- installScriptPath,
466
- `${host}:${INSTALL_SCRIPT_REMOTE_PATH}`,
467
- ]);
468
- } else {
469
- console.warn("⚠️ Skipping install script upload (not available in compiled binary)");
470
- }
483
+ console.log("📋 Uploading install script to instance...");
484
+ await exec("scp", [
485
+ ...buildScpArgs(sshKeyPath),
486
+ installScriptPath,
487
+ `${host}:${INSTALL_SCRIPT_REMOTE_PATH}`,
488
+ ]);
471
489
 
472
490
  console.log("📋 Uploading startup script to instance...");
473
491
  const remoteStartupPath = `/tmp/${instanceName}-startup.sh`;
474
492
  await exec("scp", [
475
- "-o", "StrictHostKeyChecking=no",
476
- "-o", "UserKnownHostsFile=/dev/null",
477
- "-o", "LogLevel=ERROR",
493
+ ...buildScpArgs(sshKeyPath),
478
494
  startupScriptPath,
479
495
  `${host}:${remoteStartupPath}`,
480
496
  ]);
@@ -485,9 +501,8 @@ async function hatchCustom(
485
501
  `chmod +x ${remoteStartupPath} ${INSTALL_SCRIPT_REMOTE_PATH} && bash ${remoteStartupPath}`,
486
502
  ]);
487
503
  } finally {
488
- try {
489
- unlinkSync(startupScriptPath);
490
- } catch {}
504
+ try { unlinkSync(startupScriptPath); } catch {}
505
+ try { unlinkSync(installScriptPath); } catch {}
491
506
  }
492
507
 
493
508
  const runtimeUrl = `http://${hostname}:${GATEWAY_PORT}`;
@@ -520,6 +535,43 @@ async function hatchCustom(
520
535
  }
521
536
  }
522
537
 
538
+ function installCLISymlink(): void {
539
+ const cliBinary = process.execPath;
540
+ if (!cliBinary || !existsSync(cliBinary)) return;
541
+
542
+ const symlinkPath = "/usr/local/bin/vellum";
543
+
544
+ try {
545
+ // Use lstatSync (not existsSync) to detect dangling symlinks —
546
+ // existsSync follows symlinks and returns false for broken links.
547
+ try {
548
+ const stats = lstatSync(symlinkPath);
549
+ if (!stats.isSymbolicLink()) {
550
+ // Real file — don't overwrite (developer's local install)
551
+ return;
552
+ }
553
+ // Already a symlink — skip if it already points to our binary
554
+ const dest = readlinkSync(symlinkPath);
555
+ if (dest === cliBinary) return;
556
+ // Stale or dangling symlink — remove before creating new one
557
+ unlinkSync(symlinkPath);
558
+ } catch (e) {
559
+ if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") throw e;
560
+ // Path doesn't exist — proceed to create symlink
561
+ }
562
+
563
+ const dir = "/usr/local/bin";
564
+ if (!existsSync(dir)) {
565
+ mkdirSync(dir, { recursive: true });
566
+ }
567
+ symlinkSync(cliBinary, symlinkPath);
568
+ console.log(` Symlinked ${symlinkPath} → ${cliBinary}`);
569
+ } catch {
570
+ // Permission denied or other error — not critical
571
+ console.log(` ⚠ Could not create symlink at ${symlinkPath} (run with sudo or create manually)`);
572
+ }
573
+ }
574
+
523
575
  async function hatchLocal(species: Species, name: string | null, daemonOnly: boolean = false): Promise<void> {
524
576
  const instanceName =
525
577
  name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
@@ -538,6 +590,8 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
538
590
  }
539
591
  }
540
592
 
593
+ const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
594
+
541
595
  console.log(`🥚 Hatching local assistant: ${instanceName}`);
542
596
  console.log(` Species: ${species}`);
543
597
  console.log("");
@@ -546,7 +600,7 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
546
600
 
547
601
  let runtimeUrl: string;
548
602
  try {
549
- runtimeUrl = await startGateway();
603
+ runtimeUrl = await startGateway(instanceName);
550
604
  } catch (error) {
551
605
  // Gateway failed — stop the daemon we just started so we don't leave
552
606
  // orphaned processes with no lock file entry.
@@ -555,8 +609,6 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
555
609
  throw error;
556
610
  }
557
611
 
558
- const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
559
-
560
612
  // Read the bearer token written by the daemon so the client can authenticate
561
613
  // with the gateway (which requires auth by default).
562
614
  let bearerToken: string | undefined;
@@ -579,6 +631,10 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
579
631
  if (!daemonOnly) {
580
632
  saveAssistantEntry(localEntry);
581
633
 
634
+ if (process.env.VELLUM_DESKTOP_APP) {
635
+ installCLISymlink();
636
+ }
637
+
582
638
  console.log("");
583
639
  console.log(`✅ Local assistant hatched!`);
584
640
  console.log("");
@@ -0,0 +1,68 @@
1
+ import {
2
+ clearPlatformToken,
3
+ fetchCurrentUser,
4
+ readPlatformToken,
5
+ savePlatformToken,
6
+ } from "../lib/platform-client";
7
+
8
+ export async function login(): Promise<void> {
9
+ const args = process.argv.slice(3);
10
+ let token: string | null = null;
11
+
12
+ for (let i = 0; i < args.length; i++) {
13
+ if (args[i] === "--token") {
14
+ token = args[i + 1];
15
+ if (!token) {
16
+ console.error("Error: --token requires a value");
17
+ process.exit(1);
18
+ }
19
+ break;
20
+ }
21
+ }
22
+
23
+ if (!token) {
24
+ console.error("Usage: vellum login --token <session-token>");
25
+ console.error("");
26
+ console.error("To get your session token:");
27
+ console.error(" 1. Log in to the Vellum platform in your browser");
28
+ console.error(" 2. Open Developer Tools → Application → Cookies");
29
+ console.error(" 3. Copy the value of the session token");
30
+ process.exit(1);
31
+ }
32
+
33
+ console.log("Validating token...");
34
+
35
+ try {
36
+ const user = await fetchCurrentUser(token);
37
+ savePlatformToken(token);
38
+ console.log(`✅ Logged in as ${user.email}`);
39
+ } catch (error) {
40
+ console.error(`❌ Login failed: ${error instanceof Error ? error.message : error}`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ export async function logout(): Promise<void> {
46
+ clearPlatformToken();
47
+ console.log("Logged out. Platform token removed.");
48
+ }
49
+
50
+ export async function whoami(): Promise<void> {
51
+ const token = readPlatformToken();
52
+ if (!token) {
53
+ console.error("Not logged in. Run `vellum login --token <token>` first.");
54
+ process.exit(1);
55
+ }
56
+
57
+ try {
58
+ const user = await fetchCurrentUser(token);
59
+ console.log(`Email: ${user.email}`);
60
+ if (user.display) {
61
+ console.log(`Name: ${user.display}`);
62
+ }
63
+ console.log(`ID: ${user.id}`);
64
+ } catch (error) {
65
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
66
+ process.exit(1);
67
+ }
68
+ }
@@ -66,7 +66,7 @@ const SSH_OPTS = [
66
66
  const REMOTE_PS_CMD = [
67
67
  // List vellum-related processes: daemon, gateway, qdrant, and any bun children
68
68
  "ps ax -o pid=,ppid=,args=",
69
- "| grep -E 'vellum|gateway|qdrant|openclaw'",
69
+ "| grep -E 'vellum|vellum-gateway|qdrant|openclaw'",
70
70
  "| grep -v grep",
71
71
  ].join(" ");
72
72
 
@@ -78,9 +78,13 @@ interface RemoteProcess {
78
78
 
79
79
  function classifyProcess(command: string): string {
80
80
  if (/qdrant/.test(command)) return "qdrant";
81
- if (/gateway/.test(command)) return "gateway";
81
+ if (/vellum-gateway/.test(command)) return "gateway";
82
82
  if (/openclaw/.test(command)) return "openclaw-adapter";
83
+ if (/vellum-daemon/.test(command)) return "daemon";
83
84
  if (/daemon\s+(start|restart)/.test(command)) return "daemon";
85
+ // Exclude macOS desktop app processes — their path contains .app/Contents/MacOS/
86
+ // but they are not background service processes.
87
+ if (/\.app\/Contents\/MacOS\//.test(command)) return "unknown";
84
88
  if (/vellum/.test(command)) return "vellum";
85
89
  return "unknown";
86
90
  }
@@ -277,7 +281,7 @@ async function detectOrphanedProcesses(): Promise<OrphanedProcess[]> {
277
281
  try {
278
282
  const output = await execOutput("sh", [
279
283
  "-c",
280
- "ps ax -o pid=,ppid=,args= | grep -E 'vellum|gateway|qdrant|openclaw' | grep -v grep",
284
+ "ps ax -o pid=,ppid=,args= | grep -E 'vellum|vellum-gateway|qdrant|openclaw' | grep -v grep",
281
285
  ]);
282
286
  const procs = parseRemotePs(output);
283
287
  const ownPid = String(process.pid);
@@ -11,7 +11,7 @@ import { exec } from "../lib/step-runner";
11
11
  export async function recover(): Promise<void> {
12
12
  const name = process.argv[3];
13
13
  if (!name) {
14
- console.error("Usage: vellum-cli recover <name>");
14
+ console.error("Usage: vellum recover <name>");
15
15
  process.exit(1);
16
16
  }
17
17
 
@@ -125,14 +125,14 @@ export async function retire(): Promise<void> {
125
125
 
126
126
  if (!name) {
127
127
  console.error("Error: Instance name is required.");
128
- console.error("Usage: vellum-cli retire <name> [--source <source>]");
128
+ console.error("Usage: vellum retire <name> [--source <source>]");
129
129
  process.exit(1);
130
130
  }
131
131
 
132
132
  const entry = findAssistantByName(name);
133
133
  if (!entry) {
134
134
  console.error(`No assistant found with name '${name}'.`);
135
- console.error("Run 'vellum-cli hatch' first, or check the instance name.");
135
+ console.error("Run 'vellum hatch' first, or check the instance name.");
136
136
  process.exit(1);
137
137
  }
138
138