@vellumai/cli 0.8.0 → 0.8.2
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__/backup.test.ts +13 -3
- package/src/__tests__/input-history.test.ts +102 -0
- package/src/__tests__/orphan-detection.test.ts +287 -0
- package/src/__tests__/preload.ts +5 -1
- package/src/__tests__/provider-secrets.test.ts +290 -0
- package/src/__tests__/ps-platform-status.test.ts +182 -0
- package/src/__tests__/search-provider-env-var-parity.test.ts +48 -0
- package/src/__tests__/setup.test.ts +296 -0
- package/src/__tests__/sync-events.test.ts +54 -0
- package/src/__tests__/teleport.test.ts +190 -163
- package/src/commands/client.ts +128 -10
- package/src/commands/events.ts +13 -1
- package/src/commands/login.ts +3 -2
- package/src/commands/ps.ts +28 -17
- package/src/commands/setup.ts +101 -96
- package/src/components/DefaultMainScreen.tsx +80 -128
- package/src/lib/__tests__/docker.test.ts +11 -0
- package/src/lib/assistant-config.ts +69 -2
- package/src/lib/client-identity.ts +1 -0
- package/src/lib/environments/paths.ts +21 -0
- package/src/lib/input-history.ts +5 -8
- package/src/lib/orphan-detection.ts +66 -1
- package/src/lib/platform-client.ts +8 -7
- package/src/lib/provider-secrets.ts +413 -0
- package/src/lib/statefulset.ts +12 -0
- package/src/lib/sync-cloud-assistants.ts +39 -18
- package/src/lib/upgrade-lifecycle.ts +9 -73
- package/src/shared/provider-env-vars.ts +15 -8
- package/src/lib/doctor-client.ts +0 -153
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
3
2
|
import { basename } from "path";
|
|
4
3
|
import {
|
|
5
4
|
useCallback,
|
|
@@ -14,7 +13,6 @@ import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
|
|
|
14
13
|
import { removeAssistantEntry } from "../lib/assistant-config";
|
|
15
14
|
|
|
16
15
|
import { SPECIES_CONFIG, type Species } from "../lib/constants";
|
|
17
|
-
import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
|
|
18
16
|
import { checkHealth } from "../lib/health-check";
|
|
19
17
|
import { appendHistory, loadHistory } from "../lib/input-history";
|
|
20
18
|
import { tuiLog } from "../lib/tui-log";
|
|
@@ -41,7 +39,6 @@ export const ANSI = {
|
|
|
41
39
|
export const SLASH_COMMANDS = [
|
|
42
40
|
"/btw",
|
|
43
41
|
"/clear",
|
|
44
|
-
"/doctor",
|
|
45
42
|
"/exit",
|
|
46
43
|
"/help",
|
|
47
44
|
"/q",
|
|
@@ -62,7 +59,7 @@ const HEADER_PREFIX_UNICODE = "── Vellum ";
|
|
|
62
59
|
const HEADER_PREFIX_ASCII = "-- Vellum ";
|
|
63
60
|
|
|
64
61
|
// Left panel structure: HEADER lines + art + FOOTER lines
|
|
65
|
-
const LEFT_HEADER_LINES =
|
|
62
|
+
const LEFT_HEADER_LINES = 4; // spacer + eyebrow + title + spacer
|
|
66
63
|
const LEFT_FOOTER_LINES = 3; // spacer + runtimeUrl + dirName
|
|
67
64
|
|
|
68
65
|
// Right panel structure
|
|
@@ -70,7 +67,7 @@ const TIPS = [
|
|
|
70
67
|
"Send a message to start chatting",
|
|
71
68
|
"Use /help to see available commands",
|
|
72
69
|
];
|
|
73
|
-
const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant, Species, Status — each with heading + value
|
|
70
|
+
const RIGHT_PANEL_INFO_SECTIONS = 3; // Assistant ID, Species, Status — each with heading + value
|
|
74
71
|
const RIGHT_PANEL_SPACERS = 2; // top spacer + spacer between tips and info
|
|
75
72
|
const RIGHT_PANEL_TIPS_HEADING = 1;
|
|
76
73
|
const RIGHT_PANEL_LINE_COUNT =
|
|
@@ -103,7 +100,7 @@ const MIN_FEED_ROWS = 3;
|
|
|
103
100
|
// Feed item height estimation
|
|
104
101
|
const TOOL_CALL_CHROME_LINES = 2; // header (┌) + footer (└)
|
|
105
102
|
const MESSAGE_SPACING = 1;
|
|
106
|
-
const HELP_DISPLAY_HEIGHT =
|
|
103
|
+
const HELP_DISPLAY_HEIGHT = 7;
|
|
107
104
|
|
|
108
105
|
interface ListMessagesResponse {
|
|
109
106
|
messages: RuntimeMessage[];
|
|
@@ -333,6 +330,8 @@ interface SseEvent {
|
|
|
333
330
|
allowedDomains?: string[];
|
|
334
331
|
// message_complete fields
|
|
335
332
|
source?: "main" | "aux";
|
|
333
|
+
// sync_changed fields
|
|
334
|
+
tags?: string[];
|
|
336
335
|
[key: string]: unknown;
|
|
337
336
|
}
|
|
338
337
|
|
|
@@ -666,6 +665,21 @@ function truncateValue(value: unknown, maxLen: number): string {
|
|
|
666
665
|
return serialized;
|
|
667
666
|
}
|
|
668
667
|
|
|
668
|
+
function formatHeaderTitle(assistantName?: string): string {
|
|
669
|
+
const rawTitle = assistantName?.trim() || "Meet your Assistant!";
|
|
670
|
+
const title = rawTitle.replace(/\s+/g, " ");
|
|
671
|
+
const maxTitleLength = LEFT_PANEL_WIDTH - 2;
|
|
672
|
+
const displayTitle =
|
|
673
|
+
title.length > maxTitleLength
|
|
674
|
+
? title.slice(0, maxTitleLength - 3) + "..."
|
|
675
|
+
: title;
|
|
676
|
+
return ` ${displayTitle}`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function formatHeaderEyebrow(): string {
|
|
680
|
+
return " Assistant";
|
|
681
|
+
}
|
|
682
|
+
|
|
669
683
|
interface ToolCallDisplayProps {
|
|
670
684
|
tc: ToolCallInfo;
|
|
671
685
|
}
|
|
@@ -732,10 +746,6 @@ function HelpDisplay(): ReactElement {
|
|
|
732
746
|
{" /btw <question> "}
|
|
733
747
|
<Text dimColor>Ask a side question while the assistant is working</Text>
|
|
734
748
|
</Text>
|
|
735
|
-
<Text>
|
|
736
|
-
{" /doctor [question] "}
|
|
737
|
-
<Text dimColor>Run diagnostics on the remote instance via SSH</Text>
|
|
738
|
-
</Text>
|
|
739
749
|
<Text>
|
|
740
750
|
{" /retire "}
|
|
741
751
|
<Text dimColor>Retire the remote instance and exit</Text>
|
|
@@ -859,28 +869,33 @@ function stripAnsi(str: string): string {
|
|
|
859
869
|
interface DefaultMainScreenProps {
|
|
860
870
|
runtimeUrl: string;
|
|
861
871
|
assistantId: string;
|
|
872
|
+
assistantName?: string;
|
|
862
873
|
species: Species;
|
|
863
874
|
healthStatus?: string;
|
|
864
875
|
}
|
|
865
876
|
|
|
866
877
|
interface StyledLine {
|
|
867
878
|
text: string;
|
|
868
|
-
style: "heading" | "dim" | "normal";
|
|
879
|
+
style: "heading" | "dim" | "normal" | "eyebrow" | "title" | "art";
|
|
869
880
|
}
|
|
870
881
|
|
|
871
882
|
function CompactHeader({
|
|
883
|
+
assistantName,
|
|
872
884
|
species,
|
|
873
885
|
healthStatus,
|
|
874
886
|
totalWidth,
|
|
875
887
|
}: {
|
|
888
|
+
assistantName?: string;
|
|
876
889
|
species: Species;
|
|
877
890
|
healthStatus?: string;
|
|
878
891
|
totalWidth: number;
|
|
879
892
|
}): ReactElement {
|
|
880
|
-
const config = SPECIES_CONFIG[species];
|
|
881
893
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
882
894
|
const status = healthStatus ?? "checking...";
|
|
883
|
-
const
|
|
895
|
+
const identity = assistantName?.trim() || species;
|
|
896
|
+
const compactIdentity =
|
|
897
|
+
identity.length > 18 ? `${identity.slice(0, 15)}...` : identity;
|
|
898
|
+
const label = ` ${compactIdentity} ${statusEmoji(status)} `;
|
|
884
899
|
const prefix = "── Vellum";
|
|
885
900
|
const suffix = "──";
|
|
886
901
|
const fillLen = Math.max(
|
|
@@ -902,13 +917,13 @@ function CompactHeader({
|
|
|
902
917
|
function DefaultMainScreen({
|
|
903
918
|
runtimeUrl,
|
|
904
919
|
assistantId,
|
|
920
|
+
assistantName,
|
|
905
921
|
species,
|
|
906
922
|
healthStatus,
|
|
907
923
|
}: DefaultMainScreenProps): ReactElement {
|
|
908
924
|
const cwd = process.cwd();
|
|
909
925
|
const dirName = basename(cwd);
|
|
910
926
|
const config = SPECIES_CONFIG[species];
|
|
911
|
-
const art = config.art;
|
|
912
927
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
913
928
|
const caps = getTerminalCapabilities();
|
|
914
929
|
const headerPrefix = caps.unicodeSupported
|
|
@@ -924,6 +939,7 @@ function DefaultMainScreen({
|
|
|
924
939
|
if (isCompact) {
|
|
925
940
|
return (
|
|
926
941
|
<CompactHeader
|
|
942
|
+
assistantName={assistantName}
|
|
927
943
|
species={species}
|
|
928
944
|
healthStatus={healthStatus}
|
|
929
945
|
totalWidth={totalWidth}
|
|
@@ -931,16 +947,21 @@ function DefaultMainScreen({
|
|
|
931
947
|
);
|
|
932
948
|
}
|
|
933
949
|
|
|
950
|
+
const art = config.art;
|
|
934
951
|
const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
|
|
935
952
|
|
|
936
|
-
const leftLines = [
|
|
937
|
-
" ",
|
|
938
|
-
|
|
939
|
-
"
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
953
|
+
const leftLines: StyledLine[] = [
|
|
954
|
+
{ text: " ", style: "normal" },
|
|
955
|
+
{ text: formatHeaderEyebrow(), style: "eyebrow" },
|
|
956
|
+
{ text: formatHeaderTitle(assistantName), style: "title" },
|
|
957
|
+
{ text: " ", style: "normal" },
|
|
958
|
+
...art.map((line) => ({
|
|
959
|
+
text: ` ${stripAnsi(line)}`,
|
|
960
|
+
style: "art" as const,
|
|
961
|
+
})),
|
|
962
|
+
{ text: " ", style: "normal" },
|
|
963
|
+
{ text: ` ${runtimeUrl}`, style: "dim" },
|
|
964
|
+
{ text: ` ~/${dirName}`, style: "dim" },
|
|
944
965
|
];
|
|
945
966
|
|
|
946
967
|
const rightLines: StyledLine[] = [
|
|
@@ -948,7 +969,7 @@ function DefaultMainScreen({
|
|
|
948
969
|
{ text: "Tips for getting started", style: "heading" },
|
|
949
970
|
...TIPS.map((t) => ({ text: t, style: "normal" as const })),
|
|
950
971
|
{ text: " ", style: "normal" },
|
|
951
|
-
{ text: "Assistant", style: "heading" },
|
|
972
|
+
{ text: "Assistant ID", style: "heading" },
|
|
952
973
|
{ text: assistantId, style: "dim" },
|
|
953
974
|
{ text: "Species", style: "heading" },
|
|
954
975
|
{
|
|
@@ -970,29 +991,37 @@ function DefaultMainScreen({
|
|
|
970
991
|
<Box flexDirection="row">
|
|
971
992
|
<Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
|
|
972
993
|
{Array.from({ length: maxLines }, (_, i) => {
|
|
973
|
-
const
|
|
974
|
-
if (
|
|
994
|
+
const item = leftLines[i];
|
|
995
|
+
if (!item) return <Text key={i}> </Text>;
|
|
996
|
+
if (item.style === "eyebrow") {
|
|
997
|
+
return (
|
|
998
|
+
<Text key={i} color={caps.isDumb ? undefined : accentColor}>
|
|
999
|
+
{item.text}
|
|
1000
|
+
</Text>
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (item.style === "title") {
|
|
975
1004
|
return (
|
|
976
1005
|
<Text key={i} bold>
|
|
977
|
-
{
|
|
1006
|
+
{item.text}
|
|
978
1007
|
</Text>
|
|
979
1008
|
);
|
|
980
1009
|
}
|
|
981
|
-
if (
|
|
1010
|
+
if (item.style === "art") {
|
|
982
1011
|
return (
|
|
983
1012
|
<Text key={i} color={caps.isDumb ? undefined : accentColor}>
|
|
984
|
-
{
|
|
1013
|
+
{item.text}
|
|
985
1014
|
</Text>
|
|
986
1015
|
);
|
|
987
1016
|
}
|
|
988
|
-
if (
|
|
1017
|
+
if (item.style === "dim") {
|
|
989
1018
|
return (
|
|
990
1019
|
<Text key={i} dimColor>
|
|
991
|
-
{
|
|
1020
|
+
{item.text}
|
|
992
1021
|
</Text>
|
|
993
1022
|
);
|
|
994
1023
|
}
|
|
995
|
-
return <Text key={i}>{
|
|
1024
|
+
return <Text key={i}>{item.text}</Text>;
|
|
996
1025
|
})}
|
|
997
1026
|
</Box>
|
|
998
1027
|
<Box flexDirection="column" width={rightPanelWidth}>
|
|
@@ -1113,8 +1142,7 @@ function calculateHeaderHeight(
|
|
|
1113
1142
|
if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
|
|
1114
1143
|
return COMPACT_HEADER_HEIGHT;
|
|
1115
1144
|
}
|
|
1116
|
-
const
|
|
1117
|
-
const artLength = config.art.length;
|
|
1145
|
+
const artLength = SPECIES_CONFIG[species].art.length;
|
|
1118
1146
|
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
1119
1147
|
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
1120
1148
|
return maxLines + HEADER_CHROME_LINES;
|
|
@@ -1126,12 +1154,11 @@ export function render(
|
|
|
1126
1154
|
runtimeUrl: string,
|
|
1127
1155
|
assistantId: string,
|
|
1128
1156
|
species: Species,
|
|
1157
|
+
assistantName?: string,
|
|
1129
1158
|
): number {
|
|
1130
1159
|
const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
1131
1160
|
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1132
|
-
|
|
1133
|
-
const config = SPECIES_CONFIG[species];
|
|
1134
|
-
const art = config.art;
|
|
1161
|
+
const art = SPECIES_CONFIG[species].art;
|
|
1135
1162
|
|
|
1136
1163
|
const leftLineCount = LEFT_HEADER_LINES + art.length + LEFT_FOOTER_LINES;
|
|
1137
1164
|
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
@@ -1140,6 +1167,7 @@ export function render(
|
|
|
1140
1167
|
<DefaultMainScreen
|
|
1141
1168
|
runtimeUrl={runtimeUrl}
|
|
1142
1169
|
assistantId={assistantId}
|
|
1170
|
+
assistantName={assistantName}
|
|
1143
1171
|
species={species}
|
|
1144
1172
|
/>,
|
|
1145
1173
|
{ exitOnCtrlC: false },
|
|
@@ -1358,6 +1386,7 @@ export interface ChatAppHandle {
|
|
|
1358
1386
|
interface ChatAppProps {
|
|
1359
1387
|
runtimeUrl: string;
|
|
1360
1388
|
assistantId: string;
|
|
1389
|
+
assistantName?: string;
|
|
1361
1390
|
species: Species;
|
|
1362
1391
|
/** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
|
|
1363
1392
|
* { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
|
|
@@ -1371,6 +1400,7 @@ interface ChatAppProps {
|
|
|
1371
1400
|
function ChatApp({
|
|
1372
1401
|
runtimeUrl,
|
|
1373
1402
|
assistantId,
|
|
1403
|
+
assistantName,
|
|
1374
1404
|
species,
|
|
1375
1405
|
auth,
|
|
1376
1406
|
project,
|
|
@@ -1403,11 +1433,9 @@ function ChatApp({
|
|
|
1403
1433
|
const connectedRef = useRef(false);
|
|
1404
1434
|
const connectingRef = useRef(false);
|
|
1405
1435
|
const seenMessageIdsRef = useRef(new Set<string>());
|
|
1406
|
-
const chatLogRef = useRef<ChatLogEntry[]>([]);
|
|
1407
1436
|
const sseAbortRef = useRef<AbortController | null>(null);
|
|
1408
1437
|
const streamingTextRef = useRef("");
|
|
1409
1438
|
const streamingToolCallsRef = useRef<ToolCallInfo[]>([]);
|
|
1410
|
-
const doctorSessionIdRef = useRef(randomUUID());
|
|
1411
1439
|
const handleRef_ = useRef<ChatAppHandle | null>(null);
|
|
1412
1440
|
|
|
1413
1441
|
const { stdout } = useStdout();
|
|
@@ -1838,10 +1866,6 @@ function ChatApp({
|
|
|
1838
1866
|
};
|
|
1839
1867
|
seenMessageIdsRef.current.add(msg.id);
|
|
1840
1868
|
hRef.addMessage(msg);
|
|
1841
|
-
chatLogRef.current.push({
|
|
1842
|
-
role: "assistant",
|
|
1843
|
-
content: text,
|
|
1844
|
-
});
|
|
1845
1869
|
process.stdout.write("\x07");
|
|
1846
1870
|
}
|
|
1847
1871
|
|
|
@@ -1856,6 +1880,11 @@ function ChatApp({
|
|
|
1856
1880
|
hRef.setBusy(false);
|
|
1857
1881
|
break;
|
|
1858
1882
|
|
|
1883
|
+
case "sync_changed":
|
|
1884
|
+
// The interactive CLI does not currently keep any sync-tagged
|
|
1885
|
+
// caches, so generic invalidations are intentionally ignored.
|
|
1886
|
+
break;
|
|
1887
|
+
|
|
1859
1888
|
default:
|
|
1860
1889
|
// Ignore events we don't handle (activity state, traces, etc.)
|
|
1861
1890
|
break;
|
|
@@ -2005,79 +2034,6 @@ function ChatApp({
|
|
|
2005
2034
|
return;
|
|
2006
2035
|
}
|
|
2007
2036
|
|
|
2008
|
-
if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
|
|
2009
|
-
if (!project || !zone) {
|
|
2010
|
-
h.showError(
|
|
2011
|
-
"No instance info available. Connect to a hatched instance first.",
|
|
2012
|
-
);
|
|
2013
|
-
return;
|
|
2014
|
-
}
|
|
2015
|
-
const userPrompt = trimmed.slice("/doctor".length).trim() || undefined;
|
|
2016
|
-
const recentChatContext = chatLogRef.current.slice(-20);
|
|
2017
|
-
|
|
2018
|
-
chatLogRef.current.push({ role: "user", content: trimmed });
|
|
2019
|
-
|
|
2020
|
-
if (userPrompt) {
|
|
2021
|
-
const doctorUserMsg: RuntimeMessage = {
|
|
2022
|
-
id: "local-user-" + Date.now(),
|
|
2023
|
-
role: "user",
|
|
2024
|
-
content: userPrompt,
|
|
2025
|
-
timestamp: new Date().toISOString(),
|
|
2026
|
-
label: "You (to Doctor):",
|
|
2027
|
-
};
|
|
2028
|
-
h.addMessage(doctorUserMsg);
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
h.showSpinner(`Analyzing ${assistantId}...`);
|
|
2032
|
-
|
|
2033
|
-
try {
|
|
2034
|
-
const result = await callDoctorDaemon(
|
|
2035
|
-
assistantId,
|
|
2036
|
-
project,
|
|
2037
|
-
zone,
|
|
2038
|
-
userPrompt,
|
|
2039
|
-
(event) => {
|
|
2040
|
-
switch (event.phase) {
|
|
2041
|
-
case "invoking_prompt":
|
|
2042
|
-
handleRef_.current?.showSpinner(
|
|
2043
|
-
`Analyzing ${assistantId}...`,
|
|
2044
|
-
);
|
|
2045
|
-
break;
|
|
2046
|
-
case "calling_tool":
|
|
2047
|
-
handleRef_.current?.showSpinner(
|
|
2048
|
-
`Running ${event.toolName ?? "tool"} on ${assistantId}...`,
|
|
2049
|
-
);
|
|
2050
|
-
break;
|
|
2051
|
-
case "processing_tool_result":
|
|
2052
|
-
handleRef_.current?.showSpinner(
|
|
2053
|
-
`Reviewing diagnostics for ${assistantId}...`,
|
|
2054
|
-
);
|
|
2055
|
-
break;
|
|
2056
|
-
}
|
|
2057
|
-
},
|
|
2058
|
-
doctorSessionIdRef.current,
|
|
2059
|
-
recentChatContext,
|
|
2060
|
-
);
|
|
2061
|
-
h.hideSpinner();
|
|
2062
|
-
if (result.recommendation) {
|
|
2063
|
-
h.addStatus(`Recommendation:\n${result.recommendation}`);
|
|
2064
|
-
chatLogRef.current.push({
|
|
2065
|
-
role: "assistant",
|
|
2066
|
-
content: result.recommendation,
|
|
2067
|
-
});
|
|
2068
|
-
} else if (result.error) {
|
|
2069
|
-
h.showError(result.error);
|
|
2070
|
-
chatLogRef.current.push({ role: "error", content: result.error });
|
|
2071
|
-
}
|
|
2072
|
-
} catch (err) {
|
|
2073
|
-
h.hideSpinner();
|
|
2074
|
-
const errorMsg = `Doctor assistant unreachable: ${err instanceof Error ? err.message : err}`;
|
|
2075
|
-
h.showError(errorMsg);
|
|
2076
|
-
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
2077
|
-
}
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
2037
|
// If a connection attempt is already in progress, don't silently drop input
|
|
2082
2038
|
if (connectingRef.current) {
|
|
2083
2039
|
h.addStatus(
|
|
@@ -2194,7 +2150,6 @@ function ChatApp({
|
|
|
2194
2150
|
);
|
|
2195
2151
|
clearTimeout(timeoutId);
|
|
2196
2152
|
if (sendResult.accepted) {
|
|
2197
|
-
chatLogRef.current.push({ role: "user", content: trimmed });
|
|
2198
2153
|
h.addStatus(
|
|
2199
2154
|
"Message queued — will be processed after current response",
|
|
2200
2155
|
"gray",
|
|
@@ -2226,7 +2181,6 @@ function ChatApp({
|
|
|
2226
2181
|
return;
|
|
2227
2182
|
}
|
|
2228
2183
|
|
|
2229
|
-
chatLogRef.current.push({ role: "user", content: trimmed });
|
|
2230
2184
|
seenMessageIdsRef.current.add("pending-user-" + Date.now());
|
|
2231
2185
|
|
|
2232
2186
|
h.showSpinner("Sending...");
|
|
@@ -2257,7 +2211,6 @@ function ChatApp({
|
|
|
2257
2211
|
const errorMsg =
|
|
2258
2212
|
sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
2259
2213
|
h.showError(errorMsg);
|
|
2260
|
-
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
2261
2214
|
return;
|
|
2262
2215
|
}
|
|
2263
2216
|
|
|
@@ -2265,15 +2218,7 @@ function ChatApp({
|
|
|
2265
2218
|
// racing with SSE events that may arrive during the sendMessage await.
|
|
2266
2219
|
h.showSpinner("Working...");
|
|
2267
2220
|
},
|
|
2268
|
-
[
|
|
2269
|
-
runtimeUrl,
|
|
2270
|
-
assistantId,
|
|
2271
|
-
auth,
|
|
2272
|
-
project,
|
|
2273
|
-
zone,
|
|
2274
|
-
cleanup,
|
|
2275
|
-
ensureConnected,
|
|
2276
|
-
],
|
|
2221
|
+
[runtimeUrl, assistantId, auth, project, zone, cleanup, ensureConnected],
|
|
2277
2222
|
);
|
|
2278
2223
|
|
|
2279
2224
|
const handleSubmit = useCallback(
|
|
@@ -2463,6 +2408,7 @@ function ChatApp({
|
|
|
2463
2408
|
<DefaultMainScreen
|
|
2464
2409
|
runtimeUrl={runtimeUrl}
|
|
2465
2410
|
assistantId={assistantId}
|
|
2411
|
+
assistantName={assistantName}
|
|
2466
2412
|
species={species}
|
|
2467
2413
|
healthStatus={healthStatus}
|
|
2468
2414
|
/>
|
|
@@ -2582,7 +2528,12 @@ export function renderChatApp(
|
|
|
2582
2528
|
assistantId: string,
|
|
2583
2529
|
species: Species,
|
|
2584
2530
|
onExit: () => void,
|
|
2585
|
-
options?: {
|
|
2531
|
+
options?: {
|
|
2532
|
+
auth?: Record<string, string>;
|
|
2533
|
+
project?: string;
|
|
2534
|
+
zone?: string;
|
|
2535
|
+
assistantName?: string;
|
|
2536
|
+
},
|
|
2586
2537
|
): ChatAppInstance {
|
|
2587
2538
|
let chatHandle: ChatAppHandle | null = null;
|
|
2588
2539
|
|
|
@@ -2590,6 +2541,7 @@ export function renderChatApp(
|
|
|
2590
2541
|
<ChatApp
|
|
2591
2542
|
runtimeUrl={runtimeUrl}
|
|
2592
2543
|
assistantId={assistantId}
|
|
2544
|
+
assistantName={options?.assistantName}
|
|
2593
2545
|
species={species}
|
|
2594
2546
|
auth={options?.auth}
|
|
2595
2547
|
project={options?.project}
|
|
@@ -128,6 +128,17 @@ describe("buildServiceRunArgs — gateway", () => {
|
|
|
128
128
|
buildGatewayArgs().some((arg) => arg.startsWith("VELAY_BASE_URL=")),
|
|
129
129
|
).toBe(false);
|
|
130
130
|
});
|
|
131
|
+
|
|
132
|
+
test("forces gateway to run as uid 0 so it can connect to the assistant's root-owned IPC socket (mirrors K8s securityContext.runAsUser=0)", () => {
|
|
133
|
+
const args = buildGatewayArgs();
|
|
134
|
+
const userIdx = args.indexOf("--user");
|
|
135
|
+
expect(userIdx).toBeGreaterThan(-1);
|
|
136
|
+
expect(args[userIdx + 1]).toBe("0");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("assistant container does NOT get a --user override (image USER root wins)", () => {
|
|
140
|
+
expect(buildAssistantArgs().includes("--user")).toBe(false);
|
|
141
|
+
});
|
|
131
142
|
});
|
|
132
143
|
|
|
133
144
|
describe("VELLUM_AVATAR_DEVICE passthrough", () => {
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
getMultiInstanceDir,
|
|
19
19
|
} from "./environments/paths.js";
|
|
20
20
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
21
|
+
import { SEEDS } from "./environments/seeds.js";
|
|
22
|
+
import type { EnvironmentDefinition } from "./environments/types.js";
|
|
21
23
|
import { probePort } from "./port-probe.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -65,6 +67,10 @@ export interface ContainerInfo {
|
|
|
65
67
|
|
|
66
68
|
export interface AssistantEntry {
|
|
67
69
|
assistantId: string;
|
|
70
|
+
/** Platform-provided display name, when available. */
|
|
71
|
+
name?: string;
|
|
72
|
+
/** Older lockfile key for the display name, if present. */
|
|
73
|
+
assistantName?: string;
|
|
68
74
|
runtimeUrl: string;
|
|
69
75
|
/** Loopback URL for same-machine health checks (e.g. `http://127.0.0.1:7831`).
|
|
70
76
|
* Avoids mDNS resolution issues when the machine checks its own gateway. */
|
|
@@ -327,6 +333,69 @@ export function loadAllAssistants(): AssistantEntry[] {
|
|
|
327
333
|
return readAssistants();
|
|
328
334
|
}
|
|
329
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Read the first existing lockfile for an explicitly-provided environment,
|
|
338
|
+
* without applying legacy migrations. This is the cross-env read path used by
|
|
339
|
+
* {@link loadAllAssistantsAcrossEnvs}: it deliberately bypasses
|
|
340
|
+
* {@link readLockfile} (which always resolves the *current* env) so callers
|
|
341
|
+
* can enumerate state from every env without flipping `process.env` or the
|
|
342
|
+
* persisted default. Migrations are skipped because we never want to write
|
|
343
|
+
* to another env's lockfile from the current env's process.
|
|
344
|
+
*/
|
|
345
|
+
function readLockfileForEnv(env: EnvironmentDefinition): LockfileData {
|
|
346
|
+
for (const lockfilePath of getLockfilePaths(env)) {
|
|
347
|
+
if (!existsSync(lockfilePath)) continue;
|
|
348
|
+
try {
|
|
349
|
+
const raw = readFileSync(lockfilePath, "utf-8");
|
|
350
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
351
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
352
|
+
return parsed as LockfileData;
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
// Malformed; try next candidate
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return {};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Load assistant entries from every known environment's lockfile.
|
|
363
|
+
*
|
|
364
|
+
* Each {@link SEEDS} entry has its own on-host data layout (config dir,
|
|
365
|
+
* lockfile path, data dir). A running assistant from `dev` is invisible to
|
|
366
|
+
* `loadAllAssistants()` when the current env is `local`, but its host
|
|
367
|
+
* processes (daemon/gateway/qdrant) still show up in `ps ax`. The orphan
|
|
368
|
+
* detector and `vellum clean` need the union of all envs' entries to avoid
|
|
369
|
+
* misclassifying — or worse, killing — another env's running services.
|
|
370
|
+
*
|
|
371
|
+
* Optional `envs` override is provided for testability so call sites can
|
|
372
|
+
* inject a curated env list with `lockfileDirOverride` set, without having
|
|
373
|
+
* to manipulate the global SEEDS table or process.env.
|
|
374
|
+
*/
|
|
375
|
+
export function loadAllAssistantsAcrossEnvs(
|
|
376
|
+
envs?: EnvironmentDefinition[],
|
|
377
|
+
): AssistantEntry[] {
|
|
378
|
+
const envList = envs ?? Object.values(SEEDS).map((env) => ({ ...env }));
|
|
379
|
+
const all: AssistantEntry[] = [];
|
|
380
|
+
for (const env of envList) {
|
|
381
|
+
const data = readLockfileForEnv(env);
|
|
382
|
+
const entries = data.assistants;
|
|
383
|
+
if (!Array.isArray(entries)) continue;
|
|
384
|
+
for (const raw of entries) {
|
|
385
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
386
|
+
const entry = raw as AssistantEntry;
|
|
387
|
+
if (
|
|
388
|
+
typeof entry.assistantId !== "string" ||
|
|
389
|
+
typeof entry.runtimeUrl !== "string"
|
|
390
|
+
) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
all.push(entry);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return all;
|
|
397
|
+
}
|
|
398
|
+
|
|
330
399
|
export function getActiveAssistant(): string | null {
|
|
331
400
|
const data = readLockfile();
|
|
332
401
|
return data.activeAssistant ?? null;
|
|
@@ -507,5 +576,3 @@ export function getLockfilePlatformBaseUrl(): string | undefined {
|
|
|
507
576
|
if (typeof url === "string" && url.trim()) return url.trim();
|
|
508
577
|
return undefined;
|
|
509
578
|
}
|
|
510
|
-
|
|
511
|
-
|
|
@@ -110,6 +110,21 @@ export function getStateDir(env: EnvironmentDefinition): string {
|
|
|
110
110
|
return join(xdgDataHome(), `vellum-${env.name}`);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Path to the interactive CLI's input history file.
|
|
115
|
+
*
|
|
116
|
+
* Follows the XDG Base Directory spec: history files are state data
|
|
117
|
+
* (persistent across runs but not portable / user-owned content), so they
|
|
118
|
+
* belong under `$XDG_STATE_HOME`, mirroring `bash`, `zsh`, `psql`, and `gh`.
|
|
119
|
+
* Defaults to `~/.local/state/vellum/input-history`.
|
|
120
|
+
*
|
|
121
|
+
* Not environment-scoped: terminal input history is per-user, not per-assistant,
|
|
122
|
+
* so dev and prod CLIs share the same history file.
|
|
123
|
+
*/
|
|
124
|
+
export function getInputHistoryPath(): string {
|
|
125
|
+
return join(xdgStateHome(), "vellum", "input-history");
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
/**
|
|
114
129
|
* Named port constants derived from `DEFAULT_PORTS`.
|
|
115
130
|
* These are the ports the assistant and gateway services bind to *inside*
|
|
@@ -127,3 +142,9 @@ function xdgDataHome(): string {
|
|
|
127
142
|
function xdgConfigHome(): string {
|
|
128
143
|
return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
129
144
|
}
|
|
145
|
+
|
|
146
|
+
function xdgStateHome(): string {
|
|
147
|
+
return (
|
|
148
|
+
process.env.XDG_STATE_HOME?.trim() || join(homedir(), ".local", "state")
|
|
149
|
+
);
|
|
150
|
+
}
|
package/src/lib/input-history.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from "path";
|
|
2
|
+
import { dirname } from "path";
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
import { getInputHistoryPath } from "./environments/paths.js";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
return join(homedir(), ".vellum", "input-history");
|
|
9
|
-
}
|
|
6
|
+
const MAX_ENTRIES = 1000;
|
|
10
7
|
|
|
11
8
|
export function loadHistory(): string[] {
|
|
12
9
|
try {
|
|
13
|
-
const path =
|
|
10
|
+
const path = getInputHistoryPath();
|
|
14
11
|
if (!existsSync(path)) return [];
|
|
15
12
|
const content = readFileSync(path, "utf-8");
|
|
16
13
|
return content
|
|
@@ -26,7 +23,7 @@ export function appendHistory(entry: string): void {
|
|
|
26
23
|
const trimmed = entry.trim();
|
|
27
24
|
if (!trimmed || trimmed.startsWith("/")) return;
|
|
28
25
|
try {
|
|
29
|
-
const path =
|
|
26
|
+
const path = getInputHistoryPath();
|
|
30
27
|
const dir = dirname(path);
|
|
31
28
|
if (!existsSync(dir)) {
|
|
32
29
|
mkdirSync(dir, { recursive: true });
|