@vellumai/cli 0.6.6 → 0.7.1
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/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +1 -7
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +146 -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 +988 -1266
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/env.ts +93 -0
- package/src/commands/events.ts +2 -0
- package/src/commands/exec.ts +58 -13
- package/src/commands/login.ts +77 -12
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +144 -25
- package/src/commands/restore.ts +26 -47
- package/src/commands/sleep.ts +5 -2
- package/src/commands/ssh.ts +17 -7
- package/src/commands/teleport.ts +462 -584
- package/src/commands/terminal.ts +9 -221
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/commands/wake.ts +2 -1
- package/src/components/DefaultMainScreen.tsx +328 -154
- package/src/index.ts +5 -7
- 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 +480 -0
- package/src/lib/__tests__/platform-client-signed-url.test.ts +405 -0
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +46 -24
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +67 -0
- package/src/lib/docker.ts +75 -77
- 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 +231 -0
- package/src/lib/local.ts +165 -72
- package/src/lib/orphan-detection.ts +2 -35
- package/src/lib/platform-client.ts +190 -194
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/retire-local.ts +6 -2
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +536 -0
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
- package/src/shared/provider-env-vars.ts +2 -3
- package/src/__tests__/orphan-detection.test.ts +0 -214
|
@@ -14,10 +14,12 @@ 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";
|
|
20
21
|
import { appendHistory, loadHistory } from "../lib/input-history";
|
|
22
|
+
import { tuiLog } from "../lib/tui-log";
|
|
21
23
|
import { statusEmoji, withStatusEmoji } from "../lib/status-emoji";
|
|
22
24
|
import {
|
|
23
25
|
getTerminalCapabilities,
|
|
@@ -50,9 +52,7 @@ export const SLASH_COMMANDS = [
|
|
|
50
52
|
"/retire",
|
|
51
53
|
];
|
|
52
54
|
|
|
53
|
-
const POLL_INTERVAL_MS = 3000;
|
|
54
55
|
const SEND_TIMEOUT_MS = 5000;
|
|
55
|
-
const RESPONSE_POLL_INTERVAL_MS = 1000;
|
|
56
56
|
|
|
57
57
|
// ── Layout constants ──────────────────────────────────────
|
|
58
58
|
const MAX_TOTAL_WIDTH = 72;
|
|
@@ -148,11 +148,6 @@ interface AddTrustRuleResponse {
|
|
|
148
148
|
accepted: boolean;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
interface PendingInteractionsResponse {
|
|
152
|
-
pendingConfirmation: (PendingConfirmation & { requestId: string }) | null;
|
|
153
|
-
pendingSecret: (PendingSecret & { requestId?: string }) | null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
151
|
type TrustDecision = "always_allow" | "always_deny";
|
|
157
152
|
|
|
158
153
|
interface HealthResponse {
|
|
@@ -301,19 +296,127 @@ async function addTrustRule(
|
|
|
301
296
|
);
|
|
302
297
|
}
|
|
303
298
|
|
|
304
|
-
|
|
299
|
+
// ── SSE event types ─────────────────────────────────────────────
|
|
300
|
+
interface SseEvent {
|
|
301
|
+
type: string;
|
|
302
|
+
text?: string;
|
|
303
|
+
thinking?: string;
|
|
304
|
+
toolName?: string;
|
|
305
|
+
toolUseId?: string;
|
|
306
|
+
input?: Record<string, unknown>;
|
|
307
|
+
result?: string;
|
|
308
|
+
isError?: boolean;
|
|
309
|
+
content?: string;
|
|
310
|
+
chunk?: string;
|
|
311
|
+
message?: string;
|
|
312
|
+
conversationId?: string;
|
|
313
|
+
messageId?: string;
|
|
314
|
+
requestId?: string;
|
|
315
|
+
// confirmation_request fields
|
|
316
|
+
riskLevel?: string;
|
|
317
|
+
riskReason?: string;
|
|
318
|
+
executionTarget?: "sandbox" | "host";
|
|
319
|
+
allowlistOptions?: Array<{
|
|
320
|
+
label: string;
|
|
321
|
+
description: string;
|
|
322
|
+
pattern: string;
|
|
323
|
+
}>;
|
|
324
|
+
scopeOptions?: Array<{ label: string; scope: string }>;
|
|
325
|
+
persistentDecisionsAllowed?: boolean;
|
|
326
|
+
isContainerized?: boolean;
|
|
327
|
+
// secret_request fields
|
|
328
|
+
service?: string;
|
|
329
|
+
field?: string;
|
|
330
|
+
label?: string;
|
|
331
|
+
description?: string;
|
|
332
|
+
placeholder?: string;
|
|
333
|
+
purpose?: string;
|
|
334
|
+
allowOneTimeSend?: boolean;
|
|
335
|
+
allowedTools?: string[];
|
|
336
|
+
allowedDomains?: string[];
|
|
337
|
+
// message_complete fields
|
|
338
|
+
source?: "main" | "aux";
|
|
339
|
+
[key: string]: unknown;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Open an SSE stream to the assistant's /events endpoint.
|
|
344
|
+
* Yields unwrapped message payloads from `data:` lines, skipping
|
|
345
|
+
* heartbeat comments. The /events endpoint emits AssistantEvent
|
|
346
|
+
* envelopes (`{ id, assistantId, message: { type, ... } }`); this
|
|
347
|
+
* generator unwraps the envelope so callers switch on `.type` directly.
|
|
348
|
+
*/
|
|
349
|
+
async function* streamEvents(
|
|
305
350
|
baseUrl: string,
|
|
306
351
|
assistantId: string,
|
|
352
|
+
conversationKey: string,
|
|
353
|
+
signal: AbortSignal,
|
|
307
354
|
bearerToken?: string,
|
|
308
|
-
):
|
|
309
|
-
const params = new URLSearchParams({ conversationKey
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
355
|
+
): AsyncGenerator<SseEvent> {
|
|
356
|
+
const params = new URLSearchParams({ conversationKey });
|
|
357
|
+
const url = `${baseUrl}/v1/assistants/${assistantId}/events?${params.toString()}`;
|
|
358
|
+
const clientHeaders = getClientRegistrationHeaders();
|
|
359
|
+
tuiLog.info("sse connect", { url, clientHeaders });
|
|
360
|
+
const response = await fetch(url, {
|
|
361
|
+
headers: {
|
|
362
|
+
Accept: "text/event-stream",
|
|
363
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
|
|
364
|
+
...clientHeaders,
|
|
365
|
+
},
|
|
366
|
+
signal,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
tuiLog.info("sse response", {
|
|
370
|
+
status: response.status,
|
|
371
|
+
statusText: response.statusText,
|
|
372
|
+
contentType: response.headers.get("content-type"),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
const body = await response.text().catch(() => "");
|
|
377
|
+
tuiLog.error("sse connection failed", {
|
|
378
|
+
status: response.status,
|
|
379
|
+
body: body.slice(0, 500),
|
|
380
|
+
});
|
|
381
|
+
throw new Error(
|
|
382
|
+
`SSE connection failed (${response.status}): ${body || response.statusText}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (!response.body) {
|
|
386
|
+
tuiLog.error("sse response has no body");
|
|
387
|
+
throw new Error("No response body from SSE endpoint");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const decoder = new TextDecoder();
|
|
391
|
+
let buffer = "";
|
|
392
|
+
for await (const chunk of response.body) {
|
|
393
|
+
buffer += decoder.decode(chunk as Uint8Array, { stream: true });
|
|
394
|
+
let boundary: number;
|
|
395
|
+
while ((boundary = buffer.indexOf("\n\n")) !== -1) {
|
|
396
|
+
const frame = buffer.slice(0, boundary);
|
|
397
|
+
buffer = buffer.slice(boundary + 2);
|
|
398
|
+
if (!frame.trim() || frame.startsWith(":")) continue;
|
|
399
|
+
let data: string | undefined;
|
|
400
|
+
for (const line of frame.split("\n")) {
|
|
401
|
+
if (line.startsWith("data: ")) {
|
|
402
|
+
data = line.slice(6);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (!data) continue;
|
|
406
|
+
try {
|
|
407
|
+
const envelope = JSON.parse(data) as {
|
|
408
|
+
message?: SseEvent;
|
|
409
|
+
[key: string]: unknown;
|
|
410
|
+
};
|
|
411
|
+
// Unwrap the AssistantEvent envelope
|
|
412
|
+
if (envelope.message && typeof envelope.message.type === "string") {
|
|
413
|
+
yield envelope.message;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// skip malformed JSON
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
317
420
|
}
|
|
318
421
|
|
|
319
422
|
function formatConfirmationPreview(
|
|
@@ -508,6 +611,7 @@ export interface ToolCallInfo {
|
|
|
508
611
|
input: Record<string, unknown>;
|
|
509
612
|
result?: string;
|
|
510
613
|
isError?: boolean;
|
|
614
|
+
toolUseId?: string;
|
|
511
615
|
}
|
|
512
616
|
|
|
513
617
|
export interface RuntimeMessage {
|
|
@@ -1307,7 +1411,9 @@ function ChatApp({
|
|
|
1307
1411
|
const connectingRef = useRef(false);
|
|
1308
1412
|
const seenMessageIdsRef = useRef(new Set<string>());
|
|
1309
1413
|
const chatLogRef = useRef<ChatLogEntry[]>([]);
|
|
1310
|
-
const
|
|
1414
|
+
const sseAbortRef = useRef<AbortController | null>(null);
|
|
1415
|
+
const streamingTextRef = useRef("");
|
|
1416
|
+
const streamingToolCallsRef = useRef<ToolCallInfo[]>([]);
|
|
1311
1417
|
const doctorSessionIdRef = useRef(randomUUID());
|
|
1312
1418
|
const handleRef_ = useRef<ChatAppHandle | null>(null);
|
|
1313
1419
|
|
|
@@ -1535,9 +1641,9 @@ function ChatApp({
|
|
|
1535
1641
|
}, []);
|
|
1536
1642
|
|
|
1537
1643
|
const cleanup = useCallback(() => {
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
|
|
1644
|
+
if (sseAbortRef.current) {
|
|
1645
|
+
sseAbortRef.current.abort();
|
|
1646
|
+
sseAbortRef.current = null;
|
|
1541
1647
|
}
|
|
1542
1648
|
}, []);
|
|
1543
1649
|
|
|
@@ -1561,6 +1667,10 @@ function ChatApp({
|
|
|
1561
1667
|
|
|
1562
1668
|
try {
|
|
1563
1669
|
const health = await checkHealthRuntime(runtimeUrl);
|
|
1670
|
+
tuiLog.info("health check", {
|
|
1671
|
+
status: health.status,
|
|
1672
|
+
message: health.message,
|
|
1673
|
+
});
|
|
1564
1674
|
h.hideSpinner();
|
|
1565
1675
|
h.updateHealthStatus(health.status);
|
|
1566
1676
|
if (health.status === "healthy" || health.status === "ok") {
|
|
@@ -1595,35 +1705,196 @@ function ChatApp({
|
|
|
1595
1705
|
h.hideSpinner();
|
|
1596
1706
|
}
|
|
1597
1707
|
|
|
1598
|
-
|
|
1708
|
+
// Open SSE stream for real-time events
|
|
1709
|
+
const sseAc = new AbortController();
|
|
1710
|
+
sseAbortRef.current = sseAc;
|
|
1711
|
+
|
|
1712
|
+
// Process SSE events in the background
|
|
1713
|
+
(async () => {
|
|
1599
1714
|
try {
|
|
1600
|
-
const
|
|
1715
|
+
for await (const event of streamEvents(
|
|
1601
1716
|
runtimeUrl,
|
|
1602
1717
|
assistantId,
|
|
1718
|
+
assistantId,
|
|
1719
|
+
sseAc.signal,
|
|
1603
1720
|
bearerToken,
|
|
1604
|
-
)
|
|
1605
|
-
|
|
1606
|
-
if (!
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1721
|
+
)) {
|
|
1722
|
+
const hRef = handleRef_.current;
|
|
1723
|
+
if (!hRef) continue;
|
|
1724
|
+
|
|
1725
|
+
switch (event.type) {
|
|
1726
|
+
case "assistant_text_delta":
|
|
1727
|
+
streamingTextRef.current += event.text ?? "";
|
|
1728
|
+
break;
|
|
1729
|
+
|
|
1730
|
+
case "assistant_thinking_delta":
|
|
1731
|
+
// Thinking deltas are suppressed in the TUI for now
|
|
1732
|
+
break;
|
|
1733
|
+
|
|
1734
|
+
case "tool_use_start":
|
|
1735
|
+
if (event.toolName) {
|
|
1736
|
+
streamingToolCallsRef.current.push({
|
|
1737
|
+
name: event.toolName,
|
|
1738
|
+
input: event.input ?? {},
|
|
1739
|
+
toolUseId: event.toolUseId,
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
break;
|
|
1743
|
+
|
|
1744
|
+
case "tool_result": {
|
|
1745
|
+
// Match by toolUseId first (robust for parallel/same-name calls),
|
|
1746
|
+
// fall back to name + missing result for backwards compat.
|
|
1747
|
+
const tc = event.toolUseId
|
|
1748
|
+
? streamingToolCallsRef.current.find(
|
|
1749
|
+
(t) => t.toolUseId === event.toolUseId,
|
|
1750
|
+
)
|
|
1751
|
+
: streamingToolCallsRef.current.find(
|
|
1752
|
+
(t) =>
|
|
1753
|
+
t.name === event.toolName && t.result === undefined,
|
|
1754
|
+
);
|
|
1755
|
+
if (tc) {
|
|
1756
|
+
tc.result = event.result;
|
|
1757
|
+
tc.isError = event.isError;
|
|
1758
|
+
} else if (event.toolName) {
|
|
1759
|
+
streamingToolCallsRef.current.push({
|
|
1760
|
+
name: event.toolName,
|
|
1761
|
+
input: event.input ?? {},
|
|
1762
|
+
result: event.result,
|
|
1763
|
+
isError: event.isError,
|
|
1764
|
+
toolUseId: event.toolUseId,
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
break;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
case "confirmation_request":
|
|
1771
|
+
hRef.hideSpinner();
|
|
1772
|
+
await handleConfirmationPrompt(
|
|
1773
|
+
runtimeUrl,
|
|
1774
|
+
assistantId,
|
|
1775
|
+
event.requestId ?? "",
|
|
1776
|
+
{
|
|
1777
|
+
toolName: event.toolName ?? "",
|
|
1778
|
+
toolUseId: event.toolUseId ?? "",
|
|
1779
|
+
input: event.input ?? {},
|
|
1780
|
+
riskLevel: event.riskLevel ?? "unknown",
|
|
1781
|
+
executionTarget: event.executionTarget,
|
|
1782
|
+
allowlistOptions: event.allowlistOptions?.map((o) => ({
|
|
1783
|
+
label: o.label,
|
|
1784
|
+
pattern: o.pattern,
|
|
1785
|
+
})),
|
|
1786
|
+
scopeOptions: event.scopeOptions,
|
|
1787
|
+
persistentDecisionsAllowed:
|
|
1788
|
+
event.persistentDecisionsAllowed,
|
|
1789
|
+
},
|
|
1790
|
+
hRef,
|
|
1791
|
+
bearerToken,
|
|
1792
|
+
);
|
|
1793
|
+
hRef.showSpinner("Working...");
|
|
1794
|
+
break;
|
|
1795
|
+
|
|
1796
|
+
case "secret_request":
|
|
1797
|
+
hRef.hideSpinner();
|
|
1798
|
+
await hRef.handleSecretPrompt(
|
|
1799
|
+
{
|
|
1800
|
+
requestId: event.requestId ?? "",
|
|
1801
|
+
service: event.service ?? "",
|
|
1802
|
+
field: event.field ?? "",
|
|
1803
|
+
label: event.label ?? "",
|
|
1804
|
+
description: event.description,
|
|
1805
|
+
placeholder: event.placeholder,
|
|
1806
|
+
purpose: event.purpose,
|
|
1807
|
+
allowOneTimeSend: event.allowOneTimeSend,
|
|
1808
|
+
},
|
|
1809
|
+
async (value, delivery) => {
|
|
1810
|
+
await runtimeRequest(
|
|
1811
|
+
runtimeUrl,
|
|
1812
|
+
assistantId,
|
|
1813
|
+
"/secret",
|
|
1814
|
+
{
|
|
1815
|
+
method: "POST",
|
|
1816
|
+
body: JSON.stringify({
|
|
1817
|
+
requestId: event.requestId,
|
|
1818
|
+
value,
|
|
1819
|
+
delivery,
|
|
1820
|
+
}),
|
|
1821
|
+
},
|
|
1822
|
+
bearerToken,
|
|
1823
|
+
);
|
|
1824
|
+
},
|
|
1825
|
+
);
|
|
1826
|
+
hRef.showSpinner("Working...");
|
|
1827
|
+
break;
|
|
1828
|
+
|
|
1829
|
+
case "message_complete": {
|
|
1830
|
+
// Only finalize main turns (ignore aux events like call transcripts)
|
|
1831
|
+
if (event.source === "aux") break;
|
|
1832
|
+
|
|
1833
|
+
const text = streamingTextRef.current;
|
|
1834
|
+
const toolCalls = [...streamingToolCallsRef.current];
|
|
1835
|
+
streamingTextRef.current = "";
|
|
1836
|
+
streamingToolCallsRef.current = [];
|
|
1837
|
+
|
|
1838
|
+
if (text || toolCalls.length > 0) {
|
|
1839
|
+
const msg: RuntimeMessage = {
|
|
1840
|
+
id: event.messageId ?? `sse-${Date.now()}`,
|
|
1841
|
+
role: "assistant",
|
|
1842
|
+
content: text,
|
|
1843
|
+
timestamp: new Date().toISOString(),
|
|
1844
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
1845
|
+
};
|
|
1846
|
+
seenMessageIdsRef.current.add(msg.id);
|
|
1847
|
+
hRef.addMessage(msg);
|
|
1848
|
+
chatLogRef.current.push({
|
|
1849
|
+
role: "assistant",
|
|
1850
|
+
content: text,
|
|
1851
|
+
});
|
|
1852
|
+
process.stdout.write("\x07");
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
hRef.setBusy(false);
|
|
1856
|
+
hRef.hideSpinner();
|
|
1857
|
+
break;
|
|
1610
1858
|
}
|
|
1859
|
+
|
|
1860
|
+
case "error":
|
|
1861
|
+
hRef.hideSpinner();
|
|
1862
|
+
hRef.showError(event.message ?? "Unknown error");
|
|
1863
|
+
hRef.setBusy(false);
|
|
1864
|
+
break;
|
|
1865
|
+
|
|
1866
|
+
default:
|
|
1867
|
+
// Ignore events we don't handle (activity state, traces, etc.)
|
|
1868
|
+
break;
|
|
1611
1869
|
}
|
|
1612
1870
|
}
|
|
1613
|
-
} catch {
|
|
1614
|
-
//
|
|
1871
|
+
} catch (sseErr) {
|
|
1872
|
+
// Stream ended — only report if not intentionally aborted
|
|
1873
|
+
if (!sseAc.signal.aborted) {
|
|
1874
|
+
tuiLog.warn("sse stream disconnected", {
|
|
1875
|
+
error: String(sseErr),
|
|
1876
|
+
});
|
|
1877
|
+
handleRef_.current?.addStatus(
|
|
1878
|
+
"SSE stream disconnected — will reconnect on next message",
|
|
1879
|
+
"yellow",
|
|
1880
|
+
);
|
|
1881
|
+
handleRef_.current?.setBusy(false);
|
|
1882
|
+
handleRef_.current?.hideSpinner();
|
|
1883
|
+
connectedRef.current = false;
|
|
1884
|
+
}
|
|
1615
1885
|
}
|
|
1616
|
-
}
|
|
1886
|
+
})();
|
|
1617
1887
|
|
|
1618
1888
|
connectedRef.current = true;
|
|
1619
1889
|
connectingRef.current = false;
|
|
1620
1890
|
setConnectionState("connected");
|
|
1621
1891
|
return true;
|
|
1622
1892
|
} catch (err) {
|
|
1893
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1894
|
+
tuiLog.error("connection failed", { error: msg });
|
|
1623
1895
|
h.hideSpinner();
|
|
1624
1896
|
connectingRef.current = false;
|
|
1625
1897
|
h.updateHealthStatus("unreachable");
|
|
1626
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1627
1898
|
setConnectionState("error");
|
|
1628
1899
|
setConnectionError(msg);
|
|
1629
1900
|
h.addStatus(
|
|
@@ -2049,135 +2320,38 @@ function ChatApp({
|
|
|
2049
2320
|
h.showSpinner("Sending...");
|
|
2050
2321
|
h.setBusy(true);
|
|
2051
2322
|
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
const timeoutId = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
|
|
2323
|
+
const controller = new AbortController();
|
|
2324
|
+
const timeoutId = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
|
|
2055
2325
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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);
|
|
2326
|
+
try {
|
|
2327
|
+
const sendResult = await sendMessage(
|
|
2328
|
+
runtimeUrl,
|
|
2329
|
+
assistantId,
|
|
2330
|
+
trimmed,
|
|
2331
|
+
controller.signal,
|
|
2332
|
+
bearerToken,
|
|
2333
|
+
);
|
|
2334
|
+
clearTimeout(timeoutId);
|
|
2335
|
+
if (!sendResult.accepted) {
|
|
2073
2336
|
h.setBusy(false);
|
|
2074
2337
|
h.hideSpinner();
|
|
2075
|
-
|
|
2076
|
-
sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
2077
|
-
h.showError(errorMsg);
|
|
2078
|
-
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
2338
|
+
h.showError("Message was not accepted by the assistant");
|
|
2079
2339
|
return;
|
|
2080
2340
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
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) {
|
|
2341
|
+
} catch (sendErr) {
|
|
2342
|
+
clearTimeout(timeoutId);
|
|
2168
2343
|
h.setBusy(false);
|
|
2169
2344
|
h.hideSpinner();
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
-
}
|
|
2345
|
+
const errorMsg =
|
|
2346
|
+
sendErr instanceof Error ? sendErr.message : String(sendErr);
|
|
2347
|
+
h.showError(errorMsg);
|
|
2348
|
+
chatLogRef.current.push({ role: "error", content: errorMsg });
|
|
2349
|
+
return;
|
|
2180
2350
|
}
|
|
2351
|
+
|
|
2352
|
+
// Accumulators are reset by message_complete; no reset here to avoid
|
|
2353
|
+
// racing with SSE events that may arrive during the sendMessage await.
|
|
2354
|
+
h.showSpinner("Working...");
|
|
2181
2355
|
},
|
|
2182
2356
|
[
|
|
2183
2357
|
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";
|
|
@@ -26,9 +27,7 @@ import { upgrade } from "./commands/upgrade";
|
|
|
26
27
|
import { use } from "./commands/use";
|
|
27
28
|
import { wake } from "./commands/wake";
|
|
28
29
|
import {
|
|
29
|
-
|
|
30
|
-
findAssistantByName,
|
|
31
|
-
loadLatestAssistant,
|
|
30
|
+
resolveAssistant,
|
|
32
31
|
setActiveAssistant,
|
|
33
32
|
} from "./lib/assistant-config";
|
|
34
33
|
import { loadGuardianToken } from "./lib/guardian-token";
|
|
@@ -38,6 +37,7 @@ const commands = {
|
|
|
38
37
|
backup,
|
|
39
38
|
clean,
|
|
40
39
|
client,
|
|
40
|
+
env,
|
|
41
41
|
events,
|
|
42
42
|
exec,
|
|
43
43
|
hatch,
|
|
@@ -72,6 +72,7 @@ function printHelp(): void {
|
|
|
72
72
|
console.log(" backup Export a backup of a running assistant");
|
|
73
73
|
console.log(" clean Kill orphaned vellum processes");
|
|
74
74
|
console.log(" client Connect to a hatched assistant");
|
|
75
|
+
console.log(" env Manage the default CLI environment");
|
|
75
76
|
console.log(" events Stream events from a running assistant");
|
|
76
77
|
console.log(" exec Execute a command inside an assistant's container");
|
|
77
78
|
console.log(" hatch Create a new assistant instance");
|
|
@@ -126,10 +127,7 @@ function applyNoColorFlags(argv: string[]): void {
|
|
|
126
127
|
* Otherwise return false so the caller can fall back to help text.
|
|
127
128
|
*/
|
|
128
129
|
async function tryLaunchClient(): Promise<boolean> {
|
|
129
|
-
const
|
|
130
|
-
const entry = activeName
|
|
131
|
-
? findAssistantByName(activeName)
|
|
132
|
-
: loadLatestAssistant();
|
|
130
|
+
const entry = resolveAssistant();
|
|
133
131
|
|
|
134
132
|
if (!entry) return false;
|
|
135
133
|
|