@vellumai/cli 0.1.5 → 0.1.7

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.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { randomBytes } from "crypto";
3
- import { existsSync, unlinkSync, writeFileSync } from "fs";
3
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
4
4
  import { createRequire } from "module";
5
5
  import { tmpdir, userInfo } from "os";
6
6
  import { dirname, join } from "path";
@@ -18,7 +18,7 @@ import {
18
18
  } from "../lib/constants";
19
19
  import type { RemoteHost, Species } from "../lib/constants";
20
20
  import type { FirewallRuleSpec } from "../lib/gcp";
21
- import { getActiveProject, instanceExists, syncFirewallRules } from "../lib/gcp";
21
+ import { fetchAndDisplayStartupLogs, getActiveProject, instanceExists, syncFirewallRules } from "../lib/gcp";
22
22
  import { buildInterfacesSeed } from "../lib/interfaces-seed";
23
23
  import { generateRandomSuffix } from "../lib/random-name";
24
24
  import { exec, execOutput } from "../lib/step-runner";
@@ -108,7 +108,7 @@ set -e
108
108
 
109
109
  ${timestampRedirect}
110
110
 
111
- trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE" > /var/log/startup-error; fi' EXIT
111
+ trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > /var/log/startup-error; echo "Last 20 log lines:" >> /var/log/startup-error; tail -20 /var/log/startup-script.log >> /var/log/startup-error 2>/dev/null || true; fi' EXIT
112
112
  ${userSetup}
113
113
  ANTHROPIC_API_KEY=${anthropicApiKey}
114
114
  GATEWAY_RUNTIME_PROXY_ENABLED=true
@@ -135,8 +135,11 @@ CONFIG_EOF
135
135
  ${ownershipFixup}
136
136
 
137
137
  export VELLUM_SSH_USER="\$SSH_USER"
138
+ echo "Downloading install script from ${platformUrl}/install.sh..."
138
139
  curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
140
+ echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
139
141
  chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
142
+ echo "Running install script..."
140
143
  source ${INSTALL_SCRIPT_REMOTE_PATH}
