@vellumai/cli 0.6.2 → 0.6.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.
Files changed (50) hide show
  1. package/AGENTS.md +12 -2
  2. package/README.md +3 -3
  3. package/bunfig.toml +6 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/assistant-config.test.ts +124 -0
  6. package/src/__tests__/env-drift.test.ts +87 -0
  7. package/src/__tests__/guardian-token.test.ts +172 -0
  8. package/src/__tests__/multi-local.test.ts +61 -14
  9. package/src/__tests__/orphan-detection.test.ts +214 -0
  10. package/src/__tests__/platform-client.test.ts +204 -0
  11. package/src/__tests__/preload.ts +27 -0
  12. package/src/__tests__/ssh-user-guard.test.ts +28 -0
  13. package/src/__tests__/teleport.test.ts +1073 -57
  14. package/src/commands/backup.ts +8 -0
  15. package/src/commands/hatch.ts +5 -28
  16. package/src/commands/login.ts +178 -9
  17. package/src/commands/logs.ts +652 -0
  18. package/src/commands/pair.ts +9 -1
  19. package/src/commands/ps.ts +37 -7
  20. package/src/commands/recover.ts +8 -4
  21. package/src/commands/restore.ts +124 -12
  22. package/src/commands/retire.ts +17 -3
  23. package/src/commands/rollback.ts +32 -33
  24. package/src/commands/sleep.ts +7 -0
  25. package/src/commands/ssh-apple-container.ts +162 -0
  26. package/src/commands/ssh.ts +7 -0
  27. package/src/commands/teleport.ts +307 -3
  28. package/src/commands/upgrade.ts +43 -52
  29. package/src/commands/wake.ts +21 -10
  30. package/src/components/DefaultMainScreen.tsx +7 -1
  31. package/src/index.ts +3 -0
  32. package/src/lib/__tests__/docker.test.ts +78 -0
  33. package/src/lib/assistant-config.ts +54 -87
  34. package/src/lib/aws.ts +12 -1
  35. package/src/lib/constants.ts +0 -10
  36. package/src/lib/docker.ts +73 -4
  37. package/src/lib/environments/__tests__/paths.test.ts +234 -0
  38. package/src/lib/environments/__tests__/resolve.test.ts +226 -0
  39. package/src/lib/environments/paths.ts +110 -0
  40. package/src/lib/environments/resolve.ts +96 -0
  41. package/src/lib/environments/seeds.ts +46 -0
  42. package/src/lib/environments/types.ts +60 -0
  43. package/src/lib/gcp.ts +12 -1
  44. package/src/lib/guardian-token.ts +8 -10
  45. package/src/lib/hatch-local.ts +30 -35
  46. package/src/lib/local.ts +46 -5
  47. package/src/lib/orphan-detection.ts +28 -12
  48. package/src/lib/platform-client.ts +261 -25
  49. package/src/lib/retire-apple-container.ts +102 -0
  50. package/src/lib/upgrade-lifecycle.ts +101 -28
