@vellumai/cli 0.10.5-dev.202607051531.dbb8c59 → 0.10.5-dev.202607051730.b756a91

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.5-dev.202607051531.dbb8c59",
3
+ "version": "0.10.5-dev.202607051730.b756a91",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -90,11 +90,16 @@ const resolveProcessStateMock = mock<typeof processLib.resolveProcessState>(
90
90
  const stopProcessByPidFileMock = mock<typeof processLib.stopProcessByPidFile>(
91
91
  async () => true,
92
92
  );
93
+ const isProcessAliveMock = mock<typeof processLib.isProcessAlive>(() => ({
94
+ alive: false,
95
+ pid: null,
96
+ }));
93
97
 
94
98
  mock.module("../lib/process", () => ({
95
99
  ...realProcessLib,
96
100
  resolveProcessState: resolveProcessStateMock,
97
101
  stopProcessByPidFile: stopProcessByPidFileMock,
102
+ isProcessAlive: isProcessAliveMock,
98
103
  }));
99
104
 
100
105
  const generateLocalSigningKeyMock = mock<typeof local.generateLocalSigningKey>(
@@ -110,6 +115,7 @@ const startLocalDaemonMock = mock<typeof local.startLocalDaemon>(async () => {})
110
115
  const startGatewayMock = mock<typeof local.startGateway>(
111
116
  async () => "http://127.0.0.1:7830",
112
117
  );
118
+ const startCesMock = mock<typeof local.startCes>(async () => {});
113
119
 
114
120
  mock.module("../lib/local", () => ({
115
121
  ...realLocal,
@@ -118,6 +124,7 @@ mock.module("../lib/local", () => ({
118
124
  isGatewayWatchModeAvailable: isGatewayWatchModeAvailableMock,
119
125
  startLocalDaemon: startLocalDaemonMock,
120
126
  startGateway: startGatewayMock,
127
+ startCes: startCesMock,
121
128
  }));
122
129
 
123
130
  const maybeStartNgrokTunnelMock = mock<typeof ngrok.maybeStartNgrokTunnel>(
@@ -184,6 +191,10 @@ beforeEach(() => {
184
191
  startLocalDaemonMock.mockResolvedValue(undefined);
185
192
  startGatewayMock.mockReset();
186
193
  startGatewayMock.mockResolvedValue("http://127.0.0.1:7830");
194
+ startCesMock.mockReset();
195
+ startCesMock.mockResolvedValue(undefined);
196
+ isProcessAliveMock.mockReset();
197
+ isProcessAliveMock.mockReturnValue({ alive: false, pid: null });
187
198
  seedGuardianTokenFromSiblingEnvMock.mockReset();
188
199
  seedGuardianTokenFromSiblingEnvMock.mockReturnValue(false);
189
200
  loadGuardianTokenMock.mockReset();
@@ -291,4 +302,27 @@ describe("vellum wake", () => {
291
302
  "generated-bootstrap-secret",
292
303
  );
293
304
  });
305
+
306
+ test("relaunches CES sibling when daemon is healthy but CES is dead", async () => {
307
+ // Daemon is healthy (default mock) but CES pid says dead.
308
+ // isProcessAliveMock defaults to { alive: false }.
309
+ await wake();
310
+
311
+ // startCes should be called to relaunch the dead sibling.
312
+ expect(startCesMock).toHaveBeenCalledTimes(1);
313
+ // startLocalDaemon should NOT be called (daemon already running).
314
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
315
+ });
316
+
317
+ test("does NOT relaunch CES sibling when both daemon and CES are healthy", async () => {
318
+ // Daemon is healthy (default mock). CES is also alive.
319
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 789 });
320
+
321
+ await wake();
322
+
323
+ // startCes should NOT be called (CES is alive).
324
+ expect(startCesMock).not.toHaveBeenCalled();
325
+ // startLocalDaemon should NOT be called (daemon already running).
326
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
327
+ });
294
328
  });
@@ -146,7 +146,7 @@ ${userSetup}
146
146
  ${envSetLines}
147
147
  VELLUM_ASSISTANT_NAME=${instanceName}
148
148
  mkdir -p "\$HOME/.config/vellum"
149
- cat > "\$HOME/.config/vellum/env" << DOTENV_EOF
149
+ cat > "\$HOME/.config/vellum/.env" << DOTENV_EOF
150
150
  ${dotenvLines}
151
151
  RUNTIME_HTTP_PORT=7821
152
152
  DOTENV_EOF
@@ -12,7 +12,7 @@ import {
12
12
  resetGuardianBootstrap,
13
13
  seedGuardianTokenFromSiblingEnv,
14
14
  } from "../lib/guardian-token.js";
15
- import { resolveProcessState, stopProcessByPidFile } from "../lib/process";
15
+ import { resolveProcessState, stopProcessByPidFile, isProcessAlive } from "../lib/process";
16
16
  import {
17
17
  generateLocalSigningKey,
18
18
  isAssistantWatchModeAvailable,
@@ -189,6 +189,19 @@ export async function wake(): Promise<void> {
189
189
  startCes(watch, resources),
190
190
  startLocalDaemon(watch, resources, { foreground, signingKey }),
191
191
  ]);
192
+ } else {
193
+ // Self-heal: the daemon is already healthy, but the CES sibling may have
194
+ // died independently (crash, OOM kill). A dead ces.pid under a live daemon
195
+ // means credential operations will fail until the next wake. Relaunch the
196
+ // sibling so the daemon's lazy reconnect (secure-keys.ts) picks it up on
197
+ // the next credential read. startCes is a no-op unless CES_STANDALONE.
198
+ const vellumDir = join(resources.instanceDir, ".vellum");
199
+ const cesPidFile = join(vellumDir, "ces.pid");
200
+ const cesAlive = isProcessAlive(cesPidFile).alive;
201
+ if (!cesAlive) {
202
+ console.log("CES sibling not running — relaunching...");
203
+ await startCes(watch, resources);
204
+ }
192
205
  }
193
206
 
194
207
  // Start gateway
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { join } from "path";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ import { resolveConfigDir } from "@vellumai/local-mode";
7
+
3
8
  import cliPkg from "../package.json";
4
9
  import { backup } from "./commands/backup";
5
10
  import { clean } from "./commands/clean";
@@ -156,6 +161,56 @@ function applyNoColorFlags(argv: string[]): void {
156
161
  }
157
162
  }
158
163
 
164
+ /**
165
+ * Load env vars from the vellum config dotenv file into `process.env` so
166
+ * that `vellum hatch` forwards provider API keys to containers and other
167
+ * commands have access to them.
168
+ *
169
+ * Reads `$XDG_CONFIG_HOME/vellum{-env}/.env` — the same config directory
170
+ * the CLI uses for guardian tokens and environment state. The file is
171
+ * written by remote-hatch scripts and can be user-managed.
172
+ *
173
+ * Existing `process.env` values take precedence (standard dotenv convention).
174
+ * Only KEY=VALUE lines are parsed. Lines starting with # are comments.
175
+ * Values may be quoted with single or double quotes.
176
+ */
177
+ function loadConfigDotenv(): void {
178
+ const configDir = resolveConfigDir(process.env);
179
+ const envPath = join(configDir, ".env");
180
+
181
+ let content: string;
182
+ try {
183
+ content = readFileSync(envPath, "utf-8");
184
+ } catch {
185
+ return;
186
+ }
187
+
188
+ for (const line of content.split("\n")) {
189
+ const trimmed = line.trim();
190
+ if (!trimmed || trimmed.startsWith("#")) continue;
191
+
192
+ const eqIndex = trimmed.indexOf("=");
193
+ if (eqIndex === -1) continue;
194
+
195
+ const key = trimmed.slice(0, eqIndex).trim();
196
+ if (!key) continue;
197
+
198
+ // Existing env vars take precedence (dotenv convention).
199
+ if (process.env[key] !== undefined) continue;
200
+
201
+ let value = trimmed.slice(eqIndex + 1).trim();
202
+ // Strip surrounding quotes.
203
+ if (
204
+ (value.startsWith('"') && value.endsWith('"')) ||
205
+ (value.startsWith("'") && value.endsWith("'"))
206
+ ) {
207
+ value = value.slice(1, -1);
208
+ }
209
+
210
+ process.env[key] = value;
211
+ }
212
+ }
213
+
159
214
  /**
160
215
  * If a running assistant is detected, launch the TUI client and return true.
161
216
  * Otherwise return false so the caller can fall back to help text.
@@ -181,6 +236,10 @@ async function tryLaunchClient(): Promise<boolean> {
181
236
  }
182
237
 
183
238
  async function main() {
239
+ // Load $XDG_CONFIG_HOME/vellum/.env before any command runs so
240
+ // provider API keys and other config are available to hatch, exec, etc.
241
+ loadConfigDotenv();
242
+
184
243
  const args = process.argv.slice(2);
185
244
 
186
245
  // Must run before any command or terminal-capabilities usage