@vellumai/cli 0.4.6 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -28,17 +28,42 @@ ensure_git() {
28
28
  # Try installing CLT first before falling back to Homebrew.
29
29
  if ! xcode-select -p >/dev/null 2>&1; then
30
30
  info "Installing Xcode Command Line Tools (includes git)..."
31
- xcode-select --install 2>/dev/null || true
32
- info "Please follow the on-screen dialog to install. Waiting..."
33
- local waited=0
34
- while ! xcode-select -p >/dev/null 2>&1; do
35
- sleep 5
36
- waited=$((waited + 5))
37
- if [ "$waited" -ge 600 ]; then
38
- error "Timed out waiting for Xcode Command Line Tools. Please install manually and re-run."
39
- exit 1
40
- fi
41
- done
31
+
32
+ # Use softwareupdate to install CLT non-interactively instead of
33
+ # xcode-select --install which opens a GUI dialog requiring manual
34
+ # confirmation.
35
+ touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
36
+ local clt_package
37
+ clt_package=$(softwareupdate -l 2>/dev/null \
38
+ | grep -o '.*Command Line Tools.*' \
39
+ | grep -v "^\*" \
40
+ | sed 's/^ *//' \
41
+ | sort -V \
42
+ | tail -1)
43
+
44
+ if [ -n "$clt_package" ]; then
45
+ info "Found package: $clt_package"
46
+ softwareupdate -i "$clt_package" --verbose 2>&1 | while IFS= read -r line; do
47
+ printf " %s\n" "$line"
48
+ done
49
+ else
50
+ # Fallback: if softwareupdate can't find the package, try
51
+ # xcode-select --install and wait for user interaction.
52
+ info "Could not find CLT package via softwareupdate, falling back to xcode-select..."
53
+ xcode-select --install 2>/dev/null || true
54
+ info "Please follow the on-screen dialog to install. Waiting..."
55
+ local waited=0
56
+ while ! xcode-select -p >/dev/null 2>&1; do
57
+ sleep 5
58
+ waited=$((waited + 5))
59
+ if [ "$waited" -ge 600 ]; then
60
+ error "Timed out waiting for Xcode Command Line Tools. Please install manually and re-run."
61
+ exit 1
62
+ fi
63
+ done
64
+ fi
65
+
66
+ rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
42
67
  hash -r 2>/dev/null || true
43
68
  fi
44
69
 
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { createHash, randomBytes, randomUUID } from "crypto";
3
- import { hostname, userInfo } from "os";
3
+ import { hostname, platform, userInfo } from "os";
4
4
  import { basename } from "path";
5
5
  import qrcode from "qrcode-terminal";
6
6
  import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
@@ -135,6 +135,7 @@ async function runtimeRequest<T>(
135
135
  path: string,
136
136
  init?: RequestInit,
137
137
  bearerToken?: string,
138
+ actorToken?: string,
138
139
  ): Promise<T> {
139
140
  const url = `${baseUrl}/v1/assistants/${assistantId}${path}`;
140
141
  const response = await fetch(url, {
@@ -142,6 +143,7 @@ async function runtimeRequest<T>(
142
143
  headers: {
143
144
  "Content-Type": "application/json",
144
145
  ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
146
+ ...(actorToken ? { "X-Actor-Token": actorToken } : {}),
145
147
  ...(init?.headers as Record<string, string> | undefined),
146
148
  },
147
149
  });
@@ -177,10 +179,47 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
177
179
  return response.json() as Promise<HealthResponse>;
178
180
  }
179
181
 
182
+ async function bootstrapActorToken(baseUrl: string, bearerToken?: string): Promise<string> {
183
+ if (!bearerToken) {
184
+ throw new Error("Missing bearer token; cannot bootstrap actor identity");
185
+ }
186
+
187
+ const deviceId = `vellum-cli:${platform()}:${hostname()}:${userInfo().username}`;
188
+ const url = `${baseUrl}/v1/integrations/guardian/vellum/bootstrap`;
189
+
190
+ const response = await fetch(url, {
191
+ method: "POST",
192
+ headers: {
193
+ "Content-Type": "application/json",
194
+ Authorization: `Bearer ${bearerToken}`,
195
+ },
196
+ body: JSON.stringify({ platform: "cli", deviceId }),
197
+ });
198
+
199
+ if (!response.ok) {
200
+ const body = await response.text().catch(() => "");
201
+ throw new Error(`HTTP ${response.status}: ${body || response.statusText}`);
202
+ }
203
+
204
+ const json: unknown = await response.json();
205
+
206
+ if (typeof json !== "object" || json === null) {
207
+ throw new Error("Invalid bootstrap response from gateway/runtime");
208
+ }
209
+
210
+ const actorToken = (json as Record<string, unknown>).actorToken;
211
+ if (typeof actorToken !== "string" || actorToken.length === 0) {
212
+ throw new Error("Invalid bootstrap response from gateway/runtime");
213
+ }
214
+
215
+ return actorToken;
216
+ }
217
+
180
218
  async function pollMessages(
181
219
  baseUrl: string,
182
220
  assistantId: string,
183
221
  bearerToken?: string,
222
+ actorToken?: string,
184
223
  ): Promise<ListMessagesResponse> {
185
224
  const params = new URLSearchParams({ conversationKey: assistantId });
186
225
  return runtimeRequest<ListMessagesResponse>(
@@ -189,6 +228,7 @@ async function pollMessages(
189
228
  `/messages?${params.toString()}`,
190
229
  undefined,
191
230
  bearerToken,
231
+ actorToken,
192
232
  );
193
233
  }
194
234
 
@@ -198,6 +238,7 @@ async function sendMessage(
198
238
  content: string,
199
239
  signal?: AbortSignal,
200
240
  bearerToken?: string,
241
+ actorToken?: string,
201
242
  ): Promise<SendMessageResponse> {
202
243
  return runtimeRequest<SendMessageResponse>(
203
244
  baseUrl,
@@ -209,6 +250,7 @@ async function sendMessage(
209
250
  signal,
210
251
  },
211
252
  bearerToken,
253
+ actorToken,
212
254
  );
213
255
  }
214
256
 
@@ -218,6 +260,7 @@ async function submitDecision(
218
260
  requestId: string,
219
261
  decision: "allow" | "deny",
220
262
  bearerToken?: string,
263
+ actorToken?: string,
221
264
  ): Promise<SubmitDecisionResponse> {
222
265
  return runtimeRequest<SubmitDecisionResponse>(
223
266
  baseUrl,
@@ -228,6 +271,7 @@ async function submitDecision(
228
271
  body: JSON.stringify({ requestId, decision }),
229
272
  },
230
273
  bearerToken,
274
+ actorToken,
231
275
  );
232
276
  }
233
277
 
@@ -239,6 +283,7 @@ async function addTrustRule(
239
283
  scope: string,
240
284
  decision: "allow" | "deny",
241
285
  bearerToken?: string,
286
+ actorToken?: string,
242
287
  ): Promise<AddTrustRuleResponse> {
243
288
  return runtimeRequest<AddTrustRuleResponse>(
244
289
  baseUrl,
@@ -249,6 +294,7 @@ async function addTrustRule(
249
294
  body: JSON.stringify({ requestId, pattern, scope, decision }),
250
295
  },
251
296
  bearerToken,
297
+ actorToken,
252
298
  );
253
299
  }
254
300
 
@@ -256,6 +302,7 @@ async function pollPendingInteractions(
256
302
  baseUrl: string,
257
303
  assistantId: string,
258
304
  bearerToken?: string,
305
+ actorToken?: string,
259
306
  ): Promise<PendingInteractionsResponse> {
260
307
  const params = new URLSearchParams({ conversationKey: assistantId });
261
308
  return runtimeRequest<PendingInteractionsResponse>(
@@ -264,6 +311,7 @@ async function pollPendingInteractions(
264
311
  `/pending-interactions?${params.toString()}`,
265
312
  undefined,
266
313
  bearerToken,
314
+ actorToken,
267
315
  );
268
316
  }
269
317
 
@@ -301,6 +349,7 @@ async function handleConfirmationPrompt(
301
349
  confirmation: PendingConfirmation,
302
350
  chatApp: ChatAppHandle,
303
351
  bearerToken?: string,
352
+ actorToken?: string,
304
353
  ): Promise<void> {
305
354
  const preview = formatConfirmationPreview(confirmation.toolName, confirmation.input);
306
355
  const allowlistOptions = confirmation.allowlistOptions ?? [];
@@ -320,7 +369,7 @@ async function handleConfirmationPrompt(
320
369
  const index = await chatApp.showSelection("Tool Approval", options);
321
370
 
322
371
  if (index === 0) {
323
- await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken);
372
+ await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken, actorToken);
324
373
  chatApp.addStatus("\u2714 Allowed", "green");
325
374
  return;
326
375
  }
@@ -333,6 +382,7 @@ async function handleConfirmationPrompt(
333
382
  chatApp,
334
383
  "always_allow",
335
384
  bearerToken,
385
+ actorToken,
336
386
  );
337
387
  return;
338
388
  }
@@ -345,11 +395,12 @@ async function handleConfirmationPrompt(
345
395
  chatApp,
346
396
  "always_deny",
347
397
  bearerToken,
398
+ actorToken,
348
399
  );
349
400
  return;
350
401
  }
351
402
 
352
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
403
+ await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
353
404
  chatApp.addStatus("\u2718 Denied", "yellow");
354
405
  }
355
406
 
@@ -361,6 +412,7 @@ async function handlePatternSelection(
361
412
  chatApp: ChatAppHandle,
362
413
  trustDecision: TrustDecision,
363
414
  bearerToken?: string,
415
+ actorToken?: string,
364
416
  ): Promise<void> {
365
417
  const allowlistOptions = confirmation.allowlistOptions ?? [];
366
418
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -379,11 +431,12 @@ async function handlePatternSelection(
379
431
  selectedPattern,
380
432
  trustDecision,
381
433
  bearerToken,
434
+ actorToken,
382
435
  );
383
436
  return;
384
437
  }
385
438
 
386
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
439
+ await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
387
440
  chatApp.addStatus("\u2718 Denied", "yellow");
388
441
  }
