spectrum-ts 1.18.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 (38) hide show
  1. package/README.md +11 -1
  2. package/dist/{attachment-DfWSZS5L.d.ts → attachment-WePAHfcH.d.ts} +1 -1
  3. package/dist/{authoring-C9uDdZ2F.d.ts → authoring-DDh3muGT.d.ts} +61 -26
  4. package/dist/authoring.d.ts +3 -3
  5. package/dist/authoring.js +8 -5
  6. package/dist/chunk-34FQGGD7.js +34 -0
  7. package/dist/chunk-3GEJYGZK.js +84 -0
  8. package/dist/{chunk-MC6ZKFSG.js → chunk-5XEFJBN2.js} +25 -103
  9. package/dist/{chunk-QGJFZMD5.js → chunk-6UZFVXQF.js} +17 -101
  10. package/dist/{chunk-NNY6LMSC.js → chunk-77U6SH5A.js} +1 -1
  11. package/dist/{chunk-YN6WOTBF.js → chunk-AYCMTRVC.js} +622 -79
  12. package/dist/{chunk-JQN6CRSC.js → chunk-CHY5YLLV.js} +11 -40
  13. package/dist/{chunk-5BKZJMZV.js → chunk-EZ5SNNFS.js} +79 -38
  14. package/dist/{chunk-3OTECDNH.js → chunk-FULEQIRQ.js} +31 -23
  15. package/dist/{chunk-2ILTJC35.js → chunk-LQMDV75O.js} +205 -11
  16. package/dist/{chunk-IPOFBAIM.js → chunk-LX437ZTY.js} +439 -154
  17. package/dist/chunk-MHGCPC2V.js +35 -0
  18. package/dist/chunk-NZ5WCMTY.js +91 -0
  19. package/dist/chunk-TXRWKSNH.js +927 -0
  20. package/dist/{chunk-5TIF3FIE.js → chunk-UXJ5OO6P.js} +16 -14
  21. package/dist/index.d.ts +125 -129
  22. package/dist/index.js +180 -73
  23. package/dist/manifest.json +6 -0
  24. package/dist/providers/imessage/index.d.ts +6 -14
  25. package/dist/providers/imessage/index.js +9 -6
  26. package/dist/providers/index.d.ts +5 -2
  27. package/dist/providers/index.js +18 -10
  28. package/dist/providers/slack/index.d.ts +1 -2
  29. package/dist/providers/slack/index.js +5 -4
  30. package/dist/providers/telegram/index.d.ts +45 -0
  31. package/dist/providers/telegram/index.js +13 -0
  32. package/dist/providers/terminal/index.d.ts +18 -422
  33. package/dist/providers/terminal/index.js +7 -5
  34. package/dist/providers/whatsapp-business/index.d.ts +1 -1
  35. package/dist/providers/whatsapp-business/index.js +7 -5
  36. package/dist/types-BujGKBin.d.ts +82 -0
  37. package/dist/{types-DcQ5a7PK.d.ts → types-YqCNUDIt.d.ts} +204 -26
  38. package/package.json +3 -1
@@ -1,29 +1,36 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
+ import {
3
+ asRichlink
4
+ } from "./chunk-CHY5YLLV.js";
2
5
  import {
3
6
  asGroup,
4
- asRichlink,
5
7
  groupSchema
6
- } from "./chunk-JQN6CRSC.js";
8
+ } from "./chunk-MHGCPC2V.js";
7
9
  import {
8
10
  asPoll,
9
11
  asPollOption
10
12
  } from "./chunk-2D27WW5B.js";
