@myagentroam/node 0.12.0 → 0.12.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.
Files changed (132) hide show
  1. package/dist/code-navigation/index.js +2 -0
  2. package/dist/code-navigation/languages/c-family.js +9 -0
  3. package/dist/code-navigation/languages/c-preprocessor.js +30 -0
  4. package/dist/code-navigation/languages/c.js +8 -0
  5. package/dist/code-navigation/languages/cpp.js +10 -0
  6. package/dist/code-navigation/languages/csharp.js +18 -0
  7. package/dist/code-navigation/languages/css.js +7 -0
  8. package/dist/code-navigation/languages/ecmascript.js +44 -0
  9. package/dist/code-navigation/languages/file-target.js +9 -0
  10. package/dist/code-navigation/languages/go.js +32 -0
  11. package/dist/code-navigation/languages/html.js +7 -0
  12. package/dist/code-navigation/languages/jvm.js +17 -0
  13. package/dist/code-navigation/languages/less.js +7 -0
  14. package/dist/code-navigation/languages/markdown.js +113 -0
  15. package/dist/code-navigation/languages/markup-reference.js +27 -0
  16. package/dist/code-navigation/languages/objective-c.js +11 -0
  17. package/dist/code-navigation/languages/objective-cpp.js +10 -0
  18. package/dist/code-navigation/languages/python.js +17 -0
  19. package/dist/code-navigation/languages/rust.js +18 -0
  20. package/dist/code-navigation/languages/sass.js +7 -0
  21. package/dist/code-navigation/languages/scss.js +7 -0
  22. package/dist/code-navigation/languages/stylesheet-reference.js +37 -0
  23. package/dist/code-navigation/languages/vue.js +75 -0
  24. package/dist/code-navigation/registry.js +72 -0
  25. package/dist/code-navigation/source-ranges.js +17 -0
  26. package/dist/code-navigation/symbol-maps.js +257 -0
  27. package/dist/code-navigation/symbol-matcher.js +48 -0
  28. package/dist/code-navigation/symbol-navigation.js +1 -0
  29. package/dist/code-navigation/types.js +1 -0
  30. package/dist/ctags-binary.js +3 -3
  31. package/dist/marctl/bin/marctl.mjs +6 -0
  32. package/dist/marctl/bin/marnodes.mjs +6 -0
  33. package/dist/marctl/bin/marscp.mjs +6 -0
  34. package/dist/marctl/bin/marssh.mjs +6 -0
  35. package/dist/marctl/package.json +1 -0
  36. package/dist/marctl/src/cli.mjs +271 -0
  37. package/dist/marctl/src/client.mjs +291 -0
  38. package/dist/marctl/src/config.mjs +46 -0
  39. package/dist/marctl/src/direct-control.mjs +225 -0
  40. package/dist/marctl/src/direct-errors.mjs +48 -0
  41. package/dist/marctl/src/direct-process.mjs +374 -0
  42. package/dist/marctl/src/direct-transport.mjs +235 -0
  43. package/dist/marctl/src/direct.mjs +421 -0
  44. package/dist/marctl/src/errors.mjs +11 -0
  45. package/dist/marctl/src/i18n/en-US.mjs +285 -0
  46. package/dist/marctl/src/i18n/index.mjs +20 -0
  47. package/dist/marctl/src/i18n/zh-CN.mjs +269 -0
  48. package/dist/marctl/src/scp-channel.mjs +99 -0
  49. package/dist/marctl/src/scp-copy.mjs +46 -0
  50. package/dist/marctl/src/scp-endpoint.mjs +29 -0
  51. package/dist/marctl/src/scp-protocol.mjs +2 -0
  52. package/dist/marctl/src/scp-worker.cjs +266 -0
  53. package/dist/marctl/src/scp-workers.mjs +179 -0
  54. package/dist/marctl/src/scp.mjs +64 -0
  55. package/dist/marctl/src/session-args.mjs +233 -0
  56. package/dist/marctl/src/session-input.mjs +43 -0
  57. package/dist/marctl/src/session-prepare.mjs +87 -0
  58. package/dist/marctl/src/session-target.mjs +26 -0
  59. package/dist/marctl/src/session-wait.mjs +232 -0
  60. package/dist/marctl/src/sessions.mjs +93 -0
  61. package/dist/marctl/src/terminal-frame.mjs +48 -0
  62. package/dist/marctl/src/terminal-interaction.mjs +177 -0
  63. package/dist/marctl/src/terminal-race.mjs +117 -0
  64. package/dist/marctl/src/terminal-relay-sender.mjs +129 -0
  65. package/dist/marctl/src/terminal-relay.mjs +305 -0
  66. package/dist/marctl/src/terminal.mjs +152 -0
  67. package/dist/marctl.js +1 -1
  68. package/dist/marnodes.js +1 -1
  69. package/dist/marscp.js +1 -1
  70. package/dist/marssh.js +1 -1
  71. package/dist/ripgrep-binary.js +3 -3
  72. package/dist/runner/mar-agent/agent-operation-presentation.js +22 -10
  73. package/dist/service/code-navigation/file-reference-parser.js +1 -1
  74. package/dist/service/node-terminal-channel.js +1 -1
  75. package/dist/session/session-catalog-service.js +4 -1
  76. package/dist/workspace/workspace-code-navigation-service.js +2 -2
  77. package/package.json +11 -9
  78. package/vendor/ctags/NOTICE +0 -22
  79. package/vendor/ctags/darwin-arm64/ctags +0 -0
  80. package/vendor/ctags/darwin-x64/ctags +0 -0
  81. package/vendor/ctags/licenses/COPYING +0 -340
  82. package/vendor/ctags/licenses/COPYING.MinGW-w64-runtime.txt +0 -240
  83. package/vendor/ctags/licenses/LICENSE.janssen +0 -19
  84. package/vendor/ctags/licenses/LICENSE.libyaml +0 -19
  85. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/COPYING +0 -340
  86. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/RECEIPT.yml +0 -27
  87. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libiconv.yml +0 -24
  88. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libjansson.yml +0 -19
  89. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libxml2.yml +0 -20
  90. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libyaml.yml +0 -7
  91. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libz.yml +0 -13
  92. package/vendor/ctags/licenses/official-snapshot/darwin-x64/COPYING +0 -340
  93. package/vendor/ctags/licenses/official-snapshot/darwin-x64/RECEIPT.yml +0 -27
  94. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libiconv.yml +0 -24
  95. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libjansson.yml +0 -19
  96. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libxml2.yml +0 -20
  97. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libyaml.yml +0 -7
  98. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libz.yml +0 -13
  99. package/vendor/ctags/licenses/official-snapshot/linux-arm64/COPYING +0 -340
  100. package/vendor/ctags/licenses/official-snapshot/linux-arm64/RECEIPT.yml +0 -27
  101. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libiconv.yml +0 -24
  102. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libjansson.yml +0 -19
  103. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libxml2.yml +0 -20
  104. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libyaml.yml +0 -7
  105. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libz.yml +0 -13
  106. package/vendor/ctags/licenses/official-snapshot/linux-x64/COPYING +0 -340
  107. package/vendor/ctags/licenses/official-snapshot/linux-x64/RECEIPT.yml +0 -27
  108. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libiconv.yml +0 -24
  109. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libjansson.yml +0 -19
  110. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libxml2.yml +0 -20
  111. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libyaml.yml +0 -7
  112. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libz.yml +0 -13
  113. package/vendor/ctags/licenses/win32-x64/COPYING +0 -340
  114. package/vendor/ctags/licenses/win32-x64/COPYING.MinGW-w64-runtime.txt +0 -240
  115. package/vendor/ctags/licenses/win32-x64/Copyright.libxml2 +0 -23
  116. package/vendor/ctags/licenses/win32-x64/LICENCE.pcre2 +0 -94
  117. package/vendor/ctags/licenses/win32-x64/LICENSE.janssen +0 -19
  118. package/vendor/ctags/licenses/win32-x64/LICENSE.libyaml +0 -19
  119. package/vendor/ctags/linux-arm64/ctags +0 -0
  120. package/vendor/ctags/linux-x64/ctags +0 -0
  121. package/vendor/ctags/manifest.json +0 -42
  122. package/vendor/ctags/source/universal-ctags-ab95af198f8eaeb6d50dc96e3adf52e769d3e5f0.tar.gz +0 -0
  123. package/vendor/ctags/win32-x64/ctags.exe +0 -0
  124. package/vendor/ripgrep/LICENSE-MIT +0 -21
  125. package/vendor/ripgrep/UNLICENSE +0 -24
  126. package/vendor/ripgrep/darwin-arm64/rg +0 -0
  127. package/vendor/ripgrep/darwin-x64/rg +0 -0
  128. package/vendor/ripgrep/linux-arm64/rg +0 -0
  129. package/vendor/ripgrep/linux-x64/rg +0 -0
  130. package/vendor/ripgrep/manifest.json +0 -42
  131. package/vendor/ripgrep/win32-arm64/rg.exe +0 -0
  132. package/vendor/ripgrep/win32-x64/rg.exe +0 -0
