@rebon/cli-win32-x64 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +203 -144
  4. package/THIRD_PARTY_NOTICES.txt +1232 -128
  5. package/bin/rebon.js +22 -22
  6. package/lib/windows-managed.js +390 -390
  7. package/package.json +9 -2
  8. package/payload/compose-runtime/package.json +17 -17
  9. package/payload/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -10
  10. package/payload/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -13
  11. package/payload/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -21
  12. package/payload/compose-runtime/payload/compose/shims/dsh-llm.js +313 -313
  13. package/payload/compose-runtime/payload/compose/shims/dsh-settings.js +43 -43
  14. package/payload/compose-runtime/payload/compose/shims/dsh-tools.js +20 -20
  15. package/payload/compose-runtime/payload/compose/shims/dsh-web.js +10 -10
  16. package/payload/compose-runtime/payload/compose/shims/llm-adapter.js +32 -32
  17. package/payload/compose-runtime/payload/compose/shims/zod-lite.js +49 -49
  18. package/payload/compose-runtime/payload/vendor/cordis/LICENSE +21 -0
  19. package/payload/compose-runtime/payload/vendor/cordis/index.js +1530 -1530
  20. package/payload/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -21
  21. package/payload/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -357
  22. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -21
  23. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -21
  24. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -21
  25. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -21
  26. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -21
  27. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -21
  28. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -21
  29. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -21
  30. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -21
  31. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -21
  32. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -21
  33. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -21
  34. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -21
  35. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -0
  36. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -0
  37. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -21
  38. package/payload/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -1193
  39. package/payload/compose-runtime/payload/vendor/dsh/agent.js +714 -714
  40. package/payload/compose-runtime/payload/vendor/dsh/llm-core.js +269 -269
  41. package/payload/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -672
  42. package/payload/compose-runtime/payload/vendor/dsh/logger-console.js +83 -83
  43. package/payload/compose-runtime/payload/vendor/dsh/scope.js +287 -287
  44. package/payload/compose-runtime/payload/vendor/dsh/session.js +1668 -1668
  45. package/payload/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -309
  46. package/payload/compose-runtime/payload/vendor/dsh/timeout.js +100 -100
  47. package/payload/compose-runtime/payload/vendor/dsh/timer.js +128 -128
  48. package/payload/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -143
  49. package/payload/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -1527
  50. package/payload/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -849
  51. package/payload/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -122
  52. package/payload/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -21
  53. package/payload/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -177
  54. package/payload/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -48
  55. package/payload/compose-runtime/payload/vendor/schemastery/LICENSE +21 -0
  56. package/payload/compose-runtime/payload/vendor/schemastery/index.mjs +656 -656
  57. package/payload/compose-runtime/payload-manifests.json +48 -48
  58. package/payload/compose-runtime/src/bridge.mjs +193 -195
  59. package/payload/compose-runtime/src/credentials-runtime.mjs +50 -50
  60. package/payload/compose-runtime/src/eventsource-stream.mjs +48 -48
  61. package/payload/compose-runtime/src/index.mjs +7 -7
  62. package/payload/compose-runtime/src/llm-runtime.mjs +326 -326
  63. package/payload/compose-runtime/src/loader.mjs +78 -78
  64. package/payload/compose-runtime/src/loop-assembly.mjs +237 -237
  65. package/payload/compose-runtime/src/payload.mjs +49 -49
  66. package/payload/compose-runtime/src/plugin.mjs +59 -59
  67. package/payload/compose-runtime/src/realm.mjs +223 -223
  68. package/payload/compose-runtime/src/registry.mjs +191 -191
  69. package/payload/compose-runtime/src/resolve.mjs +115 -115
  70. package/payload/compose-runtime/src/systemprompt-runtime.mjs +67 -67
  71. package/payload/compose-runtime/src/tools-runtime.mjs +239 -239
  72. package/payload/compose-runtime/src/web-runtime.mjs +209 -209
  73. package/payload/plugin-host/package.json +11 -11
  74. package/payload/plugin-host/src/bridge.mjs +64 -64
  75. package/payload/plugin-host/src/cli.mjs +147 -147
  76. package/payload/plugin-host/src/framing.mjs +113 -113
  77. package/payload/plugin-host/src/host.mjs +691 -691
  78. package/payload/plugin-host/src/json.mjs +208 -208
  79. package/payload/plugin-host/src/lifecycle.mjs +114 -114
  80. package/payload/plugin-host/src/loader.mjs +183 -183
  81. package/payload/plugin-host/src/methods.mjs +377 -377
  82. package/payload/plugin-host/src/ownership.mjs +57 -57
  83. package/payload/plugin-host/src/protocol.mjs +129 -129
  84. package/payload/plugin-host/src/sdk.mjs +7 -7
  85. package/payload/rebon-boa-helper.exe +0 -0
  86. package/payload/rebon-browser-mcp.exe +0 -0
  87. package/payload/rebon-computer-use.exe +0 -0
  88. package/payload/rebon-lsp-mcp.exe +0 -0
  89. package/payload/rebon.exe +2 -2
  90. package/payload/sandbox-win.exe +0 -0
  91. package/scripts/postinstall.js +112 -112
  92. package/payload/compose-runtime/payload/compose/credentials-runtime.js +0 -38
  93. package/payload/compose-runtime/payload/compose/llm-runtime.js +0 -330
  94. package/payload/compose-runtime/payload/compose/loop-assembly.js +0 -149
  95. package/payload/compose-runtime/payload/compose/serve-dispatch.js +0 -82
  96. package/payload/compose-runtime/payload/compose/systemprompt-runtime.js +0 -74
  97. package/payload/compose-runtime/payload/compose/tools-runtime.js +0 -228
  98. package/payload/compose-runtime/payload/compose/web-runtime.js +0 -204
