@vellumai/cli 0.8.1 → 0.8.3
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/README.md +24 -32
- package/package.json +1 -1
- package/src/__tests__/backup.test.ts +13 -3
- package/src/__tests__/config-utils.test.ts +31 -1
- package/src/__tests__/hatch-provider-secrets.test.ts +284 -0
- package/src/__tests__/input-history.test.ts +102 -0
- package/src/__tests__/preload.ts +5 -1
- package/src/__tests__/provider-secrets.test.ts +290 -0
- package/src/__tests__/setup.test.ts +360 -0
- package/src/__tests__/teleport.test.ts +191 -163
- package/src/commands/client.ts +57 -1
- package/src/commands/hatch.ts +53 -20
- package/src/commands/setup.ts +134 -95
- package/src/commands/teleport.ts +20 -2
- package/src/components/DefaultMainScreen.tsx +72 -119
- package/src/lib/__tests__/docker.test.ts +106 -0
- package/src/lib/assistant-config.ts +6 -2
- package/src/lib/config-utils.ts +18 -0
- package/src/lib/docker.ts +180 -19
- package/src/lib/environments/paths.ts +21 -0
- package/src/lib/hatch-local.ts +42 -3
- package/src/lib/hatch-next-steps.ts +12 -0
- package/src/lib/input-history.ts +5 -8
- package/src/lib/provider-secrets.ts +564 -0
- package/src/lib/sync-cloud-assistants.ts +23 -9
- 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[];
|
|
@@ -668,6 +665,21 @@ function truncateValue(value: unknown, maxLen: number): string {
|
|
|
668
665
|
return serialized;
|
|
669
666
|
}
|
|
670
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
|
+
|
|
671
683
|
interface ToolCallDisplayProps {
|
|
672
684
|
tc: ToolCallInfo;
|
|
673
685
|
}
|
|
@@ -734,10 +746,6 @@ function HelpDisplay(): ReactElement {
|
|
|
734
746
|
{" /btw <question> "}
|
|
735
747
|
<Text dimColor>Ask a side question while the assistant is working</Text>
|
|
736
748
|
</Text>
|
|
737
|
-
<Text>
|
|
738
|
-
{" /doctor [question] "}
|
|
739
|
-
<Text dimColor>Run diagnostics on the remote instance via SSH</Text>
|
|
740
|
-
</Text>
|
|
741
749
|
<Text>
|
|
742
750
|
{" /retire "}
|
|
743
751
|
<Text dimColor>Retire the remote instance and exit</Text>
|
|
@@ -861,28 +869,33 @@ function stripAnsi(str: string): string {
|
|
|
861
869
|
interface DefaultMainScreenProps {
|
|
862
870
|
runtimeUrl: string;
|
|
863
871
|
assistantId: string;
|
|
872
|
+
assistantName?: string;
|
|
864
873
|
species: Species;
|
|
865
874
|
healthStatus?: string;
|
|
866
875
|
}
|
|
867
876
|
|
|
868
877
|
interface StyledLine {
|
|
869
878
|
text: string;
|
|
870
|
-
style: "heading" | "dim" | "normal";
|
|
879
|
+
style: "heading" | "dim" | "normal" | "eyebrow" | "title" | "art";
|
|
871
880
|
}
|
|
872
881
|
|
|
873
882
|
function CompactHeader({
|
|
883
|
+
assistantName,
|
|
874
884
|
species,
|
|
875
885
|
healthStatus,
|
|
876
886
|
totalWidth,
|
|
877
887
|
}: {
|
|
888
|
+
assistantName?: string;
|
|
878
889
|
species: Species;
|
|
879
890
|
healthStatus?: string;
|
|
880
891
|
totalWidth: number;
|
|
881
892
|
}): ReactElement {
|
|
882
|
-
const config = SPECIES_CONFIG[species];
|
|
883
893
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
884
894
|
const status = healthStatus ?? "checking...";
|
|
885
|
-
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)} `;
|
|
886
899
|
const prefix = "── Vellum";
|
|
887
900
|
const suffix = "──";
|
|
888
901
|
const fillLen = Math.max(
|
|
@@ -904,13 +917,13 @@ function CompactHeader({
|
|
|
904
917
|
function DefaultMainScreen({
|
|
905
918
|
runtimeUrl,
|
|
906
919
|
assistantId,
|
|
920
|
+
assistantName,
|
|
907
921
|
species,
|
|
908
922
|
healthStatus,
|
|
909
923
|
}: DefaultMainScreenProps): ReactElement {
|
|
910
924
|
const cwd = process.cwd();
|
|
911
925
|
const dirName = basename(cwd);
|
|
912
926
|
const config = SPECIES_CONFIG[species];
|
|
913
|
-
const art = config.art;
|
|
914
927
|
const accentColor = species === "openclaw" ? "red" : "magenta";
|
|
915
928
|
const caps = getTerminalCapabilities();
|
|
916
929
|
const headerPrefix = caps.unicodeSupported
|
|
@@ -926,6 +939,7 @@ function DefaultMainScreen({
|
|
|
926
939
|
if (isCompact) {
|
|
927
940
|
return (
|
|
928
941
|
<CompactHeader
|
|
942
|
+
assistantName={assistantName}
|
|
929
943
|
species={species}
|
|
930
944
|
healthStatus={healthStatus}
|
|
931
945
|
totalWidth={totalWidth}
|
|
@@ -933,16 +947,21 @@ function DefaultMainScreen({
|
|
|
933
947
|
);
|
|
934
948
|
}
|
|
935
949
|
|
|
950
|
+
const art = config.art;
|
|
936
951
|
const rightPanelWidth = Math.max(1, totalWidth - LEFT_PANEL_WIDTH);
|
|
937
952
|
|
|
938
|
-
const leftLines = [
|
|
939
|
-
" ",
|
|
940
|
-
|
|
941
|
-
"
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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" },
|
|
946
965
|
];
|
|
947
966
|
|
|
948
967
|
const rightLines: StyledLine[] = [
|
|
@@ -950,7 +969,7 @@ function DefaultMainScreen({
|
|
|
950
969
|
{ text: "Tips for getting started", style: "heading" },
|
|
951
970
|
...TIPS.map((t) => ({ text: t, style: "normal" as const })),
|
|
952
971
|
{ text: " ", style: "normal" },
|
|
953
|
-
{ text: "Assistant", style: "heading" },
|
|
972
|
+
{ text: "Assistant ID", style: "heading" },
|
|
954
973
|
{ text: assistantId, style: "dim" },
|
|
955
974
|
{ text: "Species", style: "heading" },
|
|
956
975
|
{
|
|
@@ -972,29 +991,37 @@ function DefaultMainScreen({
|
|
|
972
991
|
<Box flexDirection="row">
|
|
973
992
|
<Box flexDirection="column" width={LEFT_PANEL_WIDTH}>
|
|
974
993
|
{Array.from({ length: maxLines }, (_, i) => {
|
|
975
|
-
const
|
|
976
|
-
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") {
|
|
977
1004
|
return (
|
|
978
1005
|
<Text key={i} bold>
|
|
979
|
-
{
|
|
1006
|
+
{item.text}
|
|
980
1007
|
</Text>
|
|
981
1008
|
);
|
|
982
1009
|
}
|
|
983
|
-
if (
|
|
1010
|
+
if (item.style === "art") {
|
|
984
1011
|
return (
|
|
985
1012
|
<Text key={i} color={caps.isDumb ? undefined : accentColor}>
|
|
986
|
-
{
|
|
1013
|
+
{item.text}
|
|
987
1014
|
</Text>
|
|
988
1015
|
);
|
|
989
1016
|
}
|
|
990
|
-
if (
|
|
1017
|
+
if (item.style === "dim") {
|
|
991
1018
|
return (
|
|
992
1019
|
<Text key={i} dimColor>
|
|
993
|
-
{
|
|
1020
|
+
{item.text}
|
|
994
1021
|
</Text>
|
|
995
1022
|
);
|
|
996
1023
|
}
|
|
997
|
-
return <Text key={i}>{
|
|
1024
|
+
return <Text key={i}>{item.text}</Text>;
|
|
998
1025
|
})}
|
|
999
1026
|
</Box>
|
|
1000
1027
|
<Box flexDirection="column" width={rightPanelWidth}>
|
|
@@ -1115,8 +1142,7 @@ function calculateHeaderHeight(
|
|
|
1115
1142
|
if ((terminalColumns ?? DEFAULT_TERMINAL_COLUMNS) < COMPACT_THRESHOLD) {
|
|
1116
1143
|
return COMPACT_HEADER_HEIGHT;
|
|
1117
1144
|
}
|
|
1118
|
-
const
|
|
1119
|
-
const artLength = config.art.length;
|
|
1145
|
+
const artLength = SPECIES_CONFIG[species].art.length;
|
|
1120
1146
|
const leftLineCount = LEFT_HEADER_LINES + artLength + LEFT_FOOTER_LINES;
|
|
1121
1147
|
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
1122
1148
|
return maxLines + HEADER_CHROME_LINES;
|
|
@@ -1128,12 +1154,11 @@ export function render(
|
|
|
1128
1154
|
runtimeUrl: string,
|
|
1129
1155
|
assistantId: string,
|
|
1130
1156
|
species: Species,
|
|
1157
|
+
assistantName?: string,
|
|
1131
1158
|
): number {
|
|
1132
1159
|
const terminalColumns = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
|
|
1133
1160
|
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1134
|
-
|
|
1135
|
-
const config = SPECIES_CONFIG[species];
|
|
1136
|
-
const art = config.art;
|
|
1161
|
+
const art = SPECIES_CONFIG[species].art;
|
|
1137
1162
|
|
|
1138
1163
|
const leftLineCount = LEFT_HEADER_LINES + art.length + LEFT_FOOTER_LINES;
|
|
1139
1164
|
const maxLines = Math.max(leftLineCount, RIGHT_PANEL_LINE_COUNT);
|
|
@@ -1142,6 +1167,7 @@ export function render(
|
|
|
1142
1167
|
<DefaultMainScreen
|
|
1143
1168
|
runtimeUrl={runtimeUrl}
|
|
1144
1169
|
assistantId={assistantId}
|
|
1170
|
+
assistantName={assistantName}
|
|
1145
1171
|
species={species}
|
|
1146
1172
|
/>,
|
|
1147
1173
|
{ exitOnCtrlC: false },
|
|
@@ -1360,6 +1386,7 @@ export interface ChatAppHandle {
|
|
|
1360
1386
|
interface ChatAppProps {
|
|
1361
1387
|
runtimeUrl: string;
|
|
1362
1388
|
assistantId: string;
|
|
1389
|
+
assistantName?: string;
|
|
1363
1390
|
species: Species;
|
|
1364
1391
|
/** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
|
|
1365
1392
|
* { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
|
|
@@ -1373,6 +1400,7 @@ interface ChatAppProps {
|
|
|
1373
1400
|
function ChatApp({
|
|
1374
1401
|
runtimeUrl,
|
|
1375
1402
|
assistantId,
|
|
1403
|
+
assistantName,
|
|
1376
1404
|
species,
|
|
1377
1405
|
auth,
|
|
1378
1406
|
project,
|
|
@@ -1405,11 +1433,9 @@ function ChatApp({
|
|
|
1405
1433
|
const connectedRef = useRef(false);
|
|
1406
1434
|
const connectingRef = useRef(false);
|
|
1407
1435
|
const seenMessageIdsRef = useRef(new Set<string>());
|
|
1408
|
-
const chatLogRef = useRef<ChatLogEntry[]>([]);
|
|
1409
1436
|
const sseAbortRef = useRef<AbortController | null>(null);
|
|
1410
1437
|
const streamingTextRef = useRef("");
|
|
1411
1438
|
const streamingToolCallsRef = useRef<ToolCallInfo[]>([]);
|
|
1412
|
-
const doctorSessionIdRef = useRef(randomUUID());
|
|
1413
1439
|
const handleRef_ = useRef<ChatAppHandle | null>(null);
|
|
1414
1440
|
|
|
1415
1441
|
const { stdout } = useStdout();
|
|
@@ -1840,10 +1866,6 @@ function ChatApp({
|
|
|
1840
1866
|
};
|
|
1841
1867
|
seenMessageIdsRef.current.add(msg.id);
|
|
1842
1868
|
hRef.addMessage(msg);
|
|
1843
|
-
chatLogRef.current.push({
|
|
1844
|
-
role: "assistant",
|
|
1845
|
-
content: text,
|
|
1846
|
-
});
|
|
1847
1869
|
process.stdout.write("\x07");
|
|
1848
1870
|
}
|
|
1849
1871
|
|
|
@@ -2012,79 +2034,6 @@ function ChatApp({
|
|
|
2012
2034
|
return;
|
|
2013
2035
|
}
|
|
2014
2036
|
|
|
2015
|
-
if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
|
|
2016
|
-
if (!project || !zone) {
|
|
2017
|
-
h.showError(
|
|
2018
|
-
"No instance info available. Connect to a hatched instance first.",
|
|
2019
|
-
);
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
const userPrompt = trimmed.slice("/doctor".length).trim() || undefined;
|
|
2023
|
-
const recentChatContext = chatLogRef.current.slice(-20);
|
|
2024
|
-
|
|
2025
|
-
chatLogRef.current.push({ role: "user", content: trimmed });
|
|
2026
|
-
|
|
2027
|
-
if (userPrompt) {
|
|
2028
|
-
const doctorUserMsg: RuntimeMessage = {
|
|
2029
|
-
id: "local-user-" + Date.now(),
|
|
2030
|
-
role: "user",
|
|
2031
|
-
content: userPrompt,
|
|
2032
|
-
timestamp: new Date().toISOString(),
|
|
2033
|
-
label: "You (to Doctor):",
|
|
2034
|
-
};
|
|
2035
|
-
h.addMessage(doctorUserMsg);
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
h.showSpinner(`Analyzing ${assistantId}...`);
|
|
2039
|
-
|
|
2040
|
-
try {
|
|
2041
|
-
const result = await callDoctorDaemon(
|
|
2042
|
-
assistantId,
|
|
2043
|
-
project,
|
|
2044
|
-
zone,
|
|
2045
|
-
userPrompt,
|
|
2046
|
-
(event) => {
|
|
2047
|
-
switch (event.phase) {
|
|
2048
|
-
case "invoking_prompt":
|
|
2049
|
-
handleRef_.current?.showSpinner(
|
|
2050
|
-
`Analyzing ${assistantId}...`,
|
|
2051
|
-
);
|
|
2052
|
-
break;
|
|
2053
|
-
case "calling_tool":
|
|
2054
|
-
handleRef_.current?.showSpinner(
|
|
2055
|
-
`Running ${event.toolName ?? "tool"} on ${assistantId}...`,
|
|
2056
|
-
);
|
|
2057
|
-
break;
|
|
2058
|
-
case "processing_tool_result":
|
|
2059
|
-
handleRef_.current?.showSpinner(
|
|
2060
|
-
`Reviewing diagnostics for ${assistantId}...`,
|
|
2061
|
-
);
|
|
2062
|
-
break;
|
|
2063
|
-
}
|
|
2064
|
-
},
|
|
2065
|
-
doctorSessionIdRef.current,
|
|
2066
|
-
recentChatContext,
|
|
2067
|
-
);
|
|
2068
|
-
h.hideSpinner();
|
|
2069
|
-
if (result.recommendation) {
|
|
2070
|
-
h.addStatus(`Recommendation:\n${result.recommendation}`);
|
|
2071
|
-
chatLogRef.current.push({
|
|
2072
|
-
role: "assistant",
|
|
2073
|
-
content: result.recommendation,
|
|
2074
|
-
});
|
|
2075
|
-
} else if (result.error) {
|
|
2076
|
-
h.showError(result.error);
|
|
2077
|
-
chatLogRef.current.push({ role: "error", content: result.error });
|
|
2078
|
-
}
|
|
2079
|
-
} catch (err) {
|
|
2080
|
-
h.hideSpinner();
|
|
2081
|
-
const errorMsg = `Doctor assistant unreachable: ${err instanceof Error ? err.message : err}`;
|
|
2082
|
-
h.showError(errorMsg);
|
|
2083
|
-
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
2084
|
-
}
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
2037
|
// If a connection attempt is already in progress, don't silently drop input
|
|
2089
2038
|
if (connectingRef.current) {
|
|
2090
2039
|
h.addStatus(
|
|
@@ -2201,7 +2150,6 @@ function ChatApp({
|
|
|
2201
2150
|
);
|
|
2202
2151
|
clearTimeout(timeoutId);
|
|
2203
2152
|
if (sendResult.accepted) {
|
|
2204
|
-
chatLogRef.current.push({ role: "user", content: trimmed });
|
|
2205
2153
|
h.addStatus(
|
|
2206
2154
|
"Message queued — will be processed after current response",
|
|
2207
2155
|
"gray",
|
|
@@ -2233,7 +2181,6 @@ function ChatApp({
|
|
|
2233
2181
|
return;
|
|
2234
2182
|
}
|
|
2235
2183
|
|
|
2236
|
-
chatLogRef.current.push({ role: "user", content: trimmed });
|
|
2237
2184
|
seenMessageIdsRef.current.add("pending-user-" + Date.now());
|
|
2238
2185
|
|
|
2239
2186
|
h.showSpinner("Sending...");
|
|
@@ -2264,7 +2211,6 @@ function ChatApp({
|
|
|
2264
2211
|
const errorMsg =
|
|
2265
2212
|
sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
2266
2213
|
h.showError(errorMsg);
|
|
2267
|
-
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
2268
2214
|
return;
|
|
2269
2215
|
}
|
|
2270
2216
|
|
|
@@ -2462,6 +2408,7 @@ function ChatApp({
|
|
|
2462
2408
|
<DefaultMainScreen
|
|
2463
2409
|
runtimeUrl={runtimeUrl}
|
|
2464
2410
|
assistantId={assistantId}
|
|
2411
|
+
assistantName={assistantName}
|
|
2465
2412
|
species={species}
|
|
2466
2413
|
healthStatus={healthStatus}
|
|
2467
2414
|
/>
|
|
@@ -2581,7 +2528,12 @@ export function renderChatApp(
|
|
|
2581
2528
|
assistantId: string,
|
|
2582
2529
|
species: Species,
|
|
2583
2530
|
onExit: () => void,
|
|
2584
|
-
options?: {
|
|
2531
|
+
options?: {
|
|
2532
|
+
auth?: Record<string, string>;
|
|
2533
|
+
project?: string;
|
|
2534
|
+
zone?: string;
|
|
2535
|
+
assistantName?: string;
|
|
2536
|
+
},
|
|
2585
2537
|
): ChatAppInstance {
|
|
2586
2538
|
let chatHandle: ChatAppHandle | null = null;
|
|
2587
2539
|
|
|
@@ -2589,6 +2541,7 @@ export function renderChatApp(
|
|
|
2589
2541
|
<ChatApp
|
|
2590
2542
|
runtimeUrl={runtimeUrl}
|
|
2591
2543
|
assistantId={assistantId}
|
|
2544
|
+
assistantName={options?.assistantName}
|
|
2592
2545
|
species={species}
|
|
2593
2546
|
auth={options?.auth}
|
|
2594
2547
|
project={options?.project}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
AVATAR_DEVICE_ENV_VAR,
|
|
5
5
|
dockerResourceNames,
|
|
6
6
|
resolveAvatarDevicePath,
|
|
7
|
+
resolveDockerHatchMode,
|
|
8
|
+
resolveDockerProviderCredentialSetupAction,
|
|
7
9
|
type ServiceName,
|
|
8
10
|
} from "../docker.js";
|
|
9
11
|
import { buildServiceRunArgs } from "../statefulset.js";
|
|
@@ -103,6 +105,51 @@ describe("buildServiceRunArgs — assistant", () => {
|
|
|
103
105
|
});
|
|
104
106
|
});
|
|
105
107
|
|
|
108
|
+
describe("resolveDockerProviderCredentialSetupAction", () => {
|
|
109
|
+
test("defers provider setup in detached mode", () => {
|
|
110
|
+
expect(
|
|
111
|
+
resolveDockerProviderCredentialSetupAction({
|
|
112
|
+
provider: "anthropic",
|
|
113
|
+
detached: true,
|
|
114
|
+
}),
|
|
115
|
+
).toBe("defer");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("reports missing guardian token only when a lease was expected", () => {
|
|
119
|
+
expect(
|
|
120
|
+
resolveDockerProviderCredentialSetupAction({
|
|
121
|
+
provider: "anthropic",
|
|
122
|
+
detached: false,
|
|
123
|
+
}),
|
|
124
|
+
).toBe("missing-token");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("configures provider setup when a guardian token is available", () => {
|
|
128
|
+
expect(
|
|
129
|
+
resolveDockerProviderCredentialSetupAction({
|
|
130
|
+
provider: "anthropic",
|
|
131
|
+
guardianAccessToken: "guardian-token",
|
|
132
|
+
detached: false,
|
|
133
|
+
}),
|
|
134
|
+
).toBe("configure");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("skips provider setup for internal hatches and detached keyless hatches", () => {
|
|
138
|
+
expect(
|
|
139
|
+
resolveDockerProviderCredentialSetupAction({
|
|
140
|
+
provider: undefined,
|
|
141
|
+
detached: false,
|
|
142
|
+
}),
|
|
143
|
+
).toBe("skip");
|
|
144
|
+
expect(
|
|
145
|
+
resolveDockerProviderCredentialSetupAction({
|
|
146
|
+
provider: null,
|
|
147
|
+
detached: true,
|
|
148
|
+
}),
|
|
149
|
+
).toBe("skip");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
106
153
|
describe("buildServiceRunArgs — gateway", () => {
|
|
107
154
|
const savedVelayBaseUrl = process.env.VELAY_BASE_URL;
|
|
108
155
|
|
|
@@ -171,3 +218,62 @@ describe("VELLUM_AVATAR_DEVICE passthrough", () => {
|
|
|
171
218
|
);
|
|
172
219
|
});
|
|
173
220
|
});
|
|
221
|
+
|
|
222
|
+
describe("resolveDockerHatchMode", () => {
|
|
223
|
+
test("defaults to pulling published images when no source flag is set", () => {
|
|
224
|
+
expect(
|
|
225
|
+
resolveDockerHatchMode({
|
|
226
|
+
watch: false,
|
|
227
|
+
buildFromSource: false,
|
|
228
|
+
fullSourceTreeAvailable: true,
|
|
229
|
+
}),
|
|
230
|
+
).toEqual({ build: false, watcher: false, fellBackToPull: false });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("--source <path> builds without enabling the file watcher", () => {
|
|
234
|
+
expect(
|
|
235
|
+
resolveDockerHatchMode({
|
|
236
|
+
watch: false,
|
|
237
|
+
buildFromSource: true,
|
|
238
|
+
fullSourceTreeAvailable: true,
|
|
239
|
+
}),
|
|
240
|
+
).toEqual({ build: true, watcher: false, fellBackToPull: false });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("--watch builds and enables the file watcher", () => {
|
|
244
|
+
expect(
|
|
245
|
+
resolveDockerHatchMode({
|
|
246
|
+
watch: true,
|
|
247
|
+
buildFromSource: false,
|
|
248
|
+
fullSourceTreeAvailable: true,
|
|
249
|
+
}),
|
|
250
|
+
).toEqual({ build: true, watcher: true, fellBackToPull: false });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("--watch + --source <path> still enables the watcher (watch wins)", () => {
|
|
254
|
+
expect(
|
|
255
|
+
resolveDockerHatchMode({
|
|
256
|
+
watch: true,
|
|
257
|
+
buildFromSource: true,
|
|
258
|
+
fullSourceTreeAvailable: true,
|
|
259
|
+
}),
|
|
260
|
+
).toEqual({ build: true, watcher: true, fellBackToPull: false });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("falls back to pull when source flag is set but source tree is missing", () => {
|
|
264
|
+
expect(
|
|
265
|
+
resolveDockerHatchMode({
|
|
266
|
+
watch: false,
|
|
267
|
+
buildFromSource: true,
|
|
268
|
+
fullSourceTreeAvailable: false,
|
|
269
|
+
}),
|
|
270
|
+
).toEqual({ build: false, watcher: false, fellBackToPull: true });
|
|
271
|
+
expect(
|
|
272
|
+
resolveDockerHatchMode({
|
|
273
|
+
watch: true,
|
|
274
|
+
buildFromSource: false,
|
|
275
|
+
fullSourceTreeAvailable: false,
|
|
276
|
+
}),
|
|
277
|
+
).toEqual({ build: false, watcher: false, fellBackToPull: true });
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -67,6 +67,10 @@ export interface ContainerInfo {
|
|
|
67
67
|
|
|
68
68
|
export interface AssistantEntry {
|
|
69
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;
|
|
70
74
|
runtimeUrl: string;
|
|
71
75
|
/** Loopback URL for same-machine health checks (e.g. `http://127.0.0.1:7831`).
|
|
72
76
|
* Avoids mDNS resolution issues when the machine checks its own gateway. */
|
|
@@ -85,6 +89,8 @@ export interface AssistantEntry {
|
|
|
85
89
|
resources?: LocalInstanceResources;
|
|
86
90
|
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
87
91
|
watcherPid?: number;
|
|
92
|
+
/** Local bootstrap secret used to lease guardian tokens for Docker assistants after detached hatch. */
|
|
93
|
+
guardianBootstrapSecret?: string;
|
|
88
94
|
/** Docker image metadata for rollback. Only present for docker topology entries. */
|
|
89
95
|
containerInfo?: ContainerInfo;
|
|
90
96
|
/** Docker image metadata from before the last upgrade. Enables rollback to the prior version. */
|
|
@@ -572,5 +578,3 @@ export function getLockfilePlatformBaseUrl(): string | undefined {
|
|
|
572
578
|
if (typeof url === "string" && url.trim()) return url.trim();
|
|
573
579
|
return undefined;
|
|
574
580
|
}
|
|
575
|
-
|
|
576
|
-
|
package/src/lib/config-utils.ts
CHANGED
|
@@ -32,6 +32,24 @@ export function buildNestedConfig(
|
|
|
32
32
|
return config;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Ensure hatch always provides enough initial LLM config for the assistant to
|
|
37
|
+
* detect a fresh off-platform hatch and seed BYOK profiles.
|
|
38
|
+
*/
|
|
39
|
+
export function buildHatchConfigValues(
|
|
40
|
+
configValues: Record<string, string>,
|
|
41
|
+
provider: string | null | undefined,
|
|
42
|
+
): Record<string, string> {
|
|
43
|
+
if (!provider || configValues["llm.default.provider"]) {
|
|
44
|
+
return configValues;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...configValues,
|
|
49
|
+
"llm.default.provider": provider,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/**
|
|
36
54
|
* Write arbitrary key-value pairs to a temporary JSON file and return its
|
|
37
55
|
* path. The caller passes this path to the daemon via the
|