@vellumai/cli 0.4.25 → 0.4.29
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 +24 -24
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +17 -5
- package/src/__tests__/retire-archive.test.ts +6 -2
- package/src/adapters/openclaw-http-server.ts +22 -7
- package/src/commands/autonomy.ts +10 -11
- package/src/commands/client.ts +25 -9
- package/src/commands/config.ts +2 -6
- package/src/commands/contacts.ts +1 -4
- package/src/commands/hatch.ts +131 -36
- package/src/commands/login.ts +6 -2
- package/src/commands/pair.ts +26 -9
- package/src/commands/ps.ts +55 -23
- package/src/commands/recover.ts +4 -2
- package/src/commands/retire.ts +42 -14
- package/src/commands/sleep.ts +15 -3
- package/src/commands/ssh.ts +20 -13
- package/src/commands/tunnel.ts +6 -7
- package/src/commands/wake.ts +13 -4
- package/src/components/DefaultMainScreen.tsx +309 -99
- package/src/index.ts +2 -2
- package/src/lib/assistant-config.ts +9 -3
- package/src/lib/aws.ts +36 -11
- package/src/lib/constants.ts +3 -1
- package/src/lib/doctor-client.ts +23 -7
- package/src/lib/gcp.ts +74 -24
- package/src/lib/health-check.ts +14 -4
- package/src/lib/local.ts +249 -33
- package/src/lib/ngrok.ts +1 -3
- package/src/lib/openclaw-runtime-server.ts +7 -2
- package/src/lib/platform-client.ts +16 -3
- package/src/lib/xdg-log.ts +25 -5
package/src/commands/hatch.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readlinkSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "fs";
|
|
3
13
|
import { homedir, hostname, userInfo } from "os";
|
|
4
14
|
import { join } from "path";
|
|
5
15
|
|
|
@@ -10,7 +20,11 @@ import qrcode from "qrcode-terminal";
|
|
|
10
20
|
import cliPkg from "../../package.json";
|
|
11
21
|
|
|
12
22
|
import { buildOpenclawStartupScript } from "../adapters/openclaw";
|
|
13
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
loadAllAssistants,
|
|
25
|
+
saveAssistantEntry,
|
|
26
|
+
syncConfigToLockfile,
|
|
27
|
+
} from "../lib/assistant-config";
|
|
14
28
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
15
29
|
import { hatchAws } from "../lib/aws";
|
|
16
30
|
import {
|
|
@@ -22,7 +36,12 @@ import {
|
|
|
22
36
|
import type { RemoteHost, Species } from "../lib/constants";
|
|
23
37
|
import { hatchGcp } from "../lib/gcp";
|
|
24
38
|
import type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
25
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
startLocalDaemon,
|
|
41
|
+
startGateway,
|
|
42
|
+
startOutboundProxy,
|
|
43
|
+
stopLocalProcesses,
|
|
44
|
+
} from "../lib/local";
|
|
26
45
|
import { probePort } from "../lib/port-probe";
|
|
27
46
|
import { isProcessAlive } from "../lib/process";
|
|
28
47
|
import { generateRandomSuffix } from "../lib/random-name";
|
|
@@ -32,14 +51,13 @@ export type { PollResult, WatchHatchingResult } from "../lib/gcp";
|
|
|
32
51
|
|
|
33
52
|
const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
|
|
34
53
|
|
|
35
|
-
|
|
36
54
|
const HATCH_TIMEOUT_MS: Record<Species, number> = {
|
|
37
55
|
vellum: 2 * 60 * 1000,
|
|
38
56
|
openclaw: 10 * 60 * 1000,
|
|
39
57
|
};
|
|
40
58
|
const DEFAULT_SPECIES: Species = "vellum";
|
|
41
59
|
|
|
42
|
-
const SPINNER_FRAMES= ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
60
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
43
61
|
|
|
44
62
|
const IS_DESKTOP = !!process.env.VELLUM_DESKTOP_APP;
|
|
45
63
|
|
|
@@ -77,9 +95,14 @@ export async function buildStartupScript(
|
|
|
77
95
|
instanceName: string,
|
|
78
96
|
cloud: RemoteHost,
|
|
79
97
|
): Promise<string> {
|
|
80
|
-
const platformUrl =
|
|
81
|
-
|
|
82
|
-
const
|
|
98
|
+
const platformUrl =
|
|
99
|
+
process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
|
|
100
|
+
const logPath =
|
|
101
|
+
cloud === "custom"
|
|
102
|
+
? "/tmp/vellum-startup.log"
|
|
103
|
+
: "/var/log/startup-script.log";
|
|
104
|
+
const errorPath =
|
|
105
|
+
cloud === "custom" ? "/tmp/vellum-startup-error" : "/var/log/startup-error";
|
|
83
106
|
const timestampRedirect = buildTimestampRedirect(logPath);
|
|
84
107
|
const userSetup = buildUserSetup(sshUser);
|
|
85
108
|
const ownershipFixup = buildOwnershipFixup();
|
|
@@ -174,10 +197,18 @@ function parseArgs(): HatchArgs {
|
|
|
174
197
|
console.log("Options:");
|
|
175
198
|
console.log(" -d Run in detached mode");
|
|
176
199
|
console.log(" --name <name> Custom instance name");
|
|
177
|
-
console.log(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.log(
|
|
200
|
+
console.log(
|
|
201
|
+
" --remote <host> Remote host (local, gcp, aws, custom)",
|
|
202
|
+
);
|
|
203
|
+
console.log(
|
|
204
|
+
" --daemon-only Start assistant only, skip gateway",
|
|
205
|
+
);
|
|
206
|
+
console.log(
|
|
207
|
+
" --restart Restart processes without onboarding side effects",
|
|
208
|
+
);
|
|
209
|
+
console.log(
|
|
210
|
+
" --watch Run assistant and gateway in watch mode (hot reload on source changes)",
|
|
211
|
+
);
|
|
181
212
|
process.exit(0);
|
|
182
213
|
} else if (arg === "-d") {
|
|
183
214
|
detached = true;
|
|
@@ -196,7 +227,9 @@ function parseArgs(): HatchArgs {
|
|
|
196
227
|
try {
|
|
197
228
|
validateAssistantName(next);
|
|
198
229
|
} catch {
|
|
199
|
-
console.error(
|
|
230
|
+
console.error(
|
|
231
|
+
`Error: --name contains invalid characters (path separators or traversal segments are not allowed)`,
|
|
232
|
+
);
|
|
200
233
|
process.exit(1);
|
|
201
234
|
}
|
|
202
235
|
name = next;
|
|
@@ -236,7 +269,11 @@ function pickMessage(messages: string[], elapsedMs: number): string {
|
|
|
236
269
|
return messages[idx];
|
|
237
270
|
}
|
|
238
271
|
|
|
239
|
-
function getPhaseIcon(
|
|
272
|
+
function getPhaseIcon(
|
|
273
|
+
hasLogs: boolean,
|
|
274
|
+
elapsedMs: number,
|
|
275
|
+
species: Species,
|
|
276
|
+
): string {
|
|
240
277
|
if (!hasLogs) {
|
|
241
278
|
return elapsedMs < 30000 ? "🥚" : "🪺";
|
|
242
279
|
}
|
|
@@ -292,7 +329,11 @@ export async function watchHatching(
|
|
|
292
329
|
: pickMessage(config.waitingMessages, elapsed);
|
|
293
330
|
spinnerIdx++;
|
|
294
331
|
|
|
295
|
-
const lines = [
|
|
332
|
+
const lines = [
|
|
333
|
+
"",
|
|
334
|
+
` ${icon} ${spinner} ${message} ⏱ ${formatElapsed(elapsed)}`,
|
|
335
|
+
"",
|
|
336
|
+
];
|
|
296
337
|
|
|
297
338
|
for (const line of lines) {
|
|
298
339
|
process.stdout.write(`\x1b[K${line}\n`);
|
|
@@ -334,7 +375,9 @@ export async function watchHatching(
|
|
|
334
375
|
if (elapsed >= HATCH_TIMEOUT_MS[species]) {
|
|
335
376
|
clearInterval(interval);
|
|
336
377
|
console.log("");
|
|
337
|
-
console.log(
|
|
378
|
+
console.log(
|
|
379
|
+
` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
|
|
380
|
+
);
|
|
338
381
|
console.log(` Monitor with: vel logs ${instanceName}`);
|
|
339
382
|
console.log("");
|
|
340
383
|
resolve({ success: true, errorContent: lastErrorContent });
|
|
@@ -378,7 +421,9 @@ function watchHatchingDesktop(
|
|
|
378
421
|
|
|
379
422
|
if (elapsed >= HATCH_TIMEOUT_MS[species]) {
|
|
380
423
|
clearInterval(interval);
|
|
381
|
-
desktopLog(
|
|
424
|
+
desktopLog(
|
|
425
|
+
`Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
|
|
426
|
+
);
|
|
382
427
|
desktopLog(`Monitor with: vel logs ${instanceName}`);
|
|
383
428
|
resolve({ success: true, errorContent: lastErrorContent });
|
|
384
429
|
return;
|
|
@@ -474,7 +519,9 @@ function ensureLocalBinInShellProfile(localBinDir: string): void {
|
|
|
474
519
|
if (!profilePath) return;
|
|
475
520
|
|
|
476
521
|
try {
|
|
477
|
-
const contents = existsSync(profilePath)
|
|
522
|
+
const contents = existsSync(profilePath)
|
|
523
|
+
? readFileSync(profilePath, "utf-8")
|
|
524
|
+
: "";
|
|
478
525
|
// Check if ~/.local/bin is already referenced in PATH exports
|
|
479
526
|
if (contents.includes(localBinDir)) return;
|
|
480
527
|
const line = `\nexport PATH="${localBinDir}:\$PATH"\n`;
|
|
@@ -506,10 +553,16 @@ function installCLISymlink(): void {
|
|
|
506
553
|
return;
|
|
507
554
|
}
|
|
508
555
|
|
|
509
|
-
console.log(
|
|
556
|
+
console.log(
|
|
557
|
+
` ⚠ Could not create symlink for vellum CLI (tried ${preferredPath} and ${fallbackPath})`,
|
|
558
|
+
);
|
|
510
559
|
}
|
|
511
560
|
|
|
512
|
-
async function waitForDaemonReady(
|
|
561
|
+
async function waitForDaemonReady(
|
|
562
|
+
runtimeUrl: string,
|
|
563
|
+
bearerToken: string | undefined,
|
|
564
|
+
timeoutMs = 15000,
|
|
565
|
+
): Promise<boolean> {
|
|
513
566
|
const start = Date.now();
|
|
514
567
|
const pollInterval = 1000;
|
|
515
568
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -528,7 +581,10 @@ async function waitForDaemonReady(runtimeUrl: string, bearerToken: string | unde
|
|
|
528
581
|
return false;
|
|
529
582
|
}
|
|
530
583
|
|
|
531
|
-
async function displayPairingQRCode(
|
|
584
|
+
async function displayPairingQRCode(
|
|
585
|
+
runtimeUrl: string,
|
|
586
|
+
bearerToken: string | undefined,
|
|
587
|
+
): Promise<void> {
|
|
532
588
|
try {
|
|
533
589
|
const pairingRequestId = randomUUID();
|
|
534
590
|
const pairingSecret = randomBytes(32).toString("hex");
|
|
@@ -539,7 +595,9 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
539
595
|
// health endpoint through the gateway to ensure it's reachable.
|
|
540
596
|
const daemonReady = await waitForDaemonReady(runtimeUrl, bearerToken);
|
|
541
597
|
if (!daemonReady) {
|
|
542
|
-
console.warn(
|
|
598
|
+
console.warn(
|
|
599
|
+
"⚠ Assistant health check did not pass within 15s. Run `vellum pair` to try again.\n",
|
|
600
|
+
);
|
|
543
601
|
return;
|
|
544
602
|
}
|
|
545
603
|
|
|
@@ -549,16 +607,24 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
549
607
|
"Content-Type": "application/json",
|
|
550
608
|
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
551
609
|
},
|
|
552
|
-
body: JSON.stringify({
|
|
610
|
+
body: JSON.stringify({
|
|
611
|
+
pairingRequestId,
|
|
612
|
+
pairingSecret,
|
|
613
|
+
gatewayUrl: runtimeUrl,
|
|
614
|
+
}),
|
|
553
615
|
});
|
|
554
616
|
|
|
555
617
|
if (!registerRes.ok) {
|
|
556
618
|
const body = await registerRes.text().catch(() => "");
|
|
557
|
-
console.warn(
|
|
619
|
+
console.warn(
|
|
620
|
+
`⚠ Could not register pairing request: ${registerRes.status} ${registerRes.statusText}${body ? ` — ${body}` : ""}. Run \`vellum pair\` to try again.\n`,
|
|
621
|
+
);
|
|
558
622
|
return;
|
|
559
623
|
}
|
|
560
624
|
|
|
561
|
-
const hostId = createHash("sha256")
|
|
625
|
+
const hostId = createHash("sha256")
|
|
626
|
+
.update(hostname() + userInfo().username)
|
|
627
|
+
.digest("hex");
|
|
562
628
|
const payload = JSON.stringify({
|
|
563
629
|
type: "vellum-daemon",
|
|
564
630
|
v: 4,
|
|
@@ -580,11 +646,15 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
580
646
|
mkdirSync(qrDir, { recursive: true });
|
|
581
647
|
const qrPngPath = join(qrDir, "initial.png");
|
|
582
648
|
try {
|
|
583
|
-
const pngBuffer = await QRCode.toBuffer(payload, {
|
|
649
|
+
const pngBuffer = await QRCode.toBuffer(payload, {
|
|
650
|
+
type: "png",
|
|
651
|
+
width: 512,
|
|
652
|
+
});
|
|
584
653
|
writeFileSync(qrPngPath, pngBuffer);
|
|
585
654
|
console.log(`QR code PNG saved to ${qrPngPath}\n`);
|
|
586
655
|
} catch (pngErr) {
|
|
587
|
-
const pngReason =
|
|
656
|
+
const pngReason =
|
|
657
|
+
pngErr instanceof Error ? pngErr.message : String(pngErr);
|
|
588
658
|
console.warn(`\u26A0 Could not save QR code PNG: ${pngReason}\n`);
|
|
589
659
|
}
|
|
590
660
|
|
|
@@ -595,18 +665,30 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
|
|
|
595
665
|
} catch (err) {
|
|
596
666
|
// Non-fatal — pairing is optional
|
|
597
667
|
const reason = err instanceof Error ? err.message : String(err);
|
|
598
|
-
console.warn(
|
|
668
|
+
console.warn(
|
|
669
|
+
`⚠ Could not generate pairing QR code: ${reason}. Run \`vellum pair\` to try again.\n`,
|
|
670
|
+
);
|
|
599
671
|
}
|
|
600
672
|
}
|
|
601
673
|
|
|
602
|
-
async function hatchLocal(
|
|
674
|
+
async function hatchLocal(
|
|
675
|
+
species: Species,
|
|
676
|
+
name: string | null,
|
|
677
|
+
daemonOnly: boolean = false,
|
|
678
|
+
restart: boolean = false,
|
|
679
|
+
watch: boolean = false,
|
|
680
|
+
): Promise<void> {
|
|
603
681
|
if (restart && !name && !process.env.VELLUM_ASSISTANT_NAME) {
|
|
604
|
-
console.error(
|
|
682
|
+
console.error(
|
|
683
|
+
"Error: Cannot restart without a known assistant ID. Provide --name or ensure VELLUM_ASSISTANT_NAME is set.",
|
|
684
|
+
);
|
|
605
685
|
process.exit(1);
|
|
606
686
|
}
|
|
607
687
|
|
|
608
688
|
const instanceName =
|
|
609
|
-
name ??
|
|
689
|
+
name ??
|
|
690
|
+
process.env.VELLUM_ASSISTANT_NAME ??
|
|
691
|
+
`${species}-${generateRandomSuffix()}`;
|
|
610
692
|
|
|
611
693
|
// Clean up stale local state: if daemon/gateway processes are running but
|
|
612
694
|
// the lock file has no entries, stop them before starting fresh.
|
|
@@ -617,7 +699,9 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
617
699
|
const daemonPid = isProcessAlive(join(vellumDir, "vellum.pid"));
|
|
618
700
|
const gatewayPid = isProcessAlive(join(vellumDir, "gateway.pid"));
|
|
619
701
|
if (daemonPid.alive || gatewayPid.alive) {
|
|
620
|
-
console.log(
|
|
702
|
+
console.log(
|
|
703
|
+
"🧹 Cleaning up stale local processes (no lock file entry)...\n",
|
|
704
|
+
);
|
|
621
705
|
await stopLocalProcesses();
|
|
622
706
|
}
|
|
623
707
|
|
|
@@ -648,7 +732,11 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
648
732
|
}
|
|
649
733
|
}
|
|
650
734
|
|
|
651
|
-
const baseDataDir = join(
|
|
735
|
+
const baseDataDir = join(
|
|
736
|
+
process.env.BASE_DATA_DIR?.trim() ||
|
|
737
|
+
(process.env.HOME ?? userInfo().homedir),
|
|
738
|
+
".vellum",
|
|
739
|
+
);
|
|
652
740
|
|
|
653
741
|
console.log(`🥚 Hatching local assistant: ${instanceName}`);
|
|
654
742
|
console.log(` Species: ${species}`);
|
|
@@ -662,11 +750,15 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
662
750
|
} catch (error) {
|
|
663
751
|
// Gateway failed — stop the daemon we just started so we don't leave
|
|
664
752
|
// orphaned processes with no lock file entry.
|
|
665
|
-
console.error(
|
|
753
|
+
console.error(
|
|
754
|
+
`\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
|
|
755
|
+
);
|
|
666
756
|
await stopLocalProcesses();
|
|
667
757
|
throw error;
|
|
668
758
|
}
|
|
669
759
|
|
|
760
|
+
await startOutboundProxy(watch);
|
|
761
|
+
|
|
670
762
|
// Read the bearer token written by the daemon so the client can authenticate
|
|
671
763
|
// with the gateway (which requires auth by default).
|
|
672
764
|
let bearerToken: string | undefined;
|
|
@@ -715,10 +807,13 @@ export async function hatch(): Promise<void> {
|
|
|
715
807
|
const cliVersion = getCliVersion();
|
|
716
808
|
console.log(`@vellumai/cli v${cliVersion}`);
|
|
717
809
|
|
|
718
|
-
const { species, detached, name, remote, daemonOnly, restart, watch } =
|
|
810
|
+
const { species, detached, name, remote, daemonOnly, restart, watch } =
|
|
811
|
+
parseArgs();
|
|
719
812
|
|
|
720
813
|
if (restart && remote !== "local") {
|
|
721
|
-
console.error(
|
|
814
|
+
console.error(
|
|
815
|
+
"Error: --restart is only supported for local hatch targets.",
|
|
816
|
+
);
|
|
722
817
|
process.exit(1);
|
|
723
818
|
}
|
|
724
819
|
|
package/src/commands/login.ts
CHANGED
|
@@ -48,7 +48,9 @@ export async function login(): Promise<void> {
|
|
|
48
48
|
savePlatformToken(token);
|
|
49
49
|
console.log(`✅ Logged in as ${user.email}`);
|
|
50
50
|
} catch (error) {
|
|
51
|
-
console.error(
|
|
51
|
+
console.error(
|
|
52
|
+
`❌ Login failed: ${error instanceof Error ? error.message : error}`,
|
|
53
|
+
);
|
|
52
54
|
process.exit(1);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -58,7 +60,9 @@ export async function logout(): Promise<void> {
|
|
|
58
60
|
if (args.includes("--help") || args.includes("-h")) {
|
|
59
61
|
console.log("Usage: vellum logout");
|
|
60
62
|
console.log("");
|
|
61
|
-
console.log(
|
|
63
|
+
console.log(
|
|
64
|
+
"Log out of the Vellum platform and remove the stored session token.",
|
|
65
|
+
);
|
|
62
66
|
process.exit(0);
|
|
63
67
|
}
|
|
64
68
|
|
package/src/commands/pair.ts
CHANGED
|
@@ -55,16 +55,20 @@ async function pollForApproval(
|
|
|
55
55
|
|
|
56
56
|
if (!statusRes.ok) {
|
|
57
57
|
const body = await statusRes.text().catch(() => "");
|
|
58
|
-
throw new Error(
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Failed to check pairing status: HTTP ${statusRes.status}: ${body || statusRes.statusText}`,
|
|
60
|
+
);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
const statusBody = await statusRes.json() as PairingResponse;
|
|
63
|
+
const statusBody = (await statusRes.json()) as PairingResponse;
|
|
62
64
|
|
|
63
65
|
if (statusBody.status === "approved") {
|
|
64
66
|
return statusBody;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
await new Promise((resolve) =>
|
|
69
|
+
await new Promise((resolve) =>
|
|
70
|
+
setTimeout(resolve, PAIRING_POLL_INTERVAL_MS),
|
|
71
|
+
);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
throw new Error("Pairing timed out waiting for approval.");
|
|
@@ -76,7 +80,9 @@ export async function pair(): Promise<void> {
|
|
|
76
80
|
if (args.includes("--help") || args.includes("-h")) {
|
|
77
81
|
console.log("Usage: vellum pair <path-to-qrcode.png>");
|
|
78
82
|
console.log("");
|
|
79
|
-
console.log(
|
|
83
|
+
console.log(
|
|
84
|
+
"Pair with a remote assistant by scanning the QR code PNG generated during setup.",
|
|
85
|
+
);
|
|
80
86
|
process.exit(0);
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -85,7 +91,9 @@ export async function pair(): Promise<void> {
|
|
|
85
91
|
if (!qrCodePath) {
|
|
86
92
|
console.error("Usage: vellum pair <path-to-qrcode.png>");
|
|
87
93
|
console.error("");
|
|
88
|
-
console.error(
|
|
94
|
+
console.error(
|
|
95
|
+
"Pair with a remote assistant by scanning the QR code PNG generated during setup.",
|
|
96
|
+
);
|
|
89
97
|
process.exit(1);
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -102,7 +110,12 @@ export async function pair(): Promise<void> {
|
|
|
102
110
|
throw new Error("QR code does not contain valid pairing data.");
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
payload.type !== "vellum-daemon" ||
|
|
115
|
+
!payload.g ||
|
|
116
|
+
!payload.pairingRequestId ||
|
|
117
|
+
!payload.pairingSecret
|
|
118
|
+
) {
|
|
106
119
|
throw new Error("QR code does not contain valid Vellum pairing data.");
|
|
107
120
|
}
|
|
108
121
|
|
|
@@ -127,10 +140,12 @@ export async function pair(): Promise<void> {
|
|
|
127
140
|
|
|
128
141
|
if (!requestRes.ok) {
|
|
129
142
|
const body = await requestRes.text().catch(() => "");
|
|
130
|
-
throw new Error(
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Failed to initiate pairing: HTTP ${requestRes.status}: ${body || requestRes.statusText}`,
|
|
145
|
+
);
|
|
131
146
|
}
|
|
132
147
|
|
|
133
|
-
const requestBody = await requestRes.json() as PairingResponse;
|
|
148
|
+
const requestBody = (await requestRes.json()) as PairingResponse;
|
|
134
149
|
|
|
135
150
|
let bearerToken: string | undefined;
|
|
136
151
|
|
|
@@ -145,7 +160,9 @@ export async function pair(): Promise<void> {
|
|
|
145
160
|
);
|
|
146
161
|
bearerToken = approvedResponse.bearerToken;
|
|
147
162
|
} else {
|
|
148
|
-
throw new Error(
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Unexpected pairing response status: ${requestBody.status}`,
|
|
165
|
+
);
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
const customEntry: AssistantEntry = {
|
package/src/commands/ps.ts
CHANGED
|
@@ -63,10 +63,14 @@ function printTable(rows: TableRow[]): void {
|
|
|
63
63
|
// ── Remote process listing via SSH ──────────────────────────────
|
|
64
64
|
|
|
65
65
|
const SSH_OPTS = [
|
|
66
|
-
"-o",
|
|
67
|
-
"
|
|
68
|
-
"-o",
|
|
69
|
-
"
|
|
66
|
+
"-o",
|
|
67
|
+
"StrictHostKeyChecking=no",
|
|
68
|
+
"-o",
|
|
69
|
+
"UserKnownHostsFile=/dev/null",
|
|
70
|
+
"-o",
|
|
71
|
+
"ConnectTimeout=10",
|
|
72
|
+
"-o",
|
|
73
|
+
"LogLevel=ERROR",
|
|
70
74
|
];
|
|
71
75
|
|
|
72
76
|
const REMOTE_PS_CMD = [
|
|
@@ -86,8 +90,8 @@ function classifyProcess(command: string): string {
|
|
|
86
90
|
if (/qdrant/.test(command)) return "qdrant";
|
|
87
91
|
if (/vellum-gateway/.test(command)) return "gateway";
|
|
88
92
|
if (/openclaw/.test(command)) return "openclaw-adapter";
|
|
89
|
-
if (/vellum-daemon/.test(command)) return "
|
|
90
|
-
if (/daemon\s+(start|restart)/.test(command)) return "
|
|
93
|
+
if (/vellum-daemon/.test(command)) return "assistant";
|
|
94
|
+
if (/daemon\s+(start|restart)/.test(command)) return "assistant";
|
|
91
95
|
// Exclude macOS desktop app processes — their path contains .app/Contents/MacOS/
|
|
92
96
|
// but they are not background service processes.
|
|
93
97
|
if (/\.app\/Contents\/MacOS\//.test(command)) return "unknown";
|
|
@@ -126,9 +130,7 @@ function resolveCloud(entry: AssistantEntry): string {
|
|
|
126
130
|
return "local";
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
async function getRemoteProcessesGcp(
|
|
130
|
-
entry: AssistantEntry,
|
|
131
|
-
): Promise<string> {
|
|
133
|
+
async function getRemoteProcessesGcp(entry: AssistantEntry): Promise<string> {
|
|
132
134
|
return execOutput("gcloud", [
|
|
133
135
|
"compute",
|
|
134
136
|
"ssh",
|
|
@@ -148,11 +150,7 @@ async function getRemoteProcessesCustom(
|
|
|
148
150
|
): Promise<string> {
|
|
149
151
|
const host = extractHostFromUrl(entry.runtimeUrl);
|
|
150
152
|
const sshUser = entry.sshUser ?? "root";
|
|
151
|
-
return execOutput("ssh", [
|
|
152
|
-
...SSH_OPTS,
|
|
153
|
-
`${sshUser}@${host}`,
|
|
154
|
-
REMOTE_PS_CMD,
|
|
155
|
-
]);
|
|
153
|
+
return execOutput("ssh", [...SSH_OPTS, `${sshUser}@${host}`, REMOTE_PS_CMD]);
|
|
156
154
|
}
|
|
157
155
|
|
|
158
156
|
interface ProcessSpec {
|
|
@@ -192,7 +190,7 @@ async function detectProcess(spec: ProcessSpec): Promise<DetectedProcess> {
|
|
|
192
190
|
}
|
|
193
191
|
|
|
194
192
|
// Tier 2: TCP port probe (skip for processes without a port)
|
|
195
|
-
const listening = spec.port > 0 && await probePort(spec.port);
|
|
193
|
+
const listening = spec.port > 0 && (await probePort(spec.port));
|
|
196
194
|
if (listening) {
|
|
197
195
|
const filePid = readPidFile(spec.pidFile);
|
|
198
196
|
return {
|
|
@@ -222,11 +220,39 @@ function formatDetectionInfo(proc: DetectedProcess): string {
|
|
|
222
220
|
async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
223
221
|
const vellumDir = entry.baseDataDir ?? join(homedir(), ".vellum");
|
|
224
222
|
|
|
223
|
+
const PROXY_PORT = Number(process.env.PROXY_PORT) || 7829;
|
|
224
|
+
|
|
225
225
|
const specs: ProcessSpec[] = [
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
{
|
|
227
|
+
name: "assistant",
|
|
228
|
+
pgrepName: "vellum-daemon",
|
|
229
|
+
port: RUNTIME_HTTP_PORT,
|
|
230
|
+
pidFile: join(vellumDir, "vellum.pid"),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "qdrant",
|
|
234
|
+
pgrepName: "qdrant",
|
|
235
|
+
port: QDRANT_PORT,
|
|
236
|
+
pidFile: join(vellumDir, "workspace", "data", "qdrant", "qdrant.pid"),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "gateway",
|
|
240
|
+
pgrepName: "vellum-gateway",
|
|
241
|
+
port: GATEWAY_PORT,
|
|
242
|
+
pidFile: join(vellumDir, "gateway.pid"),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "outbound-proxy",
|
|
246
|
+
pgrepName: "outbound-proxy",
|
|
247
|
+
port: PROXY_PORT,
|
|
248
|
+
pidFile: join(vellumDir, "outbound-proxy.pid"),
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "embed-worker",
|
|
252
|
+
pgrepName: "embed-worker",
|
|
253
|
+
port: 0,
|
|
254
|
+
pidFile: join(vellumDir, "embed-worker.pid"),
|
|
255
|
+
},
|
|
230
256
|
];
|
|
231
257
|
|
|
232
258
|
const results = await Promise.all(specs.map(detectProcess));
|
|
@@ -260,7 +286,9 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
260
286
|
process.exit(1);
|
|
261
287
|
}
|
|
262
288
|
} catch (error) {
|
|
263
|
-
console.error(
|
|
289
|
+
console.error(
|
|
290
|
+
`Failed to list processes: ${error instanceof Error ? error.message : error}`,
|
|
291
|
+
);
|
|
264
292
|
process.exit(1);
|
|
265
293
|
}
|
|
266
294
|
|
|
@@ -295,7 +323,7 @@ async function detectOrphanedProcesses(): Promise<OrphanedProcess[]> {
|
|
|
295
323
|
|
|
296
324
|
// Strategy 1: PID file scan
|
|
297
325
|
const pidFiles: Array<{ file: string; name: string }> = [
|
|
298
|
-
{ file: join(vellumDir, "vellum.pid"), name: "
|
|
326
|
+
{ file: join(vellumDir, "vellum.pid"), name: "assistant" },
|
|
299
327
|
{ file: join(vellumDir, "gateway.pid"), name: "gateway" },
|
|
300
328
|
{ file: join(vellumDir, "qdrant.pid"), name: "qdrant" },
|
|
301
329
|
];
|
|
@@ -349,7 +377,9 @@ async function listAllAssistants(): Promise<void> {
|
|
|
349
377
|
}));
|
|
350
378
|
printTable(rows);
|
|
351
379
|
const pids = orphans.map((o) => o.pid).join(" ");
|
|
352
|
-
console.log(
|
|
380
|
+
console.log(
|
|
381
|
+
`\nHint: Run \`kill ${pids}\` to clean up orphaned processes.`,
|
|
382
|
+
);
|
|
353
383
|
}
|
|
354
384
|
|
|
355
385
|
return;
|
|
@@ -413,7 +443,9 @@ export async function ps(): Promise<void> {
|
|
|
413
443
|
if (args.includes("--help") || args.includes("-h")) {
|
|
414
444
|
console.log("Usage: vellum ps [<name>]");
|
|
415
445
|
console.log("");
|
|
416
|
-
console.log(
|
|
446
|
+
console.log(
|
|
447
|
+
"List all assistants, or show processes for a specific assistant.",
|
|
448
|
+
);
|
|
417
449
|
console.log("");
|
|
418
450
|
console.log("Arguments:");
|
|
419
451
|
console.log(" <name> Show processes for the named assistant");
|
package/src/commands/recover.ts
CHANGED
|
@@ -13,7 +13,9 @@ export async function recover(): Promise<void> {
|
|
|
13
13
|
if (args.includes("--help") || args.includes("-h")) {
|
|
14
14
|
console.log("Usage: vellum recover <name>");
|
|
15
15
|
console.log("");
|
|
16
|
-
console.log(
|
|
16
|
+
console.log(
|
|
17
|
+
"Restore a previously retired local assistant from its archive.",
|
|
18
|
+
);
|
|
17
19
|
console.log("");
|
|
18
20
|
console.log("Arguments:");
|
|
19
21
|
console.log(" <name> Name of the retired assistant to recover");
|
|
@@ -39,7 +41,7 @@ export async function recover(): Promise<void> {
|
|
|
39
41
|
const vellumDir = join(homedir(), ".vellum");
|
|
40
42
|
if (existsSync(vellumDir)) {
|
|
41
43
|
console.error(
|
|
42
|
-
"Error: ~/.vellum already exists. Retire the current assistant first."
|
|
44
|
+
"Error: ~/.vellum already exists. Retire the current assistant first.",
|
|
43
45
|
);
|
|
44
46
|
process.exit(1);
|
|
45
47
|
}
|