@photon-ai/chat-adapter-imessage 2.1.0 → 2.2.1

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.
package/README.md CHANGED
@@ -238,15 +238,23 @@ IMESSAGE_WEBHOOK_SECRET=whsec_... # per-webhook signing secret
238
238
  |---------|-----------|
239
239
  | Mentions | DMs only |
240
240
  | DMs | Yes |
241
+ | Open DM (cold-start) | Yes (`openDM`) |
241
242
  | File uploads | Yes (send) |
242
243
  | Reactions (add) | Remote only |
243
- | Reactions (remove) | No |
244
+ | Reactions (remove) | Remote only (session-added tapbacks) |
244
245
  | Message editing | Remote only |
246
+ | Message delete | Remote only (iMessage unsend window) |
247
+ | Mark read | Remote only (`markRead`) |
245
248
  | Typing indicator | Remote only |
249
+ | Message effects | Remote only (`sendEffect`) |
250
+ | Mini-app cards | Remote only (`sendMiniApp`) |
251
+ | Voice messages | Remote only (`sendVoice`) |
252
+ | Chat background | Remote only (`setBackground`) |
246
253
  | Modals | Limited (Remote only) |
254
+ | Fetch single message | Yes (`fetchMessage`) |
247
255
  | Message history | No |
248
256
  | Thread/chat info | No |
249
- | Cards | No |
257
+ | Cards | Mini-app cards only (`sendMiniApp`) |
250
258
  | Streaming | No |
251
259
  | Ephemeral messages | No |
252
260
  | Webhooks | Yes (remote — Spectrum Cloud delivery) |
@@ -312,14 +320,100 @@ iMessage uses tapbacks instead of emoji reactions. The adapter maps standard emo
312
320
  | `emphasize` / `exclamation` | Emphasize |
313
321
  | `question` | Question |
314
322
 
