@vellumai/cli 0.4.42 → 0.4.43
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 +1 -1
- package/src/__tests__/assistant-config.test.ts +33 -2
- package/src/__tests__/multi-local.test.ts +13 -22
- package/src/__tests__/sleep.test.ts +172 -0
- package/src/commands/client.ts +72 -10
- package/src/commands/hatch.ts +61 -15
- package/src/commands/ps.ts +25 -8
- package/src/commands/recover.ts +17 -8
- package/src/commands/retire.ts +14 -23
- package/src/commands/sleep.ts +88 -16
- package/src/commands/wake.ts +9 -7
- package/src/components/DefaultMainScreen.tsx +3 -83
- package/src/index.ts +0 -3
- package/src/lib/assistant-config.ts +17 -62
- package/src/lib/aws.ts +30 -1
- package/src/lib/docker.ts +319 -0
- package/src/lib/gcp.ts +53 -1
- package/src/lib/http-client.ts +114 -0
- package/src/lib/local.ts +96 -148
- package/src/lib/step-runner.ts +9 -1
- package/src/__tests__/skills-uninstall.test.ts +0 -203
- package/src/commands/skills.ts +0 -514
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
3
|
-
import { hostname,
|
|
3
|
+
import { hostname, userInfo } from "os";
|
|
4
4
|
import { basename } from "path";
|
|
5
5
|
import qrcode from "qrcode-terminal";
|
|
6
6
|
import {
|
|
@@ -145,7 +145,7 @@ interface PendingInteractionsResponse {
|
|
|
145
145
|
pendingSecret: (PendingSecret & { requestId?: string }) | null;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
type TrustDecision = "always_allow" | "
|
|
148
|
+
type TrustDecision = "always_allow" | "always_deny";
|
|
149
149
|
|
|
150
150
|
interface HealthResponse {
|
|
151
151
|
status: string;
|
|
@@ -158,18 +158,13 @@ async function runtimeRequest<T>(
|
|
|
158
158
|
path: string,
|
|
159
159
|
init?: RequestInit,
|
|
160
160
|
bearerToken?: string,
|
|
161
|
-
accessToken?: string,
|
|
162
161
|
): Promise<T> {
|
|
163
162
|
const url = `${baseUrl}/v1/assistants/${assistantId}${path}`;
|
|
164
|
-
// Prefer the JWT access token (from bootstrap) over the shared-secret
|
|
165
|
-
// bearer token. The JWT carries identity claims and is the canonical
|
|
166
|
-
// auth mechanism in the single-header auth model.
|
|
167
|
-
const authToken = accessToken ?? bearerToken;
|
|
168
163
|
const response = await fetch(url, {
|
|
169
164
|
...init,
|
|
170
165
|
headers: {
|
|
171
166
|
"Content-Type": "application/json",
|
|
172
|
-
...(
|
|
167
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
173
168
|
...(init?.headers as Record<string, string> | undefined),
|
|
174
169
|
},
|
|
175
170
|
});
|
|
@@ -205,50 +200,10 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
|
|
|
205
200
|
return response.json() as Promise<HealthResponse>;
|
|
206
201
|
}
|
|
207
202
|
|
|
208
|
-
async function bootstrapAccessToken(
|
|
209
|
-
baseUrl: string,
|
|
210
|
-
bearerToken?: string,
|
|
211
|
-
): Promise<string> {
|
|
212
|
-
if (!bearerToken) {
|
|
213
|
-
throw new Error("Missing bearer token; cannot bootstrap actor identity");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const deviceId = `vellum-cli:${platform()}:${hostname()}:${userInfo().username}`;
|
|
217
|
-
const url = `${baseUrl}/v1/integrations/guardian/vellum/bootstrap`;
|
|
218
|
-
|
|
219
|
-
const response = await fetch(url, {
|
|
220
|
-
method: "POST",
|
|
221
|
-
headers: {
|
|
222
|
-
"Content-Type": "application/json",
|
|
223
|
-
Authorization: `Bearer ${bearerToken}`,
|
|
224
|
-
},
|
|
225
|
-
body: JSON.stringify({ platform: "cli", deviceId }),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
if (!response.ok) {
|
|
229
|
-
const body = await response.text().catch(() => "");
|
|
230
|
-
throw new Error(`HTTP ${response.status}: ${body || response.statusText}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const json: unknown = await response.json();
|
|
234
|
-
|
|
235
|
-
if (typeof json !== "object" || json === null) {
|
|
236
|
-
throw new Error("Invalid bootstrap response from gateway/runtime");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const accessToken = (json as Record<string, unknown>).accessToken;
|
|
240
|
-
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
241
|
-
throw new Error("Invalid bootstrap response from gateway/runtime");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return accessToken;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
203
|
async function pollMessages(
|
|
248
204
|
baseUrl: string,
|
|
249
205
|
assistantId: string,
|
|
250
206
|
bearerToken?: string,
|
|
251
|
-
accessToken?: string,
|
|
252
207
|
): Promise<ListMessagesResponse> {
|
|
253
208
|
const params = new URLSearchParams({ conversationKey: assistantId });
|
|
254
209
|
return runtimeRequest<ListMessagesResponse>(
|
|
@@ -257,7 +212,6 @@ async function pollMessages(
|
|
|
257
212
|
`/messages?${params.toString()}`,
|
|
258
213
|
undefined,
|
|
259
214
|
bearerToken,
|
|
260
|
-
accessToken,
|
|
261
215
|
);
|
|
262
216
|
}
|
|
263
217
|
|
|
@@ -267,7 +221,6 @@ async function sendMessage(
|
|
|
267
221
|
content: string,
|
|
268
222
|
signal?: AbortSignal,
|
|
269
223
|
bearerToken?: string,
|
|
270
|
-
accessToken?: string,
|
|
271
224
|
): Promise<SendMessageResponse> {
|
|
272
225
|
return runtimeRequest<SendMessageResponse>(
|
|
273
226
|
baseUrl,
|
|
@@ -284,7 +237,6 @@ async function sendMessage(
|
|
|
284
237
|
signal,
|
|
285
238
|
},
|
|
286
239
|
bearerToken,
|
|
287
|
-
accessToken,
|
|
288
240
|
);
|
|
289
241
|
}
|
|
290
242
|
|
|
@@ -294,7 +246,6 @@ async function submitDecision(
|
|
|
294
246
|
requestId: string,
|
|
295
247
|
decision: "allow" | "deny",
|
|
296
248
|
bearerToken?: string,
|
|
297
|
-
accessToken?: string,
|
|
298
249
|
): Promise<SubmitDecisionResponse> {
|
|
299
250
|
return runtimeRequest<SubmitDecisionResponse>(
|
|
300
251
|
baseUrl,
|
|
@@ -305,7 +256,6 @@ async function submitDecision(
|
|
|
305
256
|
body: JSON.stringify({ requestId, decision }),
|
|
306
257
|
},
|
|
307
258
|
bearerToken,
|
|
308
|
-
accessToken,
|
|
309
259
|
);
|
|
310
260
|
}
|
|
311
261
|
|
|
@@ -317,7 +267,6 @@ async function addTrustRule(
|
|
|
317
267
|
scope: string,
|
|
318
268
|
decision: "allow" | "deny",
|
|
319
269
|
bearerToken?: string,
|
|
320
|
-
accessToken?: string,
|
|
321
270
|
): Promise<AddTrustRuleResponse> {
|
|
322
271
|
return runtimeRequest<AddTrustRuleResponse>(
|
|
323
272
|
baseUrl,
|
|
@@ -328,7 +277,6 @@ async function addTrustRule(
|
|
|
328
277
|
body: JSON.stringify({ requestId, pattern, scope, decision }),
|
|
329
278
|
},
|
|
330
279
|
bearerToken,
|
|
331
|
-
accessToken,
|
|
332
280
|
);
|
|
333
281
|
}
|
|
334
282
|
|
|
@@ -336,7 +284,6 @@ async function pollPendingInteractions(
|
|
|
336
284
|
baseUrl: string,
|
|
337
285
|
assistantId: string,
|
|
338
286
|
bearerToken?: string,
|
|
339
|
-
accessToken?: string,
|
|
340
287
|
): Promise<PendingInteractionsResponse> {
|
|
341
288
|
const params = new URLSearchParams({ conversationKey: assistantId });
|
|
342
289
|
return runtimeRequest<PendingInteractionsResponse>(
|
|
@@ -345,7 +292,6 @@ async function pollPendingInteractions(
|
|
|
345
292
|
`/pending-interactions?${params.toString()}`,
|
|
346
293
|
undefined,
|
|
347
294
|
bearerToken,
|
|
348
|
-
accessToken,
|
|
349
295
|
);
|
|
350
296
|
}
|
|
351
297
|
|
|
@@ -388,7 +334,6 @@ async function handleConfirmationPrompt(
|
|
|
388
334
|
confirmation: PendingConfirmation,
|
|
389
335
|
chatApp: ChatAppHandle,
|
|
390
336
|
bearerToken?: string,
|
|
391
|
-
accessToken?: string,
|
|
392
337
|
): Promise<void> {
|
|
393
338
|
const preview = formatConfirmationPreview(
|
|
394
339
|
confirmation.toolName,
|
|
@@ -420,7 +365,6 @@ async function handleConfirmationPrompt(
|
|
|
420
365
|
requestId,
|
|
421
366
|
"allow",
|
|
422
367
|
bearerToken,
|
|
423
|
-
accessToken,
|
|
424
368
|
);
|
|
425
369
|
chatApp.addStatus("\u2714 Allowed", "green");
|
|
426
370
|
return;
|
|
@@ -434,7 +378,6 @@ async function handleConfirmationPrompt(
|
|
|
434
378
|
chatApp,
|
|
435
379
|
"always_allow",
|
|
436
380
|
bearerToken,
|
|
437
|
-
accessToken,
|
|
438
381
|
);
|
|
439
382
|
return;
|
|
440
383
|
}
|
|
@@ -447,7 +390,6 @@ async function handleConfirmationPrompt(
|
|
|
447
390
|
chatApp,
|
|
448
391
|
"always_deny",
|
|
449
392
|
bearerToken,
|
|
450
|
-
accessToken,
|
|
451
393
|
);
|
|
452
394
|
return;
|
|
453
395
|
}
|
|
@@ -458,7 +400,6 @@ async function handleConfirmationPrompt(
|
|
|
458
400
|
requestId,
|
|
459
401
|
"deny",
|
|
460
402
|
bearerToken,
|
|
461
|
-
accessToken,
|
|
462
403
|
);
|
|
463
404
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
464
405
|
}
|
|
@@ -471,7 +412,6 @@ async function handlePatternSelection(
|
|
|
471
412
|
chatApp: ChatAppHandle,
|
|
472
413
|
trustDecision: TrustDecision,
|
|
473
414
|
bearerToken?: string,
|
|
474
|
-
accessToken?: string,
|
|
475
415
|
): Promise<void> {
|
|
476
416
|
const allowlistOptions = confirmation.allowlistOptions ?? [];
|
|
477
417
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
@@ -493,7 +433,6 @@ async function handlePatternSelection(
|
|
|
493
433
|
selectedPattern,
|
|
494
434
|
trustDecision,
|
|
495
435
|
bearerToken,
|
|
496
|
-
accessToken,
|
|
497
436
|
);
|
|
498
437
|
return;
|
|
499
438
|
}
|
|
@@ -504,7 +443,6 @@ async function handlePatternSelection(
|
|
|
504
443
|
requestId,
|
|
505
444
|
"deny",
|
|
506
445
|
bearerToken,
|
|
507
|
-
accessToken,
|
|
508
446
|
);
|
|
509
447
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
510
448
|
}
|
|
@@ -518,7 +456,6 @@ async function handleScopeSelection(
|
|
|
518
456
|
selectedPattern: string,
|
|
519
457
|
trustDecision: TrustDecision,
|
|
520
458
|
bearerToken?: string,
|
|
521
|
-
accessToken?: string,
|
|
522
459
|
): Promise<void> {
|
|
523
460
|
const scopeOptions = confirmation.scopeOptions ?? [];
|
|
524
461
|
const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
|
|
@@ -536,7 +473,6 @@ async function handleScopeSelection(
|
|
|
536
473
|
scopeOptions[index].scope,
|
|
537
474
|
ruleDecision,
|
|
538
475
|
bearerToken,
|
|
539
|
-
accessToken,
|
|
540
476
|
);
|
|
541
477
|
await submitDecision(
|
|
542
478
|
baseUrl,
|
|
@@ -544,7 +480,6 @@ async function handleScopeSelection(
|
|
|
544
480
|
requestId,
|
|
545
481
|
ruleDecision === "deny" ? "deny" : "allow",
|
|
546
482
|
bearerToken,
|
|
547
|
-
accessToken,
|
|
548
483
|
);
|
|
549
484
|
const ruleLabel =
|
|
550
485
|
trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
|
|
@@ -562,7 +497,6 @@ async function handleScopeSelection(
|
|
|
562
497
|
requestId,
|
|
563
498
|
"deny",
|
|
564
499
|
bearerToken,
|
|
565
|
-
accessToken,
|
|
566
500
|
);
|
|
567
501
|
chatApp.addStatus("\u2718 Denied", "yellow");
|
|
568
502
|
}
|
|
@@ -1220,7 +1154,6 @@ function ChatApp({
|
|
|
1220
1154
|
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
1221
1155
|
const doctorSessionIdRef = useRef(randomUUID());
|
|
1222
1156
|
const handleRef_ = useRef<ChatAppHandle | null>(null);
|
|
1223
|
-
const accessTokenRef = useRef<string | undefined>(undefined);
|
|
1224
1157
|
|
|
1225
1158
|
const { stdout } = useStdout();
|
|
1226
1159
|
const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
|
|
@@ -1458,12 +1391,6 @@ function ChatApp({
|
|
|
1458
1391
|
|
|
1459
1392
|
try {
|
|
1460
1393
|
const health = await checkHealthRuntime(runtimeUrl);
|
|
1461
|
-
if (!accessTokenRef.current) {
|
|
1462
|
-
accessTokenRef.current = await bootstrapAccessToken(
|
|
1463
|
-
runtimeUrl,
|
|
1464
|
-
bearerToken,
|
|
1465
|
-
);
|
|
1466
|
-
}
|
|
1467
1394
|
h.hideSpinner();
|
|
1468
1395
|
h.updateHealthStatus(health.status);
|
|
1469
1396
|
if (health.status === "healthy" || health.status === "ok") {
|
|
@@ -1486,7 +1413,6 @@ function ChatApp({
|
|
|
1486
1413
|
runtimeUrl,
|
|
1487
1414
|
assistantId,
|
|
1488
1415
|
bearerToken,
|
|
1489
|
-
accessTokenRef.current,
|
|
1490
1416
|
);
|
|
1491
1417
|
h.hideSpinner();
|
|
1492
1418
|
if (historyResponse.messages.length > 0) {
|
|
@@ -1505,7 +1431,6 @@ function ChatApp({
|
|
|
1505
1431
|
runtimeUrl,
|
|
1506
1432
|
assistantId,
|
|
1507
1433
|
bearerToken,
|
|
1508
|
-
accessTokenRef.current,
|
|
1509
1434
|
);
|
|
1510
1435
|
for (const msg of response.messages) {
|
|
1511
1436
|
if (!seenMessageIdsRef.current.has(msg.id)) {
|
|
@@ -1821,7 +1746,6 @@ function ChatApp({
|
|
|
1821
1746
|
trimmed,
|
|
1822
1747
|
controller.signal,
|
|
1823
1748
|
bearerToken,
|
|
1824
|
-
accessTokenRef.current,
|
|
1825
1749
|
);
|
|
1826
1750
|
clearTimeout(timeoutId);
|
|
1827
1751
|
if (!sendResult.accepted) {
|
|
@@ -1853,7 +1777,6 @@ function ChatApp({
|
|
|
1853
1777
|
runtimeUrl,
|
|
1854
1778
|
assistantId,
|
|
1855
1779
|
bearerToken,
|
|
1856
|
-
accessTokenRef.current,
|
|
1857
1780
|
);
|
|
1858
1781
|
|
|
1859
1782
|
if (pending.pendingConfirmation) {
|
|
@@ -1865,7 +1788,6 @@ function ChatApp({
|
|
|
1865
1788
|
pending.pendingConfirmation,
|
|
1866
1789
|
h,
|
|
1867
1790
|
bearerToken,
|
|
1868
|
-
accessTokenRef.current,
|
|
1869
1791
|
);
|
|
1870
1792
|
h.showSpinner("Working...");
|
|
1871
1793
|
continue;
|
|
@@ -1890,7 +1812,6 @@ function ChatApp({
|
|
|
1890
1812
|
}),
|
|
1891
1813
|
},
|
|
1892
1814
|
bearerToken,
|
|
1893
|
-
accessTokenRef.current,
|
|
1894
1815
|
);
|
|
1895
1816
|
},
|
|
1896
1817
|
);
|
|
@@ -1907,7 +1828,6 @@ function ChatApp({
|
|
|
1907
1828
|
runtimeUrl,
|
|
1908
1829
|
assistantId,
|
|
1909
1830
|
bearerToken,
|
|
1910
|
-
accessTokenRef.current,
|
|
1911
1831
|
);
|
|
1912
1832
|
for (const msg of pollResult.messages) {
|
|
1913
1833
|
if (!seenMessageIdsRef.current.has(msg.id)) {
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { pair } from "./commands/pair";
|
|
|
8
8
|
import { ps } from "./commands/ps";
|
|
9
9
|
import { recover } from "./commands/recover";
|
|
10
10
|
import { retire } from "./commands/retire";
|
|
11
|
-
import { skills } from "./commands/skills";
|
|
12
11
|
import { sleep } from "./commands/sleep";
|
|
13
12
|
import { ssh } from "./commands/ssh";
|
|
14
13
|
import { tunnel } from "./commands/tunnel";
|
|
@@ -24,7 +23,6 @@ const commands = {
|
|
|
24
23
|
ps,
|
|
25
24
|
recover,
|
|
26
25
|
retire,
|
|
27
|
-
skills,
|
|
28
26
|
sleep,
|
|
29
27
|
ssh,
|
|
30
28
|
tunnel,
|
|
@@ -58,7 +56,6 @@ async function main() {
|
|
|
58
56
|
);
|
|
59
57
|
console.log(" recover Restore a previously retired local assistant");
|
|
60
58
|
console.log(" retire Delete an assistant instance");
|
|
61
|
-
console.log(" skills Browse and install skills from the Vellum catalog");
|
|
62
59
|
console.log(" sleep Stop the assistant process");
|
|
63
60
|
console.log(" ssh SSH into a remote assistant instance");
|
|
64
61
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
@@ -16,13 +16,8 @@ import { probePort } from "./port-probe.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export interface LocalInstanceResources {
|
|
18
18
|
/**
|
|
19
|
-
* Instance-specific data root
|
|
20
|
-
*
|
|
21
|
-
* `~/.local/share/vellum/assistants/<name>/` (workspace at
|
|
22
|
-
* `~/.local/share/vellum/assistants/<name>/.vellum`).
|
|
23
|
-
* The daemon's `.vellum/` directory lives inside it. Equivalent to
|
|
24
|
-
* `AssistantEntry.baseDataDir` minus the trailing `/.vellum` suffix —
|
|
25
|
-
* `baseDataDir` is kept on the flat entry for legacy lockfile compat.
|
|
19
|
+
* Instance-specific data root at `~/.local/share/vellum/assistants/<name>/`.
|
|
20
|
+
* The daemon's `.vellum/` directory lives inside it.
|
|
26
21
|
*/
|
|
27
22
|
instanceDir: string;
|
|
28
23
|
/** HTTP port for the daemon runtime server */
|
|
@@ -31,8 +26,6 @@ export interface LocalInstanceResources {
|
|
|
31
26
|
gatewayPort: number;
|
|
32
27
|
/** HTTP port for the Qdrant vector store */
|
|
33
28
|
qdrantPort: number;
|
|
34
|
-
/** Absolute path to the Unix domain socket for IPC */
|
|
35
|
-
socketPath: string;
|
|
36
29
|
/** Absolute path to the daemon PID file */
|
|
37
30
|
pidFile: string;
|
|
38
31
|
}
|
|
@@ -43,8 +36,6 @@ export interface AssistantEntry {
|
|
|
43
36
|
/** Loopback URL for same-machine health checks (e.g. `http://127.0.0.1:7831`).
|
|
44
37
|
* Avoids mDNS resolution issues when the machine checks its own gateway. */
|
|
45
38
|
localUrl?: string;
|
|
46
|
-
/** @deprecated Use `resources.instanceDir` for multi-instance entries. Legacy equivalent of `join(instanceDir, ".vellum")`. */
|
|
47
|
-
baseDataDir?: string;
|
|
48
39
|
bearerToken?: string;
|
|
49
40
|
cloud: string;
|
|
50
41
|
instanceId?: string;
|
|
@@ -70,8 +61,13 @@ function getBaseDir(): string {
|
|
|
70
61
|
return process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
71
62
|
}
|
|
72
63
|
|
|
64
|
+
/** The lockfile always lives under the home directory. */
|
|
65
|
+
function getLockfileDir(): string {
|
|
66
|
+
return process.env.VELLUM_LOCKFILE_DIR?.trim() || homedir();
|
|
67
|
+
}
|
|
68
|
+
|
|
73
69
|
function readLockfile(): LockfileData {
|
|
74
|
-
const base =
|
|
70
|
+
const base = getLockfileDir();
|
|
75
71
|
const candidates = [
|
|
76
72
|
join(base, ".vellum.lock.json"),
|
|
77
73
|
join(base, ".vellum.lockfile.json"),
|
|
@@ -92,7 +88,7 @@ function readLockfile(): LockfileData {
|
|
|
92
88
|
}
|
|
93
89
|
|
|
94
90
|
function writeLockfile(data: LockfileData): void {
|
|
95
|
-
const lockfilePath = join(
|
|
91
|
+
const lockfilePath = join(getLockfileDir(), ".vellum.lock.json");
|
|
96
92
|
writeFileSync(lockfilePath, JSON.stringify(data, null, 2) + "\n");
|
|
97
93
|
}
|
|
98
94
|
|
|
@@ -138,9 +134,14 @@ export function removeAssistantEntry(assistantId: string): void {
|
|
|
138
134
|
(e: AssistantEntry) => e.assistantId !== assistantId,
|
|
139
135
|
);
|
|
140
136
|
data.assistants = entries;
|
|
141
|
-
//
|
|
137
|
+
// Reassign active assistant if it matches the removed entry
|
|
142
138
|
if (data.activeAssistant === assistantId) {
|
|
143
|
-
|
|
139
|
+
const remaining = entries[0];
|
|
140
|
+
if (remaining) {
|
|
141
|
+
data.activeAssistant = remaining.assistantId;
|
|
142
|
+
} else {
|
|
143
|
+
delete data.activeAssistant;
|
|
144
|
+
}
|
|
144
145
|
}
|
|
145
146
|
writeLockfile(data);
|
|
146
147
|
}
|
|
@@ -228,19 +229,12 @@ async function findAvailablePort(
|
|
|
228
229
|
|
|
229
230
|
/**
|
|
230
231
|
* Allocate an isolated set of resources for a named local instance.
|
|
231
|
-
*
|
|
232
|
-
* legacy single-instance layout). Subsequent assistants are placed under
|
|
232
|
+
* Each assistant is placed under
|
|
233
233
|
* `~/.local/share/vellum/assistants/<name>/` with scanned ports.
|
|
234
234
|
*/
|
|
235
235
|
export async function allocateLocalResources(
|
|
236
236
|
instanceName: string,
|
|
237
237
|
): Promise<LocalInstanceResources> {
|
|
238
|
-
// First local assistant gets the home directory — identical to legacy layout.
|
|
239
|
-
const existingLocals = loadAllAssistants().filter((e) => e.cloud === "local");
|
|
240
|
-
if (existingLocals.length === 0) {
|
|
241
|
-
return defaultLocalResources();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
238
|
const instanceDir = join(
|
|
245
239
|
homedir(),
|
|
246
240
|
".local",
|
|
@@ -263,13 +257,6 @@ export async function allocateLocalResources(
|
|
|
263
257
|
entry.resources.gatewayPort,
|
|
264
258
|
entry.resources.qdrantPort,
|
|
265
259
|
);
|
|
266
|
-
} else {
|
|
267
|
-
// Legacy entries without resources use the default ports
|
|
268
|
-
reservedPorts.push(
|
|
269
|
-
DEFAULT_DAEMON_PORT,
|
|
270
|
-
DEFAULT_GATEWAY_PORT,
|
|
271
|
-
DEFAULT_QDRANT_PORT,
|
|
272
|
-
);
|
|
273
260
|
}
|
|
274
261
|
}
|
|
275
262
|
|
|
@@ -294,42 +281,10 @@ export async function allocateLocalResources(
|
|
|
294
281
|
daemonPort,
|
|
295
282
|
gatewayPort,
|
|
296
283
|
qdrantPort,
|
|
297
|
-
socketPath: join(instanceDir, ".vellum", "vellum.sock"),
|
|
298
284
|
pidFile: join(instanceDir, ".vellum", "vellum.pid"),
|
|
299
285
|
};
|
|
300
286
|
}
|
|
301
287
|
|
|
302
|
-
/**
|
|
303
|
-
* Return default resources representing the legacy single-instance layout.
|
|
304
|
-
* Used to normalize existing lockfile entries so callers can treat all local
|
|
305
|
-
* entries uniformly.
|
|
306
|
-
*/
|
|
307
|
-
export function defaultLocalResources(): LocalInstanceResources {
|
|
308
|
-
const vellumDir = join(homedir(), ".vellum");
|
|
309
|
-
return {
|
|
310
|
-
instanceDir: homedir(),
|
|
311
|
-
daemonPort: DEFAULT_DAEMON_PORT,
|
|
312
|
-
gatewayPort: DEFAULT_GATEWAY_PORT,
|
|
313
|
-
qdrantPort: DEFAULT_QDRANT_PORT,
|
|
314
|
-
socketPath: join(vellumDir, "vellum.sock"),
|
|
315
|
-
pidFile: join(vellumDir, "vellum.pid"),
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Normalize existing lockfile entries so local entries include resource fields.
|
|
321
|
-
* Remote entries are left untouched. Returns a new array (does not mutate input).
|
|
322
|
-
*/
|
|
323
|
-
export function normalizeExistingEntryResources(
|
|
324
|
-
entries: AssistantEntry[],
|
|
325
|
-
): AssistantEntry[] {
|
|
326
|
-
return entries.map((entry) => {
|
|
327
|
-
if (entry.cloud !== "local") return entry;
|
|
328
|
-
if (entry.resources) return entry;
|
|
329
|
-
return { ...entry, resources: defaultLocalResources() };
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
288
|
/**
|
|
334
289
|
* Read the assistant config file and sync client-relevant values to the
|
|
335
290
|
* lockfile. This lets external tools (e.g. vel) discover the platform URL
|
package/src/lib/aws.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from "path";
|
|
|
5
5
|
|
|
6
6
|
import { buildStartupScript, watchHatching } from "../commands/hatch";
|
|
7
7
|
import type { PollResult } from "../commands/hatch";
|
|
8
|
-
import { saveAssistantEntry } from "./assistant-config";
|
|
8
|
+
import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
9
9
|
import type { AssistantEntry } from "./assistant-config";
|
|
10
10
|
import { GATEWAY_PORT } from "./constants";
|
|
11
11
|
import type { Species } from "./constants";
|
|
@@ -370,6 +370,28 @@ async function pollAwsInstance(
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
async function fetchRemoteBearerToken(
|
|
374
|
+
ip: string,
|
|
375
|
+
keyPath: string,
|
|
376
|
+
): Promise<string | null> {
|
|
377
|
+
try {
|
|
378
|
+
const remoteCmd =
|
|
379
|
+
'cat ~/.vellum.lock.json 2>/dev/null || cat ~/.vellum.lockfile.json 2>/dev/null || echo "{}"';
|
|
380
|
+
const output = await awsSshExec(ip, keyPath, remoteCmd);
|
|
381
|
+
const data = JSON.parse(output.trim());
|
|
382
|
+
const assistants = data.assistants;
|
|
383
|
+
if (Array.isArray(assistants) && assistants.length > 0) {
|
|
384
|
+
const token = assistants[0].bearerToken;
|
|
385
|
+
if (typeof token === "string" && token) {
|
|
386
|
+
return token;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
373
395
|
export async function hatchAws(
|
|
374
396
|
species: Species,
|
|
375
397
|
detached: boolean,
|
|
@@ -500,6 +522,7 @@ export async function hatchAws(
|
|
|
500
522
|
hatchedAt: new Date().toISOString(),
|
|
501
523
|
};
|
|
502
524
|
saveAssistantEntry(awsEntry);
|
|
525
|
+
setActiveAssistant(instanceName);
|
|
503
526
|
|
|
504
527
|
if (detached) {
|
|
505
528
|
console.log("\u{1F680} Startup script is running on the instance...");
|
|
@@ -535,6 +558,12 @@ export async function hatchAws(
|
|
|
535
558
|
}
|
|
536
559
|
process.exit(1);
|
|
537
560
|
}
|
|
561
|
+
|
|
562
|
+
const remoteBearerToken = await fetchRemoteBearerToken(ip, keyPath);
|
|
563
|
+
if (remoteBearerToken) {
|
|
564
|
+
awsEntry.bearerToken = remoteBearerToken;
|
|
565
|
+
saveAssistantEntry(awsEntry);
|
|
566
|
+
}
|
|
538
567
|
} else {
|
|
539
568
|
console.log(
|
|
540
569
|
"\u26a0\ufe0f No external IP available for monitoring. Instance is still running.",
|