spectrum-ts 4.2.0 → 5.1.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 (46) hide show
  1. package/README.md +29 -67
  2. package/dist/authoring.d.ts +1 -6
  3. package/dist/authoring.js +2 -36
  4. package/dist/elysia.d.ts +1 -94
  5. package/dist/elysia.js +2 -15
  6. package/dist/express.d.ts +1 -62
  7. package/dist/express.js +2 -19
  8. package/dist/hono.d.ts +1 -64
  9. package/dist/hono.js +2 -11
  10. package/dist/index.d.ts +1 -2851
  11. package/dist/index.js +2 -3763
  12. package/dist/manifest.json +5 -5
  13. package/dist/providers/imessage/index.d.ts +1 -222
  14. package/dist/providers/imessage/index.js +2 -25
  15. package/dist/providers/index.d.ts +6 -19
  16. package/dist/providers/index.js +6 -34
  17. package/dist/providers/slack/index.d.ts +1 -46
  18. package/dist/providers/slack/index.js +2 -11
  19. package/dist/providers/telegram/index.d.ts +1 -45
  20. package/dist/providers/telegram/index.js +2 -13
  21. package/dist/providers/terminal/index.d.ts +1 -119
  22. package/dist/providers/terminal/index.js +2 -13
  23. package/dist/providers/whatsapp-business/index.d.ts +1 -27
  24. package/dist/providers/whatsapp-business/index.js +2 -14
  25. package/package.json +11 -38
  26. package/dist/attachment-CnivEhr6.d.ts +0 -29
  27. package/dist/authoring-b9AhXgPI.d.ts +0 -304
  28. package/dist/chunk-2D27WW5B.js +0 -63
  29. package/dist/chunk-34FQGGD7.js +0 -34
  30. package/dist/chunk-3GEJYGZK.js +0 -84
  31. package/dist/chunk-5XEFJBN2.js +0 -197
  32. package/dist/chunk-6UZFVXQF.js +0 -374
  33. package/dist/chunk-A37PM5N2.js +0 -91
  34. package/dist/chunk-ARL2NOBO.js +0 -887
  35. package/dist/chunk-B52VPQO3.js +0 -1379
  36. package/dist/chunk-DMPDLSFU.js +0 -864
  37. package/dist/chunk-FAIFTUV2.js +0 -139
  38. package/dist/chunk-LZXPLXZF.js +0 -35
  39. package/dist/chunk-N6THJDZV.js +0 -929
  40. package/dist/chunk-NLMQ75LH.js +0 -2980
  41. package/dist/chunk-UXAKIXVM.js +0 -409
  42. package/dist/chunk-WXLQNANA.js +0 -539
  43. package/dist/chunk-ZR3TKZMT.js +0 -129
  44. package/dist/read-C4uvozGX.d.ts +0 -53
  45. package/dist/types-CyfLJXgu.d.ts +0 -1530
  46. package/dist/types-ZgFTj5hJ.d.ts +0 -87