141
144
  `;
142
145
  }
@@ -196,6 +199,7 @@ export interface PollResult {
196
199
  lastLine: string | null;
197
200
  done: boolean;
198
201
  failed: boolean;
202
+ errorContent: string;
199
203
  }
200
204
 
201
205
  async function pollInstance(
@@ -227,7 +231,7 @@ async function pollInstance(
227
231
  const output = await execOutput("gcloud", args);
228
232
  const sepIdx = output.indexOf("===HATCH_SEP===");
229
233
  if (sepIdx === -1) {
230
- return { lastLine: output.trim() || null, done: false, failed: false };
234
+ return { lastLine: output.trim() || null, done: false, failed: false, errorContent: "" };
231
235
  }
232
236
  const errIdx = output.indexOf("===HATCH_ERR===");
233
237
  const lastLine = output.substring(0, sepIdx).trim() || null;
@@ -237,9 +241,9 @@ async function pollInstance(
237
241
  errIdx === -1 ? "" : output.substring(errIdx + "===HATCH_ERR===".length).trim();
238
242
  const done = lastLine !== null && status !== "active" && status !== "activating";
239
243
  const failed = errorContent.length > 0 || status === "failed";
240
- return { lastLine, done, failed };
244
+ return { lastLine, done, failed, errorContent };
241
245
  } catch {
242
- return { lastLine: null, done: false, failed: false };
246
+ return { lastLine: null, done: false, failed: false, errorContent: "" };
243
247
  }
244
248
  }
245
249
 
@@ -326,17 +330,23 @@ async function recoverFromCurlFailure(
326
330
  await exec("gcloud", sshArgs);
327
331
  }
328
332
 
333
+ export interface WatchHatchingResult {
334
+ success: boolean;
335
+ errorContent: string;
336
+ }
337
+
329
338
  export async function watchHatching(
330
339
  pollFn: () => Promise<PollResult>,
331
340
  instanceName: string,
332
341
  startTime: number,
333
342
  species: Species,
334
- ): Promise<boolean> {
343
+ ): Promise<WatchHatchingResult> {
335
344
  let spinnerIdx = 0;
336
345
  let lastLogLine: string | null = null;
337
346
  let linesDrawn = 0;
338
347
  let finished = false;
339
348
  let failed = false;
349
+ let lastErrorContent = "";
340
350
  let pollInFlight = false;
341
351
  let nextPollAt = Date.now() + 15000;
342
352
 
@@ -386,6 +396,9 @@ export async function watchHatching(
386
396
  if (result.lastLine) {
387
397
  lastLogLine = result.lastLine;
388
398
  }
399
+ if (result.errorContent) {
400
+ lastErrorContent = result.errorContent;
401
+ }
389
402
  if (result.done) {
390
403
  finished = true;
391
404
  failed = result.failed;
@@ -396,12 +409,12 @@ export async function watchHatching(
396
409
  }
397
410
  }
398
411
 
399
- return new Promise<boolean>((resolve) => {
412
+ return new Promise<WatchHatchingResult>((resolve) => {
400
413
  const interval = setInterval(() => {
401
414
  if (finished) {
402
415
  draw();
403
416
  clearInterval(interval);
404
- resolve(!failed);
417
+ resolve({ success: !failed, errorContent: lastErrorContent });
405
418
  return;
406
419
  }
407
420
 
@@ -412,7 +425,7 @@ export async function watchHatching(
412
425
  console.log(` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`);
413
426
  console.log(` Monitor with: vel logs ${instanceName}`);
414
427
  console.log("");
415
- resolve(true);
428
+ resolve({ success: true, errorContent: lastErrorContent });
416
429
  return;
417
430
  }
418
431
 
@@ -574,25 +587,32 @@ async function hatchGcp(
574
587
  console.log(" Press Ctrl+C to detach (instance will keep running)");
575
588
  console.log("");
576
589
 
577
- const success = await watchHatching(
590
+ const result = await watchHatching(
578
591
  () => pollInstance(instanceName, project, zone, account),
579
592
  instanceName,
580
593
  startTime,
581
594
  species,
582
595
  );
583
596
 
584
- if (!success) {
597
+ if (!result.success) {
598
+ console.log("");
599
+ if (result.errorContent) {
600
+ console.log("📋 Startup error:");
601
+ console.log(` ${result.errorContent}`);
602
+ console.log("");
603
+ }
604
+
605
+ await fetchAndDisplayStartupLogs(instanceName, project, zone, account);
606
+
585
607
  if (
586
608
  species === "vellum" &&
587
609
  (await checkCurlFailure(instanceName, project, zone, account))
588
610
  ) {
589
- console.log("");
590
611
  const installScriptUrl = `${process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai"}/install.sh`;
591
612
  console.log(`🔄 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`);
592
613
  await recoverFromCurlFailure(instanceName, project, zone, sshUser, account);
593
614
  console.log("✅ Recovery successful!");
594
615
  } else {
595
- console.log("");
596
616
  process.exit(1);
597
617
  }
598
618
  }
@@ -769,8 +789,17 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
769
789
  console.warn("⚠️ Daemon socket did not appear within 10s — continuing anyway");
770
790
  }
