@vellumai/cli 0.3.21 → 0.3.23

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.21",
3
+ "version": "0.3.23",
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();
@@ -390,26 +390,27 @@ export async function startLocalDaemon(): Promise<void> {
390
390
  }
391
391
  }
392
392
 
393
- // Wait for socket at ~/.vellum/vellum.sock (up to 15s)
394
- let socketReady = await waitForSocketFile(socketFile, 15000);
393
+ // Wait for socket at ~/.vellum/vellum.sock (up to 60s — fresh installs
394
+ // may need 30-60s for Qdrant download, migrations, and first-time init)
395
+ let socketReady = await waitForSocketFile(socketFile, 60000);
395
396
 
396
397
  // Dev fallback: if the bundled daemon did not create a socket in time,
397
398
  // fall back to source daemon startup so local `./build.sh run` still works.
398
399
  if (!socketReady) {
399
400
  const assistantIndex = resolveAssistantIndexPath();
400
401
  if (assistantIndex) {
401
- console.log(" Bundled daemon socket not ready after 15s — falling back to source daemon...");
402
+ console.log(" Bundled daemon socket not ready after 60s — falling back to source daemon...");
402
403
  // Kill the bundled daemon to avoid two processes competing for the same socket/port
403
404
  await stopProcessByPidFile(pidFile, "bundled daemon", [socketFile]);
404
405
  await startDaemonFromSource(assistantIndex);
405
- socketReady = await waitForSocketFile(socketFile, 15000);
406
+ socketReady = await waitForSocketFile(socketFile, 60000);
406
407
  }
407
408
  }
408
409
 
409
410
  if (socketReady) {
410
411
  console.log(" Daemon socket ready\n");
411
412
  } else {
412
- console.log(" ⚠️ Daemon socket did not appear within 15s — continuing anyway\n");
413
+ console.log(" ⚠️ Daemon socket did not appear within 60s — continuing anyway\n");
413
414
  }
414
415
  } else {
415
416
  console.log("🔨 Starting local daemon...");
@@ -454,16 +455,16 @@ export async function startGateway(assistantId?: string): Promise<string> {
454
455
  }
455
456
 
456
457
  // 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.
458
+ // poll for the file to appear. On fresh installs the daemon may take 60s+
459
+ // for Qdrant download, migrations, and first-time init. Starting the
460
+ // gateway without auth is a security risk since the config is loaded once
461
+ // at startup and never reloads, so we fail rather than silently disabling auth.
462
462
  if (!runtimeProxyBearerToken) {
463
463
  console.log(" Waiting for bearer token file...");
464
- const maxWait = 30000;
464
+ const maxWait = 60000;
465
465
  const pollInterval = 500;
466
466
  const start = Date.now();
467
+ const pidFile = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum", "vellum.pid");
467
468
  while (Date.now() - start < maxWait) {
468
469
  await new Promise((r) => setTimeout(r, pollInterval));
469
470
  try {
@@ -475,12 +476,19 @@ export async function startGateway(assistantId?: string): Promise<string> {
475
476
  } catch {
476
477
  // File still doesn't exist, keep polling.
477
478
  }
479
+ // Check if the daemon process is still alive — no point waiting if it crashed
480
+ try {
481
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
482
+ if (pid) process.kill(pid, 0); // throws if process doesn't exist
483
+ } catch {
484
+ break; // daemon process is gone
485
+ }
478
486
  }
479
487
  }
480
488
 
481
489
  if (!runtimeProxyBearerToken) {
482
490
  throw new Error(
483
- `Bearer token file not found at ${httpTokenPath} after 30s.\n` +
491
+ `Bearer token file not found at ${httpTokenPath} after 60s.\n` +
484
492
  " The gateway cannot start without authentication — this would leave the proxy permanently unauthenticated.\n" +
485
493
  " Ensure the daemon is running and has written the token file, or set VELLUM_HTTP_TOKEN_PATH to the correct path.",
486
494
  );