@vellumai/cli 0.4.56 → 0.5.0

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
@@ -646,9 +646,7 @@ export async function hatchGcp(
646
646
  }
647
647
 
648
648
  try {
649
- const tokenData = await leaseGuardianToken(runtimeUrl, instanceName);
650
- gcpEntry.bearerToken = tokenData.accessToken;
651
- saveAssistantEntry(gcpEntry);
649
+ await leaseGuardianToken(runtimeUrl, instanceName);
652
650
  } catch (err) {
653
651
  console.warn(
654
652
  `\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
@@ -120,6 +120,23 @@ export function computeDeviceId(): string {
120
120
  return getOrCreatePersistedDeviceId();
121
121
  }
122
122
 
123
+ /**
124
+ * Read a previously persisted guardian token for the given assistant.
125
+ * Returns the parsed token data, or null if the file does not exist or is
126
+ * unreadable.
127
+ */
128
+ export function loadGuardianToken(
129
+ assistantId: string,
130
+ ): GuardianTokenData | null {
131
+ const tokenPath = getGuardianTokenPath(assistantId);
132
+ try {
133
+ const raw = readFileSync(tokenPath, "utf-8");
134
+ return JSON.parse(raw) as GuardianTokenData;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
123
140
  export function saveGuardianToken(
124
141
  assistantId: string,
125
142
  data: GuardianTokenData,
package/src/lib/ngrok.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import { execFileSync, spawn, type ChildProcess } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ } from "node:fs";
3
9
  import { homedir } from "node:os";
4
10
  import { dirname, join } from "node:path";
5
11
 
@@ -111,12 +117,29 @@ export async function findExistingTunnel(
111
117
 
112
118
  /**
113
119
  * Start an ngrok process tunneling HTTP traffic to the given local port.
120
+ *
121
+ * When `logFilePath` is provided, stdout/stderr are redirected to that file
122
+ * instead of being piped. This avoids keeping pipe handles open in the
123
+ * parent process — which would either prevent the CLI from exiting (if
124
+ * handles are left open) or send SIGPIPE to ngrok (if destroyed).
125
+ *
114
126
  * Returns the spawned child process.
115
127
  */
116
- export function startNgrokProcess(targetPort: number): ChildProcess {
128
+ export function startNgrokProcess(
129
+ targetPort: number,
130
+ logFilePath?: string,
131
+ ): ChildProcess {
132
+ let stdio: ("ignore" | "pipe" | number)[] = ["ignore", "pipe", "pipe"];
133
+ if (logFilePath) {
134
+ const dir = dirname(logFilePath);
135
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
136
+ const fd = openSync(logFilePath, "a");
137
+ stdio = ["ignore", fd, fd];
138
+ }
139
+
117
140
  const child = spawn("ngrok", ["http", String(targetPort), "--log=stdout"], {
118
141
  detached: true,
119
- stdio: ["ignore", "pipe", "pipe"],
142
+ stdio,
120
143
  });
121
144
  return child;
122
145
  }
@@ -170,14 +193,16 @@ function clearIngressUrl(): void {
170
193
  }
171
194
 
172
195
  /**
173
- * Check whether any webhook-based integrations (e.g. Telegram) are configured
174
- * that require a public ingress URL.
196
+ * Check whether any webhook-based integrations (e.g. Telegram, Twilio) are
197
+ * configured that require a public ingress URL.
175
198
  */
176
199
  function hasWebhookIntegrationsConfigured(): boolean {
177
200
  try {
178
201
  const config = loadRawConfig();
179
202
  const telegram = config.telegram as Record<string, unknown> | undefined;
180
203
  if (telegram?.botUsername) return true;
204
+ const twilio = config.twilio as Record<string, unknown> | undefined;
205
+ if (twilio?.accountSid || twilio?.phoneNumber) return true;
181
206
  return false;
182
207
  } catch {
183
208
  return false;
@@ -225,31 +250,23 @@ export async function maybeStartNgrokTunnel(
225
250
  }
226
251
 
227
252
  console.log(` Starting ngrok tunnel for webhook integrations...`);
228
- const ngrokProcess = startNgrokProcess(targetPort);
229
253
 
230
- // Pipe output for debugging but don't block on it
231
- ngrokProcess.stdout?.on("data", (data: Buffer) => {
232
- const line = data.toString().trim();
233
- if (line) console.log(`[ngrok] ${line}`);
234
- });
235
- ngrokProcess.stderr?.on("data", (data: Buffer) => {
236
- const line = data.toString().trim();
237
- if (line) console.error(`[ngrok] ${line}`);
238
- });
254
+ // Spawn ngrok with stdout/stderr redirected to a log file instead of pipes.
255
+ // This avoids two problems that occur with piped stdio:
256
+ // 1. If pipe handles are left open, the CLI process hangs after hatch/wake.
257
+ // 2. If pipe handles are destroyed, SIGPIPE kills ngrok on its next write.
258
+ // Writing to a log file sidesteps both issues — the file descriptor is
259
+ // inherited by the detached ngrok process and remains valid after CLI exit.
260
+ const root = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
261
+ const ngrokLogPath = join(root, "workspace", "data", "logs", "ngrok.log");
262
+ const ngrokProcess = startNgrokProcess(targetPort, ngrokLogPath);
263
+ ngrokProcess.unref();
239
264
 
240
265
  try {
241
266
  const publicUrl = await waitForNgrokUrl();
242
267
  saveIngressUrl(publicUrl);
243
268
  console.log(` Tunnel established: ${publicUrl}`);
244
269
 
245
- // Detach the ngrok process so the CLI (hatch/wake) can exit without
246
- // keeping it alive. Remove stdout/stderr listeners and unref all handles.
247
- ngrokProcess.stdout?.removeAllListeners("data");
248
- ngrokProcess.stderr?.removeAllListeners("data");
249
- ngrokProcess.stdout?.destroy();
250
- ngrokProcess.stderr?.destroy();
251
- ngrokProcess.unref();
252
-
253
270
  return ngrokProcess;
254
271
  } catch {
255
272
  console.warn(
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, unlinkSync } from "fs";
6
6
  * command line via `ps`. Prevents killing unrelated processes when a PID file
7
7
  * is stale and the OS has reused the PID.
8
8
  */
9
- function isVellumProcess(pid: number): boolean {
9
+ export function isVellumProcess(pid: number): boolean {
10
10
  try {
11
11
  const output = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
12
12
  encoding: "utf-8",