11
13
  import {
12
- cloud,
14
+ cloud
15
+ } from "./chunk-3GEJYGZK.js";
16
+ import {
17
+ asContact
18
+ } from "./chunk-NZ5WCMTY.js";
19
+ import {
13
20
  mergeStreams,
14
21
  stream
15
- } from "./chunk-MC6ZKFSG.js";
22
+ } from "./chunk-5XEFJBN2.js";
16
23
  import {
17
- asContact,
18
24
  fromVCard,
19
25
  toVCard
20
- } from "./chunk-QGJFZMD5.js";
26
+ } from "./chunk-6UZFVXQF.js";
21
27
  import {
22
28
  UnsupportedError,
23
29
  buildPhotoAction,
24
30
  definePlatform,
31
+ markdownSchema,
25
32
  photoActionSchema
26
- } from "./chunk-IPOFBAIM.js";
33
+ } from "./chunk-LX437ZTY.js";
27
34
  import {
28
35
  asAttachment,
29
36
  asCustom,
@@ -32,7 +39,7 @@ import {
32
39
  reactionSchema,
33
40
  text,
34
41
  textSchema
35
- } from "./chunk-2ILTJC35.js";
42
+ } from "./chunk-LQMDV75O.js";
36
43
 
37
44
  // src/providers/imessage/index.ts
38
45
  import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
@@ -127,6 +134,7 @@ import {
127
134
  import z3 from "zod";
128
135
  var effectInnerSchema = z3.discriminatedUnion("type", [
129
136
  textSchema,
137
+ markdownSchema,
130
138
  attachmentSchema
131
139
  ]);
132
140
  var messageEffectSchema = z3.object({
@@ -147,9 +155,9 @@ function effect(input, messageEffect) {
147
155
  );
148
156
  }
149
157
  const inner = await resolveContent(input);
150
- if (inner.type !== "text" && inner.type !== "attachment") {
158
+ if (inner.type !== "text" && inner.type !== "markdown" && inner.type !== "attachment") {
151
159
  throw new Error(
152
- `imessage effect() only supports text and attachment content, got "${inner.type}"`
160
+ `imessage effect() only supports text, markdown, and attachment content, got "${inner.type}"`
153
161
  );
154
162
  }
155
163
  return messageEffectSchema.parse({
@@ -565,6 +573,7 @@ var getMessage2 = async (client, id) => getMessage(client, id);
565
573
  // src/providers/imessage/remote/ids.ts
566
574
  var PART_PREFIX = /^p:(\d+)\//;
567
575
  var dmChatGuid = (address) => `any;-;${address}`;
576
+ var chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
568
577
  var toChatGuid = (value) => value;
569
578
  var toMessageGuid = (value) => value;
570
579
  var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
@@ -799,7 +808,7 @@ var buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
799
808
  sender: { id: resolveSenderId(message) },
800
809
  space: {
801
810
  id: chat,
802
- type: chat.includes(";+;") ? "group" : "dm",
811
+ type: chatTypeFromGuid(chat),
803
812
  phone
804
813
  },
805
814
  timestamp
@@ -1084,7 +1093,7 @@ var toReactionMessages = async (client, cache, event, phone) => {
1084
1093
  sender: { id: senderAddress },
1085
1094
  space: {
1086
1095
  id: event.chatGuid,
1087
- type: event.chatGuid.includes(";+;") ? "group" : "dm",
1096
+ type: chatTypeFromGuid(event.chatGuid),
1088
1097
  phone
1089
1098
  },
1090
1099
  timestamp: event.occurredAt,
@@ -1093,23 +1102,39 @@ var toReactionMessages = async (client, cache, event, phone) => {
1093
1102
  }
1094
1103
  ];
1095
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
+ });
1096
1113
  var reactToMessage = async (remote, spaceId, target, reaction) => {
1097
- const chat = toChatGuid(spaceId);
1098
- const parentGuid = target.parentId ?? target.id;
1099
- const guid = toMessageGuid(parentGuid);
1100
- const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
1101
- const native = EMOJI_TO_TAPBACK[reaction];
1102
- if (native) {
1103
- await remote.messages.setReaction(chat, guid, { kind: native }, true, opts);
1104
- } else {
1105
- await remote.messages.setReaction(
1106
- chat,
1107
- guid,
1108
- { kind: "emoji", emoji: reaction },
1109
- true,
1110
- opts
1111
- );
1112
- }
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
+ );
1113
1138
  };
1114
1139
 
1115
1140
  // src/providers/imessage/remote/read.ts
@@ -1245,13 +1270,314 @@ var ensureM4a = async (buffer, mimeType) => {
1245
1270
  return transcodeToM4a(buffer);
1246
1271
  };
1247
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
+
1248
1569
  // src/providers/imessage/remote/send.ts
1249
1570
  var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
1250
1571
  "text",
1572
+ "markdown",
1251
1573
  "attachment",
1252
1574
  "contact",
1253
1575
  "voice"
1254
1576
  ]);
