@vellumai/cli 0.6.6 → 0.7.1

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 (61) hide show
  1. package/AGENTS.md +8 -2
  2. package/README.md +49 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/assistant-config.test.ts +1 -7
  5. package/src/__tests__/backup.test.ts +475 -0
  6. package/src/__tests__/config-utils.test.ts +146 -0
  7. package/src/__tests__/env-drift.test.ts +10 -32
  8. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  9. package/src/__tests__/multi-local.test.ts +0 -5
  10. package/src/__tests__/sleep.test.ts +1 -2
  11. package/src/__tests__/teleport.test.ts +988 -1266
  12. package/src/commands/backup.ts +117 -71
  13. package/src/commands/client.ts +10 -9
  14. package/src/commands/env.ts +93 -0
  15. package/src/commands/events.ts +2 -0
  16. package/src/commands/exec.ts +58 -13
  17. package/src/commands/login.ts +77 -12
  18. package/src/commands/logs.ts +2 -7
  19. package/src/commands/ps.ts +144 -25
  20. package/src/commands/restore.ts +26 -47
  21. package/src/commands/sleep.ts +5 -2
  22. package/src/commands/ssh.ts +17 -7
  23. package/src/commands/teleport.ts +462 -584
  24. package/src/commands/terminal.ts +9 -221
  25. package/src/commands/tunnel.ts +2 -7
  26. package/src/commands/upgrade.ts +108 -7
  27. package/src/commands/wake.ts +2 -1
  28. package/src/components/DefaultMainScreen.tsx +328 -154
  29. package/src/index.ts +5 -7
  30. package/src/lib/__tests__/docker.test.ts +50 -74
  31. package/src/lib/__tests__/job-polling.test.ts +278 -0
  32. package/src/lib/__tests__/local-runtime-client.test.ts +480 -0
  33. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  34. package/src/lib/__tests__/runtime-url.test.ts +87 -0
  35. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  36. package/src/lib/assistant-client.ts +5 -21
  37. package/src/lib/assistant-config.ts +46 -24
  38. package/src/lib/cli-error.ts +1 -0
  39. package/src/lib/client-identity.ts +67 -0
  40. package/src/lib/docker.ts +75 -77
  41. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  42. package/src/lib/environments/resolve.ts +89 -7
  43. package/src/lib/environments/seeds.ts +8 -5
  44. package/src/lib/environments/types.ts +10 -0
  45. package/src/lib/hatch-local.ts +15 -120
  46. package/src/lib/health-check.ts +98 -0
  47. package/src/lib/job-polling.ts +195 -0
  48. package/src/lib/local-runtime-client.ts +231 -0
  49. package/src/lib/local.ts +165 -72
  50. package/src/lib/orphan-detection.ts +2 -35
  51. package/src/lib/platform-client.ts +190 -194
  52. package/src/lib/platform-releases.ts +23 -0
  53. package/src/lib/retire-local.ts +6 -2
  54. package/src/lib/runtime-url.ts +30 -0
  55. package/src/lib/sync-cloud-assistants.ts +126 -0
  56. package/src/lib/terminal-client.ts +6 -1
  57. package/src/lib/terminal-session.ts +536 -0
  58. package/src/lib/tui-log.ts +60 -0
  59. package/src/lib/xdg-log.ts +10 -4
  60. package/src/shared/provider-env-vars.ts +2 -3
  61. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -0,0 +1,146 @@
