@vellumai/cli 0.1.4 → 0.1.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.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,15 +1,15 @@
1
1
  import { GATEWAY_PORT } from "../lib/constants";
2
2
  import { buildOpenclawRuntimeServer } from "../lib/openclaw-runtime-server";
3
3
 
4
- export function buildOpenclawStartupScript(
4
+ export async function buildOpenclawStartupScript(
5
5
  bearerToken: string,
6
6
  sshUser: string,
7
7
  anthropicApiKey: string,
8
8
  timestampRedirect: string,
9
9
  userSetup: string,
10
10
  ownershipFixup: string,
11
- ): string {
12
- const runtimeServer = buildOpenclawRuntimeServer();
11
+ ): Promise<string> {
12
+ const runtimeServer = await buildOpenclawRuntimeServer();
13
13
 
14
14
  return `#!/bin/bash
15
15
  set -e
@@ -1,6 +1,7 @@
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
+ import { createRequire } from "module";
4
5
  import { tmpdir, userInfo } from "os";
5
6
  import { dirname, join } from "path";
6
7
 
@@ -17,11 +18,13 @@ import {
17
18
  } from "../lib/constants";
18
19
  import type { RemoteHost, Species } from "../lib/constants";
19
20
  import type { FirewallRuleSpec } from "../lib/gcp";
20
- import { getActiveProject, instanceExists, syncFirewallRules } from "../lib/gcp";
21
+ import { fetchAndDisplayStartupLogs, getActiveProject, instanceExists, syncFirewallRules } from "../lib/gcp";
21
22
  import { buildInterfacesSeed } from "../lib/interfaces-seed";
22
23
  import { generateRandomSuffix } from "../lib/random-name";
23
24
  import { exec, execOutput } from "../lib/step-runner";
24
25
 
26
+ const _require = createRequire(import.meta.url);
27
+
25
28
  const INSTALL_SCRIPT_REMOTE_PATH = "/tmp/vellum-install.sh";
26
29
  const INSTALL_SCRIPT_PATH = join(import.meta.dir, "..", "adapters", "install.sh");
27
30
  const MACHINE_TYPE = "e2-standard-4"; // 4 vCPUs, 16 GB memory
@@ -76,19 +79,19 @@ chown -R "$SSH_USER:$SSH_USER" "$SSH_USER_HOME" 2>/dev/null || true
76
79
  `;
77
80
  }
78
81
 
79
- export function buildStartupScript(
82
+ export async function buildStartupScript(
80
83
  species: Species,
81
84
  bearerToken: string,
82
85
  sshUser: string,
83
86
  anthropicApiKey: string,
84
- ): string {
87
+ ): Promise<string> {
85
88
  const platformUrl = process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai";
86
89
  const timestampRedirect = buildTimestampRedirect();
87
90
  const userSetup = buildUserSetup(sshUser);
88
91
  const ownershipFixup = buildOwnershipFixup();
89
92
 
90
93
  if (species === "openclaw") {
91
- return buildOpenclawStartupScript(
94
+ return await buildOpenclawStartupScript(
92
95
  bearerToken,
93
96
  sshUser,
94
97
  anthropicApiKey,
@@ -105,7 +108,7 @@ set -e
105
108
 
106
109
  ${timestampRedirect}
107
110
 
108
- 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
109
112
  ${userSetup}
110
113
  ANTHROPIC_API_KEY=${anthropicApiKey}
111
114
  GATEWAY_RUNTIME_PROXY_ENABLED=true
@@ -117,6 +120,7 @@ ANTHROPIC_API_KEY=\$ANTHROPIC_API_KEY
117
120
  GATEWAY_RUNTIME_PROXY_ENABLED=\$GATEWAY_RUNTIME_PROXY_ENABLED
118
121
  RUNTIME_PROXY_BEARER_TOKEN=\$RUNTIME_PROXY_BEARER_TOKEN
119
122
  INTERFACES_SEED_DIR=\$INTERFACES_SEED_DIR
123
+ RUNTIME_HTTP_PORT=7821
120
124
  DOTENV_EOF
121
125
 
122
126
  mkdir -p "\$HOME/.vellum/workspace"
@@ -131,8 +135,11 @@ CONFIG_EOF
131
135
  ${ownershipFixup}
132
136
 
133
137
  export VELLUM_SSH_USER="\$SSH_USER"
138
+ echo "Downloading install script from ${platformUrl}/install.sh..."
134
139
  curl -fsSL ${platformUrl}/install.sh -o ${INSTALL_SCRIPT_REMOTE_PATH}
140
+ echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes)"
135
141
  chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
142
+ echo "Running install script..."
136
143
  source ${INSTALL_SCRIPT_REMOTE_PATH}
137
144
  `;
138
145
  }
@@ -192,6 +199,7 @@ export interface PollResult {
192
199
  lastLine: string | null;
193
200
  done: boolean;
194
201
  failed: boolean;
202
+ errorContent: string;
195
203
  }
196
204
 
197
205
  async function pollInstance(
@@ -223,7 +231,7 @@ async function pollInstance(
223
231
  const output = await execOutput("gcloud", args);
224
232
  const sepIdx = output.indexOf("===HATCH_SEP===");
225
233
  if (sepIdx === -1) {
226
- return { lastLine: output.trim() || null, done: false, failed: false };
234
+ return { lastLine: output.trim() || null, done: false, failed: false, errorContent: "" };
227
235
  }
228
236
  const errIdx = output.indexOf("===HATCH_ERR===");
229
237
  const lastLine = output.substring(0, sepIdx).trim() || null;
@@ -233,9 +241,9 @@ async function pollInstance(
233
241
  errIdx === -1 ? "" : output.substring(errIdx + "===HATCH_ERR===".length).trim();
234
242
  const done = lastLine !== null && status !== "active" && status !== "activating";
235
243
  const failed = errorContent.length > 0 || status === "failed";
236
- return { lastLine, done, failed };
244
+ return { lastLine, done, failed, errorContent };
237
245
  } catch {
238
- return { lastLine: null, done: false, failed: false };
246
+ return { lastLine: null, done: false, failed: false, errorContent: "" };
239
247
  }
240
248
  }
241
249
 
@@ -322,17 +330,23 @@ async function recoverFromCurlFailure(
322
330
  await exec("gcloud", sshArgs);
323
331
  }
324
332
 
333
+ export interface WatchHatchingResult {
334
+ success: boolean;
335
+ errorContent: string;
336
+ }
337
+
325
338
  export async function watchHatching(
326
339
  pollFn: () => Promise<PollResult>,
327
340
  instanceName: string,
328
341
  startTime: number,
329
342
  species: Species,
330
- ): Promise<boolean> {
343
+ ): Promise<WatchHatchingResult> {
331
344
  let spinnerIdx = 0;
332
345
  let lastLogLine: string | null = null;
333
346
  let linesDrawn = 0;
334
347
  let finished = false;
335
348
  let failed = false;
349
+ let lastErrorContent = "";
336
350
  let pollInFlight = false;
337
351
  let nextPollAt = Date.now() + 15000;
338
352
 
@@ -382,6 +396,9 @@ export async function watchHatching(
382
396
  if (result.lastLine) {
383
397
  lastLogLine = result.lastLine;
384
398
  }
399
+ if (result.errorContent) {
400
+ lastErrorContent = result.errorContent;
401
+ }
385
402
  if (result.done) {
386
403
  finished = true;
387
404
  failed = result.failed;
@@ -392,12 +409,12 @@ export async function watchHatching(
392
409
  }
393
410
  }
394
411
 
395
- return new Promise<boolean>((resolve) => {
412
+ return new Promise<WatchHatchingResult>((resolve) => {
396
413
  const interval = setInterval(() => {
397
414
  if (finished) {
398
415
  draw();
399
416
  clearInterval(interval);
400
- resolve(!failed);
417
+ resolve({ success: !failed, errorContent: lastErrorContent });
401
418
  return;
402
419
  }
403
420
 
@@ -408,7 +425,7 @@ export async function watchHatching(
408
425
  console.log(` ⏰ Timed out after ${formatElapsed(elapsed)}. Instance is still running.`);
409
426
  console.log(` Monitor with: vel logs ${instanceName}`);
410
427
  console.log("");
411
- resolve(true);
428
+ resolve({ success: true, errorContent: lastErrorContent });
412
429
  return;
413
430
  }
414
431
 
@@ -485,7 +502,7 @@ async function hatchGcp(
485
502
  console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
486
503
  process.exit(1);
487
504
  }
488
- const startupScript = buildStartupScript(species, bearerToken, sshUser, anthropicApiKey);
505
+ const startupScript = await buildStartupScript(species, bearerToken, sshUser, anthropicApiKey);
489
506
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
490
507
  writeFileSync(startupScriptPath, startupScript);
491
508
 
@@ -570,25 +587,32 @@ async function hatchGcp(
570
587
  console.log(" Press Ctrl+C to detach (instance will keep running)");
571
588
  console.log("");
572
589
 
573
- const success = await watchHatching(
590
+ const result = await watchHatching(
574
591
  () => pollInstance(instanceName, project, zone, account),
575
592
  instanceName,
576
593
  startTime,
577
594
  species,
578
595
  );
579
596
 
580
- 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
+
581
607
  if (
582
608
  species === "vellum" &&
583
609
  (await checkCurlFailure(instanceName, project, zone, account))
584
610
  ) {
585
- console.log("");
586
611
  const installScriptUrl = `${process.env.VELLUM_ASSISTANT_PLATFORM_URL ?? "https://assistant.vellum.ai"}/install.sh`;
587
612
  console.log(`🔄 Detected install script curl failure for ${installScriptUrl}, attempting recovery...`);
588
613
  await recoverFromCurlFailure(instanceName, project, zone, sshUser, account);
589
614
  console.log("✅ Recovery successful!");
590
615
  } else {
591
- console.log("");
592
616
  process.exit(1);
593
617
  }
594
618
  }
@@ -650,7 +674,7 @@ async function hatchCustom(
650
674
  process.exit(1);
651
675
  }
652
676
 
653
- const startupScript = buildStartupScript(species, bearerToken, sshUser, anthropicApiKey);
677
+ const startupScript = await buildStartupScript(species, bearerToken, sshUser, anthropicApiKey);
654
678
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
655
679
  writeFileSync(startupScriptPath, startupScript);
656
680
 
@@ -715,6 +739,22 @@ async function hatchCustom(
715
739
  }
716
740
  }
717
741
 
742
+ function resolveGatewayDir(): string {
743
+ const sourceDir = join(import.meta.dir, "..", "..", "..", "gateway");
744
+ if (existsSync(sourceDir)) {
745
+ return sourceDir;
746
+ }
747
+
748
+ try {
749
+ const pkgPath = _require.resolve("@vellumai/vellum-gateway/package.json");
750
+ return dirname(pkgPath);
751
+ } catch {
752
+ throw new Error(
753
+ "Gateway not found. Ensure @vellumai/vellum-gateway is installed or run from the source tree.",
754
+ );
755
+ }
756
+ }
757
+
718
758
  async function hatchLocal(species: Species, name: string | null): Promise<void> {
719
759
  const instanceName = name ?? `${species}-${generateRandomSuffix()}`;
720
760
 
@@ -777,24 +817,19 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
777
817
  }
778
818
 
779
819
  console.log("🌐 Starting gateway...");
780
- const gatewayDir = join(import.meta.dir, "..", "..", "..", "gateway");
781
- if (!existsSync(gatewayDir)) {
782
- console.warn("⚠️ Gateway directory not found at", gatewayDir);
783
- console.warn(' Gateway will not be started\n');
784
- } else {
785
- const gateway = spawn("bun", ["run", "src/index.ts"], {
786
- cwd: gatewayDir,
787
- detached: true,
788
- stdio: "ignore",
789
- env: {
790
- ...process.env,
791
- GATEWAY_RUNTIME_PROXY_ENABLED: "true",
792
- GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
793
- },
794
- });
795
- gateway.unref();
796
- console.log("✅ Gateway started\n");
797
- }
820
+ const gatewayDir = resolveGatewayDir();
821
+ const gateway = spawn("bun", ["run", "src/index.ts"], {
822
+ cwd: gatewayDir,
823
+ detached: true,
824
+ stdio: "ignore",
825
+ env: {
826
+ ...process.env,
827
+ GATEWAY_RUNTIME_PROXY_ENABLED: "true",
828
+ GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
829
+ },
830
+ });
831
+ gateway.unref();
832
+ console.log("✅ Gateway started\n");
798
833
 
799
834
  const runtimeUrl = `http://localhost:${GATEWAY_PORT}`;
800
835
  const localEntry: AssistantEntry = {
@@ -815,7 +850,20 @@ async function hatchLocal(species: Species, name: string | null): Promise<void>
815
850
  console.log("");
816
851
  }
817
852
 
853
+ function getCliVersion(): string {
854
+ try {
855
+ const pkgPath = join(import.meta.dir, "..", "package.json");
856
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
857
+ return pkg.version ?? "unknown";
858
+ } catch {
859
+ return "unknown";
860
+ }
861
+ }
862
+
818
863
  export async function hatch(): Promise<void> {
864
+ const cliVersion = getCliVersion();
865
+ console.log(`@vellumai/cli v${cliVersion}`);
866
+
819
867
  const { species, detached, name, remote } = parseArgs();
820
868
 
821
869
  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
 
@@ -420,7 +420,7 @@ export async function hatchAws(
420
420
  console.log("\u{1F50D} Finding latest Debian AMI...");
421
421
  const amiId = await getLatestDebianAmi(region);
422
422
 
423
- const startupScript = buildStartupScript(species, bearerToken, sshUser, anthropicApiKey);
423
+ const startupScript = await buildStartupScript(species, bearerToken, sshUser, anthropicApiKey);
424
424
  const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
425
425
  writeFileSync(startupScriptPath, startupScript);
426
426
 
@@ -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
@@ -139,15 +139,14 @@ export async function syncFirewallRules(
139
139
  "firewall-rules",
140
140
  "list",
141
141
  `--project=${project}`,
142
- `--filter=targetTags:${tag}`,
143
- "--format=value(name)",
142
+ "--format=json(name,targetTags)",
144
143
  ];
145
144
  if (account) listArgs.push(`--account=${account}`);
146
145
  const output = await execOutput("gcloud", listArgs);
147
- existingNames = output
148
- .split("\n")
149
- .map((s) => s.trim())
150
- .filter(Boolean);
146
+ const allRules = JSON.parse(output) as Array<{ name: string; targetTags?: string[] }>;
147
+ existingNames = allRules
148
+ .filter((r) => r.targetTags?.includes(tag))
149
+ .map((r) => r.name);
151
150
  } catch {
152
151
  existingNames = [];
153
152
  }
@@ -190,10 +189,11 @@ export async function fetchFirewallRules(
190
189
  "firewall-rules",
191
190
  "list",
192
191
  `--project=${project}`,
193
- `--filter=targetTags:${tag}`,
194
192
  "--format=json",
195
193
  ]);
196
- return output;
194
+ const rules = JSON.parse(output) as Array<{ targetTags?: string[] }>;
195
+ const filtered = rules.filter((r) => r.targetTags?.includes(tag));
196
+ return JSON.stringify(filtered, null, 2);
197
197
  }
198
198
 
199
199
  export interface GcpInstance {
@@ -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,
@@ -1,8 +1,7 @@
1
1
  import { join } from "path";
2
2
 
3
- const serverSource = await Bun.file(join(import.meta.dir, "..", "adapters", "openclaw-http-server.ts")).text();
4
-
5
- export function buildOpenclawRuntimeServer(): string {
3
+ export async function buildOpenclawRuntimeServer(): Promise<string> {
4
+ const serverSource = await Bun.file(join(import.meta.dir, "..", "adapters", "openclaw-http-server.ts")).text();
6
5
 
7
6
  return `
8
7
  cat > /opt/openclaw-runtime-server.ts << 'RUNTIME_SERVER_EOF'