1577
+ var GROUP_TEXT_TYPES = /* @__PURE__ */ new Set([
1578
+ "text",
1579
+ "markdown"
1580
+ ]);
1255
1581
  var MAX_GROUP_TEXT_ITEMS = 1;
1256
1582
  var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
1257
1583
  id,
@@ -1268,6 +1594,18 @@ var providerGroup = (items) => asGroup({ items });
1268
1594
  var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
1269
1595
  var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
1270
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
+ };
1271
1609
  var replyTargetFromId = (messageId) => {
1272
1610
  const childRef = parseChildId(messageId);
1273
1611
  if (childRef) {
@@ -1325,6 +1663,22 @@ var sendContent = async (remote, spaceId, chat, content, replyTo, effect2) => {
1325
1663
  );
1326
1664
  return outboundMessage(spaceId, message, content);
1327
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
+ }
1328
1682
  case "richlink": {
1329
1683
  const message = await remote.messages.sendText(
1330
1684
  chat,
@@ -1389,7 +1743,7 @@ var validateGroupContent = (content) => {
1389
1743
  `"${itemType}" items are not supported inside a group`
1390
1744
  );
1391
1745
  }
1392
- if (itemType === "text" && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1746
+ if (GROUP_TEXT_TYPES.has(itemType) && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1393
1747
  throw unsupportedRemoteContent(
1394
1748
  "group",
1395
1749
  `groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
@@ -1401,6 +1755,10 @@ var resolvePart = async (remote, content) => {
1401
1755
  switch (content.type) {
1402
1756
  case "text":
1403
1757
  return { text: content.text };
1758
+ case "markdown": {
1759
+ const rendered = renderMarkdown(content.markdown);
1760
+ return { text: rendered.text, ...formattingOption(rendered.formatting) };
1761
+ }
1404
1762
  case "attachment": {
1405
1763
  const { guid, name } = await uploadAttachment(remote, content);
1406
1764
  return { attachmentGuid: guid, attachmentName: name };
@@ -1463,20 +1821,29 @@ var editMessage = async (remote, spaceId, msgId, content) => {
1463
1821
  childRef ? { partIndex: childRef.partIndex } : void 0
1464
1822
  );
1465
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
+ };
1466
1832
 
1467
1833
  // src/providers/imessage/remote/stream.ts
1468
1834
  import {
1469
- AuthenticationError,
1470
- IMessageError,
1471
- NotFoundError as NotFoundError3,
1472
1835
  ValidationError
1473
1836
  } from "@photon-ai/advanced-imessage";
1837
+ import { sanitizePhone } from "@photon-ai/otel";
1474
1838
 
1475
1839
  // src/utils/resumable-stream.ts
1840
+ import { createLogger } from "@photon-ai/otel";
1476
1841
  var CATCH_UP_PAGE_SIZE = 100;
1477
1842
  var MAX_BUFFERED_LIVE_EVENTS = 1e3;
1478
1843
  var RECONNECT_INITIAL_DELAY_MS = 500;
1479
1844
  var RECONNECT_MAX_DELAY_MS = 3e4;
1845
+ var PERSISTENT_FAILURE_ERROR_THRESHOLD = 5;
1846
+ var log = createLogger("spectrum.stream");
1480
1847
  var RetryableStreamError = class extends Error {
1481
1848
  constructor(message) {
1482
1849
  super(message);
@@ -1489,6 +1856,12 @@ var LiveBufferOverflowError = class extends RetryableStreamError {
1489
1856
  this.name = "LiveBufferOverflowError";
1490
1857
  }
1491
1858
  };
1859
+ var CursorRejectedError = class extends Error {
1860
+ constructor(cause) {
1861
+ super("Server rejected resume cursor", { cause });
1862
+ this.name = "CursorRejectedError";
1863
+ }
1864
+ };
1492
1865
  var closeIterable = async (iterable) => {
1493
1866
  if (!iterable) {
1494
1867
  return;
@@ -1496,7 +1869,15 @@ var closeIterable = async (iterable) => {
1496
1869
  await iterable.close?.();
1497
1870
  };
1498
1871
  var ignoreCleanupError = () => void 0;
1499
- 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
+ }
1500
1881
  var numericCursor = (cursor) => {
1501
1882
  if (!cursor) {
1502
1883
  return;
@@ -1514,15 +1895,23 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1514
1895
  const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
1515
1896
  const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
1516
1897
  const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
1898
+ const jitter = options.jitter ?? jitterDelay;
1899
+ const label = options.label;
1517
1900
  let activeLive;
1518
1901
  let closed = false;
1902
+ let failedAttempts = 0;
1519
1903
  let lastCursor;
1520
1904
  let retryDelayMs = initialRetryDelayMs;
1521
1905
  let sleepTimer;
1522
1906
  let wakeSleep;
1523
1907
  const deliveredSinceCursor = /* @__PURE__ */ new Set();
1524
- const resetRetryDelay = () => {
1908
+ const noteRecovery = () => {
1525
1909
  retryDelayMs = initialRetryDelayMs;
1910
+ if (failedAttempts === 0) {
1911
+ return;
1912
+ }
1913
+ log.info("stream recovered", { attempts: failedAttempts, label });
1914
+ failedAttempts = 0;
1526
1915
  };
1527
1916
  const advanceCursor = (cursor, clearDelivered) => {
1528
1917
  if (!cursor || cursor === lastCursor || isCursorRegression(cursor, lastCursor)) {
@@ -1543,17 +1932,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1543
1932
  advanceCursor(item.cursor, clearOnCursorAdvance);
1544
1933
  deliveredSinceCursor.add(item.id);
1545
1934
  if (resetRetry) {
1546
- resetRetryDelay();
1935
+ noteRecovery();
1547
1936
  }
1548
1937
  };
1549
- const retryable = (error) => error instanceof RetryableStreamError || options.isRetryableError(error);
1938
+ const isCursorRejected = (error) => options.isCursorRejectedError?.(error) === true;
1550
1939
  const sleep2 = async (delayMs) => {
1551
1940
  if (delayMs <= 0 || closed) {
1552
1941
  return;
1553
1942
  }
1554
1943
  await new Promise((resolve) => {
1555
1944
  wakeSleep = resolve;
1556
- sleepTimer = setTimeout(resolve, jitterDelay(delayMs));
1945
+ sleepTimer = setTimeout(resolve, jitter(delayMs));
1557
1946
  });
1558
1947
  sleepTimer = void 0;
1559
1948
  wakeSleep = void 0;
@@ -1571,6 +1960,37 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1571
1960
  retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
1572
1961
  return delay;
1573
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
+ };
1574
1994
  const consumeLive = async () => {
1575
1995
  const live = options.subscribeLive(lastCursor);
1576
1996
  activeLive = live;
@@ -1619,15 +2039,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1619
2039
  };
1620
2040
  };
1621
2041
  const replayMissed = async (cursor, getLiveError) => {
1622
- for await (const event of options.fetchMissed(cursor, {
1623
- limit: catchUpPageSize
1624
- })) {
2042
+ const missed = throwOnCursorRejection(
2043
+ options.fetchMissed(cursor, { limit: catchUpPageSize }),
2044
+ isCursorRejected
2045
+ );
2046
+ for await (const event of missed) {
1625
2047
  throwLiveError(getLiveError());
1626
2048
  await deliverItem(await options.processMissed(event), false, false);
1627
2049
  }
1628
2050
  throwLiveError(getLiveError());
1629
2051
  };
1630
- const flushLiveBuffer = async (liveBuffer, getLiveError) => {
2052
+ const flushLiveBuffer = async (liveBuffer, getLiveError, stopBuffering) => {
1631
2053
  let index = 0;
1632
2054
  let lastFlushedId;
1633
2055
  while (index < liveBuffer.length) {
@@ -1643,7 +2065,8 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1643
2065
  }
1644
2066
  liveBuffer.length = 0;
1645
2067
  throwLiveError(getLiveError());
1646
- return lastFlushedId;
2068
+ compactDeliveredIds(lastFlushedId);
2069
+ stopBuffering();
1647
2070
  };
1648
2071
  const compactDeliveredIds = (lastId) => {
1649
2072
  if (!lastId) {
@@ -1660,13 +2083,10 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1660
2083
  const livePump = startLivePump(live, () => buffering, liveBuffer);
1661
2084
  try {
1662
2085
  await replayMissed(cursor, livePump.getError);
1663
- const lastFlushedId = await flushLiveBuffer(
1664
- liveBuffer,
1665
- livePump.getError
1666
- );
1667
- compactDeliveredIds(lastFlushedId);
1668
- buffering = false;
1669
- resetRetryDelay();
2086
+ await flushLiveBuffer(liveBuffer, livePump.getError, () => {
2087
+ buffering = false;
2088
+ });
2089
+ noteRecovery();
1670
2090
  await livePump.pump;
1671
2091
  throwLiveError(livePump.getError());
1672
2092
  } finally {
@@ -1687,21 +2107,18 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1687
2107
  await consumeLive();
1688
2108
  }
1689
2109
  } catch (error) {
1690
- await closeIterable(activeLive);
2110
+ await closeIterable(activeLive).catch(ignoreCleanupError);
1691
2111
  activeLive = void 0;
1692
2112
  if (closed) {
1693
2113
  break;
1694
2114
  }
1695
- if (!retryable(error)) {
1696
- end(error);
1697
- return;
1698
- }
1699
- await sleep2(nextRetryDelay());
2115
+ await sleep2(handleFailure(error));
1700
2116
  }
1701
2117
  }
1702
2118
  end();
1703
2119
  };
1704
2120
  const pump = run().catch((error) => {
2121
+ log.error("resumable stream loop crashed", { label }, error);
1705
2122
  if (!closed) {
1706
2123
  end(error);
1707
2124
  }
@@ -1834,7 +2251,7 @@ var buildPollOptionMessage = (input) => {
1834
2251
  sender: { id: input.senderAddress },
1835
2252
  space: {
1836
2253
  id: input.chatGuid,
1837
- type: input.chatGuid.includes(";+;") ? "group" : "dm",
2254
+ type: chatTypeFromGuid(input.chatGuid),
1838
2255
  phone: input.phone
1839
2256
  },
1840
2257
  timestamp: input.event.occurredAt,
@@ -1890,15 +2307,8 @@ var toPollDeltaMessages = async (client, pollCache, event, phone) => {
1890
2307
  };
1891
2308
 
1892
2309
  // src/providers/imessage/remote/stream.ts
1893
- var isRetryableIMessageStreamError = (error) => {
1894
- if (error instanceof AuthenticationError || error instanceof NotFoundError3 || error instanceof ValidationError) {
1895
- return false;
1896
- }
1897
- if (error instanceof IMessageError) {
1898
- return true;
1899
- }
1900
- return false;
1901
- };
2310
+ var isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
2311
+ var streamLabel = (kind, phone) => `imessage.${kind}:${phone === SHARED_PHONE ? phone : sanitizePhone(phone)}`;
1902
2312
  var isEventFromCurrentAccount = (event, phone) => event.isFromMe || phone !== SHARED_PHONE && event.actor?.address !== void 0 && event.actor.address === phone;
1903
2313
  var toMessageItem = async (client, event, phone, cursor, onInbound) => {
1904
2314
  if (event.type === "message.received") {
@@ -2001,7 +2411,8 @@ var withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), {
2001
2411
  });
2002
2412
  var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2003
2413
  fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
2004
- isRetryableError: isRetryableIMessageStreamError,
2414
+ isCursorRejectedError: isCursorRejectedIMessageError,
2415
+ label: streamLabel("messages", phone),
2005
2416
  processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
2006
2417
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(
2007
2418
  client,
@@ -2014,7 +2425,8 @@ var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2014
2425
  });
2015
2426
  var pollStream = (client, pollCache, phone) => resumableOrderedStream({
2016
2427
  fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
2017
- isRetryableError: isRetryableIMessageStreamError,
2428
+ isCursorRejectedError: isCursorRejectedIMessageError,
2429
+ label: streamLabel("polls", phone),
2018
2430
  processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
2019
2431
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
2020
2432
  subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
@@ -2039,6 +2451,61 @@ var messages3 = (clients, projectConfig) => {
2039
2451
  );
2040
2452
  };
2041
2453
 
2454
+ // src/providers/imessage/remote/stream-text.ts
2455
+ var INITIAL_THROTTLE_MS = 1e3;
2456
+ var BACKOFF_FACTOR = 2;
2457
+ var MAX_EDITS = 5;
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
+ }
2465
+ const chat = toChatGuid(spaceId);
2466
+ let sent;
2467
+ let full = "";
2468
+ let lastSentText = "";
2469
+ let lastEditAt = 0;
2470
+ let editCount = 0;
2471
+ const flushEdit = async (text2) => {
2472
+ if (!sent || text2 === lastSentText) {
2473
+ return;
2474
+ }
2475
+ await remote.messages.edit(chat, toMessageGuid(sent.guid), text2);
2476
+ lastSentText = text2;
2477
+ lastEditAt = Date.now();
2478
+ editCount += 1;
2479
+ };
2480
+ for await (const delta of content.stream()) {
2481
+ full += delta;
2482
+ if (!sent) {
2483
+ sent = await remote.messages.sendText(chat, full);
2484
+ lastSentText = full;
2485
+ lastEditAt = Date.now();
2486
+ continue;
2487
+ }
2488
+ const hasBudgetForInterimEdit = editCount < MAX_EDITS - 1;
2489
+ const requiredGap = INITIAL_THROTTLE_MS * BACKOFF_FACTOR ** editCount;
2490
+ if (hasBudgetForInterimEdit && Date.now() - lastEditAt >= requiredGap) {
2491
+ await flushEdit(full);
2492
+ }
2493
+ }
2494
+ if (!sent) {
2495
+ throw unsupportedRemoteContent(
2496
+ "streamText",
2497
+ "stream produced no text \u2014 nothing to send"
2498
+ );
2499
+ }
2500
+ await flushEdit(full);
2501
+ return {
2502
+ id: sent.guid,
2503
+ content: asText(full),
2504
+ space: { id: spaceId },
2505
+ timestamp: sent.dateCreated
2506
+ };
2507
+ };
2508
+
2042
2509
  // src/providers/imessage/remote/typing.ts
2043
2510
  var startTyping = async (remote, spaceId) => {
2044
2511
  await remote.chats.setTyping(toChatGuid(spaceId), true);
@@ -2063,11 +2530,12 @@ var stopTyping2 = async (remote, spaceId) => {
2063
2530
  await stopTyping(remote, spaceId);
2064
2531
  };
2065
2532
  var send4 = async (remote, spaceId, content) => send3(remote, spaceId, content);
2533
+ var sendStreamText2 = async (remote, spaceId, content) => sendStreamText(remote, spaceId, content);
2066
2534
  var replyToMessage2 = async (remote, spaceId, msgId, content) => replyToMessage(remote, spaceId, msgId, content);
2067
2535
  var editMessage2 = async (remote, spaceId, msgId, content) => editMessage(remote, spaceId, msgId, content);
2068
- var reactToMessage2 = async (remote, spaceId, target, reaction) => {
2069
- await reactToMessage(remote, spaceId, target, reaction);
2070
- };
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);
2071
2539
  var getMessage4 = async (remote, spaceId, msgId, phone) => getMessage3(remote, spaceId, msgId, phone);
2072
2540
 
2073
2541
  // src/providers/imessage/remote/client.ts
@@ -2120,6 +2588,41 @@ var handleEdit = async (client, space, content) => {
2120
2588
  const remote = clientForPhone(client, space.phone);
2121
2589
  await editMessage2(remote, space.id, content.target.id, content.content);
2122
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
+ };
2615
+ var handleStreamText = async (client, space, content) => {
2616
+ if (isLocal(client)) {
2617
+ throw UnsupportedError.action(
2618
+ "streamText",
2619
+ "iMessage (local mode)",
2620
+ "streaming text responses require remote iMessage"
2621
+ );
2622
+ }
2623
+ const remote = clientForPhone(client, space.phone);
2624
+ return await sendStreamText2(remote, space.id, content);
2625
+ };
2123
2626
  var handleBackground = async (client, space, content) => {
2124
2627
  if (isLocal(client)) {
2125
2628
  throw UnsupportedError.action(
@@ -2249,10 +2752,10 @@ var imessage = definePlatform("iMessage", {
2249
2752
  space: {
2250
2753
  schema: spaceSchema,
2251
2754
  params: spaceParamsSchema,
2252
- resolve: async ({ input, client }) => {
2755
+ create: async ({ input, client }) => {
2253
2756
  if (isLocal(client)) {
2254
2757
  throw UnsupportedError.action(
2255
- "createSpace",
2758
+ "space.create",
2256
2759
  "iMessage (local mode)",
2257
2760
  "local mode only supports replying to existing messages"
2258
2761
  );
@@ -2263,18 +2766,52 @@ var imessage = definePlatform("iMessage", {
2263
2766
  if (client.length === 0) {
2264
2767
  throw new Error("No iMessage clients configured");
2265
2768
  }
2266
- const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? randomPhone(client);
2267
- const remote = clientForPhone(client, phone);
2268
2769
  const addresses = input.users.map((u) => u.id);
2269
- 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
+ }
2270
2778
  return {
2271
2779
  id: dmChatGuid(addresses[0] ?? ""),
2272
2780
  type: "dm",
2273
- phone
2781
+ phone: SHARED_PHONE
2274
2782
  };
2275
2783
  }
2784
+ const phone = input.params?.phone ?? randomPhone(client);
2785
+ const remote = clientForPhone(client, phone);
2276
2786
  const { chat } = await remote.chats.create(addresses);
2277
- 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
+ };
2278
2815
  },
2279
2816
  actions: {
2280
2817
  // Sugar: `space.background(input, opts?)` →
@@ -2334,13 +2871,12 @@ var imessage = definePlatform("iMessage", {
2334
2871
  );
2335
2872
  }
2336
2873
  const remote2 = clientForPhone(client, space.phone);
2337
- await reactToMessage2(
2874
+ return await reactToMessage2(
2338
2875
  remote2,
2339
2876
  space.id,
2340
2877
  content.target,
2341
2878
  content.emoji
2342
2879
  );
2343
- return;
2344
2880
  }
2345
2881
  if (content.type === "typing") {
2346
2882
  await handleTyping(client, space, content.state);
@@ -2350,6 +2886,13 @@ var imessage = definePlatform("iMessage", {
2350
2886
  await handleEdit(client, space, content);
2351
2887
  return;
2352
2888
  }
2889
+ if (content.type === "unsend") {
2890
+ await handleUnsend(client, space, content);
2891
+ return;
2892
+ }
2893
+ if (content.type === "streamText") {
2894
+ return await handleStreamText(client, space, content);
2895
+ }
2353
2896
  if (content.type === "rename") {
2354
2897
  await handleRename(client, space, content);
2355
2898
  return;