# iMessage Module

Adds an `imessage` MCP server to the Teletraan conductor agent so it can read,
filter, and reply to iMessages on macOS. Runs as a stdio MCP that the agent's
Claude Code binary spawns — not a PM2 service.

## What it provides

Three MCP tools the agent can call:

- **`reply(chat_id, text, files?)`** — send an iMessage. `text` is chunked at
  word boundaries when long. `files` (array of absolute paths) attaches images
  or other files; non-`~/Pictures` paths are auto-staged because of the
  macOS 26 sandbox.
- **`chat_messages(chat_guid?, limit?)`** — read recent conversation history,
  scoped to allowlisted chats. Used by the agent to catch up on context.
- **`manage_allowlist(action, contact?, code?)`** — `list` (shows allowlist
  + pending pairing requests), `add`, `remove`, or `pair` (redeems a pairing
  code from an unknown sender). Intended for dashboard-driven config; the
  agent's skill prompt and this server both refuse to honor it when the
  request comes through an inbound iMessage (prompt-injection guard).

A companion skill **`/imessage:access`** is installed alongside the MCP at
`<installPath>/agents/<system>/.claude/skills/imessage-access/SKILL.md`. It
maps user-typed `/imessage:access pair <code>` / `list` / `add <contact>` /
`remove <contact>` commands to the right `manage_allowlist` calls.

Inbound iMessages from allowlisted contacts are pushed to the agent via the
shared FIFO at `/tmp/<system>-input` as channel envelopes:

```
<channel source="imessage" conversation_id="<chat.guid>" user_email="imessage:<sender>" image_path="/abs/path.png">
<message text>
</channel>
```

`image_path` only appears when the inbound message has an image attachment.

## Requirements

- macOS (tested on 26.5 / Tahoe, arm64)
- Messages.app signed in to an Apple ID with iMessage enabled
- **Full Disk Access** for the process running the MCP (so it can read
  `~/Library/Messages/chat.db`). For SSH-spawned agents grant FDA to
  `/usr/libexec/sshd-keygen-wrapper`; for locally-launched agents grant FDA
  to the relevant terminal binary.
- **Automation → Messages** — auto-prompts on first send; the user clicks OK.

## Installation

### Option A: as a Teletraan framework module

Selecting `imessage` during `node installer/index.js` will:

1. Copy `modules/imessage/` to `<installPath>/services/imessage/`
2. `npm install` (compiles `better-sqlite3` native binding)
3. Prompt for an allowlist (comma-separated phones / emails) via `setup.js`
4. Run `install-hook.js`, which:
   - Writes `<installPath>/services/imessage/data/access.json` with the
     allowlist and `dmPolicy: "allowlist"`
   - Adds an `imessage` entry to `<installPath>/agents/<system>/.mcp.json`
     pointing at `tsx server.ts`, with env:
     - `IMESSAGE_STATE_DIR` → the data dir
     - `ONE_INPUT_FIFO` → `/tmp/<system>-input`
   - Copies the `/imessage:access` skill to
     `<installPath>/agents/<system>/.claude/skills/imessage-access/`

### Option B: standalone (no framework)

If you just want the iMessage MCP without the rest of Teletraan:

```bash
# Get the module (clone the framework or download just modules/imessage)
git clone https://github.com/<...>/teletraan-framework.git
cd teletraan-framework/modules/imessage

node install-standalone.js
```

The standalone installer interactively prompts for:

| Prompt | Default | Notes |
|--------|---------|-------|
| Install location | the current dir | Where the MCP files live. |
| Allowlist | (empty) | Comma-separated phones / emails. |
| Target `.mcp.json` | `~/.claude/.mcp.json` | Existing entries are preserved; the file is created if missing. |
| FIFO path for --print mode | (empty) | Set this if Claude Code is fed via a named pipe. |
| Skills directory | `<dirname of .mcp.json>/skills` | Where the `/imessage:access` skill goes. |

It runs `npm install` (the `better-sqlite3` native compile is the slow part)
and writes `data/access.json` with mode 0600.

## Configuration

Runtime config lives in `<installPath>/services/imessage/data/access.json`:

```json
{
  "dmPolicy": "allowlist" | "pairing" | "disabled",
  "allowFrom": ["+15551234567", "stephen@example.com"],
  "groups": {},
  "pending": {},
  "textChunkLimit": 10000,
  "chunkMode": "newline"
}
```

- `dmPolicy: "allowlist"` — silently drops anyone not in `allowFrom`. Default.
- `dmPolicy: "pairing"` — first message from an unknown sender gets a one-time
  pairing code; redeem via the `/imessage:access` Claude Code skill to add
  them to the allowlist.
- `dmPolicy: "disabled"` — drop all inbound DMs.

The server reloads `access.json` on every gate decision, so manual edits take
effect on the next inbound message without a restart.

## Environment variables

| Var | Default | Purpose |
|-----|---------|---------|
| `IMESSAGE_STATE_DIR` | `~/.claude/channels/imessage` | Where `access.json` lives. |
| `ONE_INPUT_FIFO` | (unset) | FIFO path for `--print` mode inbound delivery. When unset, falls back to MCP notifications. |
| `IMESSAGE_DB_PATH` | `~/Library/Messages/chat.db` | Override chat.db path (for tests). |
| `IMESSAGE_ALLOW_SMS` | `false` | Set to `"true"` to deliver SMS/RCS in addition to iMessage. Off by default since SMS sender IDs are spoofable. |
| `IMESSAGE_APPEND_SIGNATURE` | `true` | Set to `"false"` to suppress the `\nSent by Claude` signature on outbound text. |
| `IMESSAGE_ACCESS_MODE` | (unset) | Set to `"static"` to snapshot access at boot and never re-read (downgrades `pairing` to `allowlist`). |

## Security notes

- The allowlist enforces who can reach the agent. Anyone whose handle isn't
  in `allowFrom` (and isn't in a configured group) gets dropped silently.
- The `manage_allowlist` tool refuses to run when the request comes through
  an iMessage channel — sender can't bootstrap themselves in.
- Permission requests (Claude Code's claude/channel/permission flow) are
  routed only to your self-chat, so only the device owner can approve them.
- File attachments are size-capped at 100MB and refused if they live inside
  the module's state directory (don't leak access.json or pending codes).
