@vellumai/cli 0.4.56 → 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/package.json +1 -1
- package/src/commands/client.ts +2 -1
- package/src/commands/hatch.ts +2 -2
- package/src/commands/pair.ts +17 -1
- package/src/commands/ps.ts +88 -2
- package/src/commands/upgrade.ts +366 -0
- package/src/index.ts +6 -1
- package/src/lib/assistant-config.ts +2 -0
- package/src/lib/aws.ts +1 -3
- package/src/lib/docker.ts +271 -261
- package/src/lib/gcp.ts +1 -3
- package/src/lib/guardian-token.ts +17 -0
- package/src/lib/process.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -6,14 +6,11 @@ The `cli/` package (`@vellumai/cli`) manages the **lifecycle of Vellum assistant
|
|
|
6
6
|
|
|
7
7
|
This contrasts with `assistant/src/cli/`, where commands are scoped to a **single running assistant** and operate on its local state (config, memory, contacts, etc.).
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Scope
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
| Manages lifecycle (create, start, stop, delete) | Manages instance-local state (config, memory, etc.) |
|
|
15
|
-
| Requires specifying which assistant to target | Implicitly scoped to the running assistant |
|
|
16
|
-
| Works without an assistant process running | May require or start the daemon |
|
|
11
|
+
Commands here operate on or across **assistant instances** — creating, starting, stopping, connecting to, and deleting them. They require specifying which assistant to target and work without an assistant process running.
|
|
12
|
+
|
|
13
|
+
For commands scoped to a **single running assistant's** local state (config, memory, contacts), see `assistant/src/cli/AGENTS.md`.
|
|
17
14
|
|
|
18
15
|
Examples: `hatch`, `wake`, `sleep`, `retire`, `ps`, `ssh` belong here. `config`, `contacts`, `memory` belong in `assistant/src/cli/`.
|
|
19
16
|
|
|
@@ -30,9 +27,7 @@ Commands that act on a specific assistant should accept an assistant name or ID
|
|
|
30
27
|
|
|
31
28
|
## Help Text Standards
|
|
32
29
|
|
|
33
|
-
Every command must have high-quality `--help` output
|
|
34
|
-
consumption. Help text is a primary interface — both humans and AI agents read
|
|
35
|
-
it to understand what a command does and how to use it.
|
|
30
|
+
Every command must have high-quality `--help` output. Follow the same standards as `assistant/src/cli/AGENTS.md` § Help Text Standards, adapted for this package's manual argv parsing (no Commander.js).
|
|
36
31
|
|
|
37
32
|
### Requirements
|
|
38
33
|
|
package/package.json
CHANGED
package/src/commands/client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
loadLatestAssistant,
|
|
7
7
|
} from "../lib/assistant-config";
|
|
8
8
|
import { DAEMON_INTERNAL_ASSISTANT_ID, GATEWAY_PORT, type Species } from "../lib/constants";
|
|
9
|
+
import { loadGuardianToken } from "../lib/guardian-token";
|
|
9
10
|
import { getLocalLanIPv4, getMacLocalHostname } from "../lib/local";
|
|
10
11
|
|
|
11
12
|
const ANSI = {
|
|
@@ -83,7 +84,7 @@ function parseArgs(): ParsedArgs {
|
|
|
83
84
|
|
|
84
85
|
let runtimeUrl = entry?.localUrl || entry?.runtimeUrl || FALLBACK_RUNTIME_URL;
|
|
85
86
|
let assistantId = entry?.assistantId || DAEMON_INTERNAL_ASSISTANT_ID;
|
|
86
|
-
const bearerToken = entry?.
|
|
87
|
+
const bearerToken = loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined;
|
|
87
88
|
const species: Species = (entry?.species as Species) ?? "vellum";
|
|
88
89
|
|
|
89
90
|
for (let i = 0; i < flagArgs.length; i++) {
|
package/src/commands/hatch.ts
CHANGED
|
@@ -383,7 +383,7 @@ export async function watchHatching(
|
|
|
383
383
|
);
|
|
384
384
|
console.log(` Monitor with: vel logs ${instanceName}`);
|
|
385
385
|
console.log("");
|
|
386
|
-
resolve({ success:
|
|
386
|
+
resolve({ success: false, errorContent: lastErrorContent });
|
|
387
387
|
return;
|
|
388
388
|
}
|
|
389
389
|
|
|
@@ -428,7 +428,7 @@ function watchHatchingDesktop(
|
|
|
428
428
|
`Timed out after ${formatElapsed(elapsed)}. Instance is still running.`,
|
|
429
429
|
);
|
|
430
430
|
desktopLog(`Monitor with: vel logs ${instanceName}`);
|
|
431
|
-
resolve({ success:
|
|
431
|
+
resolve({ success: false, errorContent: lastErrorContent });
|
|
432
432
|
return;
|
|
433
433
|
}
|
|
434
434
|
|
package/src/commands/pair.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { PNG } from "pngjs";
|
|
|
7
7
|
import { saveAssistantEntry } from "../lib/assistant-config";
|
|
8
8
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
9
9
|
import type { Species } from "../lib/constants";
|
|
10
|
+
import { saveGuardianToken } from "../lib/guardian-token";
|
|
11
|
+
import type { GuardianTokenData } from "../lib/guardian-token";
|
|
10
12
|
import { generateInstanceName } from "../lib/random-name";
|
|
11
13
|
|
|
12
14
|
interface QRPairingPayload {
|
|
@@ -168,13 +170,27 @@ export async function pair(): Promise<void> {
|
|
|
168
170
|
const customEntry: AssistantEntry = {
|
|
169
171
|
assistantId: instanceName,
|
|
170
172
|
runtimeUrl,
|
|
171
|
-
bearerToken,
|
|
172
173
|
cloud: "custom",
|
|
173
174
|
species,
|
|
174
175
|
hatchedAt: new Date().toISOString(),
|
|
175
176
|
};
|
|
176
177
|
saveAssistantEntry(customEntry);
|
|
177
178
|
|
|
179
|
+
if (bearerToken) {
|
|
180
|
+
const tokenData: GuardianTokenData = {
|
|
181
|
+
guardianPrincipalId: "",
|
|
182
|
+
accessToken: bearerToken,
|
|
183
|
+
accessTokenExpiresAt: "",
|
|
184
|
+
refreshToken: "",
|
|
185
|
+
refreshTokenExpiresAt: "",
|
|
186
|
+
refreshAfter: "",
|
|
187
|
+
isNew: true,
|
|
188
|
+
deviceId: getDeviceId(),
|
|
189
|
+
leasedAt: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
saveGuardianToken(instanceName, tokenData);
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
console.log("");
|
|
179
195
|
console.log("Successfully paired with remote assistant!");
|
|
180
196
|
console.log("Instance details:");
|
package/src/commands/ps.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
loadAllAssistants,
|
|
7
7
|
type AssistantEntry,
|
|
8
8
|
} from "../lib/assistant-config";
|
|
9
|
+
import { loadGuardianToken } from "../lib/guardian-token";
|
|
9
10
|
import { checkHealth, checkManagedHealth } from "../lib/health-check";
|
|
11
|
+
import { dockerResourceNames } from "../lib/docker";
|
|
10
12
|
import {
|
|
11
13
|
classifyProcess,
|
|
12
14
|
detectOrphanedProcesses,
|
|
@@ -248,6 +250,73 @@ async function getLocalProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
|
248
250
|
}));
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
async function getDockerContainerState(
|
|
254
|
+
containerName: string,
|
|
255
|
+
): Promise<string | null> {
|
|
256
|
+
try {
|
|
257
|
+
const output = await execOutput("docker", [
|
|
258
|
+
"inspect",
|
|
259
|
+
"--format",
|
|
260
|
+
"{{.State.Status}}",
|
|
261
|
+
containerName,
|
|
262
|
+
]);
|
|
263
|
+
return output.trim() || "unknown";
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isLocalProcessAlive(pid: number): boolean {
|
|
270
|
+
try {
|
|
271
|
+
process.kill(pid, 0);
|
|
272
|
+
return true;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function getDockerProcesses(entry: AssistantEntry): Promise<TableRow[]> {
|
|
279
|
+
const res = dockerResourceNames(entry.assistantId);
|
|
280
|
+
|
|
281
|
+
const containers: { name: string; containerName: string }[] = [
|
|
282
|
+
{ name: "assistant", containerName: res.assistantContainer },
|
|
283
|
+
{ name: "gateway", containerName: res.gatewayContainer },
|
|
284
|
+
{ name: "credential-executor", containerName: res.cesContainer },
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const results = await Promise.all(
|
|
288
|
+
containers.map(async ({ name, containerName }) => {
|
|
289
|
+
const state = await getDockerContainerState(containerName);
|
|
290
|
+
if (!state) {
|
|
291
|
+
return {
|
|
292
|
+
name,
|
|
293
|
+
status: withStatusEmoji("not found"),
|
|
294
|
+
info: `container ${containerName}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
name,
|
|
299
|
+
status: withStatusEmoji(state === "running" ? "running" : state),
|
|
300
|
+
info: `container ${containerName}`,
|
|
301
|
+
};
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Show the file watcher process if the instance was hatched with --watch.
|
|
306
|
+
const watcherPid =
|
|
307
|
+
typeof entry.watcherPid === "number" ? entry.watcherPid : null;
|
|
308
|
+
if (watcherPid !== null) {
|
|
309
|
+
const alive = isLocalProcessAlive(watcherPid);
|
|
310
|
+
results.push({
|
|
311
|
+
name: "file-watcher",
|
|
312
|
+
status: withStatusEmoji(alive ? "running" : "not running"),
|
|
313
|
+
info: alive ? `PID ${watcherPid}` : "not detected",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return results;
|
|
318
|
+
}
|
|
319
|
+
|
|
251
320
|
async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
252
321
|
const cloud = resolveCloud(entry);
|
|
253
322
|
|
|
@@ -259,6 +328,12 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
259
328
|
return;
|
|
260
329
|
}
|
|
261
330
|
|
|
331
|
+
if (cloud === "docker") {
|
|
332
|
+
const rows = await getDockerProcesses(entry);
|
|
333
|
+
printTable(rows);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
262
337
|
let output: string;
|
|
263
338
|
try {
|
|
264
339
|
if (cloud === "gcp") {
|
|
@@ -357,12 +432,23 @@ async function listAllAssistants(): Promise<void> {
|
|
|
357
432
|
if (!alive) {
|
|
358
433
|
health = { status: "sleeping", detail: null };
|
|
359
434
|
} else {
|
|
360
|
-
|
|
435
|
+
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
436
|
+
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
437
|
+
}
|
|
438
|
+
} else if (a.cloud === "docker") {
|
|
439
|
+
const res = dockerResourceNames(a.assistantId);
|
|
440
|
+
const state = await getDockerContainerState(res.assistantContainer);
|
|
441
|
+
if (!state || state !== "running") {
|
|
442
|
+
health = { status: "sleeping", detail: null };
|
|
443
|
+
} else {
|
|
444
|
+
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
445
|
+
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
361
446
|
}
|
|
362
447
|
} else if (a.cloud === "vellum") {
|
|
363
448
|
health = await checkManagedHealth(a.runtimeUrl, a.assistantId);
|
|
364
449
|
} else {
|
|
365
|
-
|
|
450
|
+
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
451
|
+
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
366
452
|
}
|
|
367
453
|
|
|
368
454
|
const infoParts = [a.runtimeUrl];
|
|
@@ -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/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
|
|
@@ -54,6 +54,8 @@ export interface AssistantEntry {
|
|
|
54
54
|
volume?: string;
|
|
55
55
|
/** Per-instance resource config. Present for local entries in multi-instance setups. */
|
|
56
56
|
resources?: LocalInstanceResources;
|
|
57
|
+
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
58
|
+
watcherPid?: number;
|
|
57
59
|
[key: string]: unknown;
|
|
58
60
|
}
|
|
59
61
|
|
package/src/lib/aws.ts
CHANGED
|
@@ -529,9 +529,7 @@ export async function hatchAws(
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
try {
|
|
532
|
-
|
|
533
|
-
awsEntry.bearerToken = tokenData.accessToken;
|
|
534
|
-
saveAssistantEntry(awsEntry);
|
|
532
|
+
await leaseGuardianToken(runtimeUrl, instanceName);
|
|
535
533
|
} catch (err) {
|
|
536
534
|
console.warn(
|
|
537
535
|
`\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
|