389
442
 
@@ -396,6 +449,7 @@ async function handleScopeSelection(
396
449
  selectedPattern: string,
397
450
  trustDecision: TrustDecision,
398
451
  bearerToken?: string,
452
+ actorToken?: string,
399
453
  ): Promise<void> {
400
454
  const scopeOptions = confirmation.scopeOptions ?? [];
401
455
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
@@ -413,6 +467,7 @@ async function handleScopeSelection(
413
467
  scopeOptions[index].scope,
414
468
  ruleDecision,
415
469
  bearerToken,
470
+ actorToken,
416
471
  );
417
472
  await submitDecision(
418
473
  baseUrl,
@@ -420,6 +475,7 @@ async function handleScopeSelection(
420
475
  requestId,
421
476
  ruleDecision === "deny" ? "deny" : "allow",
422
477
  bearerToken,
478
+ actorToken,
423
479
  );
424
480
  const ruleLabel = trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
425
481
  const ruleColor = trustDecision === "always_deny" ? "yellow" : "green";
@@ -430,7 +486,7 @@ async function handleScopeSelection(
430
486
  return;
431
487
  }
432
488
 
433
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken);
489
+ await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, actorToken);
434
490
  chatApp.addStatus("\u2718 Denied", "yellow");
435
491
  }
