@vellumai/cli 0.8.7 → 0.8.8-dev.202606052332.17fc8ea

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 (49) hide show
  1. package/node_modules/@vellumai/local-mode/package.json +2 -1
  2. package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
  3. package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
  4. package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +15 -0
  5. package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
  6. package/node_modules/@vellumai/local-mode/src/config.ts +15 -8
  7. package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
  8. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +42 -0
  9. package/node_modules/@vellumai/local-mode/src/hatch.ts +22 -4
  10. package/node_modules/@vellumai/local-mode/src/index.ts +26 -4
  11. package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
  12. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
  13. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
  14. package/node_modules/@vellumai/local-mode/src/lockfile.ts +9 -7
  15. package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
  16. package/package.json +1 -1
  17. package/src/__tests__/assistant-client-refresh.test.ts +182 -0
  18. package/src/__tests__/clean.test.ts +179 -0
  19. package/src/__tests__/client-token.test.ts +87 -0
  20. package/src/__tests__/client-tui-refresh.test.ts +170 -0
  21. package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
  22. package/src/__tests__/connect-import.test.ts +317 -0
  23. package/src/__tests__/devices.test.ts +272 -0
  24. package/src/__tests__/guardian-token.test.ts +126 -2
  25. package/src/__tests__/pair.test.ts +271 -0
  26. package/src/__tests__/paired-lifecycle.test.ts +116 -0
  27. package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
  28. package/src/__tests__/unpair.test.ts +163 -0
  29. package/src/commands/client.ts +115 -26
  30. package/src/commands/connect/import.ts +217 -0
  31. package/src/commands/connect.ts +31 -0
  32. package/src/commands/devices.ts +247 -0
  33. package/src/commands/pair.ts +222 -0
  34. package/src/commands/ps.ts +16 -0
  35. package/src/commands/retire.ts +20 -47
  36. package/src/commands/sleep.ts +7 -0
  37. package/src/commands/tunnel.ts +46 -2
  38. package/src/commands/unpair.ts +118 -0
  39. package/src/commands/wake.ts +7 -0
  40. package/src/components/DefaultMainScreen.tsx +84 -13
  41. package/src/index.ts +16 -0
  42. package/src/lib/assistant-client.ts +58 -37
  43. package/src/lib/assistant-config.ts +12 -0
  44. package/src/lib/cloudflare-tunnel.ts +276 -0
  45. package/src/lib/confirm-action.ts +57 -0
  46. package/src/lib/docker.ts +25 -1
  47. package/src/lib/environments/resolve.ts +9 -30
  48. package/src/lib/guardian-token.ts +120 -4
  49. package/src/lib/local.ts +20 -6
@@ -5,7 +5,8 @@
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
- ".": "./src/index.ts"
8
+ ".": "./src/index.ts",
9
+ "./contract": "./src/lockfile-contract.ts"
9
10
  },
