spectrum-ts 2.0.0 → 3.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 (32) hide show
  1. package/dist/{attachment-B4nSrKVd.d.ts → attachment-CEpGtZLm.d.ts} +1 -1
  2. package/dist/{authoring-BjE5BvlO.d.ts → authoring-CP3vRza8.d.ts} +61 -26
  3. package/dist/authoring.d.ts +3 -3
  4. package/dist/authoring.js +5 -5
  5. package/dist/{chunk-NNY6LMSC.js → chunk-77U6SH5A.js} +1 -1
  6. package/dist/{chunk-WXY5QP3M.js → chunk-7ON5XHC2.js} +27 -21
  7. package/dist/{chunk-6BI4PFTP.js → chunk-CHY5YLLV.js} +1 -1
  8. package/dist/{chunk-Q537JPTG.js → chunk-FA7VA4XN.js} +10 -10
  9. package/dist/{chunk-NGC4DJIX.js → chunk-L3NUESOW.js} +425 -137
  10. package/dist/{chunk-2ILTJC35.js → chunk-LQMDV75O.js} +205 -11
  11. package/dist/{chunk-3B4QH4JG.js → chunk-MHGCPC2V.js} +1 -1
  12. package/dist/{chunk-U7AWXDH6.js → chunk-NZ5WCMTY.js} +1 -1
  13. package/dist/{chunk-5LT5J3NR.js → chunk-PSSWQBOH.js} +262 -30
  14. package/dist/{chunk-U3LXXT3W.js → chunk-Q44CIGG6.js} +20 -8
  15. package/dist/{chunk-ATNAE7OR.js → chunk-WMG36LHW.js} +676 -159
  16. package/dist/index.d.ts +107 -56
  17. package/dist/index.js +29 -182
  18. package/dist/providers/imessage/index.d.ts +7 -14
  19. package/dist/providers/imessage/index.js +6 -6
  20. package/dist/providers/index.d.ts +3 -3
  21. package/dist/providers/index.js +11 -11
  22. package/dist/providers/slack/index.d.ts +1 -2
  23. package/dist/providers/slack/index.js +3 -3
  24. package/dist/providers/telegram/index.d.ts +3 -5
  25. package/dist/providers/telegram/index.js +5 -5
  26. package/dist/providers/terminal/index.d.ts +2 -4
  27. package/dist/providers/terminal/index.js +5 -5
  28. package/dist/providers/whatsapp-business/index.d.ts +1 -1
  29. package/dist/providers/whatsapp-business/index.js +4 -4
  30. package/dist/{types-Bje8aq1k.d.ts → types-Be0T6E0e.d.ts} +172 -23
  31. package/dist/{types-BD0-kKyv.d.ts → types-CDYXH2R7.d.ts} +1 -1
  32. package/package.json +2 -1
@@ -1,11 +1,11 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
2
  import {
3
3
  asRichlink
4
- } from "./chunk-6BI4PFTP.js";
4
+ } from "./chunk-CHY5YLLV.js";
5
5
  import {
6
6
  asGroup,
7
7
  groupSchema
8
- } from "./chunk-3B4QH4JG.js";
8
+ } from "./chunk-MHGCPC2V.js";
9
9
  import {
10
10
  asPoll,
11
11
  asPollOption
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-3GEJYGZK.js";
16
16
  import {
17
17
  asContact
18
- } from "./chunk-U7AWXDH6.js";
18
+ } from "./chunk-NZ5WCMTY.js";
19
19
  import {
20
20
  mergeStreams,
21
21
  stream
@@ -28,8 +28,9 @@ import {
28
28
  UnsupportedError,
29
29
  buildPhotoAction,
30
30
  definePlatform,
31
+ markdownSchema,
31
32
  photoActionSchema
32
- } from "./chunk-NGC4DJIX.js";
33
+ } from "./chunk-L3NUESOW.js";
33
34
  import {
34
35
  asAttachment,
35
36
  asCustom,
@@ -38,10 +39,13 @@ import {
38
39
  reactionSchema,
39
40
  text,
40
41
  textSchema
41
- } from "./chunk-2ILTJC35.js";
42
+ } from "./chunk-LQMDV75O.js";
42
43
 
43
44
  // src/providers/imessage/index.ts
44
- import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
45
+ import {
46
+ createClient as createClient2,
47
+ MessageEffect as MessageEffect2
48
+ } from "@photon-ai/advanced-imessage";
45
49
  import { IMessageSDK as IMessageSDK2 } from "@photon-ai/imessage-kit";
46
50
  import { withSpan } from "@photon-ai/otel";
47
51
 
