@vellumai/cli 0.5.4 → 0.5.6
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 -0
- package/knip.json +2 -1
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +46 -0
- package/src/__tests__/health-check.test.ts +26 -1
- package/src/adapters/openclaw.ts +4 -2
- package/src/commands/backup.ts +151 -0
- package/src/commands/hatch.ts +14 -4
- package/src/commands/ps.ts +6 -1
- package/src/commands/restore.ts +330 -0
- package/src/commands/rollback.ts +280 -0
- package/src/commands/upgrade.ts +171 -2
- package/src/commands/wake.ts +8 -0
- package/src/index.ts +11 -0
- package/src/lib/assistant-config.ts +57 -0
- package/src/lib/aws.ts +13 -5
- package/src/lib/constants.ts +12 -0
- package/src/lib/docker.ts +299 -18
- package/src/lib/gcp.ts +18 -6
- package/src/lib/guardian-token.ts +46 -1
- package/src/lib/health-check.ts +4 -0
- package/src/lib/platform-client.ts +3 -2
- package/src/lib/version-compat.ts +45 -0
package/AGENTS.md
CHANGED
|
@@ -40,3 +40,15 @@ Every command must have high-quality `--help` output. Follow the same standards
|
|
|
40
40
|
3. **Write for machines**: Be precise about formats, constraints, and side effects.
|
|
41
41
|
AI agents parse help text to decide which command to run and how. Avoid vague
|
|
42
42
|
language — say exactly what the command does and where state is stored.
|
|
43
|
+
|
|
44
|
+
## Docker Volume Management
|
|
45
|
+
|
|
46
|
+
The CLI creates and manages Docker volumes for containerized instances. See the root `AGENTS.md` § Docker Volume Architecture for the full volume layout.
|
|
47
|
+
|
|
48
|
+
**Volume creation** (`hatch`): Creates four volumes per instance — workspace, gateway-security, ces-security, and socket. The legacy data volume is no longer created.
|
|
49
|
+
|
|
50
|
+
**Volume migration** (`wake`/`hatch`): On startup, existing instances that still have a legacy data volume are migrated. `migrateGatewaySecurityFiles()` and `migrateCesSecurityFiles()` in `lib/docker.ts` copy security files from the data volume to their respective security volumes. Migrations are idempotent and non-fatal.
|
|
51
|
+
|
|
52
|
+
**Volume cleanup** (`retire`): All volumes (including the legacy data volume if it exists) are removed when an instance is retired.
|
|
53
|
+
|
|
54
|
+
**Volume mount rules**: Each service container receives only the volumes it needs. The assistant never mounts `gateway-security` or `ces-security`. The gateway never mounts `ces-security`. The CES mounts the workspace volume as read-only.
|
package/knip.json
CHANGED
package/package.json
CHANGED
|
@@ -238,6 +238,7 @@ describe("migrateLegacyEntry", () => {
|
|
|
238
238
|
expect(resources.daemonPort).toBe(7821);
|
|
239
239
|
expect(resources.gatewayPort).toBe(7830);
|
|
240
240
|
expect(resources.qdrantPort).toBe(6333);
|
|
241
|
+
expect(resources.cesPort).toBe(8090);
|
|
241
242
|
expect(resources.pidFile).toContain("vellum.pid");
|
|
242
243
|
});
|
|
243
244
|
|
|
@@ -310,6 +311,7 @@ describe("migrateLegacyEntry", () => {
|
|
|
310
311
|
expect(resources.daemonPort).toBe(7821);
|
|
311
312
|
expect(resources.gatewayPort).toBe(7830);
|
|
312
313
|
expect(resources.qdrantPort).toBe(6333);
|
|
314
|
+
expect(resources.cesPort).toBe(8090);
|
|
313
315
|
expect(resources.pidFile).toBe("/custom/path/.vellum/vellum.pid");
|
|
314
316
|
});
|
|
315
317
|
|
|
@@ -328,6 +330,7 @@ describe("migrateLegacyEntry", () => {
|
|
|
328
330
|
daemonPort: 8000,
|
|
329
331
|
gatewayPort: 8001,
|
|
330
332
|
qdrantPort: 8002,
|
|
333
|
+
cesPort: 8003,
|
|
331
334
|
pidFile: "/my/path/.vellum/vellum.pid",
|
|
332
335
|
},
|
|
333
336
|
};
|
|
@@ -343,6 +346,7 @@ describe("migrateLegacyEntry", () => {
|
|
|
343
346
|
expect(resources.daemonPort).toBe(8000);
|
|
344
347
|
expect(resources.gatewayPort).toBe(8001);
|
|
345
348
|
expect(resources.qdrantPort).toBe(8002);
|
|
349
|
+
expect(resources.cesPort).toBe(8003);
|
|
346
350
|
});
|
|
347
351
|
|
|
348
352
|
test("baseDataDir does not overwrite existing resources.instanceDir", () => {
|
|
@@ -377,6 +381,48 @@ describe("migrateLegacyEntry", () => {
|
|
|
377
381
|
const resources = entry.resources as Record<string, unknown>;
|
|
378
382
|
expect(resources.instanceDir).toBe("/new/path");
|
|
379
383
|
});
|
|
384
|
+
|
|
385
|
+
test("backfills cesPort with default 8090", () => {
|
|
386
|
+
// GIVEN a local entry with resources that has no cesPort
|
|
387
|
+
const entry: Record<string, unknown> = {
|
|
388
|
+
assistantId: "no-ces-port",
|
|
389
|
+
runtimeUrl: "http://localhost:7830",
|
|
390
|
+
cloud: "local",
|
|
391
|
+
resources: {
|
|
392
|
+
instanceDir: "/custom/path",
|
|
393
|
+
daemonPort: 7821,
|
|
394
|
+
gatewayPort: 7830,
|
|
395
|
+
qdrantPort: 6333,
|
|
396
|
+
pidFile: "/custom/path/.vellum/vellum.pid",
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
// WHEN we migrate the entry
|
|
400
|
+
const changed = migrateLegacyEntry(entry);
|
|
401
|
+
// THEN cesPort should be backfilled
|
|
402
|
+
expect(changed).toBe(true);
|
|
403
|
+
const resources = entry.resources as Record<string, unknown>;
|
|
404
|
+
expect(resources.cesPort).toBe(8090);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("does not overwrite existing cesPort", () => {
|
|
408
|
+
const entry: Record<string, unknown> = {
|
|
409
|
+
assistantId: "has-ces-port",
|
|
410
|
+
runtimeUrl: "http://localhost:7830",
|
|
411
|
+
cloud: "local",
|
|
412
|
+
resources: {
|
|
413
|
+
instanceDir: "/my/path",
|
|
414
|
+
daemonPort: 8000,
|
|
415
|
+
gatewayPort: 8001,
|
|
416
|
+
qdrantPort: 8002,
|
|
417
|
+
cesPort: 9090,
|
|
418
|
+
pidFile: "/my/path/.vellum/vellum.pid",
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
const changed = migrateLegacyEntry(entry);
|
|
422
|
+
expect(changed).toBe(false);
|
|
423
|
+
const resources = entry.resources as Record<string, unknown>;
|
|
424
|
+
expect(resources.cesPort).toBe(9090);
|
|
425
|
+
});
|
|
380
426
|
});
|
|
381
427
|
|
|
382
428
|
describe("legacy migration via loadAllAssistants", () => {
|
|
@@ -10,6 +10,7 @@ describe("checkHealth", () => {
|
|
|
10
10
|
test("returns unreachable for non-existent host", async () => {
|
|
11
11
|
const result = await checkHealth("http://127.0.0.1:1");
|
|
12
12
|
expect(["unreachable", "timeout"]).toContain(result.status);
|
|
13
|
+
expect(result.version).toBeUndefined();
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
test("returns healthy for a mock healthy endpoint", async () => {
|
|
@@ -24,6 +25,24 @@ describe("checkHealth", () => {
|
|
|
24
25
|
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
25
26
|
expect(result.status).toBe("healthy");
|
|
26
27
|
expect(result.detail).toBeNull();
|
|
28
|
+
expect(result.version).toBeUndefined();
|
|
29
|
+
} finally {
|
|
30
|
+
server.stop(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns version when present in response", async () => {
|
|
35
|
+
const server = Bun.serve({
|
|
36
|
+
port: 0,
|
|
37
|
+
fetch() {
|
|
38
|
+
return Response.json({ status: "healthy", version: "1.2.3" });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
44
|
+
expect(result.status).toBe("healthy");
|
|
45
|
+
expect(result.version).toBe("1.2.3");
|
|
27
46
|
} finally {
|
|
28
47
|
server.stop(true);
|
|
29
48
|
}
|
|
@@ -33,7 +52,11 @@ describe("checkHealth", () => {
|
|
|
33
52
|
const server = Bun.serve({
|
|
34
53
|
port: 0,
|
|
35
54
|
fetch() {
|
|
36
|
-
return Response.json({
|
|
55
|
+
return Response.json({
|
|
56
|
+
status: "degraded",
|
|
57
|
+
message: "high latency",
|
|
58
|
+
version: "0.9.0",
|
|
59
|
+
});
|
|
37
60
|
},
|
|
38
61
|
});
|
|
39
62
|
|
|
@@ -41,6 +64,7 @@ describe("checkHealth", () => {
|
|
|
41
64
|
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
42
65
|
expect(result.status).toBe("degraded");
|
|
43
66
|
expect(result.detail).toBe("high latency");
|
|
67
|
+
expect(result.version).toBe("0.9.0");
|
|
44
68
|
} finally {
|
|
45
69
|
server.stop(true);
|
|
46
70
|
}
|
|
@@ -57,6 +81,7 @@ describe("checkHealth", () => {
|
|
|
57
81
|
try {
|
|
58
82
|
const result = await checkHealth(`http://localhost:${server.port}`);
|
|
59
83
|
expect(result.status).toBe("error (500)");
|
|
84
|
+
expect(result.version).toBeUndefined();
|
|
60
85
|
} finally {
|
|
61
86
|
server.stop(true);
|
|
62
87
|
}
|
package/src/adapters/openclaw.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { buildOpenclawRuntimeServer } from "../lib/openclaw-runtime-server";
|
|
|
3
3
|
|
|
4
4
|
export async function buildOpenclawStartupScript(
|
|
5
5
|
sshUser: string,
|
|
6
|
-
|
|
6
|
+
providerApiKeys: Record<string, string>,
|
|
7
7
|
timestampRedirect: string,
|
|
8
8
|
userSetup: string,
|
|
9
9
|
ownershipFixup: string,
|
|
@@ -110,7 +110,9 @@ echo -n "\$OPENCLAW_GW_TOKEN" > /tmp/openclaw-gateway-token
|
|
|
110
110
|
chmod 600 /tmp/openclaw-gateway-token
|
|
111
111
|
|
|
112
112
|
mkdir -p /root/.openclaw
|
|
113
|
-
|
|
113
|
+
${Object.entries(providerApiKeys)
|
|
114
|
+
.map(([envVar, value]) => `openclaw config set env.${envVar} "${value}"`)
|
|
115
|
+
.join("\n")}
|
|
114
116
|
openclaw config set agents.defaults.model.primary "anthropic/claude-opus-4-6"
|
|
115
117
|
openclaw config set gateway.auth.token "\$OPENCLAW_GW_TOKEN"
|
|
116
118
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
|
|
5
|
+
import { findAssistantByName } from "../lib/assistant-config";
|
|
6
|
+
import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
|
|
7
|
+
|
|
8
|
+
function getBackupsDir(): string {
|
|
9
|
+
const dataHome =
|
|
10
|
+
process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
11
|
+
return join(dataHome, "vellum", "backups");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatSize(bytes: number): string {
|
|
15
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
16
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
17
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function backup(): Promise<void> {
|
|
21
|
+
const args = process.argv.slice(3);
|
|
22
|
+
|
|
23
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
24
|
+
console.log("Usage: vellum backup <name> [--output <path>]");
|
|
25
|
+
console.log("");
|
|
26
|
+
console.log(
|
|
27
|
+
"Export a backup of a running assistant as a .vbundle archive.",
|
|
28
|
+
);
|
|
29
|
+
console.log("");
|
|
30
|
+
console.log("Arguments:");
|
|
31
|
+
console.log(" <name> Name of the assistant to back up");
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log("Options:");
|
|
34
|
+
console.log(" --output <path> Path to save the .vbundle file");
|
|
35
|
+
console.log(
|
|
36
|
+
" (default: ~/.local/share/vellum/backups/<name>-<timestamp>.vbundle)",
|
|
37
|
+
);
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("Examples:");
|
|
40
|
+
console.log(" vellum backup my-assistant");
|
|
41
|
+
console.log(
|
|
42
|
+
" vellum backup my-assistant --output ~/Desktop/backup.vbundle",
|
|
43
|
+
);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const name = args[0];
|
|
48
|
+
if (!name || name.startsWith("-")) {
|
|
49
|
+
console.error("Usage: vellum backup <name> [--output <path>]");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse --output flag
|
|
54
|
+
let outputArg: string | undefined;
|
|
55
|
+
for (let i = 1; i < args.length; i++) {
|
|
56
|
+
if (args[i] === "--output" && args[i + 1]) {
|
|
57
|
+
outputArg = args[i + 1];
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Look up the instance
|
|
63
|
+
const entry = findAssistantByName(name);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
console.error(`No assistant found with name '${name}'.`);
|
|
66
|
+
console.error("Run 'vellum hatch' first, or check the instance name.");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Obtain an auth token
|
|
71
|
+
let accessToken: string;
|
|
72
|
+
const tokenData = loadGuardianToken(entry.assistantId);
|
|
73
|
+
if (tokenData && new Date(tokenData.accessTokenExpiresAt) > new Date()) {
|
|
74
|
+
accessToken = tokenData.accessToken;
|
|
75
|
+
} else {
|
|
76
|
+
try {
|
|
77
|
+
const freshToken = await leaseGuardianToken(
|
|
78
|
+
entry.runtimeUrl,
|
|
79
|
+
entry.assistantId,
|
|
80
|
+
);
|
|
81
|
+
accessToken = freshToken.accessToken;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
85
|
+
console.error(
|
|
86
|
+
`Error: Could not connect to assistant '${name}'. Is it running?`,
|
|
87
|
+
);
|
|
88
|
+
console.error(`Try: vellum wake ${name}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Call the export endpoint
|
|
96
|
+
let response: Response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Bearer ${accessToken}`,
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({ description: "CLI backup" }),
|
|
105
|
+
signal: AbortSignal.timeout(120_000),
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
109
|
+
console.error("Error: Export request timed out after 2 minutes.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
114
|
+
console.error(
|
|
115
|
+
`Error: Could not connect to assistant '${name}'. Is it running?`,
|
|
116
|
+
);
|
|
117
|
+
console.error(`Try: vellum wake ${name}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const body = await response.text();
|
|
125
|
+
console.error(`Error: Export failed (${response.status}): ${body}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Read the response body
|
|
130
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
131
|
+
const data = new Uint8Array(arrayBuffer);
|
|
132
|
+
|
|
133
|
+
// Determine output path
|
|
134
|
+
const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
135
|
+
const outputPath =
|
|
136
|
+
outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
|
|
137
|
+
|
|
138
|
+
// Ensure parent directory exists
|
|
139
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
140
|
+
|
|
141
|
+
// Write the archive to disk
|
|
142
|
+
writeFileSync(outputPath, data);
|
|
143
|
+
|
|
144
|
+
// Print success
|
|
145
|
+
const manifestSha = response.headers.get("X-Vbundle-Manifest-Sha256");
|
|
146
|
+
console.log(`Backup saved to ${outputPath}`);
|
|
147
|
+
console.log(`Size: ${formatSize(data.byteLength)}`);
|
|
148
|
+
if (manifestSha) {
|
|
149
|
+
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/commands/hatch.ts
CHANGED
|
@@ -96,7 +96,7 @@ chown -R "$SSH_USER:$SSH_USER" "$SSH_USER_HOME" 2>/dev/null || true
|
|
|
96
96
|
export async function buildStartupScript(
|
|
97
97
|
species: Species,
|
|
98
98
|
sshUser: string,
|
|
99
|
-
|
|
99
|
+
providerApiKeys: Record<string, string>,
|
|
100
100
|
instanceName: string,
|
|
101
101
|
cloud: RemoteHost,
|
|
102
102
|
): Promise<string> {
|
|
@@ -114,13 +114,22 @@ export async function buildStartupScript(
|
|
|
114
114
|
if (species === "openclaw") {
|
|
115
115
|
return await buildOpenclawStartupScript(
|
|
116
116
|
sshUser,
|
|
117
|
-
|
|
117
|
+
providerApiKeys,
|
|
118
118
|
timestampRedirect,
|
|
119
119
|
userSetup,
|
|
120
120
|
ownershipFixup,
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// Build bash lines that set each provider API key as a shell variable
|
|
125
|
+
// and corresponding dotenv lines for the env file.
|
|
126
|
+
const envSetLines = Object.entries(providerApiKeys)
|
|
127
|
+
.map(([envVar, value]) => `${envVar}=${value}`)
|
|
128
|
+
.join("\n");
|
|
129
|
+
const dotenvLines = Object.keys(providerApiKeys)
|
|
130
|
+
.map((envVar) => `${envVar}=\$${envVar}`)
|
|
131
|
+
.join("\n");
|
|
132
|
+
|
|
124
133
|
return `#!/bin/bash
|
|
125
134
|
set -e
|
|
126
135
|
|
|
@@ -128,11 +137,11 @@ ${timestampRedirect}
|
|
|
128
137
|
|
|
129
138
|
trap 'EXIT_CODE=\$?; if [ \$EXIT_CODE -ne 0 ]; then echo "Startup script failed with exit code \$EXIT_CODE at line \$LINENO" > ${errorPath}; echo "Last 20 log lines:" >> ${errorPath}; tail -20 ${logPath} >> ${errorPath} 2>/dev/null || true; fi' EXIT
|
|
130
139
|
${userSetup}
|
|
131
|
-
|
|
140
|
+
${envSetLines}
|
|
132
141
|
VELLUM_ASSISTANT_NAME=${instanceName}
|
|
133
142
|
mkdir -p "\$HOME/.config/vellum"
|
|
134
143
|
cat > "\$HOME/.config/vellum/env" << DOTENV_EOF
|
|
135
|
-
|
|
144
|
+
${dotenvLines}
|
|
136
145
|
RUNTIME_HTTP_PORT=7821
|
|
137
146
|
DOTENV_EOF
|
|
138
147
|
|
|
@@ -749,6 +758,7 @@ async function hatchLocal(
|
|
|
749
758
|
cloud: "local",
|
|
750
759
|
species,
|
|
751
760
|
hatchedAt: new Date().toISOString(),
|
|
761
|
+
serviceGroupVersion: cliPkg.version ? `v${cliPkg.version}` : undefined,
|
|
752
762
|
resources,
|
|
753
763
|
};
|
|
754
764
|
if (!daemonOnly && !restart) {
|
package/src/commands/ps.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
6
|
loadAllAssistants,
|
|
7
|
+
updateServiceGroupVersion,
|
|
7
8
|
type AssistantEntry,
|
|
8
9
|
} from "../lib/assistant-config";
|
|
9
10
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
@@ -424,7 +425,7 @@ async function listAllAssistants(): Promise<void> {
|
|
|
424
425
|
// hitting the health endpoint. If the PID file is missing or the
|
|
425
426
|
// process isn't running, the assistant is sleeping — skip the
|
|
426
427
|
// network health check to avoid a misleading "unreachable" status.
|
|
427
|
-
let health: { status: string; detail: string | null };
|
|
428
|
+
let health: { status: string; detail: string | null; version?: string };
|
|
428
429
|
const resources = a.resources;
|
|
429
430
|
if (a.cloud === "local" && resources) {
|
|
430
431
|
const pid = readPidFile(resources.pidFile);
|
|
@@ -451,6 +452,10 @@ async function listAllAssistants(): Promise<void> {
|
|
|
451
452
|
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
if (health.status === "healthy" && health.version) {
|
|
456
|
+
updateServiceGroupVersion(a.assistantId, health.version);
|
|
457
|
+
}
|
|
458
|
+
|
|
454
459
|
const infoParts = [a.runtimeUrl];
|
|
455
460
|
if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
|
|
456
461
|
if (a.species) infoParts.push(`species: ${a.species}`);
|