@@ -1,887 +0,0 @@
1
- import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
- import {
3
- asVoice
4
- } from "./chunk-FAIFTUV2.js";
5
- import {
6
- asContact
7
- } from "./chunk-A37PM5N2.js";
8
- import {
9
- fromVCard,
10
- toVCard
11
- } from "./chunk-6UZFVXQF.js";
12
- import {
13
- stream
14
- } from "./chunk-5XEFJBN2.js";
15
- import {
16
- UnsupportedError,
17
- definePlatform
18
- } from "./chunk-B52VPQO3.js";
19
- import {
20
- asAttachment,
21
- asCustom,
22
- reactionSchema
23
- } from "./chunk-UXAKIXVM.js";
24
-
25
- // src/providers/terminal/index.ts
26
- import { spawn } from "child_process";
27
- import { createServer } from "net";
28
- import { inspect } from "util";
29
- import z from "zod";
30
-
31
- // src/providers/terminal/protocol.ts
32
- var HEADER_TERMINATOR = Buffer.from("\r\n\r\n");
33
- var CONTENT_LENGTH = "content-length:";
34
- function encode(message) {
35
- const body = Buffer.from(JSON.stringify(message), "utf8");
36
- const header = Buffer.from(`Content-Length: ${body.byteLength}\r
37
- \r
38
- `);
39
- const out = new Uint8Array(header.byteLength + body.byteLength);
40
- out.set(header, 0);
41
- out.set(body, header.byteLength);
42
- return out;
43
- }
44
- var Decoder = class {
45
- buf = Buffer.alloc(0);
46
- push(chunk) {
47
- this.buf = this.buf.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.buf, chunk]);
48
- const out = [];
49
- for (; ; ) {
50
- const msg = this.readOne();
51
- if (!msg) {
52
- break;
53
- }
54
- out.push(msg);
55
- }
56
- return out;
57
- }
58
- readOne() {
59
- const end = this.buf.indexOf(HEADER_TERMINATOR);
60
- if (end < 0) {
61
- return null;
62
- }
63
- const header = this.buf.subarray(0, end).toString("utf8");
64
- let len = -1;
65
- for (const line of header.split("\r\n")) {
66
- if (line.toLowerCase().startsWith(CONTENT_LENGTH)) {
67
- const n = Number.parseInt(line.slice(CONTENT_LENGTH.length).trim(), 10);
68
- if (!Number.isFinite(n) || n < 0) {
69
- throw new Error("invalid Content-Length");
70
- }
71
- len = n;
72
- }
73
- }
74
- if (len < 0) {
75
- throw new Error("missing Content-Length header");
76
- }
77
- const bodyStart = end + HEADER_TERMINATOR.length;
78
- const bodyEnd = bodyStart + len;
79
- if (this.buf.length < bodyEnd) {
80
- return null;
81
- }
82
- const body = this.buf.subarray(bodyStart, bodyEnd).toString("utf8");
83
- this.buf = this.buf.subarray(bodyEnd);
84
- return JSON.parse(body);
85
- }
86
- };
87
- var RpcSession = class {
88
- decoder = new Decoder();
89
- nextId = 1;
90
- pending = /* @__PURE__ */ new Map();
91
- onNotify = null;
92
- onClose = null;
93
- closed = false;
94
- socket;
95
- constructor(socket) {
96
- this.socket = socket;
97
- socket.on("data", (chunk) => this.handle(chunk));
98
- socket.on("close", () => this.shutdown());
99
- socket.on("error", () => this.shutdown());
100
- }
101
- handleNotifications(h) {
102
- this.onNotify = h;
103
- }
104
- onClosed(h) {
105
- this.onClose = h;
106
- }
107
- async request(method, params, timeoutMs) {
108
- if (this.closed) {
109
- throw new Error("session closed");
110
- }
111
- const id = this.nextId++;
112
- const msg = { jsonrpc: "2.0", id, method, params };
113
- return new Promise((resolve, reject) => {
114
- let settled = false;
115
- let timer;
116
- const done = () => {
117
- settled = true;
118
- if (timer) {
119
- clearTimeout(timer);
120
- }
121
- };
122
- this.pending.set(id, {
123
- resolve: (v) => {
124
- if (settled) {
125
- return;
126
- }
127
- done();
128
- resolve(v);
129
- },
130
- reject: (e) => {
131
- if (settled) {
132
- return;
133
- }
134
- done();
135
- reject(e);
136
- }
137
- });
138
- if (timeoutMs !== void 0 && timeoutMs >= 0) {
139
- timer = setTimeout(() => {
140
- if (settled) {
141
- return;
142
- }
143
- settled = true;
144
- this.pending.delete(id);
145
- reject(new Error(`rpc ${method} timed out after ${timeoutMs}ms`));
146
- }, timeoutMs);
147
- timer.unref?.();
148
- }
149
- try {
150
- this.socket.write(encode(msg));
151
- } catch (err) {
152
- if (settled) {
153
- return;
154
- }
155
- done();
156
- this.pending.delete(id);
157
- reject(err);
158
- }
159
- });
160
- }
161
- notify(method, params) {
162
- if (this.closed) {
163
- return;
164
- }
165
- const msg = { jsonrpc: "2.0", method, params };
166
- try {
167
- this.socket.write(encode(msg));
168
- } catch {
169
- }
170
- }
171
- close() {
172
- this.shutdown();
173
- }
174
- handle(chunk) {
175
- let msgs;
176
- try {
177
- msgs = this.decoder.push(chunk);
178
- } catch {
179
- this.shutdown();
180
- return;
181
- }
182
- for (const m of msgs) {
183
- if ("id" in m && "method" in m) {
184
- continue;
185
- }
186
- if ("id" in m) {
187
- const p = this.pending.get(m.id);
188
- if (!p) {
189
- continue;
190
- }
191
- this.pending.delete(m.id);
192
- if (m.error) {
193
- p.reject(new Error(m.error.message));
194
- } else {
195
- p.resolve(m.result);
196
- }
197
- } else if ("method" in m) {
198
- try {
199
- this.onNotify?.(m.method, m.params);
200
- } catch {
201
- }
202
- }
203
- }
204
- }
205
- shutdown() {
206
- if (this.closed) {
207
- return;
208
- }
209
- this.closed = true;
210
- for (const p of this.pending.values()) {
211
- p.reject(new Error("session closed"));
212
- }
213
- this.pending.clear();
214
- try {
215
- this.socket.end();
216
- } catch {
217
- }
218
- try {
219
- this.socket.destroy();
220
- } catch {
221
- }
222
- this.onClose?.();
223
- }
224
- };
225
-
226
- // src/providers/terminal/resolve-binary.ts
227
- import { createHash, randomBytes } from "crypto";
228
- import {
229
- chmodSync,
230
- existsSync,
231
- mkdirSync,
232
- renameSync,
233
- unlinkSync,
234
- writeFileSync
235
- } from "fs";
236
- import { homedir } from "os";
237
- import { join } from "path";
238
- var DEFAULT_VERSION = "0.1.4";
239
- var REPO = "photon-hq/tuichat";
240
- var VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
241
- var DOWNLOAD_TIMEOUT_MS = 3e4;
242
- function targetSuffix() {
243
- const key = `${process.platform}-${process.arch}`;
244
- const map = {
245
- "darwin-arm64": "darwin-arm64",
246
- "darwin-x64": "darwin-x64",
247
- "linux-x64": "linux-x64",
248
- "linux-arm64": "linux-arm64",
249
- "win32-x64": "windows-x64"
250
- };
251
- const t = map[key];
252
- if (!t) {
253
- throw new Error(`tuichat: unsupported platform/arch: ${key}`);
254
- }
255
- return t;
256
- }
257
- function cacheDir(version) {
258
- if (process.platform === "win32") {
259
- const base = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
260
- return join(base, "tuichat", `v${version}`);
261
- }
262
- if (process.platform === "darwin") {
263
- return join(homedir(), "Library", "Caches", "tuichat", `v${version}`);
264
- }
265
- const xdg = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
266
- return join(xdg, "tuichat", `v${version}`);
267
- }
268
- var LINE_SPLIT = /\r?\n/;
269
- var CHECKSUM_LINE = /^([a-f0-9]{64})\s+\*?(\S+)$/;
270
- function parseChecksums(text) {
271
- const out = {};
272
- for (const line of text.split(LINE_SPLIT)) {
273
- const m = line.match(CHECKSUM_LINE);
274
- if (m?.[1] && m[2]) {
275
- out[m[2]] = m[1];
276
- }
277
- }
278
- return out;
279
- }
280
- async function downloadVerified(version, filename) {
281
- const base = `https://github.com/${REPO}/releases/download/v${version}`;
282
- const controller = new AbortController();
283
- const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
284
- let sumsRes;
285
- let binRes;
286
- try {
287
- [sumsRes, binRes] = await Promise.all([
288
- fetch(`${base}/SHA256SUMS`, { signal: controller.signal }),
289
- fetch(`${base}/${filename}`, { signal: controller.signal })
290
- ]);
291
- } catch (err) {
292
- if (err instanceof Error && err.name === "AbortError") {
293
- throw new Error(
294
- `tuichat: timed out fetching v${version} release assets after ${DOWNLOAD_TIMEOUT_MS}ms`
295
- );
296
- }
297
- throw err;
298
- } finally {
299
- clearTimeout(timer);
300
- }
301
- if (!sumsRes.ok) {
302
- throw new Error(
303
- `tuichat: failed to fetch SHA256SUMS (v${version}): HTTP ${sumsRes.status}`
304
- );
305
- }
306
- if (!binRes.ok) {
307
- throw new Error(
308
- `tuichat: failed to fetch ${filename} (v${version}): HTTP ${binRes.status}`
309
- );
310
- }
311
- const expected = parseChecksums(await sumsRes.text())[filename];
312
- if (!expected) {
313
- throw new Error(
314
- `tuichat: no checksum for ${filename} in SHA256SUMS (v${version})`
315
- );
316
- }
317
- const bytes = Buffer.from(await binRes.arrayBuffer());
318
- const actual = createHash("sha256").update(bytes).digest("hex");
319
- if (actual !== expected) {
320
- throw new Error(
321
- `tuichat: checksum mismatch for ${filename} (expected ${expected}, got ${actual})`
322
- );
323
- }
324
- return bytes;
325
- }
326
- function writeBinary(path, bytes) {
327
- const tmpPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
328
- try {
329
- writeFileSync(tmpPath, bytes);
330
- if (process.platform !== "win32") {
331
- chmodSync(tmpPath, 493);
332
- }
333
- renameSync(tmpPath, path);
334
- } catch (err) {
335
- const renameErr = err;
336
- try {
337
- unlinkSync(tmpPath);
338
- } catch {
339
- }
340
- if (process.platform === "win32" && renameErr.code === "EEXIST" && existsSync(path)) {
341
- return;
342
- }
343
- throw err;
344
- }
345
- }
346
- async function resolveTuichatBinary(options = {}) {
347
- const override = process.env.TUICHAT_BINARY;
348
- if (override) {
349
- if (!existsSync(override)) {
350
- throw new Error(`tuichat: TUICHAT_BINARY=${override} does not exist`);
351
- }
352
- return override;
353
- }
354
- const version = options.version ?? process.env.TUICHAT_VERSION ?? DEFAULT_VERSION;
355
- if (!VERSION_RE.test(version)) {
356
- throw new Error(
357
- `tuichat: invalid version "${version}" \u2014 expected semver like 0.1.4`
358
- );
359
- }
360
- const target = targetSuffix();
361
- const ext = target.startsWith("windows") ? ".exe" : "";
362
- const filename = `tuichat-${target}${ext}`;
363
- const dir = cacheDir(version);
364
- const path = join(dir, filename);
365
- if (!options.force && existsSync(path)) {
366
- return path;
367
- }
368
- const bytes = await downloadVerified(version, filename);
369
- mkdirSync(dir, { recursive: true });
370
- writeBinary(path, bytes);
371
- return path;
372
- }
373
-
374
- // src/providers/terminal/index.ts
375
- var SHUTDOWN_TIMEOUT_MS = 2e3;
376
- var SPAWN_CONNECT_TIMEOUT_MS = 1e4;
377
- var INITIALIZE_TIMEOUT_MS = 1e4;
378
- var commandSchema = z.object({
379
- name: z.string().regex(/^\/[A-Za-z0-9_-]+$/, "command must start with /"),
380
- description: z.string().optional()
381
- });
382
- var LOG_LEVELS = ["log", "info", "warn", "error", "debug"];
383
- function installConsoleHijack(session) {
384
- const originals = {};
385
- let forwarding = false;
386
- for (const level of LOG_LEVELS) {
387
- originals[level] = console[level].bind(console);
388
- console[level] = (...args) => {
389
- if (forwarding) {
390
- originals[level](...args);
391
- return;
392
- }
393
- forwarding = true;
394
- try {
395
- const text = args.map(
396
- (a) => typeof a === "string" ? a : inspect(a, { depth: 3, colors: false })
397
- ).join(" ");
398
- session.notify("log", { level, text });
399
- } finally {
400
- forwarding = false;
401
- }
402
- };
403
- }
404
- return {
405
- restore: () => {
406
- for (const level of LOG_LEVELS) {
407
- console[level] = originals[level];
408
- }
409
- }
410
- };
411
- }
412
- function generateChatId(client) {
413
- while (client.knownChats.has(`chat-${client.nextChatIndex}`)) {
414
- client.nextChatIndex += 1;
415
- }
416
- const id = `chat-${client.nextChatIndex}`;
417
- client.nextChatIndex += 1;
418
- client.knownChats.add(id);
419
- return id;
420
- }
421
- function makeEventQueue() {
422
- const queue = [];
423
- const waiters = [];
424
- let closed = false;
425
- const drain = () => {
426
- while (waiters.length > 0) {
427
- waiters.shift()?.({ value: void 0, done: true });
428
- }
429
- };
430
- const iter = {
431
- [Symbol.asyncIterator]() {
432
- return {
433
- next() {
434
- if (closed && queue.length === 0) {
435
- return Promise.resolve({ value: void 0, done: true });
436
- }
437
- const buffered = queue.shift();
438
- if (buffered !== void 0) {
439
- return Promise.resolve({ value: buffered, done: false });
440
- }
441
- return new Promise((resolve) => waiters.push(resolve));
442
- },
443
- // return() fires when the consumer's for-await-of loop breaks or
444
- // when Spectrum.stop() calls iterator.return() upstream. Without
445
- // this, a pending next() would hang forever because no further
446
- // push/close is coming. Close + drain so shutdown is always prompt.
447
- return() {
448
- closed = true;
449
- drain();
450
- return Promise.resolve({ value: void 0, done: true });
451
- }
452
- };
453
- }
454
- };
455
- return {
456
- iter,
457
- push(v) {
458
- if (closed) {
459
- return;
460
- }
461
- const w = waiters.shift();
462
- if (w) {
463
- w({ value: v, done: false });
464
- } else {
465
- queue.push(v);
466
- }
467
- },
468
- close() {
469
- closed = true;
470
- drain();
471
- }
472
- };
473
- }
474
- async function spawnClient(options) {
475
- const binary = await resolveTuichatBinary();
476
- const server = createServer();
477
- await new Promise((resolve, reject) => {
478
- server.once("error", reject);
479
- server.listen({ host: "127.0.0.1", port: 0 }, () => {
480
- server.off("error", reject);
481
- resolve();
482
- });
483
- });
484
- const addr = server.address();
485
- if (!addr || typeof addr === "string") {
486
- server.close();
487
- throw new Error("tuichat: failed to bind adapter listener");
488
- }
489
- const host = "127.0.0.1";
490
- const port = addr.port;
491
- const proc = spawn(binary, ["--connect", `${host}:${port}`], {
492
- stdio: "inherit"
493
- });
494
- proc.unref();
495
- proc.once("exit", (code) => {
496
- if (code !== 0 && code !== null) {
497
- process.stderr.write(`[tuichat] subprocess exited with code ${code}
498
- `);
499
- }
500
- });
501
- const socket = await new Promise((resolve, reject) => {
502
- let settled = false;
503
- const cleanup = () => {
504
- clearTimeout(timer);
505
- server.off("connection", onConnect);
506
- server.off("error", onServerError);
507
- proc.off("error", onProcError);
508
- proc.off("exit", onProcExit);
509
- };
510
- const fail = (err, killProc) => {
511
- if (settled) {
512
- return;
513
- }
514
- settled = true;
515
- cleanup();
516
- server.close();
517
- if (killProc && !proc.killed) {
518
- try {
519
- proc.kill();
520
- } catch {
521
- }
522
- }
523
- reject(err);
524
- };
525
- const succeed = (sock) => {
526
- if (settled) {
527
- return;
528
- }
529
- settled = true;
530
- cleanup();
531
- server.close();
532
- resolve(sock);
533
- };
534
- const onConnect = (sock) => succeed(sock);
535
- const onServerError = (err) => fail(err, true);
536
- const onProcError = (err) => fail(err, false);
537
- const onProcExit = (code, signal) => fail(
538
- new Error(
539
- `tuichat: subprocess exited before connecting (code=${code ?? "null"}, signal=${signal ?? "null"})`
540
- ),
541
- false
542
- );
543
- const timer = setTimeout(() => {
544
- fail(
545
- new Error(
546
- `tuichat: subprocess did not connect within ${SPAWN_CONNECT_TIMEOUT_MS}ms`
547
- ),
548
- true
549
- );
550
- }, SPAWN_CONNECT_TIMEOUT_MS);
551
- server.once("connection", onConnect);
552
- server.once("error", onServerError);
553
- proc.once("error", onProcError);
554
- proc.once("exit", onProcExit);
555
- });
556
- const session = new RpcSession(socket);
557
- const eventsQ = makeEventQueue();
558
- session.handleNotifications((method, params) => {
559
- if (method === "streamEnd") {
560
- eventsQ.close();
561
- return;
562
- }
563
- if (method === "message") {
564
- eventsQ.push({
565
- kind: "message",
566
- value: params
567
- });
568
- return;
569
- }
570
- if (method === "reaction") {
571
- eventsQ.push({
572
- kind: "reaction",
573
- value: params
574
- });
575
- return;
576
- }
577
- });
578
- let hijack;
579
- session.onClosed(() => {
580
- hijack?.restore();
581
- eventsQ.close();
582
- });
583
- try {
584
- await session.request(
585
- "initialize",
586
- {
587
- commands: options.commands,
588
- clientInfo: { name: "spectrum-ts", version: "terminal-provider" }
589
- },
590
- INITIALIZE_TIMEOUT_MS
591
- );
592
- } catch (err) {
593
- session.close();
594
- try {
595
- proc.kill("SIGTERM");
596
- } catch {
597
- }
598
- throw err;
599
- }
600
- hijack = installConsoleHijack(session);
601
- return {
602
- hijack,
603
- proc,
604
- session,
605
- events: eventsQ.iter,
606
- knownChats: /* @__PURE__ */ new Set(),
607
- nextChatIndex: 1
608
- };
609
- }
610
- function parseTimestamp(s) {
611
- const t = Date.parse(s);
612
- return Number.isNaN(t) ? /* @__PURE__ */ new Date() : new Date(t);
613
- }
614
- function buildOutboundRecord(result, content, spaceId) {
615
- return {
616
- id: result.id,
617
- content,
618
- space: { id: spaceId },
619
- timestamp: parseTimestamp(result.timestamp)
620
- };
621
- }
622
- function reactionTargetFromProtocol(reaction) {
623
- const target = {
624
- id: reaction.messageId,
625
- content: asCustom({ terminal_type: "reaction-target", stub: true }),
626
- sender: { id: "__unknown__" },
627
- space: { id: reaction.spaceId },
628
- timestamp: parseTimestamp(reaction.timestamp)
629
- };
630
- return target;
631
- }
632
- function reactionContentFromProtocol(reaction) {
633
- return reactionSchema.parse({
634
- type: "reaction",
635
- emoji: reaction.reaction,
636
- target: reactionTargetFromProtocol(reaction)
637
- });
638
- }
639
- async function spectrumToProtocol(content) {
640
- if (content.type === "text" || content.type === "custom") {
641
- return content;
642
- }
643
- if (content.type === "attachment") {
644
- const buf = await content.read();
645
- return {
646
- type: "attachment",
647
- name: content.name,
648
- mimeType: content.mimeType,
649
- size: content.size,
650
- bytes: buf.toString("base64")
651
- };
652
- }
653
- if (content.type === "voice") {
654
- const buf = await content.read();
655
- return {
656
- type: "voice",
657
- name: content.name,
658
- mimeType: content.mimeType,
659
- size: content.size,
660
- bytes: buf.toString("base64")
661
- };
662
- }
663
- if (content.type === "contact") {
664
- return {
665
- type: "contact",
666
- name: content.name ? {
667
- formatted: content.name.formatted,
668
- first: content.name.first,
669
- last: content.name.last
670
- } : void 0,
671
- vcard: await toVCard(content)
672
- };
673
- }
674
- throw UnsupportedError.content(
675
- content.type,
676
- "Terminal"
677
- );
678
- }
679
- function protocolToSpectrum(p) {
680
- if (p.type === "text" || p.type === "custom") {
681
- return p;
682
- }
683
- if (p.type === "attachment" || p.type === "voice") {
684
- const path = p.path;
685
- const bytesB64 = p.bytes;
686
- let cached;
687
- const readBytes = () => {
688
- if (cached) {
689
- return cached;
690
- }
691
- if (bytesB64) {
692
- cached = Promise.resolve(Buffer.from(bytesB64, "base64"));
693
- } else if (path) {
694
- cached = import("fs/promises").then((m) => m.readFile(path));
695
- } else {
696
- cached = Promise.reject(
697
- new Error(`${p.type} has neither path nor bytes`)
698
- );
699
- }
700
- return cached;
701
- };
702
- const stream2 = async () => {
703
- if (path) {
704
- const [{ createReadStream }, { Readable }] = await Promise.all([
705
- import("fs"),
706
- import("stream")
707
- ]);
708
- return Readable.toWeb(
709
- createReadStream(path)
710
- );
711
- }
712
- const buf = await readBytes();
713
- return new ReadableStream({
714
- start(ctrl) {
715
- ctrl.enqueue(new Uint8Array(buf));
716
- ctrl.close();
717
- }
718
- });
719
- };
720
- if (p.type === "attachment") {
721
- return asAttachment({
722
- name: p.name,
723
- mimeType: p.mimeType,
724
- size: p.size,
725
- read: readBytes,
726
- stream: stream2
727
- });
728
- }
729
- return asVoice({
730
- name: p.name,
731
- mimeType: p.mimeType,
732
- size: p.size,
733
- read: readBytes,
734
- stream: stream2
735
- });
736
- }
737
- if (p.type === "contact") {
738
- if (p.vcard) {
739
- try {
740
- return asContact(fromVCard(p.vcard));
741
- } catch {
742
- }
743
- }
744
- return asContact({ name: p.name });
745
- }
746
- return { type: "custom", raw: p };
747
- }
748
- var terminal = definePlatform("Terminal", {
749
- config: z.object({
750
- commands: z.array(commandSchema).optional()
751
- }),
752
- // Declaring a message schema is how extras survive Spectrum's buildMessage
753
- // filter — without it, unknown fields on the yielded message are stripped.
754
- message: {
755
- schema: z.object({
756
- replyTo: z.object({ messageId: z.string() }).optional()
757
- })
758
- },
759
- lifecycle: {
760
- createClient: async ({ config }) => await spawnClient({ commands: config.commands }),
761
- destroyClient: async ({ client }) => {
762
- client.hijack.restore();
763
- try {
764
- await client.session.request(
765
- "shutdown",
766
- void 0,
767
- SHUTDOWN_TIMEOUT_MS
768
- );
769
- } catch {
770
- }
771
- client.session.close();
772
- try {
773
- client.proc.kill("SIGTERM");
774
- } catch {
775
- }
776
- }
777
- },
778
- user: {
779
- resolve: async ({ input }) => ({
780
- id: input.userID
781
- })
782
- },
783
- space: {
784
- create: async ({ client }) => {
785
- const id = generateChatId(client);
786
- client.knownChats.add(id);
787
- await client.session.request("ensureSpace", { id });
788
- return { id };
789
- },
790
- // Explicit (not the framework default) so targeting a known id still
791
- // materializes the chat in the TUI via `ensureSpace`.
792
- get: async ({ client, input }) => {
793
- client.knownChats.add(input.id);
794
- await client.session.request("ensureSpace", { id: input.id });
795
- return { id: input.id };
796
- }
797
- },
798
- // Return a ManagedStream (not a native async generator): a native generator
799
- // parked on an in-flight `client.events.next()` cannot be force-cancelled —
800
- // a `.return()` queues behind the pending `next()` and never reaches the
801
- // event queue, which would deadlock `Spectrum.stop()`. Driving the queue with
802
- // an explicit pump lets cleanup call the queue iterator's `return()` directly
803
- // (synchronous close + drain), so the stream tears down promptly on stop()
804
- // without waiting for destroyClient.
805
- messages({ client }) {
806
- return stream((emit, end) => {
807
- const iterator = client.events[Symbol.asyncIterator]();
808
- const pump = (async () => {
809
- try {
810
- let result = await iterator.next();
811
- while (!result.done) {
812
- const evt = result.value;
813
- if (evt.kind === "message") {
814
- const msg = evt.value;
815
- client.knownChats.add(msg.spaceId);
816
- await emit({
817
- id: msg.id,
818
- content: protocolToSpectrum(msg.content),
819
- sender: { id: msg.senderId },
820
- space: { id: msg.spaceId },
821
- timestamp: parseTimestamp(msg.timestamp),
822
- // replyTo is a terminal-specific extra — agents inspect via a
823
- // cast until Spectrum's message model grows first-class support.
824
- ...msg.replyTo ? { replyTo: msg.replyTo } : {}
825
- });
826
- } else {
827
- const r = evt.value;
828
- client.knownChats.add(r.spaceId);
829
- await emit({
830
- id: `reaction:${r.messageId}:${r.reaction}:${r.timestamp}`,
831
- content: reactionContentFromProtocol(r),
832
- sender: { id: r.senderId },
833
- space: { id: r.spaceId },
834
- timestamp: parseTimestamp(r.timestamp)
835
- });
836
- }
837
- result = await iterator.next();
838
- }
839
- end();
840
- } catch (error) {
841
- end(error);
842
- }
843
- })();
844
- return async () => {
845
- await iterator.return?.();
846
- await pump.catch(() => void 0);
847
- };
848
- });
849
- },
850
- send: async ({ client, content, space }) => {
851
- if (content.type === "reply") {
852
- const inner = await spectrumToProtocol(content.content);
853
- const result2 = await client.session.request("replyToMessage", {
854
- spaceId: space.id,
855
- messageId: content.target.id,
856
- content: inner
857
- });
858
- return buildOutboundRecord(result2, content.content, space.id);
859
- }
860
- if (content.type === "reaction") {
861
- await client.session.request("reactToMessage", {
862
- spaceId: space.id,
863
- messageId: content.target.id,
864
- reaction: content.emoji
865
- });
866
- const timestamp = /* @__PURE__ */ new Date();
867
- return {
868
- id: `reaction:${content.target.id}:${content.emoji}:${timestamp.toISOString()}`,
869
- content,
870
- space: { id: space.id },
871
- timestamp
872
- };
873
- }
874
- if (content.type === "typing") {
875
- const method = content.state === "start" ? "startTyping" : "stopTyping";
876
- await client.session.request(method, { spaceId: space.id });
877
- return;
878
- }
879
- const proto = await spectrumToProtocol(content);
880
- const result = await client.session.request("send", { spaceId: space.id, content: proto });
881
- return buildOutboundRecord(result, content, space.id);
882
- }
883
- });
884
-
885
- export {
886
- terminal
887
- };