@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 = 15000): Promise<boolean> {
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 15s)
394
- let socketReady = await waitForSocketFile(socketFile, 15000);
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 15s — falling back to source daemon...");
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, 15000);
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 15s — continuing anyway\n");
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. The daemon writes the token shortly after
458
- // startup, so we wait generously the daemon socket wait is 15s, and
459
- // the token may be written after the socket. Starting the gateway without
460
- // auth is a security risk since the config is loaded once at startup and
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 = 30000;
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 30s.\n` +
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
  );