@vellumai/cli 0.6.4 → 0.6.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/bun.lock +17 -17
- package/package.json +18 -18
- package/src/__tests__/guardian-token.test.ts +56 -3
- package/src/__tests__/llm-provider-env-var-parity.test.ts +64 -0
- package/src/__tests__/multi-local.test.ts +30 -0
- package/src/commands/exec.ts +186 -0
- package/src/commands/login.ts +32 -1
- package/src/commands/retire.ts +23 -0
- package/src/commands/ssh.ts +1 -1
- package/src/commands/teleport.ts +28 -1
- package/src/commands/terminal.ts +437 -0
- package/src/commands/wake.ts +11 -0
- package/src/index.ts +6 -0
- package/src/lib/__tests__/docker.test.ts +91 -1
- package/src/lib/assistant-config.ts +35 -22
- package/src/lib/config-utils.ts +4 -4
- package/src/lib/docker.ts +88 -4
- package/src/lib/environments/__tests__/paths.test.ts +3 -9
- package/src/lib/environments/__tests__/seeds.test.ts +72 -0
- package/src/lib/environments/paths.ts +4 -5
- package/src/lib/environments/seeds.ts +29 -1
- package/src/lib/exec-apple-container.ts +122 -0
- package/src/lib/guardian-token.ts +63 -0
- package/src/lib/hatch-local.ts +20 -4
- package/src/lib/local.ts +1 -0
- package/src/lib/platform-client.ts +134 -0
- package/src/{commands → lib}/ssh-apple-container.ts +8 -4
- package/src/lib/terminal-client.ts +177 -0
- package/src/shared/provider-env-vars.ts +30 -6
package/bun.lock
CHANGED
|
@@ -5,25 +5,25 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@vellumai/cli",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"chalk": "
|
|
9
|
-
"ink": "
|
|
10
|
-
"jsqr": "
|
|
11
|
-
"nanoid": "
|
|
12
|
-
"pngjs": "
|
|
13
|
-
"qrcode-terminal": "
|
|
14
|
-
"react": "
|
|
15
|
-
"react-devtools-core": "
|
|
8
|
+
"chalk": "5.6.2",
|
|
9
|
+
"ink": "6.8.0",
|
|
10
|
+
"jsqr": "1.4.0",
|
|
11
|
+
"nanoid": "5.1.7",
|
|
12
|
+
"pngjs": "7.0.0",
|
|
13
|
+
"qrcode-terminal": "0.12.0",
|
|
14
|
+
"react": "19.2.4",
|
|
15
|
+
"react-devtools-core": "6.1.5",
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/bun": "
|
|
19
|
-
"@types/pngjs": "
|
|
20
|
-
"@types/qrcode-terminal": "
|
|
21
|
-
"@types/react": "
|
|
22
|
-
"eslint": "
|
|
23
|
-
"knip": "
|
|
24
|
-
"prettier": "
|
|
25
|
-
"typescript": "
|
|
26
|
-
"typescript-eslint": "
|
|
18
|
+
"@types/bun": "1.3.11",
|
|
19
|
+
"@types/pngjs": "6.0.5",
|
|
20
|
+
"@types/qrcode-terminal": "0.12.2",
|
|
21
|
+
"@types/react": "19.2.14",
|
|
22
|
+
"eslint": "10.1.0",
|
|
23
|
+
"knip": "5.88.1",
|
|
24
|
+
"prettier": "3.8.1",
|
|
25
|
+
"typescript": "5.9.3",
|
|
26
|
+
"typescript-eslint": "8.58.0",
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "CLI tools for vellum-assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -24,24 +24,24 @@
|
|
|
24
24
|
"author": "Vellum AI",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"chalk": "
|
|
28
|
-
"ink": "
|
|
29
|
-
"jsqr": "
|
|
30
|
-
"nanoid": "
|
|
31
|
-
"pngjs": "
|
|
32
|
-
"qrcode-terminal": "
|
|
33
|
-
"react": "
|
|
34
|
-
"react-devtools-core": "
|
|
27
|
+
"chalk": "5.6.2",
|
|
28
|
+
"ink": "6.8.0",
|
|
29
|
+
"jsqr": "1.4.0",
|
|
30
|
+
"nanoid": "5.1.7",
|
|
31
|
+
"pngjs": "7.0.0",
|
|
32
|
+
"qrcode-terminal": "0.12.0",
|
|
33
|
+
"react": "19.2.4",
|
|
34
|
+
"react-devtools-core": "6.1.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/bun": "
|
|
38
|
-
"@types/pngjs": "
|
|
39
|
-
"@types/qrcode-terminal": "
|
|
40
|
-
"@types/react": "
|
|
41
|
-
"eslint": "
|
|
42
|
-
"knip": "
|
|
43
|
-
"prettier": "
|
|
44
|
-
"typescript": "
|
|
45
|
-
"typescript-eslint": "
|
|
37
|
+
"@types/bun": "1.3.11",
|
|
38
|
+
"@types/pngjs": "6.0.5",
|
|
39
|
+
"@types/qrcode-terminal": "0.12.2",
|
|
40
|
+
"@types/react": "19.2.14",
|
|
41
|
+
"eslint": "10.1.0",
|
|
42
|
+
"knip": "5.88.1",
|
|
43
|
+
"prettier": "3.8.1",
|
|
44
|
+
"typescript": "5.9.3",
|
|
45
|
+
"typescript-eslint": "8.58.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -7,18 +7,20 @@ import {
|
|
|
7
7
|
getOrCreatePersistedDeviceId,
|
|
8
8
|
loadGuardianToken,
|
|
9
9
|
saveGuardianToken,
|
|
10
|
+
seedGuardianTokenFromSiblingEnv,
|
|
10
11
|
type GuardianTokenData,
|
|
11
12
|
} from "../lib/guardian-token.js";
|
|
12
13
|
|
|
13
14
|
function makeTokenData(suffix: string): GuardianTokenData {
|
|
14
15
|
const now = new Date().toISOString();
|
|
16
|
+
const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
15
17
|
return {
|
|
16
18
|
guardianPrincipalId: `principal-${suffix}`,
|
|
17
19
|
accessToken: `access-${suffix}`,
|
|
18
|
-
accessTokenExpiresAt:
|
|
20
|
+
accessTokenExpiresAt: oneHourFromNow,
|
|
19
21
|
refreshToken: `refresh-${suffix}`,
|
|
20
|
-
refreshTokenExpiresAt:
|
|
21
|
-
refreshAfter:
|
|
22
|
+
refreshTokenExpiresAt: oneHourFromNow,
|
|
23
|
+
refreshAfter: oneHourFromNow,
|
|
22
24
|
isNew: true,
|
|
23
25
|
deviceId: `device-${suffix}`,
|
|
24
26
|
leasedAt: now,
|
|
@@ -169,4 +171,55 @@ describe("guardian-token paths are env-scoped", () => {
|
|
|
169
171
|
const second = getOrCreatePersistedDeviceId();
|
|
170
172
|
expect(first).toBe(second);
|
|
171
173
|
});
|
|
174
|
+
|
|
175
|
+
test("seedGuardianTokenFromSiblingEnv copies a dev token into the current local env", () => {
|
|
176
|
+
// Write a token under the dev env.
|
|
177
|
+
process.env.VELLUM_ENVIRONMENT = "dev";
|
|
178
|
+
saveGuardianToken("alpha", makeTokenData("dev"));
|
|
179
|
+
|
|
180
|
+
// Switch to local env — no token present yet.
|
|
181
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
182
|
+
expect(loadGuardianToken("alpha")).toBeNull();
|
|
183
|
+
|
|
184
|
+
const seeded = seedGuardianTokenFromSiblingEnv("alpha");
|
|
185
|
+
expect(seeded).toBe(true);
|
|
186
|
+
|
|
187
|
+
const localPath = join(
|
|
188
|
+
tempHome,
|
|
189
|
+
"vellum-local",
|
|
190
|
+
"assistants",
|
|
191
|
+
"alpha",
|
|
192
|
+
"guardian-token.json",
|
|
193
|
+
);
|
|
194
|
+
expect(existsSync(localPath)).toBe(true);
|
|
195
|
+
const loaded = loadGuardianToken("alpha");
|
|
196
|
+
expect(loaded).not.toBeNull();
|
|
197
|
+
expect(loaded!.guardianPrincipalId).toBe("principal-dev");
|
|
198
|
+
|
|
199
|
+
// Idempotent — second call is a no-op.
|
|
200
|
+
expect(seedGuardianTokenFromSiblingEnv("alpha")).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("seedGuardianTokenFromSiblingEnv returns false when no sibling token exists", () => {
|
|
204
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
205
|
+
expect(seedGuardianTokenFromSiblingEnv("nonexistent")).toBe(false);
|
|
206
|
+
expect(loadGuardianToken("nonexistent")).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("seedGuardianTokenFromSiblingEnv does not overwrite an existing token", () => {
|
|
210
|
+
// Token already present in the current env.
|
|
211
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
212
|
+
saveGuardianToken("alpha", makeTokenData("local"));
|
|
213
|
+
|
|
214
|
+
// And a different sibling token in dev.
|
|
215
|
+
process.env.VELLUM_ENVIRONMENT = "dev";
|
|
216
|
+
saveGuardianToken("alpha", makeTokenData("dev"));
|
|
217
|
+
|
|
218
|
+
// Back to local — seed should no-op because a token is already present.
|
|
219
|
+
process.env.VELLUM_ENVIRONMENT = "local";
|
|
220
|
+
expect(seedGuardianTokenFromSiblingEnv("alpha")).toBe(false);
|
|
221
|
+
expect(loadGuardianToken("alpha")!.guardianPrincipalId).toBe(
|
|
222
|
+
"principal-local",
|
|
223
|
+
);
|
|
224
|
+
});
|
|
172
225
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
LLM_PROVIDER_ENV_VAR_NAMES,
|
|
7
|
+
SEARCH_PROVIDER_ENV_VAR_NAMES,
|
|
8
|
+
} from "../shared/provider-env-vars.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Drift guard for the CLI-side LLM provider env-var mirror.
|
|
12
|
+
*
|
|
13
|
+
* `cli/src/shared/provider-env-vars.ts` hardcodes the LLM env-var names so the
|
|
14
|
+
* CLI doesn't need to import the assistant's `PROVIDER_CATALOG` (no CLI →
|
|
15
|
+
* assistant cross-package imports exist). This test pulls the catalog JSON at
|
|
16
|
+
* `meta/llm-provider-catalog.json` — which is kept in sync with
|
|
17
|
+
* `PROVIDER_CATALOG` by `assistant/src/__tests__/llm-catalog-parity.test.ts` —
|
|
18
|
+
* and asserts the CLI's mirror matches the catalog's `envVar` entries.
|
|
19
|
+
*
|
|
20
|
+
* It also asserts the search-provider mirror matches `meta/provider-env-vars.json`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = join(import.meta.dir, "..", "..", "..");
|
|
24
|
+
|
|
25
|
+
interface LlmCatalogEntry {
|
|
26
|
+
id: string;
|
|
27
|
+
envVar?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LlmCatalog {
|
|
31
|
+
version: number;
|
|
32
|
+
providers: LlmCatalogEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SearchProviderRegistry {
|
|
36
|
+
version: number;
|
|
37
|
+
providers: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadLlmCatalog(): LlmCatalog {
|
|
41
|
+
const path = join(REPO_ROOT, "meta", "llm-provider-catalog.json");
|
|
42
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadSearchProviderRegistry(): SearchProviderRegistry {
|
|
46
|
+
const path = join(REPO_ROOT, "meta", "provider-env-vars.json");
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("CLI provider env-var parity", () => {
|
|
51
|
+
test("LLM_PROVIDER_ENV_VAR_NAMES matches meta/llm-provider-catalog.json entries with envVar", () => {
|
|
52
|
+
const catalog = loadLlmCatalog();
|
|
53
|
+
const expected: Record<string, string> = {};
|
|
54
|
+
for (const provider of catalog.providers) {
|
|
55
|
+
if (provider.envVar) expected[provider.id] = provider.envVar;
|
|
56
|
+
}
|
|
57
|
+
expect(LLM_PROVIDER_ENV_VAR_NAMES).toEqual(expected);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("SEARCH_PROVIDER_ENV_VAR_NAMES matches meta/provider-env-vars.json", () => {
|
|
61
|
+
const registry = loadSearchProviderRegistry();
|
|
62
|
+
expect(SEARCH_PROVIDER_ENV_VAR_NAMES).toEqual(registry.providers);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -159,6 +159,36 @@ describe("multi-local", () => {
|
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
test("allocation picks env-specific port bases for non-prod envs", async () => {
|
|
163
|
+
// Each non-prod env sits in its own 1000-port window (see
|
|
164
|
+
// environments/seeds.ts). Hatching under VELLUM_ENVIRONMENT=dev should
|
|
165
|
+
// produce ports in the dev block (18000+), not the production defaults.
|
|
166
|
+
const prevEnv = process.env.VELLUM_ENVIRONMENT;
|
|
167
|
+
const prevXdg = process.env.XDG_DATA_HOME;
|
|
168
|
+
const xdgDataHome = mkdtempSync(join(tmpdir(), "cli-multi-xdg-ports-"));
|
|
169
|
+
process.env.VELLUM_ENVIRONMENT = "dev";
|
|
170
|
+
process.env.XDG_DATA_HOME = xdgDataHome;
|
|
171
|
+
try {
|
|
172
|
+
const res = await allocateLocalResources("dev-a");
|
|
173
|
+
expect(res.daemonPort).toBe(18000);
|
|
174
|
+
expect(res.gatewayPort).toBe(18100);
|
|
175
|
+
expect(res.qdrantPort).toBe(18200);
|
|
176
|
+
expect(res.cesPort).toBe(18300);
|
|
177
|
+
} finally {
|
|
178
|
+
if (prevEnv !== undefined) {
|
|
179
|
+
process.env.VELLUM_ENVIRONMENT = prevEnv;
|
|
180
|
+
} else {
|
|
181
|
+
delete process.env.VELLUM_ENVIRONMENT;
|
|
182
|
+
}
|
|
183
|
+
if (prevXdg !== undefined) {
|
|
184
|
+
process.env.XDG_DATA_HOME = prevXdg;
|
|
185
|
+
} else {
|
|
186
|
+
delete process.env.XDG_DATA_HOME;
|
|
187
|
+
}
|
|
188
|
+
rmSync(xdgDataHome, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
162
192
|
test("second instance gets distinct ports and dir when first instance is saved", async () => {
|
|
163
193
|
// GIVEN a first local assistant already exists in the lockfile
|
|
164
194
|
saveAssistantEntry(makeEntry("instance-a"));
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findAssistantByName,
|
|
5
|
+
loadLatestAssistant,
|
|
6
|
+
resolveCloud,
|
|
7
|
+
} from "../lib/assistant-config";
|
|
8
|
+
import { dockerResourceNames } from "../lib/docker";
|
|
9
|
+
import type { ServiceName } from "../lib/docker";
|
|
10
|
+
import { execAppleContainer } from "../lib/exec-apple-container";
|
|
11
|
+
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
12
|
+
|
|
13
|
+
const SERVICE_ALIASES: Record<string, ServiceName> = {
|
|
14
|
+
assistant: "assistant",
|
|
15
|
+
"vellum-assistant": "assistant",
|
|
16
|
+
gateway: "gateway",
|
|
17
|
+
"vellum-gateway": "gateway",
|
|
18
|
+
"credential-executor": "credential-executor",
|
|
19
|
+
"vellum-credential-executor": "credential-executor",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function normalizeService(raw: string): ServiceName {
|
|
23
|
+
const normalized = SERVICE_ALIASES[raw];
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
console.error(
|
|
26
|
+
`Unknown service '${raw}'. Valid services: assistant, gateway, credential-executor`,
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveDockerContainer(
|
|
34
|
+
instanceName: string,
|
|
35
|
+
service: ServiceName,
|
|
36
|
+
): string {
|
|
37
|
+
const res = dockerResourceNames(instanceName);
|
|
38
|
+
switch (service) {
|
|
39
|
+
case "assistant":
|
|
40
|
+
return res.assistantContainer;
|
|
41
|
+
case "gateway":
|
|
42
|
+
return res.gatewayContainer;
|
|
43
|
+
case "credential-executor":
|
|
44
|
+
return res.cesContainer;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function exec(): Promise<void> {
|
|
49
|
+
const rawArgs = process.argv.slice(3);
|
|
50
|
+
|
|
51
|
+
// Only check for help flags before the -- separator so that
|
|
52
|
+
// `vellum exec -- curl --help` passes through correctly.
|
|
53
|
+
const dashDashIndex = rawArgs.indexOf("--");
|
|
54
|
+
const preArgs =
|
|
55
|
+
dashDashIndex === -1 ? rawArgs : rawArgs.slice(0, dashDashIndex);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
preArgs.includes("--help") ||
|
|
59
|
+
preArgs.includes("-h") ||
|
|
60
|
+
rawArgs.length === 0
|
|
61
|
+
) {
|
|
62
|
+
console.log(
|
|
63
|
+
"Usage: vellum exec [<name>] [--service <svc>] [-it] -- <command...>",
|
|
64
|
+
);
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log("Execute a command inside an assistant's container.");
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log("Arguments:");
|
|
69
|
+
console.log(
|
|
70
|
+
" <name> Name of the assistant (defaults to active)",
|
|
71
|
+
);
|
|
72
|
+
console.log(
|
|
73
|
+
" <command...> Command and arguments to run (after --)",
|
|
74
|
+
);
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log("Options:");
|
|
77
|
+
console.log(
|
|
78
|
+
" --service <svc> Target service (default: assistant)",
|
|
79
|
+
);
|
|
80
|
+
console.log(
|
|
81
|
+
" -it Interactive mode with TTY (like docker exec -it)",
|
|
82
|
+
);
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log("Services:");
|
|
85
|
+
console.log(" assistant (or vellum-assistant)");
|
|
86
|
+
console.log(" gateway (or vellum-gateway)");
|
|
87
|
+
console.log(" credential-executor (or vellum-credential-executor)");
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log("Examples:");
|
|
90
|
+
console.log(" vellum exec -- ls -la /workspace");
|
|
91
|
+
console.log(" vellum exec -- cat /workspace/NOW.md");
|
|
92
|
+
console.log(" vellum exec -it -- /bin/bash");
|
|
93
|
+
console.log(
|
|
94
|
+
" vellum exec --service gateway -- cat /tmp/gateway.log",
|
|
95
|
+
);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (dashDashIndex === -1) {
|
|
100
|
+
console.error(
|
|
101
|
+
"Error: missing '--' separator before command.\n" +
|
|
102
|
+
"Usage: vellum exec [<name>] -- <command...>",
|
|
103
|
+
);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const command = rawArgs.slice(dashDashIndex + 1);
|
|
108
|
+
|
|
109
|
+
if (command.length === 0) {
|
|
110
|
+
console.error("Error: no command specified after '--'.");
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let nameArg: string | undefined;
|
|
115
|
+
let serviceRaw = "assistant";
|
|
116
|
+
let interactive = false;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < preArgs.length; i++) {
|
|
119
|
+
if (preArgs[i] === "--service" && preArgs[i + 1]) {
|
|
120
|
+
serviceRaw = preArgs[++i];
|
|
121
|
+
} else if (preArgs[i] === "-it" || preArgs[i] === "-ti") {
|
|
122
|
+
interactive = true;
|
|
123
|
+
} else if (!preArgs[i].startsWith("-")) {
|
|
124
|
+
nameArg = preArgs[i];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const service = normalizeService(serviceRaw);
|
|
129
|
+
|
|
130
|
+
const entry = nameArg
|
|
131
|
+
? findAssistantByName(nameArg)
|
|
132
|
+
: loadLatestAssistant();
|
|
133
|
+
|
|
134
|
+
if (!entry) {
|
|
135
|
+
if (nameArg) {
|
|
136
|
+
console.error(`No assistant instance found with name '${nameArg}'.`);
|
|
137
|
+
} else {
|
|
138
|
+
console.error("No assistant instance found. Run `vellum hatch` first.");
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cloud = resolveCloud(entry);
|
|
144
|
+
|
|
145
|
+
if (cloud === "local") {
|
|
146
|
+
console.error(
|
|
147
|
+
"Cannot exec into a local assistant — it runs directly on this machine.",
|
|
148
|
+
);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (cloud === "apple-container") {
|
|
153
|
+
const fullServiceName = `vellum-${service}`;
|
|
154
|
+
if (interactive) {
|
|
155
|
+
await sshAppleContainer(entry, command, fullServiceName);
|
|
156
|
+
} else {
|
|
157
|
+
await execAppleContainer(entry, command, fullServiceName);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (cloud === "docker") {
|
|
163
|
+
const container = resolveDockerContainer(entry.assistantId, service);
|
|
164
|
+
const dockerArgs = interactive
|
|
165
|
+
? ["exec", "-it", container, ...command]
|
|
166
|
+
: ["exec", container, ...command];
|
|
167
|
+
|
|
168
|
+
const child = spawn("docker", dockerArgs, { stdio: "inherit" });
|
|
169
|
+
await new Promise<void>((resolve, reject) => {
|
|
170
|
+
child.on("close", (code) => {
|
|
171
|
+
if (code === 0) resolve();
|
|
172
|
+
else {
|
|
173
|
+
process.exitCode = code ?? 1;
|
|
174
|
+
resolve();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
child.on("error", reject);
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.error(
|
|
183
|
+
`Error: 'vellum exec' is not supported for ${cloud} instances.`,
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
package/src/commands/login.ts
CHANGED
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
fetchOrganizationId,
|
|
16
16
|
getPlatformUrl,
|
|
17
17
|
injectCredentialsIntoAssistant,
|
|
18
|
+
readGatewayCredential,
|
|
18
19
|
readPlatformToken,
|
|
20
|
+
reprovisionAssistantApiKey,
|
|
19
21
|
savePlatformToken,
|
|
20
22
|
} from "../lib/platform-client";
|
|
21
23
|
|
|
@@ -193,12 +195,41 @@ export async function login(): Promise<void> {
|
|
|
193
195
|
`Registered assistant: ${registration.assistant.name} (${registration.assistant.id})`,
|
|
194
196
|
);
|
|
195
197
|
|
|
198
|
+
// Resolve the API key to inject, mirroring the macOS app's
|
|
199
|
+
// LocalAssistantBootstrapService 3-step flow:
|
|
200
|
+
// 1. Use fresh key from registration (first-time only)
|
|
201
|
+
// 2. Use existing key from the daemon's credential store
|
|
202
|
+
// 3. Reprovision (rotate) as a last resort — this revokes the
|
|
203
|
+
// old key server-side, so we only do it when the gateway
|
|
204
|
+
// confirms no key exists (not when it's merely unreachable).
|
|
205
|
+
let assistantApiKey = registration.assistant_api_key;
|
|
206
|
+
if (!assistantApiKey) {
|
|
207
|
+
const cached = await readGatewayCredential(
|
|
208
|
+
entry.runtimeUrl,
|
|
209
|
+
"vellum:assistant_api_key",
|
|
210
|
+
entry.bearerToken,
|
|
211
|
+
);
|
|
212
|
+
if (cached.value) {
|
|
213
|
+
assistantApiKey = cached.value;
|
|
214
|
+
} else if (!cached.unreachable) {
|
|
215
|
+
console.log("No API key available locally — reprovisioning...");
|
|
216
|
+
const reprovision = await reprovisionAssistantApiKey(
|
|
217
|
+
token,
|
|
218
|
+
orgId,
|
|
219
|
+
clientInstallationId,
|
|
220
|
+
entry.assistantId,
|
|
221
|
+
"cli",
|
|
222
|
+
);
|
|
223
|
+
assistantApiKey = reprovision.provisioning.assistant_api_key;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
// Inject credentials into the running assistant via the gateway,
|
|
197
228
|
// mirroring the desktop app's LocalAssistantBootstrapService flow.
|
|
198
229
|
const allInjected = await injectCredentialsIntoAssistant({
|
|
199
230
|
gatewayUrl: entry.runtimeUrl,
|
|
200
231
|
bearerToken: entry.bearerToken,
|
|
201
|
-
assistantApiKey
|
|
232
|
+
assistantApiKey,
|
|
202
233
|
platformAssistantId: registration.assistant.id,
|
|
203
234
|
platformBaseUrl: getPlatformUrl(),
|
|
204
235
|
organizationId: orgId,
|
package/src/commands/retire.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
findAssistantByName,
|
|
6
|
+
loadAllAssistants,
|
|
3
7
|
removeAssistantEntry,
|
|
4
8
|
} from "../lib/assistant-config";
|
|
5
9
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
10
|
+
import { getConfigDir } from "../lib/environments/paths";
|
|
11
|
+
import { getCurrentEnvironment } from "../lib/environments/resolve";
|
|
6
12
|
import {
|
|
7
13
|
authHeaders,
|
|
8
14
|
getPlatformUrl,
|
|
@@ -246,4 +252,21 @@ async function retireInner(): Promise<void> {
|
|
|
246
252
|
|
|
247
253
|
removeAssistantEntry(name);
|
|
248
254
|
console.log(`Removed ${name} from config.`);
|
|
255
|
+
|
|
256
|
+
// When no assistants remain, remove the dock-display-name sentinel so
|
|
257
|
+
// the next build.sh run falls back to "Vellum" instead of using the
|
|
258
|
+
// retired assistant's name.
|
|
259
|
+
if (loadAllAssistants().length === 0) {
|
|
260
|
+
const dockLabelFile = join(
|
|
261
|
+
getConfigDir(getCurrentEnvironment()),
|
|
262
|
+
"dock-display-name",
|
|
263
|
+
);
|
|
264
|
+
if (existsSync(dockLabelFile)) {
|
|
265
|
+
try {
|
|
266
|
+
unlinkSync(dockLabelFile);
|
|
267
|
+
} catch {
|
|
268
|
+
// Best-effort — the macOS app will also reset this on next launch.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
249
272
|
}
|
package/src/commands/ssh.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../lib/assistant-config";
|
|
7
7
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
8
|
import { dockerResourceNames } from "../lib/docker";
|
|
9
|
-
import { sshAppleContainer } from "
|
|
9
|
+
import { sshAppleContainer } from "../lib/ssh-apple-container";
|
|
10
10
|
|
|
11
11
|
const SSH_OPTS = [
|
|
12
12
|
"-o",
|
package/src/commands/teleport.ts
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
platformImportBundleFromGcs,
|
|
28
28
|
platformPollImportStatus,
|
|
29
29
|
ensureSelfHostedLocalRegistration,
|
|
30
|
+
readGatewayCredential,
|
|
31
|
+
reprovisionAssistantApiKey,
|
|
30
32
|
injectCredentialsIntoAssistant,
|
|
31
33
|
fetchCurrentUser,
|
|
32
34
|
fetchOrganizationId,
|
|
@@ -1135,10 +1137,35 @@ async function tryInjectPlatformCredentials(
|
|
|
1135
1137
|
"cli",
|
|
1136
1138
|
);
|
|
1137
1139
|
|
|
1140
|
+
// Resolve the API key: 1) fresh from registration, 2) existing from
|
|
1141
|
+
// daemon credential store, 3) reprovision as last resort (revokes old key).
|
|
1142
|
+
// Only reprovision when the gateway confirms no key exists — not when
|
|
1143
|
+
// the gateway is merely unreachable (would revoke without injecting).
|
|
1144
|
+
let assistantApiKey = registration.assistant_api_key;
|
|
1145
|
+
if (!assistantApiKey) {
|
|
1146
|
+
const cached = await readGatewayCredential(
|
|
1147
|
+
entry.runtimeUrl,
|
|
1148
|
+
"vellum:assistant_api_key",
|
|
1149
|
+
entry.bearerToken,
|
|
1150
|
+
);
|
|
1151
|
+
if (cached.value) {
|
|
1152
|
+
assistantApiKey = cached.value;
|
|
1153
|
+
} else if (!cached.unreachable) {
|
|
1154
|
+
const reprovision = await reprovisionAssistantApiKey(
|
|
1155
|
+
token,
|
|
1156
|
+
orgId,
|
|
1157
|
+
clientInstallationId,
|
|
1158
|
+
entry.assistantId,
|
|
1159
|
+
"cli",
|
|
1160
|
+
);
|
|
1161
|
+
assistantApiKey = reprovision.provisioning.assistant_api_key;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1138
1165
|
const allInjected = await injectCredentialsIntoAssistant({
|
|
1139
1166
|
gatewayUrl: entry.runtimeUrl,
|
|
1140
1167
|
bearerToken: entry.bearerToken,
|
|
1141
|
-
assistantApiKey
|
|
1168
|
+
assistantApiKey,
|
|
1142
1169
|
platformAssistantId: registration.assistant.id,
|
|
1143
1170
|
platformBaseUrl: getPlatformUrl(),
|
|
1144
1171
|
organizationId: orgId,
|