@vellumai/cli 0.8.5 → 0.8.7

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 (72) 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 +21 -0
  12. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +93 -0
  13. package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
  14. package/node_modules/@vellumai/local-mode/src/config.ts +59 -0
  15. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +67 -0
  16. package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
  17. package/node_modules/@vellumai/local-mode/src/hatch.ts +74 -0
  18. package/node_modules/@vellumai/local-mode/src/index.ts +26 -0
  19. package/node_modules/@vellumai/local-mode/src/lockfile.ts +131 -0
  20. package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
  21. package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
  22. package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
  23. package/package.json +12 -1
  24. package/src/__tests__/backup.test.ts +38 -0
  25. package/src/__tests__/env-drift.test.ts +32 -44
  26. package/src/__tests__/flags.test.ts +248 -0
  27. package/src/__tests__/multi-local.test.ts +1 -1
  28. package/src/__tests__/orphan-detection.test.ts +8 -6
  29. package/src/__tests__/recover.test.ts +307 -0
  30. package/src/__tests__/segments-to-plain-text.test.ts +37 -0
  31. package/src/__tests__/wake.test.ts +215 -0
  32. package/src/commands/backup.ts +2 -0
  33. package/src/commands/client.ts +471 -30
  34. package/src/commands/env.ts +1 -1
  35. package/src/commands/flags.ts +269 -0
  36. package/src/commands/gateway/token.ts +73 -0
  37. package/src/commands/gateway.ts +29 -0
  38. package/src/commands/logs.ts +6 -18
  39. package/src/commands/ps.ts +41 -41
  40. package/src/commands/recover.ts +47 -9
  41. package/src/commands/restore.ts +8 -1
  42. package/src/commands/retire.ts +3 -23
  43. package/src/commands/rollback.ts +2 -14
  44. package/src/commands/ssh.ts +5 -24
  45. package/src/commands/teleport.ts +34 -26
  46. package/src/commands/upgrade.ts +8 -16
  47. package/src/commands/wake.ts +68 -45
  48. package/src/components/DefaultMainScreen.tsx +16 -1
  49. package/src/index.ts +6 -0
  50. package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
  51. package/src/lib/__tests__/step-runner.test.ts +49 -1
  52. package/src/lib/assistant-config.ts +16 -3
  53. package/src/lib/config-utils.ts +24 -3
  54. package/src/lib/docker.ts +57 -7
  55. package/src/lib/environments/__tests__/paths.test.ts +2 -1
  56. package/src/lib/environments/__tests__/seeds.test.ts +2 -1
  57. package/src/lib/environments/paths.ts +1 -1
  58. package/src/lib/environments/resolve.ts +2 -5
  59. package/src/lib/guardian-token.ts +12 -5
  60. package/src/lib/hatch-local.ts +75 -33
  61. package/src/lib/http-client.ts +1 -3
  62. package/src/lib/lifecycle-reporter.ts +31 -0
  63. package/src/lib/local.ts +173 -292
  64. package/src/lib/orphan-detection.ts +9 -5
  65. package/src/lib/pgrep.ts +5 -1
  66. package/src/lib/platform-client.ts +97 -49
  67. package/src/lib/process.ts +109 -39
  68. package/src/lib/retire-local.ts +28 -14
  69. package/src/lib/segments-to-plain-text.ts +35 -0
  70. package/src/lib/step-runner.ts +67 -7
  71. package/src/lib/sync-cloud-assistants.ts +17 -0
  72. /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
