@vellumai/cli 0.4.55 → 0.4.57
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 +5 -10
- package/bun.lock +3 -70
- package/package.json +2 -3
- package/src/__tests__/random-name.test.ts +24 -5
- package/src/adapters/install.sh +1 -1
- package/src/adapters/openclaw.ts +6 -3
- package/src/commands/client.ts +4 -4
- package/src/commands/hatch.ts +80 -157
- package/src/commands/pair.ts +19 -3
- package/src/commands/ps.ts +88 -2
- package/src/commands/retire.ts +31 -7
- package/src/commands/upgrade.ts +366 -0
- package/src/commands/wake.ts +25 -6
- package/src/components/DefaultMainScreen.tsx +1 -1
- package/src/index.ts +6 -1
- package/src/lib/assistant-config.ts +11 -2
- package/src/lib/aws.ts +10 -38
- package/src/lib/constants.ts +7 -0
- package/src/lib/docker.ts +665 -300
- package/src/lib/gcp.ts +13 -14
- package/src/lib/guardian-token.ts +191 -0
- package/src/lib/health-check.ts +6 -30
- package/src/lib/local.ts +150 -27
- package/src/lib/platform-client.ts +24 -0
- package/src/lib/process.ts +2 -2
- package/src/lib/random-name.ts +17 -1
- package/src/lib/jwt.ts +0 -62
- package/src/lib/policy.ts +0 -7
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import cliPkg from "../../package.json";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findAssistantByName,
|
|
5
|
+
getActiveAssistant,
|
|
6
|
+
loadAllAssistants,
|
|
7
|
+
} from "../lib/assistant-config";
|
|
8
|
+
import type { AssistantEntry } from "../lib/assistant-config";
|
|
9
|
+
import {
|
|
10
|
+
DOCKERHUB_IMAGES,
|
|
11
|
+
DOCKER_READY_TIMEOUT_MS,
|
|
12
|
+
GATEWAY_INTERNAL_PORT,
|
|
13
|
+
dockerResourceNames,
|
|
14
|
+
startContainers,
|
|
15
|
+
stopContainers,
|
|
16
|
+
} from "../lib/docker";
|
|
17
|
+
import type { ServiceName } from "../lib/docker";
|
|
18
|
+
import {
|
|
19
|
+
fetchOrganizationId,
|
|
20
|
+
getPlatformUrl,
|
|
21
|
+
readPlatformToken,
|
|
22
|
+
} from "../lib/platform-client";
|
|
23
|
+
import { exec, execOutput } from "../lib/step-runner";
|
|
24
|
+
|
|
25
|
+
interface UpgradeArgs {
|
|
26
|
+
name: string | null;
|
|
27
|
+
version: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseArgs(): UpgradeArgs {
|
|
31
|
+
const args = process.argv.slice(3);
|
|
32
|
+
let name: string | null = null;
|
|
33
|
+
let version: string | null = null;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
const arg = args[i];
|
|
37
|
+
if (arg === "--help" || arg === "-h") {
|
|
38
|
+
console.log("Usage: vellum upgrade [<name>] [options]");
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("Upgrade an assistant to the latest version.");
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log("Arguments:");
|
|
43
|
+
console.log(
|
|
44
|
+
" <name> Name of the assistant to upgrade (default: active or only assistant)",
|
|
45
|
+
);
|
|
46
|
+
console.log("");
|
|
47
|
+
console.log("Options:");
|
|
48
|
+
console.log(
|
|
49
|
+
" --version <version> Target version to upgrade to (default: latest)",
|
|
50
|
+
);
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log("Examples:");
|
|
53
|
+
console.log(
|
|
54
|
+
" vellum upgrade # Upgrade the active assistant to the latest version",
|
|
55
|
+
);
|
|
56
|
+
console.log(
|
|
57
|
+
" vellum upgrade my-assistant # Upgrade a specific assistant by name",
|
|
58
|
+
);
|
|
59
|
+
console.log(
|
|
60
|
+
" vellum upgrade my-assistant --version v1.2.3 # Upgrade to a specific version",
|
|
61
|
+
);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
} else if (arg === "--version") {
|
|
64
|
+
const next = args[i + 1];
|
|
65
|
+
if (!next || next.startsWith("-")) {
|
|
66
|
+
console.error("Error: --version requires a value");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
version = next;
|
|
70
|
+
i++;
|
|
71
|
+
} else if (!arg.startsWith("-")) {
|
|
72
|
+
name = arg;
|
|
73
|
+
} else {
|
|
74
|
+
console.error(`Error: Unknown option '${arg}'.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { name, version };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveCloud(entry: AssistantEntry): string {
|
|
83
|
+
if (entry.cloud) {
|
|
84
|
+
return entry.cloud;
|
|
85
|
+
}
|
|
86
|
+
if (entry.project) {
|
|
87
|
+
return "gcp";
|
|
88
|
+
}
|
|
89
|
+
if (entry.sshUser) {
|
|
90
|
+
return "custom";
|
|
91
|
+
}
|
|
92
|
+
return "local";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve which assistant to target for the upgrade command. Priority:
|
|
97
|
+
* 1. Explicit name argument
|
|
98
|
+
* 2. Active assistant set via `vellum use`
|
|
99
|
+
* 3. Sole assistant (when exactly one exists)
|
|
100
|
+
*/
|
|
101
|
+
function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
|
|
102
|
+
if (nameArg) {
|
|
103
|
+
const entry = findAssistantByName(nameArg);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
console.error(`No assistant found with name '${nameArg}'.`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const active = getActiveAssistant();
|
|
112
|
+
if (active) {
|
|
113
|
+
const entry = findAssistantByName(active);
|
|
114
|
+
if (entry) return entry;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const all = loadAllAssistants();
|
|
118
|
+
if (all.length === 1) return all[0];
|
|
119
|
+
|
|
120
|
+
if (all.length === 0) {
|
|
121
|
+
console.error("No assistants found. Run 'vellum hatch' first.");
|
|
122
|
+
} else {
|
|
123
|
+
console.error(
|
|
124
|
+
"Multiple assistants found. Specify a name or set an active assistant with 'vellum use <name>'.",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Capture environment variables from a running Docker container so they
|
|
132
|
+
* can be replayed onto the replacement container after upgrade.
|
|
133
|
+
*/
|
|
134
|
+
async function captureContainerEnv(
|
|
135
|
+
containerName: string,
|
|
136
|
+
): Promise<Record<string, string>> {
|
|
137
|
+
const captured: Record<string, string> = {};
|
|
138
|
+
try {
|
|
139
|
+
const raw = await execOutput("docker", [
|
|
140
|
+
"inspect",
|
|
141
|
+
"--format",
|
|
142
|
+
"{{json .Config.Env}}",
|
|
143
|
+
containerName,
|
|
144
|
+
]);
|
|
145
|
+
const entries = JSON.parse(raw) as string[];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const eqIdx = entry.indexOf("=");
|
|
148
|
+
if (eqIdx > 0) {
|
|
149
|
+
captured[entry.slice(0, eqIdx)] = entry.slice(eqIdx + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Container may not exist or not be inspectable
|
|
154
|
+
}
|
|
155
|
+
return captured;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Poll the gateway `/readyz` endpoint until it returns 200 or the timeout
|
|
160
|
+
* elapses. Returns whether the assistant became ready.
|
|
161
|
+
*/
|
|
162
|
+
async function waitForReady(runtimeUrl: string): Promise<boolean> {
|
|
163
|
+
const readyUrl = `${runtimeUrl}/readyz`;
|
|
164
|
+
const start = Date.now();
|
|
165
|
+
|
|
166
|
+
while (Date.now() - start < DOCKER_READY_TIMEOUT_MS) {
|
|
167
|
+
try {
|
|
168
|
+
const resp = await fetch(readyUrl, {
|
|
169
|
+
signal: AbortSignal.timeout(5000),
|
|
170
|
+
});
|
|
171
|
+
if (resp.ok) {
|
|
172
|
+
const elapsedSec = ((Date.now() - start) / 1000).toFixed(1);
|
|
173
|
+
console.log(`Assistant ready after ${elapsedSec}s`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
let detail = "";
|
|
177
|
+
try {
|
|
178
|
+
const body = await resp.text();
|
|
179
|
+
const json = JSON.parse(body);
|
|
180
|
+
const parts = [json.status];
|
|
181
|
+
if (json.upstream != null) parts.push(`upstream=${json.upstream}`);
|
|
182
|
+
detail = ` — ${parts.join(", ")}`;
|
|
183
|
+
} catch {
|
|
184
|
+
// ignore parse errors
|
|
185
|
+
}
|
|
186
|
+
console.log(`Readiness check: ${resp.status}${detail} (retrying...)`);
|
|
187
|
+
} catch {
|
|
188
|
+
// Connection refused / timeout — not up yet
|
|
189
|
+
}
|
|
190
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function upgradeDocker(
|
|
197
|
+
entry: AssistantEntry,
|
|
198
|
+
version: string | null,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const instanceName = entry.assistantId;
|
|
201
|
+
const res = dockerResourceNames(instanceName);
|
|
202
|
+
|
|
203
|
+
const versionTag =
|
|
204
|
+
version ?? (cliPkg.version ? `v${cliPkg.version}` : "latest");
|
|
205
|
+
const imageTags: Record<ServiceName, string> = {
|
|
206
|
+
assistant: `${DOCKERHUB_IMAGES.assistant}:${versionTag}`,
|
|
207
|
+
"credential-executor": `${DOCKERHUB_IMAGES["credential-executor"]}:${versionTag}`,
|
|
208
|
+
gateway: `${DOCKERHUB_IMAGES.gateway}:${versionTag}`,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
console.log(
|
|
212
|
+
`🔄 Upgrading Docker assistant '${instanceName}' to ${versionTag}...\n`,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
console.log("📦 Pulling new Docker images...");
|
|
216
|
+
await exec("docker", ["pull", imageTags.assistant]);
|
|
217
|
+
await exec("docker", ["pull", imageTags.gateway]);
|
|
218
|
+
await exec("docker", ["pull", imageTags["credential-executor"]]);
|
|
219
|
+
console.log("✅ Docker images pulled\n");
|
|
220
|
+
|
|
221
|
+
console.log("💾 Capturing existing container environment...");
|
|
222
|
+
const capturedEnv = await captureContainerEnv(res.assistantContainer);
|
|
223
|
+
console.log(
|
|
224
|
+
` Captured ${Object.keys(capturedEnv).length} env var(s) from ${res.assistantContainer}\n`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
console.log("🛑 Stopping existing containers...");
|
|
228
|
+
await stopContainers(res);
|
|
229
|
+
console.log("✅ Containers stopped\n");
|
|
230
|
+
|
|
231
|
+
// Parse gateway port from entry's runtimeUrl, fall back to default
|
|
232
|
+
let gatewayPort = GATEWAY_INTERNAL_PORT;
|
|
233
|
+
try {
|
|
234
|
+
const parsed = new URL(entry.runtimeUrl);
|
|
235
|
+
const port = parseInt(parsed.port, 10);
|
|
236
|
+
if (!isNaN(port)) {
|
|
237
|
+
gatewayPort = port;
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// use default
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Build the set of extra env vars to replay on the new assistant container.
|
|
244
|
+
// Captured env vars serve as the base; keys already managed by
|
|
245
|
+
// serviceDockerRunArgs are excluded to avoid duplicates.
|
|
246
|
+
const envKeysSetByRunArgs = new Set([
|
|
247
|
+
"VELLUM_ASSISTANT_NAME",
|
|
248
|
+
"RUNTIME_HTTP_HOST",
|
|
249
|
+
"PATH",
|
|
250
|
+
]);
|
|
251
|
+
// Only exclude keys that serviceDockerRunArgs will actually set
|
|
252
|
+
for (const envVar of ["ANTHROPIC_API_KEY", "VELLUM_PLATFORM_URL"]) {
|
|
253
|
+
if (process.env[envVar]) {
|
|
254
|
+
envKeysSetByRunArgs.add(envVar);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const extraAssistantEnv: Record<string, string> = {};
|
|
258
|
+
for (const [key, value] of Object.entries(capturedEnv)) {
|
|
259
|
+
if (!envKeysSetByRunArgs.has(key)) {
|
|
260
|
+
extraAssistantEnv[key] = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log("🚀 Starting upgraded containers...");
|
|
265
|
+
await startContainers(
|
|
266
|
+
{
|
|
267
|
+
extraAssistantEnv,
|
|
268
|
+
gatewayPort,
|
|
269
|
+
imageTags,
|
|
270
|
+
instanceName,
|
|
271
|
+
res,
|
|
272
|
+
},
|
|
273
|
+
(msg) => console.log(msg),
|
|
274
|
+
);
|
|
275
|
+
console.log("✅ Containers started\n");
|
|
276
|
+
|
|
277
|
+
console.log("Waiting for assistant to become ready...");
|
|
278
|
+
const ready = await waitForReady(entry.runtimeUrl);
|
|
279
|
+
if (ready) {
|
|
280
|
+
console.log(
|
|
281
|
+
`\n✅ Docker assistant '${instanceName}' upgraded to ${versionTag}.`,
|
|
282
|
+
);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(
|
|
285
|
+
`\n⚠️ Containers are running but the assistant did not become ready within the timeout.`,
|
|
286
|
+
);
|
|
287
|
+
console.log(` Check logs with: docker logs -f ${res.assistantContainer}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface UpgradeApiResponse {
|
|
292
|
+
detail: string;
|
|
293
|
+
version: string | null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function upgradePlatform(
|
|
297
|
+
entry: AssistantEntry,
|
|
298
|
+
version: string | null,
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
console.log(
|
|
301
|
+
`🔄 Upgrading platform-hosted assistant '${entry.assistantId}'...\n`,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const token = readPlatformToken();
|
|
305
|
+
if (!token) {
|
|
306
|
+
console.error(
|
|
307
|
+
"Error: Not logged in. Run `vellum login --token <token>` first.",
|
|
308
|
+
);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const orgId = await fetchOrganizationId(token);
|
|
313
|
+
|
|
314
|
+
const url = `${getPlatformUrl()}/v1/assistants/upgrade/`;
|
|
315
|
+
const body: { assistant_id?: string; version?: string } = {
|
|
316
|
+
assistant_id: entry.assistantId,
|
|
317
|
+
};
|
|
318
|
+
if (version) {
|
|
319
|
+
body.version = version;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const response = await fetch(url, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: {
|
|
325
|
+
"Content-Type": "application/json",
|
|
326
|
+
"X-Session-Token": token,
|
|
327
|
+
"Vellum-Organization-Id": orgId,
|
|
328
|
+
},
|
|
329
|
+
body: JSON.stringify(body),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
const text = await response.text();
|
|
334
|
+
console.error(
|
|
335
|
+
`Error: Platform upgrade failed (${response.status}): ${text}`,
|
|
336
|
+
);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const result = (await response.json()) as UpgradeApiResponse;
|
|
341
|
+
console.log(`✅ ${result.detail}`);
|
|
342
|
+
if (result.version) {
|
|
343
|
+
console.log(` Version: ${result.version}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function upgrade(): Promise<void> {
|
|
348
|
+
const { name, version } = parseArgs();
|
|
349
|
+
const entry = resolveTargetAssistant(name);
|
|
350
|
+
const cloud = resolveCloud(entry);
|
|
351
|
+
|
|
352
|
+
if (cloud === "docker") {
|
|
353
|
+
await upgradeDocker(entry, version);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (cloud === "vellum") {
|
|
358
|
+
await upgradePlatform(entry, version);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.error(
|
|
363
|
+
`Error: Upgrade is not supported for '${cloud}' assistants. Only 'docker' and 'vellum' assistants can be upgraded via the CLI.`,
|
|
364
|
+
);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { join } from "path";
|
|
|
4
4
|
import { resolveTargetAssistant } from "../lib/assistant-config.js";
|
|
5
5
|
import { isProcessAlive, stopProcessByPidFile } from "../lib/process";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
isAssistantWatchModeAvailable,
|
|
8
|
+
isGatewayWatchModeAvailable,
|
|
8
9
|
startLocalDaemon,
|
|
9
10
|
startGateway,
|
|
10
11
|
} from "../lib/local";
|
|
@@ -24,12 +25,16 @@ export async function wake(): Promise<void> {
|
|
|
24
25
|
console.log("");
|
|
25
26
|
console.log("Options:");
|
|
26
27
|
console.log(
|
|
27
|
-
" --watch
|
|
28
|
+
" --watch Run assistant and gateway in watch mode (hot reload on source changes)",
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
" --foreground Run assistant in foreground with logs printed to terminal",
|
|
28
32
|
);
|
|
29
33
|
process.exit(0);
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const watch = args.includes("--watch");
|
|
37
|
+
const foreground = args.includes("--foreground");
|
|
33
38
|
const nameArg = args.find((a) => !a.startsWith("-"));
|
|
34
39
|
const entry = resolveTargetAssistant(nameArg);
|
|
35
40
|
|
|
@@ -64,7 +69,7 @@ export async function wake(): Promise<void> {
|
|
|
64
69
|
// Watch mode requires bun --watch with .ts sources; packaged desktop
|
|
65
70
|
// builds only have a compiled binary. Stopping the daemon without a
|
|
66
71
|
// viable watch-mode path would leave the user with no running assistant.
|
|
67
|
-
if (!
|
|
72
|
+
if (!isAssistantWatchModeAvailable()) {
|
|
68
73
|
console.log(
|
|
69
74
|
`Assistant running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
70
75
|
);
|
|
@@ -85,7 +90,7 @@ export async function wake(): Promise<void> {
|
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
if (!daemonRunning) {
|
|
88
|
-
await startLocalDaemon(watch, resources);
|
|
93
|
+
await startLocalDaemon(watch, resources, { foreground });
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
// Start gateway
|
|
@@ -95,8 +100,8 @@ export async function wake(): Promise<void> {
|
|
|
95
100
|
const { alive, pid } = isProcessAlive(gatewayPidFile);
|
|
96
101
|
if (alive) {
|
|
97
102
|
if (watch) {
|
|
98
|
-
//
|
|
99
|
-
if (!
|
|
103
|
+
// Guard gateway restart separately: check gateway source availability.
|
|
104
|
+
if (!isGatewayWatchModeAvailable()) {
|
|
100
105
|
console.log(
|
|
101
106
|
`Gateway running (pid ${pid}) — watch mode not available (no source files). Keeping existing process.`,
|
|
102
107
|
);
|
|
@@ -131,4 +136,18 @@ export async function wake(): Promise<void> {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
console.log("Wake complete.");
|
|
139
|
+
|
|
140
|
+
if (foreground) {
|
|
141
|
+
console.log("Running in foreground (Ctrl+C to stop)...\n");
|
|
142
|
+
// Block forever — the daemon is running with inherited stdio so its
|
|
143
|
+
// output streams to this terminal. When the user hits Ctrl+C, SIGINT
|
|
144
|
+
// propagates to the daemon child and both exit.
|
|
145
|
+
await new Promise<void>((resolve) => {
|
|
146
|
+
process.on("SIGINT", () => {
|
|
147
|
+
console.log("\nShutting down...");
|
|
148
|
+
resolve();
|
|
149
|
+
});
|
|
150
|
+
process.on("SIGTERM", () => resolve());
|
|
151
|
+
});
|
|
152
|
+
}
|
|
134
153
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { setup } from "./commands/setup";
|
|
|
13
13
|
import { sleep } from "./commands/sleep";
|
|
14
14
|
import { ssh } from "./commands/ssh";
|
|
15
15
|
import { tunnel } from "./commands/tunnel";
|
|
16
|
+
import { upgrade } from "./commands/upgrade";
|
|
16
17
|
import { use } from "./commands/use";
|
|
17
18
|
import { wake } from "./commands/wake";
|
|
18
19
|
import {
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
loadLatestAssistant,
|
|
22
23
|
setActiveAssistant,
|
|
23
24
|
} from "./lib/assistant-config";
|
|
25
|
+
import { loadGuardianToken } from "./lib/guardian-token";
|
|
24
26
|
import { checkHealth } from "./lib/health-check";
|
|
25
27
|
|
|
26
28
|
const commands = {
|
|
@@ -37,6 +39,7 @@ const commands = {
|
|
|
37
39
|
sleep,
|
|
38
40
|
ssh,
|
|
39
41
|
tunnel,
|
|
42
|
+
upgrade,
|
|
40
43
|
use,
|
|
41
44
|
wake,
|
|
42
45
|
whoami,
|
|
@@ -63,6 +66,7 @@ function printHelp(): void {
|
|
|
63
66
|
console.log(" sleep Stop the assistant process");
|
|
64
67
|
console.log(" ssh SSH into a remote assistant instance");
|
|
65
68
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
69
|
+
console.log(" upgrade Upgrade an assistant to the latest version");
|
|
66
70
|
console.log(" use Set the active assistant for commands");
|
|
67
71
|
console.log(" wake Start the assistant and gateway");
|
|
68
72
|
console.log(" whoami Show current logged-in user");
|
|
@@ -103,7 +107,8 @@ async function tryLaunchClient(): Promise<boolean> {
|
|
|
103
107
|
const url = entry.localUrl || entry.runtimeUrl;
|
|
104
108
|
if (!url) return false;
|
|
105
109
|
|
|
106
|
-
const
|
|
110
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
111
|
+
const result = await checkHealth(url, token);
|
|
107
112
|
if (result.status !== "healthy") return false;
|
|
108
113
|
|
|
109
114
|
// Ensure the resolved assistant is active so client() can find it
|
|
@@ -3,6 +3,7 @@ import { homedir } from "os";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
6
7
|
DEFAULT_DAEMON_PORT,
|
|
7
8
|
DEFAULT_GATEWAY_PORT,
|
|
8
9
|
DEFAULT_QDRANT_PORT,
|
|
@@ -49,8 +50,12 @@ export interface AssistantEntry {
|
|
|
49
50
|
sshUser?: string;
|
|
50
51
|
zone?: string;
|
|
51
52
|
hatchedAt?: string;
|
|
53
|
+
/** Name of the shared volume backing BASE_DATA_DIR for containerised instances. */
|
|
54
|
+
volume?: string;
|
|
52
55
|
/** Per-instance resource config. Present for local entries in multi-instance setups. */
|
|
53
56
|
resources?: LocalInstanceResources;
|
|
57
|
+
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
58
|
+
watcherPid?: number;
|
|
54
59
|
[key: string]: unknown;
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -152,7 +157,9 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
152
157
|
"share",
|
|
153
158
|
"vellum",
|
|
154
159
|
"assistants",
|
|
155
|
-
typeof raw.assistantId === "string"
|
|
160
|
+
typeof raw.assistantId === "string"
|
|
161
|
+
? raw.assistantId
|
|
162
|
+
: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
156
163
|
);
|
|
157
164
|
raw.resources = {
|
|
158
165
|
instanceDir,
|
|
@@ -172,7 +179,9 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
|
|
|
172
179
|
"share",
|
|
173
180
|
"vellum",
|
|
174
181
|
"assistants",
|
|
175
|
-
typeof raw.assistantId === "string"
|
|
182
|
+
typeof raw.assistantId === "string"
|
|
183
|
+
? raw.assistantId
|
|
184
|
+
: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
176
185
|
);
|
|
177
186
|
mutated = true;
|
|
178
187
|
}
|
package/src/lib/aws.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "crypto";
|
|
2
1
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
3
2
|
import { homedir, tmpdir, userInfo } from "os";
|
|
4
3
|
import { join } from "path";
|
|
@@ -9,7 +8,8 @@ import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
|
9
8
|
import type { AssistantEntry } from "./assistant-config";
|
|
10
9
|
import { GATEWAY_PORT } from "./constants";
|
|
11
10
|
import type { Species } from "./constants";
|
|
12
|
-
import {
|
|
11
|
+
import { leaseGuardianToken } from "./guardian-token";
|
|
12
|
+
import { generateInstanceName } from "./random-name";
|
|
13
13
|
import { exec, execOutput } from "./step-runner";
|
|
14
14
|
|
|
15
15
|
const KEY_PAIR_NAME = "vellum-assistant";
|
|
@@ -370,28 +370,6 @@ async function pollAwsInstance(
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
async function fetchRemoteBearerToken(
|
|
374
|
-
ip: string,
|
|
375
|
-
keyPath: string,
|
|
376
|
-
): Promise<string | null> {
|
|
377
|
-
try {
|
|
378
|
-
const remoteCmd =
|
|
379
|
-
'cat ~/.vellum.lock.json 2>/dev/null || cat ~/.vellum.lockfile.json 2>/dev/null || echo "{}"';
|
|
380
|
-
const output = await awsSshExec(ip, keyPath, remoteCmd);
|
|
381
|
-
const data = JSON.parse(output.trim());
|
|
382
|
-
const assistants = data.assistants;
|
|
383
|
-
if (Array.isArray(assistants) && assistants.length > 0) {
|
|
384
|
-
const token = assistants[0].bearerToken;
|
|
385
|
-
if (typeof token === "string" && token) {
|
|
386
|
-
return token;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return null;
|
|
390
|
-
} catch {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
373
|
export async function hatchAws(
|
|
396
374
|
species: Species,
|
|
397
375
|
detached: boolean,
|
|
@@ -405,12 +383,7 @@ export async function hatchAws(
|
|
|
405
383
|
(await getActiveRegion().catch(() => AWS_DEFAULT_REGION));
|
|
406
384
|
let instanceName: string;
|
|
407
385
|
|
|
408
|
-
|
|
409
|
-
instanceName = name;
|
|
410
|
-
} else {
|
|
411
|
-
const suffix = generateRandomSuffix();
|
|
412
|
-
instanceName = `${species}-${suffix}`;
|
|
413
|
-
}
|
|
386
|
+
instanceName = generateInstanceName(species, name);
|
|
414
387
|
|
|
415
388
|
console.log(`\u{1F95A} Creating new assistant: ${instanceName}`);
|
|
416
389
|
console.log(` Species: ${species}`);
|
|
@@ -431,13 +404,11 @@ export async function hatchAws(
|
|
|
431
404
|
console.log(
|
|
432
405
|
`\u26a0\ufe0f Instance name ${instanceName} already exists, generating a new name...`,
|
|
433
406
|
);
|
|
434
|
-
|
|
435
|
-
instanceName = `${species}-${suffix}`;
|
|
407
|
+
instanceName = generateInstanceName(species);
|
|
436
408
|
}
|
|
437
409
|
}
|
|
438
410
|
|
|
439
411
|
const sshUser = userInfo().username;
|
|
440
|
-
const bearerToken = randomBytes(32).toString("hex");
|
|
441
412
|
const hatchedBy = process.env.VELLUM_HATCHED_BY;
|
|
442
413
|
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
443
414
|
if (!anthropicApiKey) {
|
|
@@ -465,7 +436,6 @@ export async function hatchAws(
|
|
|
465
436
|
|
|
466
437
|
const startupScript = await buildStartupScript(
|
|
467
438
|
species,
|
|
468
|
-
bearerToken,
|
|
469
439
|
sshUser,
|
|
470
440
|
anthropicApiKey,
|
|
471
441
|
instanceName,
|
|
@@ -558,10 +528,12 @@ export async function hatchAws(
|
|
|
558
528
|
process.exit(1);
|
|
559
529
|
}
|
|
560
530
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
531
|
+
try {
|
|
532
|
+
await leaseGuardianToken(runtimeUrl, instanceName);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
console.warn(
|
|
535
|
+
`\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
|
|
536
|
+
);
|
|
565
537
|
}
|
|
566
538
|
} else {
|
|
567
539
|
console.log(
|
package/src/lib/constants.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical internal assistant ID used as the default/fallback across the CLI
|
|
3
|
+
* and daemon. Mirrors `DAEMON_INTERNAL_ASSISTANT_ID` from
|
|
4
|
+
* `assistant/src/runtime/assistant-scope.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export const DAEMON_INTERNAL_ASSISTANT_ID = "self" as const;
|
|
7
|
+
|
|
1
8
|
export const FIREWALL_TAG = "vellum-assistant";
|
|
2
9
|
export const GATEWAY_PORT = process.env.GATEWAY_PORT
|
|
3
10
|
? Number(process.env.GATEWAY_PORT)
|