323
+ ## Message effects
324
+
325
+ iMessage expressive-send effects animate a message when it arrives. The adapter exposes them through `sendEffect(threadId, message, effect)` — an adapter-specific extra (there is no first-class Chat SDK slot for effects). It sends the text with the effect attached and returns the sent message. Remote only; local mode throws `NotImplementedError`.
326
+
327
+ ```typescript
328
+ import { createiMessageAdapter, iMessageEffect } from "@photon-ai/chat-adapter-imessage";
329
+
330
+ const adapter = createiMessageAdapter({ local: false });
331
+
332
+ bot.onNewMention(async (thread) => {
333
+ // Friendly name…
334
+ await adapter.sendEffect(thread.id, "🎉 Task complete!", "confetti");
335
+ // …or the typed constant:
336
+ await adapter.sendEffect(thread.id, "🎉 Task complete!", iMessageEffect.confetti);
337
+ });
338
+ ```
339
+
340
+ The `effect` argument accepts a friendly name or a value from the re-exported `iMessageEffect` map. Full-screen effects: `confetti`, `fireworks`, `balloons`, `heart`, `lasers`, `celebration`, `sparkles`, `spotlight`, `echo`. Bubble effects: `slam`, `loud`, `gentle`, `invisible` (invisible ink). Effects attach to text only, so `sendEffect` requires non-empty text content; an unknown effect throws a `ValidationError`.
341
+
342
+ ## Mini-app cards
343
+
344
+ Mini-app cards are native `MSMessageExtension` balloons — a rich card with a tap-through URL, the closest iMessage gets to a Slack-style rich card rather than a bare link. The adapter exposes them through `sendMiniApp(threadId, card)` — an adapter-specific extra (there is no first-class Chat SDK slot for cards). Remote only; local mode throws `NotImplementedError`.
345
+
346
+ `sendMiniApp` takes **either** a bare URL **or** a fully-specified card.
347
+
348
+ ### Just a URL (`app(url)`)
349
+
350
+ The lightweight form: pass a URL string and iMessage renders it as a mini-app — no extension identifiers required. You can also pass a `Promise<string>` or a thunk (`() => string | Promise<string>`), so the link can be minted at send time (e.g. a signed URL).
351
+
352
+ ```typescript
353
+ import { createiMessageAdapter } from "@photon-ai/chat-adapter-imessage";
354
+
355
+ const adapter = createiMessageAdapter({ local: false });
356
+
357
+ bot.onNewMention(async (thread) => {
358
+ await adapter.sendMiniApp(thread.id, "https://example.com/menu");
359
+
360
+ // …or compute the URL lazily at send time:
361
+ await adapter.sendMiniApp(thread.id, async () => mintSignedLink(thread.id));
362
+ });
363
+ ```
364
+
365
+ ### A full card (`customizedMiniApp`)
366
+
367
+ Pass an object to control the bubble's image, captions, and the exact iMessage extension that opens on tap.
368
+
369
+ ```typescript
370
+ await adapter.sendMiniApp(thread.id, {
371
+ appName: "Poll Kit",
372
+ teamId: "TEAM123",
373
+ extensionBundleId: "com.example.pollkit.MessagesExtension",
374
+ url: "https://example.com/poll/42",
375
+ appStoreId: 1_234_567, // optional — for recipients without the extension
376
+ layout: {
377
+ caption: "Pizza night?",
378
+ subcaption: "Tap to vote",
379
+ imageTitle: "Friday",
380
+ image: pngBytes, // Uint8Array | Buffer | ArrayBuffer | Blob | FileUpload
381
+ summary: "Vote on Friday's dinner",
382
+ },
383
+ });
384
+ ```
385
+
386
+ `appName`, `teamId`, and `extensionBundleId` identify the iMessage extension that opens (receiving `url`) when the recipient taps the card; the server builds the matching `MSMessageExtensionBalloonPlugin` id from `teamId` + `extensionBundleId`. Every `layout` field is optional. The `url` accepts a string or `URL` and is validated; a missing required field or an invalid URL throws a `ValidationError`.
387
+
388
+ ## Chat background
389
+
390
+ iMessage lets a conversation carry its own wallpaper — a touch with no analog on the plain-text competitors. The adapter exposes it through `setBackground(threadId, input, options?)` — an adapter-specific extra (there is no first-class Chat SDK slot for it). It is fire-and-forget: iMessage acknowledges the control signal without returning a message, so the call resolves to `void`. Remote only; local mode throws `NotImplementedError`.
391
+
392
+ ```typescript
393
+ import { readFile } from "node:fs/promises";
394
+
395
+ // From image bytes (Uint8Array | Buffer | ArrayBuffer | Blob | FileUpload).
396
+ await adapter.setBackground(thread.id, await readFile("./wallpaper.jpg"), {
397
+ mimeType: "image/jpeg",
398
+ });
399
+
400
+ // From an http(s) URL, fetched at send time.
401
+ await adapter.setBackground(thread.id, "https://example.com/wallpaper.jpg");
402
+
403
+ // Remove the current background.
404
+ await adapter.setBackground(thread.id, "clear");
405
+ ```
406
+
407
+ Pass the literal `"clear"` to remove the current background, in-memory image bytes, or an `http(s)` URL (a `URL` or a string) that spectrum-ts fetches at send time. Image bytes need an `image/*` MIME type — supply `options.mimeType` (e.g. `"image/jpeg"`) or an `options.name` with an image extension when it can't be inferred. Local file-path strings are rejected — read the file into bytes and pass those instead. A non-image MIME type, an unresolvable MIME type, or a non-`http(s)` string throws a `ValidationError`.
408
+
315
409
  ## Limitations
316
410
 
