@vellumai/cli 0.6.6 → 0.7.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.
Files changed (45) hide show
  1. package/AGENTS.md +8 -2
  2. package/package.json +1 -1
  3. package/src/__tests__/assistant-config.test.ts +1 -7
  4. package/src/__tests__/config-utils.test.ts +159 -0
  5. package/src/__tests__/env-drift.test.ts +10 -32
  6. package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
  7. package/src/__tests__/multi-local.test.ts +0 -5
  8. package/src/__tests__/sleep.test.ts +1 -2
  9. package/src/__tests__/teleport.test.ts +919 -1255
  10. package/src/commands/env.ts +93 -0
  11. package/src/commands/events.ts +2 -0
  12. package/src/commands/exec.ts +40 -8
  13. package/src/commands/hatch.ts +6 -2
  14. package/src/commands/login.ts +89 -6
  15. package/src/commands/ps.ts +104 -20
  16. package/src/commands/sleep.ts +5 -2
  17. package/src/commands/ssh.ts +15 -2
  18. package/src/commands/teleport.ts +447 -583
  19. package/src/commands/terminal.ts +9 -221
  20. package/src/commands/wake.ts +2 -1
  21. package/src/components/DefaultMainScreen.tsx +304 -152
  22. package/src/index.ts +3 -0
  23. package/src/lib/__tests__/docker.test.ts +50 -74
  24. package/src/lib/__tests__/job-polling.test.ts +278 -0
  25. package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
  26. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  27. package/src/lib/assistant-config.ts +12 -8
  28. package/src/lib/client-identity.ts +67 -0
  29. package/src/lib/config-utils.ts +97 -1
  30. package/src/lib/docker.ts +73 -75
  31. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  32. package/src/lib/environments/resolve.ts +89 -7
  33. package/src/lib/environments/seeds.ts +8 -5
  34. package/src/lib/environments/types.ts +10 -0
  35. package/src/lib/hatch-local.ts +15 -120
  36. package/src/lib/health-check.ts +98 -0
  37. package/src/lib/job-polling.ts +195 -0
  38. package/src/lib/local-runtime-client.ts +178 -0
  39. package/src/lib/local.ts +139 -15
  40. package/src/lib/orphan-detection.ts +2 -35
  41. package/src/lib/platform-client.ts +215 -0
  42. package/src/lib/retire-local.ts +6 -2
  43. package/src/lib/terminal-session.ts +457 -0
  44. package/src/shared/provider-env-vars.ts +2 -3
  45. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -11,19 +11,17 @@
11
11
  * vellum terminal list — List tmux sessions
12
12
  */
13
13
 
14
- import {
15
- findAssistantByName,
16
- loadLatestAssistant,
17
- resolveCloud,
18
- } from "../lib/assistant-config.js";
19
- import { getPlatformUrl, readPlatformToken } from "../lib/platform-client.js";
20
14
  import {
21
15
  closeTerminalSession,
22
16
  createTerminalSession,
23
- resizeTerminalSession,
24
17
  sendTerminalInput,
25
18
  subscribeTerminalEvents,
26
19
  } from "../lib/terminal-client.js";
20
+ import {
21
+ interactiveSession,
22
+ resolveManagedAssistant,
23
+ } from "../lib/terminal-session.js";
24
+ import type { ResolvedManagedAssistant } from "../lib/terminal-session.js";
27
25
 
28
26
  // ---------------------------------------------------------------------------
29
27
  // Helpers
@@ -60,223 +58,13 @@ function printHelp(): void {
60
58
  console.log(" vellum terminal --assistant my-assistant");
61
59
  }
62
60
 
