@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.
- package/AGENTS.md +8 -2
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/config-utils.test.ts +159 -0
- package/src/__tests__/env-drift.test.ts +10 -32
- package/src/__tests__/llm-provider-env-var-parity.test.ts +1 -21
- package/src/__tests__/multi-local.test.ts +0 -5
- package/src/__tests__/sleep.test.ts +1 -2
- package/src/__tests__/teleport.test.ts +919 -1255
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +40 -8
- package/src/commands/hatch.ts +6 -2
- package/src/commands/login.ts +89 -6
- package/src/commands/ps.ts +104 -20
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +15 -2
- package/src/commands/teleport.ts +447 -583
- package/src/commands/terminal.ts +9 -221
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +304 -152
- package/src/index.ts +3 -0
- package/src/lib/__tests__/docker.test.ts +50 -74
- package/src/lib/__tests__/job-polling.test.ts +278 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/assistant-config.ts +12 -8
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/config-utils.ts +97 -1
- package/src/lib/docker.ts +73 -75
- package/src/lib/environments/__tests__/paths.test.ts +2 -0
- package/src/lib/environments/resolve.ts +89 -7
- package/src/lib/environments/seeds.ts +8 -5
- package/src/lib/environments/types.ts +10 -0
- package/src/lib/hatch-local.ts +15 -120
- package/src/lib/health-check.ts +98 -0
- package/src/lib/job-polling.ts +195 -0
- package/src/lib/local-runtime-client.ts +178 -0
- package/src/lib/local.ts +139 -15
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +215 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/terminal-session.ts +457 -0
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
package/src/commands/terminal.ts
CHANGED
|
@@ -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(
|
|
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 =
|
|
202
|
+
const assistant = resolveManagedAssistant(assistantName);
|
|
415
203
|
|
|
416
204
|
if (subcommand === "list") {
|
|
417
205
|
await listTmuxSessions(assistant);
|
package/src/commands/wake.ts
CHANGED
|
@@ -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
|
|
86
|
+
const pidFile = getDaemonPidPath(resources);
|
|
86
87
|
|
|
87
88
|
// Check if daemon is already running
|
|
88
89
|
let daemonRunning = false;
|