436
492
 
@@ -1025,6 +1081,7 @@ function ChatApp({
1025
1081
  const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
1026
1082
  const doctorSessionIdRef = useRef(randomUUID());
1027
1083
  const handleRef_ = useRef<ChatAppHandle | null>(null);
1084
+ const actorTokenRef = useRef<string | undefined>(undefined);
1028
1085
 
1029
1086
  const { stdout } = useStdout();
1030
1087
  const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
@@ -1250,6 +1307,9 @@ function ChatApp({
1250
1307
 
1251
1308
  try {
1252
1309
  const health = await checkHealthRuntime(runtimeUrl);
1310
+ if (!actorTokenRef.current) {
1311
+ actorTokenRef.current = await bootstrapActorToken(runtimeUrl, bearerToken);
1312
+ }
1253
1313
  h.hideSpinner();
1254
1314
  h.updateHealthStatus(health.status);
1255
1315
  if (health.status === "healthy" || health.status === "ok") {
@@ -1265,7 +1325,12 @@ function ChatApp({
1265
1325
  h.showSpinner("Loading conversation history...");
1266
1326
 
1267
1327
  try {
1268
- const historyResponse = await pollMessages(runtimeUrl, assistantId, bearerToken);
1328
+ const historyResponse = await pollMessages(
1329
+ runtimeUrl,
1330
+ assistantId,
1331
+ bearerToken,
1332
+ actorTokenRef.current,
1333
+ );
1269
1334
  h.hideSpinner();
1270
1335
  if (historyResponse.messages.length > 0) {
1271
1336
  for (const msg of historyResponse.messages) {
@@ -1279,7 +1344,12 @@ function ChatApp({
1279
1344
 
1280
1345
  pollTimerRef.current = setInterval(async () => {
1281
1346
  try {
1282
- const response = await pollMessages(runtimeUrl, assistantId, bearerToken);
1347
+ const response = await pollMessages(
1348
+ runtimeUrl,
1349
+ assistantId,
1350
+ bearerToken,
1351
+ actorTokenRef.current,
1352
+ );
1283
1353
  for (const msg of response.messages) {
1284
1354
  if (!seenMessageIdsRef.current.has(msg.id)) {
1285
1355
  seenMessageIdsRef.current.add(msg.id);
@@ -1296,11 +1366,12 @@ function ChatApp({
1296
1366
  connectedRef.current = true;
1297
1367
  connectingRef.current = false;
1298
1368
  return true;
1299
- } catch {
1369
+ } catch (err) {
1300
1370
  h.hideSpinner();
1301
1371
  connectingRef.current = false;
1302
1372
  h.updateHealthStatus("unreachable");
1303
- h.addStatus(`${statusEmoji("unreachable")} Failed to connect: Timeout`, "red");
1373
+ const msg = err instanceof Error ? err.message : String(err);
1374
+ h.addStatus(`${statusEmoji("unreachable")} Failed to connect: ${msg}`, "red");
1304
1375
  return false;
1305
1376
  }
1306
1377
  }, [runtimeUrl, assistantId, bearerToken]);
@@ -1561,6 +1632,7 @@ function ChatApp({
1561
1632
  trimmed,
1562
1633
  controller.signal,
1563
1634
  bearerToken,
1635
+ actorTokenRef.current,
1564
1636
  );
1565
1637
  clearTimeout(timeoutId);
1566
1638
  if (!sendResult.accepted) {
@@ -1586,7 +1658,12 @@ function ChatApp({
1586
1658
 
1587
1659
  // Check for pending confirmations/secrets
1588
1660
  try {
1589
- const pending = await pollPendingInteractions(runtimeUrl, assistantId, bearerToken);
1661
+ const pending = await pollPendingInteractions(
1662
+ runtimeUrl,
1663
+ assistantId,
1664
+ bearerToken,
1665
+ actorTokenRef.current,
1666
+ );
1590
1667
 
1591
1668
  if (pending.pendingConfirmation) {
1592
1669
  h.hideSpinner();
@@ -1597,6 +1674,7 @@ function ChatApp({
1597
1674
  pending.pendingConfirmation,
1598
1675
  h,
1599
1676
  bearerToken,
1677
+ actorTokenRef.current,
1600
1678
  );
1601
1679
  h.showSpinner("Working...");
1602
1680
  continue;
@@ -1615,6 +1693,7 @@ function ChatApp({
1615
1693
  body: JSON.stringify({ requestId: secretRequestId, value, delivery }),
1616
1694
  },
1617
1695
  bearerToken,
1696
+ actorTokenRef.current,
1618
1697
  );
1619
1698
  });
1620
1699
  h.showSpinner("Working...");
@@ -1626,7 +1705,12 @@ function ChatApp({
1626
1705
 
1627
1706
  // Poll for new messages to detect completion
1628
1707
  try {
1629
- const pollResult = await pollMessages(runtimeUrl, assistantId, bearerToken);
1708
+ const pollResult = await pollMessages(
1709
+ runtimeUrl,
1710
+ assistantId,
1711
+ bearerToken,
1712
+ actorTokenRef.current,
1713
+ );
1630
1714
  for (const msg of pollResult.messages) {
1631
1715
  if (!seenMessageIdsRef.current.has(msg.id)) {
1632
1716
  seenMessageIdsRef.current.add(msg.id);