@vellumai/cli 0.3.22 → 0.3.24
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/package.json +1 -1
- package/src/commands/hatch.ts +50 -1
- package/src/components/DefaultMainScreen.tsx +4 -1
- package/src/lib/local.ts +22 -13
package/package.json
CHANGED
package/src/commands/hatch.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
1
2
|
import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync } from "fs";
|
|
2
|
-
import { homedir, userInfo } from "os";
|
|
3
|
+
import { homedir, hostname, userInfo } from "os";
|
|
3
4
|
import { join } from "path";
|
|
4
5
|
|
|
6
|
+
import qrcode from "qrcode-terminal";
|
|
7
|
+
|
|
5
8
|
// Direct import — bun embeds this at compile time so it works in compiled binaries.
|
|
6
9
|
import cliPkg from "../../package.json";
|
|
7
10
|
|
|
@@ -478,6 +481,49 @@ function installCLISymlink(): void {
|
|
|
478
481
|
console.log(` ⚠ Could not create symlink for vellum CLI (tried ${preferredPath} and ${fallbackPath})`);
|
|
479
482
|
}
|
|
480
483
|
|
|
484
|
+
async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | undefined): Promise<void> {
|
|
485
|
+
try {
|
|
486
|
+
const pairingRequestId = randomUUID();
|
|
487
|
+
const pairingSecret = randomBytes(32).toString("hex");
|
|
488
|
+
|
|
489
|
+
const registerRes = await fetch(`${runtimeUrl}/v1/pairing/register`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({ pairingRequestId, pairingSecret, gatewayUrl: runtimeUrl }),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (!registerRes.ok) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const hostId = createHash("sha256").update(hostname() + userInfo().username).digest("hex");
|
|
503
|
+
const payload = JSON.stringify({
|
|
504
|
+
type: "vellum-daemon",
|
|
505
|
+
v: 4,
|
|
506
|
+
id: hostId,
|
|
507
|
+
g: runtimeUrl,
|
|
508
|
+
pairingRequestId,
|
|
509
|
+
pairingSecret,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const qrString = await new Promise<string>((resolve) => {
|
|
513
|
+
qrcode.generate(payload, { small: true }, (code: string) => {
|
|
514
|
+
resolve(code);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
console.log("Scan this QR code with the Vellum iOS app to pair:\n");
|
|
519
|
+
console.log(qrString);
|
|
520
|
+
console.log("This pairing request expires in 5 minutes.");
|
|
521
|
+
console.log("Run `vellum pair` to generate a new one.\n");
|
|
522
|
+
} catch {
|
|
523
|
+
// Non-fatal — pairing is optional
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
481
527
|
async function hatchLocal(species: Species, name: string | null, daemonOnly: boolean = false): Promise<void> {
|
|
482
528
|
const instanceName =
|
|
483
529
|
name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
|
|
@@ -549,6 +595,9 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
|
|
|
549
595
|
console.log(` Name: ${instanceName}`);
|
|
550
596
|
console.log(` Runtime: ${runtimeUrl}`);
|
|
551
597
|
console.log("");
|
|
598
|
+
|
|
599
|
+
// Generate and display pairing QR code
|
|
600
|
+
await displayPairingQRCode(runtimeUrl, bearerToken);
|
|
552
601
|
}
|
|
553
602
|
}
|
|
554
603
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { randomBytes, randomUUID } from "crypto";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
3
|
+
import { hostname, userInfo } from "os";
|
|
3
4
|
import { basename } from "path";
|
|
4
5
|
import qrcode from "qrcode-terminal";
|
|
5
6
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
|
|
@@ -1322,9 +1323,11 @@ function ChatApp({
|
|
|
1322
1323
|
throw new Error(`HTTP ${registerRes.status}: ${body || registerRes.statusText}`);
|
|
1323
1324
|
}
|
|
1324
1325
|
|
|
1326
|
+
const hostId = createHash("sha256").update(hostname() + userInfo().username).digest("hex");
|
|
1325
1327
|
const payload = JSON.stringify({
|
|
1326
1328
|
type: "vellum-daemon",
|
|
1327
1329
|
v: 4,
|
|
1330
|
+
id: hostId,
|
|
1328
1331
|
g: gatewayUrl,
|
|
1329
1332
|
pairingRequestId,
|
|
1330
1333
|
pairingSecret,
|
package/src/lib/local.ts
CHANGED
|
@@ -105,7 +105,7 @@ function resolveAssistantIndexPath(): string | undefined {
|
|
|
105
105
|
return undefined;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
async function waitForSocketFile(socketPath: string, timeoutMs =
|
|
108
|
+
async function waitForSocketFile(socketPath: string, timeoutMs = 60000): Promise<boolean> {
|
|
109
109
|
if (existsSync(socketPath)) return true;
|
|
110
110
|
|
|
111
111
|
const start = Date.now();
|
|
@@ -377,6 +377,7 @@ export async function startLocalDaemon(): Promise<void> {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
const child = spawn(daemonBinary, [], {
|
|
380
|
+
cwd: dirname(daemonBinary),
|
|
380
381
|
detached: true,
|
|
381
382
|
stdio: "ignore",
|
|
382
383
|
env: daemonEnv,
|
|
@@ -390,26 +391,27 @@ export async function startLocalDaemon(): Promise<void> {
|
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
// Wait for socket at ~/.vellum/vellum.sock (up to
|
|
394
|
-
|
|
394
|
+
// Wait for socket at ~/.vellum/vellum.sock (up to 60s — fresh installs
|
|
395
|
+
// may need 30-60s for Qdrant download, migrations, and first-time init)
|
|
396
|
+
let socketReady = await waitForSocketFile(socketFile, 60000);
|
|
395
397
|
|
|
396
398
|
// Dev fallback: if the bundled daemon did not create a socket in time,
|
|
397
399
|
// fall back to source daemon startup so local `./build.sh run` still works.
|
|
398
400
|
if (!socketReady) {
|
|
399
401
|
const assistantIndex = resolveAssistantIndexPath();
|
|
400
402
|
if (assistantIndex) {
|
|
401
|
-
console.log(" Bundled daemon socket not ready after
|
|
403
|
+
console.log(" Bundled daemon socket not ready after 60s — falling back to source daemon...");
|
|
402
404
|
// Kill the bundled daemon to avoid two processes competing for the same socket/port
|
|
403
405
|
await stopProcessByPidFile(pidFile, "bundled daemon", [socketFile]);
|
|
404
406
|
await startDaemonFromSource(assistantIndex);
|
|
405
|
-
socketReady = await waitForSocketFile(socketFile,
|
|
407
|
+
socketReady = await waitForSocketFile(socketFile, 60000);
|
|
406
408
|
}
|
|
407
409
|
}
|
|
408
410
|
|
|
409
411
|
if (socketReady) {
|
|
410
412
|
console.log(" Daemon socket ready\n");
|
|
411
413
|
} else {
|
|
412
|
-
console.log(" ⚠️ Daemon socket did not appear within
|
|
414
|
+
console.log(" ⚠️ Daemon socket did not appear within 60s — continuing anyway\n");
|
|
413
415
|
}
|
|
414
416
|
} else {
|
|
415
417
|
console.log("🔨 Starting local daemon...");
|
|
@@ -454,16 +456,16 @@ export async function startGateway(assistantId?: string): Promise<string> {
|
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
// If no token is available (first startup — daemon hasn't written it yet),
|
|
457
|
-
// poll for the file to appear.
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// never reloads, so we fail rather than silently disabling auth.
|
|
459
|
+
// poll for the file to appear. On fresh installs the daemon may take 60s+
|
|
460
|
+
// for Qdrant download, migrations, and first-time init. Starting the
|
|
461
|
+
// gateway without auth is a security risk since the config is loaded once
|
|
462
|
+
// at startup and never reloads, so we fail rather than silently disabling auth.
|
|
462
463
|
if (!runtimeProxyBearerToken) {
|
|
463
464
|
console.log(" Waiting for bearer token file...");
|
|
464
|
-
const maxWait =
|
|
465
|
+
const maxWait = 60000;
|
|
465
466
|
const pollInterval = 500;
|
|
466
467
|
const start = Date.now();
|
|
468
|
+
const pidFile = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum", "vellum.pid");
|
|
467
469
|
while (Date.now() - start < maxWait) {
|
|
468
470
|
await new Promise((r) => setTimeout(r, pollInterval));
|
|
469
471
|
try {
|
|
@@ -475,12 +477,19 @@ export async function startGateway(assistantId?: string): Promise<string> {
|
|
|
475
477
|
} catch {
|
|
476
478
|
// File still doesn't exist, keep polling.
|
|
477
479
|
}
|
|
480
|
+
// Check if the daemon process is still alive — no point waiting if it crashed
|
|
481
|
+
try {
|
|
482
|
+
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
483
|
+
if (pid) process.kill(pid, 0); // throws if process doesn't exist
|
|
484
|
+
} catch {
|
|
485
|
+
break; // daemon process is gone
|
|
486
|
+
}
|
|
478
487
|
}
|
|
479
488
|
}
|
|
480
489
|
|
|
481
490
|
if (!runtimeProxyBearerToken) {
|
|
482
491
|
throw new Error(
|
|
483
|
-
`Bearer token file not found at ${httpTokenPath} after
|
|
492
|
+
`Bearer token file not found at ${httpTokenPath} after 60s.\n` +
|
|
484
493
|
" The gateway cannot start without authentication — this would leave the proxy permanently unauthenticated.\n" +
|
|
485
494
|
" Ensure the daemon is running and has written the token file, or set VELLUM_HTTP_TOKEN_PATH to the correct path.",
|
|
486
495
|
);
|