1
+ import { readFileSync, rmSync } from "fs";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { buildNestedConfig, writeInitialConfig } from "../lib/config-utils.js";
5
+
6
+ function readInitialConfig(
7
+ configValues: Record<string, string>,
8
+ ): Record<string, unknown> {
9
+ const path = writeInitialConfig(configValues);
10
+ expect(path).toBeDefined();
11
+ try {
12
+ return JSON.parse(readFileSync(path!, "utf-8")) as Record<string, unknown>;
13
+ } finally {
14
+ if (path !== undefined) rmSync(path, { force: true });
15
+ }
16
+ }
17
+
18
+ describe("config-utils", () => {
19
+ test("buildNestedConfig only converts dot-notation values", () => {
20
+ expect(
21
+ buildNestedConfig({
22
+ "llm.default.provider": "anthropic",
23
+ "llm.default.model": "claude-sonnet-4-6",
24
+ }),
25
+ ).toEqual({
26
+ llm: {
27
+ default: {
28
+ provider: "anthropic",
29
+ model: "claude-sonnet-4-6",
30
+ },
31
+ },
32
+ });
33
+ });
34
+
35
+ test("writeInitialConfig does not add a mainAgent callSite for Anthropic defaults", () => {
36
+ expect(
37
+ readInitialConfig({
38
+ "llm.default.provider": "anthropic",
39
+ "llm.default.model": "claude-opus-4-7",
40
+ }),
41
+ ).toEqual({
42
+ llm: {
43
+ default: {
44
+ provider: "anthropic",
45
+ model: "claude-opus-4-7",
46
+ },
47
+ },
48
+ });
49
+ });
50
+
51
+ test("writeInitialConfig preserves profile-based Anthropic model selection", () => {
52
+ expect(
53
+ readInitialConfig({
54
+ "llm.activeProfile": "quality-optimized",
55
+ "llm.profiles.quality-optimized.provider": "anthropic",
56
+ "llm.profiles.quality-optimized.model": "claude-opus-4-7",
57
+ "llm.profiles.quality-optimized.maxTokens": "32000",
58
+ }),
59
+ ).toEqual({
60
+ llm: {
61
+ activeProfile: "quality-optimized",
62
+ profiles: {
63
+ "quality-optimized": {
64
+ provider: "anthropic",
65
+ model: "claude-opus-4-7",
66
+ maxTokens: "32000",
67
+ },
68
+ },
69
+ },
70
+ });
71
+ });
72
+
73
+ test("writeInitialConfig preserves explicit mainAgent overrides without rewriting them", () => {
74
+ expect(
75
+ readInitialConfig({
76
+ "llm.default.provider": "anthropic",
77
+ "llm.default.model": "claude-opus-4-7",
78
+ "llm.callSites.mainAgent.model": "claude-haiku-4-5-20251001",
79
+ }),
80
+ ).toEqual({
81
+ llm: {
82
+ default: {
83
+ provider: "anthropic",
84
+ model: "claude-opus-4-7",
85
+ },
86
+ callSites: {
87
+ mainAgent: {
88
+ model: "claude-haiku-4-5-20251001",
89
+ },
90
+ },
91
+ },
92
+ });
93
+ });
94
+
95
+ test("writeInitialConfig respects explicit non-default Anthropic models", () => {
96
+ expect(
97
+ readInitialConfig({
98
+ "llm.default.provider": "anthropic",
99
+ "llm.default.model": "claude-haiku-4-5-20251001",
100
+ }),
101
+ ).toEqual({
102
+ llm: {
103
+ default: {
104
+ provider: "anthropic",
105
+ model: "claude-haiku-4-5-20251001",
106
+ },
107
+ },
108
+ });
109
+ });
110
+
111
+ test("writeInitialConfig leaves active OpenAI profile config unchanged", () => {
112
+ expect(
113
+ readInitialConfig({
114
+ "llm.activeProfile": "fast",
115
+ "llm.profiles.fast.provider": "openai",
116
+ "llm.profiles.fast.model": "gpt-5.5",
117
+ }),
118
+ ).toEqual({
119
+ llm: {
120
+ activeProfile: "fast",
121
+ profiles: {
122
+ fast: {
123
+ provider: "openai",
124
+ model: "gpt-5.5",
125
+ },
126
+ },
127
+ },
128
+ });
129
+ });
130
+
131
+ test("writeInitialConfig does not add Opus for non-Anthropic providers", () => {
132
+ expect(
133
+ readInitialConfig({
134
+ "llm.default.provider": "openai",
135
+ "llm.default.model": "gpt-5.5",
136
+ }),
137
+ ).toEqual({
138
+ llm: {
139
+ default: {
140
+ provider: "openai",
141
+ model: "gpt-5.5",
142
+ },
143
+ },
144
+ });
145
+ });
146
+ });
@@ -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(