@vellumai/cli 0.4.25 → 0.4.29

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.
@@ -3,7 +3,14 @@ import { createHash, randomBytes, randomUUID } from "crypto";
3
3
  import { hostname, platform, userInfo } from "os";
4
4
  import { basename } from "path";
5
5
  import qrcode from "qrcode-terminal";
6
- import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react";
6
+ import {
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ReactElement,
13
+ } from "react";
7
14
  import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
8
15
 
9
16
  import { removeAssistantEntry } from "../lib/assistant-config";
@@ -26,7 +33,16 @@ export const ANSI = {
26
33
  gray: "\x1b[90m",
27
34
  } as const;
28
35
 
29
- export const SLASH_COMMANDS = ["/clear", "/doctor", "/exit", "/help", "/pair", "/q", "/quit", "/retire"];
36
+ export const SLASH_COMMANDS = [
37
+ "/clear",
38
+ "/doctor",
39
+ "/exit",
40
+ "/help",
41
+ "/pair",
42
+ "/q",
43
+ "/quit",
44
+ "/retire",
45
+ ];
30
46
 
31
47
  const POLL_INTERVAL_MS = 3000;
32
48
  const SEND_TIMEOUT_MS = 5000;
@@ -45,17 +61,24 @@ const LEFT_HEADER_LINES = 3; // spacer + heading + spacer
45
61
  const LEFT_FOOTER_LINES = 3; // spacer + runtimeUrl + dirName
46
62
 
47
63
  // Right panel structure
48
- const TIPS = ["Send a message to start chatting", "Use /help to see available commands"];
64
+ const TIPS = [
65
+ "Send a message to start chatting",
66
+ "Use /help to see available commands",
67
+ ];
49
68
  const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant, Species, Status — each with heading + value
50
69
  const RIGHT_PANEL_SPACERS = 2; // top spacer + spacer between tips and info
51
70
  const RIGHT_PANEL_TIPS_HEADING = 1;
52
71
  const RIGHT_PANEL_LINE_COUNT =
53
- RIGHT_PANEL_SPACERS + RIGHT_PANEL_TIPS_HEADING + TIPS.length + RIGHT_PANEL_INFO_SECTIONS * 2;
72
+ RIGHT_PANEL_SPACERS +
73
+ RIGHT_PANEL_TIPS_HEADING +
74
+ TIPS.length +
75
+ RIGHT_PANEL_INFO_SECTIONS * 2;
54
76
 
55
77
  // Header chrome (borders around panel content)
56
78
  const HEADER_TOP_BORDER_LINES = 1; // "── Vellum ───..." line
57
79
  const HEADER_BOTTOM_BORDER_LINES = 2; // bottom rule + blank line
58
- const HEADER_CHROME_LINES = HEADER_TOP_BORDER_LINES + HEADER_BOTTOM_BORDER_LINES;
80
+ const HEADER_CHROME_LINES =
81
+ HEADER_TOP_BORDER_LINES + HEADER_BOTTOM_BORDER_LINES;
59
82
 
60
83
  // Selection / Secret windows
61
84
  const DIALOG_WINDOW_WIDTH = 60;
@@ -182,7 +205,10 @@ async function checkHealthRuntime(baseUrl: string): Promise<HealthResponse> {
182
205
  return response.json() as Promise<HealthResponse>;
183
206
  }
184
207
 