63
- interface ResolvedAssistant {
64
- assistantId: string;
65
- token: string;
66
- platformUrl: string;
67
- }
68
-
69
- function resolveAssistant(nameArg?: string): ResolvedAssistant {
70
- const entry = nameArg ? findAssistantByName(nameArg) : loadLatestAssistant();
71
-
72
- if (!entry) {
73
- if (nameArg) {
74
- console.error(`No assistant instance found with name '${nameArg}'.`);
75
- } else {
76
- console.error("No assistant instance found. Run `vellum hatch` first.");
77
- }
78
- process.exit(1);
79
- }
80
-
81
- const cloud = resolveCloud(entry);
82
- if (cloud !== "vellum") {
83
- if (cloud === "local") {
84
- console.error(
85
- "This assistant runs locally on your machine. You can access it directly.",
86
- );
87
- } else if (cloud === "docker") {
88
- console.error(
89
- `Use 'vellum exec -it -- /bin/bash' or 'vellum ssh' for ${cloud} instances.`,
90
- );
91
- } else {
92
- console.error(
93
- `'vellum terminal' is for managed (cloud-hosted) assistants. This assistant uses '${cloud}'.`,
94
- );
95
- }
96
- process.exit(1);
97
- }
98
-
99
- const token = readPlatformToken();
100
- if (!token) {
101
- console.error(
102
- "Not logged in. Run `vellum login` first to authenticate with the platform.",
103
- );
104
- process.exit(1);
105
- }
106
-
107
- return {
108
- assistantId: entry.assistantId,
109
- token,
110
- platformUrl: getPlatformUrl(),
111
- };
112
- }
113
-
114
- // ---------------------------------------------------------------------------
115
- // Interactive session
116
- // ---------------------------------------------------------------------------
117
-
118
- async function interactiveSession(
119
- assistant: ResolvedAssistant,
120
- initialCommand?: string,
121
- ): Promise<void> {
122
- const cols = process.stdout.columns || 80;
123
- const rows = process.stdout.rows || 24;
124
-
125
- console.error(`\x1b[2m🔗 Connecting to ${assistant.assistantId}...\x1b[0m`);
126
-
127
- const { session_id: sessionId } = await createTerminalSession(
128
- assistant.token,
129
- assistant.assistantId,
130
- cols,
131
- rows,
132
- assistant.platformUrl,
133
- );
134
-
135
- // --- TTY raw mode setup ---
136
- const wasRaw = process.stdin.isRaw;
137
- if (process.stdin.isTTY) {
138
- process.stdin.setRawMode(true);
139
- }
140
- process.stdin.resume();
141
- process.stdin.setEncoding("utf-8");
142
-
143
- // Abort controller for the SSE stream
144
- const abortController = new AbortController();
145
- let exiting = false;
146
-
147
- // --- Cleanup function (idempotent) ---
148
- async function cleanup(): Promise<void> {
149
- if (exiting) return;
150
- exiting = true;
151
-
152
- // Restore tty
153
- if (process.stdin.isTTY) {
154
- process.stdin.setRawMode(wasRaw ?? false);
155
- }
156
- process.stdin.pause();
157
-
158
- // Abort SSE stream
159
- abortController.abort();
160
-
161
- // Close remote session (best-effort)
162
- try {
163
- await closeTerminalSession(
164
- assistant.token,
165
- assistant.assistantId,
166
- sessionId,
167
- assistant.platformUrl,
168
- );
169
- } catch {
170
- // Best-effort cleanup
171
- }
172
- }
173
-
174
- // --- Signal handlers ---
175
- const onSigInt = () => {
176
- cleanup().then(() => process.exit(0));
177
- };
178
- const onSigTerm = () => {
179
- cleanup().then(() => process.exit(0));
180
- };
181
- process.on("SIGINT", onSigInt);
182
- process.on("SIGTERM", onSigTerm);
183
-
184
- // --- SIGWINCH (terminal resize) ---
185
- const onResize = () => {
186
- const newCols = process.stdout.columns || 80;
187
- const newRows = process.stdout.rows || 24;
188
- resizeTerminalSession(
189
- assistant.token,
190
- assistant.assistantId,
191
- sessionId,
192
- newCols,
193
- newRows,
194
- assistant.platformUrl,
195
- ).catch(() => {
196
- // Resize failures are non-fatal
197
- });
198
- };
199
- process.stdout.on("resize", onResize);
200
-
201
- // --- Input: stdin → remote ---
202
- let inputBuffer = "";
203
- let inputTimer: ReturnType<typeof setTimeout> | null = null;
204
- const INPUT_DEBOUNCE_MS = 30;
205
-
206
- function flushInput(): void {
207
- if (inputBuffer.length === 0) return;
208
- const data = inputBuffer;
209
- inputBuffer = "";
210
- sendTerminalInput(
211
- assistant.token,
212
- assistant.assistantId,
213
- sessionId,
214
- data,
215
- assistant.platformUrl,
216
- ).catch((err) => {
217
- if (!exiting) {
218
- console.error(`\r\nInput error: ${err.message}\r\n`);
219
- }
220
- });
221
- }
222
-
223
- process.stdin.on("data", (chunk: string) => {
224
- if (exiting) return;
225
- inputBuffer += chunk;
226
- if (inputTimer) clearTimeout(inputTimer);
227
- inputTimer = setTimeout(flushInput, INPUT_DEBOUNCE_MS);
228
- });
229
-
230
- // --- Send initial command (for `attach` subcommand) ---
231
- if (initialCommand) {
232
- // Brief delay to let the shell initialize
233
- await new Promise((resolve) => setTimeout(resolve, 300));
234
- await sendTerminalInput(
235
- assistant.token,
236
- assistant.assistantId,
237
- sessionId,
238
- initialCommand + "\r",
239
- assistant.platformUrl,
240
- );
241
- }
242
-
243
- // --- Output: remote SSE → stdout ---
244
- try {
245
- for await (const event of subscribeTerminalEvents(
246
- assistant.token,
247
- assistant.assistantId,
248
- sessionId,
249
- assistant.platformUrl,
250
- abortController.signal,
251
- )) {
252
- if (exiting) break;
253
- // Decode base64 output and write raw bytes to stdout
254
- const bytes = Buffer.from(event.data, "base64");
255
- process.stdout.write(bytes);
256
- }
257
- } catch (err) {
258
- if (!exiting) {
259
- const msg = err instanceof Error ? err.message : String(err);
260
- // AbortError is expected on cleanup
261
- if (!msg.includes("abort")) {
262
- console.error(`\r\nConnection lost: ${msg}\r\n`);
263
- }
264
- }
265
- } finally {
266
- await cleanup();
267
-
268
- // Remove listeners
269
- process.off("SIGINT", onSigInt);
270
- process.off("SIGTERM", onSigTerm);
271
- process.stdout.off("resize", onResize);
272
- }
273
- }
274
-
275
61
  // ---------------------------------------------------------------------------