10
11
  "scripts": {
11
12
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,116 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ defaultEnvironmentFilePath,
8
+ readDefaultEnvironment,
9
+ resolveEnvironmentName,
10
+ } from "../environment";
11
+ import { resolveLockfilePaths, resolveConfigDir } from "../config";
12
+
13
+ let configHome: string;
14
+
15
+ /** Write the persisted default-environment file under the temp config home. */
16
+ function persistDefault(name: string): void {
17
+ const file = path.join(configHome, "vellum", "environment");
18
+ mkdirSync(path.dirname(file), { recursive: true });
19
+ writeFileSync(file, name + "\n", "utf-8");
20
+ }
21
+
22
+ beforeEach(() => {
23
+ configHome = mkdtempSync(path.join(os.tmpdir(), "vellum-env-"));
24
+ });
25
+
26
+ afterEach(() => {
27
+ rmSync(configHome, { recursive: true, force: true });
28
+ });
29
+
30
+ describe("defaultEnvironmentFilePath", () => {
31
+ test("honors XDG_CONFIG_HOME", () => {
32
+ expect(defaultEnvironmentFilePath({ XDG_CONFIG_HOME: configHome })).toBe(
33
+ path.join(configHome, "vellum", "environment"),
34
+ );
35
+ });
36
+
37
+ test("falls back to ~/.config", () => {
38
+ expect(defaultEnvironmentFilePath({})).toBe(
39
+ path.join(os.homedir(), ".config", "vellum", "environment"),
40
+ );
41
+ });
42
+ });
43
+
44
+ describe("readDefaultEnvironment", () => {
45
+ test("returns undefined when no file exists", () => {
46
+ expect(
47
+ readDefaultEnvironment({ XDG_CONFIG_HOME: configHome }),
48
+ ).toBeUndefined();
49
+ });
50
+
51
+ test("returns the trimmed persisted name", () => {
52
+ persistDefault("dev");
53
+ expect(readDefaultEnvironment({ XDG_CONFIG_HOME: configHome })).toBe("dev");
54
+ });
55
+
56
+ test("treats an empty file as no default", () => {
57
+ const file = path.join(configHome, "vellum", "environment");
58
+ mkdirSync(path.dirname(file), { recursive: true });
59
+ writeFileSync(file, " \n", "utf-8");
60
+ expect(
61
+ readDefaultEnvironment({ XDG_CONFIG_HOME: configHome }),
62
+ ).toBeUndefined();
63
+ });
64
+ });
65
+
66
+ describe("resolveEnvironmentName", () => {
67
+ test("prefers VELLUM_ENVIRONMENT over the persisted default", () => {
68
+ persistDefault("dev");
69
+ expect(
70
+ resolveEnvironmentName({
71
+ XDG_CONFIG_HOME: configHome,
72
+ VELLUM_ENVIRONMENT: "staging",
73
+ }),
74
+ ).toBe("staging");
75
+ });
76
+
77
+ test("falls back to the persisted default when the env var is unset", () => {
78
+ persistDefault("dev");
79
+ expect(resolveEnvironmentName({ XDG_CONFIG_HOME: configHome })).toBe("dev");
80
+ });
81
+
82
+ test("falls back to production when neither is set", () => {
83
+ expect(resolveEnvironmentName({ XDG_CONFIG_HOME: configHome })).toBe(
84
+ "production",
85
+ );
86
+ });
87
+ });
88
+
89
+ describe("path resolvers honor the persisted default", () => {
90
+ test("resolveLockfilePaths points at the persisted environment", () => {
91
+ persistDefault("dev");
92
+ const env = { XDG_CONFIG_HOME: configHome };
93
+ expect(resolveLockfilePaths(env)).toEqual([
94
+ path.join(configHome, "vellum-dev", "lockfile.json"),
95
+ ]);
96
+ });
97
+
98
+ test("resolveConfigDir points at the persisted environment", () => {
99
+ persistDefault("dev");
100
+ expect(resolveConfigDir({ XDG_CONFIG_HOME: configHome })).toBe(
101
+ path.join(configHome, "vellum-dev"),
102
+ );
103
+ });
104
+
105
+ test("VELLUM_ENVIRONMENT still wins for path resolution", () => {
106
+ persistDefault("dev");
107
+ const env = {
108
+ XDG_CONFIG_HOME: configHome,
109
+ VELLUM_ENVIRONMENT: "production",
110
+ };
111
+ expect(resolveLockfilePaths(env)).toEqual([
112
+ path.join(os.homedir(), ".vellum.lock.json"),
113
+ path.join(os.homedir(), ".vellum.lockfile.json"),
114
+ ]);
115
+ });
116
+ });
@@ -0,0 +1,79 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { resolveGatewayProxyTarget } from "../gateway-proxy";
4
+
5
+ const allow =
6
+ (...ports: number[]) =>
7
+ () =>
8
+ new Set<number>(ports);
9
+
10
+ describe("resolveGatewayProxyTarget", () => {
11
+ test("passes non-gateway pathnames through untouched", () => {
12
+ expect(resolveGatewayProxyTarget("/index.html", allow(8080))).toEqual({
13
+ kind: "pass",
14
+ });
15
+ expect(resolveGatewayProxyTarget("/assistant/assets/app.js", allow())).toEqual({
16
+ kind: "pass",
17
+ });
18
+ });
19
+
20
+ test("forwards an allowlisted port to its loopback target", () => {
21
+ expect(
22
+ resolveGatewayProxyTarget("/__gateway/8080/v1/assistants", allow(8080)),
23
+ ).toEqual({
24
+ kind: "forward",
25
+ target: { port: 8080, path: "/v1/assistants" },
26
+ });
27
+ });
28
+
29
+ test("accepts the renderer's `/assistant` mount prefix", () => {
30
+ expect(
31
+ resolveGatewayProxyTarget("/assistant/__gateway/8080/auth/token", allow(8080)),
32
+ ).toEqual({
33
+ kind: "forward",
34
+ target: { port: 8080, path: "/auth/token" },
35
+ });
36
+ });
37
+
38
+ test("defaults a portless tail to the gateway root", () => {
39
+ expect(resolveGatewayProxyTarget("/__gateway/8080", allow(8080))).toEqual({
40
+ kind: "forward",
41
+ target: { port: 8080, path: "/" },
42
+ });
43
+ });
44
+
45
+ test("rejects ports outside the 1024–65535 range as invalid", () => {
46
+ expect(resolveGatewayProxyTarget("/__gateway/80/v1", allow(80))).toEqual({
47
+ kind: "invalid-port",
48
+ });
49
+ expect(resolveGatewayProxyTarget("/__gateway/70000/v1", allow(70000))).toEqual({
50
+ kind: "invalid-port",
51
+ });
52
+ });
53
+
54
+ test("forbids a well-formed port that isn't registered in the lockfile", () => {
55
+ expect(
56
+ resolveGatewayProxyTarget("/__gateway/9999/v1", allow(8080)),
57
+ ).toEqual({ kind: "forbidden-port", port: 9999 });
58
+ });
59
+
60
+ test("forbids every gateway port when the allowlist is empty", () => {
61
+ expect(resolveGatewayProxyTarget("/__gateway/8080/v1", allow())).toEqual({
62
+ kind: "forbidden-port",
63
+ port: 8080,
64
+ });
65
+ });
66
+
67
+ test("never reads the allowlist for non-gateway or invalid-port paths", () => {
68
+ let reads = 0;
69
+ const counting = () => {
70
+ reads += 1;
71
+ return new Set<number>([8080]);
72
+ };
73
+ resolveGatewayProxyTarget("/index.html", counting);
74
+ resolveGatewayProxyTarget("/__gateway/80/v1", counting);
75
+ expect(reads).toBe(0);
76
+ resolveGatewayProxyTarget("/__gateway/8080/v1", counting);
77
+ expect(reads).toBe(1);
78
+ });
79
+ });
@@ -45,6 +45,21 @@ describe("runHatch", () => {
45
45
  expect(spawnArgs[0]).toEqual(["bun", ["run", "cli", "hatch", "vellum"]]);
46
46
  });
