@vellumai/cli 0.10.12-dev.202607250047.75b9ec6 → 0.10.12-staging.1
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/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +0 -25
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +0 -7
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +0 -24
- package/node_modules/@vellumai/service-contracts/package.json +0 -1
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +1 -69
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +2 -118
- package/src/__tests__/flags.test.ts +0 -77
- package/src/__tests__/pair.test.ts +0 -146
- package/src/__tests__/tunnel.test.ts +0 -53
- package/src/__tests__/wake.test.ts +0 -178
- package/src/commands/flags.ts +1 -4
- package/src/commands/nginx-ingress.ts +71 -57
- package/src/commands/pair.ts +0 -25
- package/src/commands/tunnel.ts +2 -15
- package/src/commands/wake.ts +0 -118
- package/src/lib/assistant-config.ts +2 -5
- package/src/lib/hatch-local.ts +86 -98
- package/src/lib/local.ts +2 -20
- package/src/lib/nginx-ingress.ts +0 -90
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +0 -449
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +0 -468
- package/src/__tests__/local-hatch-lock.test.ts +0 -163
- package/src/lib/environments/__tests__/detect.test.ts +0 -101
- package/src/lib/environments/detect.ts +0 -79
- package/src/lib/local-hatch-lock.ts +0 -322
- package/src/lib/stale-cli-hint.ts +0 -11
package/src/lib/hatch-local.ts
CHANGED
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
import { logHatchNextSteps } from "./hatch-next-steps.js";
|
|
47
47
|
import { checkProviderApiKey } from "./api-key-check.js";
|
|
48
48
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
49
|
-
import { withLocalHatchLock } from "./local-hatch-lock.js";
|
|
50
49
|
|
|
51
50
|
/**
|
|
52
51
|
* Attempts to place a symlink at the given path pointing to cliBinary.
|
|
@@ -180,116 +179,89 @@ export async function hatchLocal(
|
|
|
180
179
|
name ?? process.env.VELLUM_ASSISTANT_NAME,
|
|
181
180
|
);
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
await withLocalHatchLock(async () => {
|
|
185
|
-
reporter.progress(1, 6, "Allocating resources...");
|
|
182
|
+
reporter.progress(1, 6, "Allocating resources...");
|
|
186
183
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const resources = await allocateLocalResources(instanceName);
|
|
196
|
-
|
|
197
|
-
const logsDir = join(
|
|
198
|
-
resources.instanceDir,
|
|
199
|
-
".vellum",
|
|
200
|
-
"workspace",
|
|
201
|
-
"data",
|
|
202
|
-
"logs",
|
|
203
|
-
);
|
|
204
|
-
archiveLogFile("hatch.log", logsDir);
|
|
205
|
-
resetLogFile("hatch.log");
|
|
206
|
-
|
|
207
|
-
reporter.log(`🥚 Hatching local assistant: ${instanceName}`);
|
|
208
|
-
reporter.log(` Species: ${species}`);
|
|
209
|
-
reporter.log("");
|
|
184
|
+
const existing = findAssistantByName(instanceName);
|
|
185
|
+
if (existing && (!existing.cloud || existing.cloud === "local")) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`An assistant named "${instanceName}" is already hatched.\n` +
|
|
188
|
+
`Run \`vellum wake\` to restart it, or \`vellum retire ${instanceName}\` to remove it first.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
210
191
|
|
|
211
|
-
|
|
212
|
-
if (!apiKeyCheck.hasKey) {
|
|
213
|
-
reporter.warn(
|
|
214
|
-
"Warning: No LLM provider API key is configured. The assistant will fail when you try to send a message.",
|
|
215
|
-
);
|
|
216
|
-
reporter.warn(" To fix, export your key before running vellum hatch:");
|
|
217
|
-
reporter.warn(" export ANTHROPIC_API_KEY=<your-key>");
|
|
218
|
-
reporter.warn("");
|
|
219
|
-
}
|
|
192
|
+
const resources = await allocateLocalResources(instanceName);
|
|
220
193
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
194
|
+
const logsDir = join(
|
|
195
|
+
resources.instanceDir,
|
|
196
|
+
".vellum",
|
|
197
|
+
"workspace",
|
|
198
|
+
"data",
|
|
199
|
+
"logs",
|
|
200
|
+
);
|
|
201
|
+
archiveLogFile("hatch.log", logsDir);
|
|
202
|
+
resetLogFile("hatch.log");
|
|
224
203
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
204
|
+
reporter.log(`🥚 Hatching local assistant: ${instanceName}`);
|
|
205
|
+
reporter.log(` Species: ${species}`);
|
|
206
|
+
reporter.log("");
|
|
228
207
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const startupResults = await Promise.allSettled([
|
|
239
|
-
startCes(watch, resources),
|
|
240
|
-
startLocalDaemon(watch, resources, {
|
|
241
|
-
defaultWorkspaceConfigPath,
|
|
242
|
-
requireReady: true,
|
|
243
|
-
signingKey,
|
|
244
|
-
}),
|
|
245
|
-
]);
|
|
246
|
-
const startupFailure = startupResults.find(
|
|
247
|
-
(result): result is PromiseRejectedResult =>
|
|
248
|
-
result.status === "rejected",
|
|
249
|
-
);
|
|
250
|
-
if (startupFailure) {
|
|
251
|
-
throw startupFailure.reason;
|
|
252
|
-
}
|
|
208
|
+
const apiKeyCheck = checkProviderApiKey();
|
|
209
|
+
if (!apiKeyCheck.hasKey) {
|
|
210
|
+
reporter.warn(
|
|
211
|
+
"Warning: No LLM provider API key is configured. The assistant will fail when you try to send a message.",
|
|
212
|
+
);
|
|
213
|
+
reporter.warn(" To fix, export your key before running vellum hatch:");
|
|
214
|
+
reporter.warn(" export ANTHROPIC_API_KEY=<your-key>");
|
|
215
|
+
reporter.warn("");
|
|
216
|
+
}
|
|
253
217
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
bootstrapSecret,
|
|
258
|
-
envOverrides: flagEnvVars,
|
|
259
|
-
requireReady: true,
|
|
260
|
-
});
|
|
261
|
-
} catch (error) {
|
|
262
|
-
reporter.error(
|
|
263
|
-
`\n❌ Local assistant startup failed — stopping partial processes.`,
|
|
264
|
-
);
|
|
265
|
-
await stopLocalProcesses(resources);
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
218
|
+
if (!process.env.APP_VERSION) {
|
|
219
|
+
process.env.APP_VERSION = cliPkg.version;
|
|
220
|
+
}
|
|
268
221
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
222
|
+
reporter.progress(2, 6, "Writing configuration...");
|
|
223
|
+
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
224
|
+
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
225
|
+
|
|
226
|
+
reporter.progress(3, 6, "Starting assistant...");
|
|
227
|
+
const signingKey = generateLocalSigningKey();
|
|
228
|
+
const bootstrapSecret = generateLocalSigningKey();
|
|
229
|
+
// Launch the CES sibling alongside the daemon, in parallel — matching the
|
|
230
|
+
// Docker topology. The assistant does not spawn its own CES, so a freshly
|
|
231
|
+
// hatched instance would otherwise come up with CES unavailable.
|
|
232
|
+
// startCes always launches the CES sibling.
|
|
233
|
+
await Promise.all([
|
|
234
|
+
startCes(watch, resources),
|
|
235
|
+
startLocalDaemon(watch, resources, {
|
|
236
|
+
defaultWorkspaceConfigPath,
|
|
237
|
+
signingKey,
|
|
238
|
+
}),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
reporter.progress(4, 6, "Starting gateway...");
|
|
242
|
+
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
243
|
+
try {
|
|
244
|
+
runtimeUrl = await startGateway(watch, resources, {
|
|
245
|
+
signingKey,
|
|
246
|
+
bootstrapSecret,
|
|
247
|
+
envOverrides: flagEnvVars,
|
|
286
248
|
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// Gateway failed — stop the daemon we just started so we don't leave
|
|
251
|
+
// orphaned processes with no lock file entry.
|
|
252
|
+
reporter.error(
|
|
253
|
+
`\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
|
|
254
|
+
);
|
|
255
|
+
await stopLocalProcesses(resources);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
287
258
|
|
|
288
259
|
// Lease a guardian token so the desktop app can import it on first launch
|
|
289
260
|
// instead of hitting /v1/guardian/init itself. Use loopback to satisfy
|
|
290
261
|
// the daemon's local-only check — the mDNS runtimeUrl resolves to a LAN
|
|
291
262
|
// IP which the daemon rejects as non-loopback.
|
|
292
|
-
reporter.progress(
|
|
263
|
+
reporter.progress(5, 6, "Securing connection...");
|
|
264
|
+
const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
293
265
|
const maxLeaseAttempts = 3;
|
|
294
266
|
let guardianAccessToken: string | undefined;
|
|
295
267
|
for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
|
|
@@ -318,6 +290,22 @@ export async function hatchLocal(
|
|
|
318
290
|
}
|
|
319
291
|
}
|
|
320
292
|
|
|
293
|
+
// Auto-start ngrok if webhook integrations (e.g. Telegram, Twilio) are configured.
|
|
294
|
+
const localEntry: AssistantEntry = {
|
|
295
|
+
assistantId: instanceName,
|
|
296
|
+
runtimeUrl,
|
|
297
|
+
localUrl: `http://127.0.0.1:${resources.gatewayPort}`,
|
|
298
|
+
cloud: "local",
|
|
299
|
+
species,
|
|
300
|
+
hatchedAt: new Date().toISOString(),
|
|
301
|
+
resources: { ...resources, signingKey },
|
|
302
|
+
guardianBootstrapSecret: bootstrapSecret,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
reporter.progress(6, 6, "Saving configuration...");
|
|
306
|
+
saveAssistantEntry(localEntry);
|
|
307
|
+
setActiveAssistant(instanceName);
|
|
308
|
+
|
|
321
309
|
if (process.env.VELLUM_DESKTOP_APP) {
|
|
322
310
|
installCLISymlink();
|
|
323
311
|
}
|
package/src/lib/local.ts
CHANGED
|
@@ -548,7 +548,6 @@ export function generateLocalSigningKey(): string {
|
|
|
548
548
|
type DaemonStartOptions = {
|
|
549
549
|
foreground?: boolean;
|
|
550
550
|
defaultWorkspaceConfigPath?: string;
|
|
551
|
-
requireReady?: boolean;
|
|
552
551
|
signingKey?: string;
|
|
553
552
|
};
|
|
554
553
|
|
|
@@ -598,10 +597,7 @@ function applyDaemonEnvOverrides(
|
|
|
598
597
|
applyIpcSocketDirOverride(env);
|
|
599
598
|
}
|
|
600
599
|
|
|
601
|
-
function logDaemonReadiness(
|
|
602
|
-
readiness: DaemonReadiness,
|
|
603
|
-
requireReady = false,
|
|
604
|
-
): void {
|
|
600
|
+
function logDaemonReadiness(readiness: DaemonReadiness): void {
|
|
605
601
|
switch (readiness) {
|
|
606
602
|
case "ready":
|
|
607
603
|
console.log(" Assistant ready\n");
|
|
@@ -617,11 +613,6 @@ function logDaemonReadiness(
|
|
|
617
613
|
);
|
|
618
614
|
break;
|
|
619
615
|
default:
|
|
620
|
-
if (requireReady) {
|
|
621
|
-
throw new Error(
|
|
622
|
-
"Assistant did not bind its local port within 60 seconds.",
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
616
|
console.log(
|
|
626
617
|
" ⚠️ Assistant did not become ready within 60s — continuing anyway\n",
|
|
627
618
|
);
|
|
@@ -1329,7 +1320,6 @@ export async function startLocalDaemon(
|
|
|
1329
1320
|
resources.daemonPort,
|
|
1330
1321
|
Date.now() + 60000,
|
|
1331
1322
|
),
|
|
1332
|
-
options?.requireReady,
|
|
1333
1323
|
);
|
|
1334
1324
|
}
|
|
1335
1325
|
return;
|
|
@@ -1372,7 +1362,6 @@ export async function startLocalDaemon(
|
|
|
1372
1362
|
// classifies it without blocking on an in-flight migration.
|
|
1373
1363
|
logDaemonReadiness(
|
|
1374
1364
|
await probeDaemonReadinessWithRetry(resources.daemonPort),
|
|
1375
|
-
options?.requireReady,
|
|
1376
1365
|
);
|
|
1377
1366
|
return;
|
|
1378
1367
|
}
|
|
@@ -1524,7 +1513,7 @@ export async function startLocalDaemon(
|
|
|
1524
1513
|
readiness = await probeDaemonReadiness(resources.daemonPort);
|
|
1525
1514
|
}
|
|
1526
1515
|
|
|
1527
|
-
logDaemonReadiness(readiness
|
|
1516
|
+
logDaemonReadiness(readiness);
|
|
1528
1517
|
}
|
|
1529
1518
|
} else {
|
|
1530
1519
|
console.log("🔨 Starting local assistant...");
|
|
@@ -1546,7 +1535,6 @@ export async function startLocalDaemon(
|
|
|
1546
1535
|
resources.daemonPort,
|
|
1547
1536
|
Date.now() + 60000,
|
|
1548
1537
|
),
|
|
1549
|
-
options?.requireReady,
|
|
1550
1538
|
);
|
|
1551
1539
|
}
|
|
1552
1540
|
}
|
|
@@ -1559,7 +1547,6 @@ export async function startGateway(
|
|
|
1559
1547
|
signingKey?: string;
|
|
1560
1548
|
bootstrapSecret?: string;
|
|
1561
1549
|
envOverrides?: Record<string, string>;
|
|
1562
|
-
requireReady?: boolean;
|
|
1563
1550
|
},
|
|
1564
1551
|
): Promise<string> {
|
|
1565
1552
|
const effectiveGatewayPort = resources?.gatewayPort ?? GATEWAY_PORT;
|
|
@@ -1689,11 +1676,6 @@ export async function startGateway(
|
|
|
1689
1676
|
// connection-refused errors.
|
|
1690
1677
|
const ready = await waitForDaemonReady(effectiveGatewayPort, 30000);
|
|
1691
1678
|
if (!ready) {
|
|
1692
|
-
if (options?.requireReady) {
|
|
1693
|
-
throw new Error(
|
|
1694
|
-
"Assistant gateway did not bind its local port within 30 seconds.",
|
|
1695
|
-
);
|
|
1696
|
-
}
|
|
1697
1679
|
console.warn(
|
|
1698
1680
|
"⚠ Gateway started but health check did not respond within 30s",
|
|
1699
1681
|
);
|
package/src/lib/nginx-ingress.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { createRequire } from "node:module";
|
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
|
|
19
19
|
import { GATEWAY_PORT } from "./constants.js";
|
|
20
|
-
import { waitForDaemonReady } from "./http-client.js";
|
|
21
20
|
import { loadRawConfig, saveRawConfig } from "./ingress-config.js";
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -541,95 +540,6 @@ export async function stopIngressNginx(workspaceDir: string): Promise<boolean> {
|
|
|
541
540
|
return true;
|
|
542
541
|
}
|
|
543
542
|
|
|
544
|
-
/** Probe budget for confirming the freshly-spawned edge answers /healthz. */
|
|
545
|
-
export const INGRESS_READY_TIMEOUT_MS = 5_000;
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Outcome of an attempt to bring up the remote-web nginx ingress edge. Callers
|
|
549
|
-
* render their own messaging per variant: `nginx-ingress up` prints
|
|
550
|
-
* install/build guidance and exits non-zero, while the wake restore path warns
|
|
551
|
-
* and continues.
|
|
552
|
-
*/
|
|
553
|
-
export type StartRemoteWebIngressResult =
|
|
554
|
-
| {
|
|
555
|
-
status: "started";
|
|
556
|
-
listenPort: number;
|
|
557
|
-
webDistDir: string;
|
|
558
|
-
version: string;
|
|
559
|
-
}
|
|
560
|
-
| { status: "already-running"; listenPort: number }
|
|
561
|
-
| { status: "nginx-missing" }
|
|
562
|
-
| { status: "web-dist-missing" }
|
|
563
|
-
| { status: "unreachable"; listenPort: number; logPath: string };
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Generate the nginx config and start the remote-web ingress edge, then probe
|
|
567
|
-
* /healthz through it to prove the ingress → gateway path is live. A spawned
|
|
568
|
-
* but unreachable nginx is rolled back so a failed attempt leaves no half-up
|
|
569
|
-
* edge behind.
|
|
570
|
-
*
|
|
571
|
-
* Pure mechanism: it performs no console output and never exits the process, so
|
|
572
|
-
* both the `nginx-ingress up` command and the wake restore path can share one
|
|
573
|
-
* implementation and map the returned result to their own UX.
|
|
574
|
-
*/
|
|
575
|
-
export async function startRemoteWebIngress(opts: {
|
|
576
|
-
workspaceDir: string;
|
|
577
|
-
gatewayPort: number;
|
|
578
|
-
listenPort?: number;
|
|
579
|
-
readyTimeoutMs?: number;
|
|
580
|
-
/**
|
|
581
|
-
* Invoked once, after every preflight check passes and immediately before
|
|
582
|
-
* nginx is spawned, so callers can emit their own "starting" progress line
|
|
583
|
-
* with the resolved version/dist/port. Never fires on a preflight bail-out
|
|
584
|
-
* (nginx-missing, already-running, web-dist-missing).
|
|
585
|
-
*/
|
|
586
|
-
onStarting?: (info: {
|
|
587
|
-
version: string;
|
|
588
|
-
webDistDir: string;
|
|
589
|
-
listenPort: number;
|
|
590
|
-
}) => void;
|
|
591
|
-
}): Promise<StartRemoteWebIngressResult> {
|
|
592
|
-
const listenPort = opts.listenPort ?? getNginxIngressPort();
|
|
593
|
-
|
|
594
|
-
const version = getNginxVersion();
|
|
595
|
-
if (!version) {
|
|
596
|
-
return { status: "nginx-missing" };
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (isIngressRunning(opts.workspaceDir)) {
|
|
600
|
-
return { status: "already-running", listenPort };
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const webDistDir = findWebDistDir();
|
|
604
|
-
if (!webDistDir) {
|
|
605
|
-
return { status: "web-dist-missing" };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
opts.onStarting?.({ version, webDistDir, listenPort });
|
|
609
|
-
|
|
610
|
-
const child = startIngressNginx({
|
|
611
|
-
workspaceDir: opts.workspaceDir,
|
|
612
|
-
gatewayPort: opts.gatewayPort,
|
|
613
|
-
listenPort,
|
|
614
|
-
remoteWebIngress: { webDistDir },
|
|
615
|
-
});
|
|
616
|
-
child.unref();
|
|
617
|
-
|
|
618
|
-
// /healthz proxies through nginx to the gateway, so a 200 proves the whole
|
|
619
|
-
// ingress → gateway path works.
|
|
620
|
-
const ready = await waitForDaemonReady(
|
|
621
|
-
listenPort,
|
|
622
|
-
opts.readyTimeoutMs ?? INGRESS_READY_TIMEOUT_MS,
|
|
623
|
-
);
|
|
624
|
-
if (!ready) {
|
|
625
|
-
const { logPath } = getIngressPaths(opts.workspaceDir);
|
|
626
|
-
await stopIngressNginx(opts.workspaceDir);
|
|
627
|
-
return { status: "unreachable", listenPort, logPath };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
return { status: "started", listenPort, webDistDir, version };
|
|
631
|
-
}
|
|
632
|
-
|
|
633
543
|
/**
|
|
634
544
|
* Resolve the local port a tunnel should target: the nginx ingress when it is
|
|
635
545
|
* recorded AND its process is alive, otherwise the gateway port directly
|