@@ -0,0 +1,122 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import type { CliInvocation } from "./util";
6
+
7
+ const GUARDIAN_TOKEN_REFRESH_TIMEOUT_MS = 15_000;
8
+
9
+ interface GuardianTokenData {
10
+ accessToken: string;
11
+ accessTokenExpiresAt: string | number;
12
+ refreshToken: string;
13
+ refreshTokenExpiresAt: string | number;
14
+ }
15
+
16
+ function isAccessTokenExpired(data: GuardianTokenData): boolean {
17
+ const expiresAt = new Date(data.accessTokenExpiresAt).getTime();
18
+ if (!Number.isFinite(expiresAt)) return true;
19
+ return Date.now() >= expiresAt - 60_000;
20
+ }
21
+
22
+ function isRefreshTokenExpired(data: GuardianTokenData): boolean {
23
+ const expiresAt = new Date(data.refreshTokenExpiresAt).getTime();
24
+ if (!Number.isFinite(expiresAt)) return true;
25
+ return Date.now() >= expiresAt;
26
+ }
27
+
28
+ export type TokenResult =
29
+ | { ok: true; accessToken: string }
30
+ | { ok: false; status: number; error: string };
31
+
32
+ export function getGuardianAccessToken(
33
+ assistantId: string,
34
+ configDir: string,
35
+ invocation: CliInvocation,
36
+ isLoopback: boolean,
37
+ env?: Record<string, string>,
38
+ ): Promise<TokenResult> {
39
+ if (!isLoopback) {
40
+ return Promise.resolve({ ok: false, status: 403, error: "Forbidden" });
41
+ }
42
+
43
+ const tokenPath = path.join(configDir, "assistants", assistantId, "guardian-token.json");
44
+
45
+ let raw: string;
46
+ try {
47
+ raw = fs.readFileSync(tokenPath, "utf-8");
48
+ } catch {
49
+ return Promise.resolve({ ok: false, status: 404, error: "Guardian token not found" });
50
+ }
51
+
52
+ let data: GuardianTokenData;
53
+ try {
54
+ data = JSON.parse(raw) as GuardianTokenData;
55
+ } catch {
56
+ return Promise.resolve({ ok: false, status: 500, error: "Malformed guardian token file" });
57
+ }
58
+
59
+ if (!isAccessTokenExpired(data)) {
60
+ return Promise.resolve({ ok: true, accessToken: data.accessToken });
61
+ }
62
+
63
+ if (isRefreshTokenExpired(data)) {
64
+ return Promise.resolve({
65
+ ok: false,
66
+ status: 401,
67
+ error: "Guardian token expired — re-run `vellum hatch` or `vellum wake`",
68
+ });
69
+ }
70
+
71
+ return refreshToken(assistantId, invocation, env);
72
+ }
73
+
74
+ function refreshToken(
75
+ assistantId: string,
76
+ invocation: CliInvocation,
77
+ env?: Record<string, string>,
78
+ ): Promise<TokenResult> {
79
+ return new Promise((resolve) => {
80
+ const child = spawn(
81
+ invocation.command,
82
+ [...invocation.baseArgs, "gateway", "token", "refresh", assistantId],
83
+ { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } },
84
+ );
85
+
86
+ let stdout = "";
87
+ let done = false;
88
+
89
+ const finish = (result: TokenResult) => {
90
+ if (done) return;
91
+ done = true;
92
+ clearTimeout(timeout);
93
+ resolve(result);
94
+ };
95
+
96
+ const timeout = setTimeout(() => {
97
+ child.kill("SIGTERM");
98
+ finish({ ok: false, status: 500, error: "Guardian token refresh timed out" });
99
+ }, GUARDIAN_TOKEN_REFRESH_TIMEOUT_MS);
100
+
101
+ child.stdout.on("data", (chunk: Buffer) => {
102
+ stdout += chunk.toString();
103
+ });
104
+
105
+ child.on("close", (code) => {
106
+ if (code === 0) {
107
+ const accessToken = stdout.trim();
108
+ if (accessToken) {
109
+ finish({ ok: true, accessToken });
110
+ } else {
111
+ finish({ ok: false, status: 500, error: "CLI returned empty token" });
112
+ }
113
+ } else {
114
+ finish({ ok: false, status: 401, error: "Failed to refresh guardian token" });
115
+ }
116
+ });
117
+
118
+ child.on("error", (err) => {
119
+ finish({ ok: false, status: 500, error: `Failed to spawn CLI: ${err.message}` });
120
+ });
121
+ });
122
+ }
@@ -0,0 +1,74 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import type { CliInvocation } from "./util";
4
+
5
+ const HATCH_TIMEOUT_MS = 120_000;
6
+
7
+ export type HatchResult =
8
+ | { ok: true; assistantId: string }
9
+ | { ok: false; status: number; error: string };
10
+
11
+ export function runHatch(
12
+ invocation: CliInvocation,
13
+ species: string,
14
+ ): Promise<HatchResult> {
15
+ return new Promise((resolve) => {
16
+ const child = spawn(
17
+ invocation.command,
18
+ [...invocation.baseArgs, "hatch", species],
19
+ { stdio: ["ignore", "pipe", "pipe"] },
20
+ );
21
+
22
+ let stdout = "";
23
+ let stderr = "";
24
+ let done = false;
25
+
26
+ const finish = (result: HatchResult) => {
27
+ if (done) return;
28
+ done = true;
29
+ clearTimeout(timeout);
30
+ resolve(result);
31
+ };
32
+
33
+ const timeout = setTimeout(() => {
34
+ child.kill("SIGTERM");
35
+ finish({ ok: false, status: 500, error: "Hatch timed out after 120 seconds" });
36
+ }, HATCH_TIMEOUT_MS);
37
+
38
+ child.stdout.on("data", (data: Buffer) => {
39
+ stdout += data.toString();
40
+ });
41
+
42
+ child.stderr.on("data", (data: Buffer) => {
43
+ stderr += data.toString();
44
+ });
45
+
46
+ child.on("close", (code) => {
47
+ if (code !== 0) {
48
+ const error =
49
+ stderr.trim() ||
50
+ stdout.trim() ||
51
+ `Hatch failed: the CLI exited with code ${code ?? "unknown"} and produced no output.`;
52
+ finish({ ok: false, status: 500, error });
53
+ return;
54
+ }
55
+ const assistantId = stdout
56
+ .match(/Hatching local assistant:\s+(.+)/)?.[1]
57
+ ?.trim();
58
+ if (!assistantId) {
59
+ finish({
60
+ ok: false,
61
+ status: 500,
62
+ error:
63
+ "Hatch reported success but no assistant id was found in the CLI output.",
64
+ });
65
+ return;
66
+ }
67
+ finish({ ok: true, assistantId });
68
+ });
69
+
70
+ child.on("error", (err) => {
71
+ finish({ ok: false, status: 500, error: `Failed to spawn CLI: ${err.message}` });
72
+ });
73
+ });
74
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @vellumai/local-mode — shared host library for serving the local-assistant
3
+ * surface (lockfile reads, guardian-token issuance, gateway proxying, and the
4
+ * hatch/retire lifecycle ops) over a loopback HTTP boundary. Consumed by the
5
+ * CLI `client` server and the web app's dev-server middleware so the local
6
+ * endpoint behaviour is defined exactly once instead of one host reaching into
7
+ * another's source tree. Depends only on `@vellumai/environments`.
8
+ */
9
+ export {
10
+ stripSensitiveFields,
11
+ isLoopbackAddr,
12
+ resolveDevCliInvocation,
13
+ } from "./util";
14
+ export type { CliInvocation } from "./util";
15
+ export { resolveLocalConfigFromEnv, resolveLockfilePaths, resolveConfigDir } from "./config";
16
+ export type { LocalEndpointConfig } from "./config";
17
+ export { getLockfileData, upsertLockfileAssistant, replacePlatformAssistants } from "./lockfile";
18
+ export type { LockfileResult, WriteResult } from "./lockfile";
19
+ export { runHatch } from "./hatch";
20
+ export type { HatchResult } from "./hatch";
21
+ export { runRetire } from "./retire";
22
+ export type { RetireResult } from "./retire";
23
+ export { getGuardianAccessToken } from "./guardian-token";
24
+ export type { TokenResult } from "./guardian-token";
25
+ export { parseGatewayUrl, readAllowedGatewayPorts } from "./gateway-proxy";
26
+ export type { GatewayTarget, GatewayParseResult } from "./gateway-proxy";
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { stripSensitiveFields } from "./util";
5
+
6
+ export type LockfileResult =
7
+ | { ok: true; data: Record<string, unknown> }
8
+ | { ok: false; status: number; error?: string };
9
+
10
+ export function getLockfileData(lockfilePaths: string[]): LockfileResult {
11
+ let raw: string | undefined;
12
+ for (const candidate of lockfilePaths) {
13
+ try {
14
+ raw = fs.readFileSync(candidate, "utf-8");
15
+ break;
16
+ } catch (err: unknown) {
17
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
18
+ return { ok: false, status: 500 };
19
+ }
20
+ }
21
+ }
22
+
23
+ if (!raw) {
24
+ return { ok: true, data: { assistants: [], activeAssistant: null } };
25
+ }
26
+
27
+ try {
28
+ const data = JSON.parse(raw) as Record<string, unknown>;
29
+ stripSensitiveFields(data);
30
+ return { ok: true, data };
31
+ } catch {
32
+ return { ok: false, status: 500 };
33
+ }
34
+ }
35
+
36
+ export type WriteResult =
37
+ | { ok: true; lockfile: Record<string, unknown> }
38
+ | { ok: false; status: number; error: string };
39
+
40
+ export function upsertLockfileAssistant(
41
+ lockfilePaths: string[],
42
+ assistant: Record<string, unknown>,
43
+ activeAssistant: string | undefined,
44
+ ): WriteResult {
45
+ if (!assistant || typeof assistant.assistantId !== "string") {
46
+ return { ok: false, status: 400, error: "Missing assistant.assistantId" };
47
+ }
48
+
49
+ let lockfile: Record<string, unknown> = { assistants: [], activeAssistant: null };
50
+ for (const candidate of lockfilePaths) {
51
+ try {
52
+ lockfile = JSON.parse(fs.readFileSync(candidate, "utf-8")) as Record<string, unknown>;
53
+ break;
54
+ } catch {
55
+ // continue
56
+ }
57
+ }
58
+
59
+ const assistants = Array.isArray(lockfile.assistants) ? lockfile.assistants : [];
60
+ const existingIdx = assistants.findIndex(
61
+ (a: Record<string, unknown>) => a?.assistantId === assistant.assistantId,
62
+ );
63
+ if (existingIdx >= 0) {
64
+ assistants[existingIdx] = { ...assistants[existingIdx], ...assistant };
65
+ } else {
66
+ assistants.push(assistant);
67
+ }
68
+ lockfile.assistants = assistants;
69
+ if (activeAssistant !== undefined) {
70
+ lockfile.activeAssistant = activeAssistant;
71
+ }
72
+
73
+ const writePath = lockfilePaths[0]!;
74
+ try {
75
+ const dir = path.dirname(writePath);
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ const tmp = `${writePath}.tmp.${process.pid}`;
78
+ fs.writeFileSync(tmp, JSON.stringify(lockfile, null, 2));
79
+ fs.renameSync(tmp, writePath);
80
+ } catch (err) {
81
+ return { ok: false, status: 500, error: `Failed to write lockfile: ${err}` };
82
+ }
83
+
84
+ const stripped = JSON.parse(JSON.stringify(lockfile)) as Record<string, unknown>;
85
+ stripSensitiveFields(stripped);
86
+ return { ok: true, lockfile: stripped };
87
+ }
88
+
89
+ export function replacePlatformAssistants(
90
+ lockfilePaths: string[],
91
+ platformAssistants: Array<Record<string, unknown>>,
92
+ ): WriteResult {
93
+ let lockfile: Record<string, unknown> = { assistants: [], activeAssistant: null };
94
+ for (const candidate of lockfilePaths) {
95
+ try {
96
+ lockfile = JSON.parse(fs.readFileSync(candidate, "utf-8")) as Record<string, unknown>;
97
+ break;
98
+ } catch {
99
+ // continue
100
+ }
101
+ }
102
+
103
+ const existing = Array.isArray(lockfile.assistants) ? lockfile.assistants : [];
104
+ const local = existing.filter(
105
+ (a: Record<string, unknown>) => a?.cloud !== "vellum",
106
+ );
107
+ lockfile.assistants = [...local, ...platformAssistants];
108
+
109
+ const active = lockfile.activeAssistant as string | null;
110
+ if (active) {
111
+ const stillExists = (lockfile.assistants as Array<Record<string, unknown>>).some(
112
+ (a) => a.assistantId === active,
113
+ );
114
+ if (!stillExists) lockfile.activeAssistant = null;
115
+ }
116
+
117
+ const writePath = lockfilePaths[0]!;
118
+ try {
119
+ const dir = path.dirname(writePath);
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ const tmp = `${writePath}.tmp.${process.pid}`;
122
+ fs.writeFileSync(tmp, JSON.stringify(lockfile, null, 2));
123
+ fs.renameSync(tmp, writePath);
124
+ } catch (err) {
125
+ return { ok: false, status: 500, error: `Failed to write lockfile: ${err}` };
126
+ }
127
+
128
+ const stripped = JSON.parse(JSON.stringify(lockfile)) as Record<string, unknown>;
129
+ stripSensitiveFields(stripped);
130
+ return { ok: true, lockfile: stripped };
131
+ }
@@ -0,0 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import type { CliInvocation } from "./util";
4
+
5
+ const RETIRE_TIMEOUT_MS = 60_000;
6
+
7
+ export type RetireResult =
8
+ | { ok: true }
9
+ | { ok: false; status: number; error: string };
10
+
11
+ export function runRetire(
12
+ invocation: CliInvocation,
13
+ assistantId: string,
14
+ ): Promise<RetireResult> {
15
+ return new Promise((resolve) => {
16
+ const child = spawn(
17
+ invocation.command,
18
+ [...invocation.baseArgs, "retire", assistantId, "--yes"],
19
+ { stdio: ["ignore", "pipe", "pipe"] },
20
+ );
21
+
22
+ let stdout = "";
23
+ let stderr = "";
24
+ let done = false;
25
+
26
+ const finish = (result: RetireResult) => {
27
+ if (done) return;
28
+ done = true;
29
+ clearTimeout(timeout);
30
+ resolve(result);
31
+ };
32
+
33
+ const timeout = setTimeout(() => {
34
+ child.kill("SIGTERM");
35
+ finish({ ok: false, status: 500, error: "Retire timed out after 60 seconds" });
36
+ }, RETIRE_TIMEOUT_MS);
37
+
38
+ child.stdout.on("data", (data: Buffer) => {
39
+ stdout += data.toString();
40
+ });
41
+
42
+ child.stderr.on("data", (data: Buffer) => {
43
+ stderr += data.toString();
44
+ });
45
+
46
+ child.on("close", (code) => {
47
+ if (code === 0) {
48
+ finish({ ok: true });
49
+ } else {
50
+ finish({ ok: false, status: 500, error: stderr || stdout });
51
+ }
52
+ });
53
+
54
+ child.on("error", (err) => {
55
+ finish({ ok: false, status: 500, error: `Failed to spawn CLI: ${err.message}` });
56
+ });
57
+ });
58
+ }
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+
5
+ const CLI_PACKAGE_NAME = "@vellumai/cli";
6
+
7
+ /**
8
+ * How to invoke the Vellum CLI as a child process: a base command plus the
9
+ * leading arguments that precede the subcommand. Each host resolves its own
10
+ * invocation — the dev hosts run the CLI from source via `bun run <entry>`,
11
+ * a packaged host would point at its bundled runtime — and the shared
12
+ * lifecycle ops (`runHatch`, `runRetire`, guardian-token refresh) append
13
+ * their subcommand args to `baseArgs`.
14
+ */
15
+ export interface CliInvocation {
16
+ command: string;
17
+ baseArgs: string[];
18
+ }
19
+
20
+ const SENSITIVE_FIELDS = [
21
+ "signingKey",
22
+ "bearerToken",
23
+ "guardianBootstrapSecret",
24
+ ] as const;
25
+
26
+ export function stripSensitiveFields(data: Record<string, unknown>): void {
27
+ const assistants = data.assistants;
28
+ if (!Array.isArray(assistants)) return;
29
+ for (const assistant of assistants) {
30
+ if (assistant && typeof assistant === "object") {
31
+ const entry = assistant as Record<string, unknown>;
32
+ for (const field of SENSITIVE_FIELDS) {
33
+ delete entry[field];
34
+ }
35
+ const resources = entry.resources;
36
+ if (resources && typeof resources === "object") {
37
+ for (const field of SENSITIVE_FIELDS) {
38
+ delete (resources as Record<string, unknown>)[field];
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ export function isLoopbackAddr(addr: string): boolean {
46
+ const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
47
+ const normalized = v4Mapped ? v4Mapped[1]! : addr;
48
+ if (normalized.includes(".")) {
49
+ return normalized.startsWith("127.");
50
+ }
51
+ return normalized === "::1";
52
+ }
53
+
54
+ let _resolvedCliPath: string | undefined;
55
+
56
+ /**
57
+ * Resolve the CLI entry point.
58
+ *
59
+ * 1. Source tree — `<baseDir>/cli/src/index.ts` (dev mode in monorepo).
60
+ * 2. Installed package — `require.resolve("@vellumai/cli/package.json")`.
61
+ */
62
+ function resolveCliPath(baseDir: string, importMetaUrl?: string): string {
63
+ if (_resolvedCliPath) return _resolvedCliPath;
64
+
65
+ const sourceTreePath = path.join(baseDir, "cli", "src", "index.ts");
66
+ if (fs.existsSync(sourceTreePath)) {
67
+ _resolvedCliPath = sourceTreePath;
68
+ return _resolvedCliPath;
69
+ }
70
+
71
+ const _require = createRequire(importMetaUrl ?? `file://${baseDir}/`);
72
+ try {
73
+ const pkgPath = _require.resolve(`${CLI_PACKAGE_NAME}/package.json`);
74
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { bin?: Record<string, string> };
75
+ const binEntry = pkg.bin?.["vellum"];
76
+ if (binEntry) {
77
+ const entryPoint = path.resolve(path.dirname(pkgPath), binEntry);
78
+ if (fs.existsSync(entryPoint)) {
79
+ _resolvedCliPath = entryPoint;
80
+ return _resolvedCliPath;
81
+ }
82
+ }
83
+ } catch {
84
+ // Not found in node_modules
85
+ }
86
+
87
+ throw new Error(
88
+ `Vellum CLI not found. Looked for source tree at ${sourceTreePath} and npm package ${CLI_PACKAGE_NAME}.`,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Build the CLI invocation used by the dev hosts (CLI client server and the
94
+ * web dev-server middleware), which run the CLI from source via Bun:
95
+ * `bun run <cli-entry> <subcommand> …`.
96
+ */
97
+ export function resolveDevCliInvocation(
98
+ baseDir: string,
99
+ importMetaUrl?: string,
100
+ ): CliInvocation {
101
+ return { command: "bun", baseArgs: ["run", resolveCliPath(baseDir, importMetaUrl)] };
102
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "noEmit": true,
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules"]
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,6 +8,10 @@
8
8
  "./package.json": "./package.json",
9
9
  "./src/components/DefaultMainScreen": "./src/components/DefaultMainScreen.tsx",
10
10
  "./src/lib/constants": "./src/lib/constants.ts",
11
+ "./src/lib/hatch-local": "./src/lib/hatch-local.ts",
12
+ "./src/lib/retire-local": "./src/lib/retire-local.ts",
13
+ "./src/lib/guardian-token": "./src/lib/guardian-token.ts",
14
+ "./src/lib/lifecycle-reporter": "./src/lib/lifecycle-reporter.ts",
11
15
  "./src/commands/*": "./src/commands/*.ts"
12
16
  },
13
17
  "bin": {
@@ -18,18 +22,25 @@
18
22
  "format:check": "prettier --check .",
19
23
  "lint": "eslint",
20
24
  "lint:unused": "knip --include files,dependencies,unlisted",
25
+ "prepack": "node ../scripts/prepack-bundled-deps.mjs",
21
26
  "test": "bun test",
22
27
  "typecheck": "bunx tsc --noEmit"
23
28
  },
24
29
  "author": "Vellum AI",
25
30
  "license": "MIT",
26
31
  "dependencies": {
32
+ "@vellumai/environments": "file:../packages/environments",
33
+ "@vellumai/local-mode": "file:../packages/local-mode",
27
34
  "chalk": "5.6.2",
28
35
  "ink": "6.8.0",
29
36
  "nanoid": "5.1.7",
30
37
  "react": "19.2.4",
31
38
  "react-devtools-core": "6.1.5"
32
39
  },
40
+ "bundledDependencies": [
41
+ "@vellumai/environments",
42
+ "@vellumai/local-mode"
43
+ ],
33
44
  "devDependencies": {
34
45
  "@types/bun": "1.3.11",
35
46
  "@types/react": "19.2.14",
@@ -162,6 +162,16 @@ beforeEach(() => {
162
162
  });
163
163
  getBackupsDirMock.mockReset();
164
164
  getBackupsDirMock.mockReturnValue("/tmp/backups-default");
165
+ loadGuardianTokenSpy.mockReset();
166
+ loadGuardianTokenSpy.mockReturnValue({
167
+ accessToken: "local-token",
168
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
169
+ } as unknown as ReturnType<typeof guardianToken.loadGuardianToken>);
170
+ leaseGuardianTokenSpy.mockReset();
171
+ leaseGuardianTokenSpy.mockResolvedValue({
172
+ accessToken: "leased-token",
173
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
174
+ } as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
165
175
  mkdirSyncMock.mockReset();
166
176
  mkdirSyncMock.mockImplementation((() => undefined) as never);
167
177
  writeFileSyncMock.mockReset();
@@ -207,6 +217,34 @@ function mockGcsDownload(body: Uint8Array, ok = true, status = 200) {
207
217
  }) as unknown as typeof globalThis.fetch;
208
218
  }
209
219
 
220
+ describe("vellum backup <local>: guardian bootstrap secret", () => {
221
+ test("passes the lockfile bootstrap secret when leasing a fresh guardian token", async () => {
222
+ const localEntry = {
223
+ assistantId: "local-assistant",
224
+ runtimeUrl: "http://127.0.0.1:7830",
225
+ cloud: "local",
226
+ guardianBootstrapSecret: "bootstrap-secret-value",
227
+ } satisfies assistantConfig.AssistantEntry;
228
+ findAssistantByNameMock.mockReturnValue(localEntry);
229
+ loadGuardianTokenSpy.mockReturnValue(null);
230
+ setArgv("my-local", "--output", "/tmp/local-backup.vbundle");
231
+
232
+ globalThis.fetch = mock(async () => {
233
+ return new Response(new Uint8Array([1, 2, 3]), {
234
+ status: 200,
235
+ });
236
+ }) as unknown as typeof globalThis.fetch;
237
+
238
+ await backup();
239
+
240
+ expect(leaseGuardianTokenSpy).toHaveBeenCalledWith(
241
+ "http://127.0.0.1:7830",
242
+ "local-assistant",
243
+ "bootstrap-secret-value",
244
+ );
245
+ });
246
+ });
247
+
210
248
  describe("vellum backup <platform-managed>: GCS happy path", () => {
211
249
  test("requests upload URL → kicks off runtime export → polls → downloads from GCS → writes file", async () => {
212
250
  findAssistantByNameMock.mockReturnValue(VELLUM_ENTRY);