@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.
Files changed (47) 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/retire.ts +23 -0
  17. package/src/commands/sleep.ts +5 -2
  18. package/src/commands/ssh.ts +15 -2
  19. package/src/commands/teleport.ts +447 -583
  20. package/src/commands/terminal.ts +225 -0
  21. package/src/commands/wake.ts +2 -1
  22. package/src/components/DefaultMainScreen.tsx +304 -152
  23. package/src/index.ts +6 -0
  24. package/src/lib/__tests__/docker.test.ts +50 -74
  25. package/src/lib/__tests__/job-polling.test.ts +278 -0
  26. package/src/lib/__tests__/local-runtime-client.test.ts +383 -0
  27. package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
  28. package/src/lib/assistant-config.ts +12 -8
  29. package/src/lib/client-identity.ts +67 -0
  30. package/src/lib/config-utils.ts +97 -1
  31. package/src/lib/docker.ts +73 -75
  32. package/src/lib/environments/__tests__/paths.test.ts +2 -0
  33. package/src/lib/environments/resolve.ts +89 -7
  34. package/src/lib/environments/seeds.ts +8 -5
  35. package/src/lib/environments/types.ts +10 -0
  36. package/src/lib/hatch-local.ts +15 -120
  37. package/src/lib/health-check.ts +98 -0
  38. package/src/lib/job-polling.ts +195 -0
  39. package/src/lib/local-runtime-client.ts +178 -0
  40. package/src/lib/local.ts +139 -15
  41. package/src/lib/orphan-detection.ts +2 -35
  42. package/src/lib/platform-client.ts +215 -0
  43. package/src/lib/retire-local.ts +6 -2
  44. package/src/lib/terminal-client.ts +177 -0
  45. package/src/lib/terminal-session.ts +457 -0
  46. package/src/shared/provider-env-vars.ts +2 -3
  47. package/src/__tests__/orphan-detection.test.ts +0 -214