@@ -64,6 +64,14 @@ export async function backup(): Promise<void> {
64
64
  // Detect topology and route platform assistants through Django export
65
65
  const cloud =
66
66
  entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
67
+
68
+ if (cloud === "apple-container") {
69
+ console.error(
70
+ `Error: '${name}' uses the Apple Containers runtime. Backup is not yet supported for this topology.`,
71
+ );
72
+ process.exit(1);
73
+ }
74
+
67
75
  if (cloud === "vellum") {
68
76
  await backupPlatform(name, outputArg, entry.runtimeUrl);
69
77
  return;
@@ -176,7 +176,6 @@ interface HatchArgs {
176
176
  keepAlive: boolean;
177
177
  name: string | null;
178
178
  remote: RemoteHost;
179
- restart: boolean;
180
179
  watch: boolean;
181
180
  configValues: Record<string, string>;
182
181
  }
@@ -188,7 +187,6 @@ function parseArgs(): HatchArgs {
188
187
  let keepAlive = false;
189
188
  let name: string | null = null;
190
189
  let remote: RemoteHost = DEFAULT_REMOTE;
191
- let restart = false;
192
190
  let watch = false;
193
191
  const configValues: Record<string, string> = {};
194
192
 
@@ -209,9 +207,6 @@ function parseArgs(): HatchArgs {
209
207
  console.log(
210
208
  " --remote <host> Remote host (local, gcp, aws, docker, custom, vellum)",
211
209
  );
212
- console.log(
213
- " --restart Restart processes without onboarding side effects",
214
- );
215
210
  console.log(
216
211
  " --watch Run assistant and gateway in watch mode (hot reload on source changes)",
217
212
  );
@@ -224,8 +219,6 @@ function parseArgs(): HatchArgs {
224
219
  process.exit(0);
225
220
  } else if (arg === "-d") {
226
221
  detached = true;
227
- } else if (arg === "--restart") {
228
- restart = true;
229
222
  } else if (arg === "--watch") {
230
223
  watch = true;
231
224
  } else if (arg === "--keep-alive") {
@@ -277,7 +270,7 @@ function parseArgs(): HatchArgs {
277
270
  species = arg as Species;
278
271
  } else {
279
272
  console.error(
280
- `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --restart, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
273
+ `Error: Unknown argument '${arg}'. Valid options: ${VALID_SPECIES.join(", ")}, -d, --watch, --keep-alive, --name <name>, --remote <${VALID_REMOTE_HOSTS.join("|")}>, --config <key=value>`,
281
274
  );
282
275
  process.exit(1);
283
276
  }
@@ -289,7 +282,6 @@ function parseArgs(): HatchArgs {
289
282
  keepAlive,
290
283
  name,
291
284
  remote,
292
- restart,
293
285
  watch,
294
286
  configValues,
295
287
  };
@@ -516,23 +508,8 @@ export async function hatch(): Promise<void> {
516
508
  const cliVersion = getCliVersion();
517
509
  console.log(`@vellumai/cli v${cliVersion}`);
518
510
 
519
- const {
520
- species,
521
- detached,
522
- keepAlive,
523
- name,
524
- remote,
525
- restart,
526
- watch,
527
- configValues,
528
- } = parseArgs();
529
-
530
- if (restart && remote !== "local") {
531
- console.error(
532
- "Error: --restart is only supported for local hatch targets.",
533
- );
534
- process.exit(1);
535
- }
511
+ const { species, detached, keepAlive, name, remote, watch, configValues } =
512
+ parseArgs();
536
513
 
537
514
  if (watch && remote !== "local" && remote !== "docker") {
538
515
  console.error(
@@ -542,7 +519,7 @@ export async function hatch(): Promise<void> {
542
519
  }
543
520
 
544
521
  if (remote === "local") {
545
- await hatchLocal(species, name, restart, watch, keepAlive, configValues);
522
+ await hatchLocal(species, name, watch, keepAlive, configValues);
546
523
  return;
547
524
  }
548
525
 
@@ -593,7 +570,7 @@ async function hatchVellumPlatform(): Promise<void> {
593
570
  console.log(" Hatching assistant on Vellum platform...");
594
571
  console.log("");
595
572
 
596
- const result = await hatchAssistant(token);
573
+ const { assistant: result } = await hatchAssistant(token);
597
574
 
598
575
  const platformUrl = getPlatformUrl();
599
576
 
@@ -1,20 +1,141 @@
1
+ import { createServer } from "http";
2
+ import { spawn } from "child_process";
3
+ import { randomBytes } from "crypto";
4
+
5
+ import {
6
+ findAssistantByName,
7
+ getActiveAssistant,
8
+ loadLatestAssistant,
9
+ } from "../lib/assistant-config";
10
+ import { computeDeviceId } from "../lib/guardian-token";
1
11
  import {
2
12
  clearPlatformToken,
13
+ ensureSelfHostedLocalRegistration,
3
14
  fetchCurrentUser,
15
+ fetchOrganizationId,
16
+ getPlatformUrl,
17
+ injectCredentialsIntoAssistant,
4
18
  readPlatformToken,
5
19
  savePlatformToken,
6
20
  } from "../lib/platform-client";
7
21
 
22
+ const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
23
+
24
+ /**
25
+ * Open a URL in the user's default browser.
26
+ */
27
+ function openBrowser(url: string): void {
28
+ const platform = process.platform;
29
+ const cmd =
30
+ platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
31
+ const args =
32
+ platform === "win32"
33
+ ? ["/c", "start", '""', url.replace(/&/g, "^&")]
34
+ : [url];
35
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
36
+ child.on("error", () => {
37
+ // Silently ignore — the user can still copy the URL from the console
38
+ });
39
+ child.unref();
40
+ }
41
+
42
+ /**
43
+ * Start a local HTTP server, open the browser to the platform login page,
44
+ * and wait for the platform to redirect back with the session token.
45
+ */
46
+ function browserLogin(platformUrl: string): Promise<string> {
47
+ return new Promise((resolve, reject) => {
48
+ const state = randomBytes(32).toString("hex");
49
+
50
+ const server = createServer((req, res) => {
51
+ const url = new URL(req.url ?? "/", `http://localhost`);
52
+
53
+ if (url.pathname !== "/callback") {
54
+ res.writeHead(404, { "Content-Type": "text/plain" });
55
+ res.end("Not found");
56
+ return;
57
+ }
58
+
59
+ const receivedState = url.searchParams.get("state");
60
+ const sessionToken = url.searchParams.get("session_token");
61
+
62
+ if (receivedState !== state) {
63
+ res.writeHead(400, { "Content-Type": "text/html" });
64
+ res.end(
65
+ "<html><body><h2>Login failed</h2><p>State mismatch. Please try again.</p></body></html>",
66
+ );
67
+ cleanup("State mismatch — possible CSRF attack.");
68
+ return;
69
+ }
70
+
71
+ if (!sessionToken) {
72
+ res.writeHead(400, { "Content-Type": "text/html" });
73
+ res.end(
74
+ "<html><body><h2>Login failed</h2><p>No session token received. Please try again.</p></body></html>",
75
+ );
76
+ cleanup("No session token received from platform.");
77
+ return;
78
+ }
79
+
80
+ res.writeHead(200, { "Content-Type": "text/html" });
81
+ res.end(
82
+ "<html><body><h2>Login successful!</h2><p>You can close this window and return to your terminal.</p></body></html>",
83
+ );
84
+ cleanup(null, sessionToken);
85
+ });
86
+
87
+ const timeout = setTimeout(() => {
88
+ cleanup("Login timed out. Please try again.");
89
+ }, LOGIN_TIMEOUT_MS);
90
+
91
+ function cleanup(error: string | null, token?: string): void {
92
+ clearTimeout(timeout);
93
+ server.close();
94
+ if (error) {
95
+ reject(new Error(error));
96
+ } else if (token) {
97
+ resolve(token);
98
+ } else {
99
+ reject(new Error("Unknown error during login."));
100
+ }
101
+ }
102
+
103
+ server.on("error", (err) => cleanup(err.message));
104
+ server.listen(0, "127.0.0.1", () => {
105
+ const addr = server.address();
106
+ if (!addr || typeof addr === "string") {
107
+ cleanup("Failed to start local server.");
108
+ return;
109
+ }
110
+
111
+ const port = addr.port;
112
+ const returnTo = `/accounts/cli/callback?port=${port}&state=${state}`;
113
+ const loginUrl = `${platformUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
114
+
115
+ console.log("Opening browser for login...");
116
+ console.log(`If the browser doesn't open, visit: ${loginUrl}`);
117
+ openBrowser(loginUrl);
118
+ });
119
+ });
120
+ }
121
+
8
122
  export async function login(): Promise<void> {
9
123
  const args = process.argv.slice(3);
10
124
 
11
125
  if (args.includes("--help") || args.includes("-h")) {
12
- console.log("Usage: vellum login --token <session-token>");
126
+ console.log("Usage: vellum login [--token <session-token>]");
13
127
  console.log("");
14
128
  console.log("Log in to the Vellum platform.");
15
129
  console.log("");
130
+ console.log("By default, opens a browser window for authentication.");
131
+ console.log("Alternatively, pass a session token directly with --token.");
132
+ console.log("");
16
133
  console.log("Options:");
17
134
  console.log(" --token <token> Session token from the Vellum platform");
135
+ console.log("");
136
+ console.log("Examples:");
137
+ console.log(" vellum login");
138
+ console.log(" vellum login --token <session-token>");
18
139
  process.exit(0);
19
140
  }
20
141
 
@@ -31,14 +152,15 @@ export async function login(): Promise<void> {
31
152
  }
32
153
  }
33
154
 
155
+ // If no --token flag, use browser-based login
34
156
  if (!token) {
35
- console.error("Usage: vellum login --token <session-token>");
36
- console.error("");
37
- console.error("To get your session token:");
38
- console.error(" 1. Log in to the Vellum platform in your browser");
39
- console.error(" 2. Open Developer Tools Application Cookies");
40
- console.error(" 3. Copy the value of the session token");
41
- process.exit(1);
157
+ const platformUrl = getPlatformUrl();
158
+ try {
159
+ token = await browserLogin(platformUrl);
160
+ } catch (error) {
161
+ console.error(`❌ ${error instanceof Error ? error.message : error}`);
162
+ process.exit(1);
163
+ }
42
164
  }
43
165
 
44
166
  console.log("Validating token...");
@@ -47,6 +169,53 @@ export async function login(): Promise<void> {
47
169
  const user = await fetchCurrentUser(token);
48
170
  savePlatformToken(token);
49
171
  console.log(`✅ Logged in as ${user.email}`);
172
+
173
+ // Register the local assistant with the platform (non-fatal).
174
+ // Mirrors the desktop app's LocalAssistantBootstrapService flow.
175
+ try {
176
+ const activeName = getActiveAssistant();
177
+ const entry = activeName
178
+ ? findAssistantByName(activeName)
179
+ : loadLatestAssistant();
180
+
181
+ // Skip managed ("vellum") assistants — they are handled by the platform.
182
+ if (entry && entry.cloud !== "vellum") {
183
+ const orgId = await fetchOrganizationId(token);
184
+ const clientInstallationId = computeDeviceId();
185
+ const registration = await ensureSelfHostedLocalRegistration(
186
+ token,
187
+ orgId,
188
+ clientInstallationId,
189
+ entry.assistantId,
190
+ "cli",
191
+ );
192
+ console.log(
193
+ `Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
194
+ );
195
+
196
+ // Inject credentials into the running assistant via the gateway,
197
+ // mirroring the desktop app's LocalAssistantBootstrapService flow.
198
+ const allInjected = await injectCredentialsIntoAssistant({
199
+ gatewayUrl: entry.runtimeUrl,
200
+ bearerToken: entry.bearerToken,
201
+ assistantApiKey: registration.assistant_api_key,
202
+ platformAssistantId: registration.assistant.id,
203
+ platformBaseUrl: getPlatformUrl(),
204
+ organizationId: orgId,
205
+ userId: user.id,
206
+ webhookSecret: registration.webhook_secret,
207
+ });
208
+ if (allInjected) {
209
+ console.log("Injected platform credentials into assistant.");
210
+ } else {
211
+ console.warn(
212
+ "Some credentials could not be injected into the assistant.",
213
+ );
214
+ }
215
+ }
216
+ } catch {
217
+ // Non-fatal — login succeeded even if registration fails
218
+ }
50
219
  } catch (error) {
51
220
  console.error(
52
221
  `❌ Login failed: ${error instanceof Error ? error.message : error}`,
@@ -81,7 +250,7 @@ export async function whoami(): Promise<void> {
81
250
 
82
251
  const token = readPlatformToken();
83
252
  if (!token) {
84
- console.error("Not logged in. Run `vellum login --token <token>` first.");
253
+ console.error("Not logged in. Run `vellum login` first.");
85
254
  process.exit(1);
86
255
  }
87
256