@vellumai/cli 0.4.42 → 0.4.44

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/src/lib/gcp.ts CHANGED
@@ -3,7 +3,7 @@ import { unlinkSync, writeFileSync } from "fs";
3
3
  import { tmpdir, userInfo } from "os";
4
4
  import { join } from "path";
5
5
 
6
- import { saveAssistantEntry } from "./assistant-config";
6
+ import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
7
7
  import type { AssistantEntry } from "./assistant-config";
8
8
  import { FIREWALL_TAG, GATEWAY_PORT } from "./constants";
9
9
  import type { Species } from "./constants";
@@ -441,6 +441,45 @@ async function recoverFromCurlFailure(
441
441
  } catch {}
442
442
  }
443
443
 
444
+ async function fetchRemoteBearerToken(
445
+ instanceName: string,
446
+ project: string,
447
+ zone: string,
448
+ sshUser: string,
449
+ account?: string,
450
+ ): Promise<string | null> {
451
+ try {
452
+ const remoteCmd =
453
+ 'cat ~/.vellum.lock.json 2>/dev/null || cat ~/.vellum.lockfile.json 2>/dev/null || echo "{}"';
454
+ const args = [
455
+ "compute",
456
+ "ssh",
457
+ `${sshUser}@${instanceName}`,
458
+ `--project=${project}`,
459
+ `--zone=${zone}`,
460
+ "--quiet",
461
+ "--ssh-flag=-o StrictHostKeyChecking=no",
462
+ "--ssh-flag=-o UserKnownHostsFile=/dev/null",
463
+ "--ssh-flag=-o ConnectTimeout=10",
464
+ "--ssh-flag=-o LogLevel=ERROR",
465
+ `--command=${remoteCmd}`,
466
+ ];
467
+ if (account) args.push(`--account=${account}`);
468
+ const output = await execOutput("gcloud", args);
469
+ const data = JSON.parse(output.trim());
470
+ const assistants = data.assistants;
471
+ if (Array.isArray(assistants) && assistants.length > 0) {
472
+ const token = assistants[0].bearerToken;
473
+ if (typeof token === "string" && token) {
474
+ return token;
475
+ }
476
+ }
477
+ return null;
478
+ } catch {
479
+ return null;
480
+ }
481
+ }
482
+
444
483
  export async function hatchGcp(
445
484
  species: Species,
446
485
  detached: boolean,
@@ -599,6 +638,7 @@ export async function hatchGcp(
599
638
  hatchedAt: new Date().toISOString(),
600
639
  };
601
640
  saveAssistantEntry(gcpEntry);
641
+ setActiveAssistant(instanceName);
602
642
 
603
643
  if (detached) {
604
644
  console.log("\ud83d\ude80 Startup script is running on the instance...");
@@ -654,6 +694,18 @@ export async function hatchGcp(
654
694
  }
655
695
  }
656
696
 
697
+ const remoteBearerToken = await fetchRemoteBearerToken(
698
+ instanceName,
699
+ project,
700
+ zone,
701
+ sshUser,
702
+ account,
703
+ );
704
+ if (remoteBearerToken) {
705
+ gcpEntry.bearerToken = remoteBearerToken;
706
+ saveAssistantEntry(gcpEntry);
707
+ }
708
+
657
709
  console.log("Instance details:");
658
710
  console.log(` Name: ${instanceName}`);
659
711
  console.log(` Project: ${project}`);
@@ -0,0 +1,114 @@
1
+ import { readFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ import { DEFAULT_DAEMON_PORT } from "./constants.js";
6
+
7
+ /**
8
+ * Resolve the HTTP port for the daemon runtime server.
9
+ * Uses RUNTIME_HTTP_PORT env var, or the default (7821).
10
+ */
11
+ export function resolveDaemonPort(overridePort?: number): number {
12
+ if (overridePort !== undefined) return overridePort;
13
+ const envPort = process.env.RUNTIME_HTTP_PORT;
14
+ if (envPort) {
15
+ const parsed = parseInt(envPort, 10);
16
+ if (!isNaN(parsed)) return parsed;
17
+ }
18
+ return DEFAULT_DAEMON_PORT;
19
+ }
20
+
21
+ /**
22
+ * Build the base URL for the daemon HTTP server.
23
+ */
24
+ export function buildDaemonUrl(port: number): string {
25
+ return `http://127.0.0.1:${port}`;
26
+ }
27
+
28
+ /**
29
+ * Read the HTTP bearer token from `<vellumDir>/http-token`.
30
+ * Respects BASE_DATA_DIR for named instances.
31
+ * Returns undefined if the token file doesn't exist or is empty.
32
+ */
33
+ export function readHttpToken(instanceDir?: string): string | undefined {
34
+ const baseDataDir =
35
+ instanceDir ??
36
+ (process.env.BASE_DATA_DIR?.trim() || homedir());
37
+ const tokenPath = join(baseDataDir, ".vellum", "http-token");
38
+ try {
39
+ const token = readFileSync(tokenPath, "utf-8").trim();
40
+ return token || undefined;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Perform an HTTP health check against the daemon's `/healthz` endpoint.
48
+ * Returns true if the daemon responds with HTTP 200, false otherwise.
49
+ *
50
+ * This replaces the socket-based `isSocketResponsive()` check.
51
+ */
52
+ export async function httpHealthCheck(
53
+ port: number,
54
+ timeoutMs = 1500,
55
+ ): Promise<boolean> {
56
+ try {
57
+ const url = `${buildDaemonUrl(port)}/healthz`;
58
+ const response = await fetch(url, {
59
+ signal: AbortSignal.timeout(timeoutMs),
60
+ });
61
+ return response.ok;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Poll the daemon's `/healthz` endpoint until it responds with 200 or the
69
+ * timeout is reached. This replaces `waitForSocketFile()`.
70
+ *
71
+ * Returns true if the daemon became healthy within the timeout, false otherwise.
72
+ */
73
+ export async function waitForDaemonReady(
74
+ port: number,
75
+ timeoutMs = 60000,
76
+ ): Promise<boolean> {
77
+ const start = Date.now();
78
+ while (Date.now() - start < timeoutMs) {
79
+ if (await httpHealthCheck(port)) {
80
+ return true;
81
+ }
82
+ await new Promise((r) => setTimeout(r, 250));
83
+ }
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Make an authenticated HTTP request to the daemon.
89
+ *
90
+ * @param port - The daemon's HTTP port
91
+ * @param path - The request path (e.g. `/v1/sessions`)
92
+ * @param options - Fetch options (method, body, etc.)
93
+ * @param bearerToken - The bearer token for authentication
94
+ * @returns The fetch Response
95
+ */
96
+ export async function httpSend(
97
+ port: number,
98
+ path: string,
99
+ options: RequestInit = {},
100
+ bearerToken?: string,
101
+ ): Promise<Response> {
102
+ const url = `${buildDaemonUrl(port)}${path}`;
103
+ const headers: Record<string, string> = {
104
+ "Content-Type": "application/json",
105
+ ...(options.headers as Record<string, string> | undefined),
106
+ };
107
+ if (bearerToken) {
108
+ headers["Authorization"] = `Bearer ${bearerToken}`;
109
+ }
110
+ return fetch(url, {
111
+ ...options,
112
+ headers,
113
+ });
114
+ }