@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/AGENTS.md +5 -10
- package/package.json +1 -1
- package/src/__tests__/coverage.test.ts +6 -0
- package/src/commands/client.ts +2 -1
- package/src/commands/hatch.ts +42 -31
- package/src/commands/pair.ts +17 -1
- package/src/commands/ps.ts +88 -2
- package/src/commands/upgrade.ts +366 -0
- package/src/index.ts +6 -1
- package/src/lib/assistant-config.ts +2 -0
- package/src/lib/aws.ts +1 -3
- package/src/lib/docker.ts +458 -307
- package/src/lib/gcp.ts +1 -3
- package/src/lib/guardian-token.ts +17 -0
- package/src/lib/ngrok.ts +40 -23
- package/src/lib/process.ts +1 -1
package/src/lib/gcp.ts
CHANGED
|
@@ -646,9 +646,7 @@ export async function hatchGcp(
|
|
|
646
646
|
}
|
|
647
647
|
|
|
648
648
|
try {
|
|
649
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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(
|
package/src/lib/process.ts
CHANGED
|
@@ -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",
|