@vellumai/cli 0.8.5 → 0.8.7-dev.202606052118.34cd356

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.
Files changed (102) hide show
  1. package/AGENTS.md +6 -0
  2. package/bun.lock +8 -0
  3. package/knip.json +6 -1
  4. package/node_modules/@vellumai/environments/bun.lock +24 -0
  5. package/node_modules/@vellumai/environments/package.json +18 -0
  6. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
  7. package/node_modules/@vellumai/environments/src/index.ts +11 -0
  8. package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
  9. package/node_modules/@vellumai/environments/tsconfig.json +20 -0
  10. package/node_modules/@vellumai/local-mode/bun.lock +29 -0
  11. package/node_modules/@vellumai/local-mode/package.json +22 -0
  12. package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
  13. package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
  14. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +108 -0
  15. package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
  16. package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
  17. package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
  18. package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
  19. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
  20. package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
  21. package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
  22. package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
  23. package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
  24. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
  25. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
  26. package/node_modules/@vellumai/local-mode/src/lockfile.ts +133 -0
  27. package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
  28. package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
  29. package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
  30. package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
  31. package/package.json +12 -1
  32. package/src/__tests__/assistant-client-refresh.test.ts +182 -0
  33. package/src/__tests__/backup.test.ts +38 -0
  34. package/src/__tests__/clean.test.ts +179 -0
  35. package/src/__tests__/client-token.test.ts +87 -0
  36. package/src/__tests__/client-tui-refresh.test.ts +170 -0
  37. package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
  38. package/src/__tests__/connect-import.test.ts +317 -0
  39. package/src/__tests__/devices.test.ts +272 -0
  40. package/src/__tests__/env-drift.test.ts +32 -44
  41. package/src/__tests__/flags.test.ts +248 -0
  42. package/src/__tests__/guardian-token.test.ts +126 -2
  43. package/src/__tests__/multi-local.test.ts +1 -1
  44. package/src/__tests__/orphan-detection.test.ts +8 -6
  45. package/src/__tests__/pair.test.ts +271 -0
  46. package/src/__tests__/paired-lifecycle.test.ts +116 -0
  47. package/src/__tests__/recover.test.ts +307 -0
  48. package/src/__tests__/segments-to-plain-text.test.ts +37 -0
  49. package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
  50. package/src/__tests__/unpair.test.ts +163 -0
  51. package/src/__tests__/wake.test.ts +215 -0
  52. package/src/commands/backup.ts +2 -0
  53. package/src/commands/client.ts +569 -39
  54. package/src/commands/connect/import.ts +217 -0
  55. package/src/commands/connect.ts +31 -0
  56. package/src/commands/devices.ts +247 -0
  57. package/src/commands/env.ts +1 -1
  58. package/src/commands/flags.ts +269 -0
  59. package/src/commands/gateway/token.ts +73 -0
  60. package/src/commands/gateway.ts +29 -0
  61. package/src/commands/logs.ts +6 -18
  62. package/src/commands/pair.ts +222 -0
  63. package/src/commands/ps.ts +57 -41
  64. package/src/commands/recover.ts +47 -9
  65. package/src/commands/restore.ts +8 -1
  66. package/src/commands/retire.ts +23 -70
  67. package/src/commands/rollback.ts +2 -14
  68. package/src/commands/sleep.ts +7 -0
  69. package/src/commands/ssh.ts +5 -24
  70. package/src/commands/teleport.ts +34 -26
  71. package/src/commands/tunnel.ts +46 -2
  72. package/src/commands/unpair.ts +118 -0
  73. package/src/commands/upgrade.ts +8 -16
  74. package/src/commands/wake.ts +75 -45
  75. package/src/components/DefaultMainScreen.tsx +100 -14
  76. package/src/index.ts +22 -0
  77. package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
  78. package/src/lib/__tests__/step-runner.test.ts +49 -1
  79. package/src/lib/assistant-client.ts +58 -37
  80. package/src/lib/assistant-config.ts +28 -3
  81. package/src/lib/cloudflare-tunnel.ts +276 -0
  82. package/src/lib/config-utils.ts +24 -3
  83. package/src/lib/confirm-action.ts +57 -0
  84. package/src/lib/docker.ts +82 -8
  85. package/src/lib/environments/__tests__/paths.test.ts +2 -1
  86. package/src/lib/environments/__tests__/seeds.test.ts +2 -1
  87. package/src/lib/environments/paths.ts +1 -1
  88. package/src/lib/environments/resolve.ts +11 -35
  89. package/src/lib/guardian-token.ts +132 -9
  90. package/src/lib/hatch-local.ts +75 -33
  91. package/src/lib/http-client.ts +1 -3
  92. package/src/lib/lifecycle-reporter.ts +31 -0
  93. package/src/lib/local.ts +193 -298
  94. package/src/lib/orphan-detection.ts +9 -5
  95. package/src/lib/pgrep.ts +5 -1
  96. package/src/lib/platform-client.ts +97 -49
  97. package/src/lib/process.ts +109 -39
  98. package/src/lib/retire-local.ts +28 -14
  99. package/src/lib/segments-to-plain-text.ts +35 -0
  100. package/src/lib/step-runner.ts +67 -7
  101. package/src/lib/sync-cloud-assistants.ts +17 -0
  102. /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