@@ -14,6 +14,7 @@ import {
14
14
  import { Box, render as inkRender, Text, useInput, useStdout } from "ink";
15
15
 
16
16
  import { removeAssistantEntry } from "../lib/assistant-config";
17
+ import { getClientRegistrationHeaders } from "../lib/client-identity";
17
18
  import { SPECIES_CONFIG, type Species } from "../lib/constants";
18
19
  import { callDoctorDaemon, type ChatLogEntry } from "../lib/doctor-client";
19
20
  import { checkHealth } from "../lib/health-check";
@@ -50,9 +51,7 @@ export const SLASH_COMMANDS = [
50
51
  "/retire",
51
52
  ];
52
53
 
53
- const POLL_INTERVAL_MS = 3000;
54
54
  const SEND_TIMEOUT_MS = 5000;
55
- const RESPONSE_POLL_INTERVAL_MS = 1000;
56
55
 
57
56
  // ── Layout constants ──────────────────────────────────────
58
57
  const MAX_TOTAL_WIDTH = 72;
@@ -148,11 +147,6 @@ interface AddTrustRuleResponse {
148
147
  accepted: boolean;
149
148
  }
150
149
 
151
- interface PendingInteractionsResponse {
152
- pendingConfirmation: (PendingConfirmation & { requestId: string }) | null;
153
- pendingSecret: (PendingSecret & { requestId?: string }) | null;
154
- }
155
-
156
150
  type TrustDecision = "always_allow" | "always_deny";
157
151
 
158
152
  interface HealthResponse {
@@ -301,19 +295,114 @@ async function addTrustRule(
301
295
  );
302
296
  }
303
297
 
304
- async function pollPendingInteractions(
298
+ // ── SSE event types ─────────────────────────────────────────────
299
+ interface SseEvent {
300
+ type: string;
301
+ text?: string;
302
+ thinking?: string;
303
+ toolName?: string;
304
+ toolUseId?: string;
305
+ input?: Record<string, unknown>;
306
+ result?: string;
307
+ isError?: boolean;
308
+ content?: string;
309
+ chunk?: string;
310
+ message?: string;
311
+ conversationId?: string;
312
+ messageId?: string;
313
+ requestId?: string;
314
+ // confirmation_request fields
315
+ riskLevel?: string;
316
+ riskReason?: string;
317
+ executionTarget?: "sandbox" | "host";
318
+ allowlistOptions?: Array<{
319
+ label: string;
320
+ description: string;
321
+ pattern: string;
322
+ }>;
323
+ scopeOptions?: Array<{ label: string; scope: string }>;
324
+ persistentDecisionsAllowed?: boolean;
325
+ isContainerized?: boolean;
326
+ // secret_request fields
327
+ service?: string;
328
+ field?: string;
329
+ label?: string;
330
+ description?: string;
331
+ placeholder?: string;
332
+ purpose?: string;
333
+ allowOneTimeSend?: boolean;
334
+ allowedTools?: string[];
335
+ allowedDomains?: string[];
336
+ // message_complete fields
337
+ source?: "main" | "aux";
338
+ [key: string]: unknown;
339
+ }
340
+
341
+ /**
342
+ * Open an SSE stream to the assistant's /events endpoint.
343
+ * Yields unwrapped message payloads from `data:` lines, skipping
344
+ * heartbeat comments. The /events endpoint emits AssistantEvent
345
+ * envelopes (`{ id, assistantId, message: { type, ... } }`); this
346
+ * generator unwraps the envelope so callers switch on `.type` directly.
347
+ */
348
+ async function* streamEvents(
305
349
  baseUrl: string,
306
350
  assistantId: string,
351
+ conversationKey: string,
352
+ signal: AbortSignal,
307
353
  bearerToken?: string,
308
- ): Promise<PendingInteractionsResponse> {
309
- const params = new URLSearchParams({ conversationKey: assistantId });
310
- return runtimeRequest<PendingInteractionsResponse>(
311
- baseUrl,
312
- assistantId,
313
- `/pending-interactions?${params.toString()}`,
314
- undefined,
315
- bearerToken,
316
- );
354
+ ): AsyncGenerator<SseEvent> {
355
+ const params = new URLSearchParams({ conversationKey });
356
+ const url = `${baseUrl}/v1/assistants/${assistantId}/events?${params.toString()}`;
357
+ const response = await fetch(url, {
358
+ headers: {
359
+ Accept: "text/event-stream",
360
+ ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
361
+ ...getClientRegistrationHeaders(),
362
+ },
363
+ signal,
364
+ });
365
+
366
+ if (!response.ok) {
367
+ const body = await response.text().catch(() => "");
368
+ throw new Error(
369
+ `SSE connection failed (${response.status}): ${body || response.statusText}`,
370
+ );
371
+ }
372
+ if (!response.body) {
373
+ throw new Error("No response body from SSE endpoint");
374
+ }
375
+
376
+ const decoder = new TextDecoder();
377
+ let buffer = "";
378
+ for await (const chunk of response.body) {
379
+ buffer += decoder.decode(chunk as Uint8Array, { stream: true });
380
+ let boundary: number;
381
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
382
+ const frame = buffer.slice(0, boundary);
383
+ buffer = buffer.slice(boundary + 2);
384
+ if (!frame.trim() || frame.startsWith(":")) continue;
385
+ let data: string | undefined;
386
+ for (const line of frame.split("\n")) {
387
+ if (line.startsWith("data: ")) {
388
+ data = line.slice(6);
389
+ }
390
+ }
391
+ if (!data) continue;
392
+ try {
393
+ const envelope = JSON.parse(data) as {
394
+ message?: SseEvent;
395
+ [key: string]: unknown;
396
+ };
397
+ // Unwrap the AssistantEvent envelope
398
+ if (envelope.message && typeof envelope.message.type === "string") {
399
+ yield envelope.message;
400
+ }
401
+ } catch {
402
+ // skip malformed JSON
403
+ }
404
+ }
405
+ }
317
406
  }
318
407
 
319
408
  function formatConfirmationPreview(
@@ -508,6 +597,7 @@ export interface ToolCallInfo {
508
597
  input: Record<string, unknown>;
509
598
  result?: string;
510
599
  isError?: boolean;
600
+ toolUseId?: string;
511
601
  }
512
602
 
513
603
  export interface RuntimeMessage {
@@ -1307,7 +1397,9 @@ function ChatApp({
1307
1397
  const connectingRef = useRef(false);
1308
1398
  const seenMessageIdsRef = useRef(new Set<string>());
1309
1399
  const chatLogRef = useRef<ChatLogEntry[]>([]);
1310
- const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
1400
+ const sseAbortRef = useRef<AbortController | null>(null);
1401
+ const streamingTextRef = useRef("");
1402
+ const streamingToolCallsRef = useRef<ToolCallInfo[]>([]);
1311
1403
  const doctorSessionIdRef = useRef(randomUUID());
1312
1404
  const handleRef_ = useRef<ChatAppHandle | null>(null);
1313
1405
 
@@ -1535,9 +1627,9 @@ function ChatApp({
1535
1627
  }, []);
1536
1628
 
1537
1629
  const cleanup = useCallback(() => {
1538
- if (pollTimerRef.current) {
1539
- clearInterval(pollTimerRef.current);
1540
- pollTimerRef.current = null;
1630
+ if (sseAbortRef.current) {
1631
+ sseAbortRef.current.abort();
1632
+ sseAbortRef.current = null;
1541
1633
  }
1542
1634
  }, []);
1543
1635
 
@@ -1595,25 +1687,182 @@ function ChatApp({
1595
1687
  h.hideSpinner();
1596
1688
  }
1597
1689
 
1598
- pollTimerRef.current = setInterval(async () => {
1690
+ // Open SSE stream for real-time events
1691
+ const sseAc = new AbortController();
1692
+ sseAbortRef.current = sseAc;
1693
+
1694
+ // Process SSE events in the background
1695
+ (async () => {
1599
1696
  try {
1600
- const response = await pollMessages(
1697
+ for await (const event of streamEvents(
1601
1698
  runtimeUrl,
1602
1699
  assistantId,
1700
+ assistantId,
1701
+ sseAc.signal,
1603
1702
  bearerToken,
1604
- );
1605
- for (const msg of response.messages) {
1606
- if (!seenMessageIdsRef.current.has(msg.id)) {
1607
- seenMessageIdsRef.current.add(msg.id);
1608
- if (msg.role === "assistant") {
1609
- handleRef_.current?.addMessage(msg);
1703
+ )) {
1704
+ const hRef = handleRef_.current;
1705
+ if (!hRef) continue;
1706
+
1707
+ switch (event.type) {
1708
+ case "assistant_text_delta":
1709
+ streamingTextRef.current += event.text ?? "";
1710
+ break;
1711
+
1712
+ case "assistant_thinking_delta":
1713
+ // Thinking deltas are suppressed in the TUI for now
1714
+ break;
1715
+
1716
+ case "tool_use_start":
1717
+ if (event.toolName) {
1718
+ streamingToolCallsRef.current.push({
1719
+ name: event.toolName,
1720
+ input: event.input ?? {},
1721
+ toolUseId: event.toolUseId,
1722
+ });
1723
+ }
1724
+ break;
1725
+
1726
+ case "tool_result": {
1727
+ // Match by toolUseId first (robust for parallel/same-name calls),
1728
+ // fall back to name + missing result for backwards compat.
1729
+ const tc = event.toolUseId
1730
+ ? streamingToolCallsRef.current.find(
1731
+ (t) => t.toolUseId === event.toolUseId,
1732
+ )
1733
+ : streamingToolCallsRef.current.find(
1734
+ (t) =>
1735
+ t.name === event.toolName && t.result === undefined,
1736
+ );
1737
+ if (tc) {
1738
+ tc.result = event.result;
1739
+ tc.isError = event.isError;
1740
+ } else if (event.toolName) {
1741
+ streamingToolCallsRef.current.push({
1742
+ name: event.toolName,
1743
+ input: event.input ?? {},
1744
+ result: event.result,
1745
+ isError: event.isError,
1746
+ toolUseId: event.toolUseId,
1747
+ });
1748
+ }
1749
+ break;
1610
1750
  }
1751
+
1752
+ case "confirmation_request":
1753
+ hRef.hideSpinner();
1754
+ await handleConfirmationPrompt(
1755
+ runtimeUrl,
1756
+ assistantId,
1757
+ event.requestId ?? "",
1758
+ {
1759
+ toolName: event.toolName ?? "",
1760
+ toolUseId: event.toolUseId ?? "",
1761
+ input: event.input ?? {},
1762
+ riskLevel: event.riskLevel ?? "unknown",
1763
+ executionTarget: event.executionTarget,
1764
+ allowlistOptions: event.allowlistOptions?.map((o) => ({
1765
+ label: o.label,
1766
+ pattern: o.pattern,
1767
+ })),
1768
+ scopeOptions: event.scopeOptions,
1769
+ persistentDecisionsAllowed:
1770
+ event.persistentDecisionsAllowed,
1771
+ },
1772
+ hRef,
1773
+ bearerToken,
1774
+ );
1775
+ hRef.showSpinner("Working...");
1776
+ break;
1777
+
1778
+ case "secret_request":
1779
+ hRef.hideSpinner();
1780
+ await hRef.handleSecretPrompt(
1781
+ {
1782
+ requestId: event.requestId ?? "",
1783
+ service: event.service ?? "",
1784
+ field: event.field ?? "",
1785
+ label: event.label ?? "",
1786
+ description: event.description,
1787
+ placeholder: event.placeholder,
1788
+ purpose: event.purpose,
1789
+ allowOneTimeSend: event.allowOneTimeSend,
1790
+ },
1791
+ async (value, delivery) => {
1792
+ await runtimeRequest(
1793
+ runtimeUrl,
1794
+ assistantId,
1795
+ "/secret",
1796
+ {
1797
+ method: "POST",
1798
+ body: JSON.stringify({
1799
+ requestId: event.requestId,
1800
+ value,
1801
+ delivery,
1802
+ }),
1803
+ },
1804
+ bearerToken,
1805
+ );
1806
+ },
1807
+ );
1808
+ hRef.showSpinner("Working...");
1809
+ break;
1810
+
1811
+ case "message_complete": {
1812
+ // Only finalize main turns (ignore aux events like call transcripts)
1813
+ if (event.source === "aux") break;
1814
+
1815
+ const text = streamingTextRef.current;
1816
+ const toolCalls = [...streamingToolCallsRef.current];
1817
+ streamingTextRef.current = "";
1818
+ streamingToolCallsRef.current = [];
1819
+
1820
+ if (text || toolCalls.length > 0) {
1821
+ const msg: RuntimeMessage = {
1822
+ id: event.messageId ?? `sse-${Date.now()}`,
1823
+ role: "assistant",
1824
+ content: text,
1825
+ timestamp: new Date().toISOString(),
1826
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
1827
+ };
1828
+ seenMessageIdsRef.current.add(msg.id);
1829
+ hRef.addMessage(msg);
1830
+ chatLogRef.current.push({
1831
+ role: "assistant",
1832
+ content: text,
1833
+ });
1834
+ process.stdout.write("\x07");
1835
+ }
1836
+
1837
+ hRef.setBusy(false);
1838
+ hRef.hideSpinner();
1839
+ break;
1840
+ }
1841
+
1842
+ case "error":
1843
+ hRef.hideSpinner();
1844
+ hRef.showError(event.message ?? "Unknown error");
1845
+ hRef.setBusy(false);
1846
+ break;
1847
+
1848
+ default:
1849
+ // Ignore events we don't handle (activity state, traces, etc.)
1850
+ break;
1611
1851
  }
1612
1852
  }
1613
1853
  } catch {
1614
- // Poll failure; continue silently
1854
+ // Stream ended only report if not intentionally aborted
1855
+ if (!sseAc.signal.aborted) {
1856
+ handleRef_.current?.addStatus(
1857
+ "SSE stream disconnected — will reconnect on next message",
1858
+ "yellow",
1859
+ );
1860
+ handleRef_.current?.setBusy(false);
1861
+ handleRef_.current?.hideSpinner();
1862
+ connectedRef.current = false;
1863
+ }
1615
1864
  }
1616
- }, POLL_INTERVAL_MS);
1865
+ })();
1617
1866
 
1618
1867
  connectedRef.current = true;
1619
1868
  connectingRef.current = false;
@@ -2049,135 +2298,38 @@ function ChatApp({
2049
2298
  h.showSpinner("Sending...");
2050
2299
  h.setBusy(true);
2051
2300
 
2052
- try {
2053
- const controller = new AbortController();
2054
- const timeoutId = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
2301
+ const controller = new AbortController();
2302
+ const timeoutId = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
2055
2303
 
2056
- try {
2057
- const sendResult = await sendMessage(
2058
- runtimeUrl,
2059
- assistantId,
2060
- trimmed,
2061
- controller.signal,
2062
- bearerToken,
2063
- );
2064
- clearTimeout(timeoutId);
2065
- if (!sendResult.accepted) {
2066
- h.setBusy(false);
2067
- h.hideSpinner();
2068
- h.showError("Message was not accepted by the assistant");
2069
- return;
2070
- }
2071
- } catch (sendErr) {
2072
- clearTimeout(timeoutId);
2304
+ try {
2305
+ const sendResult = await sendMessage(
2306
+ runtimeUrl,
2307
+ assistantId,
2308
+ trimmed,
2309
+ controller.signal,
2310
+ bearerToken,
2311
+ );
2312
+ clearTimeout(timeoutId);
2313
+ if (!sendResult.accepted) {
2073
2314
  h.setBusy(false);
2074
2315
  h.hideSpinner();
2075
- const errorMsg =
2076
- sendErr instanceof Error ? sendErr.message : String(sendErr);
2077
- h.showError(errorMsg);
2078
- chatLogRef.current.push({ role: "error", content: errorMsg });
2316
+ h.showError("Message was not accepted by the assistant");
2079
2317
  return;
2080
2318
  }
2081
-
2082
- h.showSpinner("Working...");
2083
-
2084
- while (true) {
2085
- await new Promise((resolve) =>
2086
- setTimeout(resolve, RESPONSE_POLL_INTERVAL_MS),
2087
- );
2088
-
2089
- // Check for pending confirmations/secrets
2090
- try {
2091
- const pending = await pollPendingInteractions(
2092
- runtimeUrl,
2093
- assistantId,
2094
- bearerToken,
2095
- );
2096
-
2097
- if (pending.pendingConfirmation) {
2098
- h.hideSpinner();
2099
- await handleConfirmationPrompt(
2100
- runtimeUrl,
2101
- assistantId,
2102
- pending.pendingConfirmation.requestId,
2103
- pending.pendingConfirmation,
2104
- h,
2105
- bearerToken,
2106
- );
2107
- h.showSpinner("Working...");
2108
- continue;
2109
- }
2110
-
2111
- if (pending.pendingSecret) {
2112
- const secretRequestId = pending.pendingSecret.requestId ?? "";
2113
- h.hideSpinner();
2114
- await h.handleSecretPrompt(
2115
- pending.pendingSecret,
2116
- async (value, delivery) => {
2117
- await runtimeRequest(
2118
- runtimeUrl,
2119
- assistantId,
2120
- "/secret",
2121
- {
2122
- method: "POST",
2123
- body: JSON.stringify({
2124
- requestId: secretRequestId,
2125
- value,
2126
- delivery,
2127
- }),
2128
- },
2129
- bearerToken,
2130
- );
2131
- },
2132
- );
2133
- h.showSpinner("Working...");
2134
- continue;
2135
- }
2136
- } catch {
2137
- // Pending interactions poll failure; fall through to message poll
2138
- }
2139
-
2140
- // Poll for new messages to detect completion
2141
- try {
2142
- const pollResult = await pollMessages(
2143
- runtimeUrl,
2144
- assistantId,
2145
- bearerToken,
2146
- );
2147
- for (const msg of pollResult.messages) {
2148
- if (!seenMessageIdsRef.current.has(msg.id)) {
2149
- seenMessageIdsRef.current.add(msg.id);
2150
- if (msg.role === "assistant") {
2151
- h.addMessage(msg);
2152
- chatLogRef.current.push({
2153
- role: "assistant",
2154
- content: msg.content,
2155
- });
2156
- process.stdout.write("\x07");
2157
- h.setBusy(false);
2158
- h.hideSpinner();
2159
- return;
2160
- }
2161
- }
2162
- }
2163
- } catch {
2164
- // Poll failure; retry
2165
- }
2166
- }
2167
- } catch (error) {
2319
+ } catch (sendErr) {
2320
+ clearTimeout(timeoutId);
2168
2321
  h.setBusy(false);
2169
2322
  h.hideSpinner();
2170
- const isTimeout = error instanceof Error && error.name === "AbortError";
2171
- if (isTimeout) {
2172
- const errorMsg = "Send timed out";
2173
- h.showError(errorMsg);
2174
- chatLogRef.current.push({ role: "error", content: errorMsg });
2175
- } else {
2176
- const errorMsg = `Failed to send: ${error instanceof Error ? error.message : error}`;
2177
- h.showError(errorMsg);
2178
- chatLogRef.current.push({ role: "error", content: errorMsg });
2179
- }
2323
+ const errorMsg =
2324
+ sendErr instanceof Error ? sendErr.message : String(sendErr);
2325
+ h.showError(errorMsg);
2326
+ chatLogRef.current.push({ role: "error", content: errorMsg });
2327
+ return;
2180
2328
  }
2329
+
2330
+ // Accumulators are reset by message_complete; no reset here to avoid
2331
+ // racing with SSE events that may arrive during the sendMessage await.
2332
+ h.showSpinner("Working...");
2181
2333
  },
2182
2334
  [
2183
2335
  runtimeUrl,
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import cliPkg from "../package.json";
4
4
  import { backup } from "./commands/backup";
5
5
  import { clean } from "./commands/clean";
6
6
  import { client } from "./commands/client";
7
+ import { env } from "./commands/env";
7
8
  import { events } from "./commands/events";
8
9
  import { exec } from "./commands/exec";
9
10
  import { hatch } from "./commands/hatch";
@@ -20,6 +21,7 @@ import { setup } from "./commands/setup";
20
21
  import { sleep } from "./commands/sleep";
21
22
  import { ssh } from "./commands/ssh";
22
23
  import { teleport } from "./commands/teleport";
24
+ import { terminal } from "./commands/terminal";
23
25
  import { tunnel } from "./commands/tunnel";
24
26
  import { upgrade } from "./commands/upgrade";
25
27
  import { use } from "./commands/use";
@@ -37,6 +39,7 @@ const commands = {
37
39
  backup,
38
40
  clean,
39
41
  client,
42
+ env,
40
43
  events,
41
44
  exec,
42
45
  hatch,
@@ -54,6 +57,7 @@ const commands = {
54
57
  sleep,
55
58
  ssh,
56
59
  teleport,
60
+ terminal,
57
61
  tunnel,
58
62
  upgrade,
59
63
  use,
@@ -70,6 +74,7 @@ function printHelp(): void {
70
74
  console.log(" backup Export a backup of a running assistant");
71
75
  console.log(" clean Kill orphaned vellum processes");
72
76
  console.log(" client Connect to a hatched assistant");
77
+ console.log(" env Manage the default CLI environment");
73
78
  console.log(" events Stream events from a running assistant");
74
79
  console.log(" exec Execute a command inside an assistant's container");
75
80
  console.log(" hatch Create a new assistant instance");
@@ -91,6 +96,7 @@ function printHelp(): void {
91
96
  console.log(" sleep Stop the assistant process");
92
97
  console.log(" ssh SSH into a remote assistant instance");
93
98
  console.log(" teleport Transfer assistant data between environments");
99
+ console.log(" terminal Open a terminal into a managed assistant container");
94
100
  console.log(" tunnel Create a tunnel for a locally hosted assistant");
95
101
  console.log(" upgrade Upgrade an assistant to a newer version");
96
102
  console.log(" use Set the active assistant for commands");