@vellumai/cli 0.8.3 → 0.8.5

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.
@@ -4,9 +4,12 @@ import path from "node:path";
4
4
 
5
5
  import {
6
6
  findAssistantByName,
7
+ formatAssistantLookupError,
7
8
  getActiveAssistant,
9
+ lookupAssistantByIdentifier,
8
10
  resolveAssistant,
9
11
  saveAssistantEntry,
12
+ type AssistantEntry,
10
13
  } from "../lib/assistant-config";
11
14
  import {
12
15
  DAEMON_INTERNAL_ASSISTANT_ID,
@@ -20,6 +23,7 @@ import {
20
23
  WEB_INTERFACE_ID,
21
24
  getClientRegistrationHeaders,
22
25
  } from "../lib/client-identity";
26
+ import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
23
27
  import {
24
28
  fetchOrganizationId,
25
29
  fetchPlatformAssistants,
@@ -51,13 +55,9 @@ interface ParsedArgs {
51
55
  bearerToken?: string;
52
56
  /** Interface identifier sent as X-Vellum-Interface-Id on all requests. */
53
57
  interfaceId: SupportedInterface;
54
- project?: string;
55
- zone?: string;
56
58
  }
57
59
 
58
- function readAssistantName(
59
- entry: ReturnType<typeof findAssistantByName>,
60
- ): string | undefined {
60
+ function readAssistantName(entry: AssistantEntry | null): string | undefined {
61
61
  const rawName = entry?.name ?? entry?.assistantName;
62
62
  return typeof rawName === "string" && rawName.trim()
63
63
  ? rawName.trim()
@@ -67,7 +67,14 @@ function readAssistantName(
67
67
  function parseArgs(): ParsedArgs {
68
68
  const args = process.argv.slice(3);
69
69
 
70
- let positionalName: string | undefined;
70
+ const positionalName = parseAssistantTargetArg(args, [
71
+ "--url",
72
+ "-u",
73
+ "--assistant-id",
74
+ "-a",
75
+ "--interface",
76
+ "-i",
77
+ ]);
71
78
  const flagArgs: string[] = [];
72
79
  for (let i = 0; i < args.length; i++) {
73
80
  const arg = args[i];
@@ -84,29 +91,29 @@ function parseArgs(): ParsedArgs {
84
91
  args[i + 1]
85
92
  ) {
86
93
  flagArgs.push(arg, args[++i]);
87
- } else if (!arg.startsWith("-") && positionalName === undefined) {
88
- positionalName = arg;
89
94
  }
90
95
  }
91
96
 
92
- let entry: ReturnType<typeof findAssistantByName> = null;
97
+ let entry: AssistantEntry | null = null;
93
98
  if (positionalName) {
94
- entry = findAssistantByName(positionalName);
95
- if (!entry) {
96
- console.error(
97
- `No assistant instance found with name '${positionalName}'.`,
98
- );
99
+ const result = lookupAssistantByIdentifier(positionalName);
100
+ if (result.status !== "found") {
101
+ console.error(formatAssistantLookupError(positionalName, result));
99
102
  process.exit(1);
100
103
  }
104
+ entry = result.entry;
101
105
  } else {
102
106
  const hasExplicitUrl =
103
107
  flagArgs.includes("--url") || flagArgs.includes("-u");
104
108
  const active = getActiveAssistant();
105
109
  if (active) {
106
- entry = findAssistantByName(active);
110
+ const result = lookupAssistantByIdentifier(active);
111
+ if (result.status === "found") {
112
+ entry = result.entry;
113
+ }
107
114
  if (!entry && !hasExplicitUrl) {
108
115
  console.error(
109
- `Active assistant '${active}' not found in lockfile. Set an active assistant with 'vellum use <name>'.`,
116
+ `Active assistant '${active}' not found in lockfile. Set an active assistant with 'vellum use <name-or-id>'.`,
110
117
  );
111
118
  process.exit(1);
112
119
  }
@@ -116,7 +123,7 @@ function parseArgs(): ParsedArgs {
116
123
  entry = resolveAssistant();
117
124
  } else if (!entry) {
118
125
  console.error(
119
- "No active assistant set. Set one with 'vellum use <name>' or specify a name: 'vellum client <name>'.",
126
+ "No active assistant set. Set one with 'vellum use <name-or-id>' or specify one: 'vellum client <name-or-id>'.",
120
127
  );
121
128
  process.exit(1);
122
129
  }
@@ -169,8 +176,6 @@ function parseArgs(): ParsedArgs {
169
176
  platformToken,
170
177
  bearerToken,
171
178
  interfaceId,
172
- project: entry?.project,
173
- zone: entry?.zone,
174
179
  };
175
180
  }
176
181
 
@@ -217,10 +222,10 @@ function printUsage(): void {
217
222
  console.log(`${ANSI.bold}vellum client${ANSI.reset} - Connect to a hatched assistant
218
223
 
219
224
  ${ANSI.bold}USAGE:${ANSI.reset}
220
- vellum client [name] [options]
225
+ vellum client [name-or-id] [options]
221
226
 
222
227
  ${ANSI.bold}ARGUMENTS:${ANSI.reset}
223
- [name] Instance name (default: active)
228
+ [name-or-id] Assistant display name or ID (default: active)
224
229
 
225
230
  ${ANSI.bold}OPTIONS:${ANSI.reset}
226
231
  -u, --url <url> Runtime URL
@@ -343,8 +348,6 @@ export async function client(): Promise<void> {
343
348
  platformToken,
344
349
  bearerToken,
345
350
  interfaceId,
346
- project,
347
- zone,
348
351
  } = parseArgs();
349
352
 
350
353
  if (interfaceId === WEB_INTERFACE_ID) {
@@ -404,6 +407,6 @@ export async function client(): Promise<void> {
404
407
  console.log(`${ANSI.dim}Disconnected.${ANSI.reset}`);
405
408
  process.exit(0);
406
409
  },
407
- { auth, project, zone, assistantName },
410
+ { auth, assistantName },
408
411
  );
409
412
  }
@@ -2,11 +2,16 @@ import { join } from "path";
2
2
 
3
3
  import {
4
4
  findAssistantByName,
5
+ formatAssistantLookupError,
6
+ formatAssistantReference,
5
7
  getActiveAssistant,
8
+ getAssistantDisplayName,
6
9
  getDaemonPidPath,
7
10
  loadAllAssistants,
11
+ lookupAssistantByIdentifier,
8
12
  type AssistantEntry,
9
13
  } from "../lib/assistant-config";
14
+ import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
10
15
  import { resolveEnvironmentSource } from "../lib/environments/resolve";
11
16
  import { loadGuardianToken } from "../lib/guardian-token";
12
17
  import {
@@ -52,8 +57,10 @@ function pad(s: string, w: number): string {
52
57
  return s + " ".repeat(Math.max(0, w - s.length));
53
58
  }
54
59
 
55
- function computeColWidths(rows: TableRow[]): ColWidths {
56
- const headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" };
60
+ function computeColWidths(
61
+ rows: TableRow[],
62
+ headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" },
63
+ ): ColWidths {
57
64
  const all = [headers, ...rows];
58
65
  return {
59
66
  name: Math.max(...all.map((r) => r.name.length)),
@@ -66,9 +73,11 @@ function formatRow(r: TableRow, colWidths: ColWidths): string {
66
73
  return ` ${pad(r.name, colWidths.name)} ${pad(r.status, colWidths.status)} ${r.info}`;
67
74
  }
68
75
 
69
- function printTable(rows: TableRow[]): void {
70
- const colWidths = computeColWidths(rows);
71
- const headers: TableRow = { name: "PROCESS", status: "STATUS", info: "INFO" };
76
+ function printTable(
77
+ rows: TableRow[],
78
+ headers: TableRow = { name: "PROCESS", status: "STATUS", info: "INFO" },
79
+ ): void {
80
+ const colWidths = computeColWidths(rows, headers);
72
81
  console.log(formatRow(headers, colWidths));
73
82
  const sep = ` ${"-".repeat(colWidths.name)} ${"-".repeat(colWidths.status)} ${"-".repeat(colWidths.info)}`;
74
83
  console.log(sep);
@@ -377,7 +386,7 @@ async function getDockerProcesses(entry: AssistantEntry): Promise<TableRow[]> {
377
386
  async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
378
387
  const cloud = resolveCloud(entry);
379
388
 
380
- console.log(`Processes for ${entry.assistantId} (${cloud}):\n`);
389
+ console.log(`Processes for ${formatAssistantReference(entry)} (${cloud}):\n`);
381
390
 
382
391
  if (cloud === "local") {
383
392
  const rows = await getLocalProcesses(entry);
@@ -473,6 +482,75 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
473
482
 
474
483
  // ── List all assistants (no arg) ────────────────────────────────
475
484
 
485
+ type AssistantHealth = {
486
+ status: string;
487
+ detail: string | null;
488
+ version?: string;
489
+ };
490
+
491
+ async function getAssistantListHealth(
492
+ entry: AssistantEntry,
493
+ ): Promise<AssistantHealth> {
494
+ const resources = entry.resources;
495
+ if (entry.cloud === "local" && resources) {
496
+ // TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
497
+ // fetching daemon PIDs via the health API (Gateway Security Migration).
498
+ const pid = readPidFile(getDaemonPidPath(resources));
499
+ const alive = pid !== null && isProcessAlive(pid);
500
+ if (!alive) {
501
+ return { status: "sleeping", detail: null };
502
+ }
503
+ const token = loadGuardianToken(entry.assistantId)?.accessToken;
504
+ return checkHealth(entry.localUrl ?? entry.runtimeUrl, token);
505
+ }
506
+
507
+ if (entry.cloud === "docker") {
508
+ const res = dockerResourceNames(entry.assistantId);
509
+ const state = await getDockerContainerState(res.assistantContainer);
510
+ if (!state || state !== "running") {
511
+ return { status: "sleeping", detail: null };
512
+ }
513
+ const token = loadGuardianToken(entry.assistantId)?.accessToken;
514
+ return checkHealth(entry.localUrl ?? entry.runtimeUrl, token);
515
+ }
516
+
517
+ if (entry.cloud === "apple-container") {
518
+ // Apple containers are managed by the macOS app. Probe the gateway
519
+ // (runtimeUrl is always written to the lockfile during hatch).
520
+ const token = loadGuardianToken(entry.assistantId)?.accessToken;
521
+ return entry.runtimeUrl
522
+ ? checkHealth(entry.runtimeUrl, token)
523
+ : { status: "unknown", detail: "no runtime URL" };
524
+ }
525
+
526
+ if (entry.cloud === "vellum") {
527
+ return checkManagedHealth(entry.runtimeUrl, entry.assistantId);
528
+ }
529
+
530
+ const token = loadGuardianToken(entry.assistantId)?.accessToken;
531
+ return checkHealth(entry.localUrl ?? entry.runtimeUrl, token);
532
+ }
533
+
534
+ function formatAssistantListRow(
535
+ entry: AssistantEntry,
536
+ activeAssistantId: string | null,
537
+ health: AssistantHealth,
538
+ ): TableRow {
539
+ const infoParts: string[] = [];
540
+ infoParts.push(`id: ${entry.assistantId}`);
541
+ if (entry.runtimeUrl) infoParts.push(entry.runtimeUrl);
542
+ if (entry.cloud) infoParts.push(`cloud: ${entry.cloud}`);
543
+ if (entry.species) infoParts.push(`species: ${entry.species}`);
544
+ if (health.detail) infoParts.push(health.detail);
545
+
546
+ const prefix = entry.assistantId === activeAssistantId ? "* " : " ";
547
+ return {
548
+ name: prefix + getAssistantDisplayName(entry),
549
+ status: withStatusEmoji(health.status),
550
+ info: infoParts.join(" | "),
551
+ };
552
+ }
553
+
476
554
  export async function listAllAssistants(verbose: boolean): Promise<void> {
477
555
  const { name: envName, source: envSource } = resolveEnvironmentSource();
478
556
  const sourceLabels: Record<typeof envSource, string> = {
@@ -519,6 +597,9 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
519
597
 
520
598
  const assistants = loadAllAssistants();
521
599
  const activeId = getActiveAssistant();
600
+ const activeAssistantId = activeId
601
+ ? (findAssistantByName(activeId)?.assistantId ?? activeId)
602
+ : null;
522
603
 
523
604
  if (assistants.length === 0) {
524
605
  console.log("No assistants found.");
@@ -540,97 +621,17 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
540
621
  return;
541
622
  }
542
623
 
543
- const rows: TableRow[] = assistants.map((a) => {
544
- const infoParts: string[] = [];
545
- if (a.runtimeUrl) infoParts.push(a.runtimeUrl);
546
- if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
547
- if (a.species) infoParts.push(`species: ${a.species}`);
548
- const prefix = a.assistantId === activeId ? "* " : " ";
549
-
550
- return {
551
- name: prefix + a.assistantId,
552
- status: withStatusEmoji("checking..."),
553
- info: infoParts.join(" | "),
554
- };
555
- });
556
-
557
- const colWidths = computeColWidths(rows);
558
-
559
- const headers: TableRow = { name: "NAME", status: "STATUS", info: "INFO" };
560
- console.log(formatRow(headers, colWidths));
561
- const sep = ` ${"-".repeat(colWidths.name)} ${"-".repeat(colWidths.status)} ${"-".repeat(colWidths.info)}`;
562
- console.log(sep);
563
- for (const row of rows) {
564
- console.log(formatRow(row, colWidths));
565
- }
566
-
567
- const totalDataRows = rows.length;
568
-
569
- await Promise.all(
570
- assistants.map(async (a, rowIndex) => {
571
- // For local assistants, check if the daemon process is alive before
572
- // hitting the health endpoint. If the PID file is missing or the
573
- // process isn't running, the assistant is sleeping — skip the
574
- // network health check to avoid a misleading "unreachable" status.
575
- let health: { status: string; detail: string | null; version?: string };
576
- const resources = a.resources;
577
- if (a.cloud === "local" && resources) {
578
- // TODO(ATL-306): Remove readPidFile/getDaemonPidPath in favor of
579
- // fetching daemon PIDs via the health API (Gateway Security Migration).
580
- const pid = readPidFile(getDaemonPidPath(resources));
581
- const alive = pid !== null && isProcessAlive(pid);
582
- if (!alive) {
583
- health = { status: "sleeping", detail: null };
584
- } else {
585
- const token = loadGuardianToken(a.assistantId)?.accessToken;
586
- health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
587
- }
588
- } else if (a.cloud === "docker") {
589
- const res = dockerResourceNames(a.assistantId);
590
- const state = await getDockerContainerState(res.assistantContainer);
591
- if (!state || state !== "running") {
592
- health = { status: "sleeping", detail: null };
593
- } else {
594
- const token = loadGuardianToken(a.assistantId)?.accessToken;
595
- health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
596
- }
597
- } else if (a.cloud === "apple-container") {
598
- // Apple containers are managed by the macOS app. Probe the gateway
599
- // (runtimeUrl is always written to the lockfile during hatch).
600
- const token = loadGuardianToken(a.assistantId)?.accessToken;
601
- health = a.runtimeUrl
602
- ? await checkHealth(a.runtimeUrl, token)
603
- : { status: "unknown" as const, detail: "no runtime URL" };
604
- } else if (a.cloud === "vellum") {
605
- health = await checkManagedHealth(a.runtimeUrl, a.assistantId);
606
- } else {
607
- const token = loadGuardianToken(a.assistantId)?.accessToken;
608
- health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
609
- }
610
-
611
- const infoParts: string[] = [];
612
- if (a.runtimeUrl) infoParts.push(a.runtimeUrl);
613
- if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
614
- if (a.species) infoParts.push(`species: ${a.species}`);
615
- if (health.detail) infoParts.push(health.detail);
616
-
617
- const prefix = a.assistantId === activeId ? "* " : " ";
618
- const updatedRow: TableRow = {
619
- name: prefix + a.assistantId,
620
- status: withStatusEmoji(health.status),
621
- info: infoParts.join(" | "),
622
- };
623
-
624
- const linesUp = totalDataRows - rowIndex;
625
- process.stdout.write(
626
- `\x1b[${linesUp}A` +
627
- `\r\x1b[K` +
628
- formatRow(updatedRow, colWidths) +
629
- `\n` +
630
- (linesUp > 1 ? `\x1b[${linesUp - 1}B` : ""),
631
- );
632
- }),
624
+ const rows = await Promise.all(
625
+ assistants.map(async (entry) =>
626
+ formatAssistantListRow(
627
+ entry,
628
+ activeAssistantId,
629
+ await getAssistantListHealth(entry),
630
+ ),
631
+ ),
633
632
  );
633
+
634
+ printTable(rows, { name: "NAME", status: "STATUS", info: "INFO" });
634
635
  }
635
636
 
636
637
  // ── Entry point ─────────────────────────────────────────────────
@@ -638,14 +639,16 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
638
639
  export async function ps(): Promise<void> {
639
640
  const args = process.argv.slice(3);
640
641
  if (args.includes("--help") || args.includes("-h")) {
641
- console.log("Usage: vellum ps [<name>] [--verbose]");
642
+ console.log("Usage: vellum ps [<name-or-id>] [--verbose]");
642
643
  console.log("");
643
644
  console.log(
644
645
  "List all assistants, or show processes for a specific assistant.",
645
646
  );
646
647
  console.log("");
647
648
  console.log("Arguments:");
648
- console.log(" <name> Show processes for the named assistant");
649
+ console.log(
650
+ " <name-or-id> Show processes for the assistant display name or ID",
651
+ );
649
652
  console.log("");
650
653
  console.log("Options:");
651
654
  console.log(
@@ -655,19 +658,18 @@ export async function ps(): Promise<void> {
655
658
  }
656
659
 
657
660
  const verbose = args.includes("--verbose");
658
- const positional = args.filter((a) => !a.startsWith("--"));
659
- const assistantId = positional[0];
661
+ const assistantIdentifier = parseAssistantTargetArg(args);
660
662
 
661
- if (!assistantId) {
663
+ if (!assistantIdentifier) {
662
664
  await listAllAssistants(verbose);
663
665
  return;
664
666
  }
665
667
 
666
- const entry = findAssistantByName(assistantId);
667
- if (!entry) {
668
- console.error(`No assistant found with name '${assistantId}'.`);
668
+ const result = lookupAssistantByIdentifier(assistantIdentifier);
669
+ if (result.status !== "found") {
670
+ console.error(formatAssistantLookupError(assistantIdentifier, result));
669
671
  process.exit(1);
670
672
  }
671
673
 
672
- await showAssistantProcesses(entry);
674
+ await showAssistantProcesses(result.entry);
673
675
  }
@@ -2,30 +2,34 @@ import { existsSync, unlinkSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
4
  import {
5
- findAssistantByName,
5
+ formatAssistantLookupError,
6
+ formatAssistantReference,
7
+ getAssistantDisplayName,
6
8
  loadAllAssistants,
9
+ lookupAssistantByIdentifier,
7
10
  removeAssistantEntry,
8
- } from "../lib/assistant-config";
9
- import type { AssistantEntry } from "../lib/assistant-config";
10
- import { getConfigDir } from "../lib/environments/paths";
11
- import { getCurrentEnvironment } from "../lib/environments/resolve";
11
+ } from "../lib/assistant-config.js";
12
+ import type { AssistantEntry } from "../lib/assistant-config.js";
13
+ import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
14
+ import { getConfigDir } from "../lib/environments/paths.js";
15
+ import { getCurrentEnvironment } from "../lib/environments/resolve.js";
12
16
  import {
13
17
  authHeaders,
14
18
  getPlatformUrl,
15
19
  readPlatformToken,
16
- } from "../lib/platform-client";
17
- import { retireInstance as retireAwsInstance } from "../lib/aws";
18
- import { retireDocker } from "../lib/docker";
19
- import { retireInstance as retireGcpInstance } from "../lib/gcp";
20
- import { retireLocal } from "../lib/retire-local";
21
- import { retireAppleContainer } from "../lib/retire-apple-container";
22
- import { exec } from "../lib/step-runner";
20
+ } from "../lib/platform-client.js";
21
+ import { retireInstance as retireAwsInstance } from "../lib/aws.js";
22
+ import { retireDocker } from "../lib/docker.js";
23
+ import { retireInstance as retireGcpInstance } from "../lib/gcp.js";
24
+ import { retireLocal } from "../lib/retire-local.js";
25
+ import { retireAppleContainer } from "../lib/retire-apple-container.js";
26
+ import { exec } from "../lib/step-runner.js";
23
27
  import {
24
28
  openLogFile,
25
29
  closeLogFile,
26
30
  resetLogFile,
27
31
  writeToLogFile,
28
- } from "../lib/xdg-log";
32
+ } from "../lib/xdg-log.js";
29
33
 
30
34
  function resolveCloud(entry: AssistantEntry): string {
31
35
  if (entry.cloud) {
@@ -51,6 +55,12 @@ function extractHostFromUrl(url: string): string {
51
55
 
52
56
  export { retireLocal };
53
57
 
58
+ interface RetireArgs {
59
+ name?: string;
60
+ source?: string;
61
+ yes: boolean;
62
+ }
63
+
54
64
  async function retireCustom(entry: AssistantEntry): Promise<void> {
55
65
  const host = extractHostFromUrl(entry.runtimeUrl);
56
66
  const sshUser = entry.sshUser ?? "root";
@@ -129,14 +139,82 @@ async function retireVellum(
129
139
  }
130
140
  }
131
141
 
132
- function parseSource(): string | undefined {
133
- const args = process.argv.slice(4);
142
+ function parseRetireArgs(args: string[]): RetireArgs {
143
+ let source: string | undefined;
134
144
  for (let i = 0; i < args.length; i++) {
135
145
  if (args[i] === "--source" && args[i + 1]) {
136
- return args[i + 1];
146
+ source = args[i + 1];
147
+ i++;
137
148
  }
138
149
  }
139
- return undefined;
150
+
151
+ return {
152
+ name: parseAssistantTargetArg(args, ["--source"]),
153
+ source,
154
+ yes: args.includes("--yes"),
155
+ };
156
+ }
157
+
158
+ function formatRuntimeUrl(entry: AssistantEntry): string {
159
+ return entry.localUrl ?? entry.runtimeUrl;
160
+ }
161
+
162
+ function printRetireTarget(entry: AssistantEntry, cloud: string): void {
163
+ const displayName = getAssistantDisplayName(entry);
164
+
165
+ console.log("Assistant to retire:");
166
+ if (displayName !== entry.assistantId) {
167
+ console.log(` Name: ${displayName}`);
168
+ }
169
+ console.log(` ID: ${entry.assistantId}`);
170
+ console.log(` Cloud: ${cloud}`);
171
+ console.log(` Runtime: ${formatRuntimeUrl(entry)}`);
172
+ console.log("");
173
+ }
174
+
175
+ function canPromptForRetireConfirmation(): boolean {
176
+ return (
177
+ process.stdin.isTTY === true &&
178
+ process.stdout.isTTY === true &&
179
+ typeof process.stdin.setRawMode === "function"
180
+ );
181
+ }
182
+
183
+ async function confirmRetireInteractive(): Promise<boolean> {
184
+ const stdin = process.stdin;
185
+ const stdout = process.stdout;
186
+ const wasRaw = stdin.isRaw === true;
187
+ const wasPaused = stdin.isPaused();
188
+
189
+ stdout.write("Press Enter to retire, or Esc/q to cancel: ");
190
+ stdin.setRawMode(true);
191
+ stdin.resume();
192
+
193
+ return await new Promise<boolean>((resolve) => {
194
+ const cleanup = () => {
195
+ stdin.off("data", onData);
196
+ stdin.setRawMode(wasRaw);
197
+ if (wasPaused) {
198
+ stdin.pause();
199
+ }
200
+ stdout.write("\n");
201
+ };
202
+
203
+ const onData = (chunk: Buffer) => {
204
+ const byte = chunk[0];
205
+ if (byte === 13 || byte === 10) {
206
+ cleanup();
207
+ resolve(true);
208
+ return;
209
+ }
210
+ if (byte === 27 || byte === 3 || byte === 113 || byte === 81) {
211
+ cleanup();
212
+ resolve(false);
213
+ }
214
+ };
215
+
216
+ stdin.on("data", onData);
217
+ });
140
218
  }
141
219
 
142
220
  /** Patch console methods to also append output to the given log file descriptor. */
@@ -188,38 +266,70 @@ export async function retire(): Promise<void> {
188
266
  async function retireInner(): Promise<void> {
189
267
  const args = process.argv.slice(3);
190
268
  if (args.includes("--help") || args.includes("-h")) {
191
- console.log("Usage: vellum retire <name> [--source <source>]");
269
+ console.log(
270
+ "Usage: vellum retire <name-or-id> [--source <source>] [--yes]",
271
+ );
192
272
  console.log("");
193
273
  console.log("Delete an assistant instance and archive its data.");
274
+ console.log(
275
+ "By default, retire prints the assistant name, ID, cloud, and runtime before asking for confirmation.",
276
+ );
194
277
  console.log("");
195
278
  console.log("Arguments:");
196
- console.log(" <name> Name of the assistant to retire");
279
+ console.log(
280
+ " <name-or-id> Assistant display name or ID to retire",
281
+ );
197
282
  console.log("");
198
283
  console.log("Options:");
199
284
  console.log(" --source <source> Source identifier for the retirement");
285
+ console.log(
286
+ " --yes Skip the interactive confirmation prompt",
287
+ );
200
288
  process.exit(0);
201
289
  }
202
290
 
203
- const name = process.argv[3];
291
+ const parsed = parseRetireArgs(args);
292
+ const name = parsed.name;
204
293
 
205
294
  if (!name) {
206
- console.error("Error: Instance name is required.");
207
- console.error("Usage: vellum retire <name> [--source <source>]");
295
+ console.error("Error: Assistant name or ID is required.");
296
+ console.error(
297
+ "Usage: vellum retire <name-or-id> [--source <source>] [--yes]",
298
+ );
208
299
  process.exit(1);
209
300
  }
210
301
 
211
- const entry = findAssistantByName(name);
212
- if (!entry) {
213
- console.error(`No assistant found with name '${name}'.`);
302
+ const lookup = lookupAssistantByIdentifier(name);
303
+ if (lookup.status !== "found") {
304
+ console.error(formatAssistantLookupError(name, lookup));
214
305
  console.error("Run 'vellum hatch' first, or check the instance name.");
215
306
  process.exit(1);
216
307
  }
217
308
 
218
- const source = parseSource();
309
+ const entry = lookup.entry;
310
+ const assistantId = entry.assistantId;
311
+ const source = parsed.source;
219
312
  const cloud = resolveCloud(entry);
313
+ printRetireTarget(entry, cloud);
314
+
315
+ if (!parsed.yes) {
316
+ if (!canPromptForRetireConfirmation()) {
317
+ console.error(
318
+ "Error: Refusing to retire without confirmation in a non-interactive terminal.",
319
+ );
320
+ console.error("Re-run with --yes to confirm from automation.");
321
+ process.exit(1);
322
+ }
323
+
324
+ const confirmed = await confirmRetireInteractive();
325
+ if (!confirmed) {
326
+ console.log("Retire cancelled.");
327
+ process.exit(1);
328
+ }
329
+ }
220
330
 
221
331
  if (cloud === "apple-container") {
222
- await retireAppleContainer(name, entry);
332
+ await retireAppleContainer(assistantId, entry);
223
333
  } else if (cloud === "gcp") {
224
334
  const project = entry.project;
225
335
  const zone = entry.zone;
@@ -229,29 +339,29 @@ async function retireInner(): Promise<void> {
229
339
  );
230
340
  process.exit(1);
231
341
  }
232
- await retireGcpInstance(name, project, zone, source);
342
+ await retireGcpInstance(assistantId, project, zone, source);
233
343
  } else if (cloud === "aws") {
234
344
  const region = entry.region;
235
345
  if (!region) {
236
346
  console.error("Error: AWS region not found in assistant config.");
237
347
  process.exit(1);
238
348
  }
239
- await retireAwsInstance(name, region, source);
349
+ await retireAwsInstance(assistantId, region, source);
240
350
  } else if (cloud === "docker") {
241
- await retireDocker(name);
351
+ await retireDocker(assistantId);
242
352
  } else if (cloud === "local") {
243
- await retireLocal(name, entry);
353
+ await retireLocal(assistantId, entry);
244
354
  } else if (cloud === "custom") {
245
355
  await retireCustom(entry);
246
356
  } else if (cloud === "vellum") {
247
- await retireVellum(entry.assistantId, entry.runtimeUrl);
357
+ await retireVellum(assistantId, entry.runtimeUrl);
248
358
  } else {
249
359
  console.error(`Error: Unknown cloud type '${cloud}'.`);
250
360
  process.exit(1);
251
361
  }
252
362
 
253
- removeAssistantEntry(name);
254
- console.log(`Removed ${name} from config.`);
363
+ removeAssistantEntry(assistantId);
364
+ console.log(`Removed ${formatAssistantReference(entry)} from config.`);
255
365
 
256
366
  // When no assistants remain, remove the dock-display-name sentinel so
257
367
  // the next build.sh run falls back to "Vellum" instead of using the