@@ -133,6 +137,7 @@ import {
133
137
  import z3 from "zod";
134
138
  var effectInnerSchema = z3.discriminatedUnion("type", [
135
139
  textSchema,
140
+ markdownSchema,
136
141
  attachmentSchema
137
142
  ]);
138
143
  var messageEffectSchema = z3.object({
@@ -153,9 +158,9 @@ function effect(input, messageEffect) {
153
158
  );
154
159
  }
155
160
  const inner = await resolveContent(input);
156
- if (inner.type !== "text" && inner.type !== "attachment") {
161
+ if (inner.type !== "text" && inner.type !== "markdown" && inner.type !== "attachment") {
157
162
  throw new Error(
158
- `imessage effect() only supports text and attachment content, got "${inner.type}"`
163
+ `imessage effect() only supports text, markdown, and attachment content, got "${inner.type}"`
159
164
  );
160
165
  }
161
166
  return messageEffectSchema.parse({
@@ -349,6 +354,77 @@ async function disposeCloudAuth(clients) {
349
354
  }
350
355
  }
351
356
 
357
+ // src/providers/imessage/cache.ts
358
+ var DEFAULT_MAX = 1e3;
359
+ var MessageCache = class {
360
+ map = /* @__PURE__ */ new Map();
361
+ max;
362
+ constructor(max = DEFAULT_MAX) {
363
+ this.max = max;
364
+ }
365
+ get(id) {
366
+ return this.map.get(id);
367
+ }
368
+ set(id, message) {
369
+ if (this.map.has(id)) {
370
+ this.map.delete(id);
371
+ }
372
+ this.map.set(id, message);
373
+ if (this.map.size > this.max) {
374
+ const first = this.map.keys().next().value;
375
+ if (first !== void 0) {
376
+ this.map.delete(first);
377
+ }
378
+ }
379
+ }
380
+ clear() {
381
+ this.map.clear();
382
+ }
383
+ };
384
+ var PollCache = class {
385
+ map = /* @__PURE__ */ new Map();
386
+ max;
387
+ constructor(max = DEFAULT_MAX) {
388
+ this.max = max;
389
+ }
390
+ get(id) {
391
+ return this.map.get(id);
392
+ }
393
+ set(id, poll) {
394
+ if (this.map.has(id)) {
395
+ this.map.delete(id);
396
+ }
397
+ this.map.set(id, poll);
398
+ if (this.map.size > this.max) {
399
+ const first = this.map.keys().next().value;
400
+ if (first !== void 0) {
401
+ this.map.delete(first);
402
+ }
403
+ }
404
+ }
405
+ clear() {
406
+ this.map.clear();
407
+ }
408
+ };
409
+ var messageCaches = /* @__PURE__ */ new WeakMap();
410
+ var pollCaches = /* @__PURE__ */ new WeakMap();
411
+ var getMessageCache = (owner) => {
412
+ let cache = messageCaches.get(owner);
413
+ if (!cache) {
414
+ cache = new MessageCache();
415
+ messageCaches.set(owner, cache);
416
+ }
417
+ return cache;
418
+ };
419
+ var getPollCache = (owner) => {
420
+ let cache = pollCaches.get(owner);
421
+ if (!cache) {
422
+ cache = new PollCache();
423
+ pollCaches.set(owner, cache);
424
+ }
425
+ return cache;
426
+ };
427
+
352
428
  // src/providers/imessage/local/inbound.ts
353
429
  import { setTimeout as sleep } from "timers/promises";
354
430
 
@@ -571,6 +647,7 @@ var getMessage2 = async (client, id) => getMessage(client, id);
571
647
  // src/providers/imessage/remote/ids.ts
572
648
  var PART_PREFIX = /^p:(\d+)\//;
573
649
  var dmChatGuid = (address) => `any;-;${address}`;
650
+ var chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
574
651
  var toChatGuid = (value) => value;
575
652
  var toMessageGuid = (value) => value;
576
653
  var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
@@ -620,6 +697,7 @@ var sendCustomizedMiniApp = async (remote, spaceId, content) => {
620
697
  return {
621
698
  id: message.guid,
622
699
  content,
700
+ direction: "outbound",
623
701
  space: { id: spaceId },
624
702
  timestamp: message.dateCreated
625
703
  };
@@ -630,77 +708,6 @@ import {
630
708
  NotFoundError as NotFoundError2
631
709
  } from "@photon-ai/advanced-imessage";
632
710
 
633
- // src/providers/imessage/cache.ts
634
- var DEFAULT_MAX = 1e3;
635
- var MessageCache = class {
636
- map = /* @__PURE__ */ new Map();
637
- max;
638
- constructor(max = DEFAULT_MAX) {
639
- this.max = max;
640
- }
641
- get(id) {
642
- return this.map.get(id);
643
- }
644
- set(id, message) {
645
- if (this.map.has(id)) {
646
- this.map.delete(id);
647
- }
648
- this.map.set(id, message);
649
- if (this.map.size > this.max) {
650
- const first = this.map.keys().next().value;
651
- if (first !== void 0) {
652
- this.map.delete(first);
653
- }
654
- }
655
- }
656
- clear() {
657
- this.map.clear();
658
- }
659
- };
660
- var PollCache = class {
661
- map = /* @__PURE__ */ new Map();
662
- max;
663
- constructor(max = DEFAULT_MAX) {
664
- this.max = max;
665
- }
666
- get(id) {
667
- return this.map.get(id);
668
- }
669
- set(id, poll) {
670
- if (this.map.has(id)) {
671
- this.map.delete(id);
672
- }
673
- this.map.set(id, poll);
674
- if (this.map.size > this.max) {
675
- const first = this.map.keys().next().value;
676
- if (first !== void 0) {
677
- this.map.delete(first);
678
- }
679
- }
680
- }
681
- clear() {
682
- this.map.clear();
683
- }
684
- };
685
- var messageCaches = /* @__PURE__ */ new WeakMap();
686
- var pollCaches = /* @__PURE__ */ new WeakMap();
687
- var getMessageCache = (owner) => {
688
- let cache = messageCaches.get(owner);
689
- if (!cache) {
690
- cache = new MessageCache();
691
- messageCaches.set(owner, cache);
692
- }
693
- return cache;
694
- };
695
- var getPollCache = (owner) => {
696
- let cache = pollCaches.get(owner);
697
- if (!cache) {
698
- cache = new PollCache();
699
- pollCaches.set(owner, cache);
700
- }
701
- return cache;
702
- };
703
-
704
711
  // src/providers/imessage/remote/attachments.ts
705
712
  import {
706
713
  NotFoundError
@@ -796,16 +803,17 @@ var isIMessageMessage = (value) => {
796
803
  return false;
797
804
  }
798
805
  const record = value;
799
- return typeof record.id === "string" && record.id.length > 0 && typeof record.content === "object" && record.content !== null && typeof record.sender === "object" && record.sender !== null && typeof record.space === "object" && record.space !== null;
806
+ return typeof record.id === "string" && record.id.length > 0 && typeof record.content === "object" && record.content !== null && typeof record.space === "object" && record.space !== null;
800
807
  };
801
808
  var asProviderGroup = (items) => groupSchema.parse({ type: "group", items });
802
809
  var buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
803
810
  const chat = resolveChatGuid(message, chatGuidHint);
804
811
  return {
812
+ direction: message.isFromMe ? "outbound" : "inbound",
805
813
  sender: { id: resolveSenderId(message) },
806
814
  space: {
807
815
  id: chat,
808
- type: chat.includes(";+;") ? "group" : "dm",
816
+ type: chatTypeFromGuid(chat),
809
817
  phone
810
818
  },
811
819
  timestamp
@@ -1090,7 +1098,7 @@ var toReactionMessages = async (client, cache, event, phone) => {
1090
1098
  sender: { id: senderAddress },
1091
1099
  space: {
1092
1100
  id: event.chatGuid,
1093
- type: event.chatGuid.includes(";+;") ? "group" : "dm",
1101
+ type: chatTypeFromGuid(event.chatGuid),
1094
1102
  phone
1095
1103
  },
1096
1104
  timestamp: event.occurredAt,
@@ -1099,23 +1107,40 @@ var toReactionMessages = async (client, cache, event, phone) => {
1099
1107
  }
1100
1108
  ];
1101
1109
  };
1110
+ var toSettableReaction = (emoji) => {
1111
+ const native = EMOJI_TO_TAPBACK[emoji];
1112
+ return native ? { kind: native } : { kind: "emoji", emoji };
1113
+ };
1114
+ var tapbackTarget = (target) => ({
1115
+ guid: toMessageGuid(target.parentId ?? target.id),
1116
+ opts: typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0
1117
+ });
1102
1118
  var reactToMessage = async (remote, spaceId, target, reaction) => {
1103
- const chat = toChatGuid(spaceId);
1104
- const parentGuid = target.parentId ?? target.id;
1105
- const guid = toMessageGuid(parentGuid);
1106
- const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
1107
- const native = EMOJI_TO_TAPBACK[reaction];
1108
- if (native) {
1109
- await remote.messages.setReaction(chat, guid, { kind: native }, true, opts);
1110
- } else {
1111
- await remote.messages.setReaction(
1112
- chat,
1113
- guid,
1114
- { kind: "emoji", emoji: reaction },
1115
- true,
1116
- opts
1117
- );
1118
- }
1119
+ const { guid, opts } = tapbackTarget(target);
1120
+ const sent = await remote.messages.setReaction(
1121
+ toChatGuid(spaceId),
1122
+ guid,
1123
+ toSettableReaction(reaction),
1124
+ true,
1125
+ opts
1126
+ );
1127
+ return {
1128
+ id: sent.guid,
1129
+ content: asProviderReaction(reaction, target),
1130
+ direction: "outbound",
1131
+ space: { id: spaceId },
1132
+ timestamp: sent.dateCreated
1133
+ };
1134
+ };
1135
+ var unsendReaction = async (remote, spaceId, target, reaction) => {
1136
+ const { guid, opts } = tapbackTarget(target);
1137
+ await remote.messages.setReaction(
1138
+ toChatGuid(spaceId),
1139
+ guid,
1140
+ toSettableReaction(reaction),
1141
+ false,
1142
+ opts
1143
+ );
1119
1144
  };
1120
1145
 
1121
1146
  // src/providers/imessage/remote/read.ts
@@ -1251,17 +1276,319 @@ var ensureM4a = async (buffer, mimeType) => {
1251
1276
  return transcodeToM4a(buffer);
1252
1277
  };
1253
1278
 
1279
+ // src/providers/imessage/remote/markdown.ts
1280
+ import { Marked } from "marked";
1281
+ var markdownLexer = new Marked();
1282
+ var BULLET = "\u2022 ";
1283
+ var HR_LINE = "\u2014\u2014\u2014";
1284
+ var NESTED_LIST_INDENT = " ";
1285
+ var BLOCK_SEPARATOR = "\n\n";
1286
+ var TABLE_CELL_SEPARATOR = " | ";
1287
+ var DEFAULT_LIST_START = 1;
1288
+ var LEADING_WHITESPACE = /^\s+/;
1289
+ var TRAILING_WHITESPACE = /\s+$/;
1290
+ var MONOSPACE_UPPER_A = 120432;
1291
+ var MONOSPACE_LOWER_A = 120458;
1292
+ var MONOSPACE_DIGIT_ZERO = 120822;
1293
+ var UPPER_A = 65;
1294
+ var UPPER_Z = 90;
1295
+ var LOWER_A = 97;
1296
+ var LOWER_Z = 122;
1297
+ var DIGIT_ZERO = 48;
1298
+ var DIGIT_NINE = 57;
1299
+ var monospaceCodePoint = (codePoint) => {
1300
+ if (codePoint >= UPPER_A && codePoint <= UPPER_Z) {
1301
+ return MONOSPACE_UPPER_A + (codePoint - UPPER_A);
1302
+ }
1303
+ if (codePoint >= LOWER_A && codePoint <= LOWER_Z) {
1304
+ return MONOSPACE_LOWER_A + (codePoint - LOWER_A);
1305
+ }
1306
+ if (codePoint >= DIGIT_ZERO && codePoint <= DIGIT_NINE) {
1307
+ return MONOSPACE_DIGIT_ZERO + (codePoint - DIGIT_ZERO);
1308
+ }
1309
+ return codePoint;
1310
+ };
1311
+ var toMonospace = (text2) => {
1312
+ let out = "";
1313
+ for (const char of text2) {
1314
+ const codePoint = char.codePointAt(0);
1315
+ out += codePoint === void 0 ? char : String.fromCodePoint(monospaceCodePoint(codePoint));
1316
+ }
1317
+ return out;
1318
+ };
1319
+ var STYLE_ORDER = ["bold", "italic", "strikethrough"];
1320
+ var plain = (text2) => ({ text: text2, styles: [] });
1321
+ var withStyle = (spans, style) => spans.map(
1322
+ (span) => span.styles.includes(style) ? span : { ...span, styles: [...span.styles, style] }
1323
+ );
1324
+ var asLink = (spans) => spans.map((span) => ({ ...span, link: true }));
1325
+ var spanText = (spans) => {
1326
+ let out = "";
1327
+ for (const span of spans) {
1328
+ out += span.text;
1329
+ }
1330
+ return out;
1331
+ };
1332
+ var joinSpans = (blocks, separator) => {
1333
+ const out = [];
1334
+ for (const [index, block] of blocks.entries()) {
1335
+ if (index > 0) {
1336
+ out.push(plain(separator));
1337
+ }
1338
+ out.push(...block);
1339
+ }
1340
+ return out;
1341
+ };
1342
+ var splitSpanLines = (spans) => {
1343
+ let current = [];
1344
+ const lines = [current];
1345
+ for (const span of spans) {
1346
+ const parts = span.text.split("\n");
1347
+ for (const [index, part] of parts.entries()) {
1348
+ if (index > 0) {
1349
+ current = [];
1350
+ lines.push(current);
1351
+ }
1352
+ if (part) {
1353
+ current.push({ ...span, text: part });
1354
+ }
1355
+ }
1356
+ }
1357
+ return lines;
1358
+ };
1359
+ var asMarkedToken = (token) => token;
1360
+ var checkboxPrefix = (item) => {
1361
+ if (!item.task) {
1362
+ return "";
1363
+ }
1364
+ return item.checked ? "[x] " : "[ ] ";
1365
+ };
1366
+ var listMarker = (list, index) => {
1367
+ if (!list.ordered) {
1368
+ return BULLET;
1369
+ }
1370
+ const start = list.start === "" ? DEFAULT_LIST_START : list.start;
1371
+ return `${start + index}. `;
1372
+ };
1373
+ var renderLink = (token) => {
1374
+ if (token.text === token.href) {
1375
+ return [{ text: token.href, styles: [], link: true }];
1376
+ }
1377
+ return [
1378
+ ...asLink(renderInlineTokens(token.tokens)),
1379
+ { text: ` (${token.href})`, styles: [], link: true }
1380
+ ];
1381
+ };
1382
+ var renderImage = (token) => [
1383
+ {
1384
+ text: token.text ? `${token.text} (${token.href})` : token.href,
1385
+ styles: [],
1386
+ link: true
1387
+ }
1388
+ ];
1389
+ var renderInlineToken = (token) => {
1390
+ switch (token.type) {
1391
+ case "strong":
1392
+ return withStyle(renderInlineTokens(token.tokens), "bold");
1393
+ case "em":
1394
+ return withStyle(renderInlineTokens(token.tokens), "italic");
1395
+ case "del":
1396
+ return withStyle(renderInlineTokens(token.tokens), "strikethrough");
1397
+ case "codespan":
1398
+ return [plain(toMonospace(token.text))];
1399
+ case "br":
1400
+ return [plain("\n")];
1401
+ case "link":
1402
+ return renderLink(token);
1403
+ case "image":
1404
+ return renderImage(token);
1405
+ case "escape":
1406
+ return [plain(token.text)];
1407
+ case "text":
1408
+ return token.tokens ? renderInlineTokens(token.tokens) : [plain(token.text)];
1409
+ // Raw HTML in markdown source stays literal — styled text has no markup.
1410
+ case "html":
1411
+ return [plain(token.text)];
1412
+ // Task-item checkboxes are rendered from `ListItem.task`/`checked`.
1413
+ case "checkbox":
1414
+ return [];
1415
+ default:
1416
+ return "raw" in token ? [plain(String(token.raw))] : [];
1417
+ }
1418
+ };
1419
+ var renderInlineTokens = (tokens) => {
1420
+ const out = [];
1421
+ for (const token of tokens) {
1422
+ out.push(...renderInlineToken(asMarkedToken(token)));
1423
+ }
1424
+ return out;
1425
+ };
1426
+ var renderBlockquote = (quote) => {
1427
+ const lines = splitSpanLines(renderBlockTokens(quote.tokens));
1428
+ const out = [];
1429
+ for (const [index, line] of lines.entries()) {
1430
+ if (index > 0) {
1431
+ out.push(plain("\n"));
1432
+ }
1433
+ out.push(plain(line.length > 0 ? "> " : ">"), ...line);
1434
+ }
1435
+ return out;
1436
+ };
1437
+ var renderList = (list) => {
1438
+ const out = [];
1439
+ for (const [index, item] of list.items.entries()) {
1440
+ const prefix = `${listMarker(list, index)}${checkboxPrefix(item)}`;
1441
+ const blocks = [];
1442
+ for (const token of item.tokens) {
1443
+ const rendered = renderBlockToken(asMarkedToken(token));
1444
+ if (spanText(rendered)) {
1445
+ blocks.push(rendered);
1446
+ }
1447
+ }
1448
+ const [first = [], ...rest] = splitSpanLines(joinSpans(blocks, "\n"));
1449
+ if (out.length > 0) {
1450
+ out.push(plain("\n"));
1451
+ }
1452
+ out.push(plain(prefix), ...first);
1453
+ for (const line of rest) {
1454
+ out.push(plain(`
1455
+ ${NESTED_LIST_INDENT}`), ...line);
1456
+ }
1457
+ }
1458
+ return out;
1459
+ };
1460
+ var renderTable = (table) => {
1461
+ const out = [];
1462
+ const pushRow = (cells, rowIndex) => {
1463
+ if (rowIndex > 0) {
1464
+ out.push(plain("\n"));
1465
+ }
1466
+ for (const [cellIndex, cell] of cells.entries()) {
1467
+ if (cellIndex > 0) {
1468
+ out.push(plain(TABLE_CELL_SEPARATOR));
1469
+ }
1470
+ out.push(...renderInlineTokens(cell.tokens));
1471
+ }
1472
+ };
1473
+ pushRow(table.header, 0);
1474
+ for (const [index, row] of table.rows.entries()) {
1475
+ pushRow(row, index + 1);
1476
+ }
1477
+ return out;
1478
+ };
1479
+ var renderBlockToken = (token) => {
1480
+ switch (token.type) {
1481
+ // iMessage formatting has no heading sizes; bold is the conventional
1482
+ // stand-in (Telegram precedent).
1483
+ case "heading":
1484
+ return withStyle(renderInlineTokens(token.tokens), "bold");
1485
+ case "paragraph":
1486
+ return renderInlineTokens(token.tokens);
1487
+ case "code":
1488
+ return [plain(toMonospace(token.text))];
1489
+ case "blockquote":
1490
+ return renderBlockquote(token);
1491
+ case "list":
1492
+ return renderList(token);
1493
+ case "table":
1494
+ return renderTable(token);
1495
+ case "hr":
1496
+ return [plain(HR_LINE)];
1497
+ case "space":
1498
+ case "def":
1499
+ return [];
1500
+ default:
1501
+ return renderInlineToken(token);
1502
+ }
1503
+ };
1504
+ var renderBlockTokens = (tokens) => {
1505
+ const blocks = [];
1506
+ for (const token of tokens) {
1507
+ const rendered = renderBlockToken(asMarkedToken(token));
1508
+ if (spanText(rendered)) {
1509
+ blocks.push(rendered);
1510
+ }
1511
+ }
1512
+ return joinSpans(blocks, BLOCK_SEPARATOR);
1513
+ };
1514
+ var trimSpans = (spans) => {
1515
+ const trimmed = [...spans];
1516
+ while (trimmed.length > 0) {
1517
+ const first = trimmed.at(0);
1518
+ const text2 = first?.text.replace(LEADING_WHITESPACE, "");
1519
+ if (first && text2) {
1520
+ trimmed[0] = { ...first, text: text2 };
1521
+ break;
1522
+ }
1523
+ trimmed.shift();
1524
+ }
1525
+ while (trimmed.length > 0) {
1526
+ const last = trimmed.at(-1);
1527
+ const text2 = last?.text.replace(TRAILING_WHITESPACE, "");
1528
+ if (last && text2) {
1529
+ trimmed[trimmed.length - 1] = { ...last, text: text2 };
1530
+ break;
1531
+ }
1532
+ trimmed.pop();
1533
+ }
1534
+ return trimmed;
1535
+ };
1536
+ var finalize = (spans) => {
1537
+ let text2 = "";
1538
+ let hasLinks = false;
1539
+ const open = /* @__PURE__ */ new Map();
1540
+ const ranges = [];
1541
+ const close = (style, end) => {
1542
+ const start = open.get(style);
1543
+ open.delete(style);
1544
+ if (start !== void 0 && end > start) {
1545
+ ranges.push({ type: style, start, length: end - start });
1546
+ }
1547
+ };
1548
+ for (const span of spans) {
1549
+ if (!span.text) {
1550
+ continue;
1551
+ }
1552
+ hasLinks ||= span.link === true;
1553
+ const offset = text2.length;
1554
+ for (const style of STYLE_ORDER) {
1555
+ if (span.styles.includes(style)) {
1556
+ if (!open.has(style)) {
1557
+ open.set(style, offset);
1558
+ }
1559
+ } else {
1560
+ close(style, offset);
1561
+ }
1562
+ }
1563
+ text2 += span.text;
1564
+ }
1565
+ for (const style of STYLE_ORDER) {
1566
+ close(style, text2.length);
1567
+ }
1568
+ ranges.sort(
1569
+ (a, b) => a.start - b.start || STYLE_ORDER.indexOf(a.type) - STYLE_ORDER.indexOf(b.type)
1570
+ );
1571
+ return { text: text2, formatting: ranges, hasLinks };
1572
+ };
1573
+ var markdownToIMessageText = (markdown) => finalize(trimSpans(renderBlockTokens(markdownLexer.lexer(markdown))));
1574
+
1254
1575
  // src/providers/imessage/remote/send.ts
1255
1576
  var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
1256
1577
  "text",
1578
+ "markdown",
1257
1579
  "attachment",
1258
1580
  "contact",
1259
1581
  "voice"
1260
1582
  ]);
1583
+ var GROUP_TEXT_TYPES = /* @__PURE__ */ new Set([
1584
+ "text",
1585
+ "markdown"
1586
+ ]);
1261
1587
  var MAX_GROUP_TEXT_ITEMS = 1;
1262
1588
  var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
1263
1589
  id,
1264
1590
  content,
1591
+ direction: "outbound",
1265
1592
  space: { id: spaceId },
1266
1593
  timestamp,
1267
1594
  ...extras
@@ -1274,6 +1601,18 @@ var providerGroup = (items) => asGroup({ items });
1274
1601
  var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
1275
1602
  var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
1276
1603
  var effectOption = (effect2) => effect2 ? { effect: effect2 } : {};
1604
+ var formattingOption = (formatting) => formatting.length > 0 ? { formatting } : {};
1605
+ var dataDetectionOption = (hasLinks) => hasLinks ? { enableDataDetection: true } : {};
1606
+ var renderMarkdown = (markdown) => {
1607
+ const rendered = markdownToIMessageText(markdown);
1608
+ if (!rendered.text) {
1609
+ throw unsupportedRemoteContent(
1610
+ "markdown",
1611
+ "renders to empty text \u2014 nothing to send"
1612
+ );
1613
+ }
1614
+ return rendered;
1615
+ };
1277
1616
  var replyTargetFromId = (messageId) => {
1278
1617
  const childRef = parseChildId(messageId);
1279
1618
  if (childRef) {
@@ -1331,6 +1670,22 @@ var sendContent = async (remote, spaceId, chat, content, replyTo, effect2) => {
1331
1670
  );
1332
1671
  return outboundMessage(spaceId, message, content);
1333
1672
  }
1673
+ case "markdown": {
1674
+ const rendered = renderMarkdown(content.markdown);
1675
+ const message = await remote.messages.sendText(
1676
+ chat,
1677
+ rendered.text,
1678
+ withReply(
1679
+ {
1680
+ ...effectOption(effect2),
1681
+ ...formattingOption(rendered.formatting),
1682
+ ...dataDetectionOption(rendered.hasLinks)
1683
+ },
1684
+ replyTo
1685
+ )
1686
+ );
1687
+ return outboundMessage(spaceId, message, content);
1688
+ }
1334
1689
  case "richlink": {
1335
1690
  const message = await remote.messages.sendText(
1336
1691
  chat,
@@ -1395,7 +1750,7 @@ var validateGroupContent = (content) => {
1395
1750
  `"${itemType}" items are not supported inside a group`
1396
1751
  );
1397
1752
  }
1398
- if (itemType === "text" && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1753
+ if (GROUP_TEXT_TYPES.has(itemType) && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1399
1754
  throw unsupportedRemoteContent(
1400
1755
  "group",
1401
1756
  `groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
@@ -1407,6 +1762,10 @@ var resolvePart = async (remote, content) => {
1407
1762
  switch (content.type) {
1408
1763
  case "text":
1409
1764
  return { text: content.text };
1765
+ case "markdown": {
1766
+ const rendered = renderMarkdown(content.markdown);
1767
+ return { text: rendered.text, ...formattingOption(rendered.formatting) };
1768
+ }
1410
1769
  case "attachment": {
1411
1770
  const { guid, name } = await uploadAttachment(remote, content);
1412
1771
  return { attachmentGuid: guid, attachmentName: name };
@@ -1469,20 +1828,29 @@ var editMessage = async (remote, spaceId, msgId, content) => {
1469
1828
  childRef ? { partIndex: childRef.partIndex } : void 0
1470
1829
  );
1471
1830
  };
1831
+ var unsendMessage = async (remote, spaceId, msgId) => {
1832
+ const childRef = parseChildId(msgId);
1833
+ await remote.messages.unsend(
1834
+ toChatGuid(spaceId),
1835
+ toMessageGuid(childRef?.parentGuid ?? msgId),
1836
+ childRef ? { partIndex: childRef.partIndex } : void 0
1837
+ );
1838
+ };
1472
1839
 
1473
1840
  // src/providers/imessage/remote/stream.ts
1474
1841
  import {
1475
- AuthenticationError,
1476
- IMessageError,
1477
- NotFoundError as NotFoundError3,
1478
1842
  ValidationError
1479
1843
  } from "@photon-ai/advanced-imessage";
1844
+ import { sanitizePhone } from "@photon-ai/otel";
1480
1845
 
1481
1846
  // src/utils/resumable-stream.ts
1847
+ import { createLogger } from "@photon-ai/otel";
1482
1848
  var CATCH_UP_PAGE_SIZE = 100;
1483
1849
  var MAX_BUFFERED_LIVE_EVENTS = 1e3;
1484
1850
  var RECONNECT_INITIAL_DELAY_MS = 500;
1485
1851
  var RECONNECT_MAX_DELAY_MS = 3e4;
1852
+ var PERSISTENT_FAILURE_ERROR_THRESHOLD = 5;
1853
+ var log = createLogger("spectrum.stream");
1486
1854
  var RetryableStreamError = class extends Error {
1487
1855
  constructor(message) {
1488
1856
  super(message);
@@ -1495,6 +1863,12 @@ var LiveBufferOverflowError = class extends RetryableStreamError {
1495
1863
  this.name = "LiveBufferOverflowError";
1496
1864
  }
1497
1865
  };
1866
+ var CursorRejectedError = class extends Error {
1867
+ constructor(cause) {
1868
+ super("Server rejected resume cursor", { cause });
1869
+ this.name = "CursorRejectedError";
1870
+ }
1871
+ };
1498
1872
  var closeIterable = async (iterable) => {
1499
1873
  if (!iterable) {
1500
1874
  return;
@@ -1502,7 +1876,15 @@ var closeIterable = async (iterable) => {
1502
1876
  await iterable.close?.();
1503
1877
  };
1504
1878
  var ignoreCleanupError = () => void 0;
1505
- var jitterDelay = (delayMs) => Math.random() * delayMs;
1879
+ var jitterDelay = (delayMs) => delayMs * (0.5 + Math.random() * 0.5);
1880
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1881
+ async function* throwOnCursorRejection(source, isCursorRejected) {
1882
+ try {
1883
+ yield* source;
1884
+ } catch (error) {
1885
+ throw isCursorRejected(error) ? new CursorRejectedError(error) : error;
1886
+ }
1887
+ }
1506
1888
  var numericCursor = (cursor) => {
1507
1889
  if (!cursor) {
1508
1890
  return;
@@ -1520,15 +1902,23 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1520
1902
  const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
1521
1903
  const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
1522
1904
  const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
1905
+ const jitter = options.jitter ?? jitterDelay;
1906
+ const label = options.label;
1523
1907
  let activeLive;
1524
1908
  let closed = false;
1909
+ let failedAttempts = 0;
1525
1910
  let lastCursor;
1526
1911
  let retryDelayMs = initialRetryDelayMs;
1527
1912
  let sleepTimer;
1528
1913
  let wakeSleep;
1529
1914
  const deliveredSinceCursor = /* @__PURE__ */ new Set();
1530
- const resetRetryDelay = () => {
1915
+ const noteRecovery = () => {
1531
1916
  retryDelayMs = initialRetryDelayMs;
1917
+ if (failedAttempts === 0) {
1918
+ return;
1919
+ }
1920
+ log.info("stream recovered", { attempts: failedAttempts, label });
1921
+ failedAttempts = 0;
1532
1922
  };
1533
1923
  const advanceCursor = (cursor, clearDelivered) => {
1534
1924
  if (!cursor || cursor === lastCursor || isCursorRegression(cursor, lastCursor)) {
@@ -1549,17 +1939,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1549
1939
  advanceCursor(item.cursor, clearOnCursorAdvance);
1550
1940
  deliveredSinceCursor.add(item.id);
1551
1941
  if (resetRetry) {
1552
- resetRetryDelay();
1942
+ noteRecovery();
1553
1943
  }
1554
1944
  };
1555
- const retryable = (error) => error instanceof RetryableStreamError || options.isRetryableError(error);
1945
+ const isCursorRejected = (error) => options.isCursorRejectedError?.(error) === true;
1556
1946
  const sleep2 = async (delayMs) => {
1557
1947
  if (delayMs <= 0 || closed) {
1558
1948
  return;
1559
1949
  }
1560
1950
  await new Promise((resolve) => {
1561
1951
  wakeSleep = resolve;
1562
- sleepTimer = setTimeout(resolve, jitterDelay(delayMs));
1952
+ sleepTimer = setTimeout(resolve, jitter(delayMs));
1563
1953
  });
1564
1954
  sleepTimer = void 0;
1565
1955
  wakeSleep = void 0;
@@ -1577,6 +1967,37 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1577
1967
  retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
1578
1968
  return delay;
1579
1969
  };
1970
+ const handleFailure = (error) => {
1971
+ failedAttempts += 1;
1972
+ const delayMs = nextRetryDelay();
1973
+ if (error instanceof CursorRejectedError) {
1974
+ lastCursor = void 0;
1975
+ deliveredSinceCursor.clear();
1976
+ log.warn(
1977
+ "resume cursor rejected; accepting event gap and resuming live",
1978
+ {
1979
+ attempt: failedAttempts,
1980
+ delayMs,
1981
+ error: errorMessage(error.cause),
1982
+ label
1983
+ }
1984
+ );
1985
+ return delayMs;
1986
+ }
1987
+ const attrs = {
1988
+ attempt: failedAttempts,
1989
+ delayMs,
1990
+ error: errorMessage(error),
1991
+ hasCursor: lastCursor !== void 0,
1992
+ label
1993
+ };
1994
+ if (failedAttempts >= PERSISTENT_FAILURE_ERROR_THRESHOLD) {
1995
+ log.error("stream persistently failing; still retrying", attrs, error);
1996
+ return delayMs;
1997
+ }
1998
+ log.warn("stream interrupted; reconnecting", attrs);
1999
+ return delayMs;
2000
+ };
1580
2001
  const consumeLive = async () => {
1581
2002
  const live = options.subscribeLive(lastCursor);
1582
2003
  activeLive = live;
@@ -1625,15 +2046,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1625
2046
  };
1626
2047
  };
1627
2048
  const replayMissed = async (cursor, getLiveError) => {
1628
- for await (const event of options.fetchMissed(cursor, {
1629
- limit: catchUpPageSize
1630
- })) {
2049
+ const missed = throwOnCursorRejection(
2050
+ options.fetchMissed(cursor, { limit: catchUpPageSize }),
2051
+ isCursorRejected
2052
+ );
2053
+ for await (const event of missed) {
1631
2054
  throwLiveError(getLiveError());
1632
2055
  await deliverItem(await options.processMissed(event), false, false);
1633
2056
  }
1634
2057
  throwLiveError(getLiveError());
1635
2058
  };
1636
- const flushLiveBuffer = async (liveBuffer, getLiveError) => {
2059
+ const flushLiveBuffer = async (liveBuffer, getLiveError, stopBuffering) => {
1637
2060
  let index = 0;
1638
2061
  let lastFlushedId;
1639
2062
  while (index < liveBuffer.length) {
@@ -1649,7 +2072,8 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1649
2072
  }
1650
2073
  liveBuffer.length = 0;
1651
2074
  throwLiveError(getLiveError());
1652
- return lastFlushedId;
2075
+ compactDeliveredIds(lastFlushedId);
2076
+ stopBuffering();
1653
2077
  };
1654
2078
  const compactDeliveredIds = (lastId) => {
1655
2079
  if (!lastId) {
@@ -1666,13 +2090,10 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1666
2090
  const livePump = startLivePump(live, () => buffering, liveBuffer);
1667
2091
  try {
1668
2092
  await replayMissed(cursor, livePump.getError);
1669
- const lastFlushedId = await flushLiveBuffer(
1670
- liveBuffer,
1671
- livePump.getError
1672
- );
1673
- compactDeliveredIds(lastFlushedId);
1674
- buffering = false;
1675
- resetRetryDelay();
2093
+ await flushLiveBuffer(liveBuffer, livePump.getError, () => {
2094
+ buffering = false;
2095
+ });
2096
+ noteRecovery();
1676
2097
  await livePump.pump;
1677
2098
  throwLiveError(livePump.getError());
1678
2099
  } finally {
@@ -1693,21 +2114,18 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1693
2114
  await consumeLive();
1694
2115
  }
1695
2116
  } catch (error) {
1696
- await closeIterable(activeLive);
2117
+ await closeIterable(activeLive).catch(ignoreCleanupError);
1697
2118
  activeLive = void 0;
1698
2119
  if (closed) {
1699
2120
  break;
1700
2121
  }
1701
- if (!retryable(error)) {
1702
- end(error);
1703
- return;
1704
- }
1705
- await sleep2(nextRetryDelay());
2122
+ await sleep2(handleFailure(error));
1706
2123
  }
1707
2124
  }
1708
2125
  end();
1709
2126
  };
1710
2127
  const pump = run().catch((error) => {
2128
+ log.error("resumable stream loop crashed", { label }, error);
1711
2129
  if (!closed) {
1712
2130
  end(error);
1713
2131
  }
@@ -1840,7 +2258,7 @@ var buildPollOptionMessage = (input) => {
1840
2258
  sender: { id: input.senderAddress },
1841
2259
  space: {
1842
2260
  id: input.chatGuid,
1843
- type: input.chatGuid.includes(";+;") ? "group" : "dm",
2261
+ type: chatTypeFromGuid(input.chatGuid),
1844
2262
  phone: input.phone
1845
2263
  },
1846
2264
  timestamp: input.event.occurredAt,
@@ -1896,15 +2314,8 @@ var toPollDeltaMessages = async (client, pollCache, event, phone) => {
1896
2314
  };
1897
2315
 
1898
2316
  // src/providers/imessage/remote/stream.ts
1899
- var isRetryableIMessageStreamError = (error) => {
1900
- if (error instanceof AuthenticationError || error instanceof NotFoundError3 || error instanceof ValidationError) {
1901
- return false;
1902
- }
1903
- if (error instanceof IMessageError) {
1904
- return true;
1905
- }
1906
- return false;
1907
- };
2317
+ var isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
2318
+ var streamLabel = (kind, phone) => `imessage.${kind}:${phone === SHARED_PHONE ? phone : sanitizePhone(phone)}`;
1908
2319
  var isEventFromCurrentAccount = (event, phone) => event.isFromMe || phone !== SHARED_PHONE && event.actor?.address !== void 0 && event.actor.address === phone;
1909
2320
  var toMessageItem = async (client, event, phone, cursor, onInbound) => {
1910
2321
  if (event.type === "message.received") {
@@ -2007,7 +2418,8 @@ var withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), {
2007
2418
  });
2008
2419
  var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2009
2420
  fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
2010
- isRetryableError: isRetryableIMessageStreamError,
2421
+ isCursorRejectedError: isCursorRejectedIMessageError,
2422
+ label: streamLabel("messages", phone),
2011
2423
  processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
2012
2424
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(
2013
2425
  client,
@@ -2020,7 +2432,8 @@ var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2020
2432
  });
2021
2433
  var pollStream = (client, pollCache, phone) => resumableOrderedStream({
2022
2434
  fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
2023
- isRetryableError: isRetryableIMessageStreamError,
2435
+ isCursorRejectedError: isCursorRejectedIMessageError,
2436
+ label: streamLabel("polls", phone),
2024
2437
  processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
2025
2438
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
2026
2439
  subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
@@ -2050,6 +2463,12 @@ var INITIAL_THROTTLE_MS = 1e3;
2050
2463
  var BACKOFF_FACTOR = 2;
2051
2464
  var MAX_EDITS = 5;
2052
2465
  var sendStreamText = async (remote, spaceId, content) => {
2466
+ if (content.format === "markdown") {
2467
+ throw unsupportedRemoteContent(
2468
+ "streamText",
2469
+ "markdown-formatted streams have no native iMessage delivery"
2470
+ );
2471
+ }
2053
2472
  const chat = toChatGuid(spaceId);
2054
2473
  let sent;
2055
2474
  let full = "";
@@ -2089,6 +2508,7 @@ var sendStreamText = async (remote, spaceId, content) => {
2089
2508
  return {
2090
2509
  id: sent.guid,
2091
2510
  content: asText(full),
2511
+ direction: "outbound",
2092
2512
  space: { id: spaceId },
2093
2513
  timestamp: sent.dateCreated
2094
2514
  };
@@ -2121,9 +2541,9 @@ var send4 = async (remote, spaceId, content) => send3(remote, spaceId, content);
2121
2541
  var sendStreamText2 = async (remote, spaceId, content) => sendStreamText(remote, spaceId, content);
2122
2542
  var replyToMessage2 = async (remote, spaceId, msgId, content) => replyToMessage(remote, spaceId, msgId, content);
2123
2543
  var editMessage2 = async (remote, spaceId, msgId, content) => editMessage(remote, spaceId, msgId, content);
2124
- var reactToMessage2 = async (remote, spaceId, target, reaction) => {
2125
- await reactToMessage(remote, spaceId, target, reaction);
2126
- };
2544
+ var reactToMessage2 = async (remote, spaceId, target, reaction) => reactToMessage(remote, spaceId, target, reaction);
2545
+ var unsendMessage2 = async (remote, spaceId, msgId) => unsendMessage(remote, spaceId, msgId);
2546
+ var unsendReaction2 = async (remote, spaceId, target, reaction) => unsendReaction(remote, spaceId, target, reaction);
2127
2547
  var getMessage4 = async (remote, spaceId, msgId, phone) => getMessage3(remote, spaceId, msgId, phone);
2128
2548
 
2129
2549
  // src/providers/imessage/remote/client.ts
@@ -2162,6 +2582,22 @@ var randomPhone = (clients) => {
2162
2582
 
2163
2583
  // src/providers/imessage/index.ts
2164
2584
  var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
2585
+ var cacheRemoteOutbound = (remote, space, record) => {
2586
+ if (!record) {
2587
+ return record;
2588
+ }
2589
+ cacheMessage(getMessageCache(remote), {
2590
+ ...record,
2591
+ direction: record.direction ?? "outbound",
2592
+ space: {
2593
+ ...record.space,
2594
+ id: record.space.id,
2595
+ phone: space.phone,
2596
+ type: space.type
2597
+ }
2598
+ });
2599
+ return record;
2600
+ };
2165
2601
  var handleEdit = async (client, space, content) => {
2166
2602
  if (isLocal(client)) {
2167
2603
  throw UnsupportedError.action("edit", "iMessage (local mode)");
@@ -2176,6 +2612,30 @@ var handleEdit = async (client, space, content) => {
2176
2612
  const remote = clientForPhone(client, space.phone);
2177
2613
  await editMessage2(remote, space.id, content.target.id, content.content);
2178
2614
  };
2615
+ var handleUnsend = async (client, space, content) => {
2616
+ if (isLocal(client)) {
2617
+ throw UnsupportedError.action("unsend", "iMessage (local mode)");
2618
+ }
2619
+ if (isPollContent(content.target.content)) {
2620
+ throw UnsupportedError.action(
2621
+ "unsend",
2622
+ "iMessage",
2623
+ "iMessage polls cannot be unsent"
2624
+ );
2625
+ }
2626
+ const remote = clientForPhone(client, space.phone);
2627
+ const targetContent = content.target.content;
2628
+ if (targetContent.type === "reaction") {
2629
+ await unsendReaction2(
2630
+ remote,
2631
+ space.id,
2632
+ targetContent.target,
2633
+ targetContent.emoji
2634
+ );
2635
+ return;
2636
+ }
2637
+ await unsendMessage2(remote, space.id, content.target.id);
2638
+ };
2179
2639
  var handleStreamText = async (client, space, content) => {
2180
2640
  if (isLocal(client)) {
2181
2641
  throw UnsupportedError.action(
@@ -2185,7 +2645,11 @@ var handleStreamText = async (client, space, content) => {
2185
2645
  );
2186
2646
  }
2187
2647
  const remote = clientForPhone(client, space.phone);
2188
- return await sendStreamText2(remote, space.id, content);
2648
+ return cacheRemoteOutbound(
2649
+ remote,
2650
+ space,
2651
+ await sendStreamText2(remote, space.id, content)
2652
+ );
2189
2653
  };
2190
2654
  var handleBackground = async (client, space, content) => {
2191
2655
  if (isLocal(client)) {
@@ -2207,7 +2671,11 @@ var handleCustomizedMiniApp = async (client, space, content) => {
2207
2671
  );
2208
2672
  }
2209
2673
  const remote = clientForPhone(client, space.phone);
2210
- return await sendCustomizedMiniApp2(remote, space.id, content);
2674
+ return cacheRemoteOutbound(
2675
+ remote,
2676
+ space,
2677
+ await sendCustomizedMiniApp2(remote, space.id, content)
2678
+ );
2211
2679
  };
2212
2680
  var handleRead = async (client, space) => {
2213
2681
  if (isLocal(client)) {
@@ -2316,10 +2784,10 @@ var imessage = definePlatform("iMessage", {
2316
2784
  space: {
2317
2785
  schema: spaceSchema,
2318
2786
  params: spaceParamsSchema,
2319
- resolve: async ({ input, client }) => {
2787
+ create: async ({ input, client }) => {
2320
2788
  if (isLocal(client)) {
2321
2789
  throw UnsupportedError.action(
2322
- "createSpace",
2790
+ "space.create",
2323
2791
  "iMessage (local mode)",
2324
2792
  "local mode only supports replying to existing messages"
2325
2793
  );
@@ -2330,18 +2798,52 @@ var imessage = definePlatform("iMessage", {
2330
2798
  if (client.length === 0) {
2331
2799
  throw new Error("No iMessage clients configured");
2332
2800
  }
2333
- const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? randomPhone(client);
2334
- const remote = clientForPhone(client, phone);
2335
2801
  const addresses = input.users.map((u) => u.id);
2336
- if (input.users.length === 1) {
2802
+ if (isSharedMode(client)) {
2803
+ if (addresses.length > 1) {
2804
+ throw UnsupportedError.action(
2805
+ "space.create",
2806
+ "iMessage (shared mode)",
2807
+ "shared mode cannot create group chats \u2014 use a dedicated number, or space.get(chatGuid) for an existing group"
2808
+ );
2809
+ }
2337
2810
  return {
2338
2811
  id: dmChatGuid(addresses[0] ?? ""),
2339
2812
  type: "dm",
2340
- phone
2813
+ phone: SHARED_PHONE
2341
2814
  };
2342
2815
  }
2816
+ const phone = input.params?.phone ?? randomPhone(client);
2817
+ const remote = clientForPhone(client, phone);
2343
2818
  const { chat } = await remote.chats.create(addresses);
2344
- return { id: chat.guid, type: "group", phone };
2819
+ return {
2820
+ id: chat.guid,
2821
+ type: chat.isGroup ? "group" : "dm",
2822
+ phone
2823
+ };
2824
+ },
2825
+ get: async ({ input, client }) => {
2826
+ if (isLocal(client)) {
2827
+ throw UnsupportedError.action(
2828
+ "space.get",
2829
+ "iMessage (local mode)",
2830
+ "local mode only supports replying to existing messages"
2831
+ );
2832
+ }
2833
+ if (client.length === 0) {
2834
+ throw new Error("No iMessage clients configured");
2835
+ }
2836
+ const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
2837
+ if (!phone) {
2838
+ throw new Error(
2839
+ `iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`
2840
+ );
2841
+ }
2842
+ return {
2843
+ id: input.id,
2844
+ type: chatTypeFromGuid(input.id),
2845
+ phone
2846
+ };
2345
2847
  },
2346
2848
  actions: {
2347
2849
  // Sugar: `space.background(input, opts?)` →
@@ -2382,11 +2884,15 @@ var imessage = definePlatform("iMessage", {
2382
2884
  );
2383
2885
  }
2384
2886
  const remote2 = clientForPhone(client, space.phone);
2385
- return await replyToMessage2(
2887
+ return cacheRemoteOutbound(
2386
2888
  remote2,
2387
- space.id,
2388
- content.target.id,
2389
- content.content
2889
+ space,
2890
+ await replyToMessage2(
2891
+ remote2,
2892
+ space.id,
2893
+ content.target.id,
2894
+ content.content
2895
+ )
2390
2896
  );
2391
2897
  }
2392
2898
  if (content.type === "reaction") {
@@ -2401,13 +2907,16 @@ var imessage = definePlatform("iMessage", {
2401
2907
  );
2402
2908
  }
2403
2909
  const remote2 = clientForPhone(client, space.phone);
2404
- await reactToMessage2(
2910
+ return cacheRemoteOutbound(
2405
2911
  remote2,
2406
- space.id,
2407
- content.target,
2408
- content.emoji
2912
+ space,
2913
+ await reactToMessage2(
2914
+ remote2,
2915
+ space.id,
2916
+ content.target,
2917
+ content.emoji
2918
+ )
2409
2919
  );
2410
- return;
2411
2920
  }
2412
2921
  if (content.type === "typing") {
2413
2922
  await handleTyping(client, space, content.state);
@@ -2417,6 +2926,10 @@ var imessage = definePlatform("iMessage", {
2417
2926
  await handleEdit(client, space, content);
2418
2927
  return;
2419
2928
  }
2929
+ if (content.type === "unsend") {
2930
+ await handleUnsend(client, space, content);
2931
+ return;
2932
+ }
2420
2933
  if (content.type === "streamText") {
2421
2934
  return await handleStreamText(client, space, content);
2422
2935
  }
@@ -2443,7 +2956,11 @@ var imessage = definePlatform("iMessage", {
2443
2956
  return await send2(client, space.id, content);
2444
2957
  }
2445
2958
  const remote = clientForPhone(client, space.phone);
2446
- return await send4(remote, space.id, content);
2959
+ return cacheRemoteOutbound(
2960
+ remote,
2961
+ space,
2962
+ await send4(remote, space.id, content)
2963
+ );
2447
2964
  },
2448
2965
  actions: {
2449
2966
  getMessage: async ({ client }, space, messageId) => {