@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.
- package/AGENTS.md +12 -2
- package/README.md +3 -3
- package/bunfig.toml +6 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +124 -0
- package/src/__tests__/env-drift.test.ts +87 -0
- package/src/__tests__/guardian-token.test.ts +172 -0
- package/src/__tests__/multi-local.test.ts +61 -14
- package/src/__tests__/orphan-detection.test.ts +214 -0
- package/src/__tests__/platform-client.test.ts +204 -0
- package/src/__tests__/preload.ts +27 -0
- package/src/__tests__/ssh-user-guard.test.ts +28 -0
- package/src/__tests__/teleport.test.ts +1073 -57
- package/src/commands/backup.ts +8 -0
- package/src/commands/hatch.ts +5 -28
- package/src/commands/login.ts +178 -9
- package/src/commands/logs.ts +652 -0
- package/src/commands/pair.ts +9 -1
- package/src/commands/ps.ts +37 -7
- package/src/commands/recover.ts +8 -4
- package/src/commands/restore.ts +124 -12
- package/src/commands/retire.ts +17 -3
- package/src/commands/rollback.ts +32 -33
- package/src/commands/sleep.ts +7 -0
- package/src/commands/ssh-apple-container.ts +162 -0
- package/src/commands/ssh.ts +7 -0
- package/src/commands/teleport.ts +307 -3
- package/src/commands/upgrade.ts +43 -52
- package/src/commands/wake.ts +21 -10
- package/src/components/DefaultMainScreen.tsx +7 -1
- package/src/index.ts +3 -0
- package/src/lib/__tests__/docker.test.ts +78 -0
- package/src/lib/assistant-config.ts +54 -87
- package/src/lib/aws.ts +12 -1
- package/src/lib/constants.ts +0 -10
- package/src/lib/docker.ts +73 -4
- package/src/lib/environments/__tests__/paths.test.ts +234 -0
- package/src/lib/environments/__tests__/resolve.test.ts +226 -0
- package/src/lib/environments/paths.ts +110 -0
- package/src/lib/environments/resolve.ts +96 -0
- package/src/lib/environments/seeds.ts +46 -0
- package/src/lib/environments/types.ts +60 -0
- package/src/lib/gcp.ts +12 -1
- package/src/lib/guardian-token.ts +8 -10
- package/src/lib/hatch-local.ts +30 -35
- package/src/lib/local.ts +46 -5
- package/src/lib/orphan-detection.ts +28 -12
- package/src/lib/platform-client.ts +261 -25
- package/src/lib/retire-apple-container.ts +102 -0
- package/src/lib/upgrade-lifecycle.ts +101 -28
package/src/commands/backup.ts
CHANGED
|
@@ -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;
|
package/src/commands/hatch.ts
CHANGED
|
@@ -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, --
|
|
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
|
-
|
|
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,
|
|
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
|
|
package/src/commands/login.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
253
|
+
console.error("Not logged in. Run `vellum login` first.");
|
|
85
254
|
process.exit(1);
|
|
86
255
|
}
|
|
87
256
|
|