276
62
  // List tmux sessions
277
63
  // ---------------------------------------------------------------------------
278
64
 
279
- async function listTmuxSessions(assistant: ResolvedAssistant): Promise<void> {
65
+ async function listTmuxSessions(
66
+ assistant: ResolvedManagedAssistant,
67
+ ): Promise<void> {
280
68
  const cols = 120;
281
69
  const rows = 24;
282
70
 
@@ -411,7 +199,7 @@ export async function terminal(): Promise<void> {
411
199
  }
412
200
  }
413
201
 
414
- const assistant = resolveAssistant(assistantName);
202
+ const assistant = resolveManagedAssistant(assistantName);
415
203
 
416
204
  if (subcommand === "list") {
417
205
  await listTmuxSessions(assistant);
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
3
 
4
4
  import {
5
+ getDaemonPidPath,
5
6
  resolveTargetAssistant,
6
7
  saveAssistantEntry,
7
8
  } from "../lib/assistant-config.js";
@@ -82,7 +83,7 @@ export async function wake(): Promise<void> {
82
83
  }
83
84
  const resources = entry.resources;
84
85
 
85
- const pidFile = resources.pidFile;
86
+ const pidFile = getDaemonPidPath(resources);
86
87
 
87
88
  // Check if daemon is already running
88
89
  let daemonRunning = false;