@vellumai/cli 0.4.46 → 0.4.49

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.
@@ -17,7 +17,12 @@ import { removeAssistantEntry } from "../lib/assistant-config";
17
17
  import { SPECIES_CONFIG, type Species } from "../lib/constants";
18
18
  import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
19
19
  import { checkHealth } from "../lib/health-check";
20
+ import { appendHistory, loadHistory } from "../lib/input-history";
20
21
  import { statusEmoji, withStatusEmoji } from "../lib/status-emoji";
22
+ import {
23
+ getTerminalCapabilities,
24
+ unicodeOrFallback,
25
+ } from "../lib/terminal-capabilities";
21
26
  import TextInput from "./TextInput";
22
27
  import { Tooltip } from "./Tooltip";
23
28
 
@@ -55,7 +60,9 @@ const DEFAULT_TERMINAL_COLUMNS = 80;
55
60
  const DEFAULT_TERMINAL_ROWS = 24;
56
61
  const LEFT_PANEL_WIDTH = 36;
57
62
 
58
- const HEADER_PREFIX = "── Vellum ";
63
+ const COMPACT_THRESHOLD = 60;
64
+ const HEADER_PREFIX_UNICODE = "── Vellum ";
65
+ const HEADER_PREFIX_ASCII = "-- Vellum ";
59
66
 
60
67
  // Left panel structure: HEADER lines + art + FOOTER lines
61
68
  const LEFT_HEADER_LINES = 3; // spacer + heading + spacer
@@ -694,6 +701,37 @@ interface StyledLine {
694
701
  style: "heading" | "dim" | "normal";
695
702
  }
696
703
 
