@vellumai/cli 0.8.5 → 0.8.7-dev.202606052118.34cd356

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 (102) hide show
  1. package/AGENTS.md +6 -0
  2. package/bun.lock +8 -0
  3. package/knip.json +6 -1
  4. package/node_modules/@vellumai/environments/bun.lock +24 -0
  5. package/node_modules/@vellumai/environments/package.json +18 -0
  6. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
  7. package/node_modules/@vellumai/environments/src/index.ts +11 -0
  8. package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
  9. package/node_modules/@vellumai/environments/tsconfig.json +20 -0
  10. package/node_modules/@vellumai/local-mode/bun.lock +29 -0
  11. package/node_modules/@vellumai/local-mode/package.json +22 -0
  12. package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
  13. package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
  14. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +108 -0
  15. package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
  16. package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
  17. package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
  18. package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
  19. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
  20. package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
  21. package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
  22. package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
  23. package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
  24. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
  25. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
  26. package/node_modules/@vellumai/local-mode/src/lockfile.ts +133 -0
  27. package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
  28. package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
  29. package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
  30. package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
  31. package/package.json +12 -1
  32. package/src/__tests__/assistant-client-refresh.test.ts +182 -0
  33. package/src/__tests__/backup.test.ts +38 -0
  34. package/src/__tests__/clean.test.ts +179 -0
  35. package/src/__tests__/client-token.test.ts +87 -0
  36. package/src/__tests__/client-tui-refresh.test.ts +170 -0
  37. package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
  38. package/src/__tests__/connect-import.test.ts +317 -0
  39. package/src/__tests__/devices.test.ts +272 -0
  40. package/src/__tests__/env-drift.test.ts +32 -44
  41. package/src/__tests__/flags.test.ts +248 -0
  42. package/src/__tests__/guardian-token.test.ts +126 -2
  43. package/src/__tests__/multi-local.test.ts +1 -1
  44. package/src/__tests__/orphan-detection.test.ts +8 -6
  45. package/src/__tests__/pair.test.ts +271 -0
  46. package/src/__tests__/paired-lifecycle.test.ts +116 -0
  47. package/src/__tests__/recover.test.ts +307 -0
  48. package/src/__tests__/segments-to-plain-text.test.ts +37 -0
  49. package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
  50. package/src/__tests__/unpair.test.ts +163 -0
  51. package/src/__tests__/wake.test.ts +215 -0
  52. package/src/commands/backup.ts +2 -0
  53. package/src/commands/client.ts +569 -39
  54. package/src/commands/connect/import.ts +217 -0
  55. package/src/commands/connect.ts +31 -0
  56. package/src/commands/devices.ts +247 -0
  57. package/src/commands/env.ts +1 -1
  58. package/src/commands/flags.ts +269 -0
  59. package/src/commands/gateway/token.ts +73 -0
  60. package/src/commands/gateway.ts +29 -0
  61. package/src/commands/logs.ts +6 -18
  62. package/src/commands/pair.ts +222 -0
  63. package/src/commands/ps.ts +57 -41
  64. package/src/commands/recover.ts +47 -9
  65. package/src/commands/restore.ts +8 -1
  66. package/src/commands/retire.ts +23 -70
  67. package/src/commands/rollback.ts +2 -14
  68. package/src/commands/sleep.ts +7 -0
  69. package/src/commands/ssh.ts +5 -24
  70. package/src/commands/teleport.ts +34 -26
  71. package/src/commands/tunnel.ts +46 -2
  72. package/src/commands/unpair.ts +118 -0
  73. package/src/commands/upgrade.ts +8 -16
  74. package/src/commands/wake.ts +75 -45
  75. package/src/components/DefaultMainScreen.tsx +100 -14
  76. package/src/index.ts +22 -0
  77. package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
  78. package/src/lib/__tests__/step-runner.test.ts +49 -1
  79. package/src/lib/assistant-client.ts +58 -37
  80. package/src/lib/assistant-config.ts +28 -3
  81. package/src/lib/cloudflare-tunnel.ts +276 -0
  82. package/src/lib/config-utils.ts +24 -3
  83. package/src/lib/confirm-action.ts +57 -0
  84. package/src/lib/docker.ts +82 -8
  85. package/src/lib/environments/__tests__/paths.test.ts +2 -1
  86. package/src/lib/environments/__tests__/seeds.test.ts +2 -1
  87. package/src/lib/environments/paths.ts +1 -1
  88. package/src/lib/environments/resolve.ts +11 -35
  89. package/src/lib/guardian-token.ts +132 -9
  90. package/src/lib/hatch-local.ts +75 -33
  91. package/src/lib/http-client.ts +1 -3
  92. package/src/lib/lifecycle-reporter.ts +31 -0
  93. package/src/lib/local.ts +193 -298
  94. package/src/lib/orphan-detection.ts +9 -5
  95. package/src/lib/pgrep.ts +5 -1
  96. package/src/lib/platform-client.ts +97 -49
  97. package/src/lib/process.ts +109 -39
  98. package/src/lib/retire-local.ts +28 -14
  99. package/src/lib/segments-to-plain-text.ts +35 -0
  100. package/src/lib/step-runner.ts +67 -7
  101. package/src/lib/sync-cloud-assistants.ts +17 -0
  102. /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Tests for `vellum unpair <name>`: forget a paired (cloud:"paired") assistant
