@vellumai/cli 0.4.49 → 0.4.50
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/commands/ps.ts +3 -1
- package/src/components/DefaultMainScreen.tsx +115 -8
- package/src/index.ts +99 -23
- package/src/lib/docker.ts +49 -3
- package/src/lib/health-check.ts +87 -0
- package/src/lib/terminal-capabilities.ts +9 -0
package/package.json
CHANGED
package/src/commands/ps.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadAllAssistants,
|
|
7
7
|
type AssistantEntry,
|
|
8
8
|
} from "../lib/assistant-config";
|
|
9
|
-
import { checkHealth } from "../lib/health-check";
|
|
9
|
+
import { checkHealth, checkManagedHealth } from "../lib/health-check";
|
|
10
10
|
import {
|
|
11
11
|
classifyProcess,
|
|
12
12
|
detectOrphanedProcesses,
|
|
@@ -359,6 +359,8 @@ async function listAllAssistants(): Promise<void> {
|
|
|
359
359
|
} else {
|
|
360
360
|
health = await checkHealth(a.localUrl ?? a.runtimeUrl, a.bearerToken);
|
|
361
361
|
}
|
|
362
|
+
} else if (a.cloud === "vellum") {
|
|
363
|
+
health = await checkManagedHealth(a.runtimeUrl, a.assistantId);
|
|
362
364
|
} else {
|
|
363
365
|
health = await checkHealth(a.localUrl ?? a.runtimeUrl, a.bearerToken);
|
|
364
366
|
}
|
|
@@ -500,6 +500,9 @@ async function handleScopeSelection(
|
|
|
500
500
|
|
|
501
501
|
export const TYPING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
502
502
|
|
|
503
|
+
/** ASCII-safe spinner frames for the connection screen. */
|
|
504
|
+
const CONNECTION_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
505
|
+
|
|
503
506
|
export interface ToolCallInfo {
|
|
504
507
|
name: string;
|
|
505
508
|
input: Record<string, unknown>;
|
|
@@ -675,6 +678,75 @@ function SpinnerDisplay({ text }: { text: string }): ReactElement {
|
|
|
675
678
|
);
|
|
676
679
|
}
|
|
677
680
|
|
|
681
|
+
type ConnectionState = "connecting" | "connected" | "error";
|
|
682
|
+
|
|
683
|
+
function ConnectionScreen({
|
|
684
|
+
state,
|
|
685
|
+
errorMessage,
|
|
686
|
+
species,
|
|
687
|
+
terminalRows,
|
|
688
|
+
terminalColumns,
|
|
689
|
+
onRetry,
|
|
690
|
+
onExit,
|
|
691
|
+
}: {
|
|
692
|
+
state: ConnectionState;
|
|
693
|
+
errorMessage?: string;
|
|
694
|
+
species: Species;
|
|
695
|
+
terminalRows: number;
|
|
696
|
+
terminalColumns: number;
|
|
697
|
+
onRetry: () => void;
|
|
698
|
+
onExit: () => void;
|
|
699
|
+
}): ReactElement {
|
|
700
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
701
|
+
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
if (state !== "connecting") return;
|
|
704
|
+
const timer = setInterval(() => {
|
|
705
|
+
setFrameIndex((prev) => (prev + 1) % CONNECTION_SPINNER_FRAMES.length);
|
|
706
|
+
}, 150);
|
|
707
|
+
return () => clearInterval(timer);
|
|
708
|
+
}, [state]);
|
|
709
|
+
|
|
710
|
+
useInput((input, key) => {
|
|
711
|
+
if (key.ctrl && input === "c") {
|
|
712
|
+
onExit();
|
|
713
|
+
}
|
|
714
|
+
if (state === "error" && input === "r") {
|
|
715
|
+
onRetry();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const config = SPECIES_CONFIG[species];
|
|
720
|
+
const title = `Vellum ${config.hatchedEmoji} ${species}`;
|
|
721
|
+
const width = Math.min(terminalColumns, MAX_TOTAL_WIDTH);
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
<Box
|
|
725
|
+
flexDirection="column"
|
|
726
|
+
height={terminalRows}
|
|
727
|
+
width={width}
|
|
728
|
+
justifyContent="center"
|
|
729
|
+
alignItems="center"
|
|
730
|
+
>
|
|
731
|
+
<Text dimColor bold>
|
|
732
|
+
{title}
|
|
733
|
+
</Text>
|
|
734
|
+
<Text> </Text>
|
|
735
|
+
{state === "connecting" ? (
|
|
736
|
+
<Text dimColor>
|
|
737
|
+
{CONNECTION_SPINNER_FRAMES[frameIndex]} Connecting to assistant...
|
|
738
|
+
</Text>
|
|
739
|
+
) : (
|
|
740
|
+
<>
|
|
741
|
+
<Text color="red">Failed to connect: {errorMessage}</Text>
|
|
742
|
+
<Text> </Text>
|
|
743
|
+
<Text dimColor>Press r to retry or Ctrl+C to quit</Text>
|
|
744
|
+
</>
|
|
745
|
+
)}
|
|
746
|
+
</Box>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
678
750
|
export function renderErrorMainScreen(error: unknown): number {
|
|
679
751
|
const msg = error instanceof Error ? error.message : String(error);
|
|
680
752
|
console.log(
|
|
@@ -1224,6 +1296,11 @@ function ChatApp({
|
|
|
1224
1296
|
const [healthStatus, setHealthStatus] = useState<string | undefined>(
|
|
1225
1297
|
undefined,
|
|
1226
1298
|
);
|
|
1299
|
+
const [connectionState, setConnectionState] =
|
|
1300
|
+
useState<ConnectionState>("connecting");
|
|
1301
|
+
const [connectionError, setConnectionError] = useState<string | undefined>(
|
|
1302
|
+
undefined,
|
|
1303
|
+
);
|
|
1227
1304
|
const prevFeedLengthRef = useRef(0);
|
|
1228
1305
|
const busyRef = useRef(false);
|
|
1229
1306
|
const connectedRef = useRef(false);
|
|
@@ -1240,7 +1317,7 @@ function ChatApp({
|
|
|
1240
1317
|
const headerHeight = calculateHeaderHeight(species, terminalColumns);
|
|
1241
1318
|
|
|
1242
1319
|
const isCompact = terminalColumns < COMPACT_THRESHOLD;
|
|
1243
|
-
const compactInputAreaHeight =
|
|
1320
|
+
const compactInputAreaHeight = 1; // input row only, no separators
|
|
1244
1321
|
const inputAreaHeight = isCompact
|
|
1245
1322
|
? compactInputAreaHeight
|
|
1246
1323
|
: INPUT_AREA_HEIGHT;
|
|
@@ -1476,6 +1553,8 @@ function ChatApp({
|
|
|
1476
1553
|
return false;
|
|
1477
1554
|
}
|
|
1478
1555
|
connectingRef.current = true;
|
|
1556
|
+
setConnectionState("connecting");
|
|
1557
|
+
setConnectionError(undefined);
|
|
1479
1558
|
const h = handleRef_.current;
|
|
1480
1559
|
|
|
1481
1560
|
h.showSpinner("Connecting...");
|
|
@@ -1538,12 +1617,15 @@ function ChatApp({
|
|
|
1538
1617
|
|
|
1539
1618
|
connectedRef.current = true;
|
|
1540
1619
|
connectingRef.current = false;
|
|
1620
|
+
setConnectionState("connected");
|
|
1541
1621
|
return true;
|
|
1542
1622
|
} catch (err) {
|
|
1543
1623
|
h.hideSpinner();
|
|
1544
1624
|
connectingRef.current = false;
|
|
1545
1625
|
h.updateHealthStatus("unreachable");
|
|
1546
1626
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1627
|
+
setConnectionState("error");
|
|
1628
|
+
setConnectionError(msg);
|
|
1547
1629
|
h.addStatus(
|
|
1548
1630
|
`${statusEmoji("unreachable")} Failed to connect: ${msg}`,
|
|
1549
1631
|
"red",
|
|
@@ -2065,6 +2147,7 @@ function ChatApp({
|
|
|
2065
2147
|
role: "assistant",
|
|
2066
2148
|
content: msg.content,
|
|
2067
2149
|
});
|
|
2150
|
+
process.stdout.write("\x07");
|
|
2068
2151
|
h.setBusy(false);
|
|
2069
2152
|
h.hideSpinner();
|
|
2070
2153
|
return;
|
|
@@ -2174,6 +2257,13 @@ function ChatApp({
|
|
|
2174
2257
|
updateHealthStatus,
|
|
2175
2258
|
]);
|
|
2176
2259
|
|
|
2260
|
+
const retryConnection = useCallback(() => {
|
|
2261
|
+
if (connectingRef.current) return; // already retrying
|
|
2262
|
+
connectedRef.current = false;
|
|
2263
|
+
setConnectionState("connecting");
|
|
2264
|
+
ensureConnected();
|
|
2265
|
+
}, [ensureConnected]);
|
|
2266
|
+
|
|
2177
2267
|
useEffect(() => {
|
|
2178
2268
|
ensureConnected();
|
|
2179
2269
|
}, [ensureConnected]);
|
|
@@ -2262,6 +2352,20 @@ function ChatApp({
|
|
|
2262
2352
|
}
|
|
2263
2353
|
}, [selection]);
|
|
2264
2354
|
|
|
2355
|
+
if (connectionState !== "connected") {
|
|
2356
|
+
return (
|
|
2357
|
+
<ConnectionScreen
|
|
2358
|
+
state={connectionState}
|
|
2359
|
+
errorMessage={connectionError}
|
|
2360
|
+
species={species}
|
|
2361
|
+
terminalRows={terminalRows}
|
|
2362
|
+
terminalColumns={terminalColumns}
|
|
2363
|
+
onRetry={retryConnection}
|
|
2364
|
+
onExit={onExit}
|
|
2365
|
+
/>
|
|
2366
|
+
);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2265
2369
|
return (
|
|
2266
2370
|
<Box flexDirection="column" height={terminalRows}>
|
|
2267
2371
|
<DefaultMainScreen
|
|
@@ -2274,8 +2378,9 @@ function ChatApp({
|
|
|
2274
2378
|
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
2275
2379
|
{visibleWindow.hiddenAbove > 0 ? (
|
|
2276
2380
|
<Text dimColor>
|
|
2277
|
-
{
|
|
2278
|
-
|
|
2381
|
+
{isCompact
|
|
2382
|
+
? `\u2191 ${visibleWindow.hiddenAbove} more above`
|
|
2383
|
+
: `\u2191 ${visibleWindow.hiddenAbove} more above (Shift+\u2191/Cmd+\u2191)`}
|
|
2279
2384
|
</Text>
|
|
2280
2385
|
) : null}
|
|
2281
2386
|
|
|
@@ -2341,12 +2446,14 @@ function ChatApp({
|
|
|
2341
2446
|
|
|
2342
2447
|
{!selection && !secretInput ? (
|
|
2343
2448
|
<Box flexDirection="column" flexShrink={0}>
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2449
|
+
{isCompact ? null : (
|
|
2450
|
+
<Text dimColor>
|
|
2451
|
+
{unicodeOrFallback("\u2500", "-").repeat(terminalColumns)}
|
|
2452
|
+
</Text>
|
|
2453
|
+
)}
|
|
2454
|
+
<Box paddingLeft={isCompact ? 0 : 1} height={1} flexShrink={0}>
|
|
2348
2455
|
<Text color="green" bold>
|
|
2349
|
-
|
|
2456
|
+
{isCompact ? ">" : "you>"}
|
|
2350
2457
|
{" "}
|
|
2351
2458
|
</Text>
|
|
2352
2459
|
<TextInput
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,13 @@ import { ssh } from "./commands/ssh";
|
|
|
15
15
|
import { tunnel } from "./commands/tunnel";
|
|
16
16
|
import { use } from "./commands/use";
|
|
17
17
|
import { wake } from "./commands/wake";
|
|
18
|
+
import {
|
|
19
|
+
getActiveAssistant,
|
|
20
|
+
findAssistantByName,
|
|
21
|
+
loadLatestAssistant,
|
|
22
|
+
setActiveAssistant,
|
|
23
|
+
} from "./lib/assistant-config";
|
|
24
|
+
import { checkHealth } from "./lib/health-check";
|
|
18
25
|
|
|
19
26
|
const commands = {
|
|
20
27
|
clean,
|
|
@@ -37,37 +44,106 @@ const commands = {
|
|
|
37
44
|
|
|
38
45
|
type CommandName = keyof typeof commands;
|
|
39
46
|
|
|
47
|
+
function printHelp(): void {
|
|
48
|
+
console.log("Usage: vellum <command> [options]");
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log("Commands:");
|
|
51
|
+
console.log(" clean Kill orphaned vellum processes");
|
|
52
|
+
console.log(" client Connect to a hatched assistant");
|
|
53
|
+
console.log(" hatch Create a new assistant instance");
|
|
54
|
+
console.log(" login Log in to the Vellum platform");
|
|
55
|
+
console.log(" logout Log out of the Vellum platform");
|
|
56
|
+
console.log(" pair Pair with a remote assistant via QR code");
|
|
57
|
+
console.log(
|
|
58
|
+
" ps List assistants (or processes for a specific assistant)",
|
|
59
|
+
);
|
|
60
|
+
console.log(" recover Restore a previously retired local assistant");
|
|
61
|
+
console.log(" retire Delete an assistant instance");
|
|
62
|
+
console.log(" setup Configure API keys interactively");
|
|
63
|
+
console.log(" sleep Stop the assistant process");
|
|
64
|
+
console.log(" ssh SSH into a remote assistant instance");
|
|
65
|
+
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
66
|
+
console.log(" use Set the active assistant for commands");
|
|
67
|
+
console.log(" wake Start the assistant and gateway");
|
|
68
|
+
console.log(" whoami Show current logged-in user");
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log("Options:");
|
|
71
|
+
console.log(
|
|
72
|
+
" --no-color, --plain Disable colored output (honors NO_COLOR env)",
|
|
73
|
+
);
|
|
74
|
+
console.log(" --version, -v Show version");
|
|
75
|
+
console.log(" --help, -h Show this help");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check for --no-color / --plain flags and set NO_COLOR env var
|
|
80
|
+
* before any terminal capability detection runs.
|
|
81
|
+
*
|
|
82
|
+
* Per https://no-color.org/, setting NO_COLOR to any non-empty value
|
|
83
|
+
* signals that color output should be suppressed.
|
|
84
|
+
*/
|
|
85
|
+
function applyNoColorFlags(argv: string[]): void {
|
|
86
|
+
if (argv.includes("--no-color") || argv.includes("--plain")) {
|
|
87
|
+
process.env.NO_COLOR = "1";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* If a running assistant is detected, launch the TUI client and return true.
|
|
93
|
+
* Otherwise return false so the caller can fall back to help text.
|
|
94
|
+
*/
|
|
95
|
+
async function tryLaunchClient(): Promise<boolean> {
|
|
96
|
+
const activeName = getActiveAssistant();
|
|
97
|
+
const entry = activeName
|
|
98
|
+
? findAssistantByName(activeName)
|
|
99
|
+
: loadLatestAssistant();
|
|
100
|
+
|
|
101
|
+
if (!entry) return false;
|
|
102
|
+
|
|
103
|
+
const url = entry.localUrl || entry.runtimeUrl;
|
|
104
|
+
if (!url) return false;
|
|
105
|
+
|
|
106
|
+
const result = await checkHealth(url, entry.bearerToken);
|
|
107
|
+
if (result.status !== "healthy") return false;
|
|
108
|
+
|
|
109
|
+
// Ensure the resolved assistant is active so client() can find it
|
|
110
|
+
// (client() independently reads the active assistant from config).
|
|
111
|
+
setActiveAssistant(String(entry.assistantId));
|
|
112
|
+
|
|
113
|
+
await client();
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
40
117
|
async function main() {
|
|
41
118
|
const args = process.argv.slice(2);
|
|
42
|
-
|
|
119
|
+
|
|
120
|
+
// Must run before any command or terminal-capabilities usage
|
|
121
|
+
applyNoColorFlags(args);
|
|
122
|
+
|
|
123
|
+
// Global flags that are not command names
|
|
124
|
+
const GLOBAL_FLAGS = new Set(["--no-color", "--plain"]);
|
|
125
|
+
const commandName = args.find((a) => !GLOBAL_FLAGS.has(a));
|
|
126
|
+
|
|
127
|
+
// Strip global flags from process.argv so subcommands that parse
|
|
128
|
+
// process.argv.slice(3) don't see them as positional arguments.
|
|
129
|
+
const filteredArgs = args.filter((a) => !GLOBAL_FLAGS.has(a));
|
|
130
|
+
process.argv = [...process.argv.slice(0, 2), ...filteredArgs];
|
|
43
131
|
|
|
44
132
|
if (commandName === "--version" || commandName === "-v") {
|
|
45
133
|
console.log(`@vellumai/cli v${cliPkg.version}`);
|
|
46
134
|
process.exit(0);
|
|
47
135
|
}
|
|
48
136
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
console.log(
|
|
60
|
-
" ps List assistants (or processes for a specific assistant)",
|
|
61
|
-
);
|
|
62
|
-
console.log(" recover Restore a previously retired local assistant");
|
|
63
|
-
console.log(" retire Delete an assistant instance");
|
|
64
|
-
console.log(" setup Configure API keys interactively");
|
|
65
|
-
console.log(" sleep Stop the assistant process");
|
|
66
|
-
console.log(" ssh SSH into a remote assistant instance");
|
|
67
|
-
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
68
|
-
console.log(" use Set the active assistant for commands");
|
|
69
|
-
console.log(" wake Start the assistant and gateway");
|
|
70
|
-
console.log(" whoami Show current logged-in user");
|
|
137
|
+
if (commandName === "--help" || commandName === "-h") {
|
|
138
|
+
printHelp();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!commandName) {
|
|
143
|
+
const launched = await tryLaunchClient();
|
|
144
|
+
if (!launched) {
|
|
145
|
+
printHelp();
|
|
146
|
+
}
|
|
71
147
|
process.exit(0);
|
|
72
148
|
}
|
|
73
149
|
|
package/src/lib/docker.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { saveAssistantEntry, setActiveAssistant } from "./assistant-config";
|
|
|
7
7
|
import type { AssistantEntry } from "./assistant-config";
|
|
8
8
|
import { DEFAULT_GATEWAY_PORT } from "./constants";
|
|
9
9
|
import type { Species } from "./constants";
|
|
10
|
-
import { discoverPublicUrl } from "./local";
|
|
11
10
|
import { generateRandomSuffix } from "./random-name";
|
|
12
11
|
import { exec, execOutput } from "./step-runner";
|
|
13
12
|
import {
|
|
@@ -34,6 +33,36 @@ async function ensureDockerInstalled(): Promise<void> {
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
if (!installed) {
|
|
36
|
+
// Check whether Homebrew is available before attempting to use it.
|
|
37
|
+
let hasBrew = false;
|
|
38
|
+
try {
|
|
39
|
+
await execOutput("brew", ["--version"]);
|
|
40
|
+
hasBrew = true;
|
|
41
|
+
} catch {
|
|
42
|
+
// brew not found
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!hasBrew) {
|
|
46
|
+
console.log("🍺 Homebrew not found. Installing Homebrew...");
|
|
47
|
+
try {
|
|
48
|
+
await exec("bash", [
|
|
49
|
+
"-c",
|
|
50
|
+
'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
51
|
+
]);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to install Homebrew. Please install Docker manually from https://www.docker.com/products/docker-desktop/\n${message}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Homebrew on Apple Silicon installs to /opt/homebrew; add it to PATH
|
|
60
|
+
// so subsequent brew/colima/docker invocations work in this session.
|
|
61
|
+
if (!process.env.PATH?.includes("/opt/homebrew")) {
|
|
62
|
+
process.env.PATH = `/opt/homebrew/bin:/opt/homebrew/sbin:${process.env.PATH}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
console.log("🐳 Docker not found. Installing via Homebrew...");
|
|
38
67
|
try {
|
|
39
68
|
await exec("brew", ["install", "colima", "docker"]);
|
|
@@ -58,6 +87,21 @@ async function ensureDockerInstalled(): Promise<void> {
|
|
|
58
87
|
try {
|
|
59
88
|
await exec("docker", ["info"]);
|
|
60
89
|
} catch {
|
|
90
|
+
let hasColima = false;
|
|
91
|
+
try {
|
|
92
|
+
await execOutput("colima", ["version"]);
|
|
93
|
+
hasColima = true;
|
|
94
|
+
} catch {
|
|
95
|
+
// colima not found
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!hasColima) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"Docker daemon is not running and Colima is not installed.\n" +
|
|
101
|
+
"Please start Docker Desktop, or install Colima with 'brew install colima' and run 'colima start'.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
61
105
|
console.log("🚀 Docker daemon not running. Starting Colima...");
|
|
62
106
|
try {
|
|
63
107
|
await exec("colima", ["start"]);
|
|
@@ -326,8 +370,10 @@ export async function hatchDocker(
|
|
|
326
370
|
);
|
|
327
371
|
}
|
|
328
372
|
|
|
329
|
-
|
|
330
|
-
|
|
373
|
+
// Docker containers bind to 0.0.0.0 so localhost always works. Skip
|
|
374
|
+
// mDNS/LAN discovery — the .local hostname often fails to resolve on the
|
|
375
|
+
// host machine itself (mDNS is designed for cross-device discovery).
|
|
376
|
+
const runtimeUrl = `http://localhost:${gatewayPort}`;
|
|
331
377
|
const dockerEntry: AssistantEntry = {
|
|
332
378
|
assistantId: instanceName,
|
|
333
379
|
runtimeUrl,
|
package/src/lib/health-check.ts
CHANGED
|
@@ -10,6 +10,93 @@ export interface HealthCheckResult {
|
|
|
10
10
|
detail: string | null;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
interface OrgListResponse {
|
|
14
|
+
results: { id: string }[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fetchOrganizationId(
|
|
18
|
+
platformUrl: string,
|
|
19
|
+
token: string,
|
|
20
|
+
): Promise<{ orgId: string } | { error: string }> {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${platformUrl}/v1/organizations/`, {
|
|
23
|
+
headers: { "X-Session-Token": token },
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return { error: `org lookup failed (${response.status})` };
|
|
27
|
+
}
|
|
28
|
+
const body = (await response.json()) as OrgListResponse;
|
|
29
|
+
const orgId = body.results?.[0]?.id;
|
|
30
|
+
if (!orgId) {
|
|
31
|
+
return { error: "no organization found" };
|
|
32
|
+
}
|
|
33
|
+
return { orgId };
|
|
34
|
+
} catch {
|
|
35
|
+
return { error: "org lookup unreachable" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function checkManagedHealth(
|
|
40
|
+
runtimeUrl: string,
|
|
41
|
+
assistantId: string,
|
|
42
|
+
): Promise<HealthCheckResult> {
|
|
43
|
+
const { readPlatformToken } = await import("./platform-client.js");
|
|
44
|
+
const token = readPlatformToken();
|
|
45
|
+
if (!token) {
|
|
46
|
+
return {
|
|
47
|
+
status: "error (auth)",
|
|
48
|
+
detail: "not logged in — run `vellum login`",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const orgResult = await fetchOrganizationId(runtimeUrl, token);
|
|
53
|
+
if ("error" in orgResult) {
|
|
54
|
+
return {
|
|
55
|
+
status: "error (auth)",
|
|
56
|
+
detail: orgResult.error,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const { orgId } = orgResult;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const url = `${runtimeUrl}/v1/assistants/${encodeURIComponent(assistantId)}/healthz/`;
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timeoutId = setTimeout(
|
|
65
|
+
() => controller.abort(),
|
|
66
|
+
HEALTH_CHECK_TIMEOUT_MS,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
"X-Session-Token": token,
|
|
71
|
+
"Vellum-Organization-Id": orgId,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
headers,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
return { status: `error (${response.status})`, detail: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = (await response.json()) as HealthResponse;
|
|
86
|
+
const status = data.status || "unknown";
|
|
87
|
+
return {
|
|
88
|
+
status,
|
|
89
|
+
detail: status !== "healthy" ? (data.message ?? null) : null,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const status =
|
|
93
|
+
error instanceof Error && error.name === "AbortError"
|
|
94
|
+
? "timeout"
|
|
95
|
+
: "unreachable";
|
|
96
|
+
return { status, detail: null };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
13
100
|
export async function checkHealth(
|
|
14
101
|
runtimeUrl: string,
|
|
15
102
|
bearerToken?: string,
|
|
@@ -107,6 +107,15 @@ export function getTerminalCapabilities(): TerminalCapabilities {
|
|
|
107
107
|
return _cached;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Clear the cached capabilities so the next `getTerminalCapabilities()`
|
|
112
|
+
* call re-detects from the current environment. Useful after modifying
|
|
113
|
+
* `process.env.NO_COLOR` at startup or in tests.
|
|
114
|
+
*/
|
|
115
|
+
export function resetCapabilitiesCache(): void {
|
|
116
|
+
_cached = undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
// ── Convenience helpers ──────────────────────────────────────
|
|
111
120
|
|
|
112
121
|
/** True when colors should be used (any level above "none"). */
|