@vellumai/cli 0.6.5 → 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/retire.ts +23 -0
- 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 +225 -0
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +304 -152
- package/src/index.ts +6 -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-client.ts +177 -0
- 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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vellum terminal` — Interactive shell into a managed assistant container.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the local tty to a platform terminal session (K8s exec) so the
|
|
5
|
+
* user can interact with their assistant's sandbox from iTerm2 or any
|
|
6
|
+
* local terminal emulator.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* vellum terminal — Interactive shell
|
|
10
|
+
* vellum terminal attach <name> — Attach to a tmux session
|
|
11
|
+
* vellum terminal list — List tmux sessions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
closeTerminalSession,
|
|
16
|
+
createTerminalSession,
|
|
17
|
+
sendTerminalInput,
|
|
18
|
+
subscribeTerminalEvents,
|
|
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";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function printHelp(): void {
|
|
31
|
+
console.log("Usage: vellum terminal [subcommand] [options]");
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log(
|
|
34
|
+
"Open an interactive terminal session into a managed assistant container.",
|
|
35
|
+
);
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log("Subcommands:");
|
|
38
|
+
console.log(" (none) Interactive shell");
|
|
39
|
+
console.log(
|
|
40
|
+
" attach <name> Attach to a tmux session inside the container",
|
|
41
|
+
);
|
|
42
|
+
console.log(
|
|
43
|
+
" list List tmux sessions running inside the container",
|
|
44
|
+
);
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log("Options:");
|
|
47
|
+
console.log(
|
|
48
|
+
" <name> Name of the assistant (defaults to active)",
|
|
49
|
+
);
|
|
50
|
+
console.log(
|
|
51
|
+
" --assistant <name> Explicit assistant name (alternative to positional)",
|
|
52
|
+
);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log("Examples:");
|
|
55
|
+
console.log(" vellum terminal");
|
|
56
|
+
console.log(" vellum terminal attach my-session");
|
|
57
|
+
console.log(" vellum terminal list");
|
|
58
|
+
console.log(" vellum terminal --assistant my-assistant");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// List tmux sessions
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
async function listTmuxSessions(
|
|
66
|
+
assistant: ResolvedManagedAssistant,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
const cols = 120;
|
|
69
|
+
const rows = 24;
|
|
70
|
+
|
|
71
|
+
const { session_id: sessionId } = await createTerminalSession(
|
|
72
|
+
assistant.token,
|
|
73
|
+
assistant.assistantId,
|
|
74
|
+
cols,
|
|
75
|
+
rows,
|
|
76
|
+
assistant.platformUrl,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const abortController = new AbortController();
|
|
80
|
+
const output: string[] = [];
|
|
81
|
+
let commandSent = false;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const timeout = setTimeout(() => abortController.abort(), 5000);
|
|
85
|
+
|
|
86
|
+
const streamPromise = (async () => {
|
|
87
|
+
for await (const event of subscribeTerminalEvents(
|
|
88
|
+
assistant.token,
|
|
89
|
+
assistant.assistantId,
|
|
90
|
+
sessionId,
|
|
91
|
+
assistant.platformUrl,
|
|
92
|
+
abortController.signal,
|
|
93
|
+
)) {
|
|
94
|
+
const text = Buffer.from(event.data, "base64").toString("utf-8");
|
|
95
|
+
output.push(text);
|
|
96
|
+
|
|
97
|
+
// Wait for shell prompt before sending command
|
|
98
|
+
if (!commandSent) {
|
|
99
|
+
const joined = output.join("");
|
|
100
|
+
if (
|
|
101
|
+
joined.includes("$") ||
|
|
102
|
+
joined.includes("#") ||
|
|
103
|
+
joined.includes("%")
|
|
104
|
+
) {
|
|
105
|
+
commandSent = true;
|
|
106
|
+
await sendTerminalInput(
|
|
107
|
+
assistant.token,
|
|
108
|
+
assistant.assistantId,
|
|
109
|
+
sessionId,
|
|
110
|
+
'tmux list-sessions 2>/dev/null || echo "No tmux sessions found"; exit\r',
|
|
111
|
+
assistant.platformUrl,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
|
|
118
|
+
await streamPromise.catch(() => {});
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
} catch {
|
|
121
|
+
// Expected — abort or stream end
|
|
122
|
+
} finally {
|
|
123
|
+
abortController.abort();
|
|
124
|
+
await closeTerminalSession(
|
|
125
|
+
assistant.token,
|
|
126
|
+
assistant.assistantId,
|
|
127
|
+
sessionId,
|
|
128
|
+
assistant.platformUrl,
|
|
129
|
+
).catch(() => {});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Parse and display results
|
|
133
|
+
const raw = output.join("");
|
|
134
|
+
// Strip ANSI escape sequences for clean parsing
|
|
135
|
+
const clean = raw.replace(
|
|
136
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: needed for ANSI stripping
|
|
137
|
+
/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][^\n]|\r/g,
|
|
138
|
+
"",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Find tmux output lines (format: "session_name: N windows ...")
|
|
142
|
+
const lines = clean.split("\n");
|
|
143
|
+
const sessionLines = lines.filter(
|
|
144
|
+
(l) =>
|
|
145
|
+
/^\S+:\s+\d+\s+windows?/.test(l.trim()) ||
|
|
146
|
+
l.includes("No tmux sessions found"),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (sessionLines.length === 0) {
|
|
150
|
+
console.log("No tmux sessions found.");
|
|
151
|
+
} else {
|
|
152
|
+
for (const line of sessionLines) {
|
|
153
|
+
console.log(line.trim());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Main entry point
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export async function terminal(): Promise<void> {
|
|
163
|
+
const args = process.argv.slice(3);
|
|
164
|
+
|
|
165
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
166
|
+
printHelp();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Parse arguments
|
|
171
|
+
//
|
|
172
|
+
// Accepted forms:
|
|
173
|
+
// vellum terminal [--assistant <name>]
|
|
174
|
+
// vellum terminal list [--assistant <name>]
|
|
175
|
+
// vellum terminal attach <session> [--assistant <name>]
|
|
176
|
+
let subcommand: string | undefined;
|
|
177
|
+
let assistantName: string | undefined;
|
|
178
|
+
let tmuxSessionName: string | undefined;
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < args.length; i++) {
|
|
181
|
+
if (args[i] === "--assistant" && args[i + 1]) {
|
|
182
|
+
assistantName = args[++i];
|
|
183
|
+
} else if (args[i].startsWith("-")) {
|
|
184
|
+
// Skip unknown flags
|
|
185
|
+
continue;
|
|
186
|
+
} else if (!subcommand) {
|
|
187
|
+
// First positional — subcommand or assistant name
|
|
188
|
+
if (args[i] === "list" || args[i] === "attach") {
|
|
189
|
+
subcommand = args[i];
|
|
190
|
+
} else {
|
|
191
|
+
assistantName = args[i];
|
|
192
|
+
}
|
|
193
|
+
} else if (subcommand === "attach" && !tmuxSessionName) {
|
|
194
|
+
// Second positional after "attach" — tmux session name
|
|
195
|
+
tmuxSessionName = args[i];
|
|
196
|
+
} else if (!assistantName) {
|
|
197
|
+
// Trailing positional after subcommand args — assistant name
|
|
198
|
+
assistantName = args[i];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const assistant = resolveManagedAssistant(assistantName);
|
|
203
|
+
|
|
204
|
+
if (subcommand === "list") {
|
|
205
|
+
await listTmuxSessions(assistant);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (subcommand === "attach") {
|
|
210
|
+
if (!tmuxSessionName) {
|
|
211
|
+
console.error("Usage: vellum terminal attach <session-name>");
|
|
212
|
+
console.error(
|
|
213
|
+
"\nUse 'vellum terminal list' to see available tmux sessions.",
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
// Shell-escape the session name to handle spaces/metacharacters
|
|
218
|
+
const escaped = tmuxSessionName.replace(/'/g, "'\\''");
|
|
219
|
+
await interactiveSession(assistant, `tmux attach -t '${escaped}'`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Default: interactive shell
|
|
224
|
+
await interactiveSession(assistant);
|
|
225
|
+
}
|
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;
|