@@ -33,7 +33,10 @@ import {
33
33
  import { generateInstanceName } from "./random-name.js";
34
34
  import { leaseGuardianToken } from "./guardian-token.js";
35
35
  import { archiveLogFile, resetLogFile } from "./xdg-log.js";
36
- import { emitProgress } from "./desktop-progress.js";
36
+ import {
37
+ consoleLifecycleReporter,
38
+ type LifecycleReporter,
39
+ } from "./lifecycle-reporter.js";
37
40
  import {
38
41
  configureHatchProviderApiKey,
39
42
  formatProviderName,
@@ -134,6 +137,25 @@ function installCLISymlink(): void {
134
137
 
135
138
  export interface HatchLocalOptions {
136
139
  setupProviderCredentials?: boolean;
140
+ /**
141
+ * Sink for progress and log output. Defaults to the console reporter so CLI
142
+ * callers keep their existing terminal output; in-process callers can inject
143
+ * their own reporter to consume progress without writing to stdout.
144
+ */
145
+ reporter?: LifecycleReporter;
146
+ }
147
+
148
+ export interface HatchLocalResult {
149
+ assistantId: string;
150
+ runtimeUrl: string;
151
+ localUrl: string;
152
+ species: Species;
153
+ /**
154
+ * Guardian access token leased during hatch, when the lease succeeded. The
155
+ * full token pair is persisted to disk regardless; this is surfaced so an
156
+ * in-process caller can prime a connection without re-reading the file.
157
+ */
158
+ guardianAccessToken?: string;
137
159
  }
138
160
 
139
161
  export async function hatchLocal(
@@ -143,7 +165,8 @@ export async function hatchLocal(
143
165
  keepAlive: boolean = false,
144
166
  configValues: Record<string, string> = {},
145
167
  options: HatchLocalOptions = {},
146
- ): Promise<void> {
168
+ ): Promise<HatchLocalResult> {
169
+ const reporter = options.reporter ?? consoleLifecycleReporter;
147
170
  const provider =
148
171
  options.setupProviderCredentials === false
149
172
  ? undefined
@@ -153,7 +176,7 @@ export async function hatchLocal(
153
176
  name ?? process.env.VELLUM_ASSISTANT_NAME,
154
177
  );
155
178
 
156
- emitProgress(1, 6, "Allocating resources...");
179
+ reporter.progress(1, 6, "Allocating resources...");
157
180
 
158
181
  const existing = findAssistantByName(instanceName);
159
182
  if (existing && (!existing.cloud || existing.cloud === "local")) {
@@ -175,43 +198,47 @@ export async function hatchLocal(
175
198
  archiveLogFile("hatch.log", logsDir);
176
199
  resetLogFile("hatch.log");
177
200
 
178
- console.log(`🥚 Hatching local assistant: ${instanceName}`);
179
- console.log(` Species: ${species}`);
180
- console.log("");
201
+ reporter.log(`🥚 Hatching local assistant: ${instanceName}`);
202
+ reporter.log(` Species: ${species}`);
203
+ reporter.log("");
181
204
 
182
205
  const apiKeyCheck = checkProviderApiKey();
183
206
  if (!apiKeyCheck.hasKey) {
184
- console.warn(
207
+ reporter.warn(
185
208
  "Warning: No LLM provider API key is configured. The assistant will fail when you try to send a message.",
186
209
  );
187
- console.warn(" To fix, export your key before running vellum hatch:");
188
- console.warn(" export ANTHROPIC_API_KEY=<your-key>");
189
- console.warn("");
210
+ reporter.warn(" To fix, export your key before running vellum hatch:");
211
+ reporter.warn(" export ANTHROPIC_API_KEY=<your-key>");
212
+ reporter.warn("");
190
213
  }
191
214
 
192
215
  if (!process.env.APP_VERSION) {
193
216
  process.env.APP_VERSION = cliPkg.version;
194
217
  }
195
218
 
196
- emitProgress(2, 6, "Writing configuration...");
219
+ reporter.progress(2, 6, "Writing configuration...");
197
220
  const hatchConfigValues = buildHatchConfigValues(configValues, provider);
198
221
  const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
199
222
 
200
- emitProgress(3, 6, "Starting assistant...");
223
+ reporter.progress(3, 6, "Starting assistant...");
201
224
  const signingKey = generateLocalSigningKey();
225
+ const bootstrapSecret = generateLocalSigningKey();
202
226
  await startLocalDaemon(watch, resources, {
203
227
  defaultWorkspaceConfigPath,
204
228
  signingKey,
205
229
  });
206
230
 
207
- emitProgress(4, 6, "Starting gateway...");
231
+ reporter.progress(4, 6, "Starting gateway...");
208
232
  let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
209
233
  try {
210
- runtimeUrl = await startGateway(watch, resources, { signingKey });
234
+ runtimeUrl = await startGateway(watch, resources, {
235
+ signingKey,
236
+ bootstrapSecret,
237
+ });
211
238
  } catch (error) {
212
239
  // Gateway failed — stop the daemon we just started so we don't leave
213
240
  // orphaned processes with no lock file entry.
214
- console.error(
241
+ reporter.error(
215
242
  `\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
216
243
  );
217
244
  await stopLocalProcesses(resources);
@@ -222,24 +249,28 @@ export async function hatchLocal(
222
249
  // instead of hitting /v1/guardian/init itself. Use loopback to satisfy
223
250
  // the daemon's local-only check — the mDNS runtimeUrl resolves to a LAN
224
251
  // IP which the daemon rejects as non-loopback.
225
- emitProgress(5, 6, "Securing connection...");
252
+ reporter.progress(5, 6, "Securing connection...");
226
253
  const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
227
254
  const maxLeaseAttempts = 3;
228
255
  let guardianAccessToken: string | undefined;
229
256
  for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
230
257
  try {
231
- const tokenData = await leaseGuardianToken(loopbackUrl, instanceName);
258
+ const tokenData = await leaseGuardianToken(
259
+ loopbackUrl,
260
+ instanceName,
261
+ bootstrapSecret,
262
+ );
232
263
  guardianAccessToken = tokenData.accessToken;
233
264
  break;
234
265
  } catch (err) {
235
266
  if (attempt < maxLeaseAttempts) {
236
267
  const delayMs = 2000 * 2 ** (attempt - 1);
237
- console.error(
268
+ reporter.error(
238
269
  `⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
239
270
  );
240
271
  await new Promise((r) => setTimeout(r, delayMs));
241
272
  } else {
242
- console.error(
273
+ reporter.error(
243
274
  `⚠️ Guardian token lease failed after ${maxLeaseAttempts} attempts: ${err}\n` +
244
275
  ` The assistant is running but guardian-token.json was not written.\n` +
245
276
  ` If the desktop app loses its stored credentials, re-hatch to recover.`,
@@ -257,9 +288,10 @@ export async function hatchLocal(
257
288
  species,
258
289
  hatchedAt: new Date().toISOString(),
259
290
  resources: { ...resources, signingKey },
291
+ guardianBootstrapSecret: bootstrapSecret,
260
292
  };
261
293
 
262
- emitProgress(6, 6, "Saving configuration...");
294
+ reporter.progress(6, 6, "Saving configuration...");
263
295
  saveAssistantEntry(localEntry);
264
296
  setActiveAssistant(instanceName);
265
297
 
@@ -268,13 +300,13 @@ export async function hatchLocal(
268
300
  }
269
301
 
270
302
  if (provider !== undefined && provider !== null && !guardianAccessToken) {
271
- console.error(
303
+ reporter.error(
272
304
  `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
273
305
  ` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
274
306
  );
275
307
  } else if (provider !== undefined) {
276
- console.log("");
277
- console.log(
308
+ reporter.log("");
309
+ reporter.log(
278
310
  provider === null
279
311
  ? "Checking provider credentials..."
280
312
  : `Checking ${formatProviderName(provider)} credentials...`,
@@ -287,14 +319,22 @@ export async function hatchLocal(
287
319
  });
288
320
  }
289
321
 
290
- console.log("");
291
- console.log(`✅ Local assistant hatched!`);
292
- console.log("");
293
- console.log("Instance details:");
294
- console.log(` Name: ${instanceName}`);
295
- console.log(` Runtime: ${runtimeUrl}`);
296
- console.log("");
297
- logHatchNextSteps(console.log, instanceName);
322
+ reporter.log("");
323
+ reporter.log(`✅ Local assistant hatched!`);
324
+ reporter.log("");
325
+ reporter.log("Instance details:");
326
+ reporter.log(` Name: ${instanceName}`);
327
+ reporter.log(` Runtime: ${runtimeUrl}`);
328
+ reporter.log("");
329
+ logHatchNextSteps((message) => reporter.log(message), instanceName);
330
+
331
+ const result: HatchLocalResult = {
332
+ assistantId: instanceName,
333
+ runtimeUrl,
334
+ localUrl: `http://127.0.0.1:${resources.gatewayPort}`,
335
+ species,
336
+ guardianAccessToken,
337
+ };
298
338
 
299
339
  if (keepAlive) {
300
340
  const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
@@ -304,7 +344,7 @@ export async function hatchLocal(
304
344
  let consecutiveFailures = 0;
305
345
 
306
346
  const shutdown = async (): Promise<void> => {
307
- console.log("\nShutting down local processes...");
347
+ reporter.log("\nShutting down local processes...");
308
348
  await stopLocalProcesses(resources);
309
349
  process.exit(0);
310
350
  };
@@ -328,7 +368,7 @@ export async function hatchLocal(
328
368
  consecutiveFailures++;
329
369
  }
330
370
  if (consecutiveFailures >= MAX_FAILURES) {
331
- console.log(
371
+ reporter.log(
332
372
  `\n⚠️ ${healthTarget} stopped responding — shutting down.`,
333
373
  );
334
374
  await stopLocalProcesses(resources);
@@ -336,4 +376,6 @@ export async function hatchLocal(
336
376
  }
337
377
  }
338
378
  }
379
+
380
+ return result;
339
381
  }
@@ -8,8 +8,6 @@ export function buildDaemonUrl(port: number): string {
8
8
  /**
9
9
  * Perform an HTTP health check against the daemon's `/healthz` endpoint.
10
10
  * Returns true if the daemon responds with HTTP 200, false otherwise.
11
- *
12
- * This replaces the socket-based `isSocketResponsive()` check.
13
11
  */
14
12
  export async function httpHealthCheck(
15
13
  port: number,
@@ -28,7 +26,7 @@ export async function httpHealthCheck(
28
26
 
29
27
  /**
30
28
  * Poll the daemon's `/healthz` endpoint until it responds with 200 or the
31
- * timeout is reached. This replaces `waitForSocketFile()`.
29
+ * timeout is reached.
32
30
  *
33
31
  * Returns true if the daemon became healthy within the timeout, false otherwise.
34
32
  */
@@ -0,0 +1,31 @@
1
+ import { emitProgress } from "./desktop-progress.js";
2
+
3
+ /**
4
+ * Sink for the human-facing and structured output of long-running lifecycle
5
+ * operations (hatch, retire). Injecting it lets an in-process caller (e.g. a
6
+ * desktop main process embedding these functions) observe progress without the
7
+ * operation writing to the terminal, while the CLI keeps its existing stdout.
8
+ */
9
+ export interface LifecycleReporter {
10
+ /**
11
+ * Coarse step progress. The CLI reporter mirrors this to the desktop
12
+ * `HATCH_PROGRESS:` stdout channel.
13
+ */
14
+ progress(step: number, total: number, label: string): void;
15
+ log(message: string): void;
16
+ warn(message: string): void;
17
+ error(message: string): void;
18
+ }
19
+
20
+ /**
21
+ * Reporter used by the CLI commands: human-readable lines to the console plus
22
+ * structured step events on the desktop progress channel. Reproduces the exact
23
+ * terminal output — and the `HATCH_PROGRESS:` lines under `VELLUM_DESKTOP_APP` —
24
+ * that existing subprocess consumers parse.
25
+ */
26
+ export const consoleLifecycleReporter: LifecycleReporter = {
27
+ progress: (step, total, label) => emitProgress(step, total, label),
28
+ log: (message) => console.log(message),
29
+ warn: (message) => console.warn(message),
30
+ error: (message) => console.error(message),
31
+ };