@vellumai/cli 0.6.6 → 0.7.0

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.
Files changed (45) hide show
  1. package/AGENTS.md +8 -2
  2. package/package.json +1 -1
  3. package/src/__tests__/assistant-config.test.ts +1 -7
  4. package/src/__tests__/config-utils.test.ts +159 -0
  5. package/src/__tests__/env-drift.test.ts +10 -32
  6. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  7. package/src/__tests__/multi-local.test.ts +0 -5
  8. package/src/__tests__/sleep.test.ts +1 -2
  9. package/src/__tests__/teleport.test.ts +919 -1255
  10. package/src/commands/env.ts +93 -0
  11. package/src/commands/events.ts +2 -0
  12. package/src/commands/exec.ts +40 -8
  13. package/src/commands/hatch.ts +6 -2
  14. package/src/commands/login.ts +89 -6
  15. package/src/commands/ps.ts +104 -20
  16. package/src/commands/sleep.ts +5 -2
  17. package/src/commands/ssh.ts +15 -2
  18. package/src/commands/teleport.ts +447 -583
  19. package/src/commands/terminal.ts +9 -221
  20. package/src/commands/wake.ts +2 -1
  21. package/src/components/DefaultMainScreen.tsx +304 -152
  22. package/src/index.ts +3 -0
  23. package/src/lib/__tests__/docker.test.ts +50 -74
  24. package/src/lib/__tests__/job-polling.test.ts +278 -0
  25. package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
  26. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  27. package/src/lib/assistant-config.ts +12 -8
  28. package/src/lib/client-identity.ts +67 -0
  29. package/src/lib/config-utils.ts +97 -1
  30. package/src/lib/docker.ts +73 -75
  31. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  32. package/src/lib/environments/resolve.ts +89 -7
  33. package/src/lib/environments/seeds.ts +8 -5
  34. package/src/lib/environments/types.ts +10 -0
  35. package/src/lib/hatch-local.ts +15 -120
  36. package/src/lib/health-check.ts +98 -0
  37. package/src/lib/job-polling.ts +195 -0
  38. package/src/lib/local-runtime-client.ts +178 -0
  39. package/src/lib/local.ts +139 -15
  40. package/src/lib/orphan-detection.ts +2 -35
  41. package/src/lib/platform-client.ts +215 -0
  42. package/src/lib/retire-local.ts +6 -2
  43. package/src/lib/terminal-session.ts +457 -0
  44. package/src/shared/provider-env-vars.ts +2 -3
  45. package/src/__tests__/orphan-detection.test.ts +0 -214
package/AGENTS.md CHANGED
@@ -41,6 +41,12 @@ Every command must have high-quality `--help` output. Follow the same standards
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
43
 
44
+ ## Boundary: No integration-specific references
45
+
46
+ The CLI is a generic lifecycle manager. It must **never** contain references to specific skills, integrations, or features (e.g. "Meet", "Slack", "Telegram"). Environment variables, volume mounts, and device passthroughs defined here must use generic names (e.g. `VELLUM_AVATAR_DEVICE`, not `VELLUM_MEET_AVATAR_DEVICE`). The skill that uses a resource decides how to interpret it — the CLI just passes it through.
47
+
48
+ Cross-package imports into `skills/` are forbidden. The CLI is distributed as an npm package; anything outside `cli/` is not included in the tarball and will fail to resolve at runtime.
49
+
44
50
  ## Boundary: No `.vellum/` directory access
45
51
 
46
52
  The CLI must **never** read from or write to the `.vellum/` directory (e.g. `~/.vellum/protected/`, `<instanceDir>/.vellum/`). That directory structure is an **assistant daemon / gateway implementation detail**. The CLI's job is to spawn those processes and pass configuration via environment variables — not to reach into their internal storage.
@@ -62,9 +68,9 @@ The CLI creates and manages Docker volumes for containerized instances. See the
62
68
  **Meet Docker-in-Docker support** (assistant container only): The assistant container runs an inner `dockerd` that hosts the Meet-bot containers as nested children. The CLI supports this by:
