@vellumai/cli 0.1.13 → 0.3.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.1.13",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -520,16 +520,7 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
520
520
 
521
521
  await startLocalDaemon();
522
522
 
523
- // The desktop app communicates with the daemon directly via Unix socket,
524
- // so the HTTP gateway is only needed for non-desktop (CLI) usage.
525
- let runtimeUrl: string;
526
-
527
- if (process.env.VELLUM_DESKTOP_APP) {
528
- // No gateway needed — the macOS app uses DaemonClient over the Unix socket.
529
- runtimeUrl = "local";
530
- } else {
531
- runtimeUrl = await startGateway();
532
- }
523
+ const runtimeUrl = await startGateway();
533
524
 
534
525
  const baseDataDir = join(process.env.BASE_DATA_DIR?.trim() || (process.env.HOME ?? userInfo().homedir), ".vellum");
535
526
  const localEntry: AssistantEntry = {
@@ -145,7 +145,7 @@ async function getRemoteProcessesCustom(
145
145
  ]);
146
146
  }
147
147
 
148
- async function getLocalProcesses(): Promise<TableRow[]> {
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 via ps
183
- try {
184
- const output = await execOutput("ps", ["ax", "-o", "pid=,command="]);
185
- const gatewayLines = output
186
- .split("\n")
187
- .filter((l) => /gateway\/src\/index\.ts/.test(l) && !l.includes("grep"));
188
- if (gatewayLines.length > 0) {
189
- const trimmed = gatewayLines[0].trim();
190
- const pid = trimmed.split(/\s+/)[0];
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
- } catch {
196
- rows.push({ name: "gateway", status: withStatusEmoji("unknown"), info: "" });
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 = await getLocalProcesses();
206
+ const rows = getLocalProcesses();
209
207
  printTable(rows);
210
208
  return;
211
209
  }
@@ -59,11 +59,10 @@ async function retireLocal(): Promise<void> {
59
59
  child.on("error", () => resolve());
60
60
  });
61
61
  } catch {}
62
-
63
- // Only delete ~/.vellum in non-desktop mode
64
- rmSync(vellumDir, { recursive: true, force: true });
65
62
  }
66
63
 
64
+ rmSync(vellumDir, { recursive: true, force: true });
65
+
67
66
  console.log("\u2705 Local instance retired.");
68
67
  }
69
68
 
@@ -58,9 +58,6 @@ interface PendingConfirmation {
58
58
  executionTarget?: "sandbox" | "host";
59
59
  allowlistOptions?: AllowlistOption[];
60
60
  scopeOptions?: ScopeOption[];
61
- principalKind?: string;
62
- principalId?: string;
63
- principalVersion?: string;
64
61
  persistentDecisionsAllowed?: boolean;
65
62
  }
66
63
 
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
- const child = spawnChild(
610
- "aws",
611
- [
612
- "ec2",
613
- "terminate-instances",
614
- "--instance-ids",
615
- instanceId,
616
- "--region",
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, mkdtempSync, rmSync, unlinkSync, writeFileSync } from "fs";
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
- const child = spawn(
776
- "gcloud",
777
- [
778
- "compute",
779
- "instances",
780
- "delete",
781
- name,
782
- `--project=${project}`,
783
- `--zone=${zone}`,
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 { GATEWAY_PORT } from "../lib/constants";
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,12 +287,32 @@ 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