@vellumai/cli 0.1.12 → 0.1.14
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 +14 -16
- package/src/components/DefaultMainScreen.tsx +10 -31
- package/src/lib/aws.ts +20 -33
- package/src/lib/gcp.ts +12 -50
- package/src/lib/local.ts +24 -6
package/package.json
CHANGED
package/src/commands/ps.ts
CHANGED
|
@@ -145,7 +145,7 @@ async function getRemoteProcessesCustom(
|
|
|
145
145
|
]);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
function getLocalProcesses(): TableRow[] {
|
|
149
149
|
const rows: TableRow[] = [];
|
|
150
150
|
const vellumDir = join(homedir(), ".vellum");
|
|
151
151
|
|
|
@@ -179,21 +179,19 @@ async function getLocalProcesses(): Promise<TableRow[]> {
|
|
|
179
179
|
rows.push({ name: "qdrant", status: withStatusEmoji("not running"), info: "no PID file" });
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// Check gateway
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
rows.push({ name: "gateway", status: withStatusEmoji("running"), info: `PID ${pid} | port 7830` });
|
|
192
|
-
} else {
|
|
193
|
-
rows.push({ name: "gateway", status: withStatusEmoji("not running"), info: "" });
|
|
182
|
+
// Check gateway PID
|
|
183
|
+
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
184
|
+
if (existsSync(gatewayPidFile)) {
|
|
185
|
+
const pid = readFileSync(gatewayPidFile, "utf-8").trim();
|
|
186
|
+
let status = "running";
|
|
187
|
+
try {
|
|
188
|
+
process.kill(parseInt(pid, 10), 0);
|
|
189
|
+
} catch {
|
|
190
|
+
status = "not running";
|
|
194
191
|
}
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
rows.push({ name: "gateway", status: withStatusEmoji(status), info: `PID ${pid} | port 7830` });
|
|
193
|
+
} else {
|
|
194
|
+
rows.push({ name: "gateway", status: withStatusEmoji("not running"), info: "no PID file" });
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
return rows;
|
|
@@ -205,7 +203,7 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
205
203
|
console.log(`Processes for ${entry.assistantId} (${cloud}):\n`);
|
|
206
204
|
|
|
207
205
|
if (cloud === "local") {
|
|
208
|
-
const rows =
|
|
206
|
+
const rows = getLocalProcesses();
|
|
209
207
|
printTable(rows);
|
|
210
208
|
return;
|
|
211
209
|
}
|
|
@@ -28,7 +28,6 @@ export const SLASH_COMMANDS = ["/clear", "/doctor", "/exit", "/help", "/q", "/qu
|
|
|
28
28
|
const POLL_INTERVAL_MS = 3000;
|
|
29
29
|
const SEND_TIMEOUT_MS = 5000;
|
|
30
30
|
const RESPONSE_POLL_INTERVAL_MS = 1000;
|
|
31
|
-
const RESPONSE_TIMEOUT_MS = 180000;
|
|
32
31
|
|
|
33
32
|
interface ListMessagesResponse {
|
|
34
33
|
messages: RuntimeMessage[];
|
|
@@ -59,9 +58,6 @@ interface PendingConfirmation {
|
|
|
59
58
|
executionTarget?: "sandbox" | "host";
|
|
60
59
|
allowlistOptions?: AllowlistOption[];
|
|
61
60
|
scopeOptions?: ScopeOption[];
|
|
62
|
-
principalKind?: string;
|
|
63
|
-
principalId?: string;
|
|
64
|
-
principalVersion?: string;
|
|
65
61
|
persistentDecisionsAllowed?: boolean;
|
|
66
62
|
}
|
|
67
63
|
|
|
@@ -1021,8 +1017,8 @@ function ChatApp({
|
|
|
1021
1017
|
: secretInput
|
|
1022
1018
|
? 5
|
|
1023
1019
|
: spinnerText
|
|
1024
|
-
?
|
|
1025
|
-
:
|
|
1020
|
+
? 4
|
|
1021
|
+
: 3;
|
|
1026
1022
|
const availableRows = Math.max(3, terminalRows - headerHeight - bottomHeight);
|
|
1027
1023
|
|
|
1028
1024
|
const addMessage = useCallback((msg: RuntimeMessage) => {
|
|
@@ -1515,8 +1511,7 @@ function ChatApp({
|
|
|
1515
1511
|
|
|
1516
1512
|
h.showSpinner("Working...");
|
|
1517
1513
|
|
|
1518
|
-
|
|
1519
|
-
while (Date.now() - startTime < RESPONSE_TIMEOUT_MS) {
|
|
1514
|
+
while (true) {
|
|
1520
1515
|
await new Promise((resolve) => setTimeout(resolve, RESPONSE_POLL_INTERVAL_MS));
|
|
1521
1516
|
|
|
1522
1517
|
if (runId) {
|
|
@@ -1609,26 +1604,6 @@ function ChatApp({
|
|
|
1609
1604
|
}
|
|
1610
1605
|
}
|
|
1611
1606
|
|
|
1612
|
-
h.setBusy(false);
|
|
1613
|
-
h.hideSpinner();
|
|
1614
|
-
h.showError("Response timed out. The assistant may still be processing.");
|
|
1615
|
-
try {
|
|
1616
|
-
const doctorResult = await callDoctorDaemon(
|
|
1617
|
-
assistantId,
|
|
1618
|
-
project,
|
|
1619
|
-
zone,
|
|
1620
|
-
undefined,
|
|
1621
|
-
undefined,
|
|
1622
|
-
doctorSessionIdRef.current,
|
|
1623
|
-
);
|
|
1624
|
-
if (doctorResult.diagnostics) {
|
|
1625
|
-
h.addStatus(
|
|
1626
|
-
`--- SSH Diagnostics ---\n${doctorResult.diagnostics}\n--- End Diagnostics ---`,
|
|
1627
|
-
);
|
|
1628
|
-
}
|
|
1629
|
-
} catch {
|
|
1630
|
-
// Doctor daemon unreachable; skip diagnostics
|
|
1631
|
-
}
|
|
1632
1607
|
} catch (error) {
|
|
1633
1608
|
h.setBusy(false);
|
|
1634
1609
|
h.hideSpinner();
|
|
@@ -1852,9 +1827,11 @@ function ChatApp({
|
|
|
1852
1827
|
) : null}
|
|
1853
1828
|
|
|
1854
1829
|
{!selection && !secretInput ? (
|
|
1855
|
-
<Box>
|
|
1856
|
-
<Text
|
|
1857
|
-
|
|
1830
|
+
<Box flexDirection="column">
|
|
1831
|
+
<Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
|
|
1832
|
+
<Box paddingLeft={1}>
|
|
1833
|
+
<Text color="green" bold>
|
|
1834
|
+
you{">"}
|
|
1858
1835
|
{" "}
|
|
1859
1836
|
</Text>
|
|
1860
1837
|
<TextInput
|
|
@@ -1863,6 +1840,8 @@ function ChatApp({
|
|
|
1863
1840
|
onSubmit={handleSubmit}
|
|
1864
1841
|
focus={inputFocused}
|
|
1865
1842
|
/>
|
|
1843
|
+
</Box>
|
|
1844
|
+
<Text dimColor>{"\u2500".repeat(terminalColumns)}</Text>
|
|
1866
1845
|
</Box>
|
|
1867
1846
|
) : null}
|
|
1868
1847
|
</Box>
|
package/src/lib/aws.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn as spawnChild } from "child_process";
|
|
2
1
|
import { randomBytes } from "crypto";
|
|
3
2
|
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
4
3
|
import { homedir, tmpdir, userInfo } from "os";
|
|
@@ -224,6 +223,7 @@ export async function launchInstance(
|
|
|
224
223
|
userDataPath: string,
|
|
225
224
|
species: string,
|
|
226
225
|
region: string,
|
|
226
|
+
hatchedBy?: string,
|
|
227
227
|
): Promise<string> {
|
|
228
228
|
const blockDeviceMappings = JSON.stringify([
|
|
229
229
|
{
|
|
@@ -231,14 +231,18 @@ export async function launchInstance(
|
|
|
231
231
|
Ebs: { VolumeSize: 50, VolumeType: "gp3" },
|
|
232
232
|
},
|
|
233
233
|
]);
|
|
234
|
+
const tags = [
|
|
235
|
+
{ Key: "Name", Value: name },
|
|
236
|
+
{ Key: "vellum-assistant", Value: "true" },
|
|
237
|
+
{ Key: "species", Value: species },
|
|
238
|
+
];
|
|
239
|
+
if (hatchedBy) {
|
|
240
|
+
tags.push({ Key: "hatched-by", Value: hatchedBy });
|
|
241
|
+
}
|
|
234
242
|
const tagSpecifications = JSON.stringify([
|
|
235
243
|
{
|
|
236
244
|
ResourceType: "instance",
|
|
237
|
-
Tags:
|
|
238
|
-
{ Key: "Name", Value: name },
|
|
239
|
-
{ Key: "vellum-assistant", Value: "true" },
|
|
240
|
-
{ Key: "species", Value: species },
|
|
241
|
-
],
|
|
245
|
+
Tags: tags,
|
|
242
246
|
},
|
|
243
247
|
]);
|
|
244
248
|
|
|
@@ -398,6 +402,7 @@ export async function hatchAws(
|
|
|
398
402
|
|
|
399
403
|
const sshUser = userInfo().username;
|
|
400
404
|
const bearerToken = randomBytes(32).toString("hex");
|
|
405
|
+
const hatchedBy = process.env.VELLUM_HATCHED_BY;
|
|
401
406
|
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
402
407
|
if (!anthropicApiKey) {
|
|
403
408
|
console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
|
|
@@ -442,6 +447,7 @@ export async function hatchAws(
|
|
|
442
447
|
startupScriptPath,
|
|
443
448
|
species,
|
|
444
449
|
region,
|
|
450
|
+
hatchedBy,
|
|
445
451
|
);
|
|
446
452
|
} finally {
|
|
447
453
|
try {
|
|
@@ -606,33 +612,14 @@ export async function retireInstance(
|
|
|
606
612
|
|
|
607
613
|
console.log(`\u{1F5D1}\ufe0f Terminating AWS instance ${name} (${instanceId})\n`);
|
|
608
614
|
|
|
609
|
-
|
|
610
|
-
"
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
region,
|
|
618
|
-
],
|
|
619
|
-
{ stdio: "inherit" },
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
await new Promise<void>((resolve, reject) => {
|
|
623
|
-
child.on("close", (code) => {
|
|
624
|
-
if (code === 0) {
|
|
625
|
-
resolve();
|
|
626
|
-
} else {
|
|
627
|
-
reject(
|
|
628
|
-
new Error(
|
|
629
|
-
`aws ec2 terminate-instances exited with code ${code}`,
|
|
630
|
-
),
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
child.on("error", reject);
|
|
635
|
-
});
|
|
615
|
+
await exec("aws", [
|
|
616
|
+
"ec2",
|
|
617
|
+
"terminate-instances",
|
|
618
|
+
"--instance-ids",
|
|
619
|
+
instanceId,
|
|
620
|
+
"--region",
|
|
621
|
+
region,
|
|
622
|
+
]);
|
|
636
623
|
|
|
637
624
|
console.log(`\u2705 Instance ${name} (${instanceId}) terminated.`);
|
|
638
625
|
}
|
package/src/lib/gcp.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
1
|
import { randomBytes } from "crypto";
|
|
3
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, unlinkSync, writeFileSync } from "fs";
|
|
4
3
|
import { tmpdir, userInfo } from "os";
|
|
5
4
|
import { join } from "path";
|
|
6
5
|
|
|
@@ -11,26 +10,6 @@ import type { Species } from "./constants";
|
|
|
11
10
|
import { generateRandomSuffix } from "./random-name";
|
|
12
11
|
import { exec, execOutput } from "./step-runner";
|
|
13
12
|
|
|
14
|
-
export async function activateServiceAccount(): Promise<(() => void) | null> {
|
|
15
|
-
const account = process.env.GCP_ACCOUNT_EMAIL;
|
|
16
|
-
const keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
17
|
-
if (!account || !keyFile) return null;
|
|
18
|
-
|
|
19
|
-
const gcpConfigDir = mkdtempSync(join(tmpdir(), "vellum-gcloud-"));
|
|
20
|
-
process.env.CLOUDSDK_CONFIG = gcpConfigDir;
|
|
21
|
-
await exec("gcloud", [
|
|
22
|
-
"auth",
|
|
23
|
-
"activate-service-account",
|
|
24
|
-
account,
|
|
25
|
-
`--key-file=${keyFile}`,
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
return () => {
|
|
29
|
-
delete process.env.CLOUDSDK_CONFIG;
|
|
30
|
-
try { rmSync(gcpConfigDir, { recursive: true, force: true }); } catch {}
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
13
|
export async function getActiveProject(): Promise<string> {
|
|
35
14
|
const output = await execOutput("gcloud", [
|
|
36
15
|
"config",
|
|
@@ -532,7 +511,6 @@ export async function hatchGcp(
|
|
|
532
511
|
): Promise<void> {
|
|
533
512
|
const startTime = Date.now();
|
|
534
513
|
const account = process.env.GCP_ACCOUNT_EMAIL;
|
|
535
|
-
const cleanupServiceAccount = await activateServiceAccount();
|
|
536
514
|
|
|
537
515
|
try {
|
|
538
516
|
const project = process.env.GCP_PROJECT ?? (await getActiveProject());
|
|
@@ -576,6 +554,7 @@ export async function hatchGcp(
|
|
|
576
554
|
|
|
577
555
|
const sshUser = userInfo().username;
|
|
578
556
|
const bearerToken = randomBytes(32).toString("hex");
|
|
557
|
+
const hatchedBy = process.env.VELLUM_HATCHED_BY;
|
|
579
558
|
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
580
559
|
if (!anthropicApiKey) {
|
|
581
560
|
console.error("Error: ANTHROPIC_API_KEY environment variable is not set.");
|
|
@@ -607,7 +586,7 @@ export async function hatchGcp(
|
|
|
607
586
|
"--boot-disk-size=50GB",
|
|
608
587
|
"--boot-disk-type=pd-standard",
|
|
609
588
|
`--metadata-from-file=startup-script=${startupScriptPath}`,
|
|
610
|
-
`--labels=species=${species},vellum-assistant=true`,
|
|
589
|
+
`--labels=species=${species},vellum-assistant=true${hatchedBy ? `,hatched-by=${hatchedBy.toLowerCase().replace(/[^a-z0-9_-]/g, "_")}` : ""}`,
|
|
611
590
|
"--tags=vellum-assistant",
|
|
612
591
|
"--no-service-account",
|
|
613
592
|
"--no-scopes",
|
|
@@ -716,8 +695,6 @@ export async function hatchGcp(
|
|
|
716
695
|
} catch (error) {
|
|
717
696
|
console.error("\u274c Error:", error instanceof Error ? error.message : error);
|
|
718
697
|
process.exit(1);
|
|
719
|
-
} finally {
|
|
720
|
-
cleanupServiceAccount?.();
|
|
721
698
|
}
|
|
722
699
|
}
|
|
723
700
|
|
|
@@ -772,30 +749,15 @@ export async function retireInstance(
|
|
|
772
749
|
|
|
773
750
|
console.log(`\u{1F5D1}\ufe0f Deleting GCP instance ${name}\n`);
|
|
774
751
|
|
|
775
|
-
|
|
776
|
-
"
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
"--quiet",
|
|
785
|
-
],
|
|
786
|
-
{ stdio: "inherit" },
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
await new Promise<void>((resolve, reject) => {
|
|
790
|
-
child.on("close", (code) => {
|
|
791
|
-
if (code === 0) {
|
|
792
|
-
resolve();
|
|
793
|
-
} else {
|
|
794
|
-
reject(new Error(`gcloud instance delete exited with code ${code}`));
|
|
795
|
-
}
|
|
796
|
-
});
|
|
797
|
-
child.on("error", reject);
|
|
798
|
-
});
|
|
752
|
+
await exec("gcloud", [
|
|
753
|
+
"compute",
|
|
754
|
+
"instances",
|
|
755
|
+
"delete",
|
|
756
|
+
name,
|
|
757
|
+
`--project=${project}`,
|
|
758
|
+
`--zone=${zone}`,
|
|
759
|
+
"--quiet",
|
|
760
|
+
]);
|
|
799
761
|
|
|
800
762
|
console.log(`\u2705 Instance ${name} deleted.`);
|
|
801
763
|
}
|
package/src/lib/local.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { createRequire } from "module";
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { dirname, join } from "path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { loadAllAssistants, loadLatestAssistant } from "./assistant-config.js";
|
|
8
|
+
import { GATEWAY_PORT } from "./constants.js";
|
|
8
9
|
|
|
9
10
|
const _require = createRequire(import.meta.url);
|
|
10
11
|
|
|
@@ -286,24 +287,41 @@ export async function startGateway(): Promise<string> {
|
|
|
286
287
|
|
|
287
288
|
console.log("🌐 Starting gateway...");
|
|
288
289
|
const gatewayDir = resolveGatewayDir();
|
|
290
|
+
// Only auto-configure default routing when the workspace has exactly one
|
|
291
|
+
// assistant. In multi-assistant deployments, falling back to "default"
|
|
292
|
+
// would silently deliver unmapped Telegram chats to whichever assistant was
|
|
293
|
+
// most recently hatched — keep the "reject" policy instead.
|
|
294
|
+
const assistants = loadAllAssistants();
|
|
295
|
+
const isSingleAssistant = assistants.length === 1;
|
|
296
|
+
|
|
289
297
|
const gatewayEnv: Record<string, string> = {
|
|
290
298
|
...process.env as Record<string, string>,
|
|
291
299
|
GATEWAY_RUNTIME_PROXY_ENABLED: "true",
|
|
292
300
|
GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "false",
|
|
293
301
|
RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
|
|
294
302
|
};
|
|
303
|
+
|
|
304
|
+
if (process.env.GATEWAY_UNMAPPED_POLICY) {
|
|
305
|
+
gatewayEnv.GATEWAY_UNMAPPED_POLICY = process.env.GATEWAY_UNMAPPED_POLICY;
|
|
306
|
+
} else if (isSingleAssistant) {
|
|
307
|
+
gatewayEnv.GATEWAY_UNMAPPED_POLICY = "default";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (process.env.GATEWAY_DEFAULT_ASSISTANT_ID) {
|
|
311
|
+
gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID = process.env.GATEWAY_DEFAULT_ASSISTANT_ID;
|
|
312
|
+
} else if (isSingleAssistant) {
|
|
313
|
+
gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID =
|
|
314
|
+
assistants[0].assistantId || loadLatestAssistant()?.assistantId || "default";
|
|
315
|
+
}
|
|
295
316
|
const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl();
|
|
296
317
|
const ingressPublicBaseUrl =
|
|
297
318
|
workspaceIngressPublicBaseUrl
|
|
298
|
-
?? normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL)
|
|
319
|
+
?? normalizeIngressUrl(process.env.INGRESS_PUBLIC_BASE_URL)
|
|
320
|
+
?? publicUrl;
|
|
299
321
|
if (ingressPublicBaseUrl) {
|
|
300
322
|
gatewayEnv.INGRESS_PUBLIC_BASE_URL = ingressPublicBaseUrl;
|
|
301
323
|
console.log(` Ingress URL: ${ingressPublicBaseUrl}`);
|
|
302
|
-
if (!workspaceIngressPublicBaseUrl) {
|
|
303
|
-
console.log(" (using INGRESS_PUBLIC_BASE_URL env fallback)");
|
|
304
|
-
}
|
|
305
324
|
}
|
|
306
|
-
if (publicUrl) gatewayEnv.GATEWAY_PUBLIC_URL = publicUrl;
|
|
307
325
|
|
|
308
326
|
const gateway = spawn("bun", ["run", "src/index.ts"], {
|
|
309
327
|
cwd: gatewayDir,
|