185
- async function bootstrapAccessToken(baseUrl: string, bearerToken?: string): Promise<string> {
208
+ async function bootstrapAccessToken(
209
+ baseUrl: string,
210
+ bearerToken?: string,
211
+ ): Promise<string> {
186
212
  if (!bearerToken) {
187
213
  throw new Error("Missing bearer token; cannot bootstrap actor identity");
188
214
  }
@@ -249,7 +275,12 @@ async function sendMessage(
249
275
  "/messages",
250
276
  {
251
277
  method: "POST",
252
- body: JSON.stringify({ conversationKey: assistantId, content, sourceChannel: "vellum", interface: "cli" }),
278
+ body: JSON.stringify({
279
+ conversationKey: assistantId,
280
+ content,
281
+ sourceChannel: "vellum",
282
+ interface: "cli",
283
+ }),
253
284
  signal,
254
285
  },
255
286
  bearerToken,
@@ -318,7 +349,10 @@ async function pollPendingInteractions(
318
349
  );
319
350
  }
320
351
 
321
- function formatConfirmationPreview(toolName: string, input: Record<string, unknown>): string {
352
+ function formatConfirmationPreview(
353
+ toolName: string,
354
+ input: Record<string, unknown>,
355
+ ): string {
322
356
  switch (toolName) {
323
357
  case "bash":
324
358
  return String(input.command ?? "");
@@ -333,7 +367,9 @@ function formatConfirmationPreview(toolName: string, input: Record<string, unkno
333
367
  case "browser_navigate":
334
368
  return `navigate ${String(input.url ?? "").slice(0, 80)}`;
335
369
  case "browser_close":
336
- return input.close_all_pages ? "close all browser pages" : "close browser page";
370
+ return input.close_all_pages
371
+ ? "close all browser pages"
372
+ : "close browser page";
337
373
  case "browser_click":
338
374
  return `click ${input.element_id ?? input.selector ?? ""}`;
339
375
  case "browser_type":
@@ -354,7 +390,10 @@ async function handleConfirmationPrompt(
354
390
  bearerToken?: string,
355
391
  accessToken?: string,
356
392
  ): Promise<void> {
357
- const preview = formatConfirmationPreview(confirmation.toolName, confirmation.input);
393
+ const preview = formatConfirmationPreview(
394
+ confirmation.toolName,
395
+ confirmation.input,
396
+ );
358
397
  const allowlistOptions = confirmation.allowlistOptions ?? [];
359
398
 
360
399
  chatApp.addStatus(`\u250C ${confirmation.toolName}: ${preview}`);
@@ -365,14 +404,24 @@ async function handleConfirmationPrompt(
365
404
  chatApp.addStatus("\u2514");
366
405
 
367
406
  const options = ["Allow once", "Deny once"];
368
- if (allowlistOptions.length > 0 && confirmation.persistentDecisionsAllowed !== false) {
407
+ if (
408
+ allowlistOptions.length > 0 &&
409
+ confirmation.persistentDecisionsAllowed !== false
410
+ ) {
369
411
  options.push("Allowlist...", "Denylist...");
370
412
  }
371
413
 
372
414
  const index = await chatApp.showSelection("Tool Approval", options);
373
415
 
374
416
  if (index === 0) {
375
- await submitDecision(baseUrl, assistantId, requestId, "allow", bearerToken, accessToken);
417
+ await submitDecision(
418
+ baseUrl,
419
+ assistantId,
420
+ requestId,
421
+ "allow",
422
+ bearerToken,
423
+ accessToken,
424
+ );
376
425
  chatApp.addStatus("\u2714 Allowed", "green");
377
426
  return;
378
427
  }
@@ -403,7 +452,14 @@ async function handleConfirmationPrompt(
403
452
  return;
404
453
  }
405
454
 
406
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, accessToken);
455
+ await submitDecision(
456
+ baseUrl,
457
+ assistantId,
458
+ requestId,
459
+ "deny",
460
+ bearerToken,
461
+ accessToken,
462
+ );
407
463
  chatApp.addStatus("\u2718 Denied", "yellow");
408
464
  }
409
465
 
@@ -421,7 +477,10 @@ async function handlePatternSelection(
421
477
  const label = trustDecision === "always_deny" ? "Denylist" : "Allowlist";
422
478
  const options = allowlistOptions.map((o) => o.label);
423
479
 
424
- const index = await chatApp.showSelection(`${label}: choose command pattern`, options);
480
+ const index = await chatApp.showSelection(
481
+ `${label}: choose command pattern`,
482
+ options,
483
+ );
425
484
 
426
485
  if (index >= 0 && index < allowlistOptions.length) {
427
486
  const selectedPattern = allowlistOptions[index].pattern;
@@ -439,7 +498,14 @@ async function handlePatternSelection(
439
498
  return;
440
499
  }
441
500
 
442
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, accessToken);
501
+ await submitDecision(
502
+ baseUrl,
503
+ assistantId,
504
+ requestId,
505
+ "deny",
506
+ bearerToken,
507
+ accessToken,
508
+ );
443
509
  chatApp.addStatus("\u2718 Denied", "yellow");
444
510
  }
445
511
 
@@ -480,7 +546,8 @@ async function handleScopeSelection(
480
546
  bearerToken,
481
547
  accessToken,
482
548
  );
483
- const ruleLabel = trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
549
+ const ruleLabel =
550
+ trustDecision === "always_deny" ? "Denylisted" : "Allowlisted";
484
551
  const ruleColor = trustDecision === "always_deny" ? "yellow" : "green";
485
552
  chatApp.addStatus(
486
553
  `${trustDecision === "always_deny" ? "\u2718" : "\u2714"} ${ruleLabel}`,
@@ -489,7 +556,14 @@ async function handleScopeSelection(
489
556
  return;
490
557
  }
491
558
 
492
- await submitDecision(baseUrl, assistantId, requestId, "deny", bearerToken, accessToken);
559
+ await submitDecision(
560
+ baseUrl,
561
+ assistantId,
562
+ requestId,
563
+ "deny",
564
+ bearerToken,
565
+ accessToken,
566
+ );
493
567
  chatApp.addStatus("\u2718 Denied", "yellow");
494
568
  }
495
569
 
@@ -582,7 +656,8 @@ function ToolCallDisplay({ tc }: ToolCallDisplayProps): ReactElement {
582
656
  : null}
583
657
  {tc.result !== undefined ? (
584
658
  <Text dimColor>
585
- {"\u2502"} <Text color={statusColor}>{statusIcon}</Text> {truncateValue(tc.result, 70)}
659
+ {"\u2502"} <Text color={statusColor}>{statusIcon}</Text>{" "}
660
+ {truncateValue(tc.result, 70)}
586
661
  </Text>
587
662
  ) : null}
588
663
  <Text dimColor>{"\u2514"}</Text>
@@ -667,7 +742,9 @@ function SpinnerDisplay({ text }: { text: string }): ReactElement {
667
742
 
668
743
  export function renderErrorMainScreen(error: unknown): number {
669
744
  const msg = error instanceof Error ? error.message : String(error);
670
- console.log(`${ANSI.red}${ANSI.bold}Failed to render MainWindow${ANSI.reset}`);
745
+ console.log(
746
+ `${ANSI.red}${ANSI.bold}Failed to render MainWindow${ANSI.reset}`,
747
+ );
671
748
  console.log(`${ANSI.dim}${msg}${ANSI.reset}`);
672
749
  console.log(`${ANSI.dim}Run /clear to retry${ANSI.reset}`);
673
750
  return 3;
@@ -733,7 +810,10 @@ function DefaultMainScreen({
733
810
 
734
811
  return (
735
812
  <Box flexDirection="column" width={totalWidth}>
736
- <Text dimColor>{HEADER_PREFIX + "─".repeat(Math.max(0, totalWidth - HEADER_PREFIX.length))}</Text>
813
+ <Text dimColor>
814
+ {HEADER_PREFIX +
815
+ "─".repeat(Math.max(0, totalWidth - HEADER_PREFIX.length))}
816
+ </Text>
737
817
  <Box flexDirection="row">
738
818
  <Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
739
819
  {Array.from({ length: maxLines }, (_, i) => {
@@ -790,7 +870,6 @@ function DefaultMainScreen({
790
870
  );
791
871
  }
792
872
 
793
-
794
873
  export interface SelectionRequest {
795
874
  title: string;
796
875
  options: string[];
@@ -817,7 +896,12 @@ interface ErrorLine {
817
896
  text: string;
818
897
  }
819
898
 
820
- type FeedItem = RuntimeMessage | StatusLine | SpinnerLine | HelpLine | ErrorLine;
899
+ type FeedItem =
900
+ | RuntimeMessage
901
+ | StatusLine
902
+ | SpinnerLine
903
+ | HelpLine
904
+ | ErrorLine;
821
905
 
822
906
  function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
823
907
  return "role" in item;
@@ -833,8 +917,13 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
833
917
  if (item.role === "assistant" && item.toolCalls) {
834
918
  for (const tc of item.toolCalls) {
835
919
  const paramCount =
836
- typeof tc.input === "object" && tc.input ? Object.keys(tc.input).length : 0;
837
- lines += TOOL_CALL_CHROME_LINES + paramCount + (tc.result !== undefined ? 1 : 0);
920
+ typeof tc.input === "object" && tc.input
921
+ ? Object.keys(tc.input).length
922
+ : 0;
923
+ lines +=
924
+ TOOL_CALL_CHROME_LINES +
925
+ paramCount +
926
+ (tc.result !== undefined ? 1 : 0);
838
927
  }
839
928
  }
840
929
  return lines + MESSAGE_SPACING;
@@ -863,7 +952,11 @@ function calculateHeaderHeight(species: Species): number {
863
952
 
864
953
  const SCROLL_STEP = 5;
865
954
 
866
- export function render(runtimeUrl: string, assistantId: string, species: Species): number {
955
+ export function render(
956
+ runtimeUrl: string,
957
+ assistantId: string,
958
+ species: Species,
959
+ ): number {
867
960
  const config = SPECIES_CONFIG[species];
868
961
  const art = config.art;
869
962
 
@@ -871,7 +964,11 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
871
964
  const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
872
965
 
873
966
  const { unmount } = inkRender(
874
- <DefaultMainScreen runtimeUrl={runtimeUrl} assistantId={assistantId} species={species} />,
967
+ <DefaultMainScreen
968
+ runtimeUrl={runtimeUrl}
969
+ assistantId={assistantId}
970
+ species={species}
971
+ />,
875
972
  { exitOnCtrlC: false },
876
973
  );
877
974
  unmount();
@@ -883,7 +980,9 @@ export function render(runtimeUrl: string, assistantId: string, species: Species
883
980
  const statusText = health.detail
884
981
  ? `${withStatusEmoji(health.status)} (${health.detail})`
885
982
  : withStatusEmoji(health.status);
886
- process.stdout.write(`\x1b7\x1b[${statusCanvasLine};${statusCol}H\x1b[K${statusText}\x1b8`);
983
+ process.stdout.write(
984
+ `\x1b7\x1b[${statusCanvasLine};${statusCol}H\x1b[K${statusText}\x1b8`,
985
+ );
887
986
  })
888
987
  .catch(() => {});
889
988
 
@@ -917,7 +1016,9 @@ function SelectionWindow({
917
1016
  },
918
1017
  ) => {
919
1018
  if (key.upArrow) {
920
- setSelectedIndex((prev: number) => (prev - 1 + options.length) % options.length);
1019
+ setSelectedIndex(
1020
+ (prev: number) => (prev - 1 + options.length) % options.length,
1021
+ );
921
1022
  } else if (key.downArrow) {
922
1023
  setSelectedIndex((prev: number) => (prev + 1) % options.length);
923
1024
  } else if (key.return) {
@@ -928,26 +1029,42 @@ function SelectionWindow({
928
1029
  },
929
1030
  );
930
1031
 
931
- const borderH = "\u2500".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - title.length - DIALOG_TITLE_CHROME));
1032
+ const borderH = "\u2500".repeat(
1033
+ Math.max(0, DIALOG_WINDOW_WIDTH - title.length - DIALOG_TITLE_CHROME),
1034
+ );
932
1035
 
933
1036
  return (
934
1037
  <Box flexDirection="column" width={DIALOG_WINDOW_WIDTH}>
935
1038
  <Text>{"\u250C\u2500 " + title + " " + borderH + "\u2510"}</Text>
936
1039
  {options.map((option, i) => {
937
1040
  const marker = i === selectedIndex ? "\u276F" : " ";
938
- const padding = " ".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - option.length - SELECTION_OPTION_CHROME));
1041
+ const padding = " ".repeat(
1042
+ Math.max(
1043
+ 0,
1044
+ DIALOG_WINDOW_WIDTH - option.length - SELECTION_OPTION_CHROME,
1045
+ ),
1046
+ );
939
1047
  return (
940
1048
  <Text key={i}>
941
1049
  {"\u2502 "}
942
- <Text color={i === selectedIndex ? "cyan" : undefined}>{marker}</Text>{" "}
1050
+ <Text color={i === selectedIndex ? "cyan" : undefined}>
1051
+ {marker}
1052
+ </Text>{" "}
943
1053
  <Text bold={i === selectedIndex}>{option}</Text>
944
1054
  {padding}
945
1055
  {"\u2502"}
946
1056
  </Text>
947
1057
  );
948
1058
  })}
949
- <Text>{"\u2514" + "\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) + "\u2518"}</Text>
950
- <Tooltip text="\u2191/\u2193 navigate Enter select Esc cancel" delay={1000} />
1059
+ <Text>
1060
+ {"\u2514" +
1061
+ "\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) +
1062
+ "\u2518"}
1063
+ </Text>
1064
+ <Tooltip
1065
+ text="\u2191/\u2193 navigate Enter select Esc cancel"
1066
+ delay={1000}
1067
+ />
951
1068
  </Box>
952
1069
  );
953
1070
  }
@@ -990,11 +1107,19 @@ function SecretInputWindow({
990
1107
  },
991
1108
  );
992
1109
 
993
- const borderH = "\u2500".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - label.length - DIALOG_TITLE_CHROME));
1110
+ const borderH = "\u2500".repeat(
1111
+ Math.max(0, DIALOG_WINDOW_WIDTH - label.length - DIALOG_TITLE_CHROME),
1112
+ );
994
1113
  const masked = "\u2022".repeat(value.length);
995
- const displayText = value.length > 0 ? masked : (placeholder ?? "Enter secret...");
1114
+ const displayText =
1115
+ value.length > 0 ? masked : (placeholder ?? "Enter secret...");
996
1116
  const displayColor = value.length > 0 ? undefined : "gray";
997
- const contentPad = " ".repeat(Math.max(0, DIALOG_WINDOW_WIDTH - displayText.length - SECRET_CONTENT_CHROME));
1117
+ const contentPad = " ".repeat(
1118
+ Math.max(
1119
+ 0,
1120
+ DIALOG_WINDOW_WIDTH - displayText.length - SECRET_CONTENT_CHROME,
1121
+ ),
1122
+ );
998
1123
 
999
1124
  return (
1000
1125
  <Box flexDirection="column" width={DIALOG_WINDOW_WIDTH}>
@@ -1005,7 +1130,11 @@ function SecretInputWindow({
1005
1130
  {contentPad}
1006
1131
  {"\u2502"}
1007
1132
  </Text>
1008
- <Text>{"\u2514" + "\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) + "\u2518"}</Text>
1133
+ <Text>
1134
+ {"\u2514" +
1135
+ "\u2500".repeat(DIALOG_WINDOW_WIDTH - DIALOG_BORDER_CORNERS) +
1136
+ "\u2518"}
1137
+ </Text>
1009
1138
  <Tooltip text="Enter submit Esc cancel" delay={1000} />
1010
1139
  </Box>
1011
1140
  );
@@ -1039,7 +1168,10 @@ export interface ChatAppHandle {
1039
1168
  showSecretInput: (label: string, placeholder?: string) => Promise<string>;
1040
1169
  handleSecretPrompt: (
1041
1170
  secret: PendingSecret,
1042
- onSubmit: (value: string, delivery?: "store" | "transient_send") => Promise<void>,
1171
+ onSubmit: (
1172
+ value: string,
1173
+ delivery?: "store" | "transient_send",
1174
+ ) => Promise<void>,
1043
1175
  ) => Promise<void>;
1044
1176
  clearFeed: () => void;
1045
1177
  setBusy: (busy: boolean) => void;
@@ -1071,10 +1203,14 @@ function ChatApp({
1071
1203
  const [feed, setFeed] = useState<FeedItem[]>([]);
1072
1204
  const [spinnerText, setSpinnerText] = useState<string | null>(null);
1073
1205
  const [selection, setSelection] = useState<SelectionRequest | null>(null);
1074
- const [secretInput, setSecretInput] = useState<SecretInputRequest | null>(null);
1206
+ const [secretInput, setSecretInput] = useState<SecretInputRequest | null>(
1207
+ null,
1208
+ );
1075
1209
  const [inputFocused, setInputFocused] = useState(true);
1076
1210
  const [scrollIndex, setScrollIndex] = useState<number | null>(null);
1077
- const [healthStatus, setHealthStatus] = useState<string | undefined>(undefined);
1211
+ const [healthStatus, setHealthStatus] = useState<string | undefined>(
1212
+ undefined,
1213
+ );
1078
1214
  const prevFeedLengthRef = useRef(0);
1079
1215
  const busyRef = useRef(false);
1080
1216
  const connectedRef = useRef(false);
@@ -1098,7 +1234,10 @@ function ChatApp({
1098
1234
  : spinnerText
1099
1235
  ? SPINNER_HEIGHT + INPUT_AREA_HEIGHT
1100
1236
  : INPUT_AREA_HEIGHT;
1101
- const availableRows = Math.max(MIN_FEED_ROWS, terminalRows - headerHeight - bottomHeight);
1237
+ const availableRows = Math.max(
1238
+ MIN_FEED_ROWS,
1239
+ terminalRows - headerHeight - bottomHeight,
1240
+ );
1102
1241
 
1103
1242
  const addMessage = useCallback((msg: RuntimeMessage) => {
1104
1243
  setFeed((prev) => [...prev, msg]);
@@ -1198,24 +1337,33 @@ function ChatApp({
1198
1337
  setFeed((prev) => [...prev, item]);
1199
1338
  }, []);
1200
1339
 
1201
- const showSelection = useCallback((title: string, options: string[]): Promise<number> => {
1202
- setInputFocused(false);
1203
- return new Promise<number>((resolve) => {
1204
- setSelection({ title, options, resolve });
1205
- });
1206
- }, []);
1340
+ const showSelection = useCallback(
1341
+ (title: string, options: string[]): Promise<number> => {
1342
+ setInputFocused(false);
1343
+ return new Promise<number>((resolve) => {
1344
+ setSelection({ title, options, resolve });
1345
+ });
1346
+ },
1347
+ [],
1348
+ );
1207
1349
 
1208
- const showSecretInput = useCallback((label: string, placeholder?: string): Promise<string> => {
1209
- setInputFocused(false);
1210
- return new Promise<string>((resolve) => {
1211
- setSecretInput({ label, placeholder, resolve });
1212
- });
1213
- }, []);
1350
+ const showSecretInput = useCallback(
1351
+ (label: string, placeholder?: string): Promise<string> => {
1352
+ setInputFocused(false);
1353
+ return new Promise<string>((resolve) => {
1354
+ setSecretInput({ label, placeholder, resolve });
1355
+ });
1356
+ },
1357
+ [],
1358
+ );
1214
1359
 
1215
1360
  const handleSecretPromptFn = useCallback(
1216
1361
  async (
1217
1362
  secret: PendingSecret,
1218
- onSubmit: (value: string, delivery?: "store" | "transient_send") => Promise<void>,
1363
+ onSubmit: (
1364
+ value: string,
1365
+ delivery?: "store" | "transient_send",
1366
+ ) => Promise<void>,
1219
1367
  ): Promise<void> => {
1220
1368
  addStatus(`\u250C Secret needed: ${secret.label}`);
1221
1369
  addStatus(`\u2502 Service: ${secret.service} / ${secret.field}`);
@@ -1311,12 +1459,18 @@ function ChatApp({
1311
1459
  try {
1312
1460
  const health = await checkHealthRuntime(runtimeUrl);
1313
1461
  if (!accessTokenRef.current) {
1314
- accessTokenRef.current = await bootstrapAccessToken(runtimeUrl, bearerToken);
1462
+ accessTokenRef.current = await bootstrapAccessToken(
1463
+ runtimeUrl,
1464
+ bearerToken,
1465
+ );
1315
1466
  }
1316
1467
  h.hideSpinner();
1317
1468
  h.updateHealthStatus(health.status);
1318
1469
  if (health.status === "healthy" || health.status === "ok") {
1319
- h.addStatus(`${statusEmoji(health.status)} Connected to assistant`, "green");
1470
+ h.addStatus(
1471
+ `${statusEmoji(health.status)} Connected to assistant`,
1472
+ "green",
1473
+ );
1320
1474
  } else {
1321
1475
  const statusMsg = health.message ? ` - ${health.message}` : "";
1322
1476
  h.addStatus(
@@ -1374,7 +1528,10 @@ function ChatApp({
1374
1528
  connectingRef.current = false;
1375
1529
  h.updateHealthStatus("unreachable");
1376
1530
  const msg = err instanceof Error ? err.message : String(err);
1377
- h.addStatus(`${statusEmoji("unreachable")} Failed to connect: ${msg}`, "red");
1531
+ h.addStatus(
1532
+ `${statusEmoji("unreachable")} Failed to connect: ${msg}`,
1533
+ "red",
1534
+ );
1378
1535
  return false;
1379
1536
  }
1380
1537
  }, [runtimeUrl, assistantId, bearerToken]);
@@ -1427,17 +1584,27 @@ function ChatApp({
1427
1584
  method: "POST",
1428
1585
  headers: {
1429
1586
  "Content-Type": "application/json",
1430
- ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
1587
+ ...(bearerToken
1588
+ ? { Authorization: `Bearer ${bearerToken}` }
1589
+ : {}),
1431
1590
  },
1432
- body: JSON.stringify({ pairingRequestId, pairingSecret, gatewayUrl }),
1591
+ body: JSON.stringify({
1592
+ pairingRequestId,
1593
+ pairingSecret,
1594
+ gatewayUrl,
1595
+ }),
1433
1596
  });
1434
1597
 
1435
1598
  if (!registerRes.ok) {
1436
1599
  const body = await registerRes.text().catch(() => "");
1437
- throw new Error(`HTTP ${registerRes.status}: ${body || registerRes.statusText}`);
1600
+ throw new Error(
1601
+ `HTTP ${registerRes.status}: ${body || registerRes.statusText}`,
1602
+ );
1438
1603
  }
1439
1604
 
1440
- const hostId = createHash("sha256").update(hostname() + userInfo().username).digest("hex");
1605
+ const hostId = createHash("sha256")
1606
+ .update(hostname() + userInfo().username)
1607
+ .digest("hex");
1441
1608
  const payload = JSON.stringify({
1442
1609
  type: "vellum-daemon",
1443
1610
  v: 4,
@@ -1462,14 +1629,18 @@ function ChatApp({
1462
1629
  );
1463
1630
  } catch (err) {
1464
1631
  h.hideSpinner();
1465
- h.showError(`Pairing failed: ${err instanceof Error ? err.message : err}`);
1632
+ h.showError(
1633
+ `Pairing failed: ${err instanceof Error ? err.message : err}`,
1634
+ );
1466
1635
  }
1467
1636
  return;
1468
1637
  }
1469
1638
 
1470
1639
  if (trimmed === "/retire") {
1471
1640
  if (!project || !zone) {
1472
- h.showError("No instance info available. Connect to a hatched instance first.");
1641
+ h.showError(
1642
+ "No instance info available. Connect to a hatched instance first.",
1643
+ );
1473
1644
  return;
1474
1645
  }
1475
1646
 
@@ -1524,9 +1695,13 @@ function ChatApp({
1524
1695
  handleRef_.current?.hideSpinner();
1525
1696
  if (code === 0) {
1526
1697
  removeAssistantEntry(assistantId);
1527
- handleRef_.current?.addStatus(`Removed ${assistantId} from lockfile.json`);
1698
+ handleRef_.current?.addStatus(
1699
+ `Removed ${assistantId} from lockfile.json`,
1700
+ );
1528
1701
  } else {
1529
- handleRef_.current?.showError(`Failed to delete instance (exit code ${code})`);
1702
+ handleRef_.current?.showError(
1703
+ `Failed to delete instance (exit code ${code})`,
1704
+ );
1530
1705
  }
1531
1706
  cleanup();
1532
1707
  process.exit(code === 0 ? 0 : 1);
@@ -1534,14 +1709,18 @@ function ChatApp({
1534
1709
 
1535
1710
  child.on("error", (err) => {
1536
1711
  handleRef_.current?.hideSpinner();
1537
- handleRef_.current?.showError(`Failed to retire instance: ${err.message}`);
1712
+ handleRef_.current?.showError(
1713
+ `Failed to retire instance: ${err.message}`,
1714
+ );
1538
1715
  });
1539
1716
  return;
1540
1717
  }
1541
1718
 
1542
1719
  if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
1543
1720
  if (!project || !zone) {
1544
- h.showError("No instance info available. Connect to a hatched instance first.");
1721
+ h.showError(
1722
+ "No instance info available. Connect to a hatched instance first.",
1723
+ );
1545
1724
  return;
1546
1725
  }
1547
1726
  const userPrompt = trimmed.slice("/doctor".length).trim() || undefined;
@@ -1571,7 +1750,9 @@ function ChatApp({
1571
1750
  (event) => {
1572
1751
  switch (event.phase) {
1573
1752
  case "invoking_prompt":
1574
- handleRef_.current?.showSpinner(`Analyzing ${assistantId}...`);
1753
+ handleRef_.current?.showSpinner(
1754
+ `Analyzing ${assistantId}...`,
1755
+ );
1575
1756
  break;
1576
1757
  case "calling_tool":
1577
1758
  handleRef_.current?.showSpinner(
@@ -1579,7 +1760,9 @@ function ChatApp({
1579
1760
  );
1580
1761
  break;
1581
1762
  case "processing_tool_result":
1582
- handleRef_.current?.showSpinner(`Reviewing diagnostics for ${assistantId}...`);
1763
+ handleRef_.current?.showSpinner(
1764
+ `Reviewing diagnostics for ${assistantId}...`,
1765
+ );
1583
1766
  break;
1584
1767
  }
1585
1768
  },
@@ -1589,14 +1772,17 @@ function ChatApp({
1589
1772
  h.hideSpinner();
1590
1773
  if (result.recommendation) {
1591
1774
  h.addStatus(`Recommendation:\n${result.recommendation}`);
1592
- chatLogRef.current.push({ role: "assistant", content: result.recommendation });
1775
+ chatLogRef.current.push({
1776
+ role: "assistant",
1777
+ content: result.recommendation,
1778
+ });
1593
1779
  } else if (result.error) {
1594
1780
  h.showError(result.error);
1595
1781
  chatLogRef.current.push({ role: "error", content: result.error });
1596
1782
  }
1597
1783
  } catch (err) {
1598
1784
  h.hideSpinner();
1599
- const errorMsg = `Doctor daemon unreachable: ${err instanceof Error ? err.message : err}`;
1785
+ const errorMsg = `Doctor assistant unreachable: ${err instanceof Error ? err.message : err}`;
1600
1786
  h.showError(errorMsg);
1601
1787
  chatLogRef.current.push({ role: "error", content: errorMsg });
1602
1788
  }
@@ -1657,7 +1843,9 @@ function ChatApp({
1657
1843
  h.showSpinner("Working...");
1658
1844
 
1659
1845
  while (true) {
1660
- await new Promise((resolve) => setTimeout(resolve, RESPONSE_POLL_INTERVAL_MS));
1846
+ await new Promise((resolve) =>
1847
+ setTimeout(resolve, RESPONSE_POLL_INTERVAL_MS),
1848
+ );
1661
1849
 
1662
1850
  // Check for pending confirmations/secrets
1663
1851
  try {
@@ -1686,19 +1874,26 @@ function ChatApp({
1686
1874
  if (pending.pendingSecret) {
1687
1875
  const secretRequestId = pending.pendingSecret.requestId ?? "";
1688
1876
  h.hideSpinner();
1689
- await h.handleSecretPrompt(pending.pendingSecret, async (value, delivery) => {
1690
- await runtimeRequest(
1691
- runtimeUrl,
1692
- assistantId,
1693
- "/secret",
1694
- {
1695
- method: "POST",
1696
- body: JSON.stringify({ requestId: secretRequestId, value, delivery }),
1697
- },
1698
- bearerToken,
1699
- accessTokenRef.current,
1700
- );
1701
- });
1877
+ await h.handleSecretPrompt(
1878
+ pending.pendingSecret,
1879
+ async (value, delivery) => {
1880
+ await runtimeRequest(
1881
+ runtimeUrl,
1882
+ assistantId,
1883
+ "/secret",
1884
+ {
1885
+ method: "POST",
1886
+ body: JSON.stringify({
1887
+ requestId: secretRequestId,
1888
+ value,
1889
+ delivery,
1890
+ }),
1891
+ },
1892
+ bearerToken,
1893
+ accessTokenRef.current,
1894
+ );
1895
+ },
1896
+ );
1702
1897
  h.showSpinner("Working...");
1703
1898
  continue;
1704
1899
  }
@@ -1719,7 +1914,10 @@ function ChatApp({
1719
1914
  seenMessageIdsRef.current.add(msg.id);
1720
1915
  if (msg.role === "assistant") {
1721
1916
  h.addMessage(msg);
1722
- chatLogRef.current.push({ role: "assistant", content: msg.content });
1917
+ chatLogRef.current.push({
1918
+ role: "assistant",
1919
+ content: msg.content,
1920
+ });
1723
1921
  h.setBusy(false);
1724
1922
  h.hideSpinner();
1725
1923
  return;
@@ -1730,7 +1928,6 @@ function ChatApp({
1730
1928
  // Poll failure; retry
1731
1929
  }
1732
1930
  }
1733
-
1734
1931
  } catch (error) {
1735
1932
  h.setBusy(false);
1736
1933
  h.hideSpinner();
@@ -1746,7 +1943,15 @@ function ChatApp({
1746
1943
  }
1747
1944
  }
1748
1945
  },
1749
- [runtimeUrl, assistantId, bearerToken, project, zone, cleanup, ensureConnected],
1946
+ [
1947
+ runtimeUrl,
1948
+ assistantId,
1949
+ bearerToken,
1950
+ project,
1951
+ zone,
1952
+ cleanup,
1953
+ ensureConnected,
1954
+ ],
1750
1955
  );
1751
1956
 
1752
1957
  const handleSubmit = useCallback(
@@ -1890,7 +2095,8 @@ function ChatApp({
1890
2095
  <Box flexDirection="column" flexGrow={1} overflow="hidden">
1891
2096
  {visibleWindow.hiddenAbove > 0 ? (
1892
2097
  <Text dimColor>
1893
- {"\u2191"} {visibleWindow.hiddenAbove} more above (Shift+\u2191/Cmd+\u2191)
2098
+ {"\u2191"} {visibleWindow.hiddenAbove} more above
2099
+ (Shift+\u2191/Cmd+\u2191)
1894
2100
  </Text>
1895
2101
  ) : null}
1896
2102
 
@@ -1905,7 +2111,10 @@ function ChatApp({
1905
2111
  }
1906
2112
  if (item.type === "status") {
1907
2113
  return (
1908
- <Text key={feedIndex} color={item.color as "green" | "yellow" | "red" | undefined}>
2114
+ <Text
2115
+ key={feedIndex}
2116
+ color={item.color as "green" | "yellow" | "red" | undefined}
2117
+ >
1909
2118
  {item.text}
1910
2119
  </Text>
1911
2120
  );
@@ -1925,7 +2134,8 @@ function ChatApp({
1925
2134
 
1926
2135
  {visibleWindow.hiddenBelow > 0 ? (
1927
2136
  <Text dimColor>
1928
- {"\u2193"} {visibleWindow.hiddenBelow} more below (Shift+\u2193/Cmd+\u2193)
2137
+ {"\u2193"} {visibleWindow.hiddenBelow} more below
2138
+ (Shift+\u2193/Cmd+\u2193)
1929
2139
  </Text>
1930
2140
  ) : null}
1931
2141
  </Box>
@@ -1956,14 +2166,14 @@ function ChatApp({
1956
2166
  <Box paddingLeft={1}>
1957
2167
  <Text color="green" bold>
1958
2168
  you{">"}
1959
- {" "}
1960
- </Text>
1961
- <TextInput
1962
- value={inputValue}
1963
- onChange={setInputValue}
1964
- onSubmit={handleSubmit}
1965
- focus={inputFocused}
1966
- />
2169
+ {" "}
2170
+ </Text>
2171
+ <TextInput
2172
+ value={inputValue}
2173
+ onChange={setInputValue}
2174
+ onSubmit={handleSubmit}
2175
+ focus={inputFocused}
2176
+ />
1967
2177
  </Box>
1968
2178
  <Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
1969
2179
  <Text dimColor> ? for shortcuts</Text>