spectrum-ts 2.0.0 → 3.0.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-WePAHfcH.d.ts} +1 -1
  2. package/dist/{authoring-BjE5BvlO.d.ts → authoring-DDh3muGT.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-ATNAE7OR.js → chunk-AYCMTRVC.js} +549 -76
  7. package/dist/{chunk-6BI4PFTP.js → chunk-CHY5YLLV.js} +1 -1
  8. package/dist/{chunk-U3LXXT3W.js → chunk-EZ5SNNFS.js} +20 -8
  9. package/dist/{chunk-WXY5QP3M.js → chunk-FULEQIRQ.js} +27 -21
  10. package/dist/{chunk-2ILTJC35.js → chunk-LQMDV75O.js} +205 -11
  11. package/dist/{chunk-NGC4DJIX.js → chunk-LX437ZTY.js} +416 -135
  12. package/dist/{chunk-3B4QH4JG.js → chunk-MHGCPC2V.js} +1 -1
  13. package/dist/{chunk-U7AWXDH6.js → chunk-NZ5WCMTY.js} +1 -1
  14. package/dist/{chunk-5LT5J3NR.js → chunk-TXRWKSNH.js} +262 -30
  15. package/dist/{chunk-Q537JPTG.js → chunk-UXJ5OO6P.js} +10 -10
  16. package/dist/index.d.ts +107 -56
  17. package/dist/index.js +29 -182
  18. package/dist/providers/imessage/index.d.ts +6 -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-BD0-kKyv.d.ts → types-BujGKBin.d.ts} +1 -1
  31. package/dist/{types-Bje8aq1k.d.ts → types-YqCNUDIt.d.ts} +171 -23
  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-LX437ZTY.js";
