@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.
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { createHash, randomBytes, randomUUID } from "crypto";
3
- import { hostname, platform, userInfo } from "os";
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" | "always_allow_high_risk" | "always_deny";
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
- ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
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. The first local assistant uses `~` (workspace
20
- * at `~/.vellum`); subsequent assistants use
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 = getBaseDir();
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(getBaseDir(), ".vellum.lock.json");
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
- // Clear active assistant if it matches the removed entry
137
+ // Reassign active assistant if it matches the removed entry
142
138
  if (data.activeAssistant === assistantId) {
143
- delete data.activeAssistant;
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
- * The first local assistant gets `instanceDir = ~` with default ports (same as
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.",