@@ -1,147 +1,147 @@
1
- #!/usr/bin/env node
2
- import { pathToFileURL } from 'node:url';
3
- import { NdjsonDecoder, SerializedWriter } from './framing.mjs';
4
- import { PluginHost } from './host.mjs';
5
- import { loadPlugin } from './loader.mjs';
6
- import { currentOwner } from './ownership.mjs';
7
- import { ProtocolError } from './protocol.mjs';
8
-
9
- export function diagnostic(stream, code, message) {
10
- const bounded = String(message).replace(/[\r\n]+/g, ' ').slice(0, 512);
11
- stream.write(`${JSON.stringify({ level: 'error', code, message: bounded })}\n`);
12
- }
13
- export function installHostConsole(stream) {
14
- const write = (level, values) => stream.write(`${JSON.stringify({ level, message: values.map(String).join(' ').slice(0, 512) })}\n`);
15
- return Object.freeze({ log: (...v) => write('info', v), info: (...v) => write('info', v), warn: (...v) => write('warn', v), error: (...v) => write('error', v) });
16
- }
17
-
18
- // Frames are not handled one-at-a-time.
19
- //
20
- // A request handler may itself call rebon — that is the whole Node → rebon
21
- // direction — and the answer to that call arrives as a frame on this very
22
- // stream. Awaiting each dispatch before reading the next would mean the handler
23
- // waits for a frame the loop cannot read until the handler returns: a deadlock
24
- // that no amount of care inside a plugin can avoid. So dispatches are tracked
25
- // rather than awaited, and everything that needs them finished waits here.
26
- /// Loads the module that decides what a plugin package *is*.
27
- ///
28
- /// The host itself knows one shape: a module exporting `activate`. Anything
29
- /// else — a Cordis plugin mounted into a shared composition realm, say — is a
30
- /// different answer to the same question, and the host has no business knowing
31
- /// which answers exist. So rebon names the module that does, and this loads it.
32
- ///
33
- /// The module exports `createLoader({ next })` and returns `{ load, unload }`;
34
- /// `next` is the built-in loader, so an adapter that does not recognise a
35
- /// package hands it back rather than reimplementing it.
36
- export async function resolveLoader(specifier, error) {
37
- if (!specifier) return {};
38
- const url = specifier.startsWith('file:') ? specifier : pathToFileURL(specifier).href;
39
- const module = await import(url);
40
- if (typeof module.createLoader !== 'function') {
41
- throw new ProtocolError('loader_invalid', `${specifier} exports no createLoader`);
42
- }
43
- const seams = await module.createLoader({ next: loadPlugin });
44
- if (typeof seams?.load !== 'function') {
45
- throw new ProtocolError('loader_invalid', `${specifier} produced no load function`);
46
- }
47
- return seams;
48
- }
49
-
50
- export async function runHost({ input, output, error, loader }) {
51
- const decoder = new NdjsonDecoder();
52
- const writer = new SerializedWriter(output);
53
- const host = new PluginHost(writer, loader ?? {});
54
- const running = new Set();
55
- let fatal = null;
56
- const track = (promise) => {
57
- const tracked = promise.catch((cause) => { fatal ??= cause; }).finally(() => running.delete(tracked));
58
- running.add(tracked);
59
- };
60
- // Draining rather than a single `Promise.all`: a dispatch that is still
61
- // running can schedule another one, and the process must not exit under it.
62
- const quiesce = async () => { while (running.size > 0) await Promise.all([...running]); };
63
- try {
64
- for await (const chunk of input) {
65
- for (const frame of decoder.push(chunk)) {
66
- if (fatal) throw fatal;
67
- if (host.shuttingDown) throw new ProtocolError('frame_after_shutdown', 'frame received after shutdown');
68
- track(host.accept(frame));
69
- }
70
- if (fatal) throw fatal;
71
- if (host.shuttingDown) { await quiesce(); if (fatal) throw fatal; return 0; }
72
- }
73
- decoder.finish();
74
- await quiesce();
75
- if (fatal) throw fatal;
76
- diagnostic(error, 'connection_lost', 'stdin reached EOF before successful shutdown');
77
- return 3;
78
- } catch (cause) {
79
- // Terminals already owed still have to reach stdout: a caller waiting on
80
- // one is not made whole by the host exiting with a diagnosis.
81
- await quiesce();
82
- const code = cause?.code ?? 'host_failure';
83
- diagnostic(error, code, cause?.message ?? 'host failed');
84
- return code === 'write_error' ? 4 : 2;
85
- }
86
- }
87
-
88
- /// A rejection nobody handled, said out loud instead of swallowed, and
89
- /// attributed where the async chain reaches back to an entry.
90
- ///
91
- /// Node does not stop this process for one, and before T20 nothing else looked
92
- /// either: a plugin could start an async task, have it fail, and leave no trace
93
- /// anywhere rebon reads. Three rounds of T19 were spent on exactly that.
94
- ///
95
- /// T20 recorded these as unattributed, believing a failed promise cannot say
96
- /// who created it. It can: `plugin/load` runs inside a store that survives
97
- /// timers, microtasks and awaits, so this handler reads the owner straight out
98
- /// of it. What escapes the chain — through an `EventEmitter`, a native callback
99
- /// or a third-party library — is still `unattributed`, which is the truthful
100
- /// answer and is kept distinct from a wrong one.
101
- ///
102
- /// It never exits. A stray rejection in one plugin must not take down a host
103
- /// other plugins are being served from; a load still in flight is failed by its
104
- /// own path instead, which is the caller's to run.
105
- export function installRejectionDiagnostic(stream) {
106
- process.on('unhandledRejection', (reason) => {
107
- const first = String(reason?.stack ?? reason ?? '').split('\n')[0];
108
- const owner = currentOwner();
109
- if (owner === undefined) {
110
- diagnostic(stream, 'unhandled_rejection', `unattributed: ${first}`);
111
- return;
112
- }
113
- // `loading` says which of the two this is. While a load is in flight the
114
- // entry did not install correctly and nothing depends on it yet, so the
115
- // load fails and the composition skips it with a reason. Afterwards the
116
- // plugin stays ready: a load that already succeeded cannot be un-failed,
117
- // and tearing down a plane other entries serve from would cost far more
118
- // than the one bad promise.
119
- if (owner.loading) {
120
- owner.rejection = first;
121
- diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry} during load: ${first}`);
122
- return;
123
- }
124
- diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry}: ${first}`);
125
- });
126
- }
127
-
128
- if (import.meta.url === pathToFileURL(process.argv[1]).href) {
129
- installRejectionDiagnostic(process.stderr);
130
- const flag = process.argv.indexOf('--loader');
131
- const specifier = flag >= 0 ? process.argv[flag + 1] : process.env.REBON_PLUGIN_LOADER;
132
- let loader;
133
- try {
134
- loader = await resolveLoader(specifier, process.stderr);
135
- } catch (cause) {
136
- // A host that cannot build the loader rebon asked for must not fall back to
137
- // the built-in one: it would load a different set of plugins than the one
138
- // rebon believes it asked for, and say nothing.
139
- diagnostic(process.stderr, cause?.code ?? 'loader_failure', cause?.message ?? 'loader failed');
140
- process.exitCode = 2;
141
- }
142
- if (loader !== undefined) {
143
- process.exitCode = await runHost({
144
- input: process.stdin, output: process.stdout, error: process.stderr, loader,
145
- });
146
- }
147
- }
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from 'node:url';
3
+ import { NdjsonDecoder, SerializedWriter } from './framing.mjs';
4
+ import { PluginHost } from './host.mjs';
5
+ import { loadPlugin } from './loader.mjs';
6
+ import { currentOwner } from './ownership.mjs';
7
+ import { ProtocolError } from './protocol.mjs';
8
+
9
+ export function diagnostic(stream, code, message) {
10
+ const bounded = String(message).replace(/[\r\n]+/g, ' ').slice(0, 512);
11
+ stream.write(`${JSON.stringify({ level: 'error', code, message: bounded })}\n`);
12
+ }
13
+ export function installHostConsole(stream) {
14
+ const write = (level, values) => stream.write(`${JSON.stringify({ level, message: values.map(String).join(' ').slice(0, 512) })}\n`);
15
+ return Object.freeze({ log: (...v) => write('info', v), info: (...v) => write('info', v), warn: (...v) => write('warn', v), error: (...v) => write('error', v) });
16
+ }
17
+
18
+ // Frames are not handled one-at-a-time.
19
+ //
20
+ // A request handler may itself call rebon — that is the whole Node → rebon
21
+ // direction — and the answer to that call arrives as a frame on this very
22
+ // stream. Awaiting each dispatch before reading the next would mean the handler
23
+ // waits for a frame the loop cannot read until the handler returns: a deadlock
24
+ // that no amount of care inside a plugin can avoid. So dispatches are tracked
25
+ // rather than awaited, and everything that needs them finished waits here.
26
+ /// Loads the module that decides what a plugin package *is*.
27
+ ///
28
+ /// The host itself knows one shape: a module exporting `activate`. Anything
29
+ /// else — a Cordis plugin mounted into a shared composition realm, say — is a
30
+ /// different answer to the same question, and the host has no business knowing
31
+ /// which answers exist. So rebon names the module that does, and this loads it.
32
+ ///
33
+ /// The module exports `createLoader({ next })` and returns `{ load, unload }`;
34
+ /// `next` is the built-in loader, so an adapter that does not recognise a
35
+ /// package hands it back rather than reimplementing it.
36
+ export async function resolveLoader(specifier, error) {
37
+ if (!specifier) return {};
38
+ const url = specifier.startsWith('file:') ? specifier : pathToFileURL(specifier).href;
39
+ const module = await import(url);
40
+ if (typeof module.createLoader !== 'function') {
41
+ throw new ProtocolError('loader_invalid', `${specifier} exports no createLoader`);
42
+ }
43
+ const seams = await module.createLoader({ next: loadPlugin });
44
+ if (typeof seams?.load !== 'function') {
45
+ throw new ProtocolError('loader_invalid', `${specifier} produced no load function`);
46
+ }
47
+ return seams;
48
+ }
49
+
50
+ export async function runHost({ input, output, error, loader }) {
51
+ const decoder = new NdjsonDecoder();
52
+ const writer = new SerializedWriter(output);
53
+ const host = new PluginHost(writer, loader ?? {});
54
+ const running = new Set();
55
+ let fatal = null;
56
+ const track = (promise) => {
57
+ const tracked = promise.catch((cause) => { fatal ??= cause; }).finally(() => running.delete(tracked));
58
+ running.add(tracked);
59
+ };
60
+ // Draining rather than a single `Promise.all`: a dispatch that is still
61
+ // running can schedule another one, and the process must not exit under it.
62
+ const quiesce = async () => { while (running.size > 0) await Promise.all([...running]); };
63
+ try {
64
+ for await (const chunk of input) {
65
+ for (const frame of decoder.push(chunk)) {
66
+ if (fatal) throw fatal;
67
+ if (host.shuttingDown) throw new ProtocolError('frame_after_shutdown', 'frame received after shutdown');
68
+ track(host.accept(frame));
69
+ }
70
+ if (fatal) throw fatal;
71
+ if (host.shuttingDown) { await quiesce(); if (fatal) throw fatal; return 0; }
72
+ }
73
+ decoder.finish();
74
+ await quiesce();
75
+ if (fatal) throw fatal;
76
+ diagnostic(error, 'connection_lost', 'stdin reached EOF before successful shutdown');
77
+ return 3;
78
+ } catch (cause) {
79
+ // Terminals already owed still have to reach stdout: a caller waiting on
80
+ // one is not made whole by the host exiting with a diagnosis.
81
+ await quiesce();
82
+ const code = cause?.code ?? 'host_failure';
83
+ diagnostic(error, code, cause?.message ?? 'host failed');
84
+ return code === 'write_error' ? 4 : 2;
85
+ }
86
+ }
87
+
88
+ /// A rejection nobody handled, said out loud instead of swallowed, and
89
+ /// attributed where the async chain reaches back to an entry.
90
+ ///
91
+ /// Node does not stop this process for one, and nothing else looks either: a
92
+ /// plugin could start an async task, have it fail, and leave no trace anywhere
93
+ /// rebon reads.
94
+ ///
95
+ /// A failed promise does not carry who created it, but `plugin/load` runs
96
+ /// inside a store that survives timers, microtasks and awaits, so this handler
97
+ /// reads the owner straight out of it. What escapes the chain — through an
98
+ /// `EventEmitter`, a native callback or a third-party library — is still
99
+ /// `unattributed`, which is the truthful answer and is kept distinct from a
100
+ /// wrong one.
101
+ ///
102
+ /// It never exits. A stray rejection in one plugin must not take down a host
103
+ /// other plugins are being served from; a load still in flight is failed by its
104
+ /// own path instead, which is the caller's to run.
105
+ export function installRejectionDiagnostic(stream) {
106
+ process.on('unhandledRejection', (reason) => {
107
+ const first = String(reason?.stack ?? reason ?? '').split('\n')[0];
108
+ const owner = currentOwner();
109
+ if (owner === undefined) {
110
+ diagnostic(stream, 'unhandled_rejection', `unattributed: ${first}`);
111
+ return;
112
+ }
113
+ // `loading` says which of the two this is. While a load is in flight the
114
+ // entry did not install correctly and nothing depends on it yet, so the
115
+ // load fails and the composition skips it with a reason. Afterwards the
116
+ // plugin stays ready: a load that already succeeded cannot be un-failed,
117
+ // and tearing down a plane other entries serve from would cost far more
118
+ // than the one bad promise.
119
+ if (owner.loading) {
120
+ owner.rejection = first;
121
+ diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry} during load: ${first}`);
122
+ return;
123
+ }
124
+ diagnostic(stream, 'unhandled_rejection', `plugin=${owner.entry}: ${first}`);
125
+ });
126
+ }
127
+
128
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
129
+ installRejectionDiagnostic(process.stderr);
130
+ const flag = process.argv.indexOf('--loader');
131
+ const specifier = flag >= 0 ? process.argv[flag + 1] : process.env.REBON_PLUGIN_LOADER;
132
+ let loader;
133
+ try {
134
+ loader = await resolveLoader(specifier, process.stderr);
135
+ } catch (cause) {
136
+ // A host that cannot build the loader rebon asked for must not fall back to
137
+ // the built-in one: it would load a different set of plugins than the one
138
+ // rebon believes it asked for, and say nothing.
139
+ diagnostic(process.stderr, cause?.code ?? 'loader_failure', cause?.message ?? 'loader failed');
140
+ process.exitCode = 2;
141
+ }
142
+ if (loader !== undefined) {
143
+ process.exitCode = await runHost({
144
+ input: process.stdin, output: process.stdout, error: process.stderr, loader,
145
+ });
146
+ }
147
+ }
@@ -1,113 +1,113 @@
1
- import { MAX_FRAME_BYTES, ProtocolError, validateEnvelope } from './protocol.mjs';
2
- import { parseEnvelopeJson } from './json.mjs';
3
-
4
- export class FramingError extends ProtocolError {}
5
-
6
- export class NdjsonDecoder {
7
- #segments = []; #byteCount = 0; #failed = false; #copiedBytes = 0;
8
- constructor(maxFrameBytes = MAX_FRAME_BYTES) { this.maxFrameBytes = maxFrameBytes; }
9
- get stats() { return Object.freeze({ retained_bytes: this.#byteCount, copied_bytes: this.#copiedBytes }); }
10
- push(chunk) {
11
- if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
12
- if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) return this.#fail('wrong_chunk', 'input chunk must be bytes');
13
- const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
14
- const frames = [];
15
- let start = 0;
16
- for (let index = 0; index < input.length; index++) {
17
- if (input[index] !== 0x0a) continue;
18
- this.#append(input.subarray(start, index));
19
- let bytes = this.#join();
20
- this.#segments = []; this.#byteCount = 0;
21
- if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
22
- if (bytes.length > this.maxFrameBytes) return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
23
- if (bytes.length === 0) return this.#fail('empty_frame', 'NDJSON frame is empty');
24
- if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
25
- return this.#fail('utf8_bom', 'UTF-8 BOM is not permitted');
26
- }
27
- try {
28
- const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
29
- frames.push(parseEnvelopeJson(text));
30
- } catch (error) {
31
- if (error instanceof ProtocolError) return this.#fail(error.code, error.message);
32
- return this.#fail('invalid_utf8', 'frame is not valid UTF-8');
33
- }
34
- start = index + 1;
35
- }
36
- this.#append(input.subarray(start));
37
- if (this.#byteCount > this.maxFrameBytes
38
- && !(this.#byteCount === this.maxFrameBytes + 1 && this.#lastByte() === 0x0d)) {
39
- return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
40
- }
41
- return frames;
42
- }
43
- finish() {
44
- if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
45
- if (this.#byteCount) return this.#fail('incomplete_frame', `EOF with unterminated ${this.#byteCount}-byte frame`);
46
- }
47
- #append(bytes) {
48
- if (!bytes.length) return;
49
- const copy = Buffer.from(bytes);
50
- this.#segments.push(copy); this.#byteCount += copy.length; this.#copiedBytes += copy.length;
51
- }
52
- #join() {
53
- if (this.#segments.length === 1) return this.#segments[0];
54
- this.#copiedBytes += this.#byteCount;
55
- return Buffer.concat(this.#segments, this.#byteCount);
56
- }
57
- #lastByte() { return this.#segments.at(-1)?.at(-1); }
58
- #fail(code, message) { this.#failed = true; this.#segments = []; this.#byteCount = 0; throw new FramingError(code, message); }
59
- }
60
-
61
- export function encodeFrame(value, maxFrameBytes = MAX_FRAME_BYTES) {
62
- const normalized = validateEnvelope(value);
63
- let text;
64
- try { text = JSON.stringify(normalized); } catch { throw new FramingError('encode_error', 'envelope cannot be encoded'); }
65
- const bytes = Buffer.from(text);
66
- if (bytes.length > maxFrameBytes) throw new FramingError('frame_too_large', `frame exceeds ${maxFrameBytes} bytes`);
67
- return Buffer.concat([bytes, Buffer.from('\n')]);
68
- }
69
-
70
- export class SerializedWriter {
71
- #tail = Promise.resolve(); #error;
72
- constructor(stream, maxFrameBytes = MAX_FRAME_BYTES) { this.stream = stream; this.maxFrameBytes = maxFrameBytes; }
73
- send(value) {
74
- if (this.#error) return Promise.reject(this.#error);
75
- const bytes = encodeFrame(value, this.maxFrameBytes);
76
- const operation = this.#tail.then(() => {
77
- if (this.#error) throw this.#error;
78
- return this.#write(bytes);
79
- });
80
- this.#tail = operation.catch((error) => { this.#error ??= error; });
81
- return operation;
82
- }
83
- async flush() { await this.#tail; if (this.#error) throw this.#error; }
84
- #write(bytes) {
85
- return new Promise((resolve, reject) => {
86
- let callbackDone = false; let accepted; let drainSeen = false; let settled = false;
87
- const cleanup = () => { this.stream.removeListener('error', onError); this.stream.removeListener('drain', onDrain); };
88
- const fail = (cause) => {
89
- if (settled) return;
90
- settled = true; cleanup();
91
- const error = cause instanceof FramingError ? cause
92
- : new FramingError('write_error', `protocol output failed: ${cause?.code ?? 'stream error'}`);
93
- this.#error ??= error; reject(error);
94
- };
95
- const complete = () => {
96
- if (settled || accepted === undefined || !callbackDone || (!accepted && !drainSeen)) return;
97
- settled = true; cleanup(); resolve();
98
- };
99
- const onError = (error) => fail(error);
100
- const onDrain = () => { drainSeen = true; complete(); };
101
- this.stream.once('error', onError);
102
- this.stream.once('drain', onDrain);
103
- try {
104
- accepted = this.stream.write(bytes, (error) => {
105
- if (error) { setImmediate(() => fail(error)); return; }
106
- callbackDone = true; complete();
107
- });
108
- if (accepted) this.stream.removeListener('drain', onDrain);
109
- complete();
110
- } catch (error) { fail(error); }
111
- });
112
- }
113
- }
1
+ import { MAX_FRAME_BYTES, ProtocolError, validateEnvelope } from './protocol.mjs';
2
+ import { parseEnvelopeJson } from './json.mjs';
3
+
4
+ export class FramingError extends ProtocolError {}
5
+
6
+ export class NdjsonDecoder {
7
+ #segments = []; #byteCount = 0; #failed = false; #copiedBytes = 0;
8
+ constructor(maxFrameBytes = MAX_FRAME_BYTES) { this.maxFrameBytes = maxFrameBytes; }
9
+ get stats() { return Object.freeze({ retained_bytes: this.#byteCount, copied_bytes: this.#copiedBytes }); }
10
+ push(chunk) {
11
+ if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
12
+ if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array)) return this.#fail('wrong_chunk', 'input chunk must be bytes');
13
+ const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
14
+ const frames = [];
15
+ let start = 0;
16
+ for (let index = 0; index < input.length; index++) {
17
+ if (input[index] !== 0x0a) continue;
18
+ this.#append(input.subarray(start, index));
19
+ let bytes = this.#join();
20
+ this.#segments = []; this.#byteCount = 0;
21
+ if (bytes.at(-1) === 0x0d) bytes = bytes.subarray(0, -1);
22
+ if (bytes.length > this.maxFrameBytes) return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
23
+ if (bytes.length === 0) return this.#fail('empty_frame', 'NDJSON frame is empty');
24
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
25
+ return this.#fail('utf8_bom', 'UTF-8 BOM is not permitted');
26
+ }
27
+ try {
28
+ const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
29
+ frames.push(parseEnvelopeJson(text));
30
+ } catch (error) {
31
+ if (error instanceof ProtocolError) return this.#fail(error.code, error.message);
32
+ return this.#fail('invalid_utf8', 'frame is not valid UTF-8');
33
+ }
34
+ start = index + 1;
35
+ }
36
+ this.#append(input.subarray(start));
37
+ if (this.#byteCount > this.maxFrameBytes
38
+ && !(this.#byteCount === this.maxFrameBytes + 1 && this.#lastByte() === 0x0d)) {
39
+ return this.#fail('frame_too_large', `frame exceeds ${this.maxFrameBytes} bytes`);
40
+ }
41
+ return frames;
42
+ }
43
+ finish() {
44
+ if (this.#failed) throw new FramingError('poisoned', 'decoder is poisoned');
45
+ if (this.#byteCount) return this.#fail('incomplete_frame', `EOF with unterminated ${this.#byteCount}-byte frame`);
46
+ }
47
+ #append(bytes) {
48
+ if (!bytes.length) return;
49
+ const copy = Buffer.from(bytes);
50
+ this.#segments.push(copy); this.#byteCount += copy.length; this.#copiedBytes += copy.length;
51
+ }
52
+ #join() {
53
+ if (this.#segments.length === 1) return this.#segments[0];
54
+ this.#copiedBytes += this.#byteCount;
55
+ return Buffer.concat(this.#segments, this.#byteCount);
56
+ }
57
+ #lastByte() { return this.#segments.at(-1)?.at(-1); }
58
+ #fail(code, message) { this.#failed = true; this.#segments = []; this.#byteCount = 0; throw new FramingError(code, message); }
59
+ }
60
+
61
+ export function encodeFrame(value, maxFrameBytes = MAX_FRAME_BYTES) {
62
+ const normalized = validateEnvelope(value);
63
+ let text;
64
+ try { text = JSON.stringify(normalized); } catch { throw new FramingError('encode_error', 'envelope cannot be encoded'); }
65
+ const bytes = Buffer.from(text);
66
+ if (bytes.length > maxFrameBytes) throw new FramingError('frame_too_large', `frame exceeds ${maxFrameBytes} bytes`);
67
+ return Buffer.concat([bytes, Buffer.from('\n')]);
68
+ }
69
+
70
+ export class SerializedWriter {
71
+ #tail = Promise.resolve(); #error;
72
+ constructor(stream, maxFrameBytes = MAX_FRAME_BYTES) { this.stream = stream; this.maxFrameBytes = maxFrameBytes; }
73
+ send(value) {
74
+ if (this.#error) return Promise.reject(this.#error);
75
+ const bytes = encodeFrame(value, this.maxFrameBytes);
76
+ const operation = this.#tail.then(() => {
77
+ if (this.#error) throw this.#error;
78
+ return this.#write(bytes);
79
+ });
80
+ this.#tail = operation.catch((error) => { this.#error ??= error; });
81
+ return operation;
82
+ }
83
+ async flush() { await this.#tail; if (this.#error) throw this.#error; }
84
+ #write(bytes) {
85
+ return new Promise((resolve, reject) => {
86
+ let callbackDone = false; let accepted; let drainSeen = false; let settled = false;
87
+ const cleanup = () => { this.stream.removeListener('error', onError); this.stream.removeListener('drain', onDrain); };
88
+ const fail = (cause) => {
89
+ if (settled) return;
90
+ settled = true; cleanup();
91
+ const error = cause instanceof FramingError ? cause
92
+ : new FramingError('write_error', `protocol output failed: ${cause?.code ?? 'stream error'}`);
93
+ this.#error ??= error; reject(error);
94
+ };
95
+ const complete = () => {
96
+ if (settled || accepted === undefined || !callbackDone || (!accepted && !drainSeen)) return;
97
+ settled = true; cleanup(); resolve();
98
+ };
99
+ const onError = (error) => fail(error);
100
+ const onDrain = () => { drainSeen = true; complete(); };
101
+ this.stream.once('error', onError);
102
+ this.stream.once('drain', onDrain);
103
+ try {
104
+ accepted = this.stream.write(bytes, (error) => {
105
+ if (error) { setImmediate(() => fail(error)); return; }
106
+ callbackDone = true; complete();
107
+ });
108
+ if (accepted) this.stream.removeListener('drain', onDrain);
109
+ complete();
110
+ } catch (error) { fail(error); }
111
+ });
112
+ }
113
+ }