@vellumai/cli 0.8.2 → 0.8.4

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/docker.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from "crypto";
2
2
  import { chmodSync, existsSync, mkdirSync, watch as fsWatch } from "fs";
3
3
  import { arch, platform } from "os";
4
- import { dirname, join } from "path";
4
+ import { dirname, join, resolve } from "path";
5
5
 
6
6
  // Direct import — bun embeds this at compile time so it works in compiled binaries.
7
7
  import cliPkg from "../../package.json";
@@ -12,15 +12,29 @@ import {
12
12
  setActiveAssistant,
13
13
  } from "./assistant-config";
14
14
  import type { AssistantEntry } from "./assistant-config";
15
- import { writeInitialConfig } from "./config-utils";
15
+ import { buildHatchConfigValues, writeInitialConfig } from "./config-utils";
16
16
  import { buildServiceRunArgs } from "./statefulset.js";
17
17
  import type { Species } from "./constants";
18
18
  import { getDefaultPorts } from "./environments/paths.js";
19
19
  import { getCurrentEnvironment } from "./environments/resolve.js";
20
20
  import { leaseGuardianToken } from "./guardian-token";
21
+ import { logHatchNextSteps } from "./hatch-next-steps.js";
21
22
  import { isVellumProcess, stopProcess } from "./process";
22
23
  import { generateInstanceName } from "./random-name";
23
- import { resolveImageRefs } from "./platform-releases.js";
24
+ import {
25
+ HOST_IMAGE_LOADER_URL,
26
+ isLocalBuildRef,
27
+ loadImageViaHost,
28
+ } from "./host-image-loader.js";
29
+ import {
30
+ fetchLatestStableVersion,
31
+ resolveImageRefs,
32
+ } from "./platform-releases.js";
33
+ import {
34
+ configureHatchProviderApiKey,
35
+ formatProviderName,
36
+ resolveHatchProvider,
37
+ } from "./provider-secrets.js";
24
38
  import { exec, execOutput } from "./step-runner";
25
39
  import {
26
40
  closeLogFile,
@@ -40,10 +54,13 @@ export const DOCKERHUB_IMAGES: Record<ServiceName, string> = {
40
54
  };
41
55
 
42
56
  /** Internal ports exposed by each service's Dockerfile. Re-exported from environments/paths.ts. */
43
- export { ASSISTANT_INTERNAL_PORT, GATEWAY_INTERNAL_PORT } from "./environments/paths.js";
57
+ export {
58
+ ASSISTANT_INTERNAL_PORT,
59
+ GATEWAY_INTERNAL_PORT,
60
+ } from "./environments/paths.js";
44
61
 
45
62
  /** Max time to wait for the assistant container to emit the readiness sentinel. */
46
- export const DOCKER_READY_TIMEOUT_MS = 3 * 60 * 1000;
63
+ export const DOCKER_READY_TIMEOUT_MS = 5 * 60 * 1000;
47
64
 
48
65
  /** Default virtual-camera device path. Overridable via `VELLUM_AVATAR_DEVICE`. */
49
66
  const DEFAULT_AVATAR_DEVICE_PATH = "/dev/video10";
@@ -247,6 +264,28 @@ function ensureLocalBinOnPath(): void {
247
264
  }
248
265
  }
249
266
 
