@vellumai/cli 0.8.6 → 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 (43) hide show
  1. package/bun.lock +8 -0
  2. package/knip.json +5 -1
  3. package/node_modules/@vellumai/environments/bun.lock +24 -0
  4. package/node_modules/@vellumai/environments/package.json +18 -0
  5. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
  6. package/node_modules/@vellumai/environments/src/index.ts +11 -0
  7. package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
  8. package/node_modules/@vellumai/environments/tsconfig.json +20 -0
  9. package/node_modules/@vellumai/local-mode/bun.lock +29 -0
  10. package/node_modules/@vellumai/local-mode/package.json +21 -0
  11. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +93 -0
  12. package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
  13. package/node_modules/@vellumai/local-mode/src/config.ts +59 -0
  14. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +67 -0
  15. package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
  16. package/node_modules/@vellumai/local-mode/src/hatch.ts +74 -0
  17. package/node_modules/@vellumai/local-mode/src/index.ts +26 -0
  18. package/node_modules/@vellumai/local-mode/src/lockfile.ts +131 -0
  19. package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
  20. package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
  21. package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
  22. package/package.json +12 -1
  23. package/src/__tests__/env-drift.test.ts +32 -44
  24. package/src/__tests__/flags.test.ts +248 -0
  25. package/src/__tests__/multi-local.test.ts +1 -1
  26. package/src/__tests__/orphan-detection.test.ts +8 -6
  27. package/src/__tests__/segments-to-plain-text.test.ts +37 -0
  28. package/src/commands/client.ts +413 -2
  29. package/src/commands/env.ts +1 -1
  30. package/src/commands/flags.ts +89 -17
  31. package/src/components/DefaultMainScreen.tsx +16 -1
  32. package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
  33. package/src/lib/assistant-config.ts +3 -3
  34. package/src/lib/environments/__tests__/paths.test.ts +2 -1
  35. package/src/lib/environments/__tests__/seeds.test.ts +2 -1
  36. package/src/lib/environments/paths.ts +1 -1
  37. package/src/lib/environments/resolve.ts +2 -5
  38. package/src/lib/guardian-token.ts +12 -5
  39. package/src/lib/hatch-local.ts +73 -33
  40. package/src/lib/lifecycle-reporter.ts +31 -0
  41. package/src/lib/retire-local.ts +28 -14
  42. package/src/lib/segments-to-plain-text.ts +35 -0
  43. /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
package/bun.lock CHANGED
@@ -5,6 +5,8 @@
5
5
  "": {
6
6
  "name": "@vellumai/cli",
7
7
  "dependencies": {
8
+ "@vellumai/environments": "file:../packages/environments",
9
+ "@vellumai/local-mode": "file:../packages/local-mode",
8
10
  "chalk": "5.6.2",
9
11
  "ink": "6.8.0",
10
12
  "nanoid": "5.1.7",
@@ -135,6 +137,10 @@
135
137
 
136
138
  "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
137
139
 
140
+ "@vellumai/environments": ["@vellumai/environments@file:../packages/environments", { "devDependencies": { "@types/bun": "1.3.11", "typescript": "5.9.3" } }],
141
+
142
+ "@vellumai/local-mode": ["@vellumai/local-mode@file:../packages/local-mode", { "dependencies": { "@vellumai/environments": "file:../environments" }, "devDependencies": { "@types/bun": "1.3.11", "typescript": "5.9.3" } }],
143
+
138
144
  "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
139
145
 
140
146
  "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -397,6 +403,8 @@
397
403
 
398
404
  "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
399
405
 
406
+ "@vellumai/local-mode/@vellumai/environments": ["@vellumai/environments@file:../packages/environments", {}],
407
+
400
408
  "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
401
409
 
402
410
  "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
package/knip.json CHANGED
@@ -8,5 +8,9 @@
8
8
  "src/lib/cli-error.ts"
9
9
  ],
10
10
  "project": ["src/**/*.ts", "src/**/*.tsx"],
11
- "ignoreDependencies": ["@vellumai/web"]
11
+ "ignoreDependencies": [
12
+ "@vellumai/environments",
13
+ "@vellumai/local-mode",
14
+ "@vellumai/web"
15
+ ]
12
16
  }