47
47
 
48
+ test("parses the assistant id from a Docker hatch banner", async () => {
49
+ const pending = runHatch(invocation, "vellum", { remote: "docker" });
50
+ lastChild.stdout.emit(
51
+ "data",
52
+ Buffer.from("🥚 Hatching Docker assistant: asst-docker\n"),
53
+ );
54
+ lastChild.emit("close", 0);
55
+
56
+ expect(await pending).toEqual({ ok: true, assistantId: "asst-docker" });
57
+ expect(spawnArgs[0]).toEqual([
58
+ "bun",
59
+ ["run", "cli", "hatch", "vellum", "--remote", "docker"],
60
+ ]);
61
+ });
62
+
48
63
  test("a non-zero exit resolves to a failure carrying the CLI's output", async () => {
49
64
  const pending = runHatch(invocation, "vellum");
50
65
  lastChild.stderr.emit("data", Buffer.from("daemon already running"));
@@ -0,0 +1,66 @@
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 runWake: typeof import("../wake").runWake;
23
+
24
+ beforeAll(async () => {
25
+ ({ runWake } = await import("../wake"));
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("runWake", () => {
36
+ test("spawns the CLI wake command for the assistant and resolves ok on exit 0", async () => {
37
+ const pending = runWake(invocation, "asst-42");
38
+ lastChild.emit("close", 0);
39
+
40
+ expect(await pending).toEqual({ ok: true });
41
+ expect(spawnArgs[0]).toEqual(["bun", ["run", "cli", "wake", "asst-42"]]);
42
+ });
43
+
44
+ test("a non-zero exit resolves to a failure carrying the CLI's output", async () => {
45
+ const pending = runWake(invocation, "asst-42");
46
+ lastChild.stderr.emit("data", Buffer.from("no sibling environment to seed from"));
47
+ lastChild.emit("close", 1);
48
+
49
+ expect(await pending).toEqual({
50
+ ok: false,
51
+ status: 500,
52
+ error: "no sibling environment to seed from",
53
+ });
54
+ });
55
+
56
+ test("a spawn failure resolves to a failure rather than rejecting", async () => {
57
+ const pending = runWake(invocation, "asst-42");
58
+ lastChild.emit("error", new Error("ENOENT"));
59
+
60
+ const result = await pending;
61
+ expect(result.ok).toBe(false);
62
+ if (!result.ok) {
63
+ expect(result.error).toContain("ENOENT");
64
+ }
65
+ });
66
+ });
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
 
4
4
  import { SEEDS } from "@vellumai/environments";
5
5
 
6
+ import { resolveEnvironmentName } from "./environment";
7
+
6
8
  const PRODUCTION_ENVIRONMENT_NAME = "production";
7
9
 
8
10
  export interface LocalEndpointConfig {
@@ -13,13 +15,14 @@ export interface LocalEndpointConfig {
13
15
  }
14
16
 
15
17
  /**
16
- * Resolve config from environment variables (Vite plugin context, where
17
- * `env` comes from `loadEnv` and process.env).
18
+ * Resolve config from environment variables. Accepts any environment-shaped
19
+ * map, including `process.env` (whose values are `string | undefined`) and the
20
+ * Vite plugin's `loadEnv` result.
18
21
  */
19
22
  export function resolveLocalConfigFromEnv(
20
- env: Record<string, string>,
23
+ env: Record<string, string | undefined>,
21
24
  ): LocalEndpointConfig {
22
- const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
25
+ const vellumEnv = resolveEnvironmentName(env);
23
26
  const seed = SEEDS[vellumEnv] ?? SEEDS[PRODUCTION_ENVIRONMENT_NAME]!;
24
27
 
25
28
  return {
@@ -30,8 +33,10 @@ export function resolveLocalConfigFromEnv(
30
33
  };
31
34
  }
32
35
 
33
- export function resolveLockfilePaths(env: Record<string, string>): string[] {
34
- const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
36
+ export function resolveLockfilePaths(
37
+ env: Record<string, string | undefined>,
38
+ ): string[] {
39
+ const vellumEnv = resolveEnvironmentName(env);
35
40
  const lockfileDir = env.VELLUM_LOCKFILE_DIR;
36
41
 
37
42
  if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
@@ -48,8 +53,10 @@ export function resolveLockfilePaths(env: Record<string, string>): string[] {
48
53
  return [path.join(dir, "lockfile.json")];
49
54
  }
50
55
 
51
- export function resolveConfigDir(env: Record<string, string>): string {
52
- const vellumEnv = env.VELLUM_ENVIRONMENT || PRODUCTION_ENVIRONMENT_NAME;
56
+ export function resolveConfigDir(
57
+ env: Record<string, string | undefined>,
58
+ ): string {
59
+ const vellumEnv = resolveEnvironmentName(env);
53
60
  const xdgConfigHome =
54
61
  env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
55
62
  if (vellumEnv === PRODUCTION_ENVIRONMENT_NAME) {
@@ -0,0 +1,62 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const PRODUCTION_ENVIRONMENT_NAME = "production";
6
+
7
+ /**
8
+ * Location of the persisted default-environment file, written by
9
+ * `vellum env set`. It lives at a fixed, environment-agnostic path so it can
10
+ * be read before the environment is known. Honors `XDG_CONFIG_HOME`, falling
11
+ * back to `~/.config`.
12
+ */
13
+ export function defaultEnvironmentFilePath(
14
+ env: Record<string, string | undefined>,
15
+ ): string {
16
+ const xdgConfigHome =
17
+ env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
18
+ return path.join(xdgConfigHome, "vellum", "environment");
19
+ }
20
+
21
+ /**
22
+ * Read the persisted default environment name, or `undefined` when no default
23
+ * has been set (no file, empty file, or unreadable).
24
+ */
25
+ export function readDefaultEnvironment(
26
+ env: Record<string, string | undefined>,
27
+ ): string | undefined {
28
+ const filePath = defaultEnvironmentFilePath(env);
29
+ try {
30
+ if (!existsSync(filePath)) return undefined;
31
+ const content = readFileSync(filePath, "utf-8").trim();
32
+ return content.length > 0 ? content : undefined;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Resolve the active environment name from a single source of truth so every
40
+ * local-mode host — the CLI `client` server, the web app's Vite dev
41
+ * middleware, and the Electron main process — reads and writes the same
42
+ * lockfile and config directory.
43
+ *
44
+ * Resolution order matches the CLI's `resolveEnvironmentSource`:
45
+ * 1. `VELLUM_ENVIRONMENT` (explicit override, also inherited by the
46
+ * hatch/retire child processes the CLI spawns)
47
+ * 2. the persisted default written by `vellum env set`
48
+ * 3. `production`
49
+ *
50
+ * Resolving the persisted default here — rather than only inspecting
51
+ * `VELLUM_ENVIRONMENT` — is what keeps a host that did not go through the CLI's
52
+ * resolver (the Electron main) pointed at the same environment the CLI uses.
53
+ */
54
+ export function resolveEnvironmentName(
55
+ env: Record<string, string | undefined>,
56
+ ): string {
57
+ return (
58
+ env.VELLUM_ENVIRONMENT?.trim() ||
59
+ readDefaultEnvironment(env) ||
60
+ PRODUCTION_ENVIRONMENT_NAME
61
+ );
62
+ }
@@ -22,6 +22,48 @@ export function parseGatewayUrl(pathname: string): GatewayParseResult {
22
22
  return { match: true, valid: true, target: { port, path: match[2] || "/" } };
23
23
  }
24
24
 
25
+ /**
26
+ * Verdict for a gateway-proxy URL, combining the URL parse with the
27
+ * lockfile port-allowlist check into one decision a host can act on
28
+ * without re-deriving the rules.
29
+ *
30
+ * - `pass` — not a gateway URL; the host serves it normally.
31
+ * - `invalid-port` — a gateway URL whose port is outside 1024–65535.
32
+ * - `forbidden-port`— a well-formed gateway URL for a port that isn't
33
+ * registered in the lockfile (the security
34
+ * boundary: the proxy only reaches gateway ports
35
+ * the user actually hatched, never arbitrary
36
+ * loopback services).
37
+ * - `forward` — forward to `127.0.0.1:{port}{path}`.
38
+ */
39
+ export type GatewayProxyDecision =
40
+ | { kind: "pass" }
41
+ | { kind: "invalid-port" }
42
+ | { kind: "forbidden-port"; port: number }
43
+ | { kind: "forward"; target: GatewayTarget };
44
+
45
+ /**
46
+ * Resolve a request pathname to a gateway-proxy verdict. Identical across every
47
+ * host that proxies the data plane (the Vite dev middleware and the Electron
48
+ * `app://` protocol handler).
49
+ *
50
+ * `getAllowedPorts` is a thunk (typically `() => readAllowedGatewayPorts(...)`)
51
+ * so the lockfile is read only once a gateway URL is matched — the hot path of
52
+ * static-asset and non-gateway requests never touches disk.
53
+ */
54
+ export function resolveGatewayProxyTarget(
55
+ pathname: string,
56
+ getAllowedPorts: () => Set<number>,
57
+ ): GatewayProxyDecision {
58
+ const parsed = parseGatewayUrl(pathname);
59
+ if (!parsed.match) return { kind: "pass" };
60
+ if (!parsed.valid) return { kind: "invalid-port" };
61
+ if (!getAllowedPorts().has(parsed.target.port)) {
62
+ return { kind: "forbidden-port", port: parsed.target.port };
63
+ }
64
+ return { kind: "forward", target: parsed.target };
65
+ }
66
+
25
67
  function addPortFromUrl(url: unknown, ports: Set<number>): void {
26
68
  if (typeof url !== "string") return;
27
69
  try {
@@ -3,19 +3,31 @@ import { spawn } from "node:child_process";
3
3
  import type { CliInvocation } from "./util";
4
4
 
5
5
  const HATCH_TIMEOUT_MS = 120_000;
6
+ // Docker hatches pull images and wait up to 5 min for container readiness.
7
+ const DOCKER_HATCH_TIMEOUT_MS = 10 * 60 * 1000;
6
8
 
7
9
  export type HatchResult =
8
10
  | { ok: true; assistantId: string }
9
11
  | { ok: false; status: number; error: string };
10
12
 
13
+ export interface RunHatchOptions {
14
+ remote?: string;
15
+ }
16
+
11
17
  export function runHatch(
12
18
  invocation: CliInvocation,
13
19
  species: string,
20
+ options?: RunHatchOptions,
14
21
  ): Promise<HatchResult> {
15
22
  return new Promise((resolve) => {
23
+ const args = [...invocation.baseArgs, "hatch", species];
24
+ if (options?.remote) {
25
+ args.push("--remote", options.remote);
26
+ }
27
+
16
28
  const child = spawn(
17
29
  invocation.command,
18
- [...invocation.baseArgs, "hatch", species],
30
+ args,
19
31
  { stdio: ["ignore", "pipe", "pipe"] },
20
32
  );
21
33
 
@@ -30,10 +42,16 @@ export function runHatch(
30
42
  resolve(result);
31
43
  };
32
44
 
45
+ const timeoutMs =
46
+ options?.remote === "docker" ? DOCKER_HATCH_TIMEOUT_MS : HATCH_TIMEOUT_MS;
33
47
  const timeout = setTimeout(() => {
34
48
  child.kill("SIGTERM");
35
- finish({ ok: false, status: 500, error: "Hatch timed out after 120 seconds" });
36
- }, HATCH_TIMEOUT_MS);
49
+ finish({
50
+ ok: false,
51
+ status: 500,
52
+ error: `Hatch timed out after ${timeoutMs / 1000} seconds`,
53
+ });
54
+ }, timeoutMs);
37
55
 
38
56
  child.stdout.on("data", (data: Buffer) => {
39
57
  stdout += data.toString();
@@ -53,7 +71,7 @@ export function runHatch(
53
71
  return;
54
72
  }
55
73
  const assistantId = stdout
56
- .match(/Hatching local assistant:\s+(.+)/)?.[1]
74
+ .match(/Hatching (?:local|Docker) assistant:\s+(.+)/)?.[1]
57
75
  ?.trim();
58
76
  if (!assistantId) {
59
77
  finish({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @vellumai/local-mode — shared host library for serving the local-assistant
3
3
  * surface (lockfile reads, guardian-token issuance, gateway proxying, and the
4
- * hatch/retire lifecycle ops) over a loopback HTTP boundary. Consumed by the
4
+ * hatch/retire/wake lifecycle ops) over a loopback HTTP boundary. Consumed by the
5
5
  * CLI `client` server and the web app's dev-server middleware so the local
6
6
  * endpoint behaviour is defined exactly once instead of one host reaching into
7
7
  * another's source tree. Depends only on `@vellumai/environments`.
@@ -14,13 +14,35 @@ export {
14
14
  export type { CliInvocation } from "./util";
15
15
  export { resolveLocalConfigFromEnv, resolveLockfilePaths, resolveConfigDir } from "./config";
16
16
  export type { LocalEndpointConfig } from "./config";
17
- export { getLockfileData, upsertLockfileAssistant, replacePlatformAssistants } from "./lockfile";
17
+ export { defaultEnvironmentFilePath, readDefaultEnvironment, resolveEnvironmentName } from "./environment";
18
+ export {
19
+ getLockfileData,
20
+ upsertLockfileAssistant,
21
+ replacePlatformAssistants,
22
+ } from "./lockfile";
18
23
  export type { LockfileResult, WriteResult } from "./lockfile";
24
+ export { parseLockfile } from "./lockfile-contract";
25
+ export type {
26
+ Lockfile,
27
+ LockfileAssistant,
28
+ LocalAssistantResources,
29
+ LockfileWriteResult,
30
+ } from "./lockfile-contract";
19
31
  export { runHatch } from "./hatch";
20
32
  export type { HatchResult } from "./hatch";
21
33
  export { runRetire } from "./retire";
22
34
  export type { RetireResult } from "./retire";
35
+ export { runWake } from "./wake";
36
+ export type { WakeResult } from "./wake";
23
37
  export { getGuardianAccessToken } from "./guardian-token";
24
38
  export type { TokenResult } from "./guardian-token";
25
- export { parseGatewayUrl, readAllowedGatewayPorts } from "./gateway-proxy";
26
- export type { GatewayTarget, GatewayParseResult } from "./gateway-proxy";
39
+ export {
40
+ parseGatewayUrl,
41
+ readAllowedGatewayPorts,
42
+ resolveGatewayProxyTarget,
43
+ } from "./gateway-proxy";
44
+ export type {
45
+ GatewayTarget,
46
+ GatewayParseResult,
47
+ GatewayProxyDecision,
48
+ } from "./gateway-proxy";