771
791
  } else {
772
- const assistantDir = join(import.meta.dir, "..", "..", "..", "assistant");
773
- const assistantIndex = join(assistantDir, "src", "index.ts");
792
+ const sourceTreeIndex = join(import.meta.dir, "..", "..", "..", "assistant", "src", "index.ts");
793
+ let assistantIndex = sourceTreeIndex;
794
+
795
+ if (!existsSync(assistantIndex)) {
796
+ try {
797
+ const vellumPkgPath = _require.resolve("vellum/package.json");
798
+ assistantIndex = join(dirname(vellumPkgPath), "src", "index.ts");
799
+ } catch {
800
+ // resolve failed, will fall through to existsSync check below
801
+ }
802
+ }
774
803
 
775
804
  if (!existsSync(assistantIndex)) {
776
805
  throw new Error(
@@ -830,7 +859,20 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
830
859
  console.log("");
831
860
  }
832
861
 
862
+ function getCliVersion(): string {
863
+ try {
864
+ const pkgPath = join(import.meta.dir, "..", "..", "package.json");
865
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
866
+ return pkg.version ?? "unknown";
867
+ } catch {
868
+ return "unknown";
869
+ }
870
+ }
871
+
833
872
  export async function hatch(): Promise<void> {
873
+ const cliVersion = getCliVersion();
874
+ console.log(`@vellumai/cli v${cliVersion}`);
875
+
834
876
  const { species, detached, name, remote } = parseArgs();
835
877
 
836
878
  if (remote === "local") {
package/src/lib/aws.ts CHANGED
@@ -338,7 +338,7 @@ async function pollAwsInstance(
338
338
  const output = await awsSshExec(ip, keyPath, remoteCmd);
339
339
  const sepIdx = output.indexOf("===HATCH_SEP===");
340
340
  if (sepIdx === -1) {
341
- return { lastLine: output.trim() || null, done: false, failed: false };
341
+ return { lastLine: output.trim() || null, done: false, failed: false, errorContent: "" };
342
342
  }
343
343
  const errIdx = output.indexOf("===HATCH_ERR===");
344
344
  const lastLine = output.substring(0, sepIdx).trim() || null;
@@ -348,9 +348,9 @@ async function pollAwsInstance(
348
348
  errIdx === -1 ? "" : output.substring(errIdx + "===HATCH_ERR===".length).trim();
349
349
  const done = lastLine !== null && status !== "running" && status !== "pending";
350
350
  const failed = errorContent.length > 0 || status === "error";
351
- return { lastLine, done, failed };
351
+ return { lastLine, done, failed, errorContent };
352
352
  } catch {
353
- return { lastLine: null, done: false, failed: false };
353
+ return { lastLine: null, done: false, failed: false, errorContent: "" };
354
354
  }
355
355
  }
356
356
 
@@ -488,15 +488,20 @@ export async function hatchAws(
488
488
 
489
489
  if (externalIp) {
490
490
  const ip = externalIp;
491
- const success = await watchHatching(
491
+ const result = await watchHatching(
492
492
  () => pollAwsInstance(ip, keyPath),
493
493
  instanceName,
494
494
  startTime,
495
495
  species,
496
496
  );
497
497
 
498
- if (!success) {
498
+ if (!result.success) {
499
499
  console.log("");
500
+ if (result.errorContent) {
501
+ console.log("📋 Startup error:");
502
+ console.log(` ${result.errorContent}`);
503
+ console.log("");
504
+ }
500
505
  process.exit(1);
501
506
  }
502
507
  } else {
package/src/lib/gcp.ts CHANGED
@@ -274,6 +274,45 @@ export async function sshCommand(
274
274
  ]);
275
275
  }
276
276
 
277
+ export async function fetchAndDisplayStartupLogs(
278
+ instanceName: string,
279
+ project: string,
280
+ zone: string,
281
+ account?: string,
282
+ ): Promise<void> {
283
+ try {
284
+ const remoteCmd =
285
+ 'echo "=== Last 50 lines of /var/log/startup-script.log ==="; ' +
286
+ "tail -50 /var/log/startup-script.log 2>/dev/null || echo '(no startup log found)'; " +
287
+ 'echo ""; ' +
288
+ 'echo "=== /var/log/startup-error ==="; ' +
289
+ "cat /var/log/startup-error 2>/dev/null || echo '(no error file found)'";
290
+ const args = [
291
+ "compute",
292
+ "ssh",
293
+ instanceName,
294
+ `--project=${project}`,
295
+ `--zone=${zone}`,
296
+ "--quiet",
297
+ "--ssh-flag=-o StrictHostKeyChecking=no",
298
+ "--ssh-flag=-o UserKnownHostsFile=/dev/null",
299
+ "--ssh-flag=-o ConnectTimeout=10",
300
+ "--ssh-flag=-o LogLevel=ERROR",
301
+ `--command=${remoteCmd}`,
302
+ ];
303
+ if (account) args.push(`--account=${account}`);
304
+ const output = await execOutput("gcloud", args);
305
+ console.log("📋 Startup logs from instance:");
306
+ for (const line of output.split("\n")) {
307
+ console.log(` ${line}`);
308
+ }
309
+ console.log("");
310
+ } catch {
311
+ console.log("⚠️ Could not retrieve startup logs from instance");
312
+ console.log("");
313
+ }
314
+ }
315
+
277
316
  export async function retireInstance(
278
317
  name: string,
279
318
  project: string,