@vellumai/cli 0.3.10 → 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.
- package/README.md +8 -8
- package/bun.lock +6 -0
- package/package.json +5 -2
- package/src/__tests__/assistant-config.test.ts +139 -0
- package/src/__tests__/constants.test.ts +48 -0
- package/src/__tests__/health-check.test.ts +64 -0
- package/src/__tests__/random-name.test.ts +19 -0
- package/src/__tests__/retire-archive.test.ts +30 -0
- package/src/__tests__/status-emoji.test.ts +44 -0
- package/src/commands/autonomy.ts +321 -0
- package/src/commands/client.ts +15 -8
- package/src/commands/contacts.ts +265 -0
- package/src/commands/hatch.ts +93 -37
- package/src/commands/login.ts +68 -0
- package/src/commands/ps.ts +7 -3
- package/src/commands/recover.ts +1 -1
- package/src/commands/retire.ts +2 -2
- package/src/commands/skills.ts +355 -0
- package/src/commands/ssh.ts +5 -2
- package/src/components/DefaultMainScreen.tsx +67 -3
- package/src/index.ts +23 -0
- package/src/lib/assistant-config.ts +1 -0
- package/src/lib/gcp.ts +9 -13
- package/src/lib/local.ts +23 -28
- package/src/lib/platform-client.ts +74 -0
- package/src/types/sh.d.ts +4 -0
package/src/commands/hatch.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 >
|
|
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
|
|
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" >
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/ps.ts
CHANGED
|
@@ -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);
|
package/src/commands/recover.ts
CHANGED
|
@@ -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
|
|
14
|
+
console.error("Usage: vellum recover <name>");
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
17
|
|
package/src/commands/retire.ts
CHANGED
|
@@ -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
|
|
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
|
|
135
|
+
console.error("Run 'vellum hatch' first, or check the instance name.");
|
|
136
136
|
process.exit(1);
|
|
137
137
|
}
|
|
138
138
|
|