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