317
411
  - **Cold sends need a resolvable line.** The adapter rebuilds a thread — DM or group — from its chat GUID via spectrum-ts's `space.get`, so it can send, react, edit, and show typing even into a thread it hasn't seen this session, including a [webhook](#webhooks) delivery. With **multiple iMessage lines** configured, spectrum-ts cannot infer which line an unseen chat belongs to, so cold sends there throw `NotImplementedError` — respond within a received message's thread instead.
318
- - **No message history.** `fetchMessages` is not supported — spectrum-ts exposes no paginated history API.
412
+ - **No message history.** `fetchMessages` is not supported — spectrum-ts exposes no paginated history API. Single messages resolve via `fetchMessage` (from the session cache or spectrum-ts's by-id lookup).
319
413
  - **No thread/chat info.** `fetchThread` is not supported.
320
- - **No reaction removal.** `removeReaction` is not supported.
321
- - **Local mode** supports sending (including cold sends by chat GUID) and receiving, but not reactions, typing, editing, modals, history, or thread info.
322
- - **Formatting.** iMessage is plain-text only; Markdown formatting is stripped when sending, preserving the text content.
414
+ - **Session-scoped delete & reaction removal.** `deleteMessage` unsends a message resolved from this session (subject to iMessage's ~2-minute unsend window); `removeReaction` retracts a tapback only if it was added via `addReaction` earlier in this session — spectrum-ts exposes no by-target reaction lookup.
415
+ - **Local mode** supports sending (including cold sends by chat GUID), receiving, and opening DMs, but not reactions, typing, editing, deleting, marking read, effects, modals, history, or thread info.
416
+ - **Formatting.** Markdown-typed content (`{ markdown }` or `{ ast }`) renders as native iMessage styled text on remote bold, italics, links, and lists — via spectrum-ts's `markdown()` builder. Plain strings and `{ raw }` are sent as-is (never reinterpreted as Markdown). Inbound messages always surface as plain text.
323
417
  - **Platform.** Local mode requires macOS. Cloud and self-host run anywhere.
324
418
  - **Cards.** iMessage has no structured card layouts.
325
419
 
@@ -330,7 +424,8 @@ This version re-platforms the adapter onto **spectrum-ts**. If you are upgrading
330
424
  - **Dependency** — replaces `@photon-ai/imessage-kit` + `@photon-ai/advanced-imessage-kit` with `spectrum-ts`.
331
425
  - **`IMESSAGE_SERVER_URL` is now a gRPC `host:port`** (self-host), not an `https://` / Socket.IO URL.
332
426
  - **New cloud path** — set `IMESSAGE_PROJECT_ID` + `IMESSAGE_PROJECT_SECRET` for Spectrum Cloud.
333
- - **Removed capabilities** (now `NotImplementedError`): `fetchMessages`, `fetchThread`, and `removeReaction`. Local `fetchMessages` (previously supported) is also removed. Cold `postMessage` works for DMs and groups alike (rebuilt from the chat GUID — see [Limitations](#limitations)).
427
+ - **Unsupported capabilities** (throw `NotImplementedError`): `fetchMessages` and `fetchThread` — spectrum-ts exposes no paginated history or chat-info API. Local `fetchMessages` (previously supported) is also removed. Cold `postMessage` works for DMs and groups alike (rebuilt from the chat GUID — see [Limitations](#limitations)).
428
+ - **Newly supported on spectrum-ts v8**: `deleteMessage` (unsend), `removeReaction` (retract a session-added tapback), `openDM` (cold-start a DM from a handle), `fetchMessage` (single message by id), and `markRead` — all remote-only where noted in [Features](#features).
334
429
  - **`adapter.sdk` → `adapter.app`** — the adapter now exposes the underlying `SpectrumInstance` as `adapter.app` (null until `initialize()`).
335
430
 
336
431
  ## Troubleshooting
@@ -349,10 +444,14 @@ This version re-platforms the adapter onto **spectrum-ts**. If you are upgrading
349
444
  - Verify **Full Disk Access** is granted to your terminal or application.
350
445
  - Check that iMessage is signed in and working.
351
446
 
352
- ### `NotImplementedError` from `fetchMessages` / `fetchThread` / `removeReaction`
447
+ ### `NotImplementedError` from `fetchMessages` / `fetchThread`
353
448
 
354
449
  - These are not supported by spectrum-ts. See [Limitations](#limitations).
355
450
 
451
+ ### `NotImplementedError` from `deleteMessage` / `removeReaction` / `markRead`
452
+
453
+ - These are remote-only and session-scoped. `deleteMessage` and `markRead` need the target message to have been seen this session (and delete is bound by iMessage's ~2-minute unsend window); `removeReaction` needs the tapback to have been added via `addReaction` this session. In local mode they throw. See [Limitations](#limitations).
454
+
356
455
  ## License
357
456
 
358
457
  MIT
package/dist/index.d.ts CHANGED
@@ -1,10 +1,54 @@
1
- import { Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage, RawMessage, Message, FetchOptions, FetchResult, ThreadInfo, EmojiValue, ModalElement, FormattedContent, BaseFormatConverter, Root } from 'chat';
2
- import { SpectrumInstance } from 'spectrum-ts';
1
+ import { FileUpload, Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage, RawMessage, Message, FetchOptions, FetchResult, ThreadInfo, EmojiValue, ModalElement, FormattedContent, BaseFormatConverter, Root } from 'chat';
2
+ import { ContentBuilder, AppUrl, SpectrumInstance } from 'spectrum-ts';
3
+ export { AppUrl } from 'spectrum-ts';
4
+ import { IMessageMessageEffect, CustomizedMiniAppInput } from 'spectrum-ts/providers/imessage';
5
+ export { CustomizedMiniAppInput, IMessageMessageEffect } from 'spectrum-ts/providers/imessage';
6
+
7
+ /**
8
+ * Image bytes for a chat background. Accepts raw bytes (`Uint8Array` / `Buffer`
9
+ * / `ArrayBuffer`), a `Blob`, or a Chat SDK `FileUpload` — whatever your image
10
+ * pipeline hands back gets normalized to the bytes spectrum-ts expects.
11
+ */
12
+ type BackgroundBytes = Uint8Array | ArrayBuffer | Blob | FileUpload;
13
+ /**
14
+ * A chat-background source. Either:
15
+ *
16
+ * - the literal `"clear"` sentinel, to remove the current background;
17
+ * - in-memory {@link BackgroundBytes};
18
+ * - an `http(s)` URL (a `URL` or a string) that spectrum-ts fetches at send
19
+ * time. Local file paths aren't accepted — read the file into bytes and pass
20
+ * those instead.
21
+ */
22
+ type BackgroundInput = "clear" | BackgroundBytes | URL | string;
23
+ /** Optional chat-background metadata. */
24
+ interface BackgroundOptions {
25
+ /**
26
+ * MIME type of the image (`image/*`). Required for raw bytes when the `name`
27
+ * carries no image extension; inferred from the URL / name otherwise.
28
+ */
29
+ mimeType?: string;
30
+ /** File name used to infer the MIME type from its extension. */
31
+ name?: string;
32
+ }
33
+ /**
34
+ * Validate and normalize a chat-background source into the spectrum-ts
35
+ * `background()` content builder. The `"clear"` sentinel removes the current
36
+ * background; URL sources are fetched by spectrum-ts at send time; byte sources
37
+ * are copied to a detached `Buffer` and tagged with a resolved `image/*` MIME
38
+ * type. The builder is a fire-and-forget control signal — remote and
39
+ * iMessage-only.
40
+ */
41
+ declare function resolveBackground(input: BackgroundInput, options?: BackgroundOptions): Promise<ContentBuilder>;
3
42
 
4
43
  /** Thread ID components for iMessage */
5
44
  interface iMessageThreadId {
6
45
  /** Chat GUID (e.g., "iMessage;-;+1234567890") */
7
46
  chatGuid: string;
47
+ /**
48
+ * Sending line, when known — encoded in the thread ID so it survives a
49
+ * cold-cache reply invocation where the Space must be rebuilt.
50
+ */
51
+ phone?: string;
8
52
  }
9
53
  /**
10
54
  * Explicit self-hosted iMessage client entry, passed straight through to
@@ -69,6 +113,136 @@ interface CreateiMessageAdapterOptions {
69
113
  */
70
114
  declare function deriveAddress(serverUrl: string): string;
71
115
 
116
+ /**
117
+ * iMessage expressive-send effects, keyed by friendly name. Bubble effects
118
+ * (`slam`, `loud`, `gentle`, `invisible`) animate the message bubble; the rest
119
+ * are full-screen effects (`confetti`, `fireworks`, `balloons`, `heart`,
120
+ * `lasers`, `celebration`, `sparkles`, `spotlight`, `echo`).
121
+ *
122
+ * Re-exported from spectrum-ts so callers can reference effects by name without
123
+ * reaching into the provider package — e.g. `iMessageEffect.confetti`.
124
+ */
125
+ declare const iMessageEffect: {
126
+ readonly slam: "com.apple.MobileSMS.expressivesend.impact";
127
+ readonly loud: "com.apple.MobileSMS.expressivesend.loud";
128
+ readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
129
+ readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
130
+ readonly confetti: "com.apple.messages.effect.CKConfettiEffect";
131
+ readonly fireworks: "com.apple.messages.effect.CKFireworksEffect";
132
+ readonly balloons: "com.apple.messages.effect.CKBalloonEffect";
133
+ readonly heart: "com.apple.messages.effect.CKHeartEffect";
134
+ readonly lasers: "com.apple.messages.effect.CKLasersEffect";
135
+ readonly celebration: "com.apple.messages.effect.CKHappyBirthdayEffect";
136
+ readonly sparkles: "com.apple.messages.effect.CKSparklesEffect";
137
+ readonly spotlight: "com.apple.messages.effect.CKSpotlightEffect";
138
+ readonly echo: "com.apple.messages.effect.CKEchoEffect";
139
+ };
140
+ /** Accepted effect names (`"confetti"`, `"fireworks"`, …). */
141
+ type iMessageEffectName = keyof typeof iMessageEffect;
142
+ /**
143
+ * Resolve an effect argument to a spectrum-ts effect id. Accepts either a
144
+ * friendly name (`"confetti"`) or the raw effect id
145
+ * (`iMessageEffect.confetti`), so both styles work. Throws a `ValidationError`
146
+ * on an unknown effect.
147
+ */
148
+ declare function resolveEffect(effect: IMessageMessageEffect | iMessageEffectName): IMessageMessageEffect;
149
+
150
+ /**
151
+ * Image bytes for a mini-app card's inline image. Accepts raw bytes
152
+ * (`Uint8Array` / `Buffer` / `ArrayBuffer`), a `Blob`, or a Chat SDK
153
+ * `FileUpload` — whatever you already have on hand gets converted to the
154
+ * `Uint8Array` spectrum-ts expects.
155
+ */
156
+ type MiniAppImage = Uint8Array | ArrayBuffer | Blob | FileUpload;
157
+ /**
158
+ * Layout of a mini-app card — everything the recipient sees in the bubble.
159
+ * Every field is optional; supply the ones that make sense for your card. The
160
+ * `image` renders as the card's artwork, with `imageTitle` / `imageSubtitle`
161
+ * overlaid; the caption fields fill the text rows, and `summary` is the
162
+ * fallback text shown where the mini-app can't render.
163
+ */
164
+ interface MiniAppCardLayout {
165
+ caption?: string;
166
+ image?: MiniAppImage;
167
+ imageSubtitle?: string;
168
+ imageTitle?: string;
169
+ subcaption?: string;
170
+ summary?: string;
171
+ trailingCaption?: string;
172
+ trailingSubcaption?: string;
173
+ }
174
+ /**
175
+ * A native iMessage mini-app card (an `MSMessageExtension` balloon). The
176
+ * `appName`, `teamId`, and `extensionBundleId` identify the iMessage extension
177
+ * that opens when the recipient taps the card, receiving `url`; `appStoreId`
178
+ * optionally points recipients without the extension installed at its App
179
+ * Store entry.
180
+ */
181
+ interface MiniAppCard {
182
+ /** Display name of the iMessage extension. */
183
+ appName: string;
184
+ /** Optional App Store id, for recipients without the extension installed. */
185
+ appStoreId?: number;
186
+ /** Bundle id of the `MSMessageExtension` that receives the tap. */
187
+ extensionBundleId: string;
188
+ /** What the recipient sees in the bubble. */
189
+ layout?: MiniAppCardLayout;
190
+ /** Apple Developer team id that signs the extension. */
191
+ teamId: string;
192
+ /** URL handed to the extension when the card is tapped. */
193
+ url: string | URL;
194
+ }
195
+ /**
196
+ * Validate and normalize a {@link MiniAppCard} into the
197
+ * {@link CustomizedMiniAppInput} spectrum-ts's `customizedMiniApp()` builder
198
+ * expects: required identifiers are checked non-empty, `url` is normalized to a
199
+ * validated string, and the layout image (if any) is decoded to bytes.
200
+ */
201
+ declare function resolveMiniApp(card: MiniAppCard): Promise<CustomizedMiniAppInput>;
202
+ /**
203
+ * Distinguish the lightweight `app(url)` form from a fully-specified
204
+ * {@link MiniAppCard}. An {@link AppUrl} is a string, a `Promise<string>`, or a
205
+ * thunk (`() => string | Promise<string>`) — the mini-app card is a plain
206
+ * object with named fields, so it never matches any of those shapes.
207
+ */
208
+ declare function isAppUrl(input: MiniAppCard | AppUrl): input is AppUrl;
209
+
210
+ /**
211
+ * Audio bytes for a voice message. Accepts raw bytes (`Uint8Array` / `Buffer` /
212
+ * `ArrayBuffer`), a `Blob`, or a Chat SDK `FileUpload` — whatever your TTS
213
+ * pipeline hands back gets normalized to the bytes spectrum-ts expects.
214
+ */
215
+ type VoiceBytes = Uint8Array | ArrayBuffer | Blob | FileUpload;
216
+ /**
217
+ * A voice message source: in-memory {@link VoiceBytes}, or an `http(s)` URL (a
218
+ * `URL` or a string) that spectrum-ts fetches at send time. Local file paths
219
+ * aren't accepted — read the file into bytes and pass those instead.
220
+ */
221
+ type VoiceInput = VoiceBytes | URL | string;
222
+ /** Optional voice-message metadata. */
223
+ interface VoiceOptions {
224
+ /**
225
+ * Playback duration in seconds, surfaced on the waveform bubble. Optional —
226
+ * iMessage derives the waveform from the audio itself when omitted.
227
+ */
228
+ duration?: number;
229
+ /**
230
+ * MIME type of the audio (`audio/*`). Required for raw bytes when the `name`
231
+ * carries no audio extension; inferred from the URL / name otherwise.
232
+ */
233
+ mimeType?: string;
234
+ /** File name handed to spectrum-ts (also used to infer the MIME type). */
235
+ name?: string;
236
+ }
237
+ /**
238
+ * Validate and normalize a voice source into the spectrum-ts `voice()` content
239
+ * builder. URL sources are fetched by spectrum-ts at send time; byte sources
240
+ * are copied to a detached `Buffer` and tagged with a resolved `audio/*` MIME
241
+ * type. The builder is sent as a native iMessage voice note (a waveform
242
+ * bubble), not an audio-file attachment.
243
+ */
244
+ declare function resolveVoice(input: VoiceInput, options?: VoiceOptions): Promise<ContentBuilder>;
245
+
72
246
  declare class iMessageAdapter implements Adapter {
73
247
  readonly name = "imessage";
74
248
  readonly userName: string;
@@ -80,8 +254,10 @@ declare class iMessageAdapter implements Adapter {
80
254
  readonly clients?: IMessageClientEntry[];
81
255
  readonly phone?: string;
82
256
  readonly webhookSecret?: string;
83
- /** The spectrum-ts instance — null until `initialize()` runs. */
257
+ /** The spectrum-ts instance — null until `initialize()` or `ensureApp()` runs. */
84
258
  app: SpectrumInstance | null;
259
+ /** In-flight app build, so concurrent callers share one construction. */
260
+ private appBuild;
85
261
  private chat;
86
262
  private readonly logger;
87
263
  private readonly formatConverter;
@@ -91,6 +267,13 @@ declare class iMessageAdapter implements Adapter {
91
267
  private pump;
92
268
  constructor(config: iMessageAdapterConfig);
93
269
  initialize(chat: ChatInstance): Promise<void>;
270
+ /**
271
+ * Build the spectrum-ts app on demand. eve may call the adapter in an
272
+ * invocation that never ran `initialize()` (e.g. a Vercel Workflow reply
273
+ * callback), leaving `this.app` null — every send funnels through here first.
274
+ */
275
+ private ensureApp;
276
+ private buildApp;
94
277
  /**
95
278
  * Handle a Spectrum Cloud webhook delivery (signed JSON `messages` event).
96
279
  *
@@ -102,16 +285,105 @@ declare class iMessageAdapter implements Adapter {
102
285
  * @see https://photon.codes/docs/webhooks/overview
103
286
  */
104
287
  handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
288
+ /**
289
+ * Build the spectrum content for an outbound message. Markdown-typed inputs
290
+ * are sent via `markdown()` so remote iMessage renders them as native styled
291
+ * text; raw/string/card inputs stay plain `text()`. Returns the rendered
292
+ * `body` too so callers can skip an empty send.
293
+ */
294
+ private toSpectrumContent;
105
295
  postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage>;
296
+ /**
297
+ * Send a message with an iMessage expressive-send effect — a bubble effect
298
+ * (`slam`, `loud`, `gentle`, `invisible`) or a full-screen effect
299
+ * (`confetti`, `fireworks`, `balloons`, `heart`, `lasers`, `celebration`,
300
+ * `sparkles`, `spotlight`, `echo`). Not part of the Chat SDK `Adapter`
301
+ * interface — exposed as an adapter-specific extra (e.g. celebratory confetti
302
+ * on task completion). Remote only.
303
+ *
304
+ * The `effect` argument accepts a friendly name (`"confetti"`) or a value from
305
+ * the re-exported `iMessageEffect` map. Effects attach to text content only,
306
+ * so this requires non-empty text.
307
+ */
308
+ sendEffect(threadId: string, message: AdapterPostableMessage, effect: IMessageMessageEffect | iMessageEffectName): Promise<RawMessage>;
309
+ /**
310
+ * Send an iMessage mini-app card — an `MSMessageExtension` balloon, the
311
+ * closest iMessage gets to a rich card (à la Slack Block Kit) instead of a
312
+ * bare link. Not part of the Chat SDK `Adapter` interface — exposed as an
313
+ * adapter-specific extra. Remote only.
314
+ *
315
+ * Two forms:
316
+ *
317
+ * - **Just a URL** — pass a string (or a `Promise`/thunk resolving to one, so
318
+ * the link can be minted at send time). This is the lightweight `app(url)`
319
+ * card: the URL is rendered as a mini-app with no extension identifiers
320
+ * required.
321
+ * - **A full {@link MiniAppCard}** — pass an object to control the bubble's
322
+ * image, captions, and the exact iMessage extension that opens on tap. Its
323
+ * `appName`, `teamId`, and `extensionBundleId` identify that extension.
324
+ */
325
+ sendMiniApp(threadId: string, url: AppUrl): Promise<RawMessage>;
326
+ sendMiniApp(threadId: string, card: MiniAppCard): Promise<RawMessage>;
327
+ /**
328
+ * Send a native iMessage voice note — a real, playable waveform bubble (the
329
+ * message renders with `isAudioMessage`), not an audio file dropped in as an
330
+ * attachment. A natural fit for TTS-capable bots that reply with speech. Not
331
+ * part of the Chat SDK `Adapter` interface — exposed as an adapter-specific
332
+ * extra. Remote only.
333
+ *
334
+ * The `input` is either in-memory audio bytes (`Uint8Array` / `Buffer` /
335
+ * `ArrayBuffer`, a `Blob`, or a Chat SDK `FileUpload`) or an `http(s)` URL (a
336
+ * `URL` or a string) that is fetched at send time. Audio bytes need an
337
+ * `audio/*` MIME type — supply `options.mimeType` (e.g. `"audio/mp4"`) or an
338
+ * `options.name` with an audio extension when it can't be inferred.
339
+ */
340
+ sendVoice(threadId: string, input: VoiceInput, options?: VoiceOptions): Promise<RawMessage>;
341
+ /**
342
+ * Set or clear the chat background — the wallpaper behind a conversation, an
343
+ * iMessage-only touch with no analog on the plain-text competitors. Not part
344
+ * of the Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
345
+ * Remote only.
346
+ *
347
+ * The `input` is either the literal `"clear"` (to remove the current
348
+ * background), in-memory image bytes (`Uint8Array` / `Buffer` / `ArrayBuffer`,
349
+ * a `Blob`, or a Chat SDK `FileUpload`), or an `http(s)` URL (a `URL` or a
350
+ * string) that is fetched at send time. Image bytes need an `image/*` MIME
351
+ * type — supply `options.mimeType` (e.g. `"image/jpeg"`) or an `options.name`
352
+ * with an image extension when it can't be inferred.
353
+ *
354
+ * Fire-and-forget: iMessage acknowledges the control signal without returning
355
+ * a message, so this resolves to `void` rather than a {@link RawMessage}.
356
+ */
357
+ setBackground(threadId: string, input: BackgroundInput, options?: BackgroundOptions): Promise<void>;
106
358
  editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage>;
107
- deleteMessage(_threadId: string, _messageId: string): Promise<void>;
359
+ deleteMessage(threadId: string, messageId: string): Promise<void>;
108
360
  parseMessage(raw: unknown): Message;
361
+ /**
362
+ * Fetch a single message by id. spectrum-ts can resolve a message by id
363
+ * (from the inbound cache or the provider's by-id lookup) even though it has
364
+ * no paginated history API, so single-message reads work where
365
+ * `fetchMessages` cannot. Returns `null` when the message can't be resolved.
366
+ */
367
+ fetchMessage(threadId: string, messageId: string): Promise<Message | null>;
109
368
  fetchMessages(_threadId: string, _options?: FetchOptions): Promise<FetchResult>;
110
369
  fetchThread(_threadId: string): Promise<ThreadInfo>;
111
370
  channelIdFromThreadId(threadId: string): string;
112
371
  addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
113
- removeReaction(_threadId: string, _messageId: string, _emoji: EmojiValue | string): Promise<void>;
372
+ removeReaction(_threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
114
373
  startTyping(threadId: string, _status?: string): Promise<void>;
374
+ /**
375
+ * Cold-start a DM with a phone number / handle. spectrum-ts resolves (or
376
+ * creates) the 1:1 conversation from the participant via `space.create`, so
377
+ * the bot can message a user it has never received from. Returns the encoded
378
+ * thread id, ready to pass to `postMessage`.
379
+ */
380
+ openDM(userId: string): Promise<string>;
381
+ /**
382
+ * Mark a received message (and the conversation up to it) as read, surfacing
383
+ * a read receipt where iMessage supports one. Remote only; not part of the
384
+ * Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
385
+ */
386
+ markRead(threadId: string, messageId: string): Promise<void>;
115
387
  openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
116
388
  viewId: string;
117
389
  }>;
@@ -124,18 +396,18 @@ declare class iMessageAdapter implements Adapter {
124
396
  private routeInbound;
125
397
  private handlePollOption;
126
398
  /**
127
- * Resolve a sendable spectrum-ts `Space` for a thread.
128
- *
129
- * Prefers a live `Space` cached from the inbound stream (correct sending
130
- * line, no extra round-trip). On a miss — e.g. a webhook-only deployment, or
131
- * a cold send — it rebuilds the Space from the chat GUID via
132
- * `imessage(app).space.get(chatGuid)`, which works for DMs and groups alike.
133
- * The rebuild can still fail when several iMessage lines are configured and
134
- * spectrum-ts cannot infer which line the chat belongs to (`space.get`
135
- * requires `params.phone` there). Returns `undefined` when no Space can be
136
- * obtained.
399
+ * Resolve a sendable spectrum-ts `Space` for a thread. Prefers a cached live
400
+ * Space; otherwise rebuilds it from the chat GUID, passing the sending line
401
+ * so `space.get` can pick it when multiple lines are configured. Returns
402
+ * `undefined` when no Space can be obtained.
137
403
  */
138
404
  private resolveSpace;
405
+ /**
406
+ * The iMessage provider's Space namespace (`get` / `create`). `HasProvider`
407
+ * over the default provider tuple won't narrow to `true`, so `imessage(app)`
408
+ * types as `never` — cast to the slice of the instance we use.
409
+ */
410
+ private platformSpaces;
139
411
  private requireSpace;
140
412
  private resolveMessage;
141
413
  }
@@ -154,11 +426,24 @@ declare function createiMessageAdapter(config?: CreateiMessageAdapterOptions): i
154
426
  /**
155
427
  * iMessage format conversion using AST-based parsing.
156
428
  *
157
- * iMessage supports plain text only -- no rich formatting syntax.
158
- * The converter strips formatting markers and outputs clean plain text,
159
- * preserving structure (lists, blockquotes, code blocks) with whitespace.
429
+ * Remote iMessage (via spectrum-ts) renders CommonMark natively as styled text
430
+ * (bold/italic/links etc. via UTF-16 formatting ranges), so markdown-typed
431
+ * outbound content is sent through spectrum's `markdown()` builder verbatim --
432
+ * see `renderPostableContent`. The plain-text rendering here (`fromAst`) is
433
+ * still used for inbound parsing, `renderFormatted`, and the raw/card fallback
434
+ * paths: it strips formatting markers and preserves structure (lists,
435
+ * blockquotes, code blocks) with whitespace.
160
436
  */
161
437
 
438
+ /**
439
+ * The spectrum content a postable message should be sent as: the body string
440
+ * plus whether spectrum should render it as native markdown (styled text) or
441
+ * pass it through as plain text.
442
+ */
443
+ interface PostableContent {
444
+ body: string;
445
+ markdown: boolean;
446
+ }
162
447
  declare class iMessageFormatConverter extends BaseFormatConverter {
163
448
  /**
164
449
  * Render an AST to iMessage plain text format.
@@ -170,7 +455,21 @@ declare class iMessageFormatConverter extends BaseFormatConverter {
170
455
  * iMessage sends plain text, so we just parse it as markdown.
171
456
  */
172
457
  toAst(text: string): Root;
458
+ /**
459
+ * Decide how a postable message should reach spectrum-ts.
460
+ *
461
+ * Markdown-typed inputs -- `{ markdown }` and `{ ast }` -- carry CommonMark
462
+ * the caller wants styled, so their source is preserved verbatim and flagged
463
+ * `markdown: true`; the adapter sends it via spectrum's `markdown()` builder,
464
+ * which renders bold/italic/links/lists as native iMessage styled text.
465
+ *
466
+ * Everything else is pass-through-as-is by contract -- a plain `string` or
467
+ * `{ raw }` must not have stray `*`/`_` reinterpreted as formatting, and
468
+ * cards fall back to plain text -- so those are rendered to plain text and
469
+ * flagged `markdown: false`.
470
+ */
471
+ renderPostableContent(message: AdapterPostableMessage): PostableContent;
173
472
  private nodeToPlainText;
174
473
  }
175
474
 
176
- export { type CreateiMessageAdapterOptions, type IMessageClientEntry, createiMessageAdapter, deriveAddress, iMessageAdapter, type iMessageAdapterConfig, type iMessageAdapterLocalConfig, type iMessageAdapterRemoteConfig, iMessageFormatConverter, type iMessageThreadId };
475
+ export { type BackgroundBytes, type BackgroundInput, type BackgroundOptions, type CreateiMessageAdapterOptions, type IMessageClientEntry, type MiniAppCard, type MiniAppCardLayout, type MiniAppImage, type VoiceBytes, type VoiceInput, type VoiceOptions, createiMessageAdapter, deriveAddress, iMessageAdapter, type iMessageAdapterConfig, type iMessageAdapterLocalConfig, type iMessageAdapterRemoteConfig, iMessageEffect, type iMessageEffectName, iMessageFormatConverter, type iMessageThreadId, isAppUrl, resolveBackground, resolveEffect, resolveMiniApp, resolveVoice };