@vellumai/cli 0.4.4 → 0.4.6

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.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -24,11 +24,33 @@ ensure_git() {
24
24
 
25
25
  info "Installing git..."
26
26
  if [ "$(uname -s)" = "Darwin" ]; then
27
- if command -v brew >/dev/null 2>&1; then
28
- brew install git
29
- else
30
- error "git is required. Install Homebrew (https://brew.sh) then run: brew install git"
31
- exit 1
27
+ # On macOS, the standard way to get git is via Xcode Command Line Tools.
28
+ # Try installing CLT first before falling back to Homebrew.
29
+ if ! xcode-select -p >/dev/null 2>&1; then
30
+ info "Installing Xcode Command Line Tools (includes git)..."
31
+ xcode-select --install 2>/dev/null || true
32
+ info "Please follow the on-screen dialog to install. Waiting..."
33
+ local waited=0
34
+ while ! xcode-select -p >/dev/null 2>&1; do
35
+ sleep 5
36
+ waited=$((waited + 5))
37
+ if [ "$waited" -ge 600 ]; then
38
+ error "Timed out waiting for Xcode Command Line Tools. Please install manually and re-run."
39
+ exit 1
40
+ fi
41
+ done
42
+ hash -r 2>/dev/null || true
43
+ fi
44
+
45
+ # If git still doesn't work after CLT, try Homebrew as a fallback.
46
+ if ! git --version >/dev/null 2>&1; then
47
+ hash -r 2>/dev/null || true
48
+ if command -v brew >/dev/null 2>&1; then
49
+ brew install git
50
+ else
51
+ error "git is still not available. Please install manually: xcode-select --install"
52
+ exit 1
53
+ fi
32
54
  fi
33
55
  elif command -v apt-get >/dev/null 2>&1; then
34
56
  sudo apt-get update -qq && sudo apt-get install -y -qq git
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomBytes, randomUUID } from "crypto";
2
- import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync } from "fs";
2
+ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
3
3
  import { homedir, hostname, userInfo } from "os";
4
4
  import { join } from "path";
5
5
 
6
+ import QRCode from "qrcode";
6
7
  import qrcode from "qrcode-terminal";
7
8
 
8
9
  // Direct import — bun embeds this at compile time so it works in compiled binaries.
@@ -508,7 +509,7 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
508
509
  const pairingRequestId = randomUUID();
509
510
  const pairingSecret = randomBytes(32).toString("hex");
510
511
 
