@vellumai/cli 0.8.2 → 0.8.4
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/AGENTS.md +18 -6
- package/README.md +24 -32
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +108 -0
- package/src/__tests__/assistant-target-args.test.ts +30 -0
- package/src/__tests__/config-utils.test.ts +31 -1
- package/src/__tests__/hatch-provider-secrets.test.ts +284 -0
- package/src/__tests__/host-image-loader.test.ts +206 -0
- package/src/__tests__/ps-platform-status.test.ts +100 -22
- package/src/__tests__/setup.test.ts +65 -1
- package/src/__tests__/teleport.test.ts +1 -0
- package/src/__tests__/use.test.ts +144 -0
- package/src/commands/client.ts +27 -24
- package/src/commands/hatch.ts +53 -20
- package/src/commands/ps.ts +107 -105
- package/src/commands/setup.ts +46 -12
- package/src/commands/teleport.ts +20 -2
- package/src/commands/use.ts +24 -10
- package/src/components/DefaultMainScreen.tsx +27 -115
- package/src/lib/__tests__/docker.test.ts +106 -0
- package/src/lib/assistant-config.ts +86 -5
- package/src/lib/assistant-target-args.ts +21 -0
- package/src/lib/config-utils.ts +18 -0
- package/src/lib/docker.ts +225 -27
- package/src/lib/hatch-local.ts +42 -3
- package/src/lib/hatch-next-steps.ts +12 -0
- package/src/lib/host-image-loader.ts +138 -0
- package/src/lib/platform-releases.ts +12 -5
- package/src/lib/provider-secrets.ts +151 -0
- package/src/shared/provider-env-vars.ts +0 -3
package/src/commands/teleport.ts
CHANGED
|
@@ -877,7 +877,16 @@ export async function resolveOrHatchTarget(
|
|
|
877
877
|
// Hatch a new assistant in the target environment
|
|
878
878
|
if (targetEnv === "local") {
|
|
879
879
|
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
880
|
-
await hatchLocal(
|
|
880
|
+
await hatchLocal(
|
|
881
|
+
"vellum",
|
|
882
|
+
targetName ?? null,
|
|
883
|
+
false,
|
|
884
|
+
false,
|
|
885
|
+
{},
|
|
886
|
+
{
|
|
887
|
+
setupProviderCredentials: false,
|
|
888
|
+
},
|
|
889
|
+
);
|
|
881
890
|
const entry = targetName
|
|
882
891
|
? findAssistantByName(targetName)
|
|
883
892
|
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
@@ -892,7 +901,16 @@ export async function resolveOrHatchTarget(
|
|
|
892
901
|
|
|
893
902
|
if (targetEnv === "docker") {
|
|
894
903
|
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
895
|
-
await hatchDocker(
|
|
904
|
+
await hatchDocker(
|
|
905
|
+
"vellum",
|
|
906
|
+
false,
|
|
907
|
+
targetName ?? null,
|
|
908
|
+
false,
|
|
909
|
+
{},
|
|
910
|
+
{
|
|
911
|
+
setupProviderCredentials: false,
|
|
912
|
+
},
|
|
913
|
+
);
|
|
896
914
|
const entry = targetName
|
|
897
915
|
? findAssistantByName(targetName)
|
|
898
916
|
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
package/src/commands/use.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
formatAssistantLookupError,
|
|
3
|
+
formatAssistantReference,
|
|
3
4
|
getActiveAssistant,
|
|
5
|
+
lookupAssistantByIdentifier,
|
|
4
6
|
setActiveAssistant,
|
|
5
7
|
} from "../lib/assistant-config.js";
|
|
8
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
6
9
|
|
|
7
10
|
export async function use(): Promise<void> {
|
|
8
11
|
const args = process.argv.slice(3);
|
|
9
12
|
|
|
10
13
|
if (args.includes("--help") || args.includes("-h")) {
|
|
11
|
-
console.log("Usage: vellum use [<name>]");
|
|
14
|
+
console.log("Usage: vellum use [<name-or-id>]");
|
|
12
15
|
console.log("");
|
|
13
16
|
console.log("Set the active assistant for commands.");
|
|
14
17
|
console.log("");
|
|
15
18
|
console.log("Arguments:");
|
|
16
|
-
console.log(
|
|
19
|
+
console.log(
|
|
20
|
+
" <name-or-id> Assistant display name or ID to make active",
|
|
21
|
+
);
|
|
17
22
|
console.log("");
|
|
18
23
|
console.log(
|
|
19
24
|
"When called without a name, prints the current active assistant.",
|
|
@@ -21,24 +26,33 @@ export async function use(): Promise<void> {
|
|
|
21
26
|
process.exit(0);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
const name = args
|
|
29
|
+
const name = parseAssistantTargetArg(args);
|
|
25
30
|
|
|
26
31
|
if (!name) {
|
|
27
32
|
const active = getActiveAssistant();
|
|
28
33
|
if (active) {
|
|
29
|
-
|
|
34
|
+
const result = lookupAssistantByIdentifier(active);
|
|
35
|
+
if (result.status === "found") {
|
|
36
|
+
console.log(
|
|
37
|
+
`Active assistant: ${formatAssistantReference(result.entry)}`,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`Active assistant: ${active} (not found in lockfile)`);
|
|
41
|
+
}
|
|
30
42
|
} else {
|
|
31
43
|
console.log("No active assistant set.");
|
|
32
44
|
}
|
|
33
45
|
return;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
console.error(
|
|
48
|
+
const result = lookupAssistantByIdentifier(name);
|
|
49
|
+
if (result.status !== "found") {
|
|
50
|
+
console.error(formatAssistantLookupError(name, result));
|
|
39
51
|
process.exit(1);
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
setActiveAssistant(
|
|
43
|
-
console.log(
|
|
54
|
+
setActiveAssistant(result.entry.assistantId);
|
|
55
|
+
console.log(
|
|
56
|
+
`Active assistant set to ${formatAssistantReference(result.entry)}.`,
|
|
57
|
+
);
|
|
44
58
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
1
|
import { basename } from "path";
|
|
3
2
|
import {
|
|
4
3
|
useCallback,
|
|
@@ -10,8 +9,6 @@ import {
|
|
|
10
9
|
} from "react";
|
|
11
10
|
import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
|
|
12
11
|
|
|
13
|
-
import { removeAssistantEntry } from "../lib/assistant-config";
|
|
14
|
-
|
|
15
12
|
import { SPECIES_CONFIG, type Species } from "../lib/constants";
|
|
16
13
|
import { checkHealth } from "../lib/health-check";
|
|
17
14
|
import { appendHistory, loadHistory } from "../lib/input-history";
|
|
@@ -43,9 +40,27 @@ export const SLASH_COMMANDS = [
|
|
|
43
40
|
"/help",
|
|
44
41
|
"/q",
|
|
45
42
|
"/quit",
|
|
46
|
-
"/retire",
|
|
47
43
|
];
|
|
48
44
|
|
|
45
|
+
const HELP_COMMANDS = [
|
|
46
|
+
{
|
|
47
|
+
command: "/btw <question>",
|
|
48
|
+
description: "Ask a side question while the assistant is working",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
command: "/quit, /exit, /q",
|
|
52
|
+
description: "Disconnect and exit",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
command: "/clear",
|
|
56
|
+
description: "Clear the screen",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
command: "/help, ?",
|
|
60
|
+
description: "Show this help",
|
|
61
|
+
},
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
49
64
|
const SEND_TIMEOUT_MS = 5000;
|
|
50
65
|
|
|
51
66
|
// ── Layout constants ──────────────────────────────────────
|
|
@@ -100,12 +115,11 @@ const MIN_FEED_ROWS = 3;
|
|
|
100
115
|
// Feed item height estimation
|
|
101
116
|
const TOOL_CALL_CHROME_LINES = 2; // header (┌) + footer (└)
|
|
102
117
|
const MESSAGE_SPACING = 1;
|
|
103
|
-
const HELP_DISPLAY_HEIGHT =
|
|
118
|
+
const HELP_DISPLAY_HEIGHT = HELP_COMMANDS.length + 1;
|
|
104
119
|
|
|
105
120
|
interface ListMessagesResponse {
|
|
106
121
|
messages: RuntimeMessage[];
|
|
107
122
|
nextCursor?: string;
|
|
108
|
-
interfaces?: string[];
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
interface SendMessageResponse {
|
|
@@ -742,26 +756,12 @@ function HelpDisplay(): ReactElement {
|
|
|
742
756
|
return (
|
|
743
757
|
<Box flexDirection="column">
|
|
744
758
|
<Text bold>Commands:</Text>
|
|
745
|
-
|
|
746
|
-
{
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
<Text dimColor>Retire the remote instance and exit</Text>
|
|
752
|
-
</Text>
|
|
753
|
-
<Text>
|
|
754
|
-
{" /quit, /exit, /q "}
|
|
755
|
-
<Text dimColor>Disconnect and exit</Text>
|
|
756
|
-
</Text>
|
|
757
|
-
<Text>
|
|
758
|
-
{" /clear "}
|
|
759
|
-
<Text dimColor>Clear the screen</Text>
|
|
760
|
-
</Text>
|
|
761
|
-
<Text>
|
|
762
|
-
{" /help, ? "}
|
|
763
|
-
<Text dimColor>Show this help</Text>
|
|
764
|
-
</Text>
|
|
759
|
+
{HELP_COMMANDS.map((entry) => (
|
|
760
|
+
<Text key={entry.command}>
|
|
761
|
+
{` ${entry.command.padEnd(17)} `}
|
|
762
|
+
<Text dimColor>{entry.description}</Text>
|
|
763
|
+
</Text>
|
|
764
|
+
))}
|
|
765
765
|
</Box>
|
|
766
766
|
);
|
|
767
767
|
}
|
|
@@ -1391,8 +1391,6 @@ interface ChatAppProps {
|
|
|
1391
1391
|
/** Pre-built auth headers (e.g. { Authorization: "Bearer ..." } for local,
|
|
1392
1392
|
* { "X-Session-Token": "...", "Vellum-Organization-Id": "..." } for platform). */
|
|
1393
1393
|
auth?: Record<string, string>;
|
|
1394
|
-
project?: string;
|
|
1395
|
-
zone?: string;
|
|
1396
1394
|
onExit: () => void;
|
|
1397
1395
|
handleRef: (handle: ChatAppHandle) => void;
|
|
1398
1396
|
}
|
|
@@ -1403,8 +1401,6 @@ function ChatApp({
|
|
|
1403
1401
|
assistantName,
|
|
1404
1402
|
species,
|
|
1405
1403
|
auth,
|
|
1406
|
-
project,
|
|
1407
|
-
zone,
|
|
1408
1404
|
onExit,
|
|
1409
1405
|
handleRef,
|
|
1410
1406
|
}: ChatAppProps): ReactElement {
|
|
@@ -1954,86 +1950,6 @@ function ChatApp({
|
|
|
1954
1950
|
return;
|
|
1955
1951
|
}
|
|
1956
1952
|
|
|
1957
|
-
if (trimmed === "/retire") {
|
|
1958
|
-
if (!project || !zone) {
|
|
1959
|
-
h.showError(
|
|
1960
|
-
"No instance info available. Connect to a hatched instance first.",
|
|
1961
|
-
);
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
const confirmIndex = await h.showSelection(`Retire ${assistantId}?`, [
|
|
1966
|
-
"Yes, retire",
|
|
1967
|
-
"Cancel",
|
|
1968
|
-
]);
|
|
1969
|
-
if (confirmIndex !== 0) {
|
|
1970
|
-
h.addStatus("Cancelled.");
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
h.showSpinner(`Retiring instance ${assistantId}...`);
|
|
1975
|
-
|
|
1976
|
-
try {
|
|
1977
|
-
const labelChild = spawn(
|
|
1978
|
-
"gcloud",
|
|
1979
|
-
[
|
|
1980
|
-
"compute",
|
|
1981
|
-
"instances",
|
|
1982
|
-
"add-labels",
|
|
1983
|
-
assistantId,
|
|
1984
|
-
`--project=${project}`,
|
|
1985
|
-
`--zone=${zone}`,
|
|
1986
|
-
"--labels=retired-by=vel",
|
|
1987
|
-
],
|
|
1988
|
-
{ stdio: "pipe" },
|
|
1989
|
-
);
|
|
1990
|
-
await new Promise<void>((resolve) => {
|
|
1991
|
-
labelChild.on("close", () => resolve());
|
|
1992
|
-
labelChild.on("error", () => resolve());
|
|
1993
|
-
});
|
|
1994
|
-
} catch {
|
|
1995
|
-
// Best-effort labeling before deletion
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
const child = spawn(
|
|
1999
|
-
"gcloud",
|
|
2000
|
-
[
|
|
2001
|
-
"compute",
|
|
2002
|
-
"instances",
|
|
2003
|
-
"delete",
|
|
2004
|
-
assistantId,
|
|
2005
|
-
`--project=${project}`,
|
|
2006
|
-
`--zone=${zone}`,
|
|
2007
|
-
"--quiet",
|
|
2008
|
-
],
|
|
2009
|
-
{ stdio: "pipe" },
|
|
2010
|
-
);
|
|
2011
|
-
|
|
2012
|
-
child.on("close", (code) => {
|
|
2013
|
-
handleRef_.current?.hideSpinner();
|
|
2014
|
-
if (code === 0) {
|
|
2015
|
-
removeAssistantEntry(assistantId);
|
|
2016
|
-
handleRef_.current?.addStatus(
|
|
2017
|
-
`Removed ${assistantId} from lockfile.json`,
|
|
2018
|
-
);
|
|
2019
|
-
} else {
|
|
2020
|
-
handleRef_.current?.showError(
|
|
2021
|
-
`Failed to delete instance (exit code ${code})`,
|
|
2022
|
-
);
|
|
2023
|
-
}
|
|
2024
|
-
cleanup();
|
|
2025
|
-
process.exit(code === 0 ? 0 : 1);
|
|
2026
|
-
});
|
|
2027
|
-
|
|
2028
|
-
child.on("error", (err) => {
|
|
2029
|
-
handleRef_.current?.hideSpinner();
|
|
2030
|
-
handleRef_.current?.showError(
|
|
2031
|
-
`Failed to retire instance: ${err.message}`,
|
|
2032
|
-
);
|
|
2033
|
-
});
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
1953
|
// If a connection attempt is already in progress, don't silently drop input
|
|
2038
1954
|
if (connectingRef.current) {
|
|
2039
1955
|
h.addStatus(
|
|
@@ -2218,7 +2134,7 @@ function ChatApp({
|
|
|
2218
2134
|
// racing with SSE events that may arrive during the sendMessage await.
|
|
2219
2135
|
h.showSpinner("Working...");
|
|
2220
2136
|
},
|
|
2221
|
-
[runtimeUrl, assistantId, auth,
|
|
2137
|
+
[runtimeUrl, assistantId, auth, cleanup, ensureConnected],
|
|
2222
2138
|
);
|
|
2223
2139
|
|
|
2224
2140
|
const handleSubmit = useCallback(
|
|
@@ -2530,8 +2446,6 @@ export function renderChatApp(
|
|
|
2530
2446
|
onExit: () => void,
|
|
2531
2447
|
options?: {
|
|
2532
2448
|
auth?: Record<string, string>;
|
|
2533
|
-
project?: string;
|
|
2534
|
-
zone?: string;
|
|
2535
2449
|
assistantName?: string;
|
|
2536
2450
|
},
|
|
2537
2451
|
): ChatAppInstance {
|
|
@@ -2544,8 +2458,6 @@ export function renderChatApp(
|
|
|
2544
2458
|
assistantName={options?.assistantName}
|
|
2545
2459
|
species={species}
|
|
2546
2460
|
auth={options?.auth}
|
|
2547
|
-
project={options?.project}
|
|
2548
|
-
zone={options?.zone}
|
|
2549
2461
|
onExit={onExit}
|
|
2550
2462
|
handleRef={(h) => {
|
|
2551
2463
|
chatHandle = h;
|
|
@@ -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
|
+
});
|
|
@@ -89,6 +89,8 @@ export interface AssistantEntry {
|
|
|
89
89
|
resources?: LocalInstanceResources;
|
|
90
90
|
/** PID of the file watcher process for docker instances hatched with --watch. */
|
|
91
91
|
watcherPid?: number;
|
|
92
|
+
/** Local bootstrap secret used to lease guardian tokens for Docker assistants after detached hatch. */
|
|
93
|
+
guardianBootstrapSecret?: string;
|
|
92
94
|
/** Docker image metadata for rollback. Only present for docker topology entries. */
|
|
93
95
|
containerInfo?: ContainerInfo;
|
|
94
96
|
/** Docker image metadata from before the last upgrade. Enables rollback to the prior version. */
|
|
@@ -107,6 +109,11 @@ export interface AssistantEntry {
|
|
|
107
109
|
[key: string]: unknown;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
export type AssistantLookupResult =
|
|
113
|
+
| { status: "found"; entry: AssistantEntry }
|
|
114
|
+
| { status: "not_found" }
|
|
115
|
+
| { status: "ambiguous"; matches: AssistantEntry[] };
|
|
116
|
+
|
|
110
117
|
interface LockfileData {
|
|
111
118
|
assistants?: Record<string, unknown>[];
|
|
112
119
|
activeAssistant?: string;
|
|
@@ -307,8 +314,70 @@ export function loadLatestAssistant(): AssistantEntry | null {
|
|
|
307
314
|
}
|
|
308
315
|
|
|
309
316
|
export function findAssistantByName(name: string): AssistantEntry | null {
|
|
317
|
+
return readAssistants().find((entry) => entry.assistantId === name) ?? null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function getAssistantDisplayName(entry: AssistantEntry): string {
|
|
321
|
+
const primary = entry.name?.trim();
|
|
322
|
+
if (primary) return primary;
|
|
323
|
+
|
|
324
|
+
const legacy = entry.assistantName?.trim();
|
|
325
|
+
if (legacy) return legacy;
|
|
326
|
+
|
|
327
|
+
return entry.assistantId;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function formatAssistantReference(entry: AssistantEntry): string {
|
|
331
|
+
const displayName = getAssistantDisplayName(entry);
|
|
332
|
+
return displayName === entry.assistantId
|
|
333
|
+
? entry.assistantId
|
|
334
|
+
: `${displayName} (${entry.assistantId})`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getAssistantDisplayNameCandidates(entry: AssistantEntry): string[] {
|
|
338
|
+
return Array.from(
|
|
339
|
+
new Set(
|
|
340
|
+
[entry.name?.trim(), entry.assistantName?.trim()].filter(
|
|
341
|
+
(value): value is string => typeof value === "string" && value !== "",
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function lookupAssistantByIdentifier(
|
|
348
|
+
identifier: string,
|
|
349
|
+
): AssistantLookupResult {
|
|
310
350
|
const entries = readAssistants();
|
|
311
|
-
|
|
351
|
+
const exactId = entries.find((entry) => entry.assistantId === identifier);
|
|
352
|
+
if (exactId) {
|
|
353
|
+
return { status: "found", entry: exactId };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const displayMatches = entries.filter((entry) =>
|
|
357
|
+
getAssistantDisplayNameCandidates(entry).includes(identifier),
|
|
358
|
+
);
|
|
359
|
+
if (displayMatches.length === 1) {
|
|
360
|
+
return { status: "found", entry: displayMatches[0] };
|
|
361
|
+
}
|
|
362
|
+
if (displayMatches.length > 1) {
|
|
363
|
+
return { status: "ambiguous", matches: displayMatches };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { status: "not_found" };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function formatAssistantLookupError(
|
|
370
|
+
identifier: string,
|
|
371
|
+
result: AssistantLookupResult = lookupAssistantByIdentifier(identifier),
|
|
372
|
+
): string {
|
|
373
|
+
if (result.status === "ambiguous") {
|
|
374
|
+
const matches = result.matches
|
|
375
|
+
.map((entry) => formatAssistantReference(entry))
|
|
376
|
+
.join(", ");
|
|
377
|
+
return `Multiple assistants match '${identifier}': ${matches}. Use the assistant ID to disambiguate.`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return `No assistant found with name or ID '${identifier}'.`;
|
|
312
381
|
}
|
|
313
382
|
|
|
314
383
|
export function removeAssistantEntry(assistantId: string): void {
|
|
@@ -444,13 +513,25 @@ export function resolveAssistant(nameArg?: string): AssistantEntry | null {
|
|
|
444
513
|
* 3. Sole lockfile entry (any cloud)
|
|
445
514
|
*/
|
|
446
515
|
export function resolveTargetAssistant(nameArg?: string): AssistantEntry {
|
|
447
|
-
const entry = resolveAssistant(nameArg);
|
|
448
|
-
if (entry) return entry;
|
|
449
|
-
|
|
450
516
|
if (nameArg) {
|
|
451
|
-
|
|
517
|
+
const result = lookupAssistantByIdentifier(nameArg);
|
|
518
|
+
if (result.status === "found") return result.entry;
|
|
519
|
+
console.error(formatAssistantLookupError(nameArg, result));
|
|
452
520
|
} else {
|
|
521
|
+
const active = getActiveAssistant();
|
|
522
|
+
if (active) {
|
|
523
|
+
const result = lookupAssistantByIdentifier(active);
|
|
524
|
+
if (result.status === "found") return result.entry;
|
|
525
|
+
if (result.status === "ambiguous") {
|
|
526
|
+
console.error(formatAssistantLookupError(active, result));
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
// Active assistant no longer exists in lockfile — fall through.
|
|
530
|
+
}
|
|
531
|
+
|
|
453
532
|
const all = readAssistants();
|
|
533
|
+
if (all.length === 1) return all[0];
|
|
534
|
+
|
|
454
535
|
if (all.length === 0) {
|
|
455
536
|
console.error("No assistant found. Run 'vellum hatch' first.");
|
|
456
537
|
} else {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function parseAssistantTargetArg(
|
|
2
|
+
args: string[],
|
|
3
|
+
flagsWithValues: readonly string[] = [],
|
|
4
|
+
): string | undefined {
|
|
5
|
+
const flagsWithValuesSet = new Set(flagsWithValues);
|
|
6
|
+
const parts: string[] = [];
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const arg = args[i];
|
|
10
|
+
if (flagsWithValuesSet.has(arg)) {
|
|
11
|
+
i++;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg.startsWith("-")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
parts.push(arg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
21
|
+
}
|
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
|