3
+ * by removing its lockfile entry + guardian token. Refuses non-paired entries.
4
+ */
5
+ import {
6
+ afterAll,
7
+ afterEach,
8
+ beforeEach,
9
+ describe,
10
+ expect,
11
+ spyOn,
12
+ test,
13
+ } from "bun:test";
14
+ import { mkdtempSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ const testDir = mkdtempSync(join(tmpdir(), "unpair-test-"));
19
+ const ORIGINAL_LOCKFILE_DIR = process.env.VELLUM_LOCKFILE_DIR;
20
+ const ORIGINAL_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
21
+ const ORIGINAL_ARGV = [...process.argv];
22
+
23
+ import { unpair } from "../commands/unpair.js";
24
+ import {
25
+ findAssistantByName,
26
+ saveAssistantEntry,
27
+ } from "../lib/assistant-config.js";
28
+ import {
29
+ deleteGuardianToken,
30
+ loadGuardianToken,
31
+ saveGuardianToken,
32
+ } from "../lib/guardian-token.js";
33
+
34
+ function seedToken(assistantId: string): void {
35
+ saveGuardianToken(assistantId, {
36
+ guardianPrincipalId: "imported",
37
+ accessToken: "acc",
38
+ accessTokenExpiresAt: Date.now() + 3_600_000,
39
+ refreshToken: "ref",
40
+ refreshTokenExpiresAt: Date.now() + 3_600_000,
41
+ refreshAfter: "",
42
+ isNew: false,
43
+ deviceId: "dev",
44
+ leasedAt: new Date().toISOString(),
45
+ });
46
+ }
47
+
48
+ /** Run unpair with console.error + process.exit spied. */
49
+ async function runUnpair(): Promise<{ exited: boolean; errors: string }> {
50
+ const errors: string[] = [];
51
+ const logSpy = spyOn(console, "log").mockImplementation(() => {});
52
+ const errSpy = spyOn(console, "error").mockImplementation(
53
+ (...a: unknown[]) => {
54
+ errors.push(a.join(" "));
55
+ },
56
+ );
57
+ const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
58
+ throw new Error(`exit:${c}`);
59
+ }) as never);
60
+ let exited = false;
61
+ try {
62
+ await unpair();
63
+ } catch (e) {
64
+ exited = (e as Error).message === "exit:1";
65
+ } finally {
66
+ logSpy.mockRestore();
67
+ errSpy.mockRestore();
68
+ exitSpy.mockRestore();
69
+ }
70
+ return { exited, errors: errors.join("\n") };
71
+ }
72
+
73
+ describe("vellum unpair", () => {
74
+ beforeEach(() => {
75
+ process.env.VELLUM_LOCKFILE_DIR = testDir;
76
+ process.env.XDG_CONFIG_HOME = testDir;
77
+ });
78
+
79
+ afterEach(() => {
80
+ process.argv = [...ORIGINAL_ARGV];
81
+ if (ORIGINAL_LOCKFILE_DIR === undefined)
82
+ delete process.env.VELLUM_LOCKFILE_DIR;
83
+ else process.env.VELLUM_LOCKFILE_DIR = ORIGINAL_LOCKFILE_DIR;
84
+ if (ORIGINAL_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME;
85
+ else process.env.XDG_CONFIG_HOME = ORIGINAL_CONFIG_HOME;
86
+ });
87
+
88
+ afterAll(() => {
89
+ rmSync(testDir, { recursive: true, force: true });
90
+ });
91
+
92
+ test("removes a paired entry's lockfile entry and guardian token", async () => {
93
+ saveAssistantEntry({
94
+ assistantId: "px",
95
+ name: "Paired Box",
96
+ runtimeUrl: "http://10.0.0.9:7830",
97
+ cloud: "paired",
98
+ paired: true,
99
+ species: "vellum",
100
+ });
101
+ seedToken("px");
102
+ expect(findAssistantByName("px")).not.toBeNull();
103
+ expect(loadGuardianToken("px")).not.toBeNull();
104
+
105
+ // Non-interactive test env → use --yes to bypass the confirmation prompt.
106
+ process.argv = ["bun", "vellum", "unpair", "px", "--yes"];
107
+ const { exited } = await runUnpair();
108
+
109
+ expect(exited).toBe(false);
110
+ expect(findAssistantByName("px")).toBeNull();
111
+ expect(loadGuardianToken("px")).toBeNull();
112
+ });
113
+
114
+ test("refuses to unpair without --yes in a non-interactive terminal", async () => {
115
+ saveAssistantEntry({
116
+ assistantId: "py",
117
+ name: "Paired Two",
118
+ runtimeUrl: "http://10.0.0.9:7830",
119
+ cloud: "paired",
120
+ paired: true,
121
+ species: "vellum",
122
+ });
123
+ seedToken("py");
124
+
125
+ process.argv = ["bun", "vellum", "unpair", "py"]; // no --yes
126
+ const { exited, errors } = await runUnpair();
127
+
128
+ expect(exited).toBe(true);
129
+ expect(errors).toContain("--yes");
130
+ // Not removed — confirmation was required.
131
+ expect(findAssistantByName("py")).not.toBeNull();
132
+ expect(loadGuardianToken("py")).not.toBeNull();
133
+ });
134
+
135
+ test("refuses a non-paired (local) assistant and leaves it intact", async () => {
136
+ saveAssistantEntry({
137
+ assistantId: "desk",
138
+ name: "Desk",
139
+ runtimeUrl: "http://127.0.0.1:7830",
140
+ cloud: "local",
141
+ species: "vellum",
142
+ });
143
+
144
+ process.argv = ["bun", "vellum", "unpair", "desk"];
145
+ const { exited, errors } = await runUnpair();
146
+
147
+ expect(exited).toBe(true);
148
+ expect(errors).toContain("vellum retire");
149
+ expect(findAssistantByName("desk")).not.toBeNull(); // untouched
150
+ });
151
+
152
+ test("errors on an unknown name", async () => {
153
+ process.argv = ["bun", "vellum", "unpair", "nope"];
154
+ const { exited } = await runUnpair();
155
+ expect(exited).toBe(true);
156
+ });
157
+
158
+ test("deleteGuardianToken is a no-op when the token is absent", () => {
159
+ // No token seeded for this id; calling (twice) must not throw.
160
+ expect(() => deleteGuardianToken("ghost")).not.toThrow();
161
+ expect(() => deleteGuardianToken("ghost")).not.toThrow();
162
+ });
163
+ });
@@ -0,0 +1,215 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ mock,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
11
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import * as assistantConfig from "../lib/assistant-config.js";
16
+ import * as docker from "../lib/docker.js";
17
+ import * as guardianToken from "../lib/guardian-token.js";
18
+ import * as local from "../lib/local.js";
19
+ import * as ngrok from "../lib/ngrok.js";
20
+ import * as processLib from "../lib/process.js";
21
+ import type { AssistantEntry } from "../lib/assistant-config.js";
22
+
23
+ const realAssistantConfig = { ...assistantConfig };
24
+ const realDocker = { ...docker };
25
+ const realGuardianToken = { ...guardianToken };
26
+ const realLocal = { ...local };
27
+ const realNgrok = { ...ngrok };
28
+ const realProcessLib = { ...processLib };
29
+
30
+ const resolveTargetAssistantMock = mock<
31
+ typeof assistantConfig.resolveTargetAssistant
32
+ >();
33
+ const saveAssistantEntryMock = mock<typeof assistantConfig.saveAssistantEntry>(
34
+ () => {},
35
+ );
36
+ const getDaemonPidPathMock = mock<typeof assistantConfig.getDaemonPidPath>(
37
+ (resources) => join(resources!.instanceDir, ".vellum", "daemon.pid"),
38
+ );
39
+
40
+ mock.module("../lib/assistant-config.js", () => ({
41
+ ...realAssistantConfig,
42
+ resolveTargetAssistant: resolveTargetAssistantMock,
43
+ saveAssistantEntry: saveAssistantEntryMock,
44
+ getDaemonPidPath: getDaemonPidPathMock,
45
+ }));
46
+
47
+ const dockerResourceNamesMock = mock<typeof docker.dockerResourceNames>(
48
+ realDocker.dockerResourceNames,
49
+ );
50
+ const wakeContainersMock = mock<typeof docker.wakeContainers>(async () => {});
51
+
52
+ mock.module("../lib/docker.js", () => ({
53
+ ...realDocker,
54
+ dockerResourceNames: dockerResourceNamesMock,
55
+ wakeContainers: wakeContainersMock,
56
+ }));
57
+
58
+ const seedGuardianTokenFromSiblingEnvMock = mock<
59
+ typeof guardianToken.seedGuardianTokenFromSiblingEnv
60
+ >(() => false);
61
+
62
+ mock.module("../lib/guardian-token.js", () => ({
63
+ ...realGuardianToken,
64
+ seedGuardianTokenFromSiblingEnv: seedGuardianTokenFromSiblingEnvMock,
65
+ }));
66
+
67
+ const resolveProcessStateMock = mock<typeof processLib.resolveProcessState>(
68
+ async (_pidFile, _port, label) => ({
69
+ status: "healthy",
70
+ pid: label === "Gateway" ? 456 : 123,
71
+ }),
72
+ );
73
+ const stopProcessByPidFileMock = mock<typeof processLib.stopProcessByPidFile>(
74
+ async () => true,
75
+ );
76
+
77
+ mock.module("../lib/process", () => ({
78
+ ...realProcessLib,
79
+ resolveProcessState: resolveProcessStateMock,
80
+ stopProcessByPidFile: stopProcessByPidFileMock,
81
+ }));
82
+
83
+ const generateLocalSigningKeyMock = mock<typeof local.generateLocalSigningKey>(
84
+ () => "generated-bootstrap-secret",
85
+ );
86
+ const isAssistantWatchModeAvailableMock = mock<
87
+ typeof local.isAssistantWatchModeAvailable
88
+ >(() => false);
89
+ const isGatewayWatchModeAvailableMock = mock<
90
+ typeof local.isGatewayWatchModeAvailable
91
+ >(() => false);
92
+ const startLocalDaemonMock = mock<typeof local.startLocalDaemon>(async () => {});
93
+ const startGatewayMock = mock<typeof local.startGateway>(
94
+ async () => "http://127.0.0.1:7830",
95
+ );
96
+
97
+ mock.module("../lib/local", () => ({
98
+ ...realLocal,
99
+ generateLocalSigningKey: generateLocalSigningKeyMock,
100
+ isAssistantWatchModeAvailable: isAssistantWatchModeAvailableMock,
101
+ isGatewayWatchModeAvailable: isGatewayWatchModeAvailableMock,
102
+ startLocalDaemon: startLocalDaemonMock,
103
+ startGateway: startGatewayMock,
104
+ }));
105
+
106
+ const maybeStartNgrokTunnelMock = mock<typeof ngrok.maybeStartNgrokTunnel>(
107
+ async () => null,
108
+ );
109
+
110
+ mock.module("../lib/ngrok", () => ({
111
+ ...realNgrok,
112
+ maybeStartNgrokTunnel: maybeStartNgrokTunnelMock,
113
+ }));
114
+
115
+ const { wake } = await import("../commands/wake.js");
116
+
117
+ let tempDir: string;
118
+ let originalArgv: string[];
119
+ let logSpy: ReturnType<typeof spyOn>;
120
+
121
+ function makeLocalEntry(): AssistantEntry {
122
+ tempDir = mkdtempSync(join(tmpdir(), "vellum-wake-test-"));
123
+ mkdirSync(join(tempDir, ".vellum"), { recursive: true });
124
+ return {
125
+ assistantId: "local-assistant",
126
+ runtimeUrl: "http://127.0.0.1:7830",
127
+ cloud: "local",
128
+ resources: {
129
+ instanceDir: tempDir,
130
+ daemonPort: 7821,
131
+ gatewayPort: 7830,
132
+ qdrantPort: 6333,
133
+ cesPort: 7822,
134
+ signingKey: "existing-signing-key",
135
+ },
136
+ };
137
+ }
138
+
139
+ beforeEach(() => {
140
+ originalArgv = [...process.argv];
141
+ tempDir = "";
142
+ process.argv = ["bun", "vellum", "wake", "--watch", "local-assistant"];
143
+ logSpy = spyOn(console, "log").mockImplementation(() => {});
144
+
145
+ const entry = makeLocalEntry();
146
+ resolveTargetAssistantMock.mockReset();
147
+ resolveTargetAssistantMock.mockReturnValue(entry);
148
+ saveAssistantEntryMock.mockReset();
149
+ getDaemonPidPathMock.mockReset();
150
+ getDaemonPidPathMock.mockImplementation((resources) =>
151
+ join(resources!.instanceDir, ".vellum", "daemon.pid"),
152
+ );
153
+ resolveProcessStateMock.mockReset();
154
+ resolveProcessStateMock.mockImplementation(async (_pidFile, _port, label) => ({
155
+ status: "healthy",
156
+ pid: label === "Gateway" ? 456 : 123,
157
+ }));
158
+ stopProcessByPidFileMock.mockReset();
159
+ stopProcessByPidFileMock.mockResolvedValue(true);
160
+ generateLocalSigningKeyMock.mockReset();
161
+ generateLocalSigningKeyMock.mockReturnValue("generated-bootstrap-secret");
162
+ isAssistantWatchModeAvailableMock.mockReset();
163
+ isAssistantWatchModeAvailableMock.mockReturnValue(false);
164
+ isGatewayWatchModeAvailableMock.mockReset();
165
+ isGatewayWatchModeAvailableMock.mockReturnValue(false);
166
+ startLocalDaemonMock.mockReset();
167
+ startLocalDaemonMock.mockResolvedValue(undefined);
168
+ startGatewayMock.mockReset();
169
+ startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
170
+ seedGuardianTokenFromSiblingEnvMock.mockReset();
171
+ seedGuardianTokenFromSiblingEnvMock.mockReturnValue(false);
172
+ maybeStartNgrokTunnelMock.mockReset();
173
+ maybeStartNgrokTunnelMock.mockResolvedValue(null);
174
+ });
175
+
176
+ afterEach(() => {
177
+ process.argv = originalArgv;
178
+ logSpy.mockRestore();
179
+ if (tempDir) {
180
+ rmSync(tempDir, { recursive: true, force: true });
181
+ }
182
+ });
183
+
184
+ afterAll(() => {
185
+ mock.module("../lib/assistant-config.js", () => realAssistantConfig);
186
+ mock.module("../lib/docker.js", () => realDocker);
187
+ mock.module("../lib/guardian-token.js", () => realGuardianToken);
188
+ mock.module("../lib/process", () => realProcessLib);
189
+ mock.module("../lib/local", () => realLocal);
190
+ mock.module("../lib/ngrok", () => realNgrok);
191
+ });
192
+
193
+ describe("vellum wake", () => {
194
+ test("restarts a running gateway without watch mode when backfilling the bootstrap secret", async () => {
195
+ await wake();
196
+
197
+ expect(saveAssistantEntryMock).toHaveBeenCalledWith(
198
+ expect.objectContaining({
199
+ guardianBootstrapSecret: "generated-bootstrap-secret",
200
+ }),
201
+ );
202
+ expect(stopProcessByPidFileMock).toHaveBeenCalledWith(
203
+ join(tempDir, ".vellum", "gateway.pid"),
204
+ "gateway",
205
+ );
206
+ expect(startGatewayMock).toHaveBeenCalledWith(
207
+ false,
208
+ expect.objectContaining({ instanceDir: tempDir }),
209
+ {
210
+ signingKey: "existing-signing-key",
211
+ bootstrapSecret: "generated-bootstrap-secret",
212
+ },
213
+ );
214
+ });
215
+ });
@@ -93,6 +93,7 @@ export async function backup(): Promise<void> {
93
93
  const freshToken = await leaseGuardianToken(
94
94
  entry.runtimeUrl,
95
95
  entry.assistantId,
96
+ entry.guardianBootstrapSecret,
96
97
  );
97
98
  accessToken = freshToken.accessToken;
98
99
  } catch (err) {
@@ -129,6 +130,7 @@ export async function backup(): Promise<void> {
129
130
  const freshToken = await leaseGuardianToken(
130
131
  entry.runtimeUrl,
131
132
  entry.assistantId,
133
+ entry.guardianBootstrapSecret,
132
134
  );
133
135
  refreshedToken = freshToken.accessToken;
134
136
  } catch {