@@ -0,0 +1,24 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@vellumai/environments",
7
+ "devDependencies": {
8
+ "@types/bun": "1.3.11",
9
+ "typescript": "5.9.3",
10
+ },
11
+ },
12
+ },
13
+ "packages": {
14
+ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
15
+
16
+ "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
17
+
18
+ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
19
+
20
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
21
+
22
+ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
23
+ }
24
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@vellumai/environments",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "bunx tsc --noEmit",
12
+ "test": "bun test src/"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.11",
16
+ "typescript": "5.9.3"
17
+ }
18
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Package boundary tests for @vellumai/environments.
3
+ *
4
+ * This package is the lowest layer of the shared-packages hierarchy: pure
5
+ * environment types and constants with no runtime dependencies. It must
6
+ * stay a leaf so any consumer (CLI, assistant, local-mode host) can depend
7
+ * on it without dragging in a dependency tree.
8
+ *
9
+ * Enforces that the package:
10
+ * 1. Imports only node builtins and its own relative modules — no `@vellumai/*`
11
+ * packages and no third-party runtime imports.
12
+ * 2. Declares no runtime `dependencies` (devDependencies only).
13
+ * 3. Is marked `private`.
14
+ */
15
+
16
+ import { describe, expect, test } from "bun:test";
17
+ import { readFileSync, readdirSync, statSync } from "node:fs";
18
+ import { join, resolve } from "node:path";
19
+
20
+ const PACKAGE_ROOT = resolve(import.meta.dirname, "../..");
21
+ const SRC_DIR = join(PACKAGE_ROOT, "src");
22
+
23
+ function collectSourceFiles(dir: string): string[] {
24
+ const files: string[] = [];
25
+ for (const entry of readdirSync(dir)) {
26
+ const full = join(dir, entry);
27
+ const stat = statSync(full);
28
+ if (stat.isDirectory()) {
29
+ if (entry === "node_modules" || entry === "__tests__") continue;
30
+ files.push(...collectSourceFiles(full));
31
+ } else if (
32
+ entry.endsWith(".ts") &&
33
+ !entry.endsWith(".test.ts") &&
34
+ !entry.endsWith(".d.ts")
35
+ ) {
36
+ files.push(full);
37
+ }
38
+ }
39
+ return files;
40
+ }
41
+
42
+ /**
43
+ * Matches the module specifier of any `import ... from "<spec>"` /
44
+ * `export ... from "<spec>"` / `require("<spec>")` statement.
45
+ */
46
+ const IMPORT_SPEC = /(?:from|require\s*\(\s*)["']([^"']+)["']/g;
47
+
48
+ /** A bare specifier is anything that is not a relative or node-builtin import. */
49
+ function isForbiddenSpecifier(spec: string): boolean {
50
+ if (spec.startsWith(".") || spec.startsWith("/")) return false;
51
+ if (spec.startsWith("node:")) return false;
52
+ return true;
53
+ }
54
+
55
+ describe("package boundary", () => {
56
+ const sourceFiles = collectSourceFiles(SRC_DIR);
57
+
58
+ test("has source files to validate", () => {
59
+ expect(sourceFiles.length).toBeGreaterThan(0);
60
+ });
61
+
62
+ test("imports only node builtins and relative modules", () => {
63
+ const violations: string[] = [];
64
+
65
+ for (const file of sourceFiles) {
66
+ const lines = readFileSync(file, "utf-8").split("\n");
67
+ for (let i = 0; i < lines.length; i++) {
68
+ for (const match of lines[i]!.matchAll(IMPORT_SPEC)) {
69
+ const spec = match[1]!;
70
+ if (isForbiddenSpecifier(spec)) {
71
+ const relative = file.replace(PACKAGE_ROOT + "/", "");
72
+ violations.push(`${relative}:${i + 1}: ${spec}`);
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ if (violations.length > 0) {
79
+ throw new Error(
80
+ `Found ${violations.length} forbidden import(s) in @vellumai/environments:\n` +
81
+ violations.map((v) => ` - ${v}`).join("\n") +
82
+ "\n\n@vellumai/environments is a pure types/constants leaf and must\n" +
83
+ "import only node builtins and its own relative modules.",
84
+ );
85
+ }
86
+ });
87
+
88
+ test("package.json declares it as private with no runtime dependencies", () => {
89
+ const pkg = JSON.parse(
90
+ readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
91
+ );
92
+ expect(pkg.private).toBe(true);
93
+ expect(pkg.dependencies ?? {}).toEqual({});
94
+ });
95
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @vellumai/environments — the single source of truth for Vellum's known
3
+ * deployment environments (names, platform/web URLs, per-service port
4
+ * blocks). Consumed by the CLI, the assistant daemon, and the local-mode
5
+ * host library so the environment list is defined exactly once on the TS
6
+ * side. The Swift client mirrors the same set in `VellumEnvironment.swift`.
7
+ *
8
+ * Intentionally free of runtime dependencies: pure types and constants only.
9
+ */
10
+ export * from "./types.js";
11
+ export * from "./seeds.js";
@@ -25,15 +25,11 @@ function portBlock(base: number): PortMap {
25
25
  }
26
26
 
27
27
  /**
28
- * Built-in environment definitions. Mirrors Swift's
29
- * `clients/macos/vellum-assistant/App/VellumEnvironment.swift` enum and is
30
- * the TS-side source of truth for the set of known environment names.
31
- * One other TS site duplicates the name list:
32
- * - `assistant/src/util/platform.ts` (`KNOWN_ENVIRONMENTS`)
33
- * Drift between these two sites is caught at test time by
34
- * `cli/src/__tests__/env-drift.test.ts`. Fast follow: hoist the shared
35
- * list into a `packages/environments` package so both sites import
36
- * from one place.
28
+ * Built-in environment definitions and the TS-side source of truth for the
29
+ * set of known environment names. The Swift client mirrors this list in
30
+ * `clients/macos/vellum-assistant/App/VellumEnvironment.swift`; since Swift
31
+ * can't import TypeScript, drift between the two is caught at test time by
32
+ * `cli/src/__tests__/env-drift.test.ts`.
37
33
  *
38
34
  * Custom environments via a user config file are a future phase — see the
39
35
  * "Coexisting environments" design doc. Until then, a call site that needs a
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "types": ["bun-types"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@vellumai/local-mode",
7
+ "dependencies": {
8
+ "@vellumai/environments": "file:../environments",
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "1.3.11",
12
+ "typescript": "5.9.3",
13
+ },
14
+ },
15
+ },
16
+ "packages": {
17
+ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
18
+
19
+ "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
20
+
21
+ "@vellumai/environments": ["@vellumai/environments@file:../environments", { "devDependencies": { "@types/bun": "1.3.11", "typescript": "5.9.3" } }],
22
+
23
+ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
24
+
25
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
26
+
27
+ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
28
+ }
29
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@vellumai/local-mode",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "bunx tsc --noEmit",
12
+ "test": "bun test src/"
13
+ },
14
+ "dependencies": {
15
+ "@vellumai/environments": "file:../environments"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "1.3.11",
19
+ "typescript": "5.9.3"
20
+ }
21
+ }
@@ -0,0 +1,93 @@
1
+ import { afterEach, beforeAll, describe, expect, mock, test } from "bun:test";
2
+ import { EventEmitter } from "node:events";
3
+
4
+ import type { CliInvocation } from "../util";
5
+
6
+ class FakeChild extends EventEmitter {
7
+ stdout = new EventEmitter();
8
+ stderr = new EventEmitter();
9
+ kill = mock(() => true);
10
+ }
11
+
12
+ let lastChild: FakeChild;
13
+ const spawnArgs: Array<[string, string[]]> = [];
14
+ const spawnMock = mock((command: string, args: string[]) => {
15
+ spawnArgs.push([command, args]);
16
+ lastChild = new FakeChild();
17
+ return lastChild;
18
+ });
19
+
20
+ mock.module("node:child_process", () => ({ spawn: spawnMock }));
21
+
22
+ let runHatch: typeof import("../hatch").runHatch;
23
+
24
+ beforeAll(async () => {
25
+ ({ runHatch } = await import("../hatch"));
26
+ });
27
+
28
+ afterEach(() => {
29
+ spawnArgs.length = 0;
30
+ spawnMock.mockClear();
31
+ });
32
+
33
+ const invocation: CliInvocation = { command: "bun", baseArgs: ["run", "cli"] };
34
+
35
+ describe("runHatch", () => {
36
+ test("spawns the CLI and parses the assistant id from stdout", async () => {
37
+ const pending = runHatch(invocation, "vellum");
38
+ lastChild.stdout.emit(
39
+ "data",
40
+ Buffer.from("Hatching local assistant: asst-42\n"),
41
+ );
42
+ lastChild.emit("close", 0);
43
+
44
+ expect(await pending).toEqual({ ok: true, assistantId: "asst-42" });
45
+ expect(spawnArgs[0]).toEqual(["bun", ["run", "cli", "hatch", "vellum"]]);
46
+ });
47
+
48
+ test("a non-zero exit resolves to a failure carrying the CLI's output", async () => {
49
+ const pending = runHatch(invocation, "vellum");
50
+ lastChild.stderr.emit("data", Buffer.from("daemon already running"));
51
+ lastChild.emit("close", 1);
52
+
53
+ expect(await pending).toEqual({
54
+ ok: false,
55
+ status: 500,
56
+ error: "daemon already running",
57
+ });
58
+ });
59
+
60
+ test("a non-zero exit with no output carries a descriptive fallback error", async () => {
61
+ const pending = runHatch(invocation, "vellum");
62
+ lastChild.emit("close", 1);
63
+
64
+ const result = await pending;
65
+ expect(result.ok).toBe(false);
66
+ if (!result.ok) {
67
+ expect(result.error).toContain("exited with code 1");
68
+ }
69
+ });
70
+
71
+ test("a zero exit whose stdout has no parseable id fails instead of returning a blank id", async () => {
72
+ const pending = runHatch(invocation, "vellum");
73
+ lastChild.stdout.emit("data", Buffer.from("done, but no id line\n"));
74
+ lastChild.emit("close", 0);
75
+
76
+ const result = await pending;
77
+ expect(result.ok).toBe(false);
78
+ if (!result.ok) {
79
+ expect(result.error).toContain("no assistant id");
80
+ }
81
+ });
82
+
83
+ test("a spawn failure resolves to a failure rather than rejecting", async () => {
84
+ const pending = runHatch(invocation, "vellum");
85
+ lastChild.emit("error", new Error("ENOENT"));
86
+
87
+ const result = await pending;
88
+ expect(result.ok).toBe(false);
89
+ if (!result.ok) {
90
+ expect(result.error).toContain("ENOENT");
91
+ }
92
+ });
93
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Package boundary tests for @vellumai/local-mode.
3
+ *
4
+ * This package is the shared local-assistant host surface. It sits one layer
5
+ * above @vellumai/environments (its only allowed @vellumai dependency) and
6
+ * uses node builtins for filesystem, child-process, and network work.
7
+ *
8
+ * Enforces that the package:
9
+ * 1. Imports only node builtins, its own relative modules, and
10
+ * `@vellumai/environments` — no other `@vellumai/*` packages and no
11
+ * third-party runtime imports (so any bundler host can inline it).
12
+ * 2. Declares exactly one runtime dependency: `@vellumai/environments`.
13
+ * 3. Is marked `private`.
14
+ */
15
+
16
+ import { describe, expect, test } from "bun:test";
17
+ import { readFileSync, readdirSync, statSync } from "node:fs";
18
+ import { join, resolve } from "node:path";
19
+
20
+ const PACKAGE_ROOT = resolve(import.meta.dirname, "../..");
21
+ const SRC_DIR = join(PACKAGE_ROOT, "src");
22
+
23
+ const ALLOWED_PACKAGES = new Set(["@vellumai/environments"]);
24
+
25
+ function collectSourceFiles(dir: string): string[] {
26
+ const files: string[] = [];
27
+ for (const entry of readdirSync(dir)) {
28
+ const full = join(dir, entry);
29
+ const stat = statSync(full);
30
+ if (stat.isDirectory()) {
31
+ if (entry === "node_modules" || entry === "__tests__") continue;
32
+ files.push(...collectSourceFiles(full));
33
+ } else if (
34
+ entry.endsWith(".ts") &&
35
+ !entry.endsWith(".test.ts") &&
36
+ !entry.endsWith(".d.ts")
37
+ ) {
38
+ files.push(full);
39
+ }
40
+ }
41
+ return files;
42
+ }
43
+
44
+ /**
45
+ * Matches the module specifier of any `import ... from "<spec>"` /
46
+ * `export ... from "<spec>"` / `require("<spec>")` statement.
47
+ */
48
+ const IMPORT_SPEC = /(?:from|require\s*\(\s*)["']([^"']+)["']/g;
49
+
50
+ /**
51
+ * A specifier is forbidden when it is neither relative, a node builtin, nor
52
+ * one of the explicitly allowed `@vellumai/*` packages.
53
+ */
54
+ function isForbiddenSpecifier(spec: string): boolean {
55
+ if (spec.startsWith(".") || spec.startsWith("/")) return false;
56
+ if (spec.startsWith("node:")) return false;
57
+ if (ALLOWED_PACKAGES.has(spec)) return false;
58
+ return true;
59
+ }
60
+
61
+ describe("package boundary", () => {
62
+ const sourceFiles = collectSourceFiles(SRC_DIR);
63
+
64
+ test("has source files to validate", () => {
65
+ expect(sourceFiles.length).toBeGreaterThan(0);
66
+ });
67
+
68
+ test("imports only node builtins, relative modules, and @vellumai/environments", () => {
69
+ const violations: string[] = [];
70
+
71
+ for (const file of sourceFiles) {
72
+ const lines = readFileSync(file, "utf-8").split("\n");
73
+ for (let i = 0; i < lines.length; i++) {
74
+ for (const match of lines[i]!.matchAll(IMPORT_SPEC)) {
75
+ const spec = match[1]!;
76
+ if (isForbiddenSpecifier(spec)) {
77
+ const relative = file.replace(PACKAGE_ROOT + "/", "");
78
+ violations.push(`${relative}:${i + 1}: ${spec}`);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ if (violations.length > 0) {
85
+ throw new Error(
86
+ `Found ${violations.length} forbidden import(s) in @vellumai/local-mode:\n` +
87
+ violations.map((v) => ` - ${v}`).join("\n") +
88
+ "\n\n@vellumai/local-mode may import only node builtins, its own\n" +
89
+ "relative modules, and @vellumai/environments. Any other dependency\n" +
90
+ "would break bundler hosts that inline this source-only package.",
91
+ );
92
+ }
93
+ });
94
+
95
+ test("package.json declares it as private with only the @vellumai/environments dependency", () => {
96
+ const pkg = JSON.parse(
97
+ readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
98
+ );
99
+ expect(pkg.private).toBe(true);
100
+ expect(pkg.dependencies ?? {}).toEqual({
101
+ "@vellumai/environments": "file:../environments",
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,59 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ import { SEEDS } from "@vellumai/environments";
5
+
6
+ const PRODUCTION_ENVIRONMENT_NAME = "production";
7
+
8
+ export interface LocalEndpointConfig {
9
+ lockfilePaths: string[];
10
+ configDir: string;
11
+ webUrl: string;
12
+ platformUrl: string;
13
+ }
14
+
15
+ /**
16
+ * Resolve config from environment variables (Vite plugin context, where
17
+ * `env` comes from `loadEnv` and process.env).
18
+ */
19
+ export function resolveLocalConfigFromEnv(
20
+ env: Record<string, string>,
21
+ ): LocalEndpointConfig {
22
+ const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
23
+ const seed = SEEDS[vellumEnv] ?? SEEDS[PRODUCTION_ENVIRONMENT_NAME]!;
24
+
25
+ return {
26
+ lockfilePaths: resolveLockfilePaths(env),
27
+ configDir: resolveConfigDir(env),
28
+ webUrl: env.VELLUM_WEB_URL || seed.webUrl,
29
+ platformUrl: env.VELLUM_PLATFORM_URL || seed.platformUrl,
30
+ };
31
+ }
32
+
33
+ export function resolveLockfilePaths(env: Record<string, string>): string[] {
34
+ const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
35
+ const lockfileDir = env.VELLUM_LOCKFILE_DIR;
36
+
37
+ if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
38
+ const dir = lockfileDir ?? os.homedir();
39
+ return [
40
+ path.join(dir, ".vellum.lock.json"),
41
+ path.join(dir, ".vellum.lockfile.json"),
42
+ ];
43
+ }
44
+
45
+ const xdgConfigHome =
46
+ env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
47
+ const dir = lockfileDir ?? path.join(xdgConfigHome, `vellum-${vellumEnv}`);
48
+ return [path.join(dir, "lockfile.json")];
49
+ }
50
+
51
+ export function resolveConfigDir(env: Record<string, string>): string {
52
+ const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
53
+ const xdgConfigHome =
54
+ env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
55
+ if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
56
+ return path.join(xdgConfigHome, "vellum");
57
+ }
58
+ return path.join(xdgConfigHome, `vellum-${vellumEnv}`);
59
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+
3
+ const GATEWAY_PATTERN = /^(?:\/assistant)?\/__gateway\/(\d+)(\/.*)?$/;
4
+
5
+ export interface GatewayTarget {
6
+ port: number;
7
+ path: string;
8
+ }
9
+
10
+ export type GatewayParseResult =
11
+ | { match: true; valid: true; target: GatewayTarget }
12
+ | { match: true; valid: false }
13
+ | { match: false };
14
+
15
+ export function parseGatewayUrl(pathname: string): GatewayParseResult {
16
+ const match = pathname.match(GATEWAY_PATTERN);
17
+ if (!match) return { match: false };
18
+
19
+ const port = parseInt(match[1]!, 10);
20
+ if (port < 1024 || port > 65535) return { match: true, valid: false };
21
+
22
+ return { match: true, valid: true, target: { port, path: match[2] || "/" } };
23
+ }
24
+
25
+ function addPortFromUrl(url: unknown, ports: Set<number>): void {
26
+ if (typeof url !== "string") return;
27
+ try {
28
+ const parsed = new URL(url);
29
+ if (parsed.hostname !== "127.0.0.1" && parsed.hostname !== "localhost") return;
30
+ const port = Number(parsed.port);
31
+ if (Number.isInteger(port) && port >= 1024 && port <= 65535) {
32
+ ports.add(port);
33
+ }
34
+ } catch {
35
+ // malformed URL — skip
36
+ }
37
+ }
38
+
39
+ export function readAllowedGatewayPorts(lockfilePaths: string[]): Set<number> {
40
+ const ports = new Set<number>();
41
+ for (const candidate of lockfilePaths) {
42
+ try {
43
+ const raw = fs.readFileSync(candidate, "utf-8");
44
+ const data = JSON.parse(raw) as {
45
+ assistants?: Array<{
46
+ gatewayUrl?: unknown;
47
+ localUrl?: unknown;
48
+ resources?: { gatewayPort?: unknown };
49
+ }>;
50
+ };
51
+ const assistants = Array.isArray(data.assistants) ? data.assistants : [];
52
+ for (const assistant of assistants) {
53
+ if (!assistant) continue;
54
+ addPortFromUrl(assistant.gatewayUrl, ports);
55
+ addPortFromUrl(assistant.localUrl, ports);
56
+ const gp = assistant.resources?.gatewayPort;
57
+ if (typeof gp === "number" && Number.isInteger(gp) && gp >= 1024 && gp <= 65535) {
58
+ ports.add(gp);
59
+ }
60
+ }
61
+ if (ports.size > 0) return ports;
62
+ } catch (err: unknown) {
63
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") return new Set<number>();
64
+ }
65
+ }
66
+ return ports;
67
+ }