diff --git a/README.md b/README.md index 5e9faa3d..7a9ab3c3 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ See the [Getting Started guide](https://chat-sdk.dev/docs/getting-started) for a | Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Post+Edit | Yes | | GitHub | `@chat-adapter/github` | Yes | Yes | No | No | No | No | | Linear | `@chat-adapter/linear` | Yes | Yes | No | No | No | No | +| iMessage | `@chat-adapter/imessage` | DM-only | Remote-only | No | No | No | Yes | ## Features @@ -84,6 +85,7 @@ See the [Getting Started guide](https://chat-sdk.dev/docs/getting-started) for a | `@chat-adapter/linear` | [Linear adapter](https://chat-sdk.dev/docs/adapters/linear) | | `@chat-adapter/state-redis` | [Redis state adapter](https://chat-sdk.dev/docs/state/redis) (production) | | `@chat-adapter/state-ioredis` | [ioredis state adapter](https://chat-sdk.dev/docs/state/ioredis) (alternative) | +| `@chat-adapter/imessage` | [iMessage adapter](https://chat-sdk.dev/docs/adapters/imessage) | | `@chat-adapter/state-memory` | [In-memory state adapter](https://chat-sdk.dev/docs/state/memory) (development) | ## AI coding agent support diff --git a/apps/docs/content/docs/adapters/imessage.mdx b/apps/docs/content/docs/adapters/imessage.mdx new file mode 100644 index 00000000..274945f5 --- /dev/null +++ b/apps/docs/content/docs/adapters/imessage.mdx @@ -0,0 +1,241 @@ +--- +title: iMessage +description: Configure the iMessage adapter with local (on-device) or remote (server-based) integration. +type: integration +prerequisites: + - /docs/getting-started +--- + +## Installation + +```sh title="Terminal" +pnpm add @chat-adapter/imessage +``` + +## Usage + +The adapter supports two modes: **local** (running directly on a Mac with iMessage) and **remote** (connecting to a [Photon](https://photon.codes) iMessage server). The mode is auto-detected from the `IMESSAGE_LOCAL` environment variable. + +### Remote mode + +Recommended for production. Connects to [Photon](https://photon.codes)'s managed iMessage service over HTTP and Socket.IO, so your bot can run on any platform. + +```typescript title="lib/bot.ts" lineNumbers +import { Chat } from "chat"; +import { createiMessageAdapter } from "@chat-adapter/imessage"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + imessage: createiMessageAdapter({ + local: false, + }), + }, +}); + +bot.onNewMention(async (thread, message) => { + await thread.post("Hello from iMessage!"); +}); +``` + +### Local mode + +For development or self-hosted deployments using [imessage-kit](https://github.com/photon-hq/imessage-kit). Reads from the local iMessage database and sends via AppleScript. Must run on macOS with **Full Disk Access** granted. + +```typescript title="lib/bot.ts" lineNumbers +import { Chat } from "chat"; +import { createiMessageAdapter } from "@chat-adapter/imessage"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + imessage: createiMessageAdapter({ + local: true, + }), + }, +}); + +bot.onNewMention(async (thread, message) => { + await thread.post("Hello from iMessage!"); +}); +``` + +## Setup + +### Remote mode + +Remote mode connects to a [Photon](https://photon.codes) iMessage server, which handles the macOS-side integration on your behalf. You'll need an active Photon subscription to get your server credentials. + +1. [Request access](https://photon.codes) from Photon to get your server credentials +2. Copy your **server URL** and **API key** from the Photon dashboard +3. Set `IMESSAGE_SERVER_URL` and `IMESSAGE_API_KEY` environment variables +4. Set `IMESSAGE_LOCAL=false` + +### Local mode + +Local mode requires the adapter to run directly on a macOS machine with iMessage. It uses `@photon-ai/imessage-kit` to read from the local `chat.db` database and send messages via AppleScript. + +1. Grant **Full Disk Access** to your terminal or application in **System Settings → Privacy & Security → Full Disk Access** +2. Ensure iMessage is signed in and working on the Mac +3. No additional environment variables are required — local mode is the default + +## Receiving messages + +Call `startGatewayListener()` to listen for new messages in real-time. In remote mode, this uses Socket.IO push events. In local mode, it polls the iMessage database. + +- In serverless environments, use a cron job to maintain the connection + +## Gateway setup for serverless + +### 1. Create Gateway route + +```typescript title="app/api/imessage/gateway/route.ts" +import { after } from "next/server"; +import { bot } from "@/lib/bot"; + +export const maxDuration = 800; + +export async function GET(request: Request): Promise { + const cronSecret = process.env.CRON_SECRET; + if (!cronSecret) { + return new Response("CRON_SECRET not configured", { status: 500 }); + } + + const authHeader = request.headers.get("authorization"); + if (authHeader !== `Bearer ${cronSecret}`) { + return new Response("Unauthorized", { status: 401 }); + } + + const durationMs = 600 * 1000; + + return bot.adapters.imessage.startGatewayListener( + { waitUntil: (task) => after(() => task) }, + durationMs + ); +} +``` + +### 2. Configure Vercel Cron + +```json title="vercel.json" +{ + "crons": [ + { + "path": "/api/imessage/gateway", + "schedule": "*/9 * * * *" + } + ] +} +``` + +This runs every 9 minutes, ensuring overlap with the 10-minute listener duration. + +### 3. Add environment variables + +Add `CRON_SECRET` to your Vercel project settings. + +## Configuration + +| Option | Required | Description | +|--------|----------|-------------| +| `local` | No | `true` for local mode, `false` for remote. Auto-detected from `IMESSAGE_LOCAL` (default: `true`) | +| `serverUrl` | Remote only | URL of the remote iMessage server. Auto-detected from `IMESSAGE_SERVER_URL` | +| `apiKey` | Remote only | API key for remote server authentication. Auto-detected from `IMESSAGE_API_KEY` | +| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) | + +## Environment variables + +```bash title=".env.local" +IMESSAGE_LOCAL=false # Set to "false" for remote mode (default: true) +IMESSAGE_SERVER_URL=https://... # Required for remote mode +IMESSAGE_API_KEY=... # Required for remote mode +``` + +## Features + +| Feature | Supported | +|---------|-----------| +| Mentions | DMs only | +| Reactions (add/remove) | Remote only | +| Polls | Remote only | +| Cards | No | +| Modals | No | +| Streaming | No | +| DMs | Yes | +| Ephemeral messages | No | +| File uploads | Yes | +| Typing indicator | Remote only | +| Message history | Yes | +| Message editing | Remote only | + +## Polls + +Remote mode supports native iMessage polls. Post a `Poll` to create a voting experience directly in the chat. Vote callbacks are delivered via `onAction`. + +```tsx title="lib/bot.tsx" lineNumbers +import { Poll } from "chat"; + +bot.onNewMention(async (thread, message) => { + await thread.post( + + ); +}); + +bot.onAction("fav-color", async (event) => { + await event.thread.post(`${event.user.fullName} voted for ${event.value}!`); +}); +``` + + +Polls are only available in remote mode. Attempting to post a poll in local mode will throw a `NotImplementedError`. + + +See the [Polls guide](/docs/polls) for more details. + +## Tapback reactions + +iMessage uses tapbacks instead of emoji reactions. The adapter maps standard emoji names to iMessage tapbacks: + +| Emoji | Tapback | +|-------|---------| +| `love` / `heart` | ❤️ Love | +| `like` / `thumbs_up` | 👍 Like | +| `dislike` / `thumbs_down` | 👎 Dislike | +| `laugh` | 😂 Laugh | +| `emphasize` / `exclamation` | ‼️ Emphasize | +| `question` | ❓ Question | + +## Limitations + +- **Local mode**: Only supports sending/receiving messages, message history, and file uploads. Reactions, typing indicators, message editing, and thread fetching require remote mode. +- **Formatting**: iMessage is plain-text only. Markdown formatting (bold, italic, etc.) is stripped when sending messages, preserving only the text content. +- **Platform**: Local mode requires macOS. Remote mode can run on any platform — [Photon](https://photon.codes) manages the iMessage infrastructure for you. +- **Cards and modals**: iMessage has no support for structured card layouts or interactive modals. +- **Polls**: Only supported in remote mode. Local mode does not have poll support. + +## Troubleshooting + +### "serverUrl is required" error + +- Set `IMESSAGE_SERVER_URL` or pass `serverUrl` in config when using remote mode +- This error occurs when `IMESSAGE_LOCAL=false` but no server URL is provided + +### "apiKey is required" error + +- Set `IMESSAGE_API_KEY` or pass `apiKey` in config when using remote mode + +### Local mode not receiving messages + +- Verify **Full Disk Access** is granted to your terminal or application +- Check that iMessage is signed in and working +- Messages are polled from the local database — there may be a short delay + +### Remote mode connection issues + +- Verify the server URL is correct and accessible +- Check that the API key matches your Photon iMessage service credentials +- Confirm your Photon subscription is active diff --git a/apps/docs/content/docs/adapters/index.mdx b/apps/docs/content/docs/adapters/index.mdx index e5de7913..092303b9 100644 --- a/apps/docs/content/docs/adapters/index.mdx +++ b/apps/docs/content/docs/adapters/index.mdx @@ -1,6 +1,6 @@ --- title: Overview -description: Platform-specific adapters for Slack, Teams, Google Chat, Discord, Telegram, GitHub, and Linear. +description: Platform-specific adapters for Slack, Teams, Google Chat, iMessage, Discord, Telegram, GitHub, and Linear. type: overview prerequisites: - /docs/getting-started @@ -12,48 +12,48 @@ Adapters handle webhook verification, message parsing, and API calls for each pl ### Messaging -| Feature | [Slack](/docs/adapters/slack) | [Teams](/docs/adapters/teams) | [Google Chat](/docs/adapters/gchat) | [Discord](/docs/adapters/discord) | [Telegram](/docs/adapters/telegram) | [GitHub](/docs/adapters/github) | [Linear](/docs/adapters/linear) | -|---------|-------|-------|-------------|---------|---------|--------|--------| -| Post message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Edit message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Delete message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| File uploads | ✅ | ✅ | ❌ | ✅ | ⚠️ Single file | ❌ | ❌ | -| Streaming | ✅ Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ❌ | +| Feature | [Slack](/docs/adapters/slack) | [Teams](/docs/adapters/teams) | [Google Chat](/docs/adapters/gchat) | [iMessage](/docs/adapters/imessage) | [Discord](/docs/adapters/discord) | [Telegram](/docs/adapters/telegram) | [GitHub](/docs/adapters/github) | [Linear](/docs/adapters/linear) | +|---------|-------|-------|-------------|----------|---------|----------|--------|--------| +| Post message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Edit message | ✅ | ✅ | ✅ | ⚠️ Remote only | ✅ | ✅ | ✅ | ✅ | +| Delete message | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| File uploads | ✅ | ✅ | ❌ | ✅ | ✅ | ⚠️ Single file | ❌ | ❌ | +| Streaming | ✅ Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ❌ | ### Rich content -| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | -|---------|-------|-------|-------------|---------|----------|--------|--------| -| Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | -| Buttons | ✅ | ✅ | ✅ | ✅ | ⚠️ Inline keyboard callbacks | ❌ | ❌ | -| Link buttons | ✅ | ✅ | ✅ | ✅ | ⚠️ Inline keyboard URLs | ❌ | ❌ | -| Select menus | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| Fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Images in cards | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | -| Modals | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Feature | Slack | Teams | Google Chat | iMessage | Discord | Telegram | GitHub | Linear | +|---------|-------|-------|-------------|----------|---------|----------|--------|--------| +| Card format | Block Kit | Adaptive Cards | Google Chat Cards | Plain text | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | +| Buttons | ✅ | ✅ | ✅ | ❌ | ✅ | ⚠️ Inline keyboard callbacks | ❌ | ❌ | +| Link buttons | ✅ | ✅ | ✅ | ❌ | ✅ | ⚠️ Inline keyboard URLs | ❌ | ❌ | +| Select menus | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Fields | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| Images in cards | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| Modals | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ### Conversations -| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | -|---------|-------|-------|-------------|---------|----------|--------|--------| -| Mentions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Add reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Remove reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | -| Typing indicator | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | -| DMs | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | -| Ephemeral messages | ✅ Native | ❌ | ✅ Native | ❌ | ❌ | ❌ | ❌ | +| Feature | Slack | Teams | Google Chat | iMessage | Discord | Telegram | GitHub | Linear | +|---------|-------|-------|-------------|----------|---------|----------|--------|--------| +| Mentions | ✅ | ✅ | ✅ | ⚠️ DMs only | ✅ | ✅ | ✅ | ✅ | +| Add reactions | ✅ | ❌ | ✅ | ⚠️ Remote only | ✅ | ✅ | ✅ | ✅ | +| Remove reactions | ✅ | ❌ | ✅ | ⚠️ Remote only | ✅ | ✅ | ⚠️ | ⚠️ | +| Typing indicator | ❌ | ✅ | ❌ | ⚠️ Remote only | ✅ | ✅ | ❌ | ❌ | +| DMs | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Ephemeral messages | ✅ Native | ❌ | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ | ### Message history -| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | -|---------|-------|-------|-------------|---------|----------|--------|--------| -| Fetch messages | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ✅ | -| Fetch single message | ✅ | ❌ | ❌ | ❌ | ⚠️ Cached | ❌ | ❌ | -| Fetch thread info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Fetch channel messages | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ❌ | -| List threads | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | -| Fetch channel info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | -| Post channel message | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Feature | Slack | Teams | Google Chat | iMessage | Discord | Telegram | GitHub | Linear | +|---------|-------|-------|-------------|----------|---------|----------|--------|--------| +| Fetch messages | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ✅ | +| Fetch single message | ✅ | ❌ | ❌ | ❌ | ❌ | ⚠️ Cached | ❌ | ❌ | +| Fetch thread info | ✅ | ✅ | ✅ | ⚠️ Remote only | ✅ | ✅ | ✅ | ✅ | +| Fetch channel messages | ✅ | ✅ | ✅ | ❌ | ✅ | ⚠️ Cached | ✅ | ❌ | +| List threads | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| Fetch channel info | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | +| Post channel message | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ⚠️ indicates partial support — the feature works with limitations. See individual adapter pages for details. @@ -66,6 +66,7 @@ Adapters handle webhook verification, message parsing, and API calls for each pl | [Slack](/docs/adapters/slack) | `@chat-adapter/slack` | | [Microsoft Teams](/docs/adapters/teams) | `@chat-adapter/teams` | | [Google Chat](/docs/adapters/gchat) | `@chat-adapter/gchat` | +| [iMessage](/docs/adapters/imessage) | `@chat-adapter/imessage` | | [Discord](/docs/adapters/discord) | `@chat-adapter/discord` | | [Telegram](/docs/adapters/telegram) | `@chat-adapter/telegram` | | [GitHub](/docs/adapters/github) | `@chat-adapter/github` | diff --git a/apps/docs/content/docs/adapters/meta.json b/apps/docs/content/docs/adapters/meta.json index 7767db24..35a7998d 100644 --- a/apps/docs/content/docs/adapters/meta.json +++ b/apps/docs/content/docs/adapters/meta.json @@ -5,6 +5,7 @@ "slack", "teams", "gchat", + "imessage", "discord", "telegram", "github", diff --git a/apps/docs/content/docs/api/meta.json b/apps/docs/content/docs/api/meta.json index eb58cd45..44698134 100644 --- a/apps/docs/content/docs/api/meta.json +++ b/apps/docs/content/docs/api/meta.json @@ -8,6 +8,7 @@ "message", "postable-message", "cards", + "polls", "markdown", "modals" ] diff --git a/apps/docs/content/docs/api/polls.mdx b/apps/docs/content/docs/api/polls.mdx new file mode 100644 index 00000000..3385c626 --- /dev/null +++ b/apps/docs/content/docs/api/polls.mdx @@ -0,0 +1,98 @@ +--- +title: Polls +description: Poll components for cross-platform native voting. +type: reference +--- + +Poll components render as native polls on supported platforms — iMessage polls via [advanced-imessage-kit](https://github.com/photon-hq/advanced-imessage-kit). + +```typescript +import { Poll } from "chat"; +``` + +Supports both function-call and JSX syntax. Function-call syntax is recommended for better type inference. + +## Poll + +Creates a poll element that can be posted to a thread. + +```typescript +Poll({ + id: "fav-color", + question: "What is your favorite color?", + options: ["Red", "Blue", "Green"], +}) +``` + + + +## PollElement + +The object returned by `Poll()`. Can be passed directly to `thread.post()`. + +```typescript +interface PollElement { + type: "poll"; + id: string; + question: string; + options: string[]; +} +``` + +## PostablePoll + +Wrapper type for posting a poll with explicit typing: + +```typescript +await thread.post({ poll: Poll({ id: "q1", question: "Yes or no?", options: ["Yes", "No"] }) }); +``` + + + +## Vote callbacks + +When a user votes on a poll, the adapter fires an action event routed through `onAction`. The `actionId` matches the poll's `id`, and `value` contains the option text. + +```typescript +bot.onAction("fav-color", async (event) => { + // event.actionId === "fav-color" + // event.value === "Red" (the option the user voted for) + await event.thread.post(`${event.user.fullName} voted for ${event.value}!`); +}); +``` + +See [Actions](/docs/actions) for more on action handlers. + +## isPollElement + +Type guard to check if a value is a `PollElement`. + +```typescript +import { isPollElement } from "chat"; + +if (isPollElement(message)) { + console.log(message.question, message.options); +} +``` diff --git a/apps/docs/content/docs/api/postable-message.mdx b/apps/docs/content/docs/api/postable-message.mdx index 6066ca48..45da345f 100644 --- a/apps/docs/content/docs/api/postable-message.mdx +++ b/apps/docs/content/docs/api/postable-message.mdx @@ -133,6 +133,33 @@ await thread.post({ }} /> +## PostablePoll + +Native poll with voting options. See [Polls](/docs/api/polls) for the full API. + +```typescript +import { Poll } from "chat"; + +await thread.post(Poll({ id: "fav-color", question: "Favorite color?", options: ["Red", "Blue", "Green"] })); +``` + +You can also pass a poll with explicit typing: + +```typescript +await thread.post({ + poll: Poll({ id: "fav-color", question: "Favorite color?", options: ["Red", "Blue", "Green"] }), +}); +``` + + + ## AsyncIterable (streaming) An async iterable of strings, like the AI SDK's `textStream`. The SDK streams the message in real time using platform-native APIs where available. diff --git a/apps/docs/content/docs/polls.mdx b/apps/docs/content/docs/polls.mdx new file mode 100644 index 00000000..51aa2e4e --- /dev/null +++ b/apps/docs/content/docs/polls.mdx @@ -0,0 +1,105 @@ +--- +title: Polls +description: Create native polls with voting and handle vote callbacks across platforms. +type: guide +prerequisites: + - /docs/usage +related: + - /docs/actions + - /docs/cards +--- + +Polls let you create native voting experiences that render using each platform's built-in poll UI. Vote callbacks use the same `onAction` pattern as [card buttons](/docs/actions). + +## Setup + +Configure your `tsconfig.json` to use the Chat SDK JSX runtime: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "chat" + } +} +``` + +Or use a per-file pragma: + +```tsx title="lib/bot.tsx" +/** @jsxImportSource chat */ +``` + +## Basic poll + +```tsx title="lib/bot.tsx" lineNumbers +import { Poll } from "chat"; + +bot.onNewMention(async (thread, message) => { + await thread.post( + + ); +}); +``` + +## Handle votes + +Vote callbacks use the same `onAction` pattern as [buttons](/docs/actions). The `actionId` matches the poll's `id`, and `value` contains the option text the user voted for. + +```typescript title="lib/bot.ts" lineNumbers +bot.onAction("fav-color", async (event) => { + await event.thread.post( + `${event.user.fullName} voted for ${event.value}!` + ); +}); +``` + +## Dynamic polls + +Create polls from user input: + +```tsx title="lib/bot.tsx" lineNumbers +import { Poll } from "chat"; + +bot.onNewMention(async (thread, message) => { + const text = message.text; + await thread.post( + + ); +}); +``` + +## Function-call syntax + +Polls also support function-call syntax without JSX: + +```typescript title="lib/bot.ts" lineNumbers +import { Poll } from "chat"; + +await thread.post( + Poll({ + id: "fav-color", + question: "What is your favorite color?", + options: ["Red", "Blue", "Green"], + }) +); +``` + +## Platform support + +| Platform | Native polls | Vote callbacks | +|----------|-------------|----------------| +| iMessage (remote) | Yes | Yes | +| iMessage (local) | No | No | + + +Polls are currently supported on iMessage in remote mode only. Attempting to post a poll in local mode will throw a `NotImplementedError`. + diff --git a/packages/adapter-imessage/README.md b/packages/adapter-imessage/README.md new file mode 100644 index 00000000..8b45f4c1 --- /dev/null +++ b/packages/adapter-imessage/README.md @@ -0,0 +1,41 @@ +# @chat-adapter/imessage + +[![npm version](https://img.shields.io/npm/v/@chat-adapter/imessage)](https://www.npmjs.com/package/@chat-adapter/imessage) +[![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/imessage)](https://www.npmjs.com/package/@chat-adapter/imessage) + +iMessage adapter for [Chat SDK](https://chat-sdk.dev/docs). Supports both local (on-device) and remote ([photon](https://photon.codes)-based) iMessage integration. + +## Installation + +```bash +npm install chat @chat-adapter/imessage +``` + +## Usage + +```typescript +import { Chat } from "chat"; +import { createiMessageAdapter } from "@chat-adapter/imessage"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + imessage: createiMessageAdapter({ + serverUrl: process.env.IMESSAGE_SERVER_URL!, + apiKey: process.env.IMESSAGE_API_KEY!, + }), + }, +}); + +bot.onNewMention(async (thread, message) => { + await thread.post("Hello from iMessage!"); +}); +``` + +## Documentation + +Full setup instructions, configuration reference, and features at [chat-sdk.dev/docs/adapters/imessage](https://chat-sdk.dev/docs/adapters/imessage). + +## License + +MIT diff --git a/packages/adapter-imessage/package.json b/packages/adapter-imessage/package.json new file mode 100644 index 00000000..e804b5d7 --- /dev/null +++ b/packages/adapter-imessage/package.json @@ -0,0 +1,57 @@ +{ + "name": "@chat-adapter/imessage", + "version": "4.14.0", + "description": "iMessage adapter for chat", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run --coverage", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@chat-adapter/shared": "workspace:*", + "@photon-ai/advanced-imessage-kit": "^1.14.3", + "@photon-ai/imessage-kit": "^2.1.2", + "chat": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vercel/chat.git", + "directory": "packages/adapter-imessage" + }, + "homepage": "https://github.com/vercel/chat#readme", + "bugs": { + "url": "https://github.com/vercel/chat/issues" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "chat", + "imessage", + "bot", + "adapter" + ], + "license": "MIT" +} diff --git a/packages/adapter-imessage/sample-messages.md b/packages/adapter-imessage/sample-messages.md new file mode 100644 index 00000000..d7482a1c --- /dev/null +++ b/packages/adapter-imessage/sample-messages.md @@ -0,0 +1,106 @@ +# iMessage Webhook Payload Examples + +## Forwarded Gateway Event (DM) + +POST to webhook with `x-imessage-gateway-token` header. + +```json +{ + "type": "GATEWAY_NEW_MESSAGE", + "timestamp": 1709136000000, + "data": { + "guid": "p:0/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "text": "Hello!", + "sender": "+1234567890", + "senderName": "John Doe", + "chatId": "iMessage;-;+1234567890", + "isGroupChat": false, + "isFromMe": false, + "date": "2024-02-28T12:00:00.000Z", + "attachments": [], + "source": "remote" + } +} +``` + +## Forwarded Gateway Event (Group Chat) + +```json +{ + "type": "GATEWAY_NEW_MESSAGE", + "timestamp": 1709136060000, + "data": { + "guid": "p:0/YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY", + "text": "Hey everyone", + "sender": "+1987654321", + "senderName": null, + "chatId": "iMessage;+;chat493787071395575843", + "isGroupChat": true, + "isFromMe": false, + "date": "2024-02-28T12:01:00.000Z", + "attachments": [], + "source": "remote" + } +} +``` + +## iMessage Kit (use local iMessage service) + +Sent directly by the imessage-kit SDK when `webhook` config is set. Uses the same +`x-imessage-gateway-token` header. + +```json +{ + "guid": "p:0/ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ", + "text": "Check this out", + "sender": "+1987654321", + "senderName": "Jane Smith", + "chatId": "iMessage;-;+1987654321", + "isGroupChat": false, + "isFromMe": false, + "isReaction": false, + "service": "iMessage", + "date": "2024-02-28T12:02:00.000Z", + "attachments": [ + { + "id": "att-001", + "filename": "photo.jpg", + "mimeType": "image/jpeg", + "size": 12345, + "path": "/tmp/Attachments/photo.jpg", + "isImage": true, + "createdAt": "2024-02-28T12:02:00.000Z" + } + ] +} +``` + +## Advanced iMessage Kit (use [Photon](https://photon.codes) iMessage service) + +Raw message from `AdvancedIMessageKit` `new-message` event: + +```json +{ + "guid": "p:0/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", + "text": "Hello from remote!", + "isFromMe": false, + "dateCreated": 1709136180000, + "handle": { + "address": "+1234567890" + }, + "chats": [ + { + "guid": "iMessage;-;+1234567890", + "style": 43 + } + ], + "attachments": [] +} +``` + +## Chat GUID Patterns + +- DM: `iMessage;-;+1234567890` (`;-;` = direct message) +- Group: `iMessage;+;chat493787071395575843` (`;+;` = group chat) +- SMS DM: `SMS;-;+1234567890` +- SMS Group: `SMS;+;chat987654321` diff --git a/packages/adapter-imessage/src/index.test.ts b/packages/adapter-imessage/src/index.test.ts new file mode 100644 index 00000000..b0a68796 --- /dev/null +++ b/packages/adapter-imessage/src/index.test.ts @@ -0,0 +1,1122 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const LOCAL_ID_PATTERN = /^local-\d+$/; + +const { + mockStartWatching, + mockStopWatching, + mockLocalClose, + mockSend, + mockConnect, + mockClose, + mockOnce, + mockSendMessage, + mockEditMessage, + mockGetChat, + mockSendReaction, + mockStartTyping, + mockStopTyping, + mockGatewayConnect, + mockGatewayClose, + mockGatewayOn, + mockPollCreate, + MockAdvancedIMessageKit, +} = vi.hoisted(() => { + const mockStartWatching = vi.fn(); + const mockStopWatching = vi.fn(); + const mockLocalClose = vi.fn(); + const mockSend = vi.fn(); + const mockConnect = vi.fn(); + const mockClose = vi.fn(); + const mockOn = vi.fn(); + const mockOnce = vi.fn((_event: string, cb: () => void) => cb()); + const mockSendMessage = vi.fn(); + const mockEditMessage = vi.fn(); + const mockGetChat = vi.fn(); + const mockSendReaction = vi.fn(); + const mockStartTyping = vi.fn(); + const mockStopTyping = vi.fn(); + const mockGatewayConnect = vi.fn(); + const mockGatewayClose = vi.fn(); + const mockGatewayOn = vi.fn(); + const mockPollCreate = vi.fn(); + + const MockAdvancedIMessageKit = vi.fn(() => ({ + mocked: true, + connect: mockGatewayConnect, + close: mockGatewayClose, + on: mockGatewayOn, + once: vi.fn(), + messages: {}, + chats: {}, + })); + (MockAdvancedIMessageKit as unknown as Record).getInstance = + vi.fn(() => ({ + mocked: true, + connect: mockConnect, + close: mockClose, + on: mockOn, + once: mockOnce, + messages: { + sendMessage: mockSendMessage, + editMessage: mockEditMessage, + sendReaction: mockSendReaction, + }, + chats: { + getChat: mockGetChat, + startTyping: mockStartTyping, + stopTyping: mockStopTyping, + }, + polls: { + create: mockPollCreate, + }, + })); + + return { + mockStartWatching, + mockStopWatching, + mockLocalClose, + mockSend, + mockConnect, + mockClose, + mockOn, + mockOnce, + mockSendMessage, + mockEditMessage, + mockGetChat, + mockSendReaction, + mockStartTyping, + mockStopTyping, + mockGatewayConnect, + mockGatewayClose, + mockGatewayOn, + mockPollCreate, + MockAdvancedIMessageKit, + }; +}); + +vi.mock("@photon-ai/imessage-kit", () => ({ + IMessageSDK: vi.fn(() => ({ + startWatching: mockStartWatching, + stopWatching: mockStopWatching, + close: mockLocalClose, + send: mockSend, + })), +})); + +vi.mock("@photon-ai/advanced-imessage-kit", () => ({ + AdvancedIMessageKit: MockAdvancedIMessageKit, + isPollVote: vi.fn(() => false), + parsePollVotes: vi.fn(() => null), +})); + +vi.mock("chat", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + parseMarkdown: vi.fn((text: string) => ({ + type: "root", + children: [ + { type: "paragraph", children: [{ type: "text", value: text }] }, + ], + })), + }; +}); + +import { ValidationError } from "@chat-adapter/shared"; +import { NotImplementedError, Poll } from "chat"; +import { createiMessageAdapter, iMessageAdapter } from "./index"; + +const mockLogger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + child: vi.fn(() => mockLogger), +}; + +function createMockChat() { + return { + handleIncomingMessage: vi.fn(), + }; +} + +describe("iMessageAdapter", () => { + it("should have the correct name", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(adapter.name).toBe("imessage"); + }); + + it("should store local mode config", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(adapter.local).toBe(true); + expect(adapter.serverUrl).toBeUndefined(); + expect(adapter.apiKey).toBeUndefined(); + }); + + it("should store local mode config with optional serverUrl", () => { + const adapter = new iMessageAdapter({ + local: true, + logger: mockLogger, + serverUrl: "http://localhost:1234", + }); + expect(adapter.local).toBe(true); + expect(adapter.serverUrl).toBe("http://localhost:1234"); + }); + + it("should store remote mode config", () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + expect(adapter.local).toBe(false); + expect(adapter.serverUrl).toBe("https://example.com"); + expect(adapter.apiKey).toBe("test-key"); + }); + + it("should create IMessageSDK for local mode", async () => { + const { IMessageSDK } = await import("@photon-ai/imessage-kit"); + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(IMessageSDK).toHaveBeenCalled(); + expect(adapter.sdk).toBeDefined(); + }); + + it("should create AdvancedIMessageKit for remote mode", async () => { + const { AdvancedIMessageKit } = await import( + "@photon-ai/advanced-imessage-kit" + ); + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + expect(AdvancedIMessageKit.getInstance).toHaveBeenCalledWith({ + serverUrl: "https://example.com", + apiKey: "test-key", + }); + expect(adapter.sdk).toBeDefined(); + }); + + it("should throw on non-macOS platform in local mode", () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "linux" }); + try { + expect( + () => new iMessageAdapter({ local: true, logger: mockLogger }) + ).toThrow("iMessage adapter local mode requires macOS"); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform }); + } + }); + + it("should allow remote mode on non-macOS platforms", () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "linux" }); + try { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + expect(adapter.local).toBe(false); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform }); + } + }); +}); + +describe("initialize", () => { + it("should store chat instance and not throw", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + const mockChat = createMockChat(); + await adapter.initialize(mockChat as never); + // Logger is set in constructor, not in initialize + expect(adapter.name).toBe("imessage"); + }); + + it("should connect and wait for ready in remote mode", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + const mockChat = createMockChat(); + await adapter.initialize(mockChat as never); + expect(mockConnect).toHaveBeenCalled(); + expect(mockOnce).toHaveBeenCalledWith("ready", expect.any(Function)); + }); +}); + +describe("encodeThreadId / decodeThreadId", () => { + it("should encode thread ID", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + const threadId = adapter.encodeThreadId({ + chatGuid: "iMessage;-;+1234567890", + }); + expect(threadId).toBe("imessage:iMessage;-;+1234567890"); + }); + + it("should decode thread ID", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + const result = adapter.decodeThreadId("imessage:iMessage;-;+1234567890"); + expect(result).toEqual({ chatGuid: "iMessage;-;+1234567890" }); + }); + + it("should roundtrip encode/decode", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + const original = { chatGuid: "iMessage;+;chat123456" }; + const encoded = adapter.encodeThreadId(original); + const decoded = adapter.decodeThreadId(encoded); + expect(decoded).toEqual(original); + }); + + it("should throw on thread ID from another adapter", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(() => adapter.decodeThreadId("slack:C123:1234567890.123")).toThrow( + "Invalid iMessage thread ID" + ); + }); +}); + +describe("isDM", () => { + it("should return true for DM thread IDs (;-; pattern)", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(adapter.isDM("imessage:iMessage;-;+1234567890")).toBe(true); + }); + + it("should return false for group thread IDs (;+; pattern)", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(adapter.isDM("imessage:iMessage;+;chat493787071395575843")).toBe( + false + ); + }); + + it("should return true for SMS DMs", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + expect(adapter.isDM("imessage:SMS;-;+1234567890")).toBe(true); + }); +}); + +describe("handleWebhook", () => { + it("should return 501 (not supported)", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + const request = new Request("https://example.com/webhook", { + method: "POST", + body: "{}", + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(501); + }); +}); + +describe("startGatewayListener", () => { + afterEach(() => { + mockGatewayConnect.mockReset(); + mockGatewayClose.mockReset(); + mockGatewayOn.mockReset(); + mockClose.mockReset(); + }); + + it("should return 500 without chat instance", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + const response = await adapter.startGatewayListener({ + waitUntil: vi.fn(), + }); + expect(response.status).toBe(500); + const text = await response.text(); + expect(text).toBe("Chat instance not initialized"); + }); + + it("should return 500 without waitUntil", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + const response = await adapter.startGatewayListener({}); + expect(response.status).toBe(500); + const text = await response.text(); + expect(text).toBe("waitUntil not provided"); + }); + + it("should start listening and return success response", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + const waitUntil = vi.fn(); + const response = await adapter.startGatewayListener({ waitUntil }, 5000); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.status).toBe("listening"); + expect(body.durationMs).toBe(5000); + expect(body.mode).toBe("local"); + expect(waitUntil).toHaveBeenCalledOnce(); + }); + + it("should use abort signal to stop early", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + const controller = new AbortController(); + const waitUntil = vi.fn(); + + await adapter.startGatewayListener({ waitUntil }, 60000, controller.signal); + + // The listener promise was passed to waitUntil + expect(waitUntil).toHaveBeenCalledOnce(); + const listenerPromise = waitUntil.mock.calls[0][0] as Promise; + + // Abort immediately + controller.abort(); + + // The promise should resolve after abort + await listenerPromise; + expect(mockStopWatching).toHaveBeenCalled(); + }); + + it("should create a dedicated SDK instance in remote mode and close only that", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + const controller = new AbortController(); + const waitUntil = vi.fn(); + + // Track constructor calls before starting listener + const callCountBefore = MockAdvancedIMessageKit.mock.calls.length; + + await adapter.startGatewayListener({ waitUntil }, 60000, controller.signal); + + // A new AdvancedIMessageKit instance should have been created (not getInstance) + expect(MockAdvancedIMessageKit.mock.calls.length).toBe(callCountBefore + 1); + expect(MockAdvancedIMessageKit).toHaveBeenLastCalledWith({ + serverUrl: "https://example.com", + apiKey: "test-key", + }); + + // The gateway SDK should have been connected and had listener attached + expect(mockGatewayConnect).toHaveBeenCalled(); + expect(mockGatewayOn).toHaveBeenCalledWith( + "new-message", + expect.any(Function) + ); + + controller.abort(); + const listenerPromise = waitUntil.mock.calls[0][0] as Promise; + await listenerPromise; + + // Only the gateway SDK should be closed, not the shared singleton + expect(mockGatewayClose).toHaveBeenCalled(); + expect(mockClose).not.toHaveBeenCalled(); + }); +}); + +describe("postMessage", () => { + afterEach(() => { + mockSend.mockReset(); + mockSendMessage.mockReset(); + }); + + it("should send via local SDK with DM chatGuid", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + mockSend.mockResolvedValue({ + sentAt: new Date(), + message: { guid: "sent-msg-001" }, + }); + + const result = await adapter.postMessage( + "imessage:iMessage;-;+1234567890", + "Hello!" + ); + + expect(mockSend).toHaveBeenCalledWith("+1234567890", "Hello!"); + expect(result.id).toBe("sent-msg-001"); + expect(result.threadId).toBe("imessage:iMessage;-;+1234567890"); + expect(result.raw).toEqual({ + sentAt: expect.any(Date), + message: { guid: "sent-msg-001" }, + }); + }); + + it("should send via local SDK with group chatGuid", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + mockSend.mockResolvedValue({ + sentAt: new Date(), + message: { guid: "sent-msg-002" }, + }); + + const result = await adapter.postMessage( + "imessage:iMessage;+;chat493787071395575843", + "Hello group!" + ); + + expect(mockSend).toHaveBeenCalledWith( + "chat493787071395575843", + "Hello group!" + ); + expect(result.id).toBe("sent-msg-002"); + }); + + it("should fallback to generated ID when local SDK has no message guid", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + mockSend.mockResolvedValue({ sentAt: new Date() }); + + const result = await adapter.postMessage( + "imessage:iMessage;-;+1234567890", + "Hi" + ); + + expect(result.id).toMatch(LOCAL_ID_PATTERN); + }); + + it("should send via remote SDK", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockSendMessage.mockResolvedValue({ + guid: "remote-msg-001", + text: "Hello!", + }); + + const result = await adapter.postMessage( + "imessage:iMessage;-;+1234567890", + "Hello!" + ); + + expect(mockSendMessage).toHaveBeenCalledWith({ + chatGuid: "iMessage;-;+1234567890", + message: "Hello!", + }); + expect(result.id).toBe("remote-msg-001"); + expect(result.threadId).toBe("imessage:iMessage;-;+1234567890"); + expect(result.raw).toEqual({ + guid: "remote-msg-001", + text: "Hello!", + }); + }); +}); + +describe("editMessage", () => { + afterEach(() => { + mockEditMessage.mockReset(); + }); + + it("should throw NotImplementedError in local mode", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + await expect( + adapter.editMessage( + "imessage:iMessage;-;+1234567890", + "msg-guid-001", + "Updated text" + ) + ).rejects.toThrow("editMessage is not supported in local mode"); + }); + + it("should edit via remote SDK", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockEditMessage.mockResolvedValue({ + guid: "msg-guid-001", + text: "Updated text", + dateEdited: 1234567890, + }); + + const result = await adapter.editMessage( + "imessage:iMessage;-;+1234567890", + "msg-guid-001", + "Updated text" + ); + + expect(mockEditMessage).toHaveBeenCalledWith({ + messageGuid: "msg-guid-001", + editedMessage: "Updated text", + backwardsCompatibilityMessage: "Updated text", + }); + expect(result.id).toBe("msg-guid-001"); + expect(result.threadId).toBe("imessage:iMessage;-;+1234567890"); + expect(result.raw).toEqual({ + guid: "msg-guid-001", + text: "Updated text", + dateEdited: 1234567890, + }); + }); +}); + +describe("addReaction / removeReaction", () => { + afterEach(() => { + mockSendReaction.mockReset(); + }); + + it("should throw NotImplementedError in local mode for addReaction", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + await expect( + adapter.addReaction("imessage:iMessage;-;+1234567890", "msg-001", "heart") + ).rejects.toThrow("addReaction is not supported in local mode"); + }); + + it("should throw NotImplementedError in local mode for removeReaction", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + await expect( + adapter.removeReaction( + "imessage:iMessage;-;+1234567890", + "msg-001", + "heart" + ) + ).rejects.toThrow("removeReaction is not supported in local mode"); + }); + + it("should send tapback via remote SDK for addReaction", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockSendReaction.mockResolvedValue({ guid: "reaction-001" }); + + await adapter.addReaction( + "imessage:iMessage;-;+1234567890", + "msg-001", + "heart" + ); + + expect(mockSendReaction).toHaveBeenCalledWith({ + chatGuid: "iMessage;-;+1234567890", + messageGuid: "msg-001", + reaction: "love", + }); + }); + + it("should map thumbs_up to like tapback", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockSendReaction.mockResolvedValue({ guid: "reaction-002" }); + + await adapter.addReaction( + "imessage:iMessage;-;+1234567890", + "msg-001", + "thumbs_up" + ); + + expect(mockSendReaction).toHaveBeenCalledWith({ + chatGuid: "iMessage;-;+1234567890", + messageGuid: "msg-001", + reaction: "like", + }); + }); + + it("should send remove tapback with dash prefix for removeReaction", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockSendReaction.mockResolvedValue({ guid: "reaction-003" }); + + await adapter.removeReaction( + "imessage:iMessage;-;+1234567890", + "msg-001", + "laugh" + ); + + expect(mockSendReaction).toHaveBeenCalledWith({ + chatGuid: "iMessage;-;+1234567890", + messageGuid: "msg-001", + reaction: "-laugh", + }); + }); + + it("should throw for unsupported emoji", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + await expect( + adapter.addReaction("imessage:iMessage;-;+1234567890", "msg-001", "fire") + ).rejects.toThrow('Unsupported iMessage tapback: "fire"'); + }); +}); + +describe("startTyping", () => { + afterEach(() => { + mockStartTyping.mockReset(); + mockStopTyping.mockReset(); + }); + + it("should throw NotImplementedError in local mode", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + await expect( + adapter.startTyping("imessage:iMessage;-;+1234567890") + ).rejects.toThrow("startTyping is not supported in local mode"); + }); + + it("should call startTyping via remote SDK", async () => { + vi.useFakeTimers(); + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockStartTyping.mockResolvedValue(undefined); + mockStopTyping.mockResolvedValue(undefined); + + await adapter.startTyping("imessage:iMessage;-;+1234567890"); + + expect(mockStartTyping).toHaveBeenCalledWith("iMessage;-;+1234567890"); + expect(mockStopTyping).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(3000); + + expect(mockStopTyping).toHaveBeenCalledWith("iMessage;-;+1234567890"); + vi.useRealTimers(); + }); +}); + +describe("fetchThread", () => { + afterEach(() => { + mockGetChat.mockReset(); + }); + + it("should throw NotImplementedError in local mode", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + await expect( + adapter.fetchThread("imessage:iMessage;-;+1234567890") + ).rejects.toThrow("fetchThread is not supported in local mode"); + }); + + it("should fetch DM thread via remote SDK", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockGetChat.mockResolvedValue({ + originalROWID: 1, + guid: "iMessage;-;+1234567890", + style: 43, + chatIdentifier: "+1234567890", + isArchived: false, + displayName: "", + participants: [{ address: "+1234567890" }], + }); + + const result = await adapter.fetchThread("imessage:iMessage;-;+1234567890"); + + expect(mockGetChat).toHaveBeenCalledWith("iMessage;-;+1234567890"); + expect(result.id).toBe("imessage:iMessage;-;+1234567890"); + expect(result.channelId).toBe("iMessage;-;+1234567890"); + expect(result.isDM).toBe(true); + expect(result.channelName).toBeUndefined(); + expect(result.metadata.chatIdentifier).toBe("+1234567890"); + }); + + it("should fetch group thread via remote SDK", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockGetChat.mockResolvedValue({ + originalROWID: 2, + guid: "iMessage;+;chat493787071395575843", + style: 45, + chatIdentifier: "chat493787071395575843", + isArchived: false, + displayName: "Family Group", + participants: [{ address: "+1234567890" }, { address: "+1987654321" }], + }); + + const result = await adapter.fetchThread( + "imessage:iMessage;+;chat493787071395575843" + ); + + expect(result.isDM).toBe(false); + expect(result.channelName).toBe("Family Group"); + expect(result.metadata.style).toBe(45); + }); +}); + +describe("parseMessage", () => { + it("should parse local imessage-kit Message when local is true", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + adapter.initialize(createMockChat() as never); + + const localRaw = { + id: "123", + guid: "msg-local-001", + text: "Hello from local", + sender: "+1234567890", + senderName: "Alice", + chatId: "iMessage;-;+1234567890", + isGroupChat: false, + service: "iMessage", + isRead: true, + isFromMe: false, + isReaction: false, + reactionType: null, + isReactionRemoval: false, + associatedMessageGuid: null, + attachments: [], + date: new Date("2026-01-15T12:00:00Z"), + }; + + const message = adapter.parseMessage(localRaw); + expect(message.id).toBe("msg-local-001"); + expect(message.text).toBe("Hello from local"); + expect(message.author.userId).toBe("+1234567890"); + expect(message.author.userName).toBe("Alice"); + expect(message.threadId).toBe("imessage:iMessage;-;+1234567890"); + expect(message.isMention).toBe(true); + }); + + it("should parse remote advanced-imessage-kit MessageResponse when local is false", () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + adapter.initialize(createMockChat() as never); + + const remoteRaw = { + originalROWID: 1, + guid: "msg-remote-001", + text: "Hello from remote", + handleId: 1, + otherHandle: 0, + handle: { address: "+1987654321" }, + chats: [{ guid: "iMessage;-;+1987654321", style: 43 }], + subject: "", + error: 0, + dateCreated: new Date("2026-01-15T12:00:00Z").getTime(), + dateRead: null, + dateDelivered: null, + isFromMe: false, + isArchived: false, + itemType: 0, + groupTitle: null, + groupActionType: 0, + balloonBundleId: null, + associatedMessageGuid: null, + associatedMessageType: null, + expressiveSendStyleId: null, + attachments: [], + }; + + const message = adapter.parseMessage(remoteRaw); + expect(message.id).toBe("msg-remote-001"); + expect(message.text).toBe("Hello from remote"); + expect(message.author.userId).toBe("+1987654321"); + expect(message.threadId).toBe("imessage:iMessage;-;+1987654321"); + expect(message.isMention).toBe(true); + }); + + it("should set isMention to false for group chats in local mode", () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + adapter.initialize(createMockChat() as never); + + const localRaw = { + id: "123", + guid: "msg-local-002", + text: "Group message", + sender: "+1234567890", + senderName: null, + chatId: "iMessage;+;chat123456", + isGroupChat: true, + service: "iMessage", + isRead: true, + isFromMe: false, + isReaction: false, + reactionType: null, + isReactionRemoval: false, + associatedMessageGuid: null, + attachments: [], + date: new Date("2026-01-15T12:00:00Z"), + }; + + const message = adapter.parseMessage(localRaw); + expect(message.isMention).toBe(false); + expect(message.author.userName).toBe("+1234567890"); + }); + + it("should handle attachments from remote payload", () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + adapter.initialize(createMockChat() as never); + + const remoteRaw = { + originalROWID: 2, + guid: "msg-remote-002", + text: "Photo", + handleId: 1, + otherHandle: 0, + handle: { address: "+1987654321" }, + chats: [{ guid: "iMessage;-;+1987654321", style: 43 }], + subject: "", + error: 0, + dateCreated: Date.now(), + dateRead: null, + dateDelivered: null, + isFromMe: false, + isArchived: false, + itemType: 0, + groupTitle: null, + groupActionType: 0, + balloonBundleId: null, + associatedMessageGuid: null, + associatedMessageType: null, + expressiveSendStyleId: null, + attachments: [ + { + guid: "att-001", + transferName: "photo.jpg", + mimeType: "image/jpeg", + totalBytes: 54321, + }, + ], + }; + + const message = adapter.parseMessage(remoteRaw); + expect(message.attachments).toHaveLength(1); + expect(message.attachments[0].type).toBe("image"); + expect(message.attachments[0].name).toBe("photo.jpg"); + expect(message.attachments[0].mimeType).toBe("image/jpeg"); + }); +}); + +describe("createiMessageAdapter", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("should default to local mode", () => { + const adapter = createiMessageAdapter(); + expect(adapter.local).toBe(true); + }); + + it("should use remote mode when local is false", () => { + const adapter = createiMessageAdapter({ + local: false, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + expect(adapter.local).toBe(false); + expect(adapter.serverUrl).toBe("https://example.com"); + expect(adapter.apiKey).toBe("test-key"); + }); + + it("should read IMESSAGE_LOCAL env var", () => { + vi.stubEnv("IMESSAGE_LOCAL", "false"); + vi.stubEnv("IMESSAGE_SERVER_URL", "https://env.example.com"); + vi.stubEnv("IMESSAGE_API_KEY", "env-key"); + + const adapter = createiMessageAdapter(); + expect(adapter.local).toBe(false); + expect(adapter.serverUrl).toBe("https://env.example.com"); + expect(adapter.apiKey).toBe("env-key"); + }); + + it("should throw ValidationError when remote mode is missing serverUrl", () => { + expect(() => createiMessageAdapter({ local: false })).toThrow( + ValidationError + ); + expect(() => createiMessageAdapter({ local: false })).toThrow( + "serverUrl is required when local is false" + ); + }); + + it("should throw ValidationError when remote mode is missing apiKey", () => { + expect(() => + createiMessageAdapter({ + local: false, + serverUrl: "https://example.com", + }) + ).toThrow(ValidationError); + expect(() => + createiMessageAdapter({ + local: false, + serverUrl: "https://example.com", + }) + ).toThrow("apiKey is required when local is false"); + }); + + it("should prefer config values over env vars", () => { + vi.stubEnv("IMESSAGE_SERVER_URL", "https://env.example.com"); + vi.stubEnv("IMESSAGE_API_KEY", "env-key"); + + const adapter = createiMessageAdapter({ + local: false, + serverUrl: "https://config.example.com", + apiKey: "config-key", + }); + expect(adapter.serverUrl).toBe("https://config.example.com"); + expect(adapter.apiKey).toBe("config-key"); + }); + + it("should read IMESSAGE_SERVER_URL and IMESSAGE_API_KEY for local mode", () => { + vi.stubEnv("IMESSAGE_SERVER_URL", "http://localhost:5678"); + vi.stubEnv("IMESSAGE_API_KEY", "local-key"); + + const adapter = createiMessageAdapter({ local: true }); + expect(adapter.local).toBe(true); + expect(adapter.serverUrl).toBe("http://localhost:5678"); + expect(adapter.apiKey).toBe("local-key"); + }); +}); + +describe("postMessage with polls", () => { + afterEach(() => { + mockPollCreate.mockReset(); + }); + + it("should create poll via remote SDK", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockPollCreate.mockResolvedValue({ + guid: "poll-msg-001", + text: "Poll created", + }); + + const poll = Poll({ + id: "fav-color", + question: "Favorite color?", + options: ["Red", "Blue", "Green"], + }); + + const result = await adapter.postMessage( + "imessage:iMessage;-;+1234567890", + poll + ); + + expect(mockPollCreate).toHaveBeenCalledWith({ + chatGuid: "iMessage;-;+1234567890", + title: "Favorite color?", + options: ["Red", "Blue", "Green"], + }); + expect(result.id).toBe("poll-msg-001"); + expect(result.threadId).toBe("imessage:iMessage;-;+1234567890"); + }); + + it("should create poll via PostablePoll wrapper", async () => { + const adapter = new iMessageAdapter({ + local: false, + logger: mockLogger, + serverUrl: "https://example.com", + apiKey: "test-key", + }); + await adapter.initialize(createMockChat() as never); + + mockPollCreate.mockResolvedValue({ + guid: "poll-msg-002", + text: "Poll created", + }); + + const result = await adapter.postMessage( + "imessage:iMessage;-;+1234567890", + { + poll: Poll({ + id: "yes-no", + question: "Do you agree?", + options: ["Yes", "No"], + }), + } + ); + + expect(mockPollCreate).toHaveBeenCalledWith({ + chatGuid: "iMessage;-;+1234567890", + title: "Do you agree?", + options: ["Yes", "No"], + }); + expect(result.id).toBe("poll-msg-002"); + }); + + it("should throw NotImplementedError for polls in local mode", async () => { + const adapter = new iMessageAdapter({ local: true, logger: mockLogger }); + await adapter.initialize(createMockChat() as never); + + const poll = Poll({ + id: "test", + question: "Q?", + options: ["A", "B"], + }); + + await expect( + adapter.postMessage("imessage:iMessage;-;+1234567890", poll) + ).rejects.toThrow(NotImplementedError); + await expect( + adapter.postMessage("imessage:iMessage;-;+1234567890", poll) + ).rejects.toThrow("Polls are not supported in local mode"); + }); +}); diff --git a/packages/adapter-imessage/src/index.ts b/packages/adapter-imessage/src/index.ts new file mode 100644 index 00000000..8be399a8 --- /dev/null +++ b/packages/adapter-imessage/src/index.ts @@ -0,0 +1,891 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + extractFiles, + extractPoll, + ValidationError, +} from "@chat-adapter/shared"; +import type { MessageResponse } from "@photon-ai/advanced-imessage-kit"; +import { + AdvancedIMessageKit, + isPollVote, + parsePollVotes, +} from "@photon-ai/advanced-imessage-kit"; +import type { Message as IMessageLocalMessage } from "@photon-ai/imessage-kit"; +import { IMessageSDK } from "@photon-ai/imessage-kit"; +import type { + ActionEvent, + Adapter, + AdapterPostableMessage, + ChatInstance, + EmojiValue, + FetchOptions, + FetchResult, + FileUpload, + FormattedContent, + Logger, + RawMessage, + ThreadInfo, + WebhookOptions, +} from "chat"; +import { + ConsoleLogger, + Message, + NotImplementedError, + parseMarkdown, +} from "chat"; +import { iMessageFormatConverter } from "./markdown"; +import type { iMessageGatewayMessageData, iMessageThreadId } from "./types"; + +export { iMessageFormatConverter } from "./markdown"; +export type { + iMessageGatewayMessageData, + iMessageThreadId, + NativeWebhookPayload, +} from "./types"; + +export interface iMessageAdapterLocalConfig { + apiKey?: string; + local: true; + logger: Logger; + serverUrl?: string; +} + +export interface iMessageAdapterRemoteConfig { + apiKey: string; + local: false; + logger: Logger; + serverUrl: string; +} + +export type iMessageAdapterConfig = + | iMessageAdapterLocalConfig + | iMessageAdapterRemoteConfig; + +export class iMessageAdapter implements Adapter { + readonly name = "imessage"; + readonly userName: string = ""; + readonly local: boolean; + readonly serverUrl?: string; + readonly apiKey?: string; + readonly sdk: IMessageSDK | AdvancedIMessageKit; + + private chat: ChatInstance | null = null; + private readonly logger: Logger; + private readonly formatConverter = new iMessageFormatConverter(); + /** Maps poll GUIDs from the remote API to poll metadata for action callbacks */ + private readonly pollIdMap = new Map< + string, + { id: string; options: string[] } + >(); + + constructor(config: iMessageAdapterConfig) { + if (config.local && process.platform !== "darwin") { + throw new ValidationError( + "imessage", + "iMessage adapter local mode requires macOS. Current platform: " + + process.platform + ); + } + + this.local = config.local; + this.serverUrl = config.serverUrl; + this.apiKey = config.apiKey; + this.logger = config.logger; + + if (config.local) { + this.sdk = new IMessageSDK(); + } else { + this.sdk = AdvancedIMessageKit.getInstance({ + serverUrl: config.serverUrl, + apiKey: config.apiKey, + }); + } + } + + async initialize(chat: ChatInstance): Promise { + this.chat = chat; + this.logger.info("iMessage adapter initialized", { + local: this.local, + serverUrl: this.serverUrl ? "configured" : "not configured", + }); + + if (!this.local) { + const sdk = this.sdk as AdvancedIMessageKit; + await sdk.connect(); + await new Promise((resolve) => sdk.once("ready", resolve)); + } + } + + async handleWebhook( + _request: Request, + _options?: WebhookOptions + ): Promise { + return new Response("Webhook not supported — use startGatewayListener()", { + status: 501, + }); + } + + async postMessage( + threadId: string, + message: AdapterPostableMessage + ): Promise { + const { chatGuid } = this.decodeThreadId(threadId); + + // Handle polls + const poll = extractPoll(message); + if (poll) { + if (this.local) { + throw new NotImplementedError( + "Polls are not supported in local mode", + "polls" + ); + } + const sdk = this.sdk as AdvancedIMessageKit; + const result = await sdk.polls.create({ + chatGuid, + title: poll.question, + options: poll.options, + }); + this.pollIdMap.set(result.guid, { + id: poll.id, + options: poll.options, + }); + return { id: result.guid, threadId, raw: result }; + } + + const text = this.formatConverter.renderPostable(message); + const files = extractFiles(message); + const tempFiles = + files.length > 0 ? await this.writeTempFiles(files) : null; + + try { + if (this.local) { + const sdk = this.sdk as IMessageSDK; + // sdk.send() expects the core identifier (phone/email/chatId), not the full GUID. + // chatGuid format: "iMessage;-;+1234567890" or "iMessage;+;chat123..." + // Extract last part after final semicolon. + const recipient = chatGuid.split(";").pop() ?? chatGuid; + const content = tempFiles?.paths.length + ? { text: text || undefined, files: tempFiles.paths } + : text; + const result = await sdk.send(recipient, content); + return { + id: result.message?.guid ?? `local-${Date.now()}`, + threadId, + raw: result, + }; + } + + // Remote: sdk.messages.sendMessage() takes the full chatGuid directly + const sdk = this.sdk as AdvancedIMessageKit; + let result: MessageResponse | undefined; + + if (text || !tempFiles) { + result = await sdk.messages.sendMessage({ + chatGuid, + message: text, + }); + } + + if (tempFiles) { + for (const filePath of tempFiles.paths) { + const attachmentResult = await sdk.attachments.sendAttachment({ + chatGuid, + filePath, + }); + result ??= attachmentResult; + } + } + + return { + id: result?.guid ?? `msg-${Date.now()}`, + threadId, + raw: result, + }; + } finally { + if (tempFiles) { + await rm(tempFiles.dir, { recursive: true }).catch(() => {}); + } + } + } + + async editMessage( + threadId: string, + messageId: string, + message: AdapterPostableMessage + ): Promise { + if (this.local) { + throw new NotImplementedError( + "editMessage is not supported in local mode", + "editMessage" + ); + } + + const text = this.formatConverter.renderPostable(message); + const sdk = this.sdk as AdvancedIMessageKit; + const result = await sdk.messages.editMessage({ + messageGuid: messageId, + editedMessage: text, + backwardsCompatibilityMessage: text, + }); + return { + id: result.guid, + threadId, + raw: result, + }; + } + + async deleteMessage(_threadId: string, _messageId: string): Promise { + throw new NotImplementedError( + "deleteMessage is not implemented", + "deleteMessage" + ); + } + + parseMessage(raw: unknown): Message { + const data = this.local + ? this.normalizeLocalMessage(raw as IMessageLocalMessage) + : this.normalizeRemoteMessage(raw as MessageResponse); + return this.buildMessage(data); + } + + async fetchMessages( + threadId: string, + options?: FetchOptions + ): Promise { + const { chatGuid } = this.decodeThreadId(threadId); + const direction = options?.direction ?? "backward"; + const limit = options?.limit ?? 50; + const cursor = options?.cursor; + + if (this.local) { + return this.fetchMessagesLocal(chatGuid, direction, limit, cursor); + } + + return this.fetchMessagesRemote(chatGuid, direction, limit, cursor); + } + + async fetchThread(threadId: string): Promise { + if (this.local) { + throw new NotImplementedError( + "fetchThread is not supported in local mode", + "fetchThread" + ); + } + + const { chatGuid } = this.decodeThreadId(threadId); + const sdk = this.sdk as AdvancedIMessageKit; + const chat = await sdk.chats.getChat(chatGuid); + // Chat guid ";-;" = DM, ";+;" = group (style alone is unreliable) + const isDM = chatGuid.includes(";-;"); + + return { + id: threadId, + channelId: chatGuid, + channelName: chat.displayName || undefined, + isDM, + metadata: { + chatIdentifier: chat.chatIdentifier, + style: chat.style, + participants: chat.participants, + isArchived: chat.isArchived, + raw: chat, + }, + }; + } + + async addReaction( + threadId: string, + messageId: string, + emoji: EmojiValue | string + ): Promise { + if (this.local) { + throw new NotImplementedError( + "addReaction is not supported in local mode", + "addReaction" + ); + } + + const tapback = this.emojiToTapback(emoji); + const { chatGuid } = this.decodeThreadId(threadId); + const sdk = this.sdk as AdvancedIMessageKit; + await sdk.messages.sendReaction({ + chatGuid, + messageGuid: messageId, + reaction: tapback, + }); + } + + async removeReaction( + threadId: string, + messageId: string, + emoji: EmojiValue | string + ): Promise { + if (this.local) { + throw new NotImplementedError( + "removeReaction is not supported in local mode", + "removeReaction" + ); + } + + const tapback = this.emojiToTapback(emoji); + const { chatGuid } = this.decodeThreadId(threadId); + const sdk = this.sdk as AdvancedIMessageKit; + await sdk.messages.sendReaction({ + chatGuid, + messageGuid: messageId, + reaction: `-${tapback}`, + }); + } + + async startTyping(threadId: string, _status?: string): Promise { + if (this.local) { + throw new NotImplementedError( + "startTyping is not supported in local mode", + "startTyping" + ); + } + + const { chatGuid } = this.decodeThreadId(threadId); + const sdk = this.sdk as AdvancedIMessageKit; + await sdk.chats.startTyping(chatGuid); + setTimeout(() => sdk.chats.stopTyping(chatGuid), 3000); + } + + renderFormatted(content: FormattedContent): string { + return this.formatConverter.fromAst(content); + } + + encodeThreadId(platformData: iMessageThreadId): string { + return `imessage:${platformData.chatGuid}`; + } + + decodeThreadId(threadId: string): iMessageThreadId { + if (!threadId.startsWith("imessage:")) { + throw new ValidationError( + "imessage", + `Invalid iMessage thread ID: ${threadId}` + ); + } + return { chatGuid: threadId.slice("imessage:".length) }; + } + + /** + * Check if a thread is a direct message (one-on-one) conversation. + * DM chatGuids use ";-;" (e.g., "iMessage;-;+1234567890") + * Group chatGuids use ";+;" (e.g., "iMessage;+;chat123456") + */ + isDM(threadId: string): boolean { + const { chatGuid } = this.decodeThreadId(threadId); + return chatGuid.includes(";-;"); + } + + /** + * Start listening for incoming iMessage messages via the Gateway pattern. + * + * In local mode, uses IMessageSDK.startWatching() to poll for new messages. + * In remote mode, creates a dedicated AdvancedIMessageKit socket.io connection. + * + * Messages are processed directly via the Chat instance. For webhook-based + * message ingestion (e.g. imessage-kit running on a separate Mac), use + * handleWebhook() instead. + */ + async startGatewayListener( + options: WebhookOptions, + durationMs = 180000, + abortSignal?: AbortSignal + ): Promise { + if (!this.chat) { + return new Response("Chat instance not initialized", { status: 500 }); + } + + if (!options.waitUntil) { + return new Response("waitUntil not provided", { status: 500 }); + } + + this.logger.info("Starting iMessage Gateway listener", { + durationMs, + mode: this.local ? "local" : "remote", + }); + + const listenerPromise = this.runGatewayListener(durationMs, abortSignal); + + options.waitUntil(listenerPromise); + + return new Response( + JSON.stringify({ + status: "listening", + durationMs, + mode: this.local ? "local" : "remote", + message: `Gateway listener started, will run for ${durationMs / 1000} seconds`, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + private async runGatewayListener( + durationMs: number, + abortSignal?: AbortSignal + ): Promise { + let isShuttingDown = false; + + // Create a dedicated instance for remote mode so closing it doesn't + // affect the shared this.sdk singleton used by other methods. + let remoteGatewaySdk: AdvancedIMessageKit | null = null; + + try { + if (this.local) { + const sdk = this.sdk as IMessageSDK; + + await sdk.startWatching({ + onMessage: async (message: IMessageLocalMessage) => { + if (isShuttingDown) { + return; + } + if (message.isFromMe) { + return; + } + + const data = this.normalizeLocalMessage(message); + this.handleGatewayMessage(data); + }, + onError: (error: Error) => { + this.logger.error("iMessage local watcher error", { + error: String(error), + }); + }, + }); + } else { + remoteGatewaySdk = new AdvancedIMessageKit({ + serverUrl: this.serverUrl, + apiKey: this.apiKey, + }); + await remoteGatewaySdk.connect(); + + remoteGatewaySdk.on( + "new-message", + async (messageResponse: MessageResponse) => { + if (isShuttingDown) { + return; + } + if (messageResponse.isFromMe) { + return; + } + + // Handle poll votes as action events + if (isPollVote(messageResponse)) { + this.handlePollVote(messageResponse); + return; + } + + const data = this.normalizeRemoteMessage(messageResponse); + this.handleGatewayMessage(data); + } + ); + } + + // Wait for duration or abort signal + await new Promise((resolve) => { + const timeout = setTimeout(resolve, durationMs); + + if (abortSignal) { + if (abortSignal.aborted) { + clearTimeout(timeout); + resolve(); + return; + } + abortSignal.addEventListener( + "abort", + () => { + this.logger.info( + "iMessage Gateway listener received abort signal" + ); + clearTimeout(timeout); + resolve(); + }, + { once: true } + ); + } + }); + + this.logger.info( + "iMessage Gateway listener duration elapsed, disconnecting" + ); + } catch (error) { + this.logger.error("iMessage Gateway listener error", { + error: String(error), + }); + } finally { + isShuttingDown = true; + + if (this.local) { + (this.sdk as IMessageSDK).stopWatching(); + } else if (remoteGatewaySdk) { + await remoteGatewaySdk.close(); + } + + this.logger.info("iMessage Gateway listener stopped"); + } + } + + private async writeTempFiles( + files: FileUpload[] + ): Promise<{ dir: string; paths: string[] }> { + const dir = await mkdtemp(join(tmpdir(), "imessage-")); + const paths: string[] = []; + for (const file of files) { + let buffer: Buffer; + if (Buffer.isBuffer(file.data)) { + buffer = file.data; + } else if (file.data instanceof Blob) { + buffer = Buffer.from(await file.data.arrayBuffer()); + } else { + buffer = Buffer.from(file.data as ArrayBuffer); + } + const filePath = join(dir, file.filename); + await writeFile(filePath, buffer); + paths.push(filePath); + } + return { dir, paths }; + } + + private async fetchMessagesLocal( + chatGuid: string, + direction: "forward" | "backward", + limit: number, + cursor?: string + ): Promise { + const sdk = this.sdk as IMessageSDK; + const since = + direction === "forward" && cursor ? new Date(cursor) : undefined; + const result = await sdk.getMessages({ + chatId: chatGuid, + limit: 1000, + since, + }); + + let messages = [...result.messages].sort( + (a, b) => a.date.getTime() - b.date.getTime() + ); + + if (direction === "backward" && cursor) { + const cursorTime = new Date(cursor).getTime(); + messages = messages.filter((m) => m.date.getTime() < cursorTime); + } + + const isBackward = direction === "backward"; + const start = isBackward ? Math.max(0, messages.length - limit) : 0; + const selected = messages.slice(start, start + limit); + const hasMore = isBackward ? start > 0 : messages.length > limit; + + const normalized = selected.map((m) => + this.buildMessage(this.normalizeLocalMessage(m)) + ); + + let nextCursor: string | undefined; + if (hasMore && selected.length > 0) { + nextCursor = isBackward + ? selected[0].date.toISOString() + : selected.at(-1)?.date.toISOString(); + } + + return { messages: normalized, nextCursor }; + } + + private async fetchMessagesRemote( + chatGuid: string, + direction: "forward" | "backward", + limit: number, + cursor?: string + ): Promise { + const sdk = this.sdk as AdvancedIMessageKit; + const isBackward = direction === "backward"; + + const queryOptions: { + chatGuid: string; + limit: number; + sort: "ASC" | "DESC"; + before?: number; + after?: number; + with?: string[]; + } = { + chatGuid, + limit: limit + 1, + sort: isBackward ? "DESC" : "ASC", + with: ["chat", "handle", "attachment"], + }; + + if (cursor) { + const timestamp = Number(cursor); + if (isBackward) { + queryOptions.before = timestamp; + } else { + queryOptions.after = timestamp; + } + } + + const results = await sdk.messages.getMessages(queryOptions); + const hasMore = results.length > limit; + const sliced = hasMore ? results.slice(0, limit) : results; + + if (isBackward) { + sliced.reverse(); + } + + const normalized = sliced.map((m) => + this.buildMessage(this.normalizeRemoteMessage(m)) + ); + + let nextCursor: string | undefined; + if (hasMore && sliced.length > 0) { + nextCursor = isBackward + ? String(sliced[0].dateCreated) + : String(sliced.at(-1)?.dateCreated); + } + + return { messages: normalized, nextCursor }; + } + + private normalizeLocalMessage( + message: IMessageLocalMessage + ): iMessageGatewayMessageData { + return { + guid: message.guid, + text: message.text, + sender: message.sender, + senderName: message.senderName, + chatId: message.chatId, + isGroupChat: message.isGroupChat, + isFromMe: message.isFromMe, + date: message.date.toISOString(), + attachments: message.attachments.map((a) => ({ + id: a.id, + filename: a.filename, + mimeType: a.mimeType, + size: a.size, + })), + source: "local", + raw: message, + }; + } + + private normalizeRemoteMessage( + messageResponse: MessageResponse + ): iMessageGatewayMessageData { + const chatGuid = messageResponse.chats?.[0]?.guid ?? ""; + // Chat guid format: "{service};{type};{identifier}" + // type "-" = DM (one-on-one), type "+" = group chat + // e.g. "iMessage;-;+1234567890" or "any;-;+1234567890" = DM + // "iMessage;+;chat123..." = group + // Style alone is unreliable — some servers report style=45 for DMs + // with the "any;-;" prefix. + const isGroupChat = !chatGuid.includes(";-;"); + + return { + guid: messageResponse.guid, + text: messageResponse.text, + sender: messageResponse.handle?.address ?? "", + senderName: null, + chatId: chatGuid, + isGroupChat, + isFromMe: messageResponse.isFromMe, + date: new Date(messageResponse.dateCreated).toISOString(), + attachments: (messageResponse.attachments ?? []).map((a) => ({ + id: a.guid, + filename: a.transferName, + mimeType: a.mimeType ?? "application/octet-stream", + size: a.totalBytes, + })), + source: "remote", + raw: messageResponse, + }; + } + + private buildMessage(data: iMessageGatewayMessageData): Message { + const threadId = this.encodeThreadId({ chatGuid: data.chatId }); + return new Message({ + id: data.guid, + threadId, + text: data.text ?? "", + formatted: parseMarkdown(data.text ?? ""), + author: { + userId: data.sender, + userName: data.senderName ?? data.sender, + fullName: data.senderName ?? data.sender, + isBot: false, + isMe: data.isFromMe, + }, + metadata: { + dateSent: new Date(data.date), + edited: false, + }, + attachments: data.attachments.map((a) => ({ + type: this.getAttachmentType(a.mimeType), + name: a.filename, + mimeType: a.mimeType, + size: a.size, + })), + raw: data.raw ?? data, + isMention: !data.isGroupChat, + }); + } + + private handleGatewayMessage( + data: iMessageGatewayMessageData, + options?: WebhookOptions + ): void { + if (!this.chat) { + return; + } + + const chatMessage = this.buildMessage(data); + this.chat.processMessage(this, chatMessage.threadId, chatMessage, options); + } + + private handlePollVote( + messageResponse: MessageResponse, + options?: WebhookOptions + ): void { + if (!this.chat) { + return; + } + + // The associatedMessageGuid links the vote back to the original poll + const pollGuid = messageResponse.associatedMessageGuid; + if (!pollGuid) { + this.logger.debug("Poll vote missing associatedMessageGuid, skipping"); + return; + } + + const pollMeta = this.pollIdMap.get(pollGuid); + if (!pollMeta) { + this.logger.debug("Poll vote for unknown poll, skipping", { pollGuid }); + return; + } + + const parsed = parsePollVotes(messageResponse); + if (!parsed) { + this.logger.debug("Failed to parse poll votes", { + guid: messageResponse.guid, + }); + return; + } + + const chatGuid = messageResponse.chats?.[0]?.guid ?? ""; + const threadId = this.encodeThreadId({ chatGuid }); + + for (const vote of parsed.votes) { + // Find the option index from the identifier pattern (e.g., "0", "1") + const optionIndex = Number.parseInt(vote.voteOptionIdentifier, 10); + const optionText = Number.isNaN(optionIndex) + ? vote.voteOptionIdentifier + : (pollMeta.options[optionIndex] ?? vote.voteOptionIdentifier); + + const actionEvent: Omit & { + adapter: iMessageAdapter; + } = { + actionId: pollMeta.id, + value: optionText, + user: { + userId: vote.participantHandle, + userName: vote.participantHandle, + fullName: vote.participantHandle, + isBot: false, + isMe: false, + }, + messageId: messageResponse.guid, + threadId, + adapter: this, + raw: messageResponse, + }; + + this.chat.processAction(actionEvent, options); + } + } + + private emojiToTapback(emoji: EmojiValue | string): string { + const name = typeof emoji === "string" ? emoji : emoji.name; + const tapbackMap: Record = { + heart: "love", + love: "love", + thumbs_up: "like", + like: "like", + thumbs_down: "dislike", + dislike: "dislike", + laugh: "laugh", + emphasize: "emphasize", + exclamation: "emphasize", + question: "question", + }; + const tapback = tapbackMap[name]; + if (!tapback) { + throw new ValidationError( + "imessage", + `Unsupported iMessage tapback: "${name}". Supported: heart, thumbs_up, thumbs_down, laugh, emphasize, question` + ); + } + return tapback; + } + + private getAttachmentType( + mimeType?: string + ): "image" | "video" | "audio" | "file" { + if (!mimeType) { + return "file"; + } + if (mimeType.startsWith("image/")) { + return "image"; + } + if (mimeType.startsWith("video/")) { + return "video"; + } + if (mimeType.startsWith("audio/")) { + return "audio"; + } + return "file"; + } +} + +export function createiMessageAdapter( + config?: Partial +): iMessageAdapter { + const local = config?.local ?? process.env.IMESSAGE_LOCAL !== "false"; + const logger = config?.logger ?? new ConsoleLogger("info").child("imessage"); + + if (local) { + return new iMessageAdapter({ + local: true, + logger, + serverUrl: config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL, + apiKey: config?.apiKey ?? process.env.IMESSAGE_API_KEY, + }); + } + + const serverUrl = config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL; + if (!serverUrl) { + throw new ValidationError( + "imessage", + "serverUrl is required when local is false. Set IMESSAGE_SERVER_URL or provide it in config." + ); + } + + const apiKey = config?.apiKey ?? process.env.IMESSAGE_API_KEY; + if (!apiKey) { + throw new ValidationError( + "imessage", + "apiKey is required when local is false. Set IMESSAGE_API_KEY or provide it in config." + ); + } + + return new iMessageAdapter({ + local: false, + logger, + serverUrl, + apiKey, + }); +} diff --git a/packages/adapter-imessage/src/markdown.test.ts b/packages/adapter-imessage/src/markdown.test.ts new file mode 100644 index 00000000..46a19777 --- /dev/null +++ b/packages/adapter-imessage/src/markdown.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import { iMessageFormatConverter } from "./markdown"; + +const converter = new iMessageFormatConverter(); + +describe("iMessageFormatConverter", () => { + describe("fromAst / toAst round-trip", () => { + it("should handle plain text", () => { + const ast = converter.toAst("Hello world"); + const result = converter.fromAst(ast); + expect(result).toBe("Hello world"); + }); + + it("should strip bold formatting", () => { + const ast = converter.toAst("**bold text**"); + const result = converter.fromAst(ast); + expect(result).toBe("bold text"); + }); + + it("should strip italic formatting", () => { + const ast = converter.toAst("_italic text_"); + const result = converter.fromAst(ast); + expect(result).toBe("italic text"); + }); + + it("should strip strikethrough formatting", () => { + const ast = converter.toAst("~~deleted~~"); + const result = converter.fromAst(ast); + expect(result).toBe("deleted"); + }); + + it("should render links with URL", () => { + const ast = converter.toAst("[click here](https://example.com)"); + const result = converter.fromAst(ast); + expect(result).toBe("click here (https://example.com)"); + }); + + it("should preserve inline code content", () => { + const ast = converter.toAst("`code`"); + const result = converter.fromAst(ast); + expect(result).toBe("code"); + }); + + it("should preserve code block content", () => { + const ast = converter.toAst("```\nconst x = 1;\n```"); + const result = converter.fromAst(ast); + expect(result).toContain("const x = 1;"); + }); + + it("should render unordered lists", () => { + const ast = converter.toAst("- item 1\n- item 2"); + const result = converter.fromAst(ast); + expect(result).toContain("- item 1"); + expect(result).toContain("- item 2"); + }); + + it("should render ordered lists", () => { + const ast = converter.toAst("1. first\n2. second"); + const result = converter.fromAst(ast); + expect(result).toContain("1. first"); + expect(result).toContain("2. second"); + }); + + it("should render blockquotes", () => { + const ast = converter.toAst("> quoted text"); + const result = converter.fromAst(ast); + expect(result).toContain("> quoted text"); + }); + }); + + describe("renderPostable", () => { + it("should pass through plain strings", () => { + expect(converter.renderPostable("Hello")).toBe("Hello"); + }); + + it("should pass through raw strings", () => { + expect(converter.renderPostable({ raw: "raw text" })).toBe("raw text"); + }); + + it("should convert markdown messages", () => { + const result = converter.renderPostable({ markdown: "**bold**" }); + expect(result).toBe("bold"); + }); + }); +}); diff --git a/packages/adapter-imessage/src/markdown.ts b/packages/adapter-imessage/src/markdown.ts new file mode 100644 index 00000000..913df510 --- /dev/null +++ b/packages/adapter-imessage/src/markdown.ts @@ -0,0 +1,118 @@ +/** + * iMessage format conversion using AST-based parsing. + * + * iMessage supports plain text only — no rich formatting syntax. + * The converter strips formatting markers and outputs clean plain text, + * preserving structure (lists, blockquotes, code blocks) with whitespace. + */ + +import { + BaseFormatConverter, + type Content, + getNodeChildren, + getNodeValue, + isBlockquoteNode, + isCodeNode, + isDeleteNode, + isEmphasisNode, + isInlineCodeNode, + isLinkNode, + isListItemNode, + isListNode, + isParagraphNode, + isStrongNode, + isTextNode, + parseMarkdown, + type Root, +} from "chat"; + +export class iMessageFormatConverter extends BaseFormatConverter { + /** + * Render an AST to iMessage plain text format. + * Strips all formatting markers since iMessage doesn't support rich text via API. + */ + fromAst(ast: Root): string { + return this.fromAstWithNodeConverter(ast, (node) => + this.nodeToPlainText(node) + ); + } + + /** + * Parse iMessage text into an AST. + * iMessage sends plain text, so we just parse it as markdown. + */ + toAst(text: string): Root { + return parseMarkdown(text); + } + + private nodeToPlainText(node: Content): string { + if (isParagraphNode(node)) { + return getNodeChildren(node) + .map((child) => this.nodeToPlainText(child)) + .join(""); + } + + if (isTextNode(node)) { + return node.value; + } + + if (isStrongNode(node) || isEmphasisNode(node) || isDeleteNode(node)) { + return getNodeChildren(node) + .map((child) => this.nodeToPlainText(child)) + .join(""); + } + + if (isInlineCodeNode(node)) { + return node.value; + } + + if (isCodeNode(node)) { + return node.value; + } + + if (isLinkNode(node)) { + const linkText = getNodeChildren(node) + .map((child) => this.nodeToPlainText(child)) + .join(""); + return linkText ? `${linkText} (${node.url})` : node.url; + } + + if (isBlockquoteNode(node)) { + return getNodeChildren(node) + .map((child) => `> ${this.nodeToPlainText(child)}`) + .join("\n"); + } + + if (isListNode(node)) { + return getNodeChildren(node) + .map((item, i) => { + const prefix = node.ordered ? `${i + 1}.` : "-"; + const content = getNodeChildren(item) + .map((child) => this.nodeToPlainText(child)) + .join(""); + return `${prefix} ${content}`; + }) + .join("\n"); + } + + if (isListItemNode(node)) { + return getNodeChildren(node) + .map((child) => this.nodeToPlainText(child)) + .join(""); + } + + if (node.type === "break") { + return "\n"; + } + + if (node.type === "thematicBreak") { + return "---"; + } + + const children = getNodeChildren(node); + if (children.length > 0) { + return children.map((child) => this.nodeToPlainText(child)).join(""); + } + return getNodeValue(node); + } +} diff --git a/packages/adapter-imessage/src/types.ts b/packages/adapter-imessage/src/types.ts new file mode 100644 index 00000000..7bea4536 --- /dev/null +++ b/packages/adapter-imessage/src/types.ts @@ -0,0 +1,64 @@ +/** Thread ID components for iMessage */ +export interface iMessageThreadId { + /** Chat GUID (e.g., "iMessage;-;+1234567890") */ + chatGuid: string; +} + +/** Normalized message data from either local or remote SDK */ +export interface iMessageGatewayMessageData { + /** Attachments */ + attachments: iMessageAttachment[]; + /** Chat GUID this message belongs to */ + chatId: string; + /** Message timestamp (ISO string) */ + date: string; + /** Message GUID */ + guid: string; + /** Whether the message is from the current user */ + isFromMe: boolean; + /** Whether this is a group chat */ + isGroupChat: boolean; + /** Raw data from the SDK */ + raw?: unknown; + /** Sender identifier (phone/email) */ + sender: string; + /** Sender display name */ + senderName: string | null; + /** Source SDK */ + source: "local" | "remote"; + /** Message text content */ + text: string | null; +} + +export interface iMessageAttachment { + filename: string; + id: string; + mimeType: string; + size: number; +} + +/** + * Payload shape from imessage-kit's native webhook. + * The SDK POSTs the Message object directly when webhook config is set. + */ +export interface NativeWebhookPayload { + attachments: Array<{ + createdAt: string; + filename: string; + id: string; + isImage: boolean; + mimeType: string; + path: string; + size: number; + }>; + chatId: string; + date: string; + guid: string; + isFromMe: boolean; + isGroupChat: boolean; + isReaction: boolean; + sender: string; + senderName: string | null; + service: string; + text: string | null; +} diff --git a/packages/adapter-imessage/tsconfig.json b/packages/adapter-imessage/tsconfig.json new file mode 100644 index 00000000..8768f5bd --- /dev/null +++ b/packages/adapter-imessage/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "strictNullChecks": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/adapter-imessage/tsup.config.ts b/packages/adapter-imessage/tsup.config.ts new file mode 100644 index 00000000..d76807aa --- /dev/null +++ b/packages/adapter-imessage/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + external: ["@photon-ai/advanced-imessage-kit", "@photon-ai/imessage-kit"], +}); diff --git a/packages/adapter-imessage/vitest.config.ts b/packages/adapter-imessage/vitest.config.ts new file mode 100644 index 00000000..5b01228b --- /dev/null +++ b/packages/adapter-imessage/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json-summary"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts"], + }, + }, +}); diff --git a/packages/adapter-shared/src/adapter-utils.ts b/packages/adapter-shared/src/adapter-utils.ts index 3f2f3351..264186be 100644 --- a/packages/adapter-shared/src/adapter-utils.ts +++ b/packages/adapter-shared/src/adapter-utils.ts @@ -5,8 +5,14 @@ * to reduce code duplication and ensure consistent behavior. */ -import type { AdapterPostableMessage, CardElement, FileUpload } from "chat"; -import { isCardElement } from "chat"; +import type { + AdapterPostableMessage, + CardElement, + FileUpload, + PollElement, + PostablePoll, +} from "chat"; +import { isCardElement, isPollElement } from "chat"; /** * Extract CardElement from an AdapterPostableMessage if present. @@ -74,3 +80,39 @@ export function extractFiles(message: AdapterPostableMessage): FileUpload[] { } return []; } + +/** + * Extract PollElement from an AdapterPostableMessage if present. + * + * Handles two cases: + * 1. The message IS a PollElement (type: "poll") + * 2. The message is a PostablePoll with a `poll` property + * + * @param message - The message to extract the poll from + * @returns The PollElement if found, null otherwise + * + * @example + * ```typescript + * // Case 1: Direct PollElement + * const poll = Poll({ id: "q1", question: "Favorite?", options: ["A", "B"] }); + * extractPoll(poll); // returns the poll + * + * // Case 2: PostablePoll wrapper + * const message = { poll }; + * extractPoll(message); // returns the poll + * + * // Case 3: Non-poll message + * extractPoll("Hello"); // returns null + * ``` + */ +export function extractPoll( + message: AdapterPostableMessage +): PollElement | null { + if (isPollElement(message)) { + return message; + } + if (typeof message === "object" && message !== null && "poll" in message) { + return (message as PostablePoll).poll; + } + return null; +} diff --git a/packages/adapter-shared/src/index.ts b/packages/adapter-shared/src/index.ts index 23b94344..36404569 100644 --- a/packages/adapter-shared/src/index.ts +++ b/packages/adapter-shared/src/index.ts @@ -6,7 +6,7 @@ */ // Adapter utilities -export { extractCard, extractFiles } from "./adapter-utils"; +export { extractCard, extractFiles, extractPoll } from "./adapter-utils"; // Buffer conversion utilities export { diff --git a/packages/chat/src/index.ts b/packages/chat/src/index.ts index cde5a7c3..d54c3731 100644 --- a/packages/chat/src/index.ts +++ b/packages/chat/src/index.ts @@ -51,6 +51,13 @@ export const Section = _Section; export const toCardElement = _toCardElement; export const toModalElement = _toModalElement; +import { toPollElement as _toPollElement } from "./jsx-runtime"; +// Poll builders +import { isPollElement as _isPollElement, Poll as _Poll } from "./polls"; +export const isPollElement = _isPollElement; +export const Poll = _Poll; +export const toPollElement = _toPollElement; + // Modal builders import { fromReactModalElement as _fromReactModalElement, @@ -112,6 +119,7 @@ export type { FieldProps, ImageProps, LinkButtonProps, + PollProps, TextProps, } from "./jsx-runtime"; // Re-export mdast types for adapters @@ -182,6 +190,8 @@ export type { TextInputElement, TextInputOptions, } from "./modals"; +// Poll types +export type { PollElement, PollOptions } from "./polls"; // Types export type { ActionEvent, @@ -232,6 +242,7 @@ export type { PostableCard, PostableMarkdown, PostableMessage, + PostablePoll, PostableRaw, PostEphemeralOptions, RawMessage, diff --git a/packages/chat/src/jsx-runtime.ts b/packages/chat/src/jsx-runtime.ts index 4bdd3bac..200a0454 100644 --- a/packages/chat/src/jsx-runtime.ts +++ b/packages/chat/src/jsx-runtime.ts @@ -66,6 +66,8 @@ import { TextInput, } from "./modals"; +import { Poll, type PollElement } from "./polls"; + // Symbol to identify our JSX elements before they're processed const JSX_ELEMENT = Symbol.for("chat.jsx.element"); @@ -170,6 +172,13 @@ export interface SelectOptionProps { value: string; } +/** Props for Poll component in JSX */ +export interface PollProps { + id: string; + options: string[]; + question: string; +} + /** Union of all valid JSX props */ export type CardJSXProps = | CardProps @@ -184,7 +193,8 @@ export type CardJSXProps = | ModalProps | TextInputProps | SelectProps - | SelectOptionProps; + | SelectOptionProps + | PollProps; /** Component function type with proper overloads */ type CardComponentFunction = @@ -203,7 +213,8 @@ type CardComponentFunction = | typeof TextInput | typeof Select | typeof RadioSelect - | typeof SelectOption; + | typeof SelectOption + | typeof Poll; /** * Represents a JSX element from the chat JSX runtime. @@ -286,6 +297,7 @@ type AnyCardElement = | ModalElement | ModalChild | SelectOptionElement + | PollElement | null; /** @@ -384,6 +396,18 @@ function isSelectOptionProps(props: CardJSXProps): props is SelectOptionProps { return "label" in props && "value" in props && !("id" in props); } +/** + * Type guard to check if props match PollProps + */ +function isPollProps(props: CardJSXProps): props is PollProps { + return ( + "id" in props && + "question" in props && + "options" in props && + Array.isArray((props as PollProps).options) + ); +} + /** * Resolve a JSX element by calling its component function. * Transforms JSX props into the format each builder function expects. @@ -573,6 +597,17 @@ function resolveJSXElement(element: JSXElement): AnyCardElement { }); } + if (type === Poll) { + if (!isPollProps(props)) { + throw new Error("Poll requires 'id', 'question', and 'options' props"); + } + return Poll({ + id: props.id, + question: props.question, + options: props.options, + }); + } + // Default: Card({ title, subtitle, imageUrl, children }) const cardProps = isCardProps(props) ? props : {}; return Card({ @@ -668,6 +703,36 @@ export function toCardElement(jsxElement: unknown): CardElement | null { return null; } +/** + * Convert a JSX element tree to a PollElement. + * Call this on the root JSX element to get a usable PollElement. + */ +export function toPollElement(jsxElement: unknown): PollElement | null { + if (isJSXElement(jsxElement)) { + const resolved = resolveJSXElement(jsxElement); + if ( + resolved && + typeof resolved === "object" && + "type" in resolved && + resolved.type === "poll" + ) { + return resolved as PollElement; + } + } + + // Already a PollElement + if ( + typeof jsxElement === "object" && + jsxElement !== null && + "type" in jsxElement && + (jsxElement as PollElement).type === "poll" + ) { + return jsxElement as PollElement; + } + + return null; +} + export function toModalElement(jsxElement: unknown): ModalElement | null { if (isJSXElement(jsxElement)) { const resolved = resolveJSXElement(jsxElement); diff --git a/packages/chat/src/polls.test.ts b/packages/chat/src/polls.test.ts new file mode 100644 index 00000000..ae6b6313 --- /dev/null +++ b/packages/chat/src/polls.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { isPollElement, Poll } from "./polls"; + +describe("Poll", () => { + it("creates a PollElement with all fields", () => { + const poll = Poll({ + id: "fav-color", + question: "What's your favorite color?", + options: ["Red", "Blue", "Green"], + }); + + expect(poll).toEqual({ + type: "poll", + id: "fav-color", + question: "What's your favorite color?", + options: ["Red", "Blue", "Green"], + }); + }); + + it("creates a PollElement with two options", () => { + const poll = Poll({ + id: "yes-no", + question: "Do you agree?", + options: ["Yes", "No"], + }); + + expect(poll.type).toBe("poll"); + expect(poll.id).toBe("yes-no"); + expect(poll.question).toBe("Do you agree?"); + expect(poll.options).toEqual(["Yes", "No"]); + }); +}); + +describe("isPollElement", () => { + it("returns true for PollElement", () => { + const poll = Poll({ + id: "test", + question: "Q?", + options: ["A", "B"], + }); + expect(isPollElement(poll)).toBe(true); + }); + + it("returns false for non-poll objects", () => { + expect(isPollElement("hello")).toBe(false); + expect(isPollElement(null)).toBe(false); + expect(isPollElement(undefined)).toBe(false); + expect(isPollElement(42)).toBe(false); + expect(isPollElement({ type: "card" })).toBe(false); + expect(isPollElement({ type: "text" })).toBe(false); + }); +}); diff --git a/packages/chat/src/polls.ts b/packages/chat/src/polls.ts new file mode 100644 index 00000000..aed4185a --- /dev/null +++ b/packages/chat/src/polls.ts @@ -0,0 +1,77 @@ +/** + * Poll elements for cross-platform voting. + * + * Provides a builder API for creating polls that automatically + * convert to platform-specific formats: + * - iMessage (remote): Native polls via advanced-imessage-kit + * + * Supports both function-call and JSX syntax: + * + * @example Function API + * ```ts + * import { Poll } from "chat"; + * + * await thread.post( + * Poll({ id: "fav-color", question: "Favorite color?", options: ["Red", "Blue", "Green"] }) + * ); + * ``` + * + * @example JSX API (requires jsxImportSource: "chat" in tsconfig) + * ```tsx + * /** @jsxImportSource chat *\/ + * import { Poll } from "chat"; + * + * await thread.post( + * + * ); + * ``` + */ + +// ============================================================================ +// Poll Element Types +// ============================================================================ + +export interface PollElement { + /** Unique identifier, used as actionId for vote callbacks */ + id: string; + /** List of options to vote on */ + options: string[]; + /** The poll question */ + question: string; + type: "poll"; +} + +// ============================================================================ +// Type Guards +// ============================================================================ + +export function isPollElement(value: unknown): value is PollElement { + return ( + typeof value === "object" && + value !== null && + "type" in value && + (value as PollElement).type === "poll" + ); +} + +// ============================================================================ +// Builder Function +// ============================================================================ + +export interface PollOptions { + /** Unique identifier, used as actionId for vote callbacks */ + id: string; + /** List of options to vote on */ + options: string[]; + /** The poll question */ + question: string; +} + +export function Poll(options: PollOptions): PollElement { + return { + type: "poll", + id: options.id, + question: options.question, + options: options.options, + }; +} diff --git a/packages/chat/src/thread.ts b/packages/chat/src/thread.ts index c490ed29..68acde62 100644 --- a/packages/chat/src/thread.ts +++ b/packages/chat/src/thread.ts @@ -3,7 +3,12 @@ import type { Root } from "mdast"; import { cardToFallbackText } from "./cards"; import { ChannelImpl, deriveChannelId } from "./channel"; import { getChatSingleton } from "./chat-singleton"; -import { type CardJSXElement, isJSX, toCardElement } from "./jsx-runtime"; +import { + type CardJSXElement, + isJSX, + toCardElement, + toPollElement, +} from "./jsx-runtime"; import { paragraph, parseMarkdown, @@ -333,10 +338,18 @@ export class ThreadImpl> | AdapterPostableMessage; if (isJSX(message)) { const card = toCardElement(message); - if (!card) { - throw new Error("Invalid JSX element: must be a Card element"); + if (card) { + postable = card; + } else { + const poll = toPollElement(message); + if (poll) { + postable = poll; + } else { + throw new Error( + "Invalid JSX element: must be a Card or Poll element" + ); + } } - postable = card; } const rawMessage = await this.adapter.postMessage(this.id, postable); @@ -358,14 +371,21 @@ export class ThreadImpl> const { fallbackToDM } = options; const userId = typeof user === "string" ? user : user.userId; - // Convert JSX to card if needed + // Convert JSX to card or poll if needed let postable: AdapterPostableMessage; if (isJSX(message)) { const card = toCardElement(message); - if (!card) { - throw new Error("Invalid JSX element: must be a Card element"); + if (card) { + postable = card; + } else { + const poll = toPollElement(message); + if (!poll) { + throw new Error( + "Invalid JSX element: must be a Card or Poll element" + ); + } + postable = poll; } - postable = card; } else { // Safe cast: if not JSX, it must be AdapterPostableMessage postable = message as AdapterPostableMessage; @@ -646,17 +666,24 @@ export class ThreadImpl> async edit( newContent: string | PostableMessage | CardJSXElement ): Promise { - // Auto-convert JSX elements to CardElement + // Auto-convert JSX elements to CardElement or PollElement // edit doesn't support streaming, so use AdapterPostableMessage let postable: string | AdapterPostableMessage = newContent as | string | AdapterPostableMessage; if (isJSX(newContent)) { const card = toCardElement(newContent); - if (!card) { - throw new Error("Invalid JSX element: must be a Card element"); + if (card) { + postable = card; + } else { + const poll = toPollElement(newContent); + if (!poll) { + throw new Error( + "Invalid JSX element: must be a Card or Poll element" + ); + } + postable = poll; } - postable = card; } await adapter.editMessage(threadId, messageId, postable); return self.createSentMessage(messageId, postable); @@ -707,10 +734,17 @@ export class ThreadImpl> | AdapterPostableMessage; if (isJSX(newContent)) { const card = toCardElement(newContent); - if (!card) { - throw new Error("Invalid JSX element: must be a Card element"); + if (card) { + postable = card; + } else { + const poll = toPollElement(newContent); + if (!poll) { + throw new Error( + "Invalid JSX element: must be a Card or Poll element" + ); + } + postable = poll; } - postable = card; } await adapter.editMessage(threadId, messageId, postable); return self.createSentMessage(messageId, postable, threadId); @@ -797,6 +831,26 @@ function extractMessageContent(message: AdapterPostableMessage): { }; } + if ("poll" in message) { + // PostablePoll - generate fallback text from poll question + const fallbackText = message.poll.question; + return { + plainText: fallbackText, + formatted: root([paragraph([textNode(fallbackText)])]), + attachments: [], + }; + } + + if ("type" in message && message.type === "poll") { + // Direct PollElement + const fallbackText = message.question; + return { + plainText: fallbackText, + formatted: root([paragraph([textNode(fallbackText)])]), + attachments: [], + }; + } + // Should never reach here with proper typing throw new Error("Invalid PostableMessage format"); } diff --git a/packages/chat/src/types.ts b/packages/chat/src/types.ts index 9982e4aa..35b0866a 100644 --- a/packages/chat/src/types.ts +++ b/packages/chat/src/types.ts @@ -8,6 +8,7 @@ import type { CardJSXElement } from "./jsx-runtime"; import type { Logger, LogLevel } from "./logger"; import type { Message } from "./message"; import type { ModalElement } from "./modals"; +import type { PollElement } from "./polls"; // ============================================================================= // Re-exports from extracted modules @@ -987,7 +988,9 @@ export type AdapterPostableMessage = | PostableMarkdown | PostableAst | PostableCard - | CardElement; + | CardElement + | PostablePoll + | PollElement; /** * A message that can be posted to a thread. @@ -998,6 +1001,8 @@ export type AdapterPostableMessage = * - `{ ast: Root }` - mdast AST, converted to platform format * - `{ card: CardElement }` - Rich card with buttons (Block Kit / Adaptive Cards / GChat Cards) * - `CardElement` - Direct card element + * - `{ poll: PollElement }` - Poll with voting options (iMessage native polls) + * - `PollElement` - Direct poll element * - `AsyncIterable` - Streaming text (e.g., from AI SDK's textStream) */ export type PostableMessage = AdapterPostableMessage | AsyncIterable; @@ -1038,6 +1043,11 @@ export interface PostableCard { files?: FileUpload[]; } +export interface PostablePoll { + /** Poll element */ + poll: PollElement; +} + export interface Attachment { /** Binary data (for uploading or if already fetched) */ data?: Buffer | Blob; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42e00744..ad6b19a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -197,7 +197,7 @@ importers: version: link:../../packages/adapter-telegram ai: specifier: ^6.0.5 - version: 6.0.6(zod@4.3.3) + version: 6.0.6(zod@4.3.6) chat: specifier: workspace:* version: link:../../packages/chat @@ -320,6 +320,34 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/adapter-imessage: + dependencies: + '@chat-adapter/shared': + specifier: workspace:* + version: link:../adapter-shared + '@photon-ai/advanced-imessage-kit': + specifier: ^1.14.3 + version: 1.14.3(typescript@5.9.3) + '@photon-ai/imessage-kit': + specifier: ^2.1.2 + version: 2.1.2 + chat: + specifier: workspace:* + version: link:../chat + devDependencies: + '@types/node': + specifier: ^22.10.2 + version: 22.19.13 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.13)(lightningcss@1.30.2) + packages/adapter-linear: dependencies: '@chat-adapter/shared': @@ -884,102 +912,204 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.2': resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.2': resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.2': resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.2': resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.2': resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.2': resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.2': resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.2': resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.2': resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.2': resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.2': resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.2': resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.2': resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.2': resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.2': resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.2': resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} @@ -992,6 +1122,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.2': resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} @@ -1004,6 +1140,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.2': resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} @@ -1016,24 +1158,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.2': resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.2': resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.2': resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.2': resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} @@ -1648,6 +1814,16 @@ packages: cpu: [x64] os: [win32] + '@photon-ai/advanced-imessage-kit@1.14.3': + resolution: {integrity: sha512-i/WqwhvI9CwL9sd78YkV7PJmGftR2Z03GyIpRfMb6P6WKisHja+72wErSu66HCTLzRheDwazO44tJUbNobGoig==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: ^5.9.3 + + '@photon-ai/imessage-kit@2.1.2': + resolution: {integrity: sha512-xteMkPqqWkPLv40M9gA1HJGS/fHXIWzzXNCwRfnC4+bj120KMXMacT9zOSoEcGk4MA0pGXcUMQPE16MdB+Bf/g==} + engines: {node: '>=18.0.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2569,6 +2745,9 @@ packages: resolution: {integrity: sha512-ERcExbWrnkDN8ovoWWe6Wgt/usanj1dWUd18dJLpctUI4mlPS0nKt81Joh8VI+OPbNnY1lIilVt9gdMBD9U2ig==} engines: {node: '>= 18', npm: '>= 8.6.0'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2871,6 +3050,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@22.19.13': + resolution: {integrity: sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==} + '@types/node@25.3.2': resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} @@ -2976,9 +3158,23 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.0.18': resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} peerDependencies: @@ -2990,18 +3186,33 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} @@ -3132,9 +3343,23 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + botbuilder-core@4.23.3: resolution: {integrity: sha512-48iW739I24piBH683b/Unvlu1fSzjB69ViOwZ0PbTkN2yW5cTvHJWlW7bXntO8GSqJfssgPaVthKfyaCW457ig==} @@ -3156,6 +3381,10 @@ packages: botframework-streaming@4.23.3: resolution: {integrity: sha512-GMtciQGfZXtAW6syUqFpFJQ2vDyVbpxL3T1DqFzq/GmmkAu7KTZ1zvo7PTww6+IT1kMW0lmL/XZJVq3Rhg4PQA==} + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -3170,6 +3399,9 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3209,6 +3441,10 @@ packages: peerDependencies: react: '>=17.0.0' + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -3228,6 +3464,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chevrotain-allstar@0.3.1: resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} peerDependencies: @@ -3244,6 +3484,9 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -3525,6 +3768,18 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -3638,6 +3893,16 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -3679,6 +3944,11 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.27.2: resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} @@ -3727,6 +3997,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -3770,6 +4044,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filename-reserved-regex@3.0.0: resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3829,6 +4106,9 @@ packages: react-dom: optional: true + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -3949,6 +4229,9 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -4103,6 +4386,12 @@ packages: imsc@1.1.5: resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -4436,6 +4725,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4692,6 +4984,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + minimatch@10.2.2: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} @@ -4707,6 +5003,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -4756,6 +5055,9 @@ packages: engines: {node: ^18 || >=20} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + native-promise-only@0.8.1: resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} @@ -4811,6 +5113,10 @@ packages: sass: optional: true + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -4829,6 +5135,9 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-typedstream@1.4.1: + resolution: {integrity: sha512-W9zcPlI3RRPOmwaDjwRyr7aYLoJFbvLIIHluFM3I+KZjAlbyhG4L3jSTEJlQmDqrMRQlFVTmivgJWgFlvWXx2Q==} + npm-to-yarn@3.0.1: resolution: {integrity: sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4849,6 +5158,9 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} @@ -4948,9 +5260,16 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5012,6 +5331,12 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -5026,6 +5351,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -5049,6 +5377,10 @@ packages: '@types/react-dom': optional: true + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -5108,6 +5440,10 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5142,6 +5478,9 @@ packages: resolution: {integrity: sha512-YwXjATVDT+AuxcyfOwZn046aml9jMlQPvU1VXIlLDVAExe0u93aTfPYSeRgG4p9Q/Jlkj+LXJ1XEoFV+j2JKcQ==} engines: {node: '>= 18'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -5309,6 +5648,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -5320,6 +5665,14 @@ packages: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + engines: {node: '>=10.0.0'} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -5368,6 +5721,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -5383,6 +5739,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} @@ -5436,6 +5796,13 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -5468,10 +5835,22 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5530,6 +5909,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + turbo-darwin-64@2.8.12: resolution: {integrity: sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw==} cpu: [x64] @@ -5591,6 +5973,9 @@ packages: oxlint: optional: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -5705,6 +6090,42 @@ packages: vimeo-video-element@1.6.3: resolution: {integrity: sha512-pSBMYV+7KbChvtzuXGR3Sg7ZqaAZuTOnHrlZxrauIH2GWMLDnOEK5D0o7yWCcnswtQHVlIz6hXeJYbvQC+gAkw==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5745,6 +6166,31 @@ packages: yaml: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.0.18: resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5841,6 +6287,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -5873,6 +6322,10 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + youtube-video-element@1.8.1: resolution: {integrity: sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==} @@ -5882,6 +6335,9 @@ packages: zod@4.3.3: resolution: {integrity: sha512-bQ7Rxwfn04DCrTjjRfD9SavY2vWdmf3REjs/mkc1LdwI1KkcHClBRJmnvmA/6epGeqlHePtIRF1J4SrMMlW7IA==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -5894,12 +6350,12 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.3 - '@ai-sdk/gateway@3.0.5(zod@4.3.3)': + '@ai-sdk/gateway@3.0.5(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.3.3) + '@ai-sdk/provider-utils': 4.0.2(zod@4.3.6) '@vercel/oidc': 3.0.5 - zod: 4.3.3 + zod: 4.3.6 '@ai-sdk/provider-utils@3.0.21(zod@4.3.3)': dependencies: @@ -5908,12 +6364,12 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.3 - '@ai-sdk/provider-utils@4.0.2(zod@4.3.3)': + '@ai-sdk/provider-utils@4.0.2(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.1 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 4.3.3 + zod: 4.3.6 '@ai-sdk/provider@2.0.1': dependencies: @@ -6330,81 +6786,150 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.27.2': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.27.2': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.27.2': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.27.2': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.27.2': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.27.2': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.27.2': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.27.2': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.27.2': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.27.2': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.27.2': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.27.2': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.27.2': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.27.2': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.27.2': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.27.2': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.27.2': optional: true '@esbuild/netbsd-arm64@0.27.2': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.27.2': optional: true '@esbuild/openbsd-arm64@0.27.2': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.27.2': optional: true '@esbuild/openharmony-arm64@0.27.2': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.27.2': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.27.2': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.27.2': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.27.2': optional: true @@ -6457,8 +6982,7 @@ snapshots: dependencies: react: 19.2.3 - '@img/colour@1.0.0': - optional: true + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -6938,6 +7462,28 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.16.2': optional: true + '@photon-ai/advanced-imessage-kit@1.14.3(typescript@5.9.3)': + dependencies: + axios: 1.13.2 + consola: 3.4.2 + form-data: 4.0.5 + reflect-metadata: 0.2.2 + sharp: 0.34.5 + socket.io-client: 4.8.3 + typescript: 5.9.3 + zod: 4.3.6 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@photon-ai/imessage-kit@2.1.2': + dependencies: + node-typedstream: 1.4.1 + optionalDependencies: + better-sqlite3: 12.6.2 + '@pkgjs/parseargs@0.11.0': optional: true @@ -7869,6 +8415,8 @@ snapshots: transitivePeerDependencies: - debug + '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.1.0': {} '@streamdown/cjk@1.0.2(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.3)(unified@11.0.5)': @@ -8162,6 +8710,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@22.19.13': + dependencies: + undici-types: 6.21.0 + '@types/node@25.3.2': dependencies: undici-types: 7.18.2 @@ -8234,6 +8786,13 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -8243,6 +8802,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.13)(lightningcss@1.30.2))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.13)(lightningcss@1.30.2) + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.0.18 @@ -8251,23 +8818,48 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@4.0.18': dependencies: tinyrainbow: 3.0.3 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@4.0.18': dependencies: '@vitest/utils': 4.0.18 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/snapshot@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@4.0.18': {} + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 @@ -8295,13 +8887,13 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.3 - ai@6.0.6(zod@4.3.3): + ai@6.0.6(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 3.0.5(zod@4.3.3) + '@ai-sdk/gateway': 3.0.5(zod@4.3.6) '@ai-sdk/provider': 3.0.1 - '@ai-sdk/provider-utils': 4.0.2(zod@4.3.3) + '@ai-sdk/provider-utils': 4.0.2(zod@4.3.6) '@opentelemetry/api': 1.9.0 - zod: 4.3.3 + zod: 4.3.6 ansi-colors@4.1.3: {} @@ -8380,8 +8972,28 @@ snapshots: dependencies: is-windows: 1.0.2 + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + optional: true + + big-integer@1.6.52: {} + bignumber.js@9.3.1: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + botbuilder-core@4.23.3: dependencies: botbuilder-dialogs-adaptive-runtime-core: 4.23.3-preview @@ -8471,6 +9083,10 @@ snapshots: - bufferutil - utf-8-validate + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -8485,6 +9101,12 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -8523,6 +9145,14 @@ snapshots: dependencies: react: 19.2.3 + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} character-entities-html4@2.1.0: {} @@ -8535,6 +9165,8 @@ snapshots: chardet@2.1.1: {} + check-error@2.1.3: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 @@ -8557,6 +9189,9 @@ snapshots: dependencies: readdirp: 5.0.0 + chownr@1.1.4: + optional: true + ci-info@3.9.0: {} citty@0.2.1: {} @@ -8870,6 +9505,16 @@ snapshots: dependencies: character-entities: 2.0.2 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: + optional: true + deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -8980,6 +9625,25 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + + engine.io-client@6.6.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.18.3 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 @@ -9025,6 +9689,32 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.27.2: optionalDependencies: '@esbuild/aix-ppc64': 0.27.2 @@ -9101,6 +9791,9 @@ snapshots: eventsource-parser@3.0.6: {} + expand-template@2.0.3: + optional: true + expect-type@1.3.0: {} extend@3.0.2: {} @@ -9140,6 +9833,9 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + file-uri-to-path@1.0.0: + optional: true + filename-reserved-regex@3.0.0: {} filenamify@6.0.0: @@ -9193,6 +9889,9 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + fs-constants@1.0.0: + optional: true + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -9353,6 +10052,9 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: + optional: true + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -9603,6 +10305,12 @@ snapshots: dependencies: sax: 1.2.1 + inherits@2.0.4: + optional: true + + ini@1.3.8: + optional: true + inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -9891,6 +10599,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@11.2.6: {} @@ -10434,6 +11144,9 @@ snapshots: dependencies: mime-db: 1.52.0 + mimic-response@3.1.0: + optional: true + minimatch@10.2.2: dependencies: brace-expansion: 5.0.2 @@ -10446,6 +11159,9 @@ snapshots: minipass@7.1.3: {} + mkdirp-classic@0.5.3: + optional: true + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -10485,6 +11201,9 @@ snapshots: nanoid@5.1.6: {} + napi-build-utils@2.0.0: + optional: true + native-promise-only@0.8.1: {} negotiator@1.0.0: {} @@ -10543,6 +11262,11 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-abi@3.87.0: + dependencies: + semver: 7.7.3 + optional: true + node-domexception@1.0.0: {} node-fetch@2.7.0: @@ -10555,6 +11279,10 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-typedstream@1.4.1: + dependencies: + bplist-parser: 0.3.2 + npm-to-yarn@3.0.1: {} nypm@0.6.5: @@ -10569,6 +11297,11 @@ snapshots: obug@2.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + oniguruma-parser@0.12.1: {} oniguruma-to-es@4.3.4: @@ -10687,8 +11420,12 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -10743,6 +11480,22 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + optional: true + prettier@2.8.8: {} prop-types@15.8.1: @@ -10755,6 +11508,12 @@ snapshots: proxy-from-env@1.1.0: {} + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -10826,6 +11585,14 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -10895,6 +11662,13 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -10944,6 +11718,8 @@ snapshots: transitivePeerDependencies: - '@node-rs/xxhash' + reflect-metadata@0.2.2: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -11154,7 +11930,6 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - optional: true shebang-command@2.0.0: dependencies: @@ -11205,12 +11980,40 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + sisteransi@1.0.5: {} slash@3.0.0: {} smol-toml@1.6.0: {} + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + engine.io-client: 6.6.4 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.5: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 @@ -11270,6 +12073,11 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -11285,6 +12093,9 @@ snapshots: strip-bom@3.0.0: {} + strip-json-comments@2.0.1: + optional: true + strip-json-comments@5.0.3: {} style-to-js@1.1.21: @@ -11330,6 +12141,23 @@ snapshots: tapable@2.3.0: {} + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + term-size@2.2.1: {} thenify-all@1.6.0: @@ -11355,8 +12183,14 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + tinyrainbow@3.0.3: {} + tinyspy@3.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -11414,6 +12248,11 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + turbo-darwin-64@2.8.12: optional: true @@ -11460,6 +12299,8 @@ snapshots: jsonc-parser: 3.3.1 nypm: 0.6.5 + undici-types@6.21.0: {} + undici-types@7.18.2: {} undici@6.21.3: {} @@ -11581,6 +12422,34 @@ snapshots: dependencies: '@vimeo/player': 2.29.0 + vite-node@2.1.9(@types/node@22.19.13)(lightningcss@1.30.2): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.13)(lightningcss@1.30.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.19.13)(lightningcss@1.30.2): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.54.0 + optionalDependencies: + '@types/node': 22.19.13 + fsevents: 2.3.3 + lightningcss: 1.30.2 + vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): dependencies: esbuild: 0.27.2 @@ -11596,6 +12465,41 @@ snapshots: lightningcss: 1.30.2 tsx: 4.21.0 + vitest@2.1.9(@types/node@22.19.13)(lightningcss@1.30.2): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.13)(lightningcss@1.30.2)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.13)(lightningcss@1.30.2) + vite-node: 2.1.9(@types/node@22.19.13)(lightningcss@1.30.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.13 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): dependencies: '@vitest/expect': 4.0.18 @@ -11691,6 +12595,9 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrappy@1.0.2: + optional: true + ws@7.5.10: {} ws@8.18.3: {} @@ -11703,10 +12610,14 @@ snapshots: dependencies: sax: 1.4.4 + xmlhttprequest-ssl@2.1.2: {} + youtube-video-element@1.8.1: {} zod@3.25.76: {} zod@4.3.3: {} + zod@4.3.6: {} + zwitch@2.0.4: {} diff --git a/turbo.json b/turbo.json index 98bf25f6..79609815 100644 --- a/turbo.json +++ b/turbo.json @@ -10,6 +10,9 @@ "GOOGLE_CHAT_CREDENTIALS", "GOOGLE_CHAT_PUBSUB_TOPIC", "GOOGLE_CHAT_IMPERSONATE_USER", + "IMESSAGE_LOCAL", + "IMESSAGE_SERVER_URL", + "IMESSAGE_API_KEY", "BOT_USERNAME", "REDIS_URL" ],