@moxxy/plugin-channel-signal 0.27.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/dist/channel/chunker.d.ts +69 -0
  3. package/dist/channel/chunker.d.ts.map +1 -0
  4. package/dist/channel/chunker.js +133 -0
  5. package/dist/channel/chunker.js.map +1 -0
  6. package/dist/channel/turn-runner.d.ts +33 -0
  7. package/dist/channel/turn-runner.d.ts.map +1 -0
  8. package/dist/channel/turn-runner.js +70 -0
  9. package/dist/channel/turn-runner.js.map +1 -0
  10. package/dist/channel/voice.d.ts +37 -0
  11. package/dist/channel/voice.d.ts.map +1 -0
  12. package/dist/channel/voice.js +88 -0
  13. package/dist/channel/voice.js.map +1 -0
  14. package/dist/channel.d.ts +154 -0
  15. package/dist/channel.d.ts.map +1 -0
  16. package/dist/channel.js +468 -0
  17. package/dist/channel.js.map +1 -0
  18. package/dist/index.d.ts +28 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +215 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/jsonrpc.d.ts +53 -0
  23. package/dist/jsonrpc.d.ts.map +1 -0
  24. package/dist/jsonrpc.js +167 -0
  25. package/dist/jsonrpc.js.map +1 -0
  26. package/dist/keys.d.ts +37 -0
  27. package/dist/keys.d.ts.map +1 -0
  28. package/dist/keys.js +55 -0
  29. package/dist/keys.js.map +1 -0
  30. package/dist/pair-flow.d.ts +21 -0
  31. package/dist/pair-flow.d.ts.map +1 -0
  32. package/dist/pair-flow.js +136 -0
  33. package/dist/pair-flow.js.map +1 -0
  34. package/dist/permission.d.ts +21 -0
  35. package/dist/permission.d.ts.map +1 -0
  36. package/dist/permission.js +27 -0
  37. package/dist/permission.js.map +1 -0
  38. package/dist/schema.d.ts +4295 -0
  39. package/dist/schema.d.ts.map +1 -0
  40. package/dist/schema.js +84 -0
  41. package/dist/schema.js.map +1 -0
  42. package/dist/setup-wizard.d.ts +14 -0
  43. package/dist/setup-wizard.d.ts.map +1 -0
  44. package/dist/setup-wizard.js +85 -0
  45. package/dist/setup-wizard.js.map +1 -0
  46. package/dist/sidecar.d.ts +137 -0
  47. package/dist/sidecar.d.ts.map +1 -0
  48. package/dist/sidecar.js +421 -0
  49. package/dist/sidecar.js.map +1 -0
  50. package/package.json +89 -0
  51. package/src/channel/chunker.test.ts +135 -0
  52. package/src/channel/chunker.ts +147 -0
  53. package/src/channel/turn-runner.ts +97 -0
  54. package/src/channel/voice.test.ts +136 -0
  55. package/src/channel/voice.ts +118 -0
  56. package/src/channel.test.ts +364 -0
  57. package/src/channel.ts +607 -0
  58. package/src/index.ts +289 -0
  59. package/src/jsonrpc.test.ts +128 -0
  60. package/src/jsonrpc.ts +198 -0
  61. package/src/keys.test.ts +45 -0
  62. package/src/keys.ts +56 -0
  63. package/src/pair-flow.ts +161 -0
  64. package/src/permission.ts +36 -0
  65. package/src/schema.ts +98 -0
  66. package/src/setup-wizard.ts +98 -0
  67. package/src/sidecar.test.ts +276 -0
  68. package/src/sidecar.ts +511 -0
  69. package/src/subcommands.test.ts +206 -0
