@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.
- package/AGENTS.md +6 -0
- package/bun.lock +8 -0
- package/knip.json +6 -1
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/bun.lock +29 -0
- package/node_modules/@vellumai/local-mode/package.json +21 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +93 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +59 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +67 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +74 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +26 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +131 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/backup.test.ts +38 -0
- package/src/__tests__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/recover.test.ts +307 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/wake.test.ts +215 -0
- package/src/commands/backup.ts +2 -0
- package/src/commands/client.ts +471 -30
- package/src/commands/env.ts +1 -1
- package/src/commands/flags.ts +269 -0
- package/src/commands/gateway/token.ts +73 -0
- package/src/commands/gateway.ts +29 -0
- package/src/commands/logs.ts +6 -18
- package/src/commands/ps.ts +41 -41
- package/src/commands/recover.ts +47 -9
- package/src/commands/restore.ts +8 -1
- package/src/commands/retire.ts +3 -23
- package/src/commands/rollback.ts +2 -14
- package/src/commands/ssh.ts +5 -24
- package/src/commands/teleport.ts +34 -26
- package/src/commands/upgrade.ts +8 -16
- package/src/commands/wake.ts +68 -45
- package/src/components/DefaultMainScreen.tsx +16 -1
- package/src/index.ts +6 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/__tests__/step-runner.test.ts +49 -1
- package/src/lib/assistant-config.ts +16 -3
- package/src/lib/config-utils.ts +24 -3
- package/src/lib/docker.ts +57 -7
- package/src/lib/environments/__tests__/paths.test.ts +2 -1
- package/src/lib/environments/__tests__/seeds.test.ts +2 -1
- package/src/lib/environments/paths.ts +1 -1
- package/src/lib/environments/resolve.ts +2 -5
- package/src/lib/guardian-token.ts +12 -5
- package/src/lib/hatch-local.ts +75 -33
- package/src/lib/http-client.ts +1 -3
- package/src/lib/lifecycle-reporter.ts +31 -0
- package/src/lib/local.ts +173 -292
- package/src/lib/orphan-detection.ts +9 -5
- package/src/lib/pgrep.ts +5 -1
- package/src/lib/platform-client.ts +97 -49
- package/src/lib/process.ts +109 -39
- package/src/lib/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- package/src/lib/step-runner.ts +67 -7
- package/src/lib/sync-cloud-assistants.ts +17 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { dirname, join } from "path";
|
|
12
12
|
|
|
13
|
+
import { SEEDS, type EnvironmentDefinition } from "@vellumai/environments";
|
|
14
|
+
|
|
13
15
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./constants.js";
|
|
14
16
|
import {
|
|
15
17
|
getDefaultPorts,
|
|
@@ -18,8 +20,6 @@ import {
|
|
|
18
20
|
getMultiInstanceDir,
|
|
19
21
|
} from "./environments/paths.js";
|
|
20
22
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
21
|
-
import { SEEDS } from "./environments/seeds.js";
|
|
22
|
-
import type { EnvironmentDefinition } from "./environments/types.js";
|
|
23
23
|
import { probePort } from "./port-probe.js";
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -559,6 +559,19 @@ export function resolveCloud(entry: AssistantEntry): string {
|
|
|
559
559
|
return "local";
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Extract the hostname from a URL string. Falls back to stripping the scheme
|
|
564
|
+
* and taking the hostname portion if URL parsing fails.
|
|
565
|
+
*/
|
|
566
|
+
export function extractHostFromUrl(url: string): string {
|
|
567
|
+
try {
|
|
568
|
+
const parsed = new URL(url);
|
|
569
|
+
return parsed.hostname;
|
|
570
|
+
} catch {
|
|
571
|
+
return url.replace(/^https?:\/\//, "").split(":")[0];
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
562
575
|
export function saveAssistantEntry(entry: AssistantEntry): void {
|
|
563
576
|
const entries = readAssistants().filter(
|
|
564
577
|
(e) => e.assistantId !== entry.assistantId,
|
|
@@ -618,7 +631,7 @@ export async function allocateLocalResources(
|
|
|
618
631
|
|
|
619
632
|
// Env-aware bases: non-prod envs sit in their own 1000-port window so
|
|
620
633
|
// running prod and staging assistants side-by-side doesn't collide. See
|
|
621
|
-
// `
|
|
634
|
+
// the `@vellumai/environments` `portBlock` layout.
|
|
622
635
|
const basePorts = getDefaultPorts(env);
|
|
623
636
|
const daemonPort = await findAvailablePort(basePorts.daemon, reservedPorts);
|
|
624
637
|
const gatewayPort = await findAvailablePort(basePorts.gateway, [
|
package/src/lib/config-utils.ts
CHANGED
|
@@ -35,6 +35,21 @@ export function buildNestedConfig(
|
|
|
35
35
|
/**
|
|
36
36
|
* Ensure hatch always provides enough initial LLM config for the assistant to
|
|
37
37
|
* detect a fresh off-platform hatch and seed BYOK profiles.
|
|
38
|
+
*
|
|
39
|
+
* @deprecated Part of the workspace-config overlay path — a CLI→Assistant
|
|
40
|
+
* side channel that bypasses the Assistant's public APIs and has no
|
|
41
|
+
* equivalent in web/desktop. Two replacement paths are on the table:
|
|
42
|
+
*
|
|
43
|
+
* 1. Post-hatch API calls — the CLI calls public Assistant routes after
|
|
44
|
+
* boot (`POST /v1/secrets`, plus a small read-only endpoint that
|
|
45
|
+
* returns the canonical inference-profile templates so the CLI can
|
|
46
|
+
* PATCH them in). See the closed alternatives in PR #32061 and
|
|
47
|
+
* PR #32131 for the shape this would take.
|
|
48
|
+
* 2. Move inference-profile seeds out of workspace config and into
|
|
49
|
+
* Assistant code, so there is nothing for the CLI to inject in the
|
|
50
|
+
* first place.
|
|
51
|
+
*
|
|
52
|
+
* Either path removes the need for this helper.
|
|
38
53
|
*/
|
|
39
54
|
export function buildHatchConfigValues(
|
|
40
55
|
configValues: Record<string, string>,
|
|
@@ -52,15 +67,21 @@ export function buildHatchConfigValues(
|
|
|
52
67
|
|
|
53
68
|
/**
|
|
54
69
|
* Write arbitrary key-value pairs to a temporary JSON file and return its
|
|
55
|
-
* path. The caller
|
|
56
|
-
*
|
|
57
|
-
*
|
|
70
|
+
* path. The caller is responsible for getting the file to the daemon — for
|
|
71
|
+
* the local hatch flow that means setting `VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH`
|
|
72
|
+
* on the daemon process; for the Docker hatch flow the caller stages the
|
|
73
|
+
* file into the workspace volume so the rename-after-consume step in
|
|
74
|
+
* `mergeDefaultWorkspaceConfig` is a same-filesystem rename.
|
|
58
75
|
*
|
|
59
76
|
* Keys use dot-notation to address nested fields. For example:
|
|
60
77
|
* "llm.default.provider" → {llm: {default: {provider: ...}}}
|
|
61
78
|
* "llm.default.model" → {llm: {default: {model: ...}}}
|
|
62
79
|
*
|
|
63
80
|
* Returns undefined when configValues is empty (nothing to write).
|
|
81
|
+
*
|
|
82
|
+
* @deprecated See {@link buildHatchConfigValues} for the replacement
|
|
83
|
+
* direction. This overlay path is a CLI→Assistant side channel and will be
|
|
84
|
+
* removed once one of the documented replacements lands.
|
|
64
85
|
*/
|
|
65
86
|
export function writeInitialConfig(
|
|
66
87
|
configValues: Record<string, string>,
|
package/src/lib/docker.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
watch as fsWatch,
|
|
8
|
+
} from "fs";
|
|
3
9
|
import { arch, platform } from "os";
|
|
4
10
|
import { dirname, join, resolve } from "path";
|
|
5
11
|
|
|
@@ -12,6 +18,7 @@ import {
|
|
|
12
18
|
setActiveAssistant,
|
|
13
19
|
} from "./assistant-config";
|
|
14
20
|
import type { AssistantEntry } from "./assistant-config";
|
|
21
|
+
import { buildHatchConfigValues, writeInitialConfig } from "./config-utils";
|
|
15
22
|
import { buildServiceRunArgs } from "./statefulset.js";
|
|
16
23
|
import type { Species } from "./constants";
|
|
17
24
|
import { getDefaultPorts } from "./environments/paths.js";
|
|
@@ -35,7 +42,7 @@ import {
|
|
|
35
42
|
resolveHatchProvider,
|
|
36
43
|
} from "./provider-secrets.js";
|
|
37
44
|
import { findOpenPort } from "./port-allocator.js";
|
|
38
|
-
import { exec, execOutput } from "./step-runner";
|
|
45
|
+
import { exec, execOutput, execWithStdin } from "./step-runner";
|
|
39
46
|
import {
|
|
40
47
|
closeLogFile,
|
|
41
48
|
openLogFile,
|
|
@@ -1164,11 +1171,53 @@ export async function hatchDocker(
|
|
|
1164
1171
|
"chown 1001:1001 /workspace /run/assistant-ipc /run/gateway-ipc",
|
|
1165
1172
|
]);
|
|
1166
1173
|
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
// (`
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1174
|
+
// Stage the BYOK default-workspace-config overlay *inside* the workspace
|
|
1175
|
+
// volume so the daemon's startup loader can consume it. The loader
|
|
1176
|
+
// (`mergeDefaultWorkspaceConfig` in assistant/src/config/loader.ts) does:
|
|
1177
|
+
// 1. JSON.parse + deep-merge into the workspace's config.json.
|
|
1178
|
+
// 2. `renameSync(defaultConfigPath, <workspace>/default-config.json)`
|
|
1179
|
+
// to mark the overlay consumed so subsequent restarts skip it.
|
|
1180
|
+
//
|
|
1181
|
+
// The rename must be same-filesystem. Bind-mounting the host file into
|
|
1182
|
+
// `/tmp/...` (the pre-#32025 design) crossed filesystems and silently
|
|
1183
|
+
// failed with EXDEV, so every `docker start` re-applied the overlay.
|
|
1184
|
+
// Staging into the workspace volume keeps the rename in-place.
|
|
1185
|
+
//
|
|
1186
|
+
// Streaming the JSON via stdin (instead of bind-mounting the host file
|
|
1187
|
+
// into the staging container) sidesteps macOS Colima's virtiofs share,
|
|
1188
|
+
// which doesn't expose `/var/folders/...` (where `os.tmpdir()` resolves
|
|
1189
|
+
// on macOS) and would otherwise materialize an empty directory at the
|
|
1190
|
+
// bind-mount target.
|
|
1191
|
+
//
|
|
1192
|
+
// @deprecated stopgap. Replacement direction is one of:
|
|
1193
|
+
// 1. Post-hatch API calls (POST /v1/secrets + a small endpoint that
|
|
1194
|
+
// returns canonical inference-profile templates).
|
|
1195
|
+
// 2. Move inference-profile seeds out of workspace config and into
|
|
1196
|
+
// Assistant code, eliminating the overlay entirely.
|
|
1197
|
+
// See `cli/src/lib/config-utils.ts` JSDoc for context.
|
|
1198
|
+
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
1199
|
+
const hostOverlayPath = writeInitialConfig(hatchConfigValues);
|
|
1200
|
+
const stagedOverlayInContainer = "/workspace/.default-config-overlay.json";
|
|
1201
|
+
const extraAssistantEnv: Record<string, string> = {};
|
|
1202
|
+
if (hostOverlayPath) {
|
|
1203
|
+
await execWithStdin(
|
|
1204
|
+
"docker",
|
|
1205
|
+
[
|
|
1206
|
+
"run",
|
|
1207
|
+
"--rm",
|
|
1208
|
+
"-i",
|
|
1209
|
+
"-v",
|
|
1210
|
+
`${res.workspaceVolume}:/workspace`,
|
|
1211
|
+
"busybox",
|
|
1212
|
+
"sh",
|
|
1213
|
+
"-c",
|
|
1214
|
+
`cat > ${stagedOverlayInContainer} && chown 1001:1001 ${stagedOverlayInContainer}`,
|
|
1215
|
+
],
|
|
1216
|
+
readFileSync(hostOverlayPath, "utf-8"),
|
|
1217
|
+
);
|
|
1218
|
+
extraAssistantEnv.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH =
|
|
1219
|
+
stagedOverlayInContainer;
|
|
1220
|
+
}
|
|
1172
1221
|
|
|
1173
1222
|
const cesServiceToken = randomBytes(32).toString("hex");
|
|
1174
1223
|
const signingKey = randomBytes(32).toString("hex");
|
|
@@ -1190,6 +1239,7 @@ export async function hatchDocker(
|
|
|
1190
1239
|
signingKey,
|
|
1191
1240
|
bootstrapSecret,
|
|
1192
1241
|
cesServiceToken,
|
|
1242
|
+
extraAssistantEnv,
|
|
1193
1243
|
gatewayPort,
|
|
1194
1244
|
imageTags,
|
|
1195
1245
|
instanceName,
|
|
@@ -27,7 +27,8 @@ const {
|
|
|
27
27
|
getLockfilePaths,
|
|
28
28
|
getMultiInstanceDir,
|
|
29
29
|
} = await import("../paths.js");
|
|
30
|
-
type EnvironmentDefinition =
|
|
30
|
+
type EnvironmentDefinition =
|
|
31
|
+
import("@vellumai/environments").EnvironmentDefinition;
|
|
31
32
|
|
|
32
33
|
const prod: EnvironmentDefinition = {
|
|
33
34
|
name: "production",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { SEEDS } from "@vellumai/environments";
|
|
4
|
+
|
|
3
5
|
import { getDefaultPorts } from "../paths.js";
|
|
4
|
-
import { SEEDS } from "../seeds.js";
|
|
5
6
|
|
|
6
7
|
describe("SEEDS port blocks", () => {
|
|
7
8
|
test("production uses the legacy (pre-MVP) port layout", () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
-
import type { EnvironmentDefinition, PortMap } from "
|
|
4
|
+
import type { EnvironmentDefinition, PortMap } from "@vellumai/environments";
|
|
5
5
|
|
|
6
6
|
const PRODUCTION_ENVIRONMENT_NAME = "production";
|
|
7
7
|
|
|
@@ -8,8 +8,7 @@ import {
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { dirname, join } from "path";
|
|
10
10
|
|
|
11
|
-
import { SEEDS } from "
|
|
12
|
-
import type { EnvironmentDefinition } from "./types.js";
|
|
11
|
+
import { SEEDS, type EnvironmentDefinition } from "@vellumai/environments";
|
|
13
12
|
|
|
14
13
|
const DEFAULT_ENVIRONMENT_NAME = "production";
|
|
15
14
|
|
|
@@ -115,7 +114,7 @@ export function getCurrentEnvironment(
|
|
|
115
114
|
// writers don't end up in disjoint states on a typo.
|
|
116
115
|
process.stderr.write(
|
|
117
116
|
`warning: unknown environment "${name}"; falling back to "${DEFAULT_ENVIRONMENT_NAME}". ` +
|
|
118
|
-
`Add it to
|
|
117
|
+
`Add it to packages/environments/src/seeds.ts and rebuild if this was intentional.\n`,
|
|
119
118
|
);
|
|
120
119
|
}
|
|
121
120
|
const fallback = SEEDS[DEFAULT_ENVIRONMENT_NAME];
|
|
@@ -174,5 +173,3 @@ export function resolveEnvironmentSource(override?: string): {
|
|
|
174
173
|
}
|
|
175
174
|
return { name: DEFAULT_ENVIRONMENT_NAME, source: "default" };
|
|
176
175
|
}
|
|
177
|
-
|
|
178
|
-
|
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
import { platform } from "os";
|
|
12
12
|
import { dirname, join } from "path";
|
|
13
13
|
|
|
14
|
+
import { SEEDS } from "@vellumai/environments";
|
|
15
|
+
|
|
14
16
|
import { getConfigDir } from "./environments/paths.js";
|
|
15
17
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
16
|
-
import { SEEDS } from "./environments/seeds.js";
|
|
17
18
|
|
|
18
19
|
const DEVICE_ID_SALT = "vellum-assistant-host-id";
|
|
19
20
|
|
|
@@ -176,7 +177,8 @@ export async function refreshGuardianToken(
|
|
|
176
177
|
// Gateway persists expiresAt as epoch-ms numbers; Date.parse("1234567890000")
|
|
177
178
|
// returns NaN. new Date() accepts both ISO strings and epoch-ms numbers.
|
|
178
179
|
const refreshExpiry = new Date(tokenData.refreshTokenExpiresAt).getTime();
|
|
179
|
-
if (!Number.isFinite(refreshExpiry) || refreshExpiry <= Date.now())
|
|
180
|
+
if (!Number.isFinite(refreshExpiry) || refreshExpiry <= Date.now())
|
|
181
|
+
return null;
|
|
180
182
|
|
|
181
183
|
try {
|
|
182
184
|
const response = await fetch(`${gatewayUrl}/v1/guardian/refresh`, {
|
|
@@ -191,11 +193,16 @@ export async function refreshGuardianToken(
|
|
|
191
193
|
|
|
192
194
|
const json = (await response.json()) as Record<string, unknown>;
|
|
193
195
|
const refreshed: GuardianTokenData = {
|
|
194
|
-
guardianPrincipalId:
|
|
196
|
+
guardianPrincipalId:
|
|
197
|
+
(json.guardianPrincipalId as string) ?? tokenData.guardianPrincipalId,
|
|
195
198
|
accessToken: json.accessToken as string,
|
|
196
|
-
accessTokenExpiresAt:
|
|
199
|
+
accessTokenExpiresAt:
|
|
200
|
+
(json.accessTokenExpiresAt as string | number) ??
|
|
201
|
+
tokenData.accessTokenExpiresAt,
|
|
197
202
|
refreshToken: (json.refreshToken as string) ?? tokenData.refreshToken,
|
|
198
|
-
refreshTokenExpiresAt:
|
|
203
|
+
refreshTokenExpiresAt:
|
|
204
|
+
(json.refreshTokenExpiresAt as string | number) ??
|
|
205
|
+
tokenData.refreshTokenExpiresAt,
|
|
199
206
|
refreshAfter: (json.refreshAfter as string) ?? tokenData.refreshAfter,
|
|
200
207
|
isNew: false,
|
|
201
208
|
deviceId: tokenData.deviceId,
|
package/src/lib/hatch-local.ts
CHANGED
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
import { generateInstanceName } from "./random-name.js";
|
|
34
34
|
import { leaseGuardianToken } from "./guardian-token.js";
|
|
35
35
|
import { archiveLogFile, resetLogFile } from "./xdg-log.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
consoleLifecycleReporter,
|
|
38
|
+
type LifecycleReporter,
|
|
39
|
+
} from "./lifecycle-reporter.js";
|
|
37
40
|
import {
|
|
38
41
|
configureHatchProviderApiKey,
|
|
39
42
|
formatProviderName,
|
|
@@ -134,6 +137,25 @@ function installCLISymlink(): void {
|
|
|
134
137
|
|
|
135
138
|
export interface HatchLocalOptions {
|
|
136
139
|
setupProviderCredentials?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Sink for progress and log output. Defaults to the console reporter so CLI
|
|
142
|
+
* callers keep their existing terminal output; in-process callers can inject
|
|
143
|
+
* their own reporter to consume progress without writing to stdout.
|
|
144
|
+
*/
|
|
145
|
+
reporter?: LifecycleReporter;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface HatchLocalResult {
|
|
149
|
+
assistantId: string;
|
|
150
|
+
runtimeUrl: string;
|
|
151
|
+
localUrl: string;
|
|
152
|
+
species: Species;
|
|
153
|
+
/**
|
|
154
|
+
* Guardian access token leased during hatch, when the lease succeeded. The
|
|
155
|
+
* full token pair is persisted to disk regardless; this is surfaced so an
|
|
156
|
+
* in-process caller can prime a connection without re-reading the file.
|
|
157
|
+
*/
|
|
158
|
+
guardianAccessToken?: string;
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
export async function hatchLocal(
|
|
@@ -143,7 +165,8 @@ export async function hatchLocal(
|
|
|
143
165
|
keepAlive: boolean = false,
|
|
144
166
|
configValues: Record<string, string> = {},
|
|
145
167
|
options: HatchLocalOptions = {},
|
|
146
|
-
): Promise<
|
|
168
|
+
): Promise<HatchLocalResult> {
|
|
169
|
+
const reporter = options.reporter ?? consoleLifecycleReporter;
|
|
147
170
|
const provider =
|
|
148
171
|
options.setupProviderCredentials === false
|
|
149
172
|
? undefined
|
|
@@ -153,7 +176,7 @@ export async function hatchLocal(
|
|
|
153
176
|
name ?? process.env.VELLUM_ASSISTANT_NAME,
|
|
154
177
|
);
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
reporter.progress(1, 6, "Allocating resources...");
|
|
157
180
|
|
|
158
181
|
const existing = findAssistantByName(instanceName);
|
|
159
182
|
if (existing && (!existing.cloud || existing.cloud === "local")) {
|
|
@@ -175,43 +198,47 @@ export async function hatchLocal(
|
|
|
175
198
|
archiveLogFile("hatch.log", logsDir);
|
|
176
199
|
resetLogFile("hatch.log");
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
reporter.log(`🥚 Hatching local assistant: ${instanceName}`);
|
|
202
|
+
reporter.log(` Species: ${species}`);
|
|
203
|
+
reporter.log("");
|
|
181
204
|
|
|
182
205
|
const apiKeyCheck = checkProviderApiKey();
|
|
183
206
|
if (!apiKeyCheck.hasKey) {
|
|
184
|
-
|
|
207
|
+
reporter.warn(
|
|
185
208
|
"Warning: No LLM provider API key is configured. The assistant will fail when you try to send a message.",
|
|
186
209
|
);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
reporter.warn(" To fix, export your key before running vellum hatch:");
|
|
211
|
+
reporter.warn(" export ANTHROPIC_API_KEY=<your-key>");
|
|
212
|
+
reporter.warn("");
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
if (!process.env.APP_VERSION) {
|
|
193
216
|
process.env.APP_VERSION = cliPkg.version;
|
|
194
217
|
}
|
|
195
218
|
|
|
196
|
-
|
|
219
|
+
reporter.progress(2, 6, "Writing configuration...");
|
|
197
220
|
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
198
221
|
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
199
222
|
|
|
200
|
-
|
|
223
|
+
reporter.progress(3, 6, "Starting assistant...");
|
|
201
224
|
const signingKey = generateLocalSigningKey();
|
|
225
|
+
const bootstrapSecret = generateLocalSigningKey();
|
|
202
226
|
await startLocalDaemon(watch, resources, {
|
|
203
227
|
defaultWorkspaceConfigPath,
|
|
204
228
|
signingKey,
|
|
205
229
|
});
|
|
206
230
|
|
|
207
|
-
|
|
231
|
+
reporter.progress(4, 6, "Starting gateway...");
|
|
208
232
|
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
209
233
|
try {
|
|
210
|
-
runtimeUrl = await startGateway(watch, resources, {
|
|
234
|
+
runtimeUrl = await startGateway(watch, resources, {
|
|
235
|
+
signingKey,
|
|
236
|
+
bootstrapSecret,
|
|
237
|
+
});
|
|
211
238
|
} catch (error) {
|
|
212
239
|
// Gateway failed — stop the daemon we just started so we don't leave
|
|
213
240
|
// orphaned processes with no lock file entry.
|
|
214
|
-
|
|
241
|
+
reporter.error(
|
|
215
242
|
`\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
|
|
216
243
|
);
|
|
217
244
|
await stopLocalProcesses(resources);
|
|
@@ -222,24 +249,28 @@ export async function hatchLocal(
|
|
|
222
249
|
// instead of hitting /v1/guardian/init itself. Use loopback to satisfy
|
|
223
250
|
// the daemon's local-only check — the mDNS runtimeUrl resolves to a LAN
|
|
224
251
|
// IP which the daemon rejects as non-loopback.
|
|
225
|
-
|
|
252
|
+
reporter.progress(5, 6, "Securing connection...");
|
|
226
253
|
const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
227
254
|
const maxLeaseAttempts = 3;
|
|
228
255
|
let guardianAccessToken: string | undefined;
|
|
229
256
|
for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
|
|
230
257
|
try {
|
|
231
|
-
const tokenData = await leaseGuardianToken(
|
|
258
|
+
const tokenData = await leaseGuardianToken(
|
|
259
|
+
loopbackUrl,
|
|
260
|
+
instanceName,
|
|
261
|
+
bootstrapSecret,
|
|
262
|
+
);
|
|
232
263
|
guardianAccessToken = tokenData.accessToken;
|
|
233
264
|
break;
|
|
234
265
|
} catch (err) {
|
|
235
266
|
if (attempt < maxLeaseAttempts) {
|
|
236
267
|
const delayMs = 2000 * 2 ** (attempt - 1);
|
|
237
|
-
|
|
268
|
+
reporter.error(
|
|
238
269
|
`⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
|
|
239
270
|
);
|
|
240
271
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
241
272
|
} else {
|
|
242
|
-
|
|
273
|
+
reporter.error(
|
|
243
274
|
`⚠️ Guardian token lease failed after ${maxLeaseAttempts} attempts: ${err}\n` +
|
|
244
275
|
` The assistant is running but guardian-token.json was not written.\n` +
|
|
245
276
|
` If the desktop app loses its stored credentials, re-hatch to recover.`,
|
|
@@ -257,9 +288,10 @@ export async function hatchLocal(
|
|
|
257
288
|
species,
|
|
258
289
|
hatchedAt: new Date().toISOString(),
|
|
259
290
|
resources: { ...resources, signingKey },
|
|
291
|
+
guardianBootstrapSecret: bootstrapSecret,
|
|
260
292
|
};
|
|
261
293
|
|
|
262
|
-
|
|
294
|
+
reporter.progress(6, 6, "Saving configuration...");
|
|
263
295
|
saveAssistantEntry(localEntry);
|
|
264
296
|
setActiveAssistant(instanceName);
|
|
265
297
|
|
|
@@ -268,13 +300,13 @@ export async function hatchLocal(
|
|
|
268
300
|
}
|
|
269
301
|
|
|
270
302
|
if (provider !== undefined && provider !== null && !guardianAccessToken) {
|
|
271
|
-
|
|
303
|
+
reporter.error(
|
|
272
304
|
`⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
|
|
273
305
|
` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
|
|
274
306
|
);
|
|
275
307
|
} else if (provider !== undefined) {
|
|
276
|
-
|
|
277
|
-
|
|
308
|
+
reporter.log("");
|
|
309
|
+
reporter.log(
|
|
278
310
|
provider === null
|
|
279
311
|
? "Checking provider credentials..."
|
|
280
312
|
: `Checking ${formatProviderName(provider)} credentials...`,
|
|
@@ -287,14 +319,22 @@ export async function hatchLocal(
|
|
|
287
319
|
});
|
|
288
320
|
}
|
|
289
321
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
logHatchNextSteps(
|
|
322
|
+
reporter.log("");
|
|
323
|
+
reporter.log(`✅ Local assistant hatched!`);
|
|
324
|
+
reporter.log("");
|
|
325
|
+
reporter.log("Instance details:");
|
|
326
|
+
reporter.log(` Name: ${instanceName}`);
|
|
327
|
+
reporter.log(` Runtime: ${runtimeUrl}`);
|
|
328
|
+
reporter.log("");
|
|
329
|
+
logHatchNextSteps((message) => reporter.log(message), instanceName);
|
|
330
|
+
|
|
331
|
+
const result: HatchLocalResult = {
|
|
332
|
+
assistantId: instanceName,
|
|
333
|
+
runtimeUrl,
|
|
334
|
+
localUrl: `http://127.0.0.1:${resources.gatewayPort}`,
|
|
335
|
+
species,
|
|
336
|
+
guardianAccessToken,
|
|
337
|
+
};
|
|
298
338
|
|
|
299
339
|
if (keepAlive) {
|
|
300
340
|
const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
@@ -304,7 +344,7 @@ export async function hatchLocal(
|
|
|
304
344
|
let consecutiveFailures = 0;
|
|
305
345
|
|
|
306
346
|
const shutdown = async (): Promise<void> => {
|
|
307
|
-
|
|
347
|
+
reporter.log("\nShutting down local processes...");
|
|
308
348
|
await stopLocalProcesses(resources);
|
|
309
349
|
process.exit(0);
|
|
310
350
|
};
|
|
@@ -328,7 +368,7 @@ export async function hatchLocal(
|
|
|
328
368
|
consecutiveFailures++;
|
|
329
369
|
}
|
|
330
370
|
if (consecutiveFailures >= MAX_FAILURES) {
|
|
331
|
-
|
|
371
|
+
reporter.log(
|
|
332
372
|
`\n⚠️ ${healthTarget} stopped responding — shutting down.`,
|
|
333
373
|
);
|
|
334
374
|
await stopLocalProcesses(resources);
|
|
@@ -336,4 +376,6 @@ export async function hatchLocal(
|
|
|
336
376
|
}
|
|
337
377
|
}
|
|
338
378
|
}
|
|
379
|
+
|
|
380
|
+
return result;
|
|
339
381
|
}
|
package/src/lib/http-client.ts
CHANGED
|
@@ -8,8 +8,6 @@ export function buildDaemonUrl(port: number): string {
|
|
|
8
8
|
/**
|
|
9
9
|
* Perform an HTTP health check against the daemon's `/healthz` endpoint.
|
|
10
10
|
* Returns true if the daemon responds with HTTP 200, false otherwise.
|
|
11
|
-
*
|
|
12
|
-
* This replaces the socket-based `isSocketResponsive()` check.
|
|
13
11
|
*/
|
|
14
12
|
export async function httpHealthCheck(
|
|
15
13
|
port: number,
|
|
@@ -28,7 +26,7 @@ export async function httpHealthCheck(
|
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
28
|
* Poll the daemon's `/healthz` endpoint until it responds with 200 or the
|
|
31
|
-
* timeout is reached.
|
|
29
|
+
* timeout is reached.
|
|
32
30
|
*
|
|
33
31
|
* Returns true if the daemon became healthy within the timeout, false otherwise.
|
|
34
32
|
*/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { emitProgress } from "./desktop-progress.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sink for the human-facing and structured output of long-running lifecycle
|
|
5
|
+
* operations (hatch, retire). Injecting it lets an in-process caller (e.g. a
|
|
6
|
+
* desktop main process embedding these functions) observe progress without the
|
|
7
|
+
* operation writing to the terminal, while the CLI keeps its existing stdout.
|
|
8
|
+
*/
|
|
9
|
+
export interface LifecycleReporter {
|
|
10
|
+
/**
|
|
11
|
+
* Coarse step progress. The CLI reporter mirrors this to the desktop
|
|
12
|
+
* `HATCH_PROGRESS:` stdout channel.
|
|
13
|
+
*/
|
|
14
|
+
progress(step: number, total: number, label: string): void;
|
|
15
|
+
log(message: string): void;
|
|
16
|
+
warn(message: string): void;
|
|
17
|
+
error(message: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reporter used by the CLI commands: human-readable lines to the console plus
|
|
22
|
+
* structured step events on the desktop progress channel. Reproduces the exact
|
|
23
|
+
* terminal output — and the `HATCH_PROGRESS:` lines under `VELLUM_DESKTOP_APP` —
|
|
24
|
+
* that existing subprocess consumers parse.
|
|
25
|
+
*/
|
|
26
|
+
export const consoleLifecycleReporter: LifecycleReporter = {
|
|
27
|
+
progress: (step, total, label) => emitProgress(step, total, label),
|
|
28
|
+
log: (message) => console.log(message),
|
|
29
|
+
warn: (message) => console.warn(message),
|
|
30
|
+
error: (message) => console.error(message),
|
|
31
|
+
};
|