511
- const registerRes = await fetch(`${runtimeUrl}/v1/pairing/register`, {
512
+ const registerRes = await fetch(`${runtimeUrl}/pairing/register`, {
512
513
  method: "POST",
513
514
  headers: {
514
515
  "Content-Type": "application/json",
@@ -539,6 +540,20 @@ async function displayPairingQRCode(runtimeUrl: string, bearerToken: string | un
539
540
  });
540
541
  });
541
542
 
543
+ // Save QR code as PNG to a well-known location so it can be retrieved
544
+ // (e.g. via SCP) for pairing through the Desktop app.
545
+ const qrDir = join(homedir(), ".vellum", "pairing-qr");
546
+ mkdirSync(qrDir, { recursive: true });
547
+ const qrPngPath = join(qrDir, "initial.png");
548
+ try {
549
+ const pngBuffer = await QRCode.toBuffer(payload, { type: "png", width: 512 });
550
+ writeFileSync(qrPngPath, pngBuffer);
551
+ console.log(`QR code PNG saved to ${qrPngPath}\n`);
552
+ } catch (pngErr) {
553
+ const pngReason = pngErr instanceof Error ? pngErr.message : String(pngErr);
554
+ console.warn(`\u26A0 Could not save QR code PNG: ${pngReason}\n`);
555
+ }
556
+
542
557
  console.log("Scan this QR code with the Vellum iOS app to pair:\n");
543
558
  console.log(qrString);
544
559
  console.log("This pairing request expires in 5 minutes.");
@@ -1347,8 +1347,8 @@ function ChatApp({
1347
1347
  const pairingSecret = randomBytes(32).toString("hex");
1348
1348
  const gatewayUrl = runtimeUrl;
1349
1349
 
1350
- // Call /v1/pairing/register directly (not under /v1/assistants/:id/)
1351
- const registerUrl = `${runtimeUrl}/v1/pairing/register`;
1350
+ // Call /pairing/register on the gateway (dedicated pairing proxy route)
1351
+ const registerUrl = `${runtimeUrl}/pairing/register`;
1352
1352
  const registerRes = await fetch(registerUrl, {
1353
1353
  method: "POST",
1354
1354
  headers: {
package/src/lib/local.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFileSync, spawn } from "child_process";
2
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "fs";
3
3
  import { createRequire } from "module";
4
4
  import { createConnection } from "net";
5
5
  import { homedir } from "os";
@@ -11,6 +11,33 @@ import { stopProcessByPidFile } from "./process.js";
11
11
 
12
12
  const _require = createRequire(import.meta.url);
13
13
 
14
+ /** Returns the XDG-compatible log directory for Vellum hatch logs. */
15
+ function getHatchLogDir(): string {
16
+ const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
17
+ return join(configHome, "vellum", "logs");
18
+ }
19
+
20
+ /** Open (or create) a log file in append mode, returning the file descriptor.
21
+ * Creates the parent directory if it doesn't exist. Returns "ignore" if the
22
+ * directory or file cannot be created (permissions, read-only filesystem, etc.)
23
+ * so that spawn falls back to discarding output instead of aborting startup. */
24
+ function openHatchLogFile(name: string): number | "ignore" {
25
+ try {
26
+ const dir = getHatchLogDir();
27
+ mkdirSync(dir, { recursive: true });
28
+ return openSync(join(dir, name), "a");
29
+ } catch {
30
+ return "ignore";
31
+ }
32
+ }
33
+
34
+ /** Close a file descriptor returned by openHatchLogFile (no-op for "ignore"). */
35
+ function closeHatchLogFile(fd: number | "ignore"): void {
36
+ if (typeof fd === "number") {
37
+ try { closeSync(fd); } catch { /* best-effort */ }
38
+ }
39
+ }
40
+
14
41
  function isAssistantSourceDir(dir: string): boolean {
15
42
  const pkgPath = join(dir, "package.json");
16
43
  if (!existsSync(pkgPath) || !existsSync(join(dir, "src", "index.ts"))) return false;
@@ -376,18 +403,25 @@ export async function startLocalDaemon(): Promise<void> {
376
403
  }
377
404
  }
378
405
 
379
- const child = spawn(daemonBinary, [], {
380
- cwd: dirname(daemonBinary),
381
- detached: true,
382
- stdio: "ignore",
383
- env: daemonEnv,
384
- });
385
- child.unref();
406
+ const daemonLogFd = openHatchLogFile("daemon.log");
407
+ let daemonPid: number | undefined;
408
+ try {
409
+ const child = spawn(daemonBinary, [], {
410
+ cwd: dirname(daemonBinary),
411
+ detached: true,
412
+ stdio: ["ignore", daemonLogFd, daemonLogFd],
413
+ env: daemonEnv,
414
+ });
415
+ child.unref();
416
+ daemonPid = child.pid;
417
+ } finally {
418
+ closeHatchLogFile(daemonLogFd);
419
+ }
386
420
 
387
421
  // Write PID file immediately so the health monitor can find the process
388
422
  // and concurrent hatch() calls see it as alive.
389
- if (child.pid) {
390
- writeFileSync(pidFile, String(child.pid), "utf-8");
423
+ if (daemonPid) {
424
+ writeFileSync(pidFile, String(daemonPid), "utf-8");
391
425
  }
392
426
  }
393
427
 
@@ -534,20 +568,30 @@ export async function startGateway(assistantId?: string): Promise<string> {
534
568
  );
535
569
  }
536
570
 
537
- gateway = spawn(gatewayBinary, [], {
538
- detached: true,
539
- stdio: "ignore",
540
- env: gatewayEnv,
541
- });
571
+ const gatewayLogFd = openHatchLogFile("gateway.log");
572
+ try {
573
+ gateway = spawn(gatewayBinary, [], {
574
+ detached: true,
575
+ stdio: ["ignore", gatewayLogFd, gatewayLogFd],
576
+ env: gatewayEnv,
577
+ });
578
+ } finally {
579
+ closeHatchLogFile(gatewayLogFd);
580
+ }
542
581
  } else {
543
582
  // Source tree / bunx: resolve the gateway source directory and run via bun.
544
583
  const gatewayDir = resolveGatewayDir();
545
- gateway = spawn("bun", ["run", "src/index.ts"], {
546
- cwd: gatewayDir,
547
- detached: true,
548
- stdio: "ignore",
549
- env: gatewayEnv,
550
- });
584
+ const gwLogFd = openHatchLogFile("gateway.log");
585
+ try {
586
+ gateway = spawn("bun", ["run", "src/index.ts"], {
587
+ cwd: gatewayDir,
588
+ detached: true,
589
+ stdio: ["ignore", gwLogFd, gwLogFd],
590
+ env: gatewayEnv,
591
+ });
592
+ } finally {
593
+ closeHatchLogFile(gwLogFd);
594
+ }
551
595
  }
552
596
 
553
597
  gateway.unref();