cvc-tui 0.4.0 → 0.4.2
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/dist/entry.js +71148 -61
- package/package.json +2 -2
- package/dist/app/completion.js +0 -102
- package/dist/app/createGatewayEventHandler.js +0 -508
- package/dist/app/createSlashHandler.js +0 -101
- package/dist/app/delegationStore.js +0 -51
- package/dist/app/gatewayContext.js +0 -17
- package/dist/app/historyStore.js +0 -123
- package/dist/app/inputBuffer.js +0 -120
- package/dist/app/inputSelectionStore.js +0 -8
- package/dist/app/inputStore.js +0 -28
- package/dist/app/interfaces.js +0 -6
- package/dist/app/overlayStore.js +0 -40
- package/dist/app/promptStore.js +0 -44
- package/dist/app/queueStore.js +0 -25
- package/dist/app/scroll.js +0 -44
- package/dist/app/setupHandoff.js +0 -28
- package/dist/app/slash/commands/core.js +0 -479
- package/dist/app/slash/commands/debug.js +0 -44
- package/dist/app/slash/commands/ops.js +0 -498
- package/dist/app/slash/commands/session.js +0 -431
- package/dist/app/slash/commands/setup.js +0 -20
- package/dist/app/slash/commands/toggles.js +0 -40
- package/dist/app/slash/registry.js +0 -18
- package/dist/app/slash/types.js +0 -1
- package/dist/app/spawnHistoryStore.js +0 -105
- package/dist/app/turnController.js +0 -650
- package/dist/app/turnStore.js +0 -48
- package/dist/app/uiStore.js +0 -36
- package/dist/app/useComposerState.js +0 -265
- package/dist/app/useConfigSync.js +0 -144
- package/dist/app/useInputHandlers.js +0 -403
- package/dist/app/useLongRunToolCharms.js +0 -50
- package/dist/app/useMainApp.js +0 -629
- package/dist/app/useSessionLifecycle.js +0 -175
- package/dist/app/useSubmission.js +0 -287
- package/dist/app.js +0 -15
- package/dist/banner.js +0 -57
- package/dist/components/agentsOverlay.js +0 -474
- package/dist/components/appChrome.js +0 -252
- package/dist/components/appLayout.js +0 -121
- package/dist/components/appOverlays.js +0 -65
- package/dist/components/branding.js +0 -97
- package/dist/components/fpsOverlay.js +0 -22
- package/dist/components/helpHint.js +0 -21
- package/dist/components/markdown.js +0 -501
- package/dist/components/maskedPrompt.js +0 -12
- package/dist/components/messageLine.js +0 -82
- package/dist/components/modelPicker.js +0 -254
- package/dist/components/overlayControls.js +0 -30
- package/dist/components/overlays/confirmPrompt.js +0 -25
- package/dist/components/overlays/helpOverlay.js +0 -76
- package/dist/components/overlays/historySearch.js +0 -49
- package/dist/components/overlays/modelPicker.js +0 -60
- package/dist/components/overlays/overlayUtils.js +0 -19
- package/dist/components/overlays/secretPrompt.js +0 -36
- package/dist/components/overlays/sessionPicker.js +0 -93
- package/dist/components/overlays/skillsHub.js +0 -71
- package/dist/components/prompts.js +0 -95
- package/dist/components/queuedMessages.js +0 -24
- package/dist/components/sessionPicker.js +0 -130
- package/dist/components/skillsHub.js +0 -165
- package/dist/components/streamingAssistant.js +0 -35
- package/dist/components/streamingMarkdown.js +0 -144
- package/dist/components/textInput.js +0 -794
- package/dist/components/themed.js +0 -12
- package/dist/components/thinking.js +0 -496
- package/dist/components/todoPanel.js +0 -40
- package/dist/components/transcript.js +0 -22
- package/dist/config/env.js +0 -18
- package/dist/config/limits.js +0 -22
- package/dist/config/timing.js +0 -18
- package/dist/content/charms.js +0 -5
- package/dist/content/faces.js +0 -21
- package/dist/content/fortunes.js +0 -29
- package/dist/content/hotkeys.js +0 -38
- package/dist/content/placeholders.js +0 -15
- package/dist/content/setup.js +0 -14
- package/dist/content/verbs.js +0 -41
- package/dist/domain/details.js +0 -53
- package/dist/domain/messages.js +0 -63
- package/dist/domain/paths.js +0 -16
- package/dist/domain/providers.js +0 -11
- package/dist/domain/roles.js +0 -6
- package/dist/domain/slash.js +0 -11
- package/dist/domain/usage.js +0 -1
- package/dist/domain/viewport.js +0 -33
- package/dist/gateway/client.js +0 -312
- package/dist/gatewayClient.js +0 -574
- package/dist/gatewayTypes.js +0 -1
- package/dist/hooks/useCompletion.js +0 -86
- package/dist/hooks/useGitBranch.js +0 -58
- package/dist/hooks/useInputHistory.js +0 -12
- package/dist/hooks/useQueue.js +0 -57
- package/dist/hooks/useVirtualHistory.js +0 -401
- package/dist/lib/circularBuffer.js +0 -43
- package/dist/lib/clipboard.js +0 -126
- package/dist/lib/editor.js +0 -41
- package/dist/lib/editor.test.js +0 -58
- package/dist/lib/emoji.js +0 -49
- package/dist/lib/externalCli.js +0 -11
- package/dist/lib/forceTruecolor.js +0 -26
- package/dist/lib/fpsStore.js +0 -36
- package/dist/lib/gracefulExit.js +0 -29
- package/dist/lib/history.js +0 -69
- package/dist/lib/inputMetrics.js +0 -143
- package/dist/lib/liveProgress.js +0 -51
- package/dist/lib/liveProgress.test.js +0 -89
- package/dist/lib/mathUnicode.js +0 -685
- package/dist/lib/memory.js +0 -123
- package/dist/lib/memoryMonitor.js +0 -76
- package/dist/lib/messages.js +0 -3
- package/dist/lib/messages.test.js +0 -25
- package/dist/lib/osc52.js +0 -53
- package/dist/lib/perfPane.js +0 -94
- package/dist/lib/platform.js +0 -312
- package/dist/lib/precisionWheel.js +0 -25
- package/dist/lib/reasoning.js +0 -39
- package/dist/lib/rpc.js +0 -26
- package/dist/lib/subagentTree.js +0 -287
- package/dist/lib/syntax.js +0 -89
- package/dist/lib/terminalModes.js +0 -46
- package/dist/lib/terminalParity.js +0 -48
- package/dist/lib/terminalSetup.js +0 -321
- package/dist/lib/text.js +0 -203
- package/dist/lib/text.test.js +0 -18
- package/dist/lib/todo.js +0 -2
- package/dist/lib/todo.test.js +0 -22
- package/dist/lib/viewportStore.js +0 -82
- package/dist/lib/virtualHeights.js +0 -61
- package/dist/lib/wheelAccel.js +0 -143
- package/dist/theme.js +0 -398
- package/dist/types.js +0 -1
package/dist/gatewayClient.js
DELETED
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { EventEmitter } from 'node:events';
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
8
|
-
import { delimiter, resolve } from 'node:path';
|
|
9
|
-
import { createInterface } from 'node:readline';
|
|
10
|
-
import { CircularBuffer } from './lib/circularBuffer.js';
|
|
11
|
-
const MAX_GATEWAY_LOG_LINES = 200;
|
|
12
|
-
const MAX_LOG_LINE_BYTES = 4096;
|
|
13
|
-
const MAX_BUFFERED_EVENTS = 2000;
|
|
14
|
-
const MAX_LOG_PREVIEW = 240;
|
|
15
|
-
const STARTUP_TIMEOUT_MS = Math.max(5000, parseInt(process.env.CVC_TUI_STARTUP_TIMEOUT_MS ?? '15000', 10) || 15000);
|
|
16
|
-
const REQUEST_TIMEOUT_MS = Math.max(30000, parseInt(process.env.CVC_TUI_RPC_TIMEOUT_MS ?? '120000', 10) || 120000);
|
|
17
|
-
const WS_CONNECTING = 0;
|
|
18
|
-
const WS_OPEN = 1;
|
|
19
|
-
const WS_CLOSING = 2;
|
|
20
|
-
const WS_CLOSED = 3;
|
|
21
|
-
const truncateLine = (line) => line.length > MAX_LOG_LINE_BYTES ? `${line.slice(0, MAX_LOG_LINE_BYTES)}… [truncated ${line.length} bytes]` : line;
|
|
22
|
-
const resolveGatewayAttachUrl = () => {
|
|
23
|
-
const raw = process.env.CVC_TUI_GATEWAY_URL?.trim() || process.env.CVC_GATEWAY_URL?.trim();
|
|
24
|
-
if (raw)
|
|
25
|
-
return raw;
|
|
26
|
-
// Default CVC FastAPI gateway port
|
|
27
|
-
const port = process.env.CVC_GATEWAY_PORT?.trim() || '13421';
|
|
28
|
-
return `http://127.0.0.1:${port}`;
|
|
29
|
-
};
|
|
30
|
-
const resolveSidecarUrl = () => {
|
|
31
|
-
const raw = process.env.CVC_TUI_SIDECAR_URL?.trim();
|
|
32
|
-
return raw ? raw : null;
|
|
33
|
-
};
|
|
34
|
-
const resolvePython = (root) => {
|
|
35
|
-
const configured = process.env.HERMES_PYTHON?.trim() || process.env.PYTHON?.trim();
|
|
36
|
-
if (configured) {
|
|
37
|
-
return configured;
|
|
38
|
-
}
|
|
39
|
-
const venv = process.env.VIRTUAL_ENV?.trim();
|
|
40
|
-
const hit = [
|
|
41
|
-
venv && resolve(venv, 'bin/python'),
|
|
42
|
-
venv && resolve(venv, 'Scripts/python.exe'),
|
|
43
|
-
resolve(root, '.venv/bin/python'),
|
|
44
|
-
resolve(root, '.venv/bin/python3'),
|
|
45
|
-
resolve(root, 'venv/bin/python'),
|
|
46
|
-
resolve(root, 'venv/bin/python3')
|
|
47
|
-
].find(p => p && existsSync(p));
|
|
48
|
-
return hit || (process.platform === 'win32' ? 'python' : 'python3');
|
|
49
|
-
};
|
|
50
|
-
const asGatewayEvent = (value) => value && typeof value === 'object' && !Array.isArray(value) && typeof value.type === 'string'
|
|
51
|
-
? value
|
|
52
|
-
: null;
|
|
53
|
-
// Hoisted decoder: attach mode can drive high-frequency binary frames
|
|
54
|
-
// (tool deltas, reasoning streams) and constructing a fresh TextDecoder
|
|
55
|
-
// per message creates avoidable GC pressure. One module-level instance
|
|
56
|
-
// is fine because UTF-8 is stateless and we always pass entire frames.
|
|
57
|
-
const _wireDecoder = new TextDecoder();
|
|
58
|
-
const asWireText = (raw) => {
|
|
59
|
-
if (typeof raw === 'string') {
|
|
60
|
-
return raw;
|
|
61
|
-
}
|
|
62
|
-
if (raw instanceof ArrayBuffer) {
|
|
63
|
-
return _wireDecoder.decode(raw);
|
|
64
|
-
}
|
|
65
|
-
if (ArrayBuffer.isView(raw)) {
|
|
66
|
-
return _wireDecoder.decode(raw);
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
};
|
|
70
|
-
// Matches `<scheme>://user:pass@host…` style user-info segments in
|
|
71
|
-
// otherwise-malformed URLs that the WHATWG `URL` parser can't accept.
|
|
72
|
-
// Used by the `redactUrl` fallback so embedded credentials are
|
|
73
|
-
// scrubbed from log lines even when the URL is unparseable.
|
|
74
|
-
const _USERINFO_FALLBACK_RE = /^([a-z][a-z0-9+.\-]*:\/\/)[^/?#@]*@/i;
|
|
75
|
-
// Connection URLs (gateway, sidecar) often carry bearer tokens in the query
|
|
76
|
-
// string. We surface them in user-facing log lines and the
|
|
77
|
-
// `gateway.start_timeout` payload, so always strip the query string and any
|
|
78
|
-
// embedded user-info before logging.
|
|
79
|
-
const redactUrl = (raw) => {
|
|
80
|
-
if (!raw) {
|
|
81
|
-
return raw;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
const url = new URL(raw);
|
|
85
|
-
const userInfo = url.username || url.password ? '***@' : '';
|
|
86
|
-
const query = url.search ? '?***' : '';
|
|
87
|
-
return `${url.protocol}//${userInfo}${url.host}${url.pathname}${query}`;
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
// WHATWG URL rejected the input. Best-effort: strip an embedded
|
|
91
|
-
// `user:pass@` segment AND the query string so a malformed token
|
|
92
|
-
// bearer can never escape into the log tail.
|
|
93
|
-
const noUserInfo = raw.replace(_USERINFO_FALLBACK_RE, '$1***@');
|
|
94
|
-
const queryIdx = noUserInfo.indexOf('?');
|
|
95
|
-
return queryIdx >= 0 ? `${noUserInfo.slice(0, queryIdx)}?***` : noUserInfo;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
export class GatewayClient extends EventEmitter {
|
|
99
|
-
proc = null;
|
|
100
|
-
ws = null;
|
|
101
|
-
wsConnectPromise = null;
|
|
102
|
-
sidecarWs = null;
|
|
103
|
-
attachUrl = null;
|
|
104
|
-
sidecarUrl = null;
|
|
105
|
-
reqId = 0;
|
|
106
|
-
logs = new CircularBuffer(MAX_GATEWAY_LOG_LINES);
|
|
107
|
-
pending = new Map();
|
|
108
|
-
bufferedEvents = new CircularBuffer(MAX_BUFFERED_EVENTS);
|
|
109
|
-
pendingExit;
|
|
110
|
-
ready = false;
|
|
111
|
-
readyTimer = null;
|
|
112
|
-
subscribed = false;
|
|
113
|
-
stdoutRl = null;
|
|
114
|
-
stderrRl = null;
|
|
115
|
-
constructor() {
|
|
116
|
-
super();
|
|
117
|
-
// useInput / createGatewayEventHandler can legitimately attach many
|
|
118
|
-
// listeners. Default 10-cap triggers spurious warnings.
|
|
119
|
-
this.setMaxListeners(0);
|
|
120
|
-
}
|
|
121
|
-
publish(ev) {
|
|
122
|
-
if (ev.type === 'gateway.ready') {
|
|
123
|
-
this.ready = true;
|
|
124
|
-
if (this.readyTimer) {
|
|
125
|
-
clearTimeout(this.readyTimer);
|
|
126
|
-
this.readyTimer = null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (this.subscribed) {
|
|
130
|
-
return void this.emit('event', ev);
|
|
131
|
-
}
|
|
132
|
-
this.bufferedEvents.push(ev);
|
|
133
|
-
}
|
|
134
|
-
clearReadyTimer() {
|
|
135
|
-
if (this.readyTimer) {
|
|
136
|
-
clearTimeout(this.readyTimer);
|
|
137
|
-
this.readyTimer = null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
closeSidecarSocket() {
|
|
141
|
-
try {
|
|
142
|
-
this.sidecarWs?.close();
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// best effort
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
this.sidecarWs = null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
closeGatewaySocket() {
|
|
152
|
-
// Null the active reference BEFORE invoking close(): real WebSocket
|
|
153
|
-
// implementations dispatch the 'close' event after a microtask hop,
|
|
154
|
-
// so by the time the handler runs `this.ws` should already be null
|
|
155
|
-
// and the identity guard will correctly classify the close as
|
|
156
|
-
// belonging to a discarded socket. (Test fakes emit synchronously,
|
|
157
|
-
// so doing the swap up front is also what makes the identity guard
|
|
158
|
-
// match real timing in tests.)
|
|
159
|
-
const ws = this.ws;
|
|
160
|
-
this.ws = null;
|
|
161
|
-
this.wsConnectPromise = null;
|
|
162
|
-
try {
|
|
163
|
-
ws?.close();
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
// best effort
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
resetStartupState() {
|
|
170
|
-
// Reject any in-flight RPCs left over from the previous transport
|
|
171
|
-
// before we swap. Otherwise the old transport's stale exit/close
|
|
172
|
-
// handlers (now identity-gated to ignore unrelated transports)
|
|
173
|
-
// never fire `rejectPending`, leaving callers hanging on promises
|
|
174
|
-
// attached to a discarded child / socket.
|
|
175
|
-
this.rejectPending(new Error('gateway restarting'));
|
|
176
|
-
this.ready = false;
|
|
177
|
-
this.bufferedEvents.clear();
|
|
178
|
-
this.pendingExit = undefined;
|
|
179
|
-
this.stdoutRl?.close();
|
|
180
|
-
this.stderrRl?.close();
|
|
181
|
-
this.stdoutRl = null;
|
|
182
|
-
this.stderrRl = null;
|
|
183
|
-
this.clearReadyTimer();
|
|
184
|
-
}
|
|
185
|
-
startReadyTimer(python, cwd) {
|
|
186
|
-
this.readyTimer = setTimeout(() => {
|
|
187
|
-
if (this.ready) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
// Append the most recent gateway stderr/log lines to the timeout
|
|
191
|
-
// event so users can tell apart "wrong python", "missing dep",
|
|
192
|
-
// and "config parse failure" from one glance instead of having
|
|
193
|
-
// to dig through `/logs`. Capped to keep the activity feed
|
|
194
|
-
// readable on slow boots.
|
|
195
|
-
const stderrTail = this.getLogTail(20);
|
|
196
|
-
this.pushLog(`[startup] timed out waiting for gateway.ready (python=${python}, cwd=${cwd})`);
|
|
197
|
-
this.publish({
|
|
198
|
-
type: 'gateway.start_timeout',
|
|
199
|
-
payload: { cwd, python, stderr_tail: stderrTail }
|
|
200
|
-
});
|
|
201
|
-
}, STARTUP_TIMEOUT_MS);
|
|
202
|
-
}
|
|
203
|
-
handleTransportExit(code, reason) {
|
|
204
|
-
this.clearReadyTimer();
|
|
205
|
-
this.closeSidecarSocket();
|
|
206
|
-
this.rejectPending(new Error(reason || `gateway exited${code === null ? '' : ` (${code})`}`));
|
|
207
|
-
if (this.subscribed) {
|
|
208
|
-
this.emit('exit', code);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
this.pendingExit = code;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
connectSidecarMirror() {
|
|
215
|
-
this.closeSidecarSocket();
|
|
216
|
-
if (!this.sidecarUrl) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (typeof WebSocket === 'undefined') {
|
|
220
|
-
this.pushLog(`[sidecar] WebSocket unavailable; skipping mirror to ${redactUrl(this.sidecarUrl)}`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
const ws = new WebSocket(this.sidecarUrl);
|
|
225
|
-
this.sidecarWs = ws;
|
|
226
|
-
ws.addEventListener('close', () => {
|
|
227
|
-
if (this.sidecarWs === ws) {
|
|
228
|
-
this.sidecarWs = null;
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
ws.addEventListener('error', () => {
|
|
232
|
-
this.pushLog('[sidecar] mirror connection error');
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
this.pushLog(`[sidecar] failed to connect ${redactUrl(this.sidecarUrl)} (constructor error)`);
|
|
237
|
-
this.sidecarWs = null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
mirrorEventToSidecar(rawFrame) {
|
|
241
|
-
const ws = this.sidecarWs;
|
|
242
|
-
if (!ws || ws.readyState !== WS_OPEN) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
try {
|
|
246
|
-
ws.send(rawFrame);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// best effort
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
handleWebSocketFrame(raw) {
|
|
253
|
-
const text = asWireText(raw);
|
|
254
|
-
if (!text) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
try {
|
|
258
|
-
const frame = JSON.parse(text);
|
|
259
|
-
if (frame.method === 'event') {
|
|
260
|
-
this.mirrorEventToSidecar(text);
|
|
261
|
-
}
|
|
262
|
-
this.dispatch(frame);
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
const preview = text.trim().slice(0, MAX_LOG_PREVIEW) || '(empty frame)';
|
|
266
|
-
this.pushLog(`[protocol] malformed websocket frame: ${preview}`);
|
|
267
|
-
this.publish({ type: 'gateway.protocol_error', payload: { preview } });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
startSpawnedGateway(root) {
|
|
271
|
-
const python = resolvePython(root);
|
|
272
|
-
const cwd = process.env.HERMES_CWD || root;
|
|
273
|
-
const env = { ...process.env };
|
|
274
|
-
const pyPath = env.PYTHONPATH?.trim();
|
|
275
|
-
env.PYTHONPATH = pyPath ? `${root}${delimiter}${pyPath}` : root;
|
|
276
|
-
this.startReadyTimer(python, cwd);
|
|
277
|
-
this.proc = spawn(python, ['-m', 'tui_gateway.entry'], { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
278
|
-
this.stdoutRl = createInterface({ input: this.proc.stdout });
|
|
279
|
-
this.stdoutRl.on('line', raw => {
|
|
280
|
-
try {
|
|
281
|
-
this.dispatch(JSON.parse(raw));
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
const preview = raw.trim().slice(0, MAX_LOG_PREVIEW) || '(empty line)';
|
|
285
|
-
this.pushLog(`[protocol] malformed stdout: ${preview}`);
|
|
286
|
-
this.publish({ type: 'gateway.protocol_error', payload: { preview } });
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
this.stderrRl = createInterface({ input: this.proc.stderr });
|
|
290
|
-
this.stderrRl.on('line', raw => {
|
|
291
|
-
const line = truncateLine(raw.trim());
|
|
292
|
-
if (!line) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
this.pushLog(line);
|
|
296
|
-
this.publish({ type: 'gateway.stderr', payload: { line } });
|
|
297
|
-
});
|
|
298
|
-
const ownedProc = this.proc;
|
|
299
|
-
this.proc.on('error', err => {
|
|
300
|
-
// Skip stale errors on an already-replaced child.
|
|
301
|
-
if (this.proc !== ownedProc) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const line = `[spawn] ${err.message}`;
|
|
305
|
-
this.pushLog(line);
|
|
306
|
-
this.publish({ type: 'gateway.stderr', payload: { line } });
|
|
307
|
-
// Detach the reference up front so the late `exit` event for
|
|
308
|
-
// this same child is identity-skipped (we don't want to emit
|
|
309
|
-
// 'exit' twice). Then run the full teardown — clears the
|
|
310
|
-
// startup timer so we don't fire a misleading
|
|
311
|
-
// `gateway.start_timeout`, rejects pending RPCs, and emits or
|
|
312
|
-
// queues a single `exit`.
|
|
313
|
-
this.proc = null;
|
|
314
|
-
this.handleTransportExit(1, `gateway error: ${err.message}`);
|
|
315
|
-
});
|
|
316
|
-
this.proc.on('exit', code => {
|
|
317
|
-
// start() can replace `this.proc` while an old child is still
|
|
318
|
-
// tearing down. Skip stale exits so we don't clear the new
|
|
319
|
-
// startup timer or reject newly-issued pending requests.
|
|
320
|
-
if (this.proc !== ownedProc) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
this.handleTransportExit(code);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
startAttachedGateway(attachUrl) {
|
|
327
|
-
const safeAttachUrl = redactUrl(attachUrl);
|
|
328
|
-
this.startReadyTimer('websocket', safeAttachUrl);
|
|
329
|
-
if (typeof WebSocket === 'undefined') {
|
|
330
|
-
const line = `[startup] WebSocket API unavailable; cannot attach to ${safeAttachUrl}`;
|
|
331
|
-
this.pushLog(line);
|
|
332
|
-
this.publish({ type: 'gateway.stderr', payload: { line } });
|
|
333
|
-
this.handleTransportExit(1, 'gateway websocket unavailable');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
try {
|
|
337
|
-
const ws = new WebSocket(attachUrl);
|
|
338
|
-
let settled = false;
|
|
339
|
-
this.ws = ws;
|
|
340
|
-
const connectPromise = new Promise((resolve, reject) => {
|
|
341
|
-
ws.addEventListener('open', () => {
|
|
342
|
-
if (!settled) {
|
|
343
|
-
settled = true;
|
|
344
|
-
resolve();
|
|
345
|
-
}
|
|
346
|
-
this.connectSidecarMirror();
|
|
347
|
-
}, { once: true });
|
|
348
|
-
ws.addEventListener('error', () => {
|
|
349
|
-
if (!settled) {
|
|
350
|
-
this.pushLog('[startup] gateway websocket connect error');
|
|
351
|
-
settled = true;
|
|
352
|
-
reject(new Error('gateway websocket connection failed'));
|
|
353
|
-
}
|
|
354
|
-
}, { once: true });
|
|
355
|
-
ws.addEventListener('close', ev => {
|
|
356
|
-
if (!settled) {
|
|
357
|
-
settled = true;
|
|
358
|
-
reject(new Error(`gateway websocket closed (${ev.code}) during connect`));
|
|
359
|
-
}
|
|
360
|
-
}, { once: true });
|
|
361
|
-
});
|
|
362
|
-
// The connect promise is only awaited by RPCs that arrive while
|
|
363
|
-
// the socket is still connecting. If no request races the open
|
|
364
|
-
// (or a teardown drops the reference before anyone observes it),
|
|
365
|
-
// a connect-error / early-close rejection would surface as an
|
|
366
|
-
// unhandled promise rejection in Node. Attach a no-op handler to
|
|
367
|
-
// ensure the rejection is always observed.
|
|
368
|
-
connectPromise.catch(() => { });
|
|
369
|
-
this.wsConnectPromise = connectPromise;
|
|
370
|
-
ws.addEventListener('message', ev => this.handleWebSocketFrame(ev.data));
|
|
371
|
-
ws.addEventListener('close', ev => {
|
|
372
|
-
// Skip close events from sockets that have already been
|
|
373
|
-
// replaced — start() / closeGatewaySocket() can swap `this.ws`
|
|
374
|
-
// before an in-flight close lands, and we must not clear the
|
|
375
|
-
// new ready timer or reject the new pending requests on behalf
|
|
376
|
-
// of a stale socket.
|
|
377
|
-
if (this.ws !== ws) {
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
this.ws = null;
|
|
381
|
-
this.wsConnectPromise = null;
|
|
382
|
-
this.handleTransportExit(ev.code, `gateway websocket closed${ev.code ? ` (${ev.code})` : ''}`);
|
|
383
|
-
});
|
|
384
|
-
ws.addEventListener('error', () => {
|
|
385
|
-
const line = '[gateway] websocket transport error';
|
|
386
|
-
this.pushLog(line);
|
|
387
|
-
this.publish({ type: 'gateway.stderr', payload: { line } });
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
catch (err) {
|
|
391
|
-
this.pushLog(`[startup] failed to connect websocket gateway ${safeAttachUrl} (constructor error)`);
|
|
392
|
-
this.handleTransportExit(1, 'gateway websocket startup failed');
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
start() {
|
|
396
|
-
const root = process.env.HERMES_PYTHON_SRC_ROOT ?? resolve(import.meta.dirname, '../../');
|
|
397
|
-
const attachUrl = resolveGatewayAttachUrl();
|
|
398
|
-
const sidecarUrl = resolveSidecarUrl();
|
|
399
|
-
this.attachUrl = attachUrl;
|
|
400
|
-
this.sidecarUrl = sidecarUrl;
|
|
401
|
-
this.resetStartupState();
|
|
402
|
-
if (this.proc && !this.proc.killed && this.proc.exitCode === null) {
|
|
403
|
-
this.proc.kill();
|
|
404
|
-
}
|
|
405
|
-
this.proc = null;
|
|
406
|
-
this.closeGatewaySocket();
|
|
407
|
-
this.closeSidecarSocket();
|
|
408
|
-
if (attachUrl) {
|
|
409
|
-
this.startAttachedGateway(attachUrl);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
this.startSpawnedGateway(root);
|
|
413
|
-
}
|
|
414
|
-
dispatch(msg) {
|
|
415
|
-
const id = msg.id;
|
|
416
|
-
const p = id ? this.pending.get(id) : undefined;
|
|
417
|
-
if (p) {
|
|
418
|
-
this.settle(p, msg.error ? this.toError(msg.error) : null, msg.result);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (msg.method === 'event') {
|
|
422
|
-
const ev = asGatewayEvent(msg.params);
|
|
423
|
-
if (ev) {
|
|
424
|
-
this.publish(ev);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
toError(raw) {
|
|
429
|
-
const err = raw;
|
|
430
|
-
return new Error(typeof err?.message === 'string' ? err.message : 'request failed');
|
|
431
|
-
}
|
|
432
|
-
settle(p, err, result) {
|
|
433
|
-
clearTimeout(p.timeout);
|
|
434
|
-
this.pending.delete(p.id);
|
|
435
|
-
if (err) {
|
|
436
|
-
p.reject(err);
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
p.resolve(result);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
pushLog(line) {
|
|
443
|
-
this.logs.push(truncateLine(line));
|
|
444
|
-
}
|
|
445
|
-
rejectPending(err) {
|
|
446
|
-
for (const p of this.pending.values()) {
|
|
447
|
-
clearTimeout(p.timeout);
|
|
448
|
-
p.reject(err);
|
|
449
|
-
}
|
|
450
|
-
this.pending.clear();
|
|
451
|
-
}
|
|
452
|
-
// Arrow class-field — stable identity, so `setTimeout(this.onTimeout, …, id)`
|
|
453
|
-
// doesn't allocate a bound function per request.
|
|
454
|
-
onTimeout = (id) => {
|
|
455
|
-
const p = this.pending.get(id);
|
|
456
|
-
if (p) {
|
|
457
|
-
this.pending.delete(id);
|
|
458
|
-
p.reject(new Error(`timeout: ${p.method}`));
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
drain() {
|
|
462
|
-
this.subscribed = true;
|
|
463
|
-
for (const ev of this.bufferedEvents.drain()) {
|
|
464
|
-
this.emit('event', ev);
|
|
465
|
-
}
|
|
466
|
-
if (this.pendingExit !== undefined) {
|
|
467
|
-
const code = this.pendingExit;
|
|
468
|
-
this.pendingExit = undefined;
|
|
469
|
-
this.emit('exit', code);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
getLogTail(limit = 20) {
|
|
473
|
-
return this.logs.tail(Math.max(1, limit)).join('\n');
|
|
474
|
-
}
|
|
475
|
-
async ensureAttachedWebSocket(method) {
|
|
476
|
-
if (!this.attachUrl) {
|
|
477
|
-
throw new Error('gateway not running');
|
|
478
|
-
}
|
|
479
|
-
if (!this.ws || this.ws.readyState === WS_CLOSED || this.ws.readyState === WS_CLOSING) {
|
|
480
|
-
this.start();
|
|
481
|
-
}
|
|
482
|
-
if (this.ws?.readyState === WS_CONNECTING) {
|
|
483
|
-
try {
|
|
484
|
-
await this.wsConnectPromise;
|
|
485
|
-
}
|
|
486
|
-
catch (err) {
|
|
487
|
-
throw err instanceof Error ? err : new Error(String(err));
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (!this.ws || this.ws.readyState !== WS_OPEN) {
|
|
491
|
-
throw new Error(`gateway not connected: ${method}`);
|
|
492
|
-
}
|
|
493
|
-
return this.ws;
|
|
494
|
-
}
|
|
495
|
-
requestOverWebSocket(method, params = {}) {
|
|
496
|
-
return this.ensureAttachedWebSocket(method).then(ws => new Promise((resolve, reject) => {
|
|
497
|
-
const id = `r${++this.reqId}`;
|
|
498
|
-
const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id);
|
|
499
|
-
timeout.unref?.();
|
|
500
|
-
this.pending.set(id, {
|
|
501
|
-
id,
|
|
502
|
-
method,
|
|
503
|
-
reject,
|
|
504
|
-
resolve: v => resolve(v),
|
|
505
|
-
timeout
|
|
506
|
-
});
|
|
507
|
-
try {
|
|
508
|
-
ws.send(JSON.stringify({ id, jsonrpc: '2.0', method, params }));
|
|
509
|
-
}
|
|
510
|
-
catch (e) {
|
|
511
|
-
const pending = this.pending.get(id);
|
|
512
|
-
if (pending) {
|
|
513
|
-
clearTimeout(pending.timeout);
|
|
514
|
-
this.pending.delete(id);
|
|
515
|
-
}
|
|
516
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
517
|
-
}
|
|
518
|
-
}));
|
|
519
|
-
}
|
|
520
|
-
request(method, params = {}) {
|
|
521
|
-
const attachUrl = resolveGatewayAttachUrl();
|
|
522
|
-
if (attachUrl) {
|
|
523
|
-
if (this.attachUrl !== attachUrl) {
|
|
524
|
-
// The env var rotated at runtime — restart the transport so
|
|
525
|
-
// switching from spawned-gateway mode to attach mode also
|
|
526
|
-
// tears down the old Python child. Merely closing `this.ws`
|
|
527
|
-
// would leave a previously spawned gateway process alive.
|
|
528
|
-
this.rejectPending(new Error('gateway attach url changed'));
|
|
529
|
-
this.start();
|
|
530
|
-
}
|
|
531
|
-
return this.requestOverWebSocket(method, params);
|
|
532
|
-
}
|
|
533
|
-
if (!this.proc?.stdin || this.proc.killed || this.proc.exitCode !== null) {
|
|
534
|
-
this.start();
|
|
535
|
-
}
|
|
536
|
-
if (!this.proc?.stdin) {
|
|
537
|
-
return Promise.reject(new Error('gateway not running'));
|
|
538
|
-
}
|
|
539
|
-
const id = `r${++this.reqId}`;
|
|
540
|
-
return new Promise((resolve, reject) => {
|
|
541
|
-
const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id);
|
|
542
|
-
timeout.unref?.();
|
|
543
|
-
this.pending.set(id, {
|
|
544
|
-
id,
|
|
545
|
-
method,
|
|
546
|
-
reject,
|
|
547
|
-
resolve: v => resolve(v),
|
|
548
|
-
timeout
|
|
549
|
-
});
|
|
550
|
-
try {
|
|
551
|
-
this.proc.stdin.write(JSON.stringify({ id, jsonrpc: '2.0', method, params }) + '\n');
|
|
552
|
-
}
|
|
553
|
-
catch (e) {
|
|
554
|
-
const pending = this.pending.get(id);
|
|
555
|
-
if (pending) {
|
|
556
|
-
clearTimeout(pending.timeout);
|
|
557
|
-
this.pending.delete(id);
|
|
558
|
-
}
|
|
559
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
kill() {
|
|
564
|
-
this.proc?.kill();
|
|
565
|
-
this.closeGatewaySocket();
|
|
566
|
-
this.closeSidecarSocket();
|
|
567
|
-
this.clearReadyTimer();
|
|
568
|
-
// The ws 'close' handler is identity-gated on `this.ws === ws`
|
|
569
|
-
// and we just nulled `this.ws`, so it will short-circuit and
|
|
570
|
-
// skip handleTransportExit. Reject pending RPCs explicitly so
|
|
571
|
-
// attach-mode promises do not hang after an intentional kill.
|
|
572
|
-
this.rejectPending(new Error('gateway closed'));
|
|
573
|
-
}
|
|
574
|
-
}
|
package/dist/gatewayTypes.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// SPDX-License-Identifier: MIT
|
|
3
|
-
// Ported from CVC Agent (https://github.com/NousResearch/cvc)
|
|
4
|
-
// Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
|
|
5
|
-
import { useEffect, useRef, useState } from 'react';
|
|
6
|
-
import { looksLikeSlashCommand } from '../domain/slash.js';
|
|
7
|
-
import { asRpcResult } from '../lib/rpc.js';
|
|
8
|
-
const TAB_PATH_RE = /((?:["']?(?:[A-Za-z]:[\\/]|\.{1,2}\/|~\/|\/|@|[^"'`\s]+\/))[^\s]*)$/;
|
|
9
|
-
export function completionRequestForInput(input) {
|
|
10
|
-
const isSlashCommand = looksLikeSlashCommand(input);
|
|
11
|
-
const pathWord = isSlashCommand ? null : (input.match(TAB_PATH_RE)?.[1] ?? null);
|
|
12
|
-
if (!isSlashCommand && !pathWord) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
// `/model` uses the two-step ModelPicker (real curated IDs).
|
|
16
|
-
// Slash completion here only showed short aliases + vendor/family meta.
|
|
17
|
-
if (isSlashCommand && /^\/model(?:\s|$)/.test(input)) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
if (isSlashCommand) {
|
|
21
|
-
return { method: 'complete.slash', params: { text: input }, replaceFrom: 1 };
|
|
22
|
-
}
|
|
23
|
-
return {
|
|
24
|
-
method: 'complete.path',
|
|
25
|
-
params: { word: pathWord },
|
|
26
|
-
replaceFrom: input.length - pathWord.length
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
export function useCompletion(input, blocked, gw) {
|
|
30
|
-
const [completions, setCompletions] = useState([]);
|
|
31
|
-
const [compIdx, setCompIdx] = useState(0);
|
|
32
|
-
const [compReplace, setCompReplace] = useState(0);
|
|
33
|
-
const ref = useRef('');
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
const clear = () => {
|
|
36
|
-
setCompletions(prev => (prev.length ? [] : prev));
|
|
37
|
-
setCompIdx(prev => (prev ? 0 : prev));
|
|
38
|
-
setCompReplace(prev => (prev ? 0 : prev));
|
|
39
|
-
};
|
|
40
|
-
if (blocked) {
|
|
41
|
-
ref.current = '';
|
|
42
|
-
clear();
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (input === ref.current) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
ref.current = input;
|
|
49
|
-
const request = completionRequestForInput(input);
|
|
50
|
-
if (!request) {
|
|
51
|
-
clear();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const t = setTimeout(() => {
|
|
55
|
-
if (ref.current !== input) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
gw.request(request.method, request.params)
|
|
59
|
-
.then(raw => {
|
|
60
|
-
if (ref.current !== input) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const r = asRpcResult(raw);
|
|
64
|
-
setCompletions(r?.items ?? []);
|
|
65
|
-
setCompIdx(0);
|
|
66
|
-
setCompReplace(request.method === 'complete.slash' ? (r?.replace_from ?? 1) : request.replaceFrom);
|
|
67
|
-
})
|
|
68
|
-
.catch((e) => {
|
|
69
|
-
if (ref.current !== input) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
setCompletions([
|
|
73
|
-
{
|
|
74
|
-
text: '',
|
|
75
|
-
display: 'completion unavailable',
|
|
76
|
-
meta: e instanceof Error && e.message ? e.message : 'unavailable'
|
|
77
|
-
}
|
|
78
|
-
]);
|
|
79
|
-
setCompIdx(0);
|
|
80
|
-
setCompReplace(request.replaceFrom);
|
|
81
|
-
});
|
|
82
|
-
}, 60);
|
|
83
|
-
return () => clearTimeout(t);
|
|
84
|
-
}, [blocked, gw, input]);
|
|
85
|
-
return { completions, compIdx, setCompIdx, compReplace };
|
|
86
|
-
}
|