Skip to content

feat: talk to a Dot over iMessage via a local macOS bridge - #53

Open
jibraaan wants to merge 1 commit into
CopilotKit:mainfrom
jibraaan:pr/imessage
Open

jibraaan wants to merge 1 commit into
CopilotKit:mainfrom
jibraaan:pr/imessage

Conversation

@jibraaan

@jibraaan jibraaan commented Oct 3, 2026

Copy link
Copy Markdown

Workflow

Text your Dot from your phone. When OpenDots runs natively on a Mac signed in to Messages, it answers allowlisted phone numbers and Apple ID emails. Each sender gets one persistent Dot conversation, which is also visible in the web app. Texting /new starts a fresh one.

Setup is IMESSAGE_HANDLES=+15551234567,me@icloud.com, with optional IMESSAGE_DOT_ID and IMESSAGE_DB_PATH, plus Full Disk Access for the app that launches OpenDots. It's documented in docs/IMESSAGE.md.

How it works

  • The server starts a small bridge only when IMESSAGE_HANDLES is set and the platform is darwin. On other platforms the setup status reads unsupported.
  • Receiving: it polls ~/Library/Messages/chat.db, opened read-only, for new incoming messages in one-to-one chats (chat.style = 45). Group chats and the owner's own messages are ignored. When message.text is empty, the body is decoded from the archived attributedBody.
  • Answering: each turn runs through the existing Platform.turn(), the same server-side path scheduled tasks use. The model gets nothing new: approval-gated client tools aren't available in headless turns.
  • Replying: replies go out with osascript. The handle and reply text are passed as argv, never inserted into the AppleScript source. Markdown is converted to readable plain text first.

Safety and robustness:

  • History is never answered: the first start begins at the newest message.
  • The cursor is persisted and advanced before replying, so a failure never double-answers. Messages older than an hour are skipped after downtime.
  • Replies echoed back when someone messages their own Apple ID are ignored.
  • When OpenDots is paused, the Dot replies that it is paused. Failed turns get a generic reply, and only a sanitized failure name is logged.
  • Missing Full Disk Access surfaces as needs Full Disk Access in Settings instead of crashing the server.

Verification

  • npm run check-format, lint, typecheck, test (173 passing, 9 new) and build all pass.
  • tests/imessage.test.ts builds a fixture chat.db with the real table layout. It covers:
    • one-to-one vs. group chats, and messages from the owner
    • archived bodies, both short and long length encodings
    • that Apple's nanosecond timestamps convert correctly. This fixture caught a real bug: the raw values overflow JavaScript numbers, so the conversion now happens in SQL.
    • that the AppleScript receives an injection-shaped reply as an argument only
    • the allowlist, batching and per-sender threads
    • stale-message and echo skipping
    • /new, the paused reply, recovery from a failed turn, and the no-access status
  • Not yet verified on a real Messages account: the dev machine hadn't granted Full Disk Access, and no model keys were configured.

🤖 Generated with Claude Code

A local macOS bridge, started by the server when IMESSAGE_HANDLES is set.
It reads new incoming one-to-one messages from the Messages database
(read-only) and replies through Messages via AppleScript, with the reply
passed as an argument rather than interpolated into the script.

- Only allowlisted phone numbers / Apple ID emails are answered.
- One persistent Dot conversation per sender; "/new" starts another.
- History is never answered; the cursor advances before replying so a
  failure never double-answers; stale (>1h) messages and echoes of the
  bridge's own replies are skipped.
- Archived attributedBody text is decoded; Apple nanosecond dates are
  converted in SQL to avoid overflowing JS numbers.
- Settings show the bridge status, including missing Full Disk Access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant