@myagentroam/node 0.12.0 → 0.12.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.
Files changed (142) 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/hosting/hosted-conversation-node-service.js +1 -1
  32. package/dist/marctl/bin/marctl.mjs +6 -0
  33. package/dist/marctl/bin/marnodes.mjs +6 -0
  34. package/dist/marctl/bin/marscp.mjs +6 -0
  35. package/dist/marctl/bin/marssh.mjs +6 -0
  36. package/dist/marctl/package.json +1 -0
  37. package/dist/marctl/src/cli.mjs +271 -0
  38. package/dist/marctl/src/client.mjs +291 -0
  39. package/dist/marctl/src/config.mjs +46 -0
  40. package/dist/marctl/src/direct-control.mjs +225 -0
  41. package/dist/marctl/src/direct-errors.mjs +48 -0
  42. package/dist/marctl/src/direct-process.mjs +374 -0
  43. package/dist/marctl/src/direct-transport.mjs +235 -0
  44. package/dist/marctl/src/direct.mjs +421 -0
  45. package/dist/marctl/src/errors.mjs +11 -0
  46. package/dist/marctl/src/i18n/en-US.mjs +285 -0
  47. package/dist/marctl/src/i18n/index.mjs +20 -0
  48. package/dist/marctl/src/i18n/zh-CN.mjs +269 -0
  49. package/dist/marctl/src/scp-channel.mjs +99 -0
  50. package/dist/marctl/src/scp-copy.mjs +46 -0
  51. package/dist/marctl/src/scp-endpoint.mjs +29 -0
  52. package/dist/marctl/src/scp-protocol.mjs +2 -0
  53. package/dist/marctl/src/scp-worker.cjs +266 -0
  54. package/dist/marctl/src/scp-workers.mjs +179 -0
  55. package/dist/marctl/src/scp.mjs +64 -0
  56. package/dist/marctl/src/session-args.mjs +233 -0
  57. package/dist/marctl/src/session-input.mjs +43 -0
  58. package/dist/marctl/src/session-prepare.mjs +87 -0
  59. package/dist/marctl/src/session-target.mjs +26 -0
  60. package/dist/marctl/src/session-wait.mjs +232 -0
  61. package/dist/marctl/src/sessions.mjs +93 -0
  62. package/dist/marctl/src/terminal-frame.mjs +48 -0
  63. package/dist/marctl/src/terminal-interaction.mjs +177 -0
  64. package/dist/marctl/src/terminal-race.mjs +117 -0
  65. package/dist/marctl/src/terminal-relay-sender.mjs +129 -0
  66. package/dist/marctl/src/terminal-relay.mjs +305 -0
  67. package/dist/marctl/src/terminal.mjs +152 -0
  68. package/dist/marctl-guidance.js +1 -1
  69. package/dist/marctl.js +1 -1
  70. package/dist/marnodes.js +1 -1
  71. package/dist/marscp.js +1 -1
  72. package/dist/marssh.js +1 -1
  73. package/dist/native-session-history.js +1 -1
  74. package/dist/ripgrep-binary.js +3 -3
  75. package/dist/runner/codex-runner.js +2 -2
  76. package/dist/runner/mar-agent/agent-operation-presentation.js +22 -10
  77. package/dist/runner/mar-agent/codex-imagegen-skill.js +15 -23
  78. package/dist/runner/mar-agent/conversation-normalizer.js +4 -18
  79. package/dist/runner/mar-agent/image-generation-extensions.js +1 -1
  80. package/dist/runner/mar-agent/image-generation.js +73 -20
  81. package/dist/runner/mar-agent/session-runtime-registry.js +5 -3
  82. package/dist/service/code-navigation/file-reference-parser.js +1 -1
  83. package/dist/service/node-terminal-channel.js +1 -1
  84. package/dist/session/session-catalog-service.js +4 -1
  85. package/dist/workspace/workspace-code-navigation-service.js +2 -2
  86. package/dist/workspace/workspace-git-exclude-service.js +1 -1
  87. package/package.json +11 -9
  88. package/vendor/ctags/NOTICE +0 -22
  89. package/vendor/ctags/darwin-arm64/ctags +0 -0
  90. package/vendor/ctags/darwin-x64/ctags +0 -0
  91. package/vendor/ctags/licenses/COPYING +0 -340
  92. package/vendor/ctags/licenses/COPYING.MinGW-w64-runtime.txt +0 -240
  93. package/vendor/ctags/licenses/LICENSE.janssen +0 -19
  94. package/vendor/ctags/licenses/LICENSE.libyaml +0 -19
  95. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/COPYING +0 -340
  96. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/RECEIPT.yml +0 -27
  97. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libiconv.yml +0 -24
  98. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libjansson.yml +0 -19
  99. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libxml2.yml +0 -20
  100. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libyaml.yml +0 -7
  101. package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libz.yml +0 -13
  102. package/vendor/ctags/licenses/official-snapshot/darwin-x64/COPYING +0 -340
  103. package/vendor/ctags/licenses/official-snapshot/darwin-x64/RECEIPT.yml +0 -27
  104. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libiconv.yml +0 -24
  105. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libjansson.yml +0 -19
  106. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libxml2.yml +0 -20
  107. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libyaml.yml +0 -7
  108. package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libz.yml +0 -13
  109. package/vendor/ctags/licenses/official-snapshot/linux-arm64/COPYING +0 -340
  110. package/vendor/ctags/licenses/official-snapshot/linux-arm64/RECEIPT.yml +0 -27
  111. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libiconv.yml +0 -24
  112. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libjansson.yml +0 -19
  113. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libxml2.yml +0 -20
  114. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libyaml.yml +0 -7
  115. package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libz.yml +0 -13
  116. package/vendor/ctags/licenses/official-snapshot/linux-x64/COPYING +0 -340
  117. package/vendor/ctags/licenses/official-snapshot/linux-x64/RECEIPT.yml +0 -27
  118. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libiconv.yml +0 -24
  119. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libjansson.yml +0 -19
  120. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libxml2.yml +0 -20
  121. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libyaml.yml +0 -7
  122. package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libz.yml +0 -13
  123. package/vendor/ctags/licenses/win32-x64/COPYING +0 -340
  124. package/vendor/ctags/licenses/win32-x64/COPYING.MinGW-w64-runtime.txt +0 -240
  125. package/vendor/ctags/licenses/win32-x64/Copyright.libxml2 +0 -23
  126. package/vendor/ctags/licenses/win32-x64/LICENCE.pcre2 +0 -94
  127. package/vendor/ctags/licenses/win32-x64/LICENSE.janssen +0 -19
  128. package/vendor/ctags/licenses/win32-x64/LICENSE.libyaml +0 -19
  129. package/vendor/ctags/linux-arm64/ctags +0 -0
  130. package/vendor/ctags/linux-x64/ctags +0 -0
  131. package/vendor/ctags/manifest.json +0 -42
  132. package/vendor/ctags/source/universal-ctags-ab95af198f8eaeb6d50dc96e3adf52e769d3e5f0.tar.gz +0 -0
  133. package/vendor/ctags/win32-x64/ctags.exe +0 -0
  134. package/vendor/ripgrep/LICENSE-MIT +0 -21
  135. package/vendor/ripgrep/UNLICENSE +0 -24
  136. package/vendor/ripgrep/darwin-arm64/rg +0 -0
  137. package/vendor/ripgrep/darwin-x64/rg +0 -0
  138. package/vendor/ripgrep/linux-arm64/rg +0 -0
  139. package/vendor/ripgrep/linux-x64/rg +0 -0
  140. package/vendor/ripgrep/manifest.json +0 -42
  141. package/vendor/ripgrep/win32-arm64/rg.exe +0 -0
  142. package/vendor/ripgrep/win32-x64/rg.exe +0 -0
@@ -0,0 +1,291 @@
1
+ import { MarctlError } from './errors.mjs';
2
+ import { createTranslator } from './i18n/index.mjs';
3
+
4
+ const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
5
+
6
+ export class MarctlClient {
7
+ #serverUrl;
8
+ #key;
9
+ #fetch;
10
+ #translate;
11
+ #requestTimeoutMs;
12
+
13
+ constructor({
14
+ serverUrl,
15
+ key,
16
+ fetchImpl = globalThis.fetch,
17
+ translate = createTranslator(),
18
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS
19
+ }) {
20
+ this.#serverUrl = serverUrl;
21
+ this.#key = key;
22
+ this.#fetch = fetchImpl;
23
+ this.#translate = translate;
24
+ this.#requestTimeoutMs = requestTimeoutMs;
25
+ if (typeof this.#fetch !== 'function')
26
+ throw new MarctlError(this.#translate('errors.fetchUnavailable'));
27
+ }
28
+
29
+ async listNodes() {
30
+ const payload = await this.#requestJson('/api/marctl/nodes');
31
+ if (!isRecord(payload) || !Array.isArray(payload.nodes))
32
+ throw new MarctlError(this.#translate('errors.serverInvalidNodes'));
33
+
34
+ return payload.nodes.map((node) => {
35
+ if (
36
+ !isRecord(node) ||
37
+ !isNonEmptyString(node.id) ||
38
+ typeof node.connected !== 'boolean' ||
39
+ (node.displayName !== undefined && typeof node.displayName !== 'string') ||
40
+ (node.platform !== undefined && typeof node.platform !== 'string')
41
+ )
42
+ throw new MarctlError(this.#translate('errors.serverInvalidNodes'));
43
+ return {
44
+ id: node.id,
45
+ ...(node.displayName === undefined ? {} : { displayName: node.displayName }),
46
+ connected: node.connected,
47
+ ...(node.platform === undefined ? {} : { platform: node.platform })
48
+ };
49
+ });
50
+ }
51
+
52
+ async sessionCommand(body, { timeoutMs, signal } = {}) {
53
+ const result = await this.#requestJson('/api/marctl/sessions', {
54
+ method: 'POST',
55
+ body,
56
+ preserveError: true,
57
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
58
+ ...(signal === undefined ? {} : { signal })
59
+ });
60
+ if (!isRecord(result))
61
+ throw new MarctlError(this.#translate('errors.serverInvalidJson'), {
62
+ code: 'MARCTL_RESPONSE_INVALID'
63
+ });
64
+ return result;
65
+ }
66
+
67
+ async createTerminal({ nodeId, rows, cols, command, binary = false }) {
68
+ validateNodeId(nodeId, this.#translate);
69
+ validateDimensions(rows, cols, this.#translate);
70
+ if (command !== undefined && typeof command !== 'string')
71
+ throw new MarctlError(this.#translate('errors.terminalCommandInvalid'));
72
+ const body = { nodeId, rows, cols };
73
+ if (command !== undefined) body.command = command;
74
+ if (binary) body.binary = true;
75
+ const payload = await this.#requestJson('/api/marctl/terminals', {
76
+ method: 'POST',
77
+ body
78
+ });
79
+ if (!isRecord(payload) || !isNonEmptyString(payload.terminalId))
80
+ throw new MarctlError(this.#translate('errors.serverInvalidTerminalId'));
81
+ return binary
82
+ ? { terminalId: payload.terminalId, binary: payload.binary === true }
83
+ : payload.terminalId;
84
+ }
85
+
86
+ async getTerminalManifest(nodeId) {
87
+ validateNodeId(nodeId, this.#translate);
88
+ const payload = await this.#requestJson(
89
+ `/api/marctl/nodes/${encodeURIComponent(nodeId)}/manifest`
90
+ );
91
+ if (
92
+ !isRecord(payload) ||
93
+ !isRecord(payload.manifest) ||
94
+ !isNonEmptyString(payload.manifest.nodeGeneration)
95
+ )
96
+ throw new MarctlError(this.#translate('errors.serverInvalidManifest'));
97
+ return {
98
+ ...payload.manifest,
99
+ ...(payload.deferredRelay === true ? { deferredRelay: true } : {}),
100
+ ...(payload.binaryTerminal === true ? { binaryTerminal: true } : {})
101
+ };
102
+ }
103
+
104
+ async openDirectTerminal(options) {
105
+ const { openDirectTerminalProcess } = await import('./direct-process.mjs');
106
+ return openDirectTerminalProcess({
107
+ ...options,
108
+ connection: { serverUrl: this.#serverUrl, key: this.#key }
109
+ });
110
+ }
111
+
112
+ async createAttachTicket({ nodeId, terminalId, afterSequence }) {
113
+ validateNodeId(nodeId, this.#translate);
114
+ validateTerminalId(terminalId, this.#translate);
115
+ if (afterSequence !== undefined && (!Number.isSafeInteger(afterSequence) || afterSequence < 0))
116
+ throw new MarctlError(this.#translate('errors.terminalSequenceInvalid'));
117
+
118
+ const body = { nodeId };
119
+ if (afterSequence !== undefined) body.afterSequence = afterSequence;
120
+ const payload = await this.#requestJson(
121
+ `/api/marctl/terminals/${encodeURIComponent(terminalId)}/attach-ticket`,
122
+ { method: 'POST', body }
123
+ );
124
+ if (!isRecord(payload) || !isNonEmptyString(payload.ticket))
125
+ throw new MarctlError(this.#translate('errors.serverInvalidTicket'));
126
+ return payload.ticket;
127
+ }
128
+
129
+ async deleteTerminal({ nodeId, terminalId }) {
130
+ validateNodeId(nodeId, this.#translate);
131
+ validateTerminalId(terminalId, this.#translate);
132
+ await this.#request('/api/marctl/terminals/' + encodeURIComponent(terminalId), {
133
+ method: 'DELETE',
134
+ body: { nodeId }
135
+ });
136
+ }
137
+
138
+ openTerminalWebSocket(WebSocketClass, { terminalId, ticket, deferred = false }) {
139
+ validateTerminalId(terminalId, this.#translate);
140
+ if (!isNonEmptyString(ticket))
141
+ throw new MarctlError(this.#translate('errors.terminalTicketInvalid'));
142
+
143
+ const url = new URL(
144
+ `/api/marctl/terminals/${encodeURIComponent(terminalId)}/connect`,
145
+ `${this.#serverUrl}/`
146
+ );
147
+ url.searchParams.set('ticket', ticket);
148
+ if (deferred) url.searchParams.set('deferred', '1');
149
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
150
+
151
+ try {
152
+ return new WebSocketClass(url.toString(), {
153
+ headers: { Authorization: `Bearer ${this.#key}` }
154
+ });
155
+ } catch {
156
+ throw new MarctlError(this.#translate('errors.terminalConnectionStart'));
157
+ }
158
+ }
159
+
160
+ openWorkbenchWebSocket(WebSocketClass) {
161
+ const url = new URL('/api/marctl/live', `${this.#serverUrl}/`);
162
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
163
+ try {
164
+ return new WebSocketClass(url.toString(), {
165
+ headers: { Authorization: `Bearer ${this.#key}` }
166
+ });
167
+ } catch {
168
+ throw new MarctlError(this.#translate('errors.workbenchConnectionStart'));
169
+ }
170
+ }
171
+
172
+ async #requestJson(pathname, options = {}) {
173
+ return this.#request(pathname, { ...options, json: true });
174
+ }
175
+
176
+ async #request(
177
+ pathname,
178
+ {
179
+ method = 'GET',
180
+ body,
181
+ json = false,
182
+ preserveError = false,
183
+ timeoutMs = this.#requestTimeoutMs,
184
+ signal
185
+ } = {}
186
+ ) {
187
+ const headers = {
188
+ Accept: 'application/json',
189
+ Authorization: `Bearer ${this.#key}`
190
+ };
191
+ const controller = new AbortController();
192
+ const request = {
193
+ method,
194
+ headers,
195
+ signal: signal ? AbortSignal.any([controller.signal, signal]) : controller.signal
196
+ };
197
+ if (body !== undefined) {
198
+ headers['Content-Type'] = 'application/json';
199
+ request.body = JSON.stringify(body);
200
+ }
201
+
202
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
203
+ try {
204
+ const response = await this.#fetch(new URL(pathname, `${this.#serverUrl}/`), request);
205
+ if (!response?.ok) {
206
+ if (preserveError) {
207
+ const payload = await response?.json().catch(() => undefined);
208
+ const failure = payload?.error;
209
+ if (
210
+ isRecord(failure) &&
211
+ typeof failure.code === 'string' &&
212
+ /^[A-Za-z][A-Za-z0-9_]{0,127}$/u.test(failure.code) &&
213
+ typeof failure.message === 'string'
214
+ ) {
215
+ throw new MarctlError(failure.message.slice(0, 2000), {
216
+ code: failure.code,
217
+ statusCode: response.status,
218
+ requestId:
219
+ typeof failure.requestId === 'string' ? failure.requestId.slice(0, 160) : undefined,
220
+ upstreamRequestId:
221
+ typeof failure.upstreamRequestId === 'string'
222
+ ? failure.upstreamRequestId.slice(0, 160)
223
+ : undefined,
224
+ details:
225
+ typeof failure.details === 'string' ? failure.details.slice(0, 4000) : undefined
226
+ });
227
+ }
228
+ }
229
+ throw new MarctlError(this.#translate('errors.serverRejected'), {
230
+ code: 'MARCTL_SERVER_REJECTED',
231
+ statusCode: response?.status
232
+ });
233
+ }
234
+ if (!json) return response;
235
+ try {
236
+ return await response.json();
237
+ } catch {
238
+ throw new MarctlError(
239
+ this.#translate(
240
+ controller.signal.aborted ? 'errors.serverTimeout' : 'errors.serverInvalidJson'
241
+ ),
242
+ { code: controller.signal.aborted ? 'MARCTL_REQUEST_TIMEOUT' : 'MARCTL_RESPONSE_INVALID' }
243
+ );
244
+ }
245
+ } catch (error) {
246
+ if (signal?.aborted)
247
+ throw new MarctlError(this.#translate('errors.sessionWaitCancelled'), {
248
+ code: 'MARCTL_WAIT_CANCELLED'
249
+ });
250
+ if (error instanceof MarctlError) throw error;
251
+ throw new MarctlError(
252
+ this.#translate(
253
+ controller.signal.aborted ? 'errors.serverTimeout' : 'errors.serverUnreachable'
254
+ ),
255
+ { code: controller.signal.aborted ? 'MARCTL_REQUEST_TIMEOUT' : 'MARCTL_SERVER_UNREACHABLE' }
256
+ );
257
+ } finally {
258
+ clearTimeout(timer);
259
+ }
260
+ }
261
+ }
262
+
263
+ function validateNodeId(nodeId, translate) {
264
+ if (!isNonEmptyString(nodeId) || nodeId.length > 128)
265
+ throw new MarctlError(translate('errors.nodeIdInvalid'));
266
+ }
267
+
268
+ function validateTerminalId(terminalId, translate) {
269
+ if (!isNonEmptyString(terminalId) || terminalId.length > 128)
270
+ throw new MarctlError(translate('errors.terminalIdInvalid'));
271
+ }
272
+
273
+ function validateDimensions(rows, cols, translate) {
274
+ if (
275
+ !Number.isSafeInteger(rows) ||
276
+ !Number.isSafeInteger(cols) ||
277
+ rows < 1 ||
278
+ cols < 1 ||
279
+ rows > 1000 ||
280
+ cols > 1000
281
+ )
282
+ throw new MarctlError(translate('errors.terminalSizeInvalid'));
283
+ }
284
+
285
+ function isRecord(value) {
286
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
287
+ }
288
+
289
+ function isNonEmptyString(value) {
290
+ return typeof value === 'string' && value.length > 0;
291
+ }
@@ -0,0 +1,46 @@
1
+ import { createTranslator } from './i18n/index.mjs';
2
+
3
+ import { MarctlError } from './errors.mjs';
4
+
5
+ export async function loadConfig({ env = process.env, translate = createTranslator(env) } = {}) {
6
+ const serverUrl = nonEmptyString(env.MAR_CTL_SERVER_URL);
7
+ const token = nonEmptyString(env.MAR_CTL_USER_TOKEN);
8
+ if (
9
+ env.MAR_CTL_CONTEXT_REQUIRED !== '1' ||
10
+ serverUrl === undefined ||
11
+ token === undefined ||
12
+ token.length > 16_384 ||
13
+ !/^marctl_user_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/u.test(token)
14
+ )
15
+ throw new MarctlError(translate('errors.userContextUnavailable'), {
16
+ code: 'UNAUTHENTICATED',
17
+ statusCode: 401
18
+ });
19
+ return Object.freeze({ serverUrl: validateServerUrl(serverUrl, translate), key: token });
20
+ }
21
+
22
+ export function validateServerUrl(value, translate = createTranslator()) {
23
+ let url;
24
+ try {
25
+ url = new URL(value);
26
+ } catch {
27
+ throw new MarctlError(translate('errors.serverUrlInvalid'));
28
+ }
29
+
30
+ const isLoopbackHttp =
31
+ url.protocol === 'http:' &&
32
+ (url.hostname === 'localhost' ||
33
+ url.hostname === '127.0.0.1' ||
34
+ url.hostname === '::1' ||
35
+ url.hostname === '[::1]');
36
+ if (url.protocol !== 'https:' && !isLoopbackHttp)
37
+ throw new MarctlError(translate('errors.serverUrlTransport'));
38
+ if (url.username || url.password || url.search || url.hash || url.pathname !== '/')
39
+ throw new MarctlError(translate('errors.serverUrlOrigin'));
40
+
41
+ return url.origin;
42
+ }
43
+
44
+ function nonEmptyString(value) {
45
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
46
+ }
@@ -0,0 +1,225 @@
1
+ import { DirectFallbackError, directControlError } from './direct-errors.mjs';
2
+
3
+ export class LiveControl {
4
+ #socket;
5
+ #timeoutMs;
6
+ #ready = false;
7
+ #closed = false;
8
+ #waiters = new Set();
9
+ #listeners = new Set();
10
+ #onMessage;
11
+ #onError;
12
+ #onClose;
13
+
14
+ constructor(socket, timeoutMs) {
15
+ this.#socket = socket;
16
+ this.#timeoutMs = timeoutMs;
17
+ this.#onMessage = (raw, isBinary) => {
18
+ if (isBinary) return;
19
+ let message;
20
+ try {
21
+ message = JSON.parse(rawToString(raw));
22
+ } catch {
23
+ return;
24
+ }
25
+ if (!isRecord(message) || typeof message.type !== 'string') return;
26
+ for (const listener of this.#listeners) listener(message);
27
+ for (const waiter of [...this.#waiters]) {
28
+ if (
29
+ ((waiter.request?.type === 'terminal.direct.prepare' &&
30
+ message.type === 'terminal.direct.error') ||
31
+ (waiter.request?.type === 'direct.prepare' && message.type === 'direct.error')) &&
32
+ message.requestId === waiter.request.requestId
33
+ ) {
34
+ this.#waiters.delete(waiter);
35
+ clearTimeout(waiter.timer);
36
+ waiter.reject(directControlError(message.code));
37
+ continue;
38
+ }
39
+ if (!waiter.predicate(message, waiter.request)) continue;
40
+ this.#waiters.delete(waiter);
41
+ clearTimeout(waiter.timer);
42
+ waiter.resolve(message);
43
+ }
44
+ };
45
+ this.#onError = () => this.#fail(new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE'));
46
+ this.#onClose = () => this.#fail(new DirectFallbackError('DIRECT_CONTROL_CLOSED'));
47
+ socket.on?.('message', this.#onMessage);
48
+ socket.on?.('error', this.#onError);
49
+ socket.on?.('close', this.#onClose);
50
+ }
51
+
52
+ async openAndResume() {
53
+ await this.#waitForSocket('open', 'DIRECT_CONTROL_TIMEOUT');
54
+ const ready = this.waitFor(
55
+ (message) =>
56
+ message.type === 'workbench.ready' && typeof message.workbenchConnectionId === 'string',
57
+ undefined,
58
+ 'DIRECT_RESUME_TIMEOUT'
59
+ );
60
+ if (!this.send({ type: 'resume' })) {
61
+ ready.catch(() => undefined);
62
+ throw new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE');
63
+ }
64
+ await ready;
65
+ this.#ready = true;
66
+ }
67
+
68
+ request(message, predicate, fallbackCode) {
69
+ if (!this.#ready) throw new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE');
70
+ const response = this.waitFor(predicate, message, fallbackCode);
71
+ if (!this.send(message)) {
72
+ response.catch(() => undefined);
73
+ throw new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE');
74
+ }
75
+ return response;
76
+ }
77
+
78
+ waitFor(predicate, request, fallbackCode, timeoutMs = this.#timeoutMs) {
79
+ return new Promise((resolve, reject) => {
80
+ if (this.#closed) {
81
+ reject(new DirectFallbackError('DIRECT_CONTROL_CLOSED'));
82
+ return;
83
+ }
84
+ const waiter = {
85
+ predicate,
86
+ request,
87
+ resolve,
88
+ reject,
89
+ timer: setTimeout(() => {
90
+ this.#waiters.delete(waiter);
91
+ reject(new DirectFallbackError(fallbackCode ?? 'DIRECT_SETUP_TIMEOUT'));
92
+ }, timeoutMs)
93
+ };
94
+ this.#waiters.add(waiter);
95
+ });
96
+ }
97
+
98
+ onControl(listener) {
99
+ if (this.#closed) return () => {};
100
+ this.#listeners.add(listener);
101
+ return () => this.#listeners.delete(listener);
102
+ }
103
+
104
+ send(message) {
105
+ if (this.#closed || (this.#socket.readyState !== undefined && this.#socket.readyState !== 1))
106
+ return false;
107
+ try {
108
+ this.#socket.send(JSON.stringify(message));
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ close() {
116
+ if (this.#closed) return;
117
+ this.#closed = true;
118
+ this.#fail(new DirectFallbackError('DIRECT_CONTROL_CLOSED'));
119
+ this.#listeners.clear();
120
+ this.#socket.removeListener?.('message', this.#onMessage);
121
+ this.#socket.removeListener?.('error', this.#onError);
122
+ this.#socket.removeListener?.('close', this.#onClose);
123
+ try {
124
+ this.#socket.close();
125
+ } catch {
126
+ // The control socket is already closed.
127
+ }
128
+ }
129
+
130
+ #waitForSocket(event, code) {
131
+ if (this.#socket.readyState === 1) {
132
+ return Promise.resolve();
133
+ }
134
+ return new Promise((resolve, reject) => {
135
+ const onEvent = () => {
136
+ cleanup();
137
+ resolve();
138
+ };
139
+ const onError = () => {
140
+ cleanup();
141
+ reject(new DirectFallbackError(code));
142
+ };
143
+ const onClose = () => {
144
+ cleanup();
145
+ reject(new DirectFallbackError(code));
146
+ };
147
+ const timer = setTimeout(() => {
148
+ cleanup();
149
+ reject(new DirectFallbackError(code));
150
+ }, this.#timeoutMs);
151
+ const cleanup = () => {
152
+ clearTimeout(timer);
153
+ this.#socket.removeListener?.(event, onEvent);
154
+ this.#socket.removeListener?.('error', onError);
155
+ this.#socket.removeListener?.('close', onClose);
156
+ };
157
+ this.#socket.once?.(event, onEvent);
158
+ this.#socket.once?.('error', onError);
159
+ this.#socket.once?.('close', onClose);
160
+ });
161
+ }
162
+
163
+ #fail(error) {
164
+ if (this.#closed && this.#waiters.size === 0) return;
165
+ for (const waiter of [...this.#waiters]) {
166
+ this.#waiters.delete(waiter);
167
+ clearTimeout(waiter.timer);
168
+ waiter.reject(error);
169
+ }
170
+ }
171
+ }
172
+
173
+ export async function cancelSession(live, type, directSessionId) {
174
+ if (live === undefined) return;
175
+ const closed = live.waitFor(
176
+ (message) =>
177
+ message.type === `${type.replace('.cancel', '.closed')}` &&
178
+ message.directSessionId === directSessionId,
179
+ undefined,
180
+ 'DIRECT_CANCEL_TIMEOUT',
181
+ 1000
182
+ );
183
+ live.send({
184
+ type,
185
+ directSessionId,
186
+ ...(type === 'direct.cancel' ? { reason: 'CLI_FALLBACK' } : {})
187
+ });
188
+ await closed.catch(() => undefined);
189
+ }
190
+
191
+ export function withTimeout(promise, timeoutMs, code, signal) {
192
+ let timer;
193
+ let onAbort;
194
+ return Promise.race([
195
+ promise,
196
+ new Promise((_, reject) => {
197
+ timer = setTimeout(() => reject(new DirectFallbackError(code)), timeoutMs);
198
+ }),
199
+ ...(signal
200
+ ? [
201
+ new Promise((_, reject) => {
202
+ onAbort = () => reject(new DirectFallbackError('DIRECT_RACE_CANCELLED'));
203
+ if (signal.aborted) onAbort();
204
+ else signal.addEventListener('abort', onAbort, { once: true });
205
+ })
206
+ ]
207
+ : [])
208
+ ]).finally(() => {
209
+ clearTimeout(timer);
210
+ if (onAbort) signal.removeEventListener('abort', onAbort);
211
+ });
212
+ }
213
+
214
+ export function rawToString(raw) {
215
+ if (typeof raw === 'string') return raw;
216
+ if (Buffer.isBuffer(raw)) return raw.toString('utf8');
217
+ if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
218
+ if (ArrayBuffer.isView(raw))
219
+ return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
220
+ return String(raw);
221
+ }
222
+
223
+ export function isRecord(value) {
224
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
225
+ }
@@ -0,0 +1,48 @@
1
+ export class DirectFallbackError extends Error {
2
+ constructor(code = 'DIRECT_UNAVAILABLE') {
3
+ super(code);
4
+ this.name = 'DirectFallbackError';
5
+ this.code = code;
6
+ }
7
+ }
8
+
9
+ export class DirectUnsafeError extends Error {
10
+ constructor(code = 'DIRECT_CANCEL_UNCONFIRMED') {
11
+ super(code);
12
+ this.name = 'DirectUnsafeError';
13
+ this.code = code;
14
+ }
15
+ }
16
+
17
+ const directRejections = new Set([
18
+ 'DIRECT_TRANSFER_NOT_AUTHORIZED',
19
+ 'DIRECT_SESSION_LIMIT_REACHED',
20
+ 'DIRECT_NODE_GENERATION_MISMATCH',
21
+ 'DIRECT_TRANSFER_SESSION_CONFLICT',
22
+ 'TERMINAL_NOT_AUTHORIZED',
23
+ 'TERMINAL_DIRECT_LEASE_EXPIRED',
24
+ 'TERMINAL_NOT_FOUND',
25
+ 'TERMINAL_CLOSED',
26
+ 'TERMINAL_WORKSPACE_MISMATCH',
27
+ 'TERMINAL_GIT_OWNER_REQUIRED',
28
+ 'TERMINAL_ATTACH_CONFLICT',
29
+ 'TERMINAL_RELAY_LIMIT_REACHED'
30
+ ]);
31
+
32
+ export class DirectRejectedError extends Error {
33
+ constructor(code) {
34
+ super(code);
35
+ this.name = 'DirectRejectedError';
36
+ this.code = code;
37
+ }
38
+ }
39
+
40
+ export function directControlError(code) {
41
+ const safeCode =
42
+ typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,127}$/u.test(code)
43
+ ? code
44
+ : 'TERMINAL_DIRECT_UNAVAILABLE';
45
+ return directRejections.has(safeCode)
46
+ ? new DirectRejectedError(safeCode)
47
+ : new DirectFallbackError(safeCode);
48
+ }