704
+ function CompactHeader({
705
+ species,
706
+ healthStatus,
707
+ totalWidth,
708
+ }: {
709
+ species: Species;
710
+ healthStatus?: string;
711
+ totalWidth: number;
712
+ }): ReactElement {
713
+ const config = SPECIES_CONFIG[species];
714
+ const accentColor = species === "openclaw" ? "red" : "magenta";
715
+ const status = healthStatus ?? "checking...";
716
+ const label = ` ${config.hatchedEmoji} ${species} ${statusEmoji(status)} `;
717
+ const prefix = "── Vellum";
718
+ const suffix = "──";
719
+ const fillLen = Math.max(
720
+ 0,
721
+ totalWidth - prefix.length - label.length - suffix.length,
722
+ );
723
+ return (
724
+ <Box flexDirection="column" width={totalWidth}>
725
+ <Text dimColor>
726
+ {prefix}
727
+ <Text color={accentColor}>{label}</Text>
728
+ {"─".repeat(fillLen)}
729
+ {suffix}
730
+ </Text>
731
+ </Box>
732
+ );
733
+ }
734
+
697
735
  function DefaultMainScreen({
698
736
  runtimeUrl,
699
737
  assistantId,
@@ -705,10 +743,27 @@ function DefaultMainScreen({
705
743
  const config = SPECIES_CONFIG[species];
706
744
  const art = config.art;
707
745
  const accentColor = species === "openclaw" ? "red" : "magenta";
746
+ const caps = getTerminalCapabilities();
747
+ const headerPrefix = caps.unicodeSupported
748
+ ? HEADER_PREFIX_UNICODE
749
+ : HEADER_PREFIX_ASCII;
750
+ const headerSep = caps.unicodeSupported ? "─" : "-";
708
751
 
709
752
  const { stdout } = useStdout();
710
753
  const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
711
754
  const totalWidth = Math.min(MAX_TOTAL_WIDTH, terminalColumns);
755
+ const isCompact = terminalColumns < COMPACT_THRESHOLD;
756
+
757
+ if (isCompact) {
758
+ return (
759
+ <CompactHeader
760
+ species={species}
761
+ healthStatus={healthStatus}
762
+ totalWidth={totalWidth}
763
+ />
764
+ );
765
+ }
766
+
712
767
  const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
713
768
 
714
769
  const leftLines = [
@@ -729,7 +784,10 @@ function DefaultMainScreen({
729
784
  { text: "Assistant", style: "heading" },
730
785
  { text: assistantId, style: "dim" },
731
786
  { text: "Species", style: "heading" },
732
- { text: `${config.hatchedEmoji} ${species}`, style: "dim" },
787
+ {
788
+ text: `${unicodeOrFallback(config.hatchedEmoji, `[${species}]`)} ${species}`,
789
+ style: "dim",
790
+ },
733
791
  { text: "Status", style: "heading" },
734
792
  { text: withStatusEmoji(healthStatus ?? "checking..."), style: "dim" },
735
793
  ];
@@ -739,8 +797,8 @@ function DefaultMainScreen({
739
797
  return (
740
798
  <Box flexDirection="column" width={totalWidth}>
741
799
  <Text dimColor>
742
- {HEADER_PREFIX +
743
- "─".repeat(Math.max(0, totalWidth - HEADER_PREFIX.length))}
800
+ {headerPrefix +
801
+ headerSep.repeat(Math.max(0, totalWidth - headerPrefix.length))}
744
802
  </Text>
745
803
  <Box flexDirection="row">
746
804
  <Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
@@ -755,7 +813,7 @@ function DefaultMainScreen({
755
813
  }
756
814
  if (i > 2 && i <= 2 + art.length) {
757
815
  return (
758
- <Text key={i} color={accentColor}>
816
+ <Text key={i} color={caps.isDumb ? undefined : accentColor}>
759
817
  {line}
760
818
  </Text>
761
819
  );
@@ -792,7 +850,7 @@ function DefaultMainScreen({
792
850
  })}
793
851
  </Box>
794
852
  </Box>
795
- <Text dimColor>{"─".repeat(totalWidth)}</Text>
853
+ <Text dimColor>{headerSep.repeat(totalWidth)}</Text>
796
854
  <Text> </Text>
797
855
  </Box>
798
856
  );
@@ -838,9 +896,18 @@ function isRuntimeMessage(item: FeedItem): item is RuntimeMessage {
838
896
  function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
839
897
  if (isRuntimeMessage(item)) {
840
898
  const cols = Math.max(1, terminalColumns);
899
+ // Account for "HH:MM AM Label: " prefix on the first line
900
+ const defaultLabel = item.role === "user" ? "You:" : "Assistant:";
901
+ const label = item.label ?? defaultLabel;
902
+ const prefixLen = 10 + label.length + 1; // timestamp + space + label + space
841
903
  let lines = 0;
842
- for (const line of item.content.split("\n")) {
843
- lines += Math.max(1, Math.ceil(line.length / cols));
904
+ const contentLines = item.content.split("\n");
905
+ for (let idx = 0; idx < contentLines.length; idx++) {
906
+ const lineLen =
907
+ idx === 0
908
+ ? contentLines[idx].length + prefixLen
909
+ : contentLines[idx].length;
910
+ lines += Math.max(1, Math.ceil(lineLen / cols));
844
911
  }
845
912
  if (item.role === "assistant" && item.toolCalls) {
846
913
  for (const tc of item.toolCalls) {
@@ -870,7 +937,15 @@ function estimateItemHeight(item: FeedItem, terminalColumns: number): number {
870
937
  return 1;
871
938
  }
872
939
 
873
- function calculateHeaderHeight(species: Species): number {
940
+ const COMPACT_HEADER_HEIGHT = 1;
941
+
942
+ function calculateHeaderHeight(
943
+ species: Species,
944
+ terminalColumns?: number,
945
+ ): number {
946
+ if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
947
+ return COMPACT_HEADER_HEIGHT;
948
+ }
874
949
  const config = SPECIES_CONFIG[species];
875
950
  const artLength = config.art.length;
876
951
  const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
@@ -885,6 +960,9 @@ export function render(
885
960
  assistantId: string,
886
961
  species: Species,
887
962
  ): number {
963
+ const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
964
+ const isCompact = terminalColumns < COMPACT_THRESHOLD;
965
+
888
966
  const config = SPECIES_CONFIG[species];
889
967
  const art = config.art;
890
968
 
@@ -901,6 +979,10 @@ export function render(
901
979
  );
902
980
  unmount();
903
981
 
982
+ if (isCompact) {
983
+ return COMPACT_HEADER_HEIGHT;
984
+ }
985
+
904
986
  const statusCanvasLine = RIGHT_PANEL_LINE_COUNT + HEADER_TOP_BORDER_LINES;
905
987
  const statusCol = LEFT_PANEL_WIDTH + 1;
906
988
  checkHealth(runtimeUrl)
@@ -1128,6 +1210,9 @@ function ChatApp({
1128
1210
  handleRef,
1129
1211
  }: ChatAppProps): ReactElement {
1130
1212
  const [inputValue, setInputValue] = useState("");
1213
+ const historyRef = useRef<string[]>(loadHistory());
1214
+ const historyIndexRef = useRef(-1);
1215
+ const savedInputRef = useRef("");
1131
1216
  const [feed, setFeed] = useState<FeedItem[]>([]);
1132
1217
  const [spinnerText, setSpinnerText] = useState<string | null>(null);
1133
1218
  const [selection, setSelection] = useState<SelectionRequest | null>(null);
@@ -1152,15 +1237,20 @@ function ChatApp({
1152
1237
  const { stdout } = useStdout();
1153
1238
  const terminalRows = stdout.rows || DEFAULT_TERMINAL_ROWS;
1154
1239
  const terminalColumns = stdout.columns || DEFAULT_TERMINAL_COLUMNS;
1155
- const headerHeight = calculateHeaderHeight(species);
1240
+ const headerHeight = calculateHeaderHeight(species, terminalColumns);
1156
1241
 
1242
+ const isCompact = terminalColumns < COMPACT_THRESHOLD;
1243
+ const compactInputAreaHeight = 2; // separator + input row only
1244
+ const inputAreaHeight = isCompact
1245
+ ? compactInputAreaHeight
1246
+ : INPUT_AREA_HEIGHT;
1157
1247
  const bottomHeight = selection
1158
1248
  ? selection.options.length + SELECTION_CHROME_LINES + TOOLTIP_HEIGHT
1159
1249
  : secretInput
1160
1250
  ? SECRET_INPUT_HEIGHT + TOOLTIP_HEIGHT
1161
1251
  : spinnerText
1162
- ? SPINNER_HEIGHT + INPUT_AREA_HEIGHT
1163
- : INPUT_AREA_HEIGHT;
1252
+ ? SPINNER_HEIGHT + inputAreaHeight
1253
+ : inputAreaHeight;
1164
1254
  const availableRows = Math.max(
1165
1255
  MIN_FEED_ROWS,
1166
1256
  terminalRows - headerHeight - bottomHeight,
@@ -1197,11 +1287,14 @@ function ChatApp({
1197
1287
  }
1198
1288
 
1199
1289
  if (scrollIndex === null) {
1290
+ // Reserve 1 line for "N more above" indicator when there are hidden messages
1200
1291
  let totalHeight = 0;
1201
1292
  let start = feed.length;
1202
1293
  for (let i = feed.length - 1; i >= 0; i--) {
1203
1294
  const h = estimateItemHeight(feed[i], terminalColumns);
1204
- if (totalHeight + h > availableRows) {
1295
+ // Reserve space for the "more above" indicator if we'd hide messages
1296
+ const indicatorLine = i > 0 ? 1 : 0;
1297
+ if (totalHeight + h + indicatorLine > availableRows) {
1205
1298
  break;
1206
1299
  }
1207
1300
  totalHeight += h;
@@ -1220,11 +1313,16 @@ function ChatApp({
1220
1313
  }
1221
1314
 
1222
1315
  const start = Math.max(0, Math.min(scrollIndex, feed.length - 1));
1316
+ // Reserve lines for "more above/below" indicators
1317
+ const aboveIndicator = start > 0 ? 1 : 0;
1318
+ const budget = availableRows - aboveIndicator;
1223
1319
  let totalHeight = 0;
1224
1320
  let end = start;
1225
1321
  for (let i = start; i < feed.length; i++) {
1226
1322
  const h = estimateItemHeight(feed[i], terminalColumns);
1227
- if (totalHeight + h > availableRows) {
1323
+ // Reserve space for "more below" indicator if we'd hide messages
1324
+ const belowIndicator = i + 1 < feed.length ? 1 : 0;
1325
+ if (totalHeight + h + belowIndicator > budget) {
1228
1326
  break;
1229
1327
  }
1230
1328
  totalHeight += h;
@@ -2005,12 +2103,44 @@ function ChatApp({
2005
2103
 
2006
2104
  const handleSubmit = useCallback(
2007
2105
  (value: string) => {
2106
+ const trimmed = value.trim();
2107
+ if (trimmed) {
2108
+ appendHistory(trimmed);
2109
+ historyRef.current = loadHistory();
2110
+ }
2111
+ historyIndexRef.current = -1;
2112
+ savedInputRef.current = "";
2008
2113
  setInputValue("");
2009
2114
  handleInput(value);
2010
2115
  },
2011
2116
  [handleInput],
2012
2117
  );
2013
2118
 
2119
+ const handleHistoryUp = useCallback(() => {
2120
+ const history = historyRef.current;
2121
+ if (history.length === 0) return;
2122
+ if (historyIndexRef.current === -1) {
2123
+ savedInputRef.current = inputValue;
2124
+ }
2125
+ const nextIndex = Math.min(historyIndexRef.current + 1, history.length - 1);
2126
+ historyIndexRef.current = nextIndex;
2127
+ const entry = history[history.length - 1 - nextIndex];
2128
+ setInputValue(entry);
2129
+ }, [inputValue]);
2130
+
2131
+ const handleHistoryDown = useCallback(() => {
2132
+ if (historyIndexRef.current === -1) return;
2133
+ if (historyIndexRef.current <= 0) {
2134
+ historyIndexRef.current = -1;
2135
+ setInputValue(savedInputRef.current);
2136
+ return;
2137
+ }
2138
+ historyIndexRef.current -= 1;
2139
+ const history = historyRef.current;
2140
+ const entry = history[history.length - 1 - historyIndexRef.current];
2141
+ setInputValue(entry);
2142
+ }, []);
2143
+
2014
2144
  useEffect(() => {
2015
2145
  const handle: ChatAppHandle = {
2016
2146
  addMessage,
@@ -2210,9 +2340,11 @@ function ChatApp({
2210
2340
  ) : null}
2211
2341
 
2212
2342
  {!selection && !secretInput ? (
2213
- <Box flexDirection="column">
2214
- <Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
2215
- <Box paddingLeft={1}>
2343
+ <Box flexDirection="column" flexShrink={0}>
2344
+ <Text dimColor>
2345
+ {unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
2346
+ </Text>
2347
+ <Box paddingLeft={1} height={1} flexShrink={0}>
2216
2348
  <Text color="green" bold>
2217
2349
  you{">"}
2218
2350
  {" "}
@@ -2221,11 +2353,20 @@ function ChatApp({
2221
2353
  value={inputValue}
2222
2354
  onChange={setInputValue}
2223
2355
  onSubmit={handleSubmit}
2356
+ onHistoryUp={handleHistoryUp}
2357
+ onHistoryDown={handleHistoryDown}
2358
+ completionCommands={SLASH_COMMANDS}
2224
2359
  focus={inputFocused}
2225
2360
  />
2226
2361
  </Box>
2227
- <Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
2228
- <Text dimColor> ? for shortcuts</Text>
2362
+ {terminalColumns >= COMPACT_THRESHOLD ? (
2363
+ <>
2364
+ <Text dimColor>
2365
+ {unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
2366
+ </Text>
2367
+ <Text dimColor> ? for shortcuts</Text>
2368
+ </>
2369
+ ) : null}
2229
2370
  </Box>
2230
2371
  ) : null}
2231
2372
  </Box>
@@ -6,6 +6,9 @@ interface TextInputProps {
6
6
  value: string;
7
7
  onChange: (value: string) => void;
8
8
  onSubmit?: (value: string) => void;
9
+ onHistoryUp?: () => void;
10
+ onHistoryDown?: () => void;
11
+ completionCommands?: string[];
9
12
  focus?: boolean;
10
13
  placeholder?: string;
11
14
  }
@@ -14,12 +17,19 @@ function TextInput({
14
17
  value,
15
18
  onChange,
16
19
  onSubmit,
20
+ onHistoryUp,
21
+ onHistoryDown,
22
+ completionCommands,
17
23
  focus = true,
18
24
  placeholder = "",
19
25
  }: TextInputProps): ReactElement {
20
26
  const cursorOffsetRef = useRef(value.length);
21
27
  const valueRef = useRef(value);
22
28
 
29
+ // Tab completion state
30
+ const [completionIndex, setCompletionIndex] = useState(-1);
31
+ const [completionMatches, setCompletionMatches] = useState<string[]>([]);
32
+
23
33
  valueRef.current = value;
24
34
 
25
35
  if (cursorOffsetRef.current > value.length) {
@@ -28,40 +38,162 @@ function TextInput({
28
38
 
29
39
  const [, setRenderTick] = useState(0);
30
40
 
41
+ const clearCompletion = () => {
42
+ setCompletionIndex(-1);
43
+ setCompletionMatches([]);
44
+ };
45
+
46
+ const getMatches = (text: string): string[] => {
47
+ if (!completionCommands || !text.startsWith("/") || text.includes(" ")) {
48
+ return [];
49
+ }
50
+ const prefix = text.toLowerCase();
51
+ return completionCommands.filter((cmd) =>
52
+ cmd.toLowerCase().startsWith(prefix),
53
+ );
54
+ };
55
+
31
56
  useInput(
32
57
  (input, key) => {
33
- if (
34
- key.upArrow ||
35
- key.downArrow ||
36
- (key.ctrl && input === "c") ||
37
- key.tab ||
38
- (key.shift && key.tab)
39
- ) {
58
+ if (key.upArrow && !key.shift && !key.meta) {
59
+ clearCompletion();
60
+ onHistoryUp?.();
61
+ cursorOffsetRef.current = Infinity;
62
+ setRenderTick((t) => t + 1);
63
+ return;
64
+ }
65
+ if (key.downArrow && !key.shift && !key.meta) {
66
+ clearCompletion();
67
+ onHistoryDown?.();
68
+ cursorOffsetRef.current = Infinity;
69
+ setRenderTick((t) => t + 1);
70
+ return;
71
+ }
72
+ if (key.ctrl && input === "c") {
73
+ return;
74
+ }
75
+
76
+ // Tab completion handling
77
+ if (key.tab) {
78
+ const currentValue = valueRef.current;
79
+
80
+ if (completionMatches.length > 0) {
81
+ // Already in completion mode — cycle through matches
82
+ const direction = key.shift ? -1 : 1;
83
+ const nextIndex =
84
+ (completionIndex + direction + completionMatches.length) %
85
+ completionMatches.length;
86
+ setCompletionIndex(nextIndex);
87
+
88
+ const completed = completionMatches[nextIndex]!;
89
+ valueRef.current = completed;
90
+ cursorOffsetRef.current = completed.length;
91
+ onChange(completed);
92
+ setRenderTick((t) => t + 1);
93
+ return;
94
+ }
95
+
96
+ // Start completion mode
97
+ const matches = getMatches(currentValue);
98
+ if (matches.length === 1) {
99
+ // Single match — accept immediately with trailing space
100
+ const completed = matches[0]! + " ";
101
+ valueRef.current = completed;
102
+ cursorOffsetRef.current = completed.length;
103
+ onChange(completed);
104
+ setRenderTick((t) => t + 1);
105
+ } else if (matches.length > 1) {
106
+ setCompletionMatches(matches);
107
+ const idx = key.shift ? matches.length - 1 : 0;
108
+ setCompletionIndex(idx);
109
+
110
+ const completed = matches[idx]!;
111
+ valueRef.current = completed;
112
+ cursorOffsetRef.current = completed.length;
113
+ onChange(completed);
114
+ setRenderTick((t) => t + 1);
115
+ }
40
116
  return;
41
117
  }
42
118
 
119
+ // Escape cancels completion mode
120
+ if (key.escape) {
121
+ if (completionMatches.length > 0) {
122
+ clearCompletion();
123
+ setRenderTick((t) => t + 1);
124
+ return;
125
+ }
126
+ }
127
+
128
+ // Enter accepts completion and submits
43
129
  if (key.return) {
44
- onSubmit?.(valueRef.current);
130
+ if (completionMatches.length > 0) {
131
+ // Append trailing space so the command is recognized by handleInput
132
+ const completed = valueRef.current + " ";
133
+ valueRef.current = completed;
134
+ cursorOffsetRef.current = completed.length;
135
+ onChange(completed);
136
+ clearCompletion();
137
+ onSubmit?.(completed);
138
+ } else {
139
+ clearCompletion();
140
+ onSubmit?.(valueRef.current);
141
+ }
45
142
  return;
46
143
  }
47
144
 
145
+ // Space accepts completion, then continues editing
146
+ if (input === " " && completionMatches.length > 0) {
147
+ clearCompletion();
148
+ // Let the space be inserted normally below
149
+ } else if (completionMatches.length > 0) {
150
+ // Any other key exits completion mode
151
+ clearCompletion();
152
+ }
153
+
48
154
  const currentValue = valueRef.current;
49
155
  const currentOffset = cursorOffsetRef.current;
50
156
  let nextValue = currentValue;
51
157
  let nextOffset = currentOffset;
52
158
 
53
- if (key.leftArrow) {
159
+ if (key.ctrl && input === "a") {
160
+ // Ctrl+A — move cursor to start
161
+ nextOffset = 0;
162
+ } else if (key.ctrl && input === "e") {
163
+ // Ctrl+E — move cursor to end
164
+ nextOffset = currentValue.length;
165
+ } else if (key.ctrl && input === "u") {
166
+ // Ctrl+U — clear line before cursor
167
+ nextValue = currentValue.slice(currentOffset);
168
+ nextOffset = 0;
169
+ } else if (key.ctrl && input === "k") {
170
+ // Ctrl+K — kill from cursor to end
171
+ nextValue = currentValue.slice(0, currentOffset);
172
+ } else if (key.ctrl && input === "w") {
173
+ // Ctrl+W — delete word backwards (handles tabs and other whitespace)
174
+ const before = currentValue.slice(0, currentOffset);
175
+ // Skip trailing whitespace, then find previous whitespace boundary
176
+ const match = before.match(/^(.*\s)?\S+\s*$/);
177
+ const wordStart = match?.[1]?.length ?? 0;
178
+ nextValue =
179
+ currentValue.slice(0, wordStart) + currentValue.slice(currentOffset);
180
+ nextOffset = wordStart;
181
+ } else if (key.leftArrow) {
54
182
  nextOffset = Math.max(0, currentOffset - 1);
55
183
  } else if (key.rightArrow) {
56
184
  nextOffset = Math.min(currentValue.length, currentOffset + 1);
57
185
  } else if (key.backspace || key.delete) {
58
186
  if (currentOffset > 0) {
59
- nextValue = currentValue.slice(0, currentOffset - 1) + currentValue.slice(currentOffset);
187
+ nextValue =
188
+ currentValue.slice(0, currentOffset - 1) +
189
+ currentValue.slice(currentOffset);
60
190
  nextOffset = currentOffset - 1;
61
191
  }
62
192
  } else {
63
193
  nextValue =
64
- currentValue.slice(0, currentOffset) + input + currentValue.slice(currentOffset);
194
+ currentValue.slice(0, currentOffset) +
195
+ input +
196
+ currentValue.slice(currentOffset);
65
197
  nextOffset = currentOffset + input.length;
66
198
  }
67
199
 
@@ -78,6 +210,14 @@ function TextInput({
78
210
  );
79
211
 
80
212
  const cursorOffset = cursorOffsetRef.current;
213
+ const isCompleting = completionMatches.length > 0;
214
+
215
+ // Build completion hint text
216
+ let completionHint = "";
217
+ if (isCompleting && completionMatches.length > 1) {
218
+ completionHint = ` [${completionIndex + 1}/${completionMatches.length}]`;
219
+ }
220
+
81
221
  let renderedValue: string;
82
222
  let renderedPlaceholder: string | undefined;
83
223
 
@@ -97,6 +237,9 @@ function TextInput({
97
237
  if (cursorOffset === value.length) {
98
238
  renderedValue += chalk.inverse(" ");
99
239
  }
240
+ if (completionHint) {
241
+ renderedValue += chalk.grey(completionHint);
242
+ }
100
243
  } else {
101
244
  renderedValue = chalk.inverse(" ");
102
245
  }
@@ -107,7 +250,11 @@ function TextInput({
107
250
 
108
251
  return (
109
252
  <Text>
110
- {placeholder ? (value.length > 0 ? renderedValue : renderedPlaceholder) : renderedValue}
253
+ {placeholder
254
+ ? value.length > 0
255
+ ? renderedValue
256
+ : renderedPlaceholder
257
+ : renderedValue}
111
258
  </Text>
112
259
  );
113
260
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { pair } from "./commands/pair";
9
9
  import { ps } from "./commands/ps";
10
10
  import { recover } from "./commands/recover";
11
11
  import { retire } from "./commands/retire";
12
+ import { setup } from "./commands/setup";
12
13
  import { sleep } from "./commands/sleep";
13
14
  import { ssh } from "./commands/ssh";
14
15
  import { tunnel } from "./commands/tunnel";
@@ -25,6 +26,7 @@ const commands = {
25
26
  ps,
26
27
  recover,
27
28
  retire,
29
+ setup,
28
30
  sleep,
29
31
  ssh,
30
32
  tunnel,
@@ -59,6 +61,7 @@ async function main() {
59
61
  );
60
62
  console.log(" recover Restore a previously retired local assistant");
61
63
  console.log(" retire Delete an assistant instance");
64
+ console.log(" setup Configure API keys interactively");
62
65
  console.log(" sleep Stop the assistant process");
63
66
  console.log(" ssh SSH into a remote assistant instance");
64
67
  console.log(" tunnel Create a tunnel for a locally hosted assistant");
@@ -190,11 +190,7 @@ export function migrateLegacyEntry(raw: Record<string, unknown>): boolean {
190
190
  mutated = true;
191
191
  }
192
192
  if (typeof res.pidFile !== "string") {
193
- res.pidFile = join(
194
- res.instanceDir as string,
195
- ".vellum",
196
- "vellum.pid",
197
- );
193
+ res.pidFile = join(res.instanceDir as string, ".vellum", "vellum.pid");
198
194
  mutated = true;
199
195
  }
200
196
  }
@@ -363,7 +359,6 @@ export async function allocateLocalResources(
363
359
  if (existingLocals.length === 0) {
364
360
  const home = homedir();
365
361
  const vellumDir = join(home, ".vellum");
366
- mkdirSync(vellumDir, { recursive: true });
367
362
  return {
368
363
  instanceDir: home,
369
364
  daemonPort: DEFAULT_DAEMON_PORT,
@@ -8,7 +8,13 @@ export const DEFAULT_DAEMON_PORT = 7821;
8
8
  export const DEFAULT_GATEWAY_PORT = 7830;
9
9
  export const DEFAULT_QDRANT_PORT = 6333;
10
10
 
11
- export const VALID_REMOTE_HOSTS = ["local", "gcp", "aws", "docker", "custom"] as const;
11
+ export const VALID_REMOTE_HOSTS = [
12
+ "local",
13
+ "gcp",
14
+ "aws",
15
+ "docker",
16
+ "custom",
17
+ ] as const;
12
18
  export type RemoteHost = (typeof VALID_REMOTE_HOSTS)[number];
13
19
  export const VALID_SPECIES = ["openclaw", "vellum"] as const;
14
20
  export type Species = (typeof VALID_SPECIES)[number];