33
34
  import {
34
35
  asAttachment,
35
36
  asCustom,
@@ -38,7 +39,7 @@ 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
45
  import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
@@ -133,6 +134,7 @@ import {
133
134
  import z3 from "zod";
134
135
  var effectInnerSchema = z3.discriminatedUnion("type", [
135
136
  textSchema,
137
+ markdownSchema,
136
138
  attachmentSchema
137
139
  ]);
138
140
  var messageEffectSchema = z3.object({
@@ -153,9 +155,9 @@ function effect(input, messageEffect) {
153
155
  );
154
156
  }
155
157
  const inner = await resolveContent(input);
156
- if (inner.type !== "text" && inner.type !== "attachment") {
158
+ if (inner.type !== "text" && inner.type !== "markdown" && inner.type !== "attachment") {
157
159
  throw new Error(
158
- `imessage effect() only supports text and attachment content, got "${inner.type}"`
160
+ `imessage effect() only supports text, markdown, and attachment content, got "${inner.type}"`
159
161
  );
160
162
  }
161
163
  return messageEffectSchema.parse({
@@ -571,6 +573,7 @@ var getMessage2 = async (client, id) => getMessage(client, id);
571
573
  // src/providers/imessage/remote/ids.ts
572
574
  var PART_PREFIX = /^p:(\d+)\//;
573
575
  var dmChatGuid = (address) => `any;-;${address}`;
576
+ var chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
574
577
  var toChatGuid = (value) => value;
575
578
  var toMessageGuid = (value) => value;
576
579
  var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
@@ -805,7 +808,7 @@ var buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
805
808
  sender: { id: resolveSenderId(message) },
806
809
  space: {
807
810
  id: chat,
808
- type: chat.includes(";+;") ? "group" : "dm",
811
+ type: chatTypeFromGuid(chat),
809
812
  phone
810
813
  },
811
814
  timestamp
@@ -1090,7 +1093,7 @@ var toReactionMessages = async (client, cache, event, phone) => {
1090
1093
  sender: { id: senderAddress },
1091
1094
  space: {
1092
1095
  id: event.chatGuid,
1093
- type: event.chatGuid.includes(";+;") ? "group" : "dm",
1096
+ type: chatTypeFromGuid(event.chatGuid),
1094
1097
  phone
1095
1098
  },
1096
1099
  timestamp: event.occurredAt,
@@ -1099,23 +1102,39 @@ var toReactionMessages = async (client, cache, event, phone) => {
1099
1102
  }
1100
1103
  ];
1101
1104
  };
1105
+ var toSettableReaction = (emoji) => {
1106
+ const native = EMOJI_TO_TAPBACK[emoji];
1107
+ return native ? { kind: native } : { kind: "emoji", emoji };
1108
+ };
1109
+ var tapbackTarget = (target) => ({
1110
+ guid: toMessageGuid(target.parentId ?? target.id),
1111
+ opts: typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0
1112
+ });
1102
1113
  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
- }
1114
+ const { guid, opts } = tapbackTarget(target);
1115
+ const sent = await remote.messages.setReaction(
1116
+ toChatGuid(spaceId),
1117
+ guid,
1118
+ toSettableReaction(reaction),
1119
+ true,
1120
+ opts
1121
+ );
1122
+ return {
1123
+ id: sent.guid,
1124
+ content: asProviderReaction(reaction, target),
1125
+ space: { id: spaceId },
1126
+ timestamp: sent.dateCreated
1127
+ };
1128
+ };
1129
+ var unsendReaction = async (remote, spaceId, target, reaction) => {
1130
+ const { guid, opts } = tapbackTarget(target);
1131
+ await remote.messages.setReaction(
1132
+ toChatGuid(spaceId),
1133
+ guid,
1134
+ toSettableReaction(reaction),
1135
+ false,
1136
+ opts
1137
+ );
1119
1138
  };
1120
1139
 
1121
1140
  // src/providers/imessage/remote/read.ts
@@ -1251,13 +1270,314 @@ var ensureM4a = async (buffer, mimeType) => {
1251
1270
  return transcodeToM4a(buffer);
1252
1271
  };
1253
1272
 
1273
+ // src/providers/imessage/remote/markdown.ts
1274
+ import { Marked } from "marked";
1275
+ var markdownLexer = new Marked();
1276
+ var BULLET = "\u2022 ";
1277
+ var HR_LINE = "\u2014\u2014\u2014";
1278
+ var NESTED_LIST_INDENT = " ";
1279
+ var BLOCK_SEPARATOR = "\n\n";
1280
+ var TABLE_CELL_SEPARATOR = " | ";
1281
+ var DEFAULT_LIST_START = 1;
1282
+ var LEADING_WHITESPACE = /^\s+/;
1283
+ var TRAILING_WHITESPACE = /\s+$/;
1284
+ var MONOSPACE_UPPER_A = 120432;
1285
+ var MONOSPACE_LOWER_A = 120458;
1286
+ var MONOSPACE_DIGIT_ZERO = 120822;
1287
+ var UPPER_A = 65;
1288
+ var UPPER_Z = 90;
1289
+ var LOWER_A = 97;
1290
+ var LOWER_Z = 122;
1291
+ var DIGIT_ZERO = 48;
1292
+ var DIGIT_NINE = 57;
1293
+ var monospaceCodePoint = (codePoint) => {
1294
+ if (codePoint >= UPPER_A && codePoint <= UPPER_Z) {
1295
+ return MONOSPACE_UPPER_A + (codePoint - UPPER_A);
1296
+ }
1297
+ if (codePoint >= LOWER_A && codePoint <= LOWER_Z) {
1298
+ return MONOSPACE_LOWER_A + (codePoint - LOWER_A);
1299
+ }
1300
+ if (codePoint >= DIGIT_ZERO && codePoint <= DIGIT_NINE) {
1301
+ return MONOSPACE_DIGIT_ZERO + (codePoint - DIGIT_ZERO);
1302
+ }
1303
+ return codePoint;
1304
+ };
1305
+ var toMonospace = (text2) => {
1306
+ let out = "";
1307
+ for (const char of text2) {
1308
+ const codePoint = char.codePointAt(0);
1309
+ out += codePoint === void 0 ? char : String.fromCodePoint(monospaceCodePoint(codePoint));
1310
+ }
1311
+ return out;
1312
+ };
1313
+ var STYLE_ORDER = ["bold", "italic", "strikethrough"];
1314
+ var plain = (text2) => ({ text: text2, styles: [] });
1315
+ var withStyle = (spans, style) => spans.map(
1316
+ (span) => span.styles.includes(style) ? span : { ...span, styles: [...span.styles, style] }
1317
+ );
1318
+ var asLink = (spans) => spans.map((span) => ({ ...span, link: true }));
1319
+ var spanText = (spans) => {
1320
+ let out = "";
1321
+ for (const span of spans) {
1322
+ out += span.text;
1323
+ }
1324
+ return out;
1325
+ };
1326
+ var joinSpans = (blocks, separator) => {
1327
+ const out = [];
1328
+ for (const [index, block] of blocks.entries()) {
1329
+ if (index > 0) {
1330
+ out.push(plain(separator));
1331
+ }
1332
+ out.push(...block);
1333
+ }
1334
+ return out;
1335
+ };
1336
+ var splitSpanLines = (spans) => {
1337
+ let current = [];
1338
+ const lines = [current];
1339
+ for (const span of spans) {
1340
+ const parts = span.text.split("\n");
1341
+ for (const [index, part] of parts.entries()) {
1342
+ if (index > 0) {
1343
+ current = [];
1344
+ lines.push(current);
1345
+ }
1346
+ if (part) {
1347
+ current.push({ ...span, text: part });
1348
+ }
1349
+ }
1350
+ }
1351
+ return lines;
1352
+ };
1353
+ var asMarkedToken = (token) => token;
1354
+ var checkboxPrefix = (item) => {
1355
+ if (!item.task) {
1356
+ return "";
1357
+ }
1358
+ return item.checked ? "[x] " : "[ ] ";
1359
+ };
1360
+ var listMarker = (list, index) => {
1361
+ if (!list.ordered) {
1362
+ return BULLET;
1363
+ }
1364
+ const start = list.start === "" ? DEFAULT_LIST_START : list.start;
1365
+ return `${start + index}. `;
1366
+ };
1367
+ var renderLink = (token) => {
1368
+ if (token.text === token.href) {
1369
+ return [{ text: token.href, styles: [], link: true }];
1370
+ }
1371
+ return [
1372
+ ...asLink(renderInlineTokens(token.tokens)),
1373
+ { text: ` (${token.href})`, styles: [], link: true }
1374
+ ];
1375
+ };
1376
+ var renderImage = (token) => [
1377
+ {
1378
+ text: token.text ? `${token.text} (${token.href})` : token.href,
1379
+ styles: [],
1380
+ link: true
1381
+ }
1382
+ ];
1383
+ var renderInlineToken = (token) => {
1384
+ switch (token.type) {
1385
+ case "strong":
1386
+ return withStyle(renderInlineTokens(token.tokens), "bold");
1387
+ case "em":
1388
+ return withStyle(renderInlineTokens(token.tokens), "italic");
1389
+ case "del":
1390
+ return withStyle(renderInlineTokens(token.tokens), "strikethrough");
1391
+ case "codespan":
1392
+ return [plain(toMonospace(token.text))];
1393
+ case "br":
1394
+ return [plain("\n")];
1395
+ case "link":
1396
+ return renderLink(token);
1397
+ case "image":
1398
+ return renderImage(token);
1399
+ case "escape":
1400
+ return [plain(token.text)];
1401
+ case "text":
1402
+ return token.tokens ? renderInlineTokens(token.tokens) : [plain(token.text)];
1403
+ // Raw HTML in markdown source stays literal — styled text has no markup.
1404
+ case "html":
1405
+ return [plain(token.text)];
1406
+ // Task-item checkboxes are rendered from `ListItem.task`/`checked`.
1407
+ case "checkbox":
1408
+ return [];
1409
+ default:
1410
+ return "raw" in token ? [plain(String(token.raw))] : [];
1411
+ }
1412
+ };
1413
+ var renderInlineTokens = (tokens) => {
1414
+ const out = [];
1415
+ for (const token of tokens) {
1416
+ out.push(...renderInlineToken(asMarkedToken(token)));
1417
+ }
1418
+ return out;
1419
+ };
1420
+ var renderBlockquote = (quote) => {
1421
+ const lines = splitSpanLines(renderBlockTokens(quote.tokens));
1422
+ const out = [];
1423
+ for (const [index, line] of lines.entries()) {
1424
+ if (index > 0) {
1425
+ out.push(plain("\n"));
1426
+ }
1427
+ out.push(plain(line.length > 0 ? "> " : ">"), ...line);
1428
+ }
1429
+ return out;
1430
+ };
1431
+ var renderList = (list) => {
1432
+ const out = [];
1433
+ for (const [index, item] of list.items.entries()) {
1434
+ const prefix = `${listMarker(list, index)}${checkboxPrefix(item)}`;
1435
+ const blocks = [];
1436
+ for (const token of item.tokens) {
1437
+ const rendered = renderBlockToken(asMarkedToken(token));
1438
+ if (spanText(rendered)) {
1439
+ blocks.push(rendered);
1440
+ }
1441
+ }
1442
+ const [first = [], ...rest] = splitSpanLines(joinSpans(blocks, "\n"));
1443
+ if (out.length > 0) {
1444
+ out.push(plain("\n"));
1445
+ }
1446
+ out.push(plain(prefix), ...first);
1447
+ for (const line of rest) {
1448
+ out.push(plain(`
1449
+ ${NESTED_LIST_INDENT}`), ...line);
1450
+ }
1451
+ }
1452
+ return out;
1453
+ };
1454
+ var renderTable = (table) => {
1455
+ const out = [];
1456
+ const pushRow = (cells, rowIndex) => {
1457
+ if (rowIndex > 0) {
1458
+ out.push(plain("\n"));
1459
+ }
1460
+ for (const [cellIndex, cell] of cells.entries()) {
1461
+ if (cellIndex > 0) {
1462
+ out.push(plain(TABLE_CELL_SEPARATOR));
1463
+ }
1464
+ out.push(...renderInlineTokens(cell.tokens));
1465
+ }
1466
+ };
1467
+ pushRow(table.header, 0);
1468
+ for (const [index, row] of table.rows.entries()) {
1469
+ pushRow(row, index + 1);
1470
+ }
1471
+ return out;
1472
+ };
1473
+ var renderBlockToken = (token) => {
1474
+ switch (token.type) {
1475
+ // iMessage formatting has no heading sizes; bold is the conventional
1476
+ // stand-in (Telegram precedent).
1477
+ case "heading":
1478
+ return withStyle(renderInlineTokens(token.tokens), "bold");
1479
+ case "paragraph":
1480
+ return renderInlineTokens(token.tokens);
1481
+ case "code":
1482
+ return [plain(toMonospace(token.text))];
1483
+ case "blockquote":
1484
+ return renderBlockquote(token);
1485
+ case "list":
1486
+ return renderList(token);
1487
+ case "table":
1488
+ return renderTable(token);
1489
+ case "hr":
1490
+ return [plain(HR_LINE)];
1491
+ case "space":
1492
+ case "def":
1493
+ return [];
1494
+ default:
1495
+ return renderInlineToken(token);
1496
+ }
1497
+ };
1498
+ var renderBlockTokens = (tokens) => {
1499
+ const blocks = [];
1500
+ for (const token of tokens) {
1501
+ const rendered = renderBlockToken(asMarkedToken(token));
1502
+ if (spanText(rendered)) {
1503
+ blocks.push(rendered);
1504
+ }
1505
+ }
1506
+ return joinSpans(blocks, BLOCK_SEPARATOR);
1507
+ };
1508
+ var trimSpans = (spans) => {
1509
+ const trimmed = [...spans];
1510
+ while (trimmed.length > 0) {
1511
+ const first = trimmed.at(0);
1512
+ const text2 = first?.text.replace(LEADING_WHITESPACE, "");
1513
+ if (first && text2) {
1514
+ trimmed[0] = { ...first, text: text2 };
1515
+ break;
1516
+ }
1517
+ trimmed.shift();
1518
+ }
1519
+ while (trimmed.length > 0) {
1520
+ const last = trimmed.at(-1);
1521
+ const text2 = last?.text.replace(TRAILING_WHITESPACE, "");
1522
+ if (last && text2) {
1523
+ trimmed[trimmed.length - 1] = { ...last, text: text2 };
1524
+ break;
1525
+ }
1526
+ trimmed.pop();
1527
+ }
1528
+ return trimmed;
1529
+ };
1530
+ var finalize = (spans) => {
1531
+ let text2 = "";
1532
+ let hasLinks = false;
1533
+ const open = /* @__PURE__ */ new Map();
1534
+ const ranges = [];
1535
+ const close = (style, end) => {
1536
+ const start = open.get(style);
1537
+ open.delete(style);
1538
+ if (start !== void 0 && end > start) {
1539
+ ranges.push({ type: style, start, length: end - start });
1540
+ }
1541
+ };
1542
+ for (const span of spans) {
1543
+ if (!span.text) {
1544
+ continue;
1545
+ }
1546
+ hasLinks ||= span.link === true;
1547
+ const offset = text2.length;
1548
+ for (const style of STYLE_ORDER) {
1549
+ if (span.styles.includes(style)) {
1550
+ if (!open.has(style)) {
1551
+ open.set(style, offset);
1552
+ }
1553
+ } else {
1554
+ close(style, offset);
1555
+ }
1556
+ }
1557
+ text2 += span.text;
1558
+ }
1559
+ for (const style of STYLE_ORDER) {
1560
+ close(style, text2.length);
1561
+ }
1562
+ ranges.sort(
1563
+ (a, b) => a.start - b.start || STYLE_ORDER.indexOf(a.type) - STYLE_ORDER.indexOf(b.type)
1564
+ );
1565
+ return { text: text2, formatting: ranges, hasLinks };
1566
+ };
1567
+ var markdownToIMessageText = (markdown) => finalize(trimSpans(renderBlockTokens(markdownLexer.lexer(markdown))));
1568
+
1254
1569
  // src/providers/imessage/remote/send.ts
1255
1570
  var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
1256
1571
  "text",
1572
+ "markdown",
1257
1573
  "attachment",
1258
1574
  "contact",
1259
1575
  "voice"
1260
1576
  ]);
1577
+ var GROUP_TEXT_TYPES = /* @__PURE__ */ new Set([
1578
+ "text",
1579
+ "markdown"
1580
+ ]);
1261
1581
  var MAX_GROUP_TEXT_ITEMS = 1;
1262
1582
  var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
1263
1583
  id,
@@ -1274,6 +1594,18 @@ var providerGroup = (items) => asGroup({ items });
1274
1594
  var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
1275
1595
  var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
1276
1596
  var effectOption = (effect2) => effect2 ? { effect: effect2 } : {};
1597
+ var formattingOption = (formatting) => formatting.length > 0 ? { formatting } : {};
1598
+ var dataDetectionOption = (hasLinks) => hasLinks ? { enableDataDetection: true } : {};
1599
+ var renderMarkdown = (markdown) => {
1600
+ const rendered = markdownToIMessageText(markdown);
1601
+ if (!rendered.text) {
1602
+ throw unsupportedRemoteContent(
1603
+ "markdown",
1604
+ "renders to empty text \u2014 nothing to send"
1605
+ );
1606
+ }
1607
+ return rendered;
1608
+ };
1277
1609
  var replyTargetFromId = (messageId) => {
1278
1610
  const childRef = parseChildId(messageId);
1279
1611
  if (childRef) {
@@ -1331,6 +1663,22 @@ var sendContent = async (remote, spaceId, chat, content, replyTo, effect2) => {
1331
1663
  );
1332
1664
  return outboundMessage(spaceId, message, content);
1333
1665
  }
1666
+ case "markdown": {
1667
+ const rendered = renderMarkdown(content.markdown);
1668
+ const message = await remote.messages.sendText(
1669
+ chat,
1670
+ rendered.text,
1671
+ withReply(
1672
+ {
1673
+ ...effectOption(effect2),
1674
+ ...formattingOption(rendered.formatting),
1675
+ ...dataDetectionOption(rendered.hasLinks)
1676
+ },
1677
+ replyTo
1678
+ )
1679
+ );
1680
+ return outboundMessage(spaceId, message, content);
1681
+ }
1334
1682
  case "richlink": {
1335
1683
  const message = await remote.messages.sendText(
1336
1684
  chat,
@@ -1395,7 +1743,7 @@ var validateGroupContent = (content) => {
1395
1743
  `"${itemType}" items are not supported inside a group`
1396
1744
  );
1397
1745
  }
1398
- if (itemType === "text" && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1746
+ if (GROUP_TEXT_TYPES.has(itemType) && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1399
1747
  throw unsupportedRemoteContent(
1400
1748
  "group",
1401
1749
  `groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
@@ -1407,6 +1755,10 @@ var resolvePart = async (remote, content) => {
1407
1755
  switch (content.type) {
1408
1756
  case "text":
1409
1757
  return { text: content.text };
1758
+ case "markdown": {
1759
+ const rendered = renderMarkdown(content.markdown);
1760
+ return { text: rendered.text, ...formattingOption(rendered.formatting) };
1761
+ }
1410
1762
  case "attachment": {
1411
1763
  const { guid, name } = await uploadAttachment(remote, content);
1412
1764
  return { attachmentGuid: guid, attachmentName: name };
@@ -1469,20 +1821,29 @@ var editMessage = async (remote, spaceId, msgId, content) => {
1469
1821
  childRef ? { partIndex: childRef.partIndex } : void 0
1470
1822
  );
1471
1823
  };
1824
+ var unsendMessage = async (remote, spaceId, msgId) => {
1825
+ const childRef = parseChildId(msgId);
1826
+ await remote.messages.unsend(
1827
+ toChatGuid(spaceId),
1828
+ toMessageGuid(childRef?.parentGuid ?? msgId),
1829
+ childRef ? { partIndex: childRef.partIndex } : void 0
1830
+ );
1831
+ };
1472
1832
 
1473
1833
  // src/providers/imessage/remote/stream.ts
1474
1834
  import {
1475
- AuthenticationError,
1476
- IMessageError,
1477
- NotFoundError as NotFoundError3,
1478
1835
  ValidationError
1479
1836
  } from "@photon-ai/advanced-imessage";
1837
+ import { sanitizePhone } from "@photon-ai/otel";
1480
1838
 
1481
1839
  // src/utils/resumable-stream.ts
1840
+ import { createLogger } from "@photon-ai/otel";
1482
1841
  var CATCH_UP_PAGE_SIZE = 100;
1483
1842
  var MAX_BUFFERED_LIVE_EVENTS = 1e3;
1484
1843
  var RECONNECT_INITIAL_DELAY_MS = 500;
1485
1844
  var RECONNECT_MAX_DELAY_MS = 3e4;
1845
+ var PERSISTENT_FAILURE_ERROR_THRESHOLD = 5;
1846
+ var log = createLogger("spectrum.stream");
1486
1847
  var RetryableStreamError = class extends Error {
1487
1848
  constructor(message) {
1488
1849
  super(message);
@@ -1495,6 +1856,12 @@ var LiveBufferOverflowError = class extends RetryableStreamError {
1495
1856
  this.name = "LiveBufferOverflowError";
1496
1857
  }
1497
1858
  };
1859
+ var CursorRejectedError = class extends Error {
1860
+ constructor(cause) {
1861
+ super("Server rejected resume cursor", { cause });
1862
+ this.name = "CursorRejectedError";
1863
+ }
1864
+ };
1498
1865
  var closeIterable = async (iterable) => {
1499
1866
  if (!iterable) {
1500
1867
  return;
@@ -1502,7 +1869,15 @@ var closeIterable = async (iterable) => {
1502
1869
  await iterable.close?.();
1503
1870
  };
1504
1871
  var ignoreCleanupError = () => void 0;
1505
- var jitterDelay = (delayMs) => Math.random() * delayMs;
1872
+ var jitterDelay = (delayMs) => delayMs * (0.5 + Math.random() * 0.5);
1873
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1874
+ async function* throwOnCursorRejection(source, isCursorRejected) {
1875
+ try {
1876
+ yield* source;
1877
+ } catch (error) {
1878
+ throw isCursorRejected(error) ? new CursorRejectedError(error) : error;
1879
+ }
1880
+ }
1506
1881
  var numericCursor = (cursor) => {
1507
1882
  if (!cursor) {
1508
1883
  return;
@@ -1520,15 +1895,23 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1520
1895
  const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
1521
1896
  const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
1522
1897
  const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
1898
+ const jitter = options.jitter ?? jitterDelay;
1899
+ const label = options.label;
1523
1900
  let activeLive;
1524
1901
  let closed = false;
1902
+ let failedAttempts = 0;
1525
1903
  let lastCursor;
1526
1904
  let retryDelayMs = initialRetryDelayMs;
1527
1905
  let sleepTimer;
1528
1906
  let wakeSleep;
1529
1907
  const deliveredSinceCursor = /* @__PURE__ */ new Set();
1530
- const resetRetryDelay = () => {
1908
+ const noteRecovery = () => {
1531
1909
  retryDelayMs = initialRetryDelayMs;
1910
+ if (failedAttempts === 0) {
1911
+ return;
1912
+ }
1913
+ log.info("stream recovered", { attempts: failedAttempts, label });
1914
+ failedAttempts = 0;
1532
1915
  };
1533
1916
  const advanceCursor = (cursor, clearDelivered) => {
1534
1917
  if (!cursor || cursor === lastCursor || isCursorRegression(cursor, lastCursor)) {
@@ -1549,17 +1932,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1549
1932
  advanceCursor(item.cursor, clearOnCursorAdvance);
1550
1933
  deliveredSinceCursor.add(item.id);
1551
1934
  if (resetRetry) {
1552
- resetRetryDelay();
1935
+ noteRecovery();
1553
1936
  }
1554
1937
  };
1555
- const retryable = (error) => error instanceof RetryableStreamError || options.isRetryableError(error);
1938
+ const isCursorRejected = (error) => options.isCursorRejectedError?.(error) === true;
1556
1939
  const sleep2 = async (delayMs) => {
1557
1940
  if (delayMs <= 0 || closed) {
1558
1941
  return;
1559
1942
  }
1560
1943
  await new Promise((resolve) => {
1561
1944
  wakeSleep = resolve;
1562
- sleepTimer = setTimeout(resolve, jitterDelay(delayMs));
1945
+ sleepTimer = setTimeout(resolve, jitter(delayMs));
1563
1946
  });
1564
1947
  sleepTimer = void 0;
1565
1948
  wakeSleep = void 0;
@@ -1577,6 +1960,37 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1577
1960
  retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
1578
1961
  return delay;
1579
1962
  };
1963
+ const handleFailure = (error) => {
1964
+ failedAttempts += 1;
1965
+ const delayMs = nextRetryDelay();
1966
+ if (error instanceof CursorRejectedError) {
1967
+ lastCursor = void 0;
1968
+ deliveredSinceCursor.clear();
1969
+ log.warn(
1970
+ "resume cursor rejected; accepting event gap and resuming live",
1971
+ {
1972
+ attempt: failedAttempts,
1973
+ delayMs,
1974
+ error: errorMessage(error.cause),
1975
+ label
1976
+ }
1977
+ );
1978
+ return delayMs;
1979
+ }
1980
+ const attrs = {
1981
+ attempt: failedAttempts,
1982
+ delayMs,
1983
+ error: errorMessage(error),
1984
+ hasCursor: lastCursor !== void 0,
1985
+ label
1986
+ };
1987
+ if (failedAttempts >= PERSISTENT_FAILURE_ERROR_THRESHOLD) {
1988
+ log.error("stream persistently failing; still retrying", attrs, error);
1989
+ return delayMs;
1990
+ }
1991
+ log.warn("stream interrupted; reconnecting", attrs);
1992
+ return delayMs;
1993
+ };
1580
1994
  const consumeLive = async () => {
1581
1995
  const live = options.subscribeLive(lastCursor);
1582
1996
  activeLive = live;
@@ -1625,15 +2039,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1625
2039
  };
1626
2040
  };
1627
2041
  const replayMissed = async (cursor, getLiveError) => {
1628
- for await (const event of options.fetchMissed(cursor, {
1629
- limit: catchUpPageSize
1630
- })) {
2042
+ const missed = throwOnCursorRejection(
2043
+ options.fetchMissed(cursor, { limit: catchUpPageSize }),
2044
+ isCursorRejected
2045
+ );
2046
+ for await (const event of missed) {
1631
2047
  throwLiveError(getLiveError());
1632
2048
  await deliverItem(await options.processMissed(event), false, false);
1633
2049
  }
1634
2050
  throwLiveError(getLiveError());
1635
2051
  };
1636
- const flushLiveBuffer = async (liveBuffer, getLiveError) => {
2052
+ const flushLiveBuffer = async (liveBuffer, getLiveError, stopBuffering) => {
1637
2053
  let index = 0;
1638
2054
  let lastFlushedId;
1639
2055
  while (index < liveBuffer.length) {
@@ -1649,7 +2065,8 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1649
2065
  }
1650
2066
  liveBuffer.length = 0;
1651
2067
  throwLiveError(getLiveError());
1652
- return lastFlushedId;
2068
+ compactDeliveredIds(lastFlushedId);
2069
+ stopBuffering();
1653
2070
  };
1654
2071
  const compactDeliveredIds = (lastId) => {
1655
2072
  if (!lastId) {
@@ -1666,13 +2083,10 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1666
2083
  const livePump = startLivePump(live, () => buffering, liveBuffer);
1667
2084
  try {
1668
2085
  await replayMissed(cursor, livePump.getError);
1669
- const lastFlushedId = await flushLiveBuffer(
1670
- liveBuffer,
1671
- livePump.getError
1672
- );
1673
- compactDeliveredIds(lastFlushedId);
1674
- buffering = false;
1675
- resetRetryDelay();
2086
+ await flushLiveBuffer(liveBuffer, livePump.getError, () => {
2087
+ buffering = false;
2088
+ });
2089
+ noteRecovery();
1676
2090
  await livePump.pump;
1677
2091
  throwLiveError(livePump.getError());
1678
2092
  } finally {
@@ -1693,21 +2107,18 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1693
2107
  await consumeLive();
1694
2108
  }
1695
2109
  } catch (error) {
1696
- await closeIterable(activeLive);
2110
+ await closeIterable(activeLive).catch(ignoreCleanupError);
1697
2111
  activeLive = void 0;
1698
2112
  if (closed) {
1699
2113
  break;
1700
2114
  }
1701
- if (!retryable(error)) {
1702
- end(error);
1703
- return;
1704
- }
1705
- await sleep2(nextRetryDelay());
2115
+ await sleep2(handleFailure(error));
1706
2116
  }
1707
2117
  }
1708
2118
  end();
1709
2119
  };
1710
2120
  const pump = run().catch((error) => {
2121
+ log.error("resumable stream loop crashed", { label }, error);
1711
2122
  if (!closed) {
1712
2123
  end(error);
1713
2124
  }
@@ -1840,7 +2251,7 @@ var buildPollOptionMessage = (input) => {
1840
2251
  sender: { id: input.senderAddress },
1841
2252
  space: {
1842
2253
  id: input.chatGuid,
1843
- type: input.chatGuid.includes(";+;") ? "group" : "dm",
2254
+ type: chatTypeFromGuid(input.chatGuid),
1844
2255
  phone: input.phone
1845
2256
  },
1846
2257
  timestamp: input.event.occurredAt,
@@ -1896,15 +2307,8 @@ var toPollDeltaMessages = async (client, pollCache, event, phone) => {
1896
2307
  };
1897
2308
 
1898
2309
  // 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
- };
2310
+ var isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
2311
+ var streamLabel = (kind, phone) => `imessage.${kind}:${phone === SHARED_PHONE ? phone : sanitizePhone(phone)}`;
1908
2312
  var isEventFromCurrentAccount = (event, phone) => event.isFromMe || phone !== SHARED_PHONE && event.actor?.address !== void 0 && event.actor.address === phone;
1909
2313
  var toMessageItem = async (client, event, phone, cursor, onInbound) => {
1910
2314
  if (event.type === "message.received") {
@@ -2007,7 +2411,8 @@ var withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), {
2007
2411
  });
2008
2412
  var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2009
2413
  fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
2010
- isRetryableError: isRetryableIMessageStreamError,
2414
+ isCursorRejectedError: isCursorRejectedIMessageError,
2415
+ label: streamLabel("messages", phone),
2011
2416
  processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
2012
2417
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(
2013
2418
  client,
@@ -2020,7 +2425,8 @@ var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2020
2425
  });
2021
2426
  var pollStream = (client, pollCache, phone) => resumableOrderedStream({
2022
2427
  fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
2023
- isRetryableError: isRetryableIMessageStreamError,
2428
+ isCursorRejectedError: isCursorRejectedIMessageError,
2429
+ label: streamLabel("polls", phone),
2024
2430
  processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
2025
2431
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
2026
2432
  subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
@@ -2050,6 +2456,12 @@ var INITIAL_THROTTLE_MS = 1e3;
2050
2456
  var BACKOFF_FACTOR = 2;
2051
2457
  var MAX_EDITS = 5;
2052
2458
  var sendStreamText = async (remote, spaceId, content) => {
2459
+ if (content.format === "markdown") {
2460
+ throw unsupportedRemoteContent(
2461
+ "streamText",
2462
+ "markdown-formatted streams have no native iMessage delivery"
2463
+ );
2464
+ }
2053
2465
  const chat = toChatGuid(spaceId);
2054
2466
  let sent;
2055
2467
  let full = "";
@@ -2121,9 +2533,9 @@ var send4 = async (remote, spaceId, content) => send3(remote, spaceId, content);
2121
2533
  var sendStreamText2 = async (remote, spaceId, content) => sendStreamText(remote, spaceId, content);
2122
2534
  var replyToMessage2 = async (remote, spaceId, msgId, content) => replyToMessage(remote, spaceId, msgId, content);
2123
2535
  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
- };
2536
+ var reactToMessage2 = async (remote, spaceId, target, reaction) => reactToMessage(remote, spaceId, target, reaction);
2537
+ var unsendMessage2 = async (remote, spaceId, msgId) => unsendMessage(remote, spaceId, msgId);
2538
+ var unsendReaction2 = async (remote, spaceId, target, reaction) => unsendReaction(remote, spaceId, target, reaction);
2127
2539
  var getMessage4 = async (remote, spaceId, msgId, phone) => getMessage3(remote, spaceId, msgId, phone);
2128
2540
 
2129
2541
  // src/providers/imessage/remote/client.ts
@@ -2176,6 +2588,30 @@ var handleEdit = async (client, space, content) => {
2176
2588
  const remote = clientForPhone(client, space.phone);
2177
2589
  await editMessage2(remote, space.id, content.target.id, content.content);
2178
2590
  };
2591
+ var handleUnsend = async (client, space, content) => {
2592
+ if (isLocal(client)) {
2593
+ throw UnsupportedError.action("unsend", "iMessage (local mode)");
2594
+ }
2595
+ if (isPollContent(content.target.content)) {
2596
+ throw UnsupportedError.action(
2597
+ "unsend",
2598
+ "iMessage",
2599
+ "iMessage polls cannot be unsent"
2600
+ );
2601
+ }
2602
+ const remote = clientForPhone(client, space.phone);
2603
+ const targetContent = content.target.content;
2604
+ if (targetContent.type === "reaction") {
2605
+ await unsendReaction2(
2606
+ remote,
2607
+ space.id,
2608
+ targetContent.target,
2609
+ targetContent.emoji
2610
+ );
2611
+ return;
2612
+ }
2613
+ await unsendMessage2(remote, space.id, content.target.id);
2614
+ };
2179
2615
  var handleStreamText = async (client, space, content) => {
2180
2616
  if (isLocal(client)) {
2181
2617
  throw UnsupportedError.action(
@@ -2316,10 +2752,10 @@ var imessage = definePlatform("iMessage", {
2316
2752
  space: {
2317
2753
  schema: spaceSchema,
2318
2754
  params: spaceParamsSchema,
2319
- resolve: async ({ input, client }) => {
2755
+ create: async ({ input, client }) => {
2320
2756
  if (isLocal(client)) {
2321
2757
  throw UnsupportedError.action(
2322
- "createSpace",
2758
+ "space.create",
2323
2759
  "iMessage (local mode)",
2324
2760
  "local mode only supports replying to existing messages"
2325
2761
  );
@@ -2330,18 +2766,52 @@ var imessage = definePlatform("iMessage", {
2330
2766
  if (client.length === 0) {
2331
2767
  throw new Error("No iMessage clients configured");
2332
2768
  }
2333
- const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? randomPhone(client);
2334
- const remote = clientForPhone(client, phone);
2335
2769
  const addresses = input.users.map((u) => u.id);
2336
- if (input.users.length === 1) {
2770
+ if (isSharedMode(client)) {
2771
+ if (addresses.length > 1) {
2772
+ throw UnsupportedError.action(
2773
+ "space.create",
2774
+ "iMessage (shared mode)",
2775
+ "shared mode cannot create group chats \u2014 use a dedicated number, or space.get(chatGuid) for an existing group"
2776
+ );
2777
+ }
2337
2778
  return {
2338
2779
  id: dmChatGuid(addresses[0] ?? ""),
2339
2780
  type: "dm",
2340
- phone
2781
+ phone: SHARED_PHONE
2341
2782
  };
2342
2783
  }
2784
+ const phone = input.params?.phone ?? randomPhone(client);
2785
+ const remote = clientForPhone(client, phone);
2343
2786
  const { chat } = await remote.chats.create(addresses);
2344
- return { id: chat.guid, type: "group", phone };
2787
+ return {
2788
+ id: chat.guid,
2789
+ type: chat.isGroup ? "group" : "dm",
2790
+ phone
2791
+ };
2792
+ },
2793
+ get: async ({ input, client }) => {
2794
+ if (isLocal(client)) {
2795
+ throw UnsupportedError.action(
2796
+ "space.get",
2797
+ "iMessage (local mode)",
2798
+ "local mode only supports replying to existing messages"
2799
+ );
2800
+ }
2801
+ if (client.length === 0) {
2802
+ throw new Error("No iMessage clients configured");
2803
+ }
2804
+ const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
2805
+ if (!phone) {
2806
+ throw new Error(
2807
+ `iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`
2808
+ );
2809
+ }
2810
+ return {
2811
+ id: input.id,
2812
+ type: chatTypeFromGuid(input.id),
2813
+ phone
2814
+ };
2345
2815
  },
2346
2816
  actions: {
2347
2817
  // Sugar: `space.background(input, opts?)` →
@@ -2401,13 +2871,12 @@ var imessage = definePlatform("iMessage", {
2401
2871
  );
2402
2872
  }
2403
2873
  const remote2 = clientForPhone(client, space.phone);
2404
- await reactToMessage2(
2874
+ return await reactToMessage2(
2405
2875
  remote2,
2406
2876
  space.id,
2407
2877
  content.target,
2408
2878
  content.emoji
2409
2879
  );
2410
- return;
2411
2880
  }
2412
2881
  if (content.type === "typing") {
2413
2882
  await handleTyping(client, space, content.state);
@@ -2417,6 +2886,10 @@ var imessage = definePlatform("iMessage", {
2417
2886
  await handleEdit(client, space, content);
2418
2887
  return;
2419
2888
  }
2889
+ if (content.type === "unsend") {
2890
+ await handleUnsend(client, space, content);
2891
+ return;
2892
+ }
2420
2893
  if (content.type === "streamText") {
2421
2894
  return await handleStreamText(client, space, content);
2422
2895
  }