@@ -0,0 +1,93 @@
1
+ import { MarctlError } from './errors.mjs';
2
+ import { sessionTarget } from './session-target.mjs';
3
+ import {
4
+ isAgentNetworkAction,
5
+ prepareAgentNetworkRequest,
6
+ prepareSessionRequest
7
+ } from './session-prepare.mjs';
8
+ import { waitForSessionRun } from './session-wait.mjs';
9
+
10
+ export async function runSessionCommand({
11
+ args,
12
+ client,
13
+ env,
14
+ stdin,
15
+ WebSocketClass,
16
+ translate,
17
+ signal
18
+ }) {
19
+ args = { ...args, ...(args.images === undefined ? {} : { images: [...args.images] }) };
20
+ const targeted = sessionTarget[args.action] !== 'workspace';
21
+ const sessionId =
22
+ args.sessionId ??
23
+ (sessionTarget[args.action] === 'current' ? env.MAR_CTL_SESSION_ID : undefined);
24
+ if (targeted && !sessionId && sessionTarget[args.action] !== 'optional')
25
+ throw new MarctlError(translate('errors.sessionContextRequired'), {
26
+ code: 'MARCTL_SESSION_CONTEXT_REQUIRED'
27
+ });
28
+ if (args.action === 'wait' && args.runId !== undefined)
29
+ return waitForSessionRun({
30
+ client,
31
+ sessionId,
32
+ runId: args.runId,
33
+ timeoutMs: args.timeoutMs ?? 30000,
34
+ until: args.until,
35
+ WebSocketClass,
36
+ translate,
37
+ signal
38
+ });
39
+ const network = isAgentNetworkAction(args.action);
40
+ const body = network
41
+ ? await prepareAgentNetworkRequest({ args, stdin, translate, sessionId })
42
+ : await prepareSessionRequest({ args, env, stdin, translate, sessionId, targeted });
43
+ const clientMessageId = network ? body.command.input.clientMessageId : body.clientMessageId;
44
+ try {
45
+ const requestedWait = args.timeoutMs ?? 30000;
46
+ const effectiveWait = requestedWait === 0 ? 0 : Math.max(10000, requestedWait);
47
+ const result = await client.sessionCommand(
48
+ body,
49
+ args.action === 'wait' ? { timeoutMs: effectiveWait + 10000, signal } : undefined
50
+ );
51
+ return {
52
+ ...result,
53
+ ...(typeof result?.session?.id === 'string' ? { sessionId: result.session.id } : {}),
54
+ ...(typeof result?.run?.id === 'string' ? { runId: result.run.id } : {}),
55
+ ...(clientMessageId === undefined ? {} : { clientMessageId })
56
+ };
57
+ } catch (error) {
58
+ if (clientMessageId !== undefined) error.clientMessageId = clientMessageId;
59
+ if (
60
+ [
61
+ 'start',
62
+ 'send',
63
+ 'config',
64
+ 'rename',
65
+ 'prompt.set',
66
+ 'prompt.clear',
67
+ 'approval.decide',
68
+ 'schedule.create',
69
+ 'schedule.update',
70
+ 'stop',
71
+ 'goal.set',
72
+ 'goal.clear'
73
+ ].includes(args.action) &&
74
+ ['MARCTL_REQUEST_TIMEOUT', 'MARCTL_SERVER_UNREACHABLE', 'MARCTL_RESPONSE_INVALID'].includes(
75
+ error.code
76
+ )
77
+ ) {
78
+ error.code = 'MARCTL_OUTCOME_UNKNOWN';
79
+ error.message = translate('errors.sessionOutcomeUnknown');
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ export function sessionExitCode(error) {
86
+ if (error.code === 'MARCTL_SESSION_INPUT_INVALID') return 2;
87
+ if ([401, 403].includes(error.statusCode)) return 3;
88
+ if (error.code?.includes('UNSUPPORTED')) return 4;
89
+ if (error.code === 'MARCTL_WAIT_TIMEOUT' || error.code === 'MARCTL_REQUEST_TIMEOUT') return 5;
90
+ if (['MARCTL_OUTCOME_UNKNOWN', 'MARCTL_WAIT_UNKNOWN'].includes(error.code)) return 6;
91
+ if (error.code === 'MARCTL_WAIT_CANCELLED') return 130;
92
+ return 1;
93
+ }
@@ -0,0 +1,48 @@
1
+ import { MarctlError } from './errors.mjs';
2
+
3
+ const MAX_INPUT_BYTES = 65536;
4
+
5
+ export function parseFrame(raw, isBinary, terminalId, translate) {
6
+ if (isBinary) throw new MarctlError(translate('errors.terminalBinaryFrame'));
7
+ let frame;
8
+ try {
9
+ frame = JSON.parse(rawToString(raw));
10
+ } catch {
11
+ throw new MarctlError(translate('errors.terminalInvalidJson'));
12
+ }
13
+ if (frame === null || typeof frame !== 'object' || frame.terminalId !== terminalId)
14
+ throw new MarctlError(translate('errors.terminalFrameInvalid'));
15
+
16
+ if (frame.type === 'attach') {
17
+ if (!Number.isSafeInteger(frame.afterSequence) || frame.afterSequence < 0)
18
+ throw new MarctlError(translate('errors.terminalFrameInvalid'));
19
+ return frame;
20
+ }
21
+ if (frame.type === 'output') {
22
+ if (
23
+ !Number.isSafeInteger(frame.sequence) ||
24
+ frame.sequence < 1 ||
25
+ typeof frame.data !== 'string' ||
26
+ Buffer.byteLength(frame.data) > MAX_INPUT_BYTES
27
+ )
28
+ throw new MarctlError(translate('errors.terminalFrameInvalid'));
29
+ return frame;
30
+ }
31
+ if (frame.type === 'exit') {
32
+ if (frame.exitCode !== null && !Number.isSafeInteger(frame.exitCode))
33
+ throw new MarctlError(translate('errors.terminalFrameInvalid'));
34
+ return frame;
35
+ }
36
+ if (frame.type === 'error' && typeof frame.code === 'string' && frame.code.length > 0)
37
+ return frame;
38
+ throw new MarctlError(translate('errors.terminalFrameInvalid'));
39
+ }
40
+
41
+ function rawToString(raw) {
42
+ if (typeof raw === 'string') return raw;
43
+ if (Buffer.isBuffer(raw)) return raw.toString('utf8');
44
+ if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
45
+ if (ArrayBuffer.isView(raw))
46
+ return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
47
+ return String(raw);
48
+ }
@@ -0,0 +1,177 @@
1
+ import { MarctlError } from './errors.mjs';
2
+ import { DirectRejectedError } from './direct-errors.mjs';
3
+ import { parseFrame } from './terminal-frame.mjs';
4
+
5
+ const MAX_INPUT_BYTES = 65536;
6
+ const DEFAULT_ROWS = 24;
7
+ const DEFAULT_COLS = 80;
8
+
9
+ export function splitInput(text, maxBytes = MAX_INPUT_BYTES) {
10
+ const value = Buffer.isBuffer(text) ? text.toString('utf8') : String(text);
11
+ const chunks = [];
12
+ let chunk = '';
13
+ let chunkBytes = 0;
14
+
15
+ for (const character of value) {
16
+ const characterBytes = Buffer.byteLength(character);
17
+ if (chunk !== '' && chunkBytes + characterBytes > maxBytes) {
18
+ chunks.push(chunk);
19
+ chunk = '';
20
+ chunkBytes = 0;
21
+ }
22
+ if (characterBytes > maxBytes) continue;
23
+ chunk += character;
24
+ chunkBytes += characterBytes;
25
+ }
26
+ if (chunk !== '') chunks.push(chunk);
27
+ return chunks;
28
+ }
29
+
30
+ export function runAttachedTerminal({
31
+ transport,
32
+ terminalId,
33
+ commandMode,
34
+ stdin,
35
+ stdout,
36
+ signal,
37
+ translate
38
+ }) {
39
+ return new Promise((resolve, reject) => {
40
+ let settled = false;
41
+ let inputEnabled = false;
42
+ let rawModeEnabled = false;
43
+ let inputTasks = 0;
44
+
45
+ const cleanup = () => {
46
+ transport.removeListener?.('message', onMessage);
47
+ transport.removeListener?.('error', onError);
48
+ transport.removeListener?.('close', onClose);
49
+ stdin.removeListener?.('data', onInput);
50
+ stdout.removeListener?.('resize', onResize);
51
+ signal?.removeEventListener('abort', onAbort);
52
+ if (rawModeEnabled) {
53
+ try {
54
+ stdin.setRawMode(false);
55
+ } catch {
56
+ // The terminal may already be gone.
57
+ }
58
+ }
59
+ stdin.pause?.();
60
+ };
61
+
62
+ const finish = (error, exitCode) => {
63
+ if (settled) return;
64
+ settled = true;
65
+ cleanup();
66
+ sendCloseFrame(transport, terminalId);
67
+ transport.close?.();
68
+ if (error === undefined) resolve(exitCode);
69
+ else reject(error);
70
+ };
71
+ const fail = (key) => finish(new MarctlError(translate(key)));
72
+ const send = (frame) => {
73
+ return transport.send(JSON.stringify(frame));
74
+ };
75
+ const onInput = async (data) => {
76
+ inputTasks += 1;
77
+ stdin.pause?.();
78
+ try {
79
+ for (const chunk of splitInput(data)) {
80
+ if (settled) return;
81
+ await send({ type: 'input', terminalId, data: chunk });
82
+ }
83
+ } catch {
84
+ fail('errors.terminalInputFailed');
85
+ } finally {
86
+ inputTasks -= 1;
87
+ if (!settled && inputTasks === 0) stdin.resume?.();
88
+ }
89
+ };
90
+ const onResize = () => {
91
+ try {
92
+ const { rows, cols } = getDimensions(stdout);
93
+ Promise.resolve(send({ type: 'resize', terminalId, rows, cols })).catch(() =>
94
+ fail('errors.terminalResizeFailed')
95
+ );
96
+ } catch {
97
+ fail('errors.terminalResizeFailed');
98
+ }
99
+ };
100
+ const onMessage = (raw, isBinary) => {
101
+ try {
102
+ const frame = parseFrame(raw, isBinary, terminalId, translate);
103
+ if (frame.type === 'output') stdout.write(frame.data);
104
+ else if (frame.type === 'exit') {
105
+ finish(undefined, frame.exitCode === null ? 1 : frame.exitCode);
106
+ } else if (frame.type === 'error') fail('errors.terminalRemoteError');
107
+ else fail('errors.terminalUnexpectedFrame');
108
+ } catch (error) {
109
+ finish(
110
+ error instanceof MarctlError
111
+ ? error
112
+ : new MarctlError(translate('errors.terminalFrameInvalid'))
113
+ );
114
+ }
115
+ };
116
+ const onError = (error) =>
117
+ finish(
118
+ error instanceof DirectRejectedError
119
+ ? new MarctlError(`${translate('errors.terminalConnectionFailed')} [${error.code}]`, {
120
+ code: error.code
121
+ })
122
+ : new MarctlError(translate('errors.terminalConnectionFailed'))
123
+ );
124
+ const onClose = () => finish(new MarctlError(translate('errors.terminalConnectionLost')));
125
+ const onAbort = () => finish(undefined, signal.reason === 'SIGTERM' ? 143 : 130);
126
+ const startInput = () => {
127
+ if (settled || inputEnabled) return;
128
+ inputEnabled = true;
129
+ try {
130
+ if (!commandMode) {
131
+ if (typeof stdin.setRawMode !== 'function') {
132
+ fail('errors.terminalInteractiveRequired');
133
+ return;
134
+ }
135
+ stdin.setRawMode(true);
136
+ rawModeEnabled = true;
137
+ }
138
+ stdin.resume?.();
139
+ stdin.on?.('data', onInput);
140
+ stdout.on?.('resize', onResize);
141
+ onResize();
142
+ } catch {
143
+ fail(commandMode ? 'errors.terminalSessionFailed' : 'errors.terminalInteractiveInit');
144
+ }
145
+ };
146
+ transport.on?.('message', onMessage);
147
+ transport.on?.('error', onError);
148
+ transport.on?.('close', onClose);
149
+ signal?.addEventListener('abort', onAbort, { once: true });
150
+ if (signal?.aborted) {
151
+ onAbort();
152
+ return;
153
+ }
154
+ transport.activate?.();
155
+ startInput();
156
+ });
157
+ }
158
+
159
+ function sendCloseFrame(socket, terminalId) {
160
+ if (!terminalId || !isSocketOpen(socket)) return;
161
+ try {
162
+ Promise.resolve(socket.send(JSON.stringify({ type: 'close', terminalId }))).catch(() => {});
163
+ } catch {
164
+ // The DELETE request below remains the authoritative cleanup path.
165
+ }
166
+ }
167
+
168
+ function isSocketOpen(socket) {
169
+ return socket.readyState === undefined || socket.readyState === 1;
170
+ }
171
+
172
+ export function getDimensions(stdout) {
173
+ const rows = Number.isSafeInteger(stdout.rows) && stdout.rows > 0 ? stdout.rows : DEFAULT_ROWS;
174
+ const cols =
175
+ Number.isSafeInteger(stdout.columns) && stdout.columns > 0 ? stdout.columns : DEFAULT_COLS;
176
+ return { rows: Math.min(rows, 1000), cols: Math.min(cols, 1000) };
177
+ }
@@ -0,0 +1,117 @@
1
+ import { prepareRelayTransport } from './terminal-relay.mjs';
2
+ import { DirectFallbackError, DirectRejectedError } from './direct-errors.mjs';
3
+
4
+ export async function raceTerminalConnections({
5
+ openDirectTerminal,
6
+ directOptions,
7
+ client,
8
+ WebSocketClass,
9
+ nodeId,
10
+ terminalId,
11
+ translate,
12
+ connectionTimeoutMs,
13
+ signal
14
+ }) {
15
+ if (signal?.aborted) throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
16
+ const abort = new AbortController();
17
+ const relayAbort = new AbortController();
18
+ const preparing = new Set(['direct', 'relay']);
19
+ let start;
20
+ const preparation = new Promise((resolve) => {
21
+ start = resolve;
22
+ });
23
+ const prepared = (candidate) => {
24
+ preparing.delete(candidate);
25
+ if (preparing.size === 0) start();
26
+ return preparation;
27
+ };
28
+ let selected;
29
+ let announce;
30
+ const first = new Promise((resolve) => {
31
+ announce = resolve;
32
+ });
33
+ const select = (candidate) => {
34
+ if (selected) return false;
35
+ selected = candidate;
36
+ announce(candidate);
37
+ if (candidate === 'relay') abort.abort();
38
+ else relayAbort.abort();
39
+ return true;
40
+ };
41
+ const cancel = () => {
42
+ abort.abort();
43
+ relayAbort.abort();
44
+ if (selected) return;
45
+ selected = 'cancel';
46
+ start();
47
+ announce('cancel');
48
+ };
49
+ signal?.addEventListener('abort', cancel, { once: true });
50
+ const direct = openDirectTerminal({
51
+ ...directOptions,
52
+ signal: abort.signal,
53
+ onPrepared: () => prepared('direct'),
54
+ onRejected: () => select('direct'),
55
+ onIceConnected: () => {
56
+ if (!select('direct')) throw new Error('DIRECT_RACE_CANCELLED');
57
+ },
58
+ onTransportReady: () => {
59
+ if (selected !== 'direct') throw new Error('DIRECT_RACE_CANCELLED');
60
+ }
61
+ }).then(
62
+ (value) => {
63
+ if (selected === 'cancel' || signal?.aborted) value.close();
64
+ return { value };
65
+ },
66
+ (error) => {
67
+ prepared('direct');
68
+ if (error instanceof DirectRejectedError) select('direct');
69
+ return { error };
70
+ }
71
+ );
72
+ const relay = prepareRelayTransport({
73
+ client,
74
+ WebSocketClass,
75
+ nodeId,
76
+ terminalId,
77
+ translate,
78
+ connectionTimeoutMs,
79
+ signal: relayAbort.signal,
80
+ onPrepared: () => prepared('relay')
81
+ }).then(
82
+ (value) => {
83
+ if (!select('relay')) value.close();
84
+ return { value };
85
+ },
86
+ (error) => {
87
+ prepared('relay');
88
+ return { error };
89
+ }
90
+ );
91
+ const winner = await Promise.race([
92
+ first,
93
+ Promise.all([direct, relay]).then(() => selected ?? 'none')
94
+ ]);
95
+ if (winner === 'cancel' || signal?.aborted) {
96
+ signal?.removeEventListener('abort', cancel);
97
+ abort.abort();
98
+ relayAbort.abort();
99
+ throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
100
+ }
101
+ if (winner === 'direct') {
102
+ const result = await direct;
103
+ signal?.removeEventListener('abort', cancel);
104
+ if (result.error) throw result.error;
105
+ return result.value;
106
+ }
107
+ if (winner === 'relay') {
108
+ const result = await relay;
109
+ signal?.removeEventListener('abort', cancel);
110
+ if (result.error) throw result.error;
111
+ return result.value.activate(signal);
112
+ }
113
+ const [directResult, relayResult] = await Promise.all([direct, relay]);
114
+ signal?.removeEventListener('abort', cancel);
115
+ if (directResult.error instanceof DirectRejectedError) throw directResult.error;
116
+ throw relayResult.error ?? directResult.error;
117
+ }
@@ -0,0 +1,129 @@
1
+ const MAX_PENDING_BYTES = 2 * 1024 * 1024;
2
+ const MAX_SOCKET_BYTES = 256 * 1024;
3
+
4
+ /** Shared sender for CLI input and the Node's aggregate Terminal relay output. */
5
+ export class TerminalRelaySender {
6
+ #socket;
7
+ #onPendingChange;
8
+ #pending = [];
9
+ #pendingBytes = 0;
10
+ #inFlight;
11
+ #timer;
12
+ #immediate;
13
+ #closed = false;
14
+
15
+ constructor(socket, { onPendingChange = () => {} } = {}) {
16
+ this.#socket = socket;
17
+ this.#onPendingChange = onPendingChange;
18
+ }
19
+
20
+ send(value, key = '') {
21
+ if (this.#closed || this.#socket.readyState !== 1)
22
+ return Promise.reject(new Error('TERMINAL_CONNECTION_LOST'));
23
+ const bytes = Buffer.byteLength(value, 'utf8');
24
+ if (this.#pendingBytes + bytes > MAX_PENDING_BYTES)
25
+ return Promise.reject(
26
+ Object.assign(new Error('TERMINAL_RELAY_BUFFER_EXCEEDED'), {
27
+ code: 'TERMINAL_RELAY_BUFFER_EXCEEDED'
28
+ })
29
+ );
30
+ const result = new Promise((resolve, reject) => {
31
+ this.#pending.push({ value, key, bytes, resolve, reject });
32
+ this.#pendingBytes += bytes;
33
+ });
34
+ this.#pump();
35
+ this.#notify();
36
+ return result;
37
+ }
38
+
39
+ cancel(key) {
40
+ if (this.#inFlight?.key === key) {
41
+ this.#inFlight.cancelled = true;
42
+ this.#inFlight.reject(new Error('TERMINAL_ATTACH_INVALID'));
43
+ }
44
+ const kept = [];
45
+ for (const item of this.#pending) {
46
+ if (item.key !== key) kept.push(item);
47
+ else {
48
+ this.#pendingBytes -= item.bytes;
49
+ item.reject(new Error('TERMINAL_ATTACH_INVALID'));
50
+ }
51
+ }
52
+ this.#pending = kept;
53
+ if (kept.length === 0) this.#clearScheduled();
54
+ this.#notify();
55
+ }
56
+
57
+ close(error = new Error('TERMINAL_CONNECTION_LOST')) {
58
+ if (this.#closed) return;
59
+ this.#closed = true;
60
+ this.#clearScheduled();
61
+ this.#inFlight?.reject(error);
62
+ this.#inFlight = undefined;
63
+ for (const item of this.#pending) item.reject(error);
64
+ this.#pending = [];
65
+ this.#pendingBytes = 0;
66
+ this.#notify();
67
+ }
68
+
69
+ #pump() {
70
+ if (
71
+ this.#closed ||
72
+ this.#inFlight !== undefined ||
73
+ this.#timer !== undefined ||
74
+ this.#immediate !== undefined ||
75
+ this.#pending.length === 0
76
+ )
77
+ return;
78
+ if (this.#socket.readyState !== 1) {
79
+ this.close();
80
+ return;
81
+ }
82
+ const buffered = this.#socket.bufferedAmount ?? 0;
83
+ if (buffered > 0 && buffered + this.#pending[0].bytes > MAX_SOCKET_BYTES) {
84
+ // Poll only while the transport is congested; there is no throughput timer.
85
+ this.#timer = setTimeout(() => {
86
+ this.#timer = undefined;
87
+ this.#pump();
88
+ this.#notify();
89
+ }, 20);
90
+ return;
91
+ }
92
+ const item = this.#pending.shift();
93
+ this.#inFlight = item;
94
+ try {
95
+ this.#socket.send(item.value, (error) => {
96
+ if (this.#inFlight !== item) return;
97
+ if (error) {
98
+ this.close(error);
99
+ return;
100
+ }
101
+ this.#inFlight = undefined;
102
+ this.#pendingBytes -= item.bytes;
103
+ // Even a synchronous write callback must yield before the next frame.
104
+ this.#immediate = setImmediate(() => {
105
+ this.#immediate = undefined;
106
+ this.#pump();
107
+ this.#notify();
108
+ });
109
+ item.resolve();
110
+ this.#notify();
111
+ });
112
+ } catch (error) {
113
+ this.close(error);
114
+ }
115
+ }
116
+
117
+ #notify() {
118
+ const keys = new Set(this.#pending.map((item) => item.key));
119
+ if (this.#inFlight !== undefined && !this.#inFlight.cancelled) keys.add(this.#inFlight.key);
120
+ this.#onPendingChange(keys);
121
+ }
122
+
123
+ #clearScheduled() {
124
+ clearTimeout(this.#timer);
125
+ this.#timer = undefined;
126
+ clearImmediate(this.#immediate);
127
+ this.#immediate = undefined;
128
+ }
129
+ }