267
+ export interface HatchDockerOptions {
268
+ setupProviderCredentials?: boolean;
269
+ }
270
+
271
+ export type DockerProviderCredentialSetupAction =
272
+ | "configure"
273
+ | "defer"
274
+ | "missing-token"
275
+ | "skip";
276
+
277
+ export function resolveDockerProviderCredentialSetupAction(options: {
278
+ provider: string | null | undefined;
279
+ guardianAccessToken?: string;
280
+ detached: boolean;
281
+ }): DockerProviderCredentialSetupAction {
282
+ if (options.provider === undefined) return "skip";
283
+ if (options.provider === null) return options.detached ? "skip" : "configure";
284
+ if (options.detached) return "defer";
285
+ if (!options.guardianAccessToken) return "missing-token";
286
+ return "configure";
287
+ }
288
+
250
289
  /**
251
290
  * Checks whether the `docker` CLI and daemon are available on the system.
252
291
  * Installs Colima and Docker via direct binary download if missing (no sudo
@@ -461,6 +500,37 @@ function hasFullSourceTree(root: string): boolean {
461
500
  return existsSync(join(root, "assistant", "package.json"));
462
501
  }
463
502
 
503
+ /**
504
+ * Decide which image-source path `hatchDocker` should take given the user
505
+ * flags and a probe result for the source tree.
506
+ *
507
+ * - `watch` always wants source-build *and* file-watcher (when the source
508
+ * tree is available).
509
+ * - `buildFromSource` wants source-build but no watcher — used by evals so
510
+ * each run picks up fresh CLI changes from the repo.
511
+ * - Without either flag we pull the published images.
512
+ * - If either flag was set but the source tree is missing (e.g. the CLI is
513
+ * running from a packaged .app bundle), fall back to pulling and surface
514
+ * the reason so the caller can log a warning.
515
+ *
516
+ * Returning a plain record keeps this trivially unit-testable — see
517
+ * `__tests__/docker.test.ts`.
518
+ */
519
+ export function resolveDockerHatchMode(opts: {
520
+ watch: boolean;
521
+ buildFromSource: boolean;
522
+ fullSourceTreeAvailable: boolean;
523
+ }): { build: boolean; watcher: boolean; fellBackToPull: boolean } {
524
+ const requested = opts.watch || opts.buildFromSource;
525
+ if (!requested) {
526
+ return { build: false, watcher: false, fellBackToPull: false };
527
+ }
528
+ if (!opts.fullSourceTreeAvailable) {
529
+ return { build: false, watcher: false, fellBackToPull: true };
530
+ }
531
+ return { build: true, watcher: opts.watch, fellBackToPull: false };
532
+ }
533
+
464
534
  /**
465
535
  * Locate the repository root by walking up from `cli/src/lib/` until we
466
536
  * find a directory containing the expected Dockerfiles.
@@ -581,7 +651,10 @@ export async function startContainers(
581
651
  },
582
652
  log: (msg: string) => void,
583
653
  ): Promise<void> {
584
- const runArgs = buildServiceRunArgs({ ...opts, avatarDevicePath: resolveAvatarDevicePath() });
654
+ const runArgs = buildServiceRunArgs({
655
+ ...opts,
656
+ avatarDevicePath: resolveAvatarDevicePath(),
657
+ });
585
658
  for (const service of SERVICE_START_ORDER) {
586
659
  log(`🚀 Starting ${service} container...`);
587
660
  await exec("docker", runArgs[service]());
@@ -870,14 +943,32 @@ function startFileWatcher(opts: {
870
943
  };
871
944
  }
872
945
 
946
+ export interface HatchDockerOptions {
947
+ /**
948
+ * Path to a local source tree to build images from before hatching. When
949
+ * provided, this path is used directly as the repo root and no file
950
+ * watcher is started — useful for callers (e.g. evals) that want each
951
+ * run to pick up local CLI changes without keeping a long-lived watcher
952
+ * process around. `--watch` independently auto-detects the repo root and
953
+ * also enables hot-reload.
954
+ */
955
+ sourcePath?: string | null;
956
+ analyze?: boolean;
957
+ }
958
+
873
959
  export async function hatchDocker(
874
960
  species: Species,
875
961
  detached: boolean,
876
962
  name: string | null,
877
963
  watch: boolean = false,
878
964
  configValues: Record<string, string> = {},
965
+ options: HatchDockerOptions = {},
879
966
  ): Promise<void> {
880
967
  resetLogFile("hatch.log");
968
+ const provider =
969
+ options.setupProviderCredentials === false
970
+ ? undefined
971
+ : resolveHatchProvider(configValues);
881
972
 
882
973
  let logFd = openLogFile("hatch.log");
883
974
  const log = (msg: string): void => {
@@ -898,25 +989,42 @@ export async function hatchDocker(
898
989
  gateway: "",
899
990
  };
900
991
 
992
+ const sourcePath =
993
+ typeof options.sourcePath === "string" && options.sourcePath.length > 0
994
+ ? options.sourcePath
995
+ : null;
996
+ const buildFromSource = sourcePath !== null;
901
997
  let repoRoot: string | undefined;
998
+ let fullSourceTreeAvailable = false;
902
999
 
903
- if (watch) {
904
- repoRoot = findRepoRoot();
1000
+ if (watch || buildFromSource) {
1001
+ // When --source <path> is supplied, trust the caller and use it
1002
+ // directly. Otherwise (the --watch case) walk up from known locations
1003
+ // to find the repo root.
1004
+ repoRoot = sourcePath ? resolve(sourcePath) : findRepoRoot();
905
1005
 
906
1006
  // When running from a packaged .app bundle, the Dockerfiles are
907
1007
  // present (so findRepoRoot succeeds) but the full source tree is
908
1008
  // not — we can't build images locally. Fall back to pulling
909
1009
  // pre-built images instead.
910
- if (!hasFullSourceTree(repoRoot)) {
1010
+ fullSourceTreeAvailable = hasFullSourceTree(repoRoot);
1011
+ if (!fullSourceTreeAvailable) {
911
1012
  log(
912
1013
  "⚠️ Dockerfiles found but no source tree — falling back to image pull",
913
1014
  );
914
- watch = false;
915
1015
  repoRoot = undefined;
916
1016
  }
917
1017
  }
918
1018
 
919
- if (watch && repoRoot) {
1019
+ const mode = resolveDockerHatchMode({
1020
+ watch,
1021
+ buildFromSource,
1022
+ fullSourceTreeAvailable,
1023
+ });
1024
+ // Honour the resolved mode for the rest of the flow.
1025
+ watch = mode.watcher;
1026
+
1027
+ if (mode.build && repoRoot) {
920
1028
  emitProgress(2, 6, "Building images...");
921
1029
  const localTag = `local-${instanceName}`;
922
1030
  imageTags.assistant = `vellum-assistant:${localTag}`;
@@ -926,7 +1034,9 @@ export async function hatchDocker(
926
1034
 
927
1035
  log(`🥚 Hatching Docker assistant: ${instanceName}`);
928
1036
  log(` Species: ${species}`);
929
- log(` Mode: development (watch)`);
1037
+ log(
1038
+ ` Mode: ${mode.watcher ? "development (watch)" : "build-from-source"}`,
1039
+ );
930
1040
  log(` Repo: ${repoRoot}`);
931
1041
  log(` Images (local build):`);
932
1042
  log(` assistant: ${imageTags.assistant}`);
@@ -938,7 +1048,7 @@ export async function hatchDocker(
938
1048
  log("✅ Docker images built");
939
1049
  }
940
1050
 
941
- if (!watch || !repoRoot) {
1051
+ if (!mode.build || !repoRoot) {
942
1052
  emitProgress(2, 6, "Pulling images...");
943
1053
 
944
1054
  // Allow explicit image overrides via environment variables.
@@ -957,8 +1067,23 @@ export async function hatchDocker(
957
1067
  imageSource = "env override";
958
1068
  log("Using image overrides from environment variables");
959
1069
  } else {
960
- const version = cliPkg.version;
961
- const versionTag = version ? `v${version}` : "latest";
1070
+ // Resolve image refs from a remote source that may have dev/local
1071
+ // builds. If resolution is unavailable, fall back to the CLI's own
1072
+ // version so a default tag can still be resolved.
1073
+ log("🔍 Fetching latest stable release...");
1074
+ const latestVersion = await fetchLatestStableVersion();
1075
+ let versionTag: string;
1076
+ if (latestVersion) {
1077
+ versionTag = latestVersion.startsWith("v")
1078
+ ? latestVersion
1079
+ : `v${latestVersion}`;
1080
+ } else {
1081
+ const fallback = cliPkg.version;
1082
+ versionTag = fallback ? `v${fallback}` : "latest";
1083
+ log(
1084
+ `⚠️ Platform releases unavailable; falling back to CLI version ${versionTag}`,
1085
+ );
1086
+ }
962
1087
  log("🔍 Resolving image references...");
963
1088
  const resolved = await resolveImageRefs(versionTag, log);
964
1089
  imageTags.assistant = resolved.imageTags.assistant;
@@ -976,11 +1101,25 @@ export async function hatchDocker(
976
1101
  log(` credential-executor: ${imageTags["credential-executor"]}`);
977
1102
  log("");
978
1103
 
979
- log("📦 Pulling Docker images...");
980
- await exec("docker", ["pull", imageTags.assistant]);
981
- await exec("docker", ["pull", imageTags.gateway]);
982
- await exec("docker", ["pull", imageTags["credential-executor"]]);
983
- log("✅ Docker images pulled");
1104
+ // Per-ref branching: local-build refs need the image-loader; external
1105
+ // registry refs get a normal `docker pull`. The two transports compose
1106
+ // cleanly a release can mix different sources for different images.
1107
+ log("📦 Acquiring Docker images...");
1108
+ for (const service of [
1109
+ "assistant",
1110
+ "gateway",
1111
+ "credential-executor",
1112
+ ] as const) {
1113
+ const ref = imageTags[service];
1114
+ if (isLocalBuildRef(ref)) {
1115
+ log(` ↪ loading ${ref} via host image-loader`);
1116
+ await loadImageViaHost(HOST_IMAGE_LOADER_URL, ref, log);
1117
+ } else {
1118
+ log(` ↪ pulling ${ref}`);
1119
+ await exec("docker", ["pull", ref]);
1120
+ }
1121
+ }
1122
+ log("✅ Docker images acquired");
984
1123
  }
985
1124
 
986
1125
  const res = dockerResourceNames(instanceName);
@@ -1013,7 +1152,8 @@ export async function hatchDocker(
1013
1152
 
1014
1153
  // Write --config key=value pairs to a temp file that gets bind-mounted
1015
1154
  // into the assistant container and read via VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH.
1016
- const defaultWorkspaceConfigPath = writeInitialConfig(configValues);
1155
+ const hatchConfigValues = buildHatchConfigValues(configValues, provider);
1156
+ const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
1017
1157
 
1018
1158
  const cesServiceToken = randomBytes(32).toString("hex");
1019
1159
  const signingKey = randomBytes(32).toString("hex");
@@ -1043,6 +1183,7 @@ export async function hatchDocker(
1043
1183
  },
1044
1184
  log,
1045
1185
  );
1186
+ const containersUpAt = Date.now();
1046
1187
 
1047
1188
  const imageDigests = await captureImageRefs(res);
1048
1189
 
@@ -1053,6 +1194,7 @@ export async function hatchDocker(
1053
1194
  cloud: "docker",
1054
1195
  species,
1055
1196
  hatchedAt: new Date().toISOString(),
1197
+ guardianBootstrapSecret: ownSecret,
1056
1198
  containerInfo: {
1057
1199
  assistantImage: imageTags.assistant,
1058
1200
  gatewayImage: imageTags.gateway,
@@ -1068,19 +1210,59 @@ export async function hatchDocker(
1068
1210
  setActiveAssistant(instanceName);
1069
1211
 
1070
1212
  emitProgress(6, 6, "Waiting for services...");
1071
- const { ready } = await waitForGatewayAndLease({
1213
+ const waitDetached = watch ? false : detached;
1214
+ const { ready, guardianAccessToken } = await waitForGatewayAndLease({
1072
1215
  bootstrapSecret: ownSecret,
1073
1216
  containerName: res.assistantContainer,
1074
- detached: watch ? false : detached,
1217
+ detached: waitDetached,
1075
1218
  instanceName,
1076
1219
  logFd,
1077
1220
  runtimeUrl,
1221
+ containersUpAt,
1222
+ analyze: options.analyze ?? false,
1078
1223
  });
1079
1224
 
1080
1225
  if (!ready && !(watch && repoRoot)) {
1081
1226
  throw new Error("Timed out waiting for assistant to become ready");
1082
1227
  }
1083
1228
 
1229
+ if (ready) {
1230
+ const providerSetupAction = resolveDockerProviderCredentialSetupAction({
1231
+ provider,
1232
+ guardianAccessToken,
1233
+ detached: waitDetached,
1234
+ });
1235
+
1236
+ if (providerSetupAction === "defer" && provider !== null) {
1237
+ log(
1238
+ `Provider credential setup deferred in detached mode.\n` +
1239
+ `Run \`vellum setup --provider ${provider}\` after the assistant is ready.`,
1240
+ );
1241
+ } else if (providerSetupAction === "missing-token" && provider !== null) {
1242
+ log(
1243
+ `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
1244
+ ` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
1245
+ );
1246
+ } else if (
1247
+ providerSetupAction === "configure" &&
1248
+ provider !== undefined
1249
+ ) {
1250
+ log(
1251
+ provider === null
1252
+ ? "Checking provider credentials..."
1253
+ : `Checking ${formatProviderName(provider)} credentials...`,
1254
+ );
1255
+ await configureHatchProviderApiKey({
1256
+ gatewayUrl: runtimeUrl,
1257
+ provider,
1258
+ bearerToken: guardianAccessToken,
1259
+ env: process.env,
1260
+ log,
1261
+ });
1262
+ }
1263
+ logHatchNextSteps(log, instanceName);
1264
+ }
1265
+
1084
1266
  if (watch && repoRoot) {
1085
1267
  saveAssistantEntry({ ...dockerEntry, watcherPid: process.pid });
1086
1268
 
@@ -1136,7 +1318,9 @@ async function waitForGatewayAndLease(opts: {
1136
1318
  instanceName: string;
1137
1319
  logFd: number | "ignore";
1138
1320
  runtimeUrl: string;
1139
- }): Promise<{ ready: boolean }> {
1321
+ containersUpAt: number;
1322
+ analyze: boolean;
1323
+ }): Promise<{ ready: boolean; guardianAccessToken?: string }> {
1140
1324
  const {
1141
1325
  bootstrapSecret,
1142
1326
  containerName,
@@ -1144,6 +1328,8 @@ async function waitForGatewayAndLease(opts: {
1144
1328
  instanceName,
1145
1329
  logFd,
1146
1330
  runtimeUrl,
1331
+ containersUpAt,
1332
+ analyze,
1147
1333
  } = opts;
1148
1334
 
1149
1335
  const log = (msg: string): void => {
@@ -1168,7 +1354,7 @@ async function waitForGatewayAndLease(opts: {
1168
1354
  log("Waiting for assistant to become ready...");
1169
1355
 
1170
1356
  const readyUrl = `${runtimeUrl}/readyz`;
1171
- const start = Date.now();
1357
+ const start = containersUpAt;
1172
1358
  let ready = false;
1173
1359
 
1174
1360
  while (Date.now() - start < DOCKER_READY_TIMEOUT_MS) {
@@ -1204,8 +1390,18 @@ async function waitForGatewayAndLease(opts: {
1204
1390
  return { ready: false };
1205
1391
  }
1206
1392
 
1207
- const elapsedSec = ((Date.now() - start) / 1000).toFixed(1);
1393
+ const readyAt = Date.now();
1394
+ const containersUpToReadyMs = readyAt - start;
1395
+ const elapsedSec = (containersUpToReadyMs / 1000).toFixed(1);
1208
1396
  log(`Assistant ready after ${elapsedSec}s`);
1397
+ if (analyze) {
1398
+ console.info(
1399
+ `[vellum-hatch-timing] ${JSON.stringify({
1400
+ containers_up_to_ready_ms: containersUpToReadyMs,
1401
+ instance: instanceName,
1402
+ })}`,
1403
+ );
1404
+ }
1209
1405
 
1210
1406
  // Lease guardian token. The /readyz check confirms both gateway and
1211
1407
  // assistant are reachable. Retry with backoff in case there is a brief
@@ -1215,6 +1411,7 @@ async function waitForGatewayAndLease(opts: {
1215
1411
  const leaseDeadline = start + DOCKER_READY_TIMEOUT_MS;
1216
1412
  let leaseSuccess = false;
1217
1413
  let lastLeaseError: string | undefined;
1414
+ let guardianAccessToken: string | undefined;
1218
1415
 
1219
1416
  while (Date.now() < leaseDeadline) {
1220
1417
  try {
@@ -1227,6 +1424,7 @@ async function waitForGatewayAndLease(opts: {
1227
1424
  log(
1228
1425
  `Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId}, expiresAt=${tokenData.accessTokenExpiresAt})`,
1229
1426
  );
1427
+ guardianAccessToken = tokenData.accessToken;
1230
1428
  leaseSuccess = true;
1231
1429
  break;
1232
1430
  } catch (err) {
@@ -1257,5 +1455,5 @@ async function waitForGatewayAndLease(opts: {
1257
1455
  log(` Name: ${instanceName}`);
1258
1456
  log(` Runtime: ${runtimeUrl}`);
1259
1457
  log("");
1260
- return { ready: true };
1458
+ return { ready: true, guardianAccessToken };
1261
1459
  }
@@ -22,7 +22,7 @@ import {
22
22
  } from "./assistant-config.js";
23
23
  import type { AssistantEntry } from "./assistant-config.js";
24
24
  import type { Species } from "./constants.js";
25
- import { writeInitialConfig } from "./config-utils.js";
25
+ import { buildHatchConfigValues, writeInitialConfig } from "./config-utils.js";
26
26
  import {
27
27
  generateLocalSigningKey,
28
28
  startLocalDaemon,
@@ -34,6 +34,12 @@ import { generateInstanceName } from "./random-name.js";
34
34
  import { leaseGuardianToken } from "./guardian-token.js";
35
35
  import { archiveLogFile, resetLogFile } from "./xdg-log.js";
36
36
  import { emitProgress } from "./desktop-progress.js";
37
+ import {
38
+ configureHatchProviderApiKey,
39
+ formatProviderName,
40
+ resolveHatchProvider,
41
+ } from "./provider-secrets.js";
42
+ import { logHatchNextSteps } from "./hatch-next-steps.js";
37
43
 
38
44
  /**
39
45
  * Attempts to place a symlink at the given path pointing to cliBinary.
@@ -125,13 +131,22 @@ function installCLISymlink(): void {
125
131
  );
126
132
  }
127
133
 
134
+ export interface HatchLocalOptions {
135
+ setupProviderCredentials?: boolean;
136
+ }
137
+
128
138
  export async function hatchLocal(
129
139
  species: Species,
130
140
  name: string | null,
131
141
  watch: boolean = false,
132
142
  keepAlive: boolean = false,
133
143
  configValues: Record<string, string> = {},
144
+ options: HatchLocalOptions = {},
134
145
  ): Promise<void> {
146
+ const provider =
147
+ options.setupProviderCredentials === false
148
+ ? undefined
149
+ : resolveHatchProvider(configValues);
135
150
  const instanceName = generateInstanceName(
136
151
  species,
137
152
  name ?? process.env.VELLUM_ASSISTANT_NAME,
@@ -168,7 +183,8 @@ export async function hatchLocal(
168
183
  }
169
184
 
170
185
  emitProgress(2, 6, "Writing configuration...");
171
- const defaultWorkspaceConfigPath = writeInitialConfig(configValues);
186
+ const hatchConfigValues = buildHatchConfigValues(configValues, provider);
187
+ const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
172
188
 
173
189
  emitProgress(3, 6, "Starting assistant...");
174
190
  const signingKey = generateLocalSigningKey();
@@ -198,9 +214,11 @@ export async function hatchLocal(
198
214
  emitProgress(5, 6, "Securing connection...");
199
215
  const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
200
216
  const maxLeaseAttempts = 3;
217
+ let guardianAccessToken: string | undefined;
201
218
  for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
202
219
  try {
203
- await leaseGuardianToken(loopbackUrl, instanceName);
220
+ const tokenData = await leaseGuardianToken(loopbackUrl, instanceName);
221
+ guardianAccessToken = tokenData.accessToken;
204
222
  break;
205
223
  } catch (err) {
206
224
  if (attempt < maxLeaseAttempts) {
@@ -238,6 +256,26 @@ export async function hatchLocal(
238
256
  installCLISymlink();
239
257
  }
240
258
 
259
+ if (provider !== undefined && provider !== null && !guardianAccessToken) {
260
+ console.error(
261
+ `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
262
+ ` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
263
+ );
264
+ } else if (provider !== undefined) {
265
+ console.log("");
266
+ console.log(
267
+ provider === null
268
+ ? "Checking provider credentials..."
269
+ : `Checking ${formatProviderName(provider)} credentials...`,
270
+ );
271
+ await configureHatchProviderApiKey({
272
+ gatewayUrl: loopbackUrl,
273
+ provider,
274
+ bearerToken: guardianAccessToken,
275
+ env: process.env,
276
+ });
277
+ }
278
+
241
279
  console.log("");
242
280
  console.log(`✅ Local assistant hatched!`);
243
281
  console.log("");
@@ -245,6 +283,7 @@ export async function hatchLocal(
245
283
  console.log(` Name: ${instanceName}`);
246
284
  console.log(` Runtime: ${runtimeUrl}`);
247
285
  console.log("");
286
+ logHatchNextSteps(console.log, instanceName);
248
287
 
249
288
  if (keepAlive) {
250
289
  const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
@@ -0,0 +1,12 @@
1
+ export function logHatchNextSteps(
2
+ log: (message: string) => void,
3
+ instanceName: string,
4
+ ): void {
5
+ log("Next steps:");
6
+ log(" vellum client");
7
+ log(' vellum message "hello"');
8
+ log(" vellum events");
9
+ log(" vellum ps");
10
+ log(` vellum use ${instanceName}`);
11
+ log("");
12
+ }