@vellumai/cli 0.6.3 → 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 (49) 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 -56
  14. package/src/commands/backup.ts +8 -0
  15. package/src/commands/hatch.ts +1 -1
  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 +8 -0
  22. package/src/commands/retire.ts +16 -9
  23. package/src/commands/rollback.ts +32 -33
  24. package/src/commands/ssh-apple-container.ts +162 -0
  25. package/src/commands/ssh.ts +7 -0
  26. package/src/commands/teleport.ts +226 -1
  27. package/src/commands/upgrade.ts +43 -52
  28. package/src/commands/wake.ts +14 -10
  29. package/src/components/DefaultMainScreen.tsx +7 -1
  30. package/src/index.ts +3 -0
  31. package/src/lib/__tests__/docker.test.ts +78 -0
  32. package/src/lib/assistant-config.ts +48 -87
  33. package/src/lib/aws.ts +12 -1
  34. package/src/lib/constants.ts +0 -10
  35. package/src/lib/docker.ts +70 -4
  36. package/src/lib/environments/__tests__/paths.test.ts +234 -0
  37. package/src/lib/environments/__tests__/resolve.test.ts +226 -0
  38. package/src/lib/environments/paths.ts +110 -0
  39. package/src/lib/environments/resolve.ts +96 -0
  40. package/src/lib/environments/seeds.ts +46 -0
  41. package/src/lib/environments/types.ts +60 -0
  42. package/src/lib/gcp.ts +12 -1
  43. package/src/lib/guardian-token.ts +8 -10
  44. package/src/lib/hatch-local.ts +24 -19
  45. package/src/lib/local.ts +46 -5
  46. package/src/lib/orphan-detection.ts +28 -12
  47. package/src/lib/platform-client.ts +220 -24
  48. package/src/lib/retire-apple-container.ts +102 -0
  49. 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;
@@ -570,7 +570,7 @@ async function hatchVellumPlatform(): Promise<void> {
570
570
  console.log(" Hatching assistant on Vellum platform...");
571
571
  console.log("");
572
572
 
573
- const result = await hatchAssistant(token);
573
+ const { assistant: result } = await hatchAssistant(token);
574
574
 
575
575
  const platformUrl = getPlatformUrl();
576
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