@@ -0,0 +1,421 @@
1
+ import { spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as net from 'node:net';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { sleepWithAbort } from '@moxxy/sdk';
7
+ import { SignalRpcClient } from './jsonrpc.js';
8
+ const defaultSpawn = (command, args) => spawn(command, [...args], { stdio: ['ignore', 'pipe', 'pipe'] });
9
+ const defaultConnect = (socketPath) => new Promise((resolve, reject) => {
10
+ const socket = net.createConnection(socketPath);
11
+ socket.once('connect', () => resolve(socket));
12
+ socket.once('error', (err) => reject(err));
13
+ });
14
+ /** `$XDG_DATA_HOME/signal-cli` (defaults to `~/.local/share/signal-cli`). */
15
+ export function signalCliDataDir(env = process.env) {
16
+ const xdg = env['XDG_DATA_HOME']?.trim();
17
+ const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), '.local', 'share');
18
+ return path.join(base, 'signal-cli');
19
+ }
20
+ /** Where the daemon writes received attachments (voice notes live here). */
21
+ export function signalCliAttachmentsDir(env = process.env) {
22
+ return path.join(signalCliDataDir(env), 'attachments');
23
+ }
24
+ /**
25
+ * Cheap PATH probe for the `signal-cli` binary — a directory scan with an
26
+ * exec-bit check, NO process spawn (signal-cli is a JVM app; spawning it takes
27
+ * seconds and `isAvailable` runs on every `moxxy channels list` / `doctor`).
28
+ * Returns the resolved path or null. Never throws: a missing/odd PATH must
29
+ * not crash discovery.
30
+ */
31
+ export function findSignalCliOnPath(env = process.env) {
32
+ try {
33
+ const raw = env['PATH'] ?? '';
34
+ const names = process.platform === 'win32' ? ['signal-cli.bat', 'signal-cli.cmd', 'signal-cli.exe', 'signal-cli'] : ['signal-cli'];
35
+ for (const dir of raw.split(path.delimiter)) {
36
+ if (!dir)
37
+ continue;
38
+ for (const name of names) {
39
+ const candidate = path.join(dir, name);
40
+ try {
41
+ fs.accessSync(candidate, fs.constants.X_OK);
42
+ if (fs.statSync(candidate).isFile())
43
+ return candidate;
44
+ }
45
+ catch {
46
+ /* not here — keep scanning */
47
+ }
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /** One-line install guidance surfaced when the binary is missing. */
57
+ export const SIGNAL_CLI_INSTALL_HINT = 'signal-cli not found on PATH. Install it: `brew install signal-cli` (macOS), ' +
58
+ 'or see https://github.com/AsamK/signal-cli/wiki/Quickstart for Linux/Windows packages.';
59
+ /** Poll cadence while waiting for the daemon socket to accept connections. */
60
+ const CONNECT_POLL_MS = 250;
61
+ export class SignalSidecar {
62
+ opts;
63
+ child = null;
64
+ exited = false;
65
+ exitCode = null;
66
+ client = null;
67
+ recentStderr = [];
68
+ exitListeners = new Set();
69
+ socketPath;
70
+ constructor(opts) {
71
+ this.opts = opts;
72
+ this.socketPath =
73
+ opts.socketPath ?? path.join(os.tmpdir(), `moxxy-signal-${process.pid}-${Date.now().toString(36)}.sock`);
74
+ }
75
+ /** Fires when the daemon exits (clean stop AND crash). */
76
+ onExit(listener) {
77
+ this.exitListeners.add(listener);
78
+ return () => this.exitListeners.delete(listener);
79
+ }
80
+ get rpc() {
81
+ return this.client;
82
+ }
83
+ /**
84
+ * Spawn the daemon and wait until it is actually serving: the socket accepts
85
+ * a connection AND a `version` JSON-RPC round-trips. Rejects (and reaps the
86
+ * child) when the child dies early or the boot deadline passes, carrying the
87
+ * daemon's stderr tail so the CAUSE ("User +… is not registered") surfaces
88
+ * instead of a bare timeout.
89
+ */
90
+ async start() {
91
+ if (this.client)
92
+ return this.client;
93
+ const binary = this.opts.binary ?? 'signal-cli';
94
+ const spawnFn = this.opts.spawnFn ?? defaultSpawn;
95
+ const connectFn = this.opts.connectFn ?? defaultConnect;
96
+ const bootTimeoutMs = this.opts.bootTimeoutMs ?? 45_000;
97
+ // A stale socket file from a crashed previous run would make the daemon
98
+ // fail to bind; the path is per-process+time so this is belt-and-braces.
99
+ try {
100
+ fs.rmSync(this.socketPath, { force: true });
101
+ }
102
+ catch {
103
+ /* best-effort */
104
+ }
105
+ const args = ['-a', this.opts.account, 'daemon', '--socket', this.socketPath, '--receive-mode', 'on-start'];
106
+ this.opts.logger?.info?.('signal: spawning signal-cli daemon', {
107
+ binary,
108
+ socket: this.socketPath,
109
+ });
110
+ let child;
111
+ try {
112
+ child = spawnFn(binary, args);
113
+ }
114
+ catch (err) {
115
+ throw new Error(`failed to spawn ${binary}: ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ this.child = child;
118
+ this.exited = false;
119
+ this.watchStderr(child);
120
+ child.once('error', (err) => {
121
+ // spawn-level failure (ENOENT etc.) — recorded like an exit so the boot
122
+ // loop below stops waiting on a process that never started.
123
+ this.recentStderr.push(`spawn error: ${err.message}`);
124
+ this.markExited(null);
125
+ });
126
+ child.once('exit', (code) => this.markExited(code));
127
+ const deadline = Date.now() + bootTimeoutMs;
128
+ let stream = null;
129
+ while (Date.now() < deadline) {
130
+ if (this.exited) {
131
+ throw new Error(this.describeEarlyExit());
132
+ }
133
+ try {
134
+ stream = await connectFn(this.socketPath);
135
+ break;
136
+ }
137
+ catch {
138
+ await sleepWithAbort(CONNECT_POLL_MS);
139
+ }
140
+ }
141
+ if (!stream) {
142
+ await this.stop();
143
+ throw new Error(`signal-cli daemon did not open its socket within ${bootTimeoutMs}ms` + this.stderrTailSuffix());
144
+ }
145
+ const client = new SignalRpcClient({
146
+ stream,
147
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
148
+ });
149
+ // Health check: a cheap RPC proves the JSON-RPC dispatcher is live (the
150
+ // socket can accept before the daemon is ready to serve).
151
+ try {
152
+ await client.request('version', {});
153
+ }
154
+ catch (err) {
155
+ client.close();
156
+ await this.stop();
157
+ throw new Error(`signal-cli daemon failed its health check: ${err instanceof Error ? err.message : String(err)}` +
158
+ this.stderrTailSuffix());
159
+ }
160
+ this.client = client;
161
+ this.opts.logger?.info?.('signal: daemon healthy', { pid: child.pid ?? null });
162
+ return client;
163
+ }
164
+ /**
165
+ * Graceful shutdown: close the RPC stream, SIGTERM, wait `killGraceMs` for a
166
+ * clean exit (aborting the wait the moment `exit` fires — the sdk's
167
+ * `sleepWithAbort` pattern from the runner supervisor), then SIGKILL a
168
+ * holdout so a wedged JVM can never outlive the channel as an orphan.
169
+ */
170
+ async stop() {
171
+ const child = this.child;
172
+ this.client?.close();
173
+ this.client = null;
174
+ if (!child || this.exited) {
175
+ this.child = null;
176
+ this.removeSocketFile();
177
+ return;
178
+ }
179
+ try {
180
+ child.kill('SIGTERM');
181
+ }
182
+ catch {
183
+ /* already gone */
184
+ }
185
+ const graceMs = this.opts.killGraceMs ?? 5_000;
186
+ if (!(await this.waitForExit(child, graceMs))) {
187
+ this.opts.logger?.warn?.('signal: daemon ignored SIGTERM; escalating to SIGKILL');
188
+ try {
189
+ child.kill('SIGKILL');
190
+ }
191
+ catch {
192
+ /* already gone */
193
+ }
194
+ await this.waitForExit(child, 2_000);
195
+ }
196
+ this.child = null;
197
+ this.removeSocketFile();
198
+ }
199
+ /** Resolves true when the child exits within `ms`, false on timeout. */
200
+ waitForExit(child, ms) {
201
+ if (this.exited)
202
+ return Promise.resolve(true);
203
+ return new Promise((resolve) => {
204
+ const ac = new AbortController();
205
+ child.once('exit', () => ac.abort());
206
+ if (this.exited) {
207
+ resolve(true);
208
+ return;
209
+ }
210
+ sleepWithAbort(ms, ac.signal).then(() => resolve(false), // timer ran out — still alive
211
+ () => resolve(true));
212
+ });
213
+ }
214
+ markExited(code) {
215
+ if (this.exited)
216
+ return;
217
+ this.exited = true;
218
+ this.exitCode = code;
219
+ this.client?.close();
220
+ this.client = null;
221
+ for (const listener of this.exitListeners) {
222
+ try {
223
+ listener(code);
224
+ }
225
+ catch {
226
+ /* listener errors must not break teardown */
227
+ }
228
+ }
229
+ }
230
+ watchStderr(child) {
231
+ let buf = '';
232
+ child.stderr?.on('data', (chunk) => {
233
+ buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
234
+ let nl;
235
+ while ((nl = buf.indexOf('\n')) !== -1) {
236
+ const line = buf.slice(0, nl).trim();
237
+ buf = buf.slice(nl + 1);
238
+ if (line) {
239
+ this.recentStderr.push(line);
240
+ if (this.recentStderr.length > 24)
241
+ this.recentStderr.shift();
242
+ }
243
+ }
244
+ if (buf.length > 64 * 1024)
245
+ buf = buf.slice(-64 * 1024);
246
+ });
247
+ }
248
+ describeEarlyExit() {
249
+ return (`signal-cli daemon exited during startup (code=${this.exitCode ?? 'null'})` + this.stderrTailSuffix());
250
+ }
251
+ stderrTailSuffix() {
252
+ const tail = this.recentStderr.slice(-6).join('\n').trim();
253
+ return tail ? `:\n${tail}` : '';
254
+ }
255
+ removeSocketFile() {
256
+ try {
257
+ fs.rmSync(this.socketPath, { force: true });
258
+ }
259
+ catch {
260
+ /* best-effort */
261
+ }
262
+ }
263
+ }
264
+ /** Matches both the current `sgnl://linkdevice?...` URI and the legacy `tsdevice:/?...` form. */
265
+ const LINK_URI_RE = /(sgnl:\/\/linkdevice\S+|tsdevice:\/?\?\S+)/;
266
+ /** signal-cli prints `Associated with: +49… (device id: N)` on success. */
267
+ const ASSOCIATED_RE = /Associated with:\s*(\+\d{7,15})/;
268
+ /**
269
+ * Run `signal-cli link -n <deviceName>`: it prints the linking URI, then blocks
270
+ * until the phone scans it (or the QR expires, at which point it exits
271
+ * non-zero). Separate from the daemon — linking happens BEFORE an account
272
+ * exists locally, so the daemon (which requires one) can't do it.
273
+ */
274
+ export function startLinkProcess(opts) {
275
+ const spawnFn = opts.spawnFn ?? defaultSpawn;
276
+ const binary = opts.binary ?? 'signal-cli';
277
+ let child;
278
+ let output = '';
279
+ let resolveUri;
280
+ let rejectUri;
281
+ const uri = new Promise((resolve, reject) => {
282
+ resolveUri = resolve;
283
+ rejectUri = reject;
284
+ });
285
+ let resolveCompleted;
286
+ let rejectCompleted;
287
+ const completed = new Promise((resolve, reject) => {
288
+ resolveCompleted = resolve;
289
+ rejectCompleted = reject;
290
+ });
291
+ // The two promises settle independently of consumption order (a caller may
292
+ // only await `uri` and cancel later) — pre-attach no-op catches so an
293
+ // unobserved rejection is never an unhandledRejection.
294
+ uri.catch(() => undefined);
295
+ completed.catch(() => undefined);
296
+ try {
297
+ child = spawnFn(binary, ['link', '-n', opts.deviceName]);
298
+ }
299
+ catch (err) {
300
+ const e = new Error(`failed to spawn ${binary} link: ${err instanceof Error ? err.message : String(err)}`);
301
+ rejectUri(e);
302
+ rejectCompleted(e);
303
+ return { uri, completed, cancel: () => undefined };
304
+ }
305
+ let uriSettled = false;
306
+ const scan = (chunk) => {
307
+ output += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
308
+ if (output.length > 256 * 1024)
309
+ output = output.slice(-256 * 1024);
310
+ if (!uriSettled) {
311
+ const m = LINK_URI_RE.exec(output);
312
+ if (m?.[1]) {
313
+ uriSettled = true;
314
+ resolveUri(m[1]);
315
+ }
316
+ }
317
+ };
318
+ child.stdout?.on('data', scan);
319
+ child.stderr?.on('data', scan);
320
+ child.once('error', (err) => {
321
+ const e = new Error(`signal-cli link failed to start: ${err.message}`);
322
+ if (!uriSettled) {
323
+ uriSettled = true;
324
+ rejectUri(e);
325
+ }
326
+ rejectCompleted(e);
327
+ });
328
+ child.once('exit', (code) => {
329
+ if (code === 0) {
330
+ const account = ASSOCIATED_RE.exec(output)?.[1] ?? null;
331
+ resolveCompleted({ account });
332
+ }
333
+ else {
334
+ const tail = output.split('\n').slice(-4).join('\n').trim();
335
+ const e = new Error(`signal-cli link exited with code ${code ?? 'null'} (QR expired or linking rejected)` +
336
+ (tail ? `:\n${tail}` : ''));
337
+ if (!uriSettled) {
338
+ uriSettled = true;
339
+ rejectUri(e);
340
+ }
341
+ rejectCompleted(e);
342
+ }
343
+ });
344
+ return {
345
+ uri,
346
+ completed,
347
+ cancel: () => {
348
+ try {
349
+ child.kill('SIGTERM');
350
+ }
351
+ catch {
352
+ /* already gone */
353
+ }
354
+ },
355
+ };
356
+ }
357
+ /**
358
+ * List the accounts registered in the local signal-cli store (a one-shot
359
+ * `signal-cli --output=json listAccounts` spawn). Used at channel start to
360
+ * decide "linked already?" — NOT in `isAvailable` (JVM spawns are too slow for
361
+ * a discovery probe). Parses JSON output and falls back to scraping `+<digits>`
362
+ * so an older plain-text signal-cli still works.
363
+ */
364
+ export async function listSignalAccounts(opts = {}) {
365
+ const spawnFn = opts.spawnFn ?? defaultSpawn;
366
+ const binary = opts.binary ?? 'signal-cli';
367
+ const timeoutMs = opts.timeoutMs ?? 30_000;
368
+ let child;
369
+ try {
370
+ child = spawnFn(binary, ['--output=json', 'listAccounts']);
371
+ }
372
+ catch (err) {
373
+ throw new Error(`failed to spawn ${binary}: ${err instanceof Error ? err.message : String(err)}`);
374
+ }
375
+ let out = '';
376
+ child.stdout?.on('data', (chunk) => {
377
+ out += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
378
+ if (out.length > 1024 * 1024)
379
+ out = out.slice(-1024 * 1024);
380
+ });
381
+ const exited = await new Promise((resolve) => {
382
+ const ac = new AbortController();
383
+ child.once('exit', () => ac.abort());
384
+ child.once('error', () => ac.abort());
385
+ sleepWithAbort(timeoutMs, ac.signal).then(() => resolve(false), () => resolve(true));
386
+ });
387
+ if (!exited) {
388
+ try {
389
+ child.kill('SIGKILL');
390
+ }
391
+ catch {
392
+ /* ignore */
393
+ }
394
+ throw new Error(`signal-cli listAccounts timed out after ${timeoutMs}ms`);
395
+ }
396
+ const accounts = new Set();
397
+ for (const line of out.split('\n')) {
398
+ const trimmed = line.trim();
399
+ if (!trimmed)
400
+ continue;
401
+ try {
402
+ const parsed = JSON.parse(trimmed);
403
+ const collect = (entry) => {
404
+ const number = entry?.number;
405
+ if (typeof number === 'string' && number.startsWith('+'))
406
+ accounts.add(number);
407
+ };
408
+ if (Array.isArray(parsed))
409
+ parsed.forEach(collect);
410
+ else
411
+ collect(parsed);
412
+ }
413
+ catch {
414
+ const m = /(\+\d{7,15})/.exec(trimmed);
415
+ if (m?.[1])
416
+ accounts.add(m[1]);
417
+ }
418
+ }
419
+ return [...accounts];
420
+ }
421
+ //# sourceMappingURL=sidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.js","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@moxxy/plugin-channel-signal",
3
+ "version": "0.27.0",
4
+ "description": "Signal messenger channel for moxxy via a signal-cli JSON-RPC sidecar. Links as a secondary Signal device (QR pairing), drives the Session from allow-listed senders (Note to Self by default), and replies with buffered chunked sends. Autonomous allow-list permission model.",
5
+ "keywords": [
6
+ "moxxy",
7
+ "agent",
8
+ "channel",
9
+ "signal",
10
+ "signal-cli",
11
+ "bot"
12
+ ],
13
+ "homepage": "https://moxxy.ai",
14
+ "bugs": {
15
+ "url": "https://github.com/moxxy-ai/moxxy/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/moxxy-ai/moxxy.git",
20
+ "directory": "packages/plugin-channel-signal"
21
+ },
22
+ "author": "Michal Makowski <michal.makowski97@gmail.com>",
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src"
39
+ ],
40
+ "moxxy": {
41
+ "plugin": {
42
+ "entry": "./dist/index.js",
43
+ "kind": "channel"
44
+ },
45
+ "requirements": [
46
+ {
47
+ "kind": "plugin",
48
+ "name": "@moxxy/plugin-vault",
49
+ "hint": "signal resolves the vault from the service registry for its account number + sender allow-list; @moxxy/plugin-vault must load first"
50
+ }
51
+ ],
52
+ "setup": {
53
+ "title": "Signal account",
54
+ "description": "The phone number of the Signal account moxxy links to as a secondary device (like Signal Desktop). No API token: run `moxxy channels signal pair` afterwards and scan the QR with your phone (Signal → Settings → Linked Devices → Link New Device). Requires the signal-cli binary on PATH (`brew install signal-cli`).",
55
+ "fields": [
56
+ {
57
+ "key": "account",
58
+ "label": "Signal account number (E.164, e.g. +15551234567)",
59
+ "kind": "text",
60
+ "vaultKey": "signal_account",
61
+ "required": true
62
+ }
63
+ ]
64
+ }
65
+ },
66
+ "dependencies": {
67
+ "@clack/prompts": "^1.4.0",
68
+ "qrcode": "^1.5.4",
69
+ "zod": "^3.24.0",
70
+ "@moxxy/plugin-vault": "0.27.0",
71
+ "@moxxy/core": "0.27.0",
72
+ "@moxxy/sdk": "0.27.0",
73
+ "@moxxy/channel-kit": "0.27.0"
74
+ },
75
+ "devDependencies": {
76
+ "@types/node": "^22.10.0",
77
+ "@types/qrcode": "^1.5.5",
78
+ "typescript": "^5.7.3",
79
+ "vitest": "^2.1.8",
80
+ "@moxxy/tsconfig": "0.0.0",
81
+ "@moxxy/vitest-preset": "0.0.0"
82
+ },
83
+ "scripts": {
84
+ "build": "tsc -p tsconfig.json",
85
+ "typecheck": "tsc -p tsconfig.json --noEmit",
86
+ "test": "vitest run",
87
+ "clean": "rm -rf dist .turbo"
88
+ }
89
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ ChunkedSender,
4
+ SIGNAL_CHUNK_HARD_LIMIT,
5
+ splitForSignal,
6
+ takeChunk,
7
+ } from './chunker.js';
8
+
9
+ describe('takeChunk', () => {
10
+ it('holds short text for the final flush', () => {
11
+ expect(takeChunk('hello world')).toBeNull();
12
+ expect(takeChunk('x'.repeat(1_500))).toBeNull();
13
+ });
14
+
15
+ it('splits at a paragraph boundary once past the soft limit', () => {
16
+ const text = 'a'.repeat(1_200) + '\n\n' + 'b'.repeat(600);
17
+ const taken = takeChunk(text);
18
+ expect(taken).not.toBeNull();
19
+ expect(taken!.chunk).toBe('a'.repeat(1_200));
20
+ expect(taken!.rest).toBe('b'.repeat(600));
21
+ });
22
+
23
+ it('falls back to newline, then word boundaries', () => {
24
+ const nl = 'a'.repeat(1_200) + '\n' + 'b'.repeat(600);
25
+ expect(takeChunk(nl)!.chunk).toBe('a'.repeat(1_200));
26
+ const word = 'a'.repeat(1_200) + ' ' + 'b'.repeat(600);
27
+ expect(takeChunk(word)!.chunk).toBe('a'.repeat(1_200));
28
+ });
29
+
30
+ it('never returns a chunk above the hard limit (hard cut when boundary-less)', () => {
31
+ const text = 'x'.repeat(SIGNAL_CHUNK_HARD_LIMIT + 500);
32
+ const taken = takeChunk(text);
33
+ expect(taken).not.toBeNull();
34
+ expect(taken!.chunk.length).toBe(SIGNAL_CHUNK_HARD_LIMIT);
35
+ expect(taken!.rest.length).toBe(500);
36
+ });
37
+
38
+ it('waits for more text when boundary-less but under the hard limit', () => {
39
+ expect(takeChunk('x'.repeat(1_800))).toBeNull();
40
+ });
41
+
42
+ it('ignores boundaries too early to form a meaningful chunk', () => {
43
+ const text = '\n\n' + 'x'.repeat(1_700);
44
+ // Only boundary is at position 0 — below the minimum cut, so buffer.
45
+ expect(takeChunk(text)).toBeNull();
46
+ });
47
+ });
48
+
49
+ describe('splitForSignal', () => {
50
+ it('returns short text as a single message', () => {
51
+ expect(splitForSignal('hi')).toEqual(['hi']);
52
+ });
53
+
54
+ it('drops pure whitespace', () => {
55
+ expect(splitForSignal(' \n ')).toEqual([]);
56
+ });
57
+
58
+ it('splits long text into under-limit pieces', () => {
59
+ const paragraphs = Array.from({ length: 8 }, (_, i) => `${'p'.repeat(700)}${i}`).join('\n\n');
60
+ const parts = splitForSignal(paragraphs);
61
+ expect(parts.length).toBeGreaterThan(1);
62
+ for (const part of parts) expect(part.length).toBeLessThanOrEqual(SIGNAL_CHUNK_HARD_LIMIT);
63
+ // Nothing lost (modulo boundary whitespace).
64
+ expect(parts.join('')).toContain('p'.repeat(700) + '7');
65
+ });
66
+ });
67
+
68
+ describe('ChunkedSender', () => {
69
+ function collector(): { sent: string[]; send: (t: string) => Promise<void> } {
70
+ const sent: string[] = [];
71
+ return {
72
+ sent,
73
+ send: async (t) => {
74
+ sent.push(t);
75
+ },
76
+ };
77
+ }
78
+
79
+ it('buffers below the soft limit and flushes once on finalize', async () => {
80
+ const { sent, send } = collector();
81
+ const sender = new ChunkedSender({ send });
82
+ sender.offer('Hello');
83
+ sender.offer('Hello world');
84
+ await sender.finalize('Hello world, done.');
85
+ expect(sent).toEqual(['Hello world, done.']);
86
+ });
87
+
88
+ it('streams paragraph chunks as the snapshot grows, then sends only the remainder', async () => {
89
+ const { sent, send } = collector();
90
+ const sender = new ChunkedSender({ send, limits: { softLimit: 20, hardLimit: 60 } });
91
+ sender.offer('first paragraph text\n\nsecond');
92
+ await sender.finalize('first paragraph text\n\nsecond part is done');
93
+ expect(sent).toEqual(['first paragraph text', 'second part is done']);
94
+ });
95
+
96
+ it('sends the empty-turn placeholder only when nothing was ever sent', async () => {
97
+ const { sent, send } = collector();
98
+ const sender = new ChunkedSender({ send });
99
+ await sender.finalize(' ', '(no output)');
100
+ expect(sent).toEqual(['(no output)']);
101
+ });
102
+
103
+ it('does not send the placeholder when chunks already went out', async () => {
104
+ const { sent, send } = collector();
105
+ const sender = new ChunkedSender({ send, limits: { softLimit: 5, hardLimit: 30 } });
106
+ sender.offer('streamed text already sent\n\nx');
107
+ await sender.finalize('', '(no output)');
108
+ expect(sent).toEqual(['streamed text already sent']);
109
+ });
110
+
111
+ it('re-sends the authoritative final text when it diverges from the streamed prefix', async () => {
112
+ const { sent, send } = collector();
113
+ const sender = new ChunkedSender({ send, limits: { softLimit: 10, hardLimit: 40 } });
114
+ sender.offer('streamed draft one\n\nmore text here');
115
+ await sender.finalize('a completely different final answer');
116
+ expect(sent[0]).toBe('streamed draft one');
117
+ expect(sent).toContain('a completely different final answer');
118
+ });
119
+
120
+ it('serializes sends in order', async () => {
121
+ const order: string[] = [];
122
+ const sender = new ChunkedSender({
123
+ send: async (t) => {
124
+ // Simulate slow transport; a racing offer must not interleave.
125
+ await new Promise((r) => setTimeout(r, 5));
126
+ order.push(t);
127
+ },
128
+ limits: { softLimit: 4, hardLimit: 12 },
129
+ });
130
+ sender.offer('one two\n\n');
131
+ sender.offer('one two\n\nthree four\n\n');
132
+ await sender.finalize('one two\n\nthree four\n\nfive');
133
+ expect(order).toEqual(['one two', 'three four', 'five']);
134
+ });
135
+ });