63
69
 
64
70
  - Creating a dedicated `<name>-dockerd-data` volume mounted at `/var/lib/docker` so pulled images and container state persist across assistant restarts.
65
- - Running the assistant container with `--privileged` (or `CAP_SYS_ADMIN` + `CAP_NET_ADMIN`) so the inner dockerd can configure cgroups, overlay mounts, and container networking.
71
+ - Running the assistant container with `CAP_SYS_ADMIN` + `CAP_NET_ADMIN` plus `--security-opt seccomp=unconfined` + `--security-opt apparmor=unconfined` so the inner dockerd can configure cgroups, overlay mounts, and container networking without the default seccomp profile blocking clone/unshare/pivot_root syscalls or the default AppArmor profile denying its mount operations. `--privileged` is deliberately avoided — dropping it shrinks the escape surface by withholding the rest of the host capability set and access to host device nodes.
66
72
  - No longer bind-mounting the host's `/var/run/docker.sock`; Meet-bot spawning happens entirely inside the assistant container.
67
73
 
68
74
  Both are wired in `serviceDockerRunArgs()` in `lib/docker.ts`.
69
75
 
70
- The privileged assistant container is acceptable for single-user local deployments. Managed/multi-tenant mode needs a different spawn model (e.g. a Kubernetes job runner) and is out of scope for this CLI.
76
+ This capability + security-opt set is acceptable for single-user local deployments. Managed/multi-tenant mode needs a different spawn model (e.g. a Kubernetes job runner) and is out of scope for this CLI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -216,7 +216,7 @@ describe("migrateLegacyEntry", () => {
216
216
  test("synthesises full resources when none exist", () => {
217
217
  /**
218
218
  * Tests that a legacy local entry with no resources object gets a
219
- * complete resources object synthesised with default ports and pidFile.
219
+ * complete resources object synthesised with default ports.
220
220
  */
221
221
 
222
222
  // GIVEN a local entry with no resources
@@ -239,7 +239,6 @@ describe("migrateLegacyEntry", () => {
239
239
  expect(resources.gatewayPort).toBe(7830);
240
240
  expect(resources.qdrantPort).toBe(6333);
241
241
  expect(resources.cesPort).toBe(8090);
242
- expect(resources.pidFile).toContain("vellum.pid");
243
242
  });
244
243
 
245
244
  test("infers gateway port from runtimeUrl", () => {
@@ -312,7 +311,6 @@ describe("migrateLegacyEntry", () => {
312
311
  expect(resources.gatewayPort).toBe(7830);
313
312
  expect(resources.qdrantPort).toBe(6333);
314
313
  expect(resources.cesPort).toBe(8090);
315
- expect(resources.pidFile).toBe("/custom/path/.vellum/vellum.pid");
316
314
  });
317
315
 
318
316
  test("does not overwrite existing resources fields", () => {
@@ -331,7 +329,6 @@ describe("migrateLegacyEntry", () => {
331
329
  gatewayPort: 8001,
332
330
  qdrantPort: 8002,
333
331
  cesPort: 8003,
334
- pidFile: "/my/path/.vellum/vellum.pid",
335
332
  },
336
333
  };
337
334
 
@@ -366,7 +363,6 @@ describe("migrateLegacyEntry", () => {
366
363
  daemonPort: 7821,
367
364
  gatewayPort: 7830,
368
365
  qdrantPort: 6333,
369
- pidFile: "/new/path/.vellum/vellum.pid",
370
366
  },
371
367
  };
372
368
 
@@ -393,7 +389,6 @@ describe("migrateLegacyEntry", () => {
393
389
  daemonPort: 7821,
394
390
  gatewayPort: 7830,
395
391
  qdrantPort: 6333,
396
- pidFile: "/custom/path/.vellum/vellum.pid",
397
392
  },
398
393
  };
399
394
  // WHEN we migrate the entry
@@ -415,7 +410,6 @@ describe("migrateLegacyEntry", () => {
415
410
  gatewayPort: 8001,
416
411
  qdrantPort: 8002,
417
412
  cesPort: 9090,
418
- pidFile: "/my/path/.vellum/vellum.pid",
419
413
  },
420
414
  };
421
415
  const changed = migrateLegacyEntry(entry);
@@ -0,0 +1,159 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildInitialConfig, buildNestedConfig } from "../lib/config-utils.js";
4
+
5
+ describe("config-utils", () => {
6
+ test("buildNestedConfig only converts dot-notation values", () => {
7
+ expect(
8
+ buildNestedConfig({
9
+ "llm.default.provider": "anthropic",
10
+ "llm.default.model": "claude-sonnet-4-6",
11
+ }),
12
+ ).toEqual({
13
+ llm: {
14
+ default: {
15
+ provider: "anthropic",
16
+ model: "claude-sonnet-4-6",
17
+ },
18
+ },
19
+ });
20
+ });
21
+
22
+ test("buildInitialConfig seeds mainAgent callSite for Anthropic default", () => {
23
+ expect(
24
+ buildInitialConfig({
25
+ "llm.default.provider": "anthropic",
26
+ "llm.default.model": "claude-opus-4-7",
27
+ }),
28
+ ).toEqual({
29
+ llm: {
30
+ default: {
31
+ provider: "anthropic",
32
+ model: "claude-opus-4-7",
33
+ },
34
+ callSites: {
35
+ mainAgent: {
36
+ model: "claude-opus-4-7",
37
+ maxTokens: 32000,
38
+ },
39
+ },
40
+ },
41
+ });
42
+ });
43
+
44
+ test("buildInitialConfig seeds Opus when provider falls back to Anthropic", () => {
45
+ expect(
46
+ buildInitialConfig({
47
+ "services.inference.mode": "managed",
48
+ }),
49
+ ).toEqual({
50
+ services: {
51
+ inference: {
52
+ mode: "managed",
53
+ },
54
+ },
55
+ llm: {
56
+ callSites: {
57
+ mainAgent: {
58
+ model: "claude-opus-4-7",
59
+ maxTokens: 32000,
60
+ },
61
+ },
62
+ },
63
+ });
64
+ });
65
+
66
+ test("buildInitialConfig preserves explicit mainAgent overrides", () => {
67
+ expect(
68
+ buildInitialConfig({
69
+ "llm.default.provider": "anthropic",
70
+ "llm.default.model": "claude-opus-4-7",
71
+ "llm.callSites.mainAgent.model": "claude-haiku-4-5-20251001",
72
+ }),
73
+ ).toEqual({
74
+ llm: {
75
+ default: {
76
+ provider: "anthropic",
77
+ model: "claude-opus-4-7",
78
+ },
79
+ callSites: {
80
+ mainAgent: {
81
+ model: "claude-haiku-4-5-20251001",
82
+ },
83
+ },
84
+ },
85
+ });
86
+ });
87
+
88
+ test("buildInitialConfig respects explicit non-default Anthropic models", () => {
89
+ expect(
90
+ buildInitialConfig({
91
+ "llm.default.provider": "anthropic",
92
+ "llm.default.model": "claude-haiku-4-5-20251001",
93
+ }),
94
+ ).toEqual({
95
+ llm: {
96
+ default: {
97
+ provider: "anthropic",
98
+ model: "claude-haiku-4-5-20251001",
99
+ },
100
+ },
101
+ });
102
+ });
103
+
104
+ test("buildInitialConfig respects active profile provider overrides", () => {
105
+ expect(
106
+ buildInitialConfig({
107
+ "llm.activeProfile": "fast",
108
+ "llm.profiles.fast.provider": "openai",
109
+ "llm.profiles.fast.model": "gpt-5.5",
110
+ }),
111
+ ).toEqual({
112
+ llm: {
113
+ activeProfile: "fast",
114
+ profiles: {
115
+ fast: {
116
+ provider: "openai",
117
+ model: "gpt-5.5",
118
+ },
119
+ },
120
+ },
121
+ });
122
+ });
123
+
124
+ test("buildInitialConfig uses active profile model when deciding to seed", () => {
125
+ expect(
126
+ buildInitialConfig({
127
+ "llm.activeProfile": "fast",
128
+ "llm.profiles.fast.provider": "anthropic",
129
+ "llm.profiles.fast.model": "claude-haiku-4-5-20251001",
130
+ }),
131
+ ).toEqual({
132
+ llm: {
133
+ activeProfile: "fast",
134
+ profiles: {
135
+ fast: {
136
+ provider: "anthropic",
137
+ model: "claude-haiku-4-5-20251001",
138
+ },
139
+ },
140
+ },
141
+ });
142
+ });
143
+
144
+ test("buildInitialConfig does not seed Opus for non-Anthropic providers", () => {
145
+ expect(
146
+ buildInitialConfig({
147
+ "llm.default.provider": "openai",
148
+ "llm.default.model": "gpt-5.5",
149
+ }),
150
+ ).toEqual({
151
+ llm: {
152
+ default: {
153
+ provider: "openai",
154
+ model: "gpt-5.5",
155
+ },
156
+ },
157
+ });
158
+ });
159
+ });
@@ -4,25 +4,21 @@ import { join } from "node:path";
4
4
 
5
5
  import { SEEDS } from "../lib/environments/seeds.js";
6
6
 
7
- // Drift guard for the three TypeScript sites that each hardcode the set of
8
- // known environment names:
7
+ // Drift guard for the TypeScript sites that hardcode the set of known
8
+ // environment names:
9
9
  //
10
- // 1. cli/src/lib/environments/seeds.ts — SEEDS record (source of truth)
11
- // 2. assistant/src/util/platform.ts — KNOWN_ENVIRONMENTS set
12
- // 3. clients/chrome-extension/native-host/
13
- // src/lockfile.ts — NON_PRODUCTION_ENVIRONMENTS set
10
+ // 1. cli/src/lib/environments/seeds.ts — SEEDS record (source of truth)
11
+ // 2. assistant/src/util/platform.ts — KNOWN_ENVIRONMENTS set
14
12
  //
15
13
  // Cross-package relative imports don't work here: assistant's tsconfig
16
- // restricts `include` to its own src tree, and the native host is a
17
- // standalone TS project with `rootDir: ./src`. So this test parses the
18
- // literal sets out of the two external files and asserts they agree with
19
- // CLI's SEEDS.
14
+ // restricts `include` to its own src tree. So this test parses the literal
15
+ // set out of the external file and asserts it agrees with CLI's SEEDS.
20
16
  //
21
17
  // FOLLOW-UP: split the env name list into a shared `packages/environments`
22
- // package (mirroring `packages/ces-contracts`, `credential-storage`) so
23
- // all three sites can `import { KNOWN_ENVIRONMENTS }` from one place and
24
- // this drift guard becomes a compile-time check. Planned alongside CLI-
25
- // driven context support — see the "Environments" design doc.
18
+ // package (mirroring `packages/service-contracts`, `credential-storage`) so
19
+ // both sites can `import { KNOWN_ENVIRONMENTS }` from one place and this
20
+ // drift guard becomes a compile-time check. Planned alongside CLI-driven
21
+ // context support — see the "Environments" design doc.
26
22
 
27
23
  const REPO_ROOT = join(import.meta.dir, "..", "..", "..");
28
24
  const ASSISTANT_PLATFORM = join(
@@ -32,14 +28,6 @@ const ASSISTANT_PLATFORM = join(
32
28
  "util",
33
29
  "platform.ts",
34
30
  );
35
- const NATIVE_HOST_LOCKFILE = join(
36
- REPO_ROOT,
37
- "clients",
38
- "chrome-extension",
39
- "native-host",
40
- "src",
41
- "lockfile.ts",
42
- );
43
31
 
44
32
  /**
45
33
  * Extract the string literals from a Set constructor body in a TS source
@@ -74,14 +62,4 @@ describe("KNOWN_ENVIRONMENTS drift guard (TS-side)", () => {
74
62
  );
75
63
  expect([...assistantNames].sort()).toEqual([...seedNames].sort());
76
64
  });
77
-
78
- test("native-host/src/lockfile.ts NON_PRODUCTION_ENVIRONMENTS matches CLI SEEDS minus production", () => {
79
- const source = readFileSync(NATIVE_HOST_LOCKFILE, "utf8");
80
- const nativeNames = new Set(
81
- extractSetLiterals(source, "NON_PRODUCTION_ENVIRONMENTS"),
82
- );
83
- const expected = new Set(seedNames);
84
- expected.delete("production");
85
- expect([...nativeNames].sort()).toEqual([...expected].sort());
86
- });
87
65
  });
@@ -2,10 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { describe, expect, test } from "bun:test";
4
4
 
5
- import {
6
- LLM_PROVIDER_ENV_VAR_NAMES,
7
- SEARCH_PROVIDER_ENV_VAR_NAMES,
8
- } from "../shared/provider-env-vars.js";
5
+ import { LLM_PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
9
6
 
10
7
  /**
11
8
  * Drift guard for the CLI-side LLM provider env-var mirror.
@@ -16,8 +13,6 @@ import {
16
13
  * `meta/llm-provider-catalog.json` — which is kept in sync with
17
14
  * `PROVIDER_CATALOG` by `assistant/src/__tests__/llm-catalog-parity.test.ts` —
18
15
  * 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
16
  */
22
17
 
23
18
  const REPO_ROOT = join(import.meta.dir, "..", "..", "..");
@@ -32,21 +27,11 @@ interface LlmCatalog {
32
27
  providers: LlmCatalogEntry[];
33
28
  }
34
29
 
35
- interface SearchProviderRegistry {
36
- version: number;
37
- providers: Record<string, string>;
38
- }
39
-
40
30
  function loadLlmCatalog(): LlmCatalog {
41
31
  const path = join(REPO_ROOT, "meta", "llm-provider-catalog.json");
42
32
  return JSON.parse(readFileSync(path, "utf-8"));
43
33
  }
44
34
 
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
35
  describe("CLI provider env-var parity", () => {
51
36
  test("LLM_PROVIDER_ENV_VAR_NAMES matches meta/llm-provider-catalog.json entries with envVar", () => {
52
37
  const catalog = loadLlmCatalog();
@@ -56,9 +41,4 @@ describe("CLI provider env-var parity", () => {
56
41
  }
57
42
  expect(LLM_PROVIDER_ENV_VAR_NAMES).toEqual(expected);
58
43
  });
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
44
  });
@@ -114,11 +114,6 @@ describe("multi-local", () => {
114
114
  expect(res.daemonPort).toBe(DEFAULT_DAEMON_PORT);
115
115
  expect(res.gatewayPort).toBe(DEFAULT_GATEWAY_PORT);
116
116
  expect(res.qdrantPort).toBe(DEFAULT_QDRANT_PORT);
117
-
118
- // AND the PID file is under the instance's .vellum/
119
- expect(res.pidFile).toBe(
120
- join(res.instanceDir, ".vellum", "vellum.pid"),
121
- );
122
117
  } finally {
123
118
  if (prevXdg !== undefined) {
124
119
  process.env.XDG_DATA_HOME = prevXdg;
@@ -52,7 +52,6 @@ function writeLockfile(): void {
52
52
  daemonPort: DEFAULT_DAEMON_PORT,
53
53
  gatewayPort: DEFAULT_GATEWAY_PORT,
54
54
  qdrantPort: DEFAULT_QDRANT_PORT,
55
- pidFile: join(assistantRootDir, "vellum.pid"),
56
55
  },
57
56
  },
58
57
  ],
@@ -158,7 +157,7 @@ describe("sleep command", () => {
158
157
  expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(2);
159
158
  expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(
160
159
  1,
161
- join(assistantRootDir, "vellum.pid"),
160
+ join(assistantRootDir, "workspace", "vellum.pid"),
162
161
  "assistant",
163
162
  );
164
163
  expect(stopProcessByPidFileMock).toHaveBeenNthCalledWith(