{"id":"add-imessage","name":"add-imessage","summary":"iMessageをNanoClawに追加する — 1チャネル、2バックエンド。ローカル(このMacのchat.dbはChat SDKブリッジ経由で、macOS + フルディスクアクセス)またはホスティングされたiMessage(photon.codes経由 — デバイスログインウィザード付きのネイティブSpectru…","body":"# Add iMessage\n\nNanoClaw talks to iMessage through a single **`imessage`** channel with two\npluggable backends:\n\n- **Local (this Mac)** — the Chat SDK bridge over `chat-adapter-imessage`,\n  reading this Mac's signed-in iMessage account (`chat.db`). macOS only; the\n  Node binary needs Full Disk Access.\n- **Hosted iMessage (via photon.codes)** — a native adapter over Photon's\n  `spectrum-ts` gRPC stream. The hosted service owns the iMessage line, so\n  there's no Mac relay, webhook, or public URL. Works on any OS, and a\n  device-login flow provisions everything for you.\n\nBoth register the same `imessage` channel type; only one runs per install.\nNanoClaw doesn't ship channels in trunk — this skill copies the unified\n`imessage` adapter in from the `channels` branch. Full reference:\n[docs/imessage.md](docs.md).\n\nThe mechanical steps under **Apply** carry `nc:` directive fences: an agent reads\nthe prose and applies them, and a parser can apply them deterministically from\nthe same document. Every directive is idempotent, so the whole skill is safe to\nre-run; anything a parser can't apply falls back to the prose beside it.\n\n## Apply\n\n### 1. Choose a backend\n\nPick the backend first — it decides which package gets installed and which\nwalkthrough runs below (the other backend's steps are skipped):\n\n```nc:prompt backend validate:^(local|hosted)$\nHow should iMessage run — `local` (this Mac's signed-in iMessage account; macOS only, needs Full Disk Access) or `hosted` (a managed line via photon.codes; works on any OS)?\n```\n\nThe local backend only works on a Mac — it reads this machine's iMessage\n`chat.db` directly, and there is no such database off macOS. On any other OS,\nstop here and choose `hosted` instead; otherwise you'd write a local config\nthat can never receive a message:\n\n```nc:run effect:check when:backend=local\n[ \"$(uname)\" = Darwin ]\n```\n\n### 2. Copy the adapter\n\nFetch the `channels` branch and copy the unified iMessage adapter and its tests\ninto `src/channels/`:\n\n```nc:copy from-branch:channels\nsrc/channels/imessage.ts\nsrc/channels/imessage.test.ts\nsrc/channels/imessage-registration.test.ts\n```\n\n### 3. Register the adapter\n\nAppend the self-registration import to the channel barrel (skipped if the line\nis already present). This one line is the skill's only reach-in into core:\n\n```nc:append to:src/channels/index.ts\nimport './imessage.js';\n```\n\n### 4. Install the chosen backend's package\n\nPinned to an exact version — the supply-chain policy rejects ranges and\n`latest`. Install only the chosen backend's package.\n\n**Local** — the Chat SDK iMessage adapter:\n\n```nc:dep when:backend=local\nchat-adapter-imessage@0.1.1\n```\n\n**Hosted** — Photon's Spectrum SDK:\n\n```nc:dep when:backend=hosted\nspectrum-ts@11.0.0\n```\n\n> Pin exactly. `spectrum-ts` ships breaking majors (v11 is what the adapter\n> targets); don't `@latest`. NanoClaw's pnpm gate (`minimumReleaseAge`) requires\n> a version ≥3 days old — both pins clear it. A fresher pin needs human sign-off\n> before a `minimumReleaseAgeExclude` entry (CLAUDE.md → Supply Chain Security).\n\n### 5. Build and validate\n\nBuild guards the typed `createChatSdkBridge(...)` core call used by the local\nbackend, and the registration test proves the channel is wired:\n\n```nc:run effect:build\npnpm run build\n```\n```nc:run effect:test\npnpm exec vitest run src/channels/imessage-registration.test.ts\n```\n\nBoth must be clean. `imessage-registration.test.ts` imports the real channel\nbarrel and asserts the registry contains `imessage` — it goes red if the\n`import './imessage.js';` line is missing or the barrel fails to evaluate. The\nadapter loads neither backend's SDK at import (hosted `spectrum-ts` only in\n`setup()`, local `chat-adapter-imessage` only in the factory), so the test\nneeds no package.\n\nFor the hosted backend, also run the full adapter suite — it includes an\nintegration block that exercises the real installed `spectrum-ts` (version,\nexports, builders) and auto-skips when the package is absent:\n\n```nc:run effect:test when:backend=hosted\npnpm exec vitest run src/channels/imessage.test.ts\n```\n\n## Local backend: Full Disk Access (macOS)\n\nThe adapter reads this Mac's `chat.db`, which requires Full Disk Access granted\nto the Node binary the host runs under. The Node path is buried deep (e.g.\n`~/.nvm/versions/node/v22.x.x/bin/node`), so open its folder in Finder to make\nthe drag-and-drop target obvious. Harmless off a desktop (SSH/headless) — it\njust no-ops:\n\n```nc:run effect:external when:backend=local\nopen \"$(dirname \"$(which node)\")\" 2>/dev/null || true\n```\n\nThen tell the user:\n\n```nc:operator when:backend=local\nGrant Full Disk Access to Node so iMessage can read your chat history:\n1. Open System Settings > Privacy & Security > Full Disk Access.\n2. Click +, then drag the \"node\" file from the Finder window that just opened.\n3. Toggle it on, then come back here.\n```\n\nStop and wait for the user to confirm Full Disk Access is granted before\ncontinuing.\n\nNow select the local backend in `.env`. The configure script owns this\nupsert-and-remove (a plain set-if-absent env write can neither replace a stale\nvalue nor delete a key, and a lingering hosted selector would shadow the\nchoice):\n\n```nc:run effect:external when:backend=local\nbash setup/channels/imessage-configure.sh local\n```\n\n## Hosted backend: device login (via photon.codes)\n\nThe provisioning flow needs the phone number you send iMessages from — it\nregisters that number with your project so the hosted line recognises you:\n\n```nc:prompt owner_handle normalize:trim validate:^\\+\\d{8,15}$ when:backend=hosted\nThe phone number you iMessage from, in E.164 format — + followed by country code and number, no spaces or dashes (e.g. +14155551234).\n```\n\nTell the user what's about to happen:\n\n```nc:operator when:backend=hosted\nConnect your hosted iMessage line (photon.codes):\n1. A login URL and a short code will print below.\n2. Open the URL in a browser, approve the device, and enter the code.\n3. Setup then registers your number and prints the iMessage line Photon assigned to it. Send one message from your phone to that line — a number only enters routing after it has texted its line once.\n4. Once the opt-in lands, setup finishes on its own and confirms your agent's iMessage number.\n```\n\nRun the device-login flow. It provisions the project, reuses its current secret\n(regenerating only when the API returns none), registers your number, prints\nthe line to text and waits until that message opts the number in, and surfaces\nthe iMessage number you'll use — writing\n`PHOTON_PROJECT_ID` + `PHOTON_PROJECT_SECRET` to `.env` and the assigned number\nto `data/photon-auth.json`:\n\n```nc:run effect:step when:backend=hosted\npnpm exec tsx scripts/photon-setup.ts setup --phone {{owner_handle}} --embedded\n```\n\nIf the login times out, the code expired (~30 min) — re-run the step; a stored\ntoken is reused. Check state any time with\n`pnpm exec tsx scripts/photon-setup.ts status`.\n\nThen select the hosted backend in `.env` — the Photon credentials already imply\nhosted, but the explicit selector avoids ambiguity if local keys linger:\n\n```nc:run effect:external when:backend=hosted\nbash setup/channels/imessage-configure.sh hosted\n```\n\n## Restart\n\nRestart the service so it loads the iMessage adapter and the backend config you\njust wrote, and wait for its CLI socket before wiring:\n\n```nc:run effect:restart\nbash setup/lib/restart.sh\n```\n\nFor the hosted backend, confirm the connection came up:\n`grep \"Photon channel connected\" logs/nanoclaw.log | tail -1`.\n\n## Resolve your iMessage handle\n\nThe agent greets you in the iMessage conversation tied to the handle you\nmessage from — that handle is both your identity and the conversation address.\nThe hosted flow already collected it above; for the local backend, resolve it\nnow (email works too — whatever iMessage recognises):\n\n```nc:prompt owner_handle validate:^(\\+\\d{8,15}|[^\\s@]+@[^\\s@]+\\.[^\\s@]+)$ when:backend=local\nThe phone number or email you iMessage from — a +E.164 number (e.g. +14155551234) or an email / Apple ID (e.g. you@icloud.com).\n```\n\n**Hosted first contact:** text your agent's iMessage number once (it was\nprinted above; also stored in `data/photon-auth.json`) before expecting any\nmessage from it. This first text is required, not just convenient — the hosted\nline can only message numbers that have already texted it (cold outbound is\nrejected with `Target not allowed for this project`). Tell the user:\n\n```nc:operator when:backend=hosted\nSend one text — anything — from your phone to your agent's iMessage number (printed above). The hosted line can only reply to numbers that have texted it first, so its welcome message needs yours to arrive first.\n```\n\niMessage is a native channel: it sends the raw handle as the conversation\naddress, with no channel prefix — so the messaging-group platform id is that\nhandle as-is:\n\n```nc:run capture:platform_id\necho \"{{owner_handle}}\"\n```\n\n`owner_handle` and `platform_id` are what the owner-wiring step needs. The\nwelcome iMessage goes out through the adapter once the service is running — on\nthe local backend that needs Full Disk Access granted (above); on the hosted\nbackend it goes out via your photon.codes line after your first text.\n\n## Next Steps\n\nIf you're in the middle of `/setup`, return to the setup flow now. Otherwise\n`/init-first-agent` stands up an agent on your iMessage DM, or `/manage-channels`\nwires it to an existing agent group.\n\n## Channel Info\n\n- **type**: `imessage` (one channel; the backend is local or hosted)\n- **terminology**: iMessage has 1:1 \"chats\" (DMs) and group chats. Photon\n  (hosted) calls each conversation a \"space\".\n- **platform-id-format**: DM = your bare handle (E.164 phone, or email for\n  local) — direct-addressable; the user id is `imessage:<handle>`. Group\n  (hosted) = the opaque Spectrum space id.\n- **how-to-find-id**: DMs use the counterpart's phone/email. Groups (hosted) are\n  discovered on first message —\n  `pnpm exec tsx scripts/q.ts data/v2.db \"SELECT platform_id, name FROM messaging_groups WHERE channel_type='imessage'\"`\n- **supports-threads**: no\n- **typical-use**: Interactive 1:1 chat — personal messaging\n- **default-isolation**: One agent per install. Multiple DMs with the same\n  operator can share an agent group; groups with other people should typically\n  use `isolated` session mode.\n\n### Hosted features\n\nMarkdown (native; `PHOTON_MARKDOWN=false` for plain text), file attachments in\nand out (inbound staged into the session inbox, capped by\n`PHOTON_MAX_INLINE_ATTACHMENT_BYTES`, default 20 MB), tapback reactions, read\nreceipts, typing indicators, and `ask_user_question` via `/approve` / `/reject`\nslash replies. Optional `.env`: `PHOTON_MARKDOWN`, `PHOTON_TELEMETRY`,\n`PHOTON_MAX_INLINE_ATTACHMENT_BYTES`, `PHOTON_DASHBOARD_HOST`,\n`PHOTON_SPECTRUM_HOST`. Full table in [docs/imessage.md](docs.md).\n\n## Troubleshooting\n\n**The backend answer is rejected.** It must be exactly `local` or `hosted`,\nlowercase. Local only exists on macOS — it reads this Mac's `chat.db` directly —\nso on any other OS the platform check stops you and hosted is the only path.\n\n**Local: outgoing works but nothing ever arrives.** Full Disk Access wasn't\ngranted to the *actual* Node binary the service runs under — with nvm the path\nchanges per Node version (`~/.nvm/versions/node/v22.x.x/bin/node`), so an old\ngrant silently stops covering a new binary. Re-open System Settings → Privacy &\nSecurity → Full Disk Access, add the binary at `$(which node)`, then restart\nthe service.\n\n**`spectrum-ts` not installed** (hosted) — re-run step 4\n(`pnpm install spectrum-ts@11.0.0`) and restart.\n\n**Device login times out** (hosted) — the code expires in ~30 min; re-run the\nlogin step (a stored token is reused).\n\n**`Target not allowed for this project`** (hosted) — intended: the line only\nmessages numbers that have texted it first. Text the agent's number once, then\nretry (a welcome DM queued before that first text simply fails delivery).\n\n**Your handle is rejected at the resolve step.** It must be a bare +E.164\nnumber (`+14155551234` — no spaces, dashes, or parentheses) or, on the local\nbackend, an email/Apple ID. Use the exact handle you actually send iMessages\nfrom — a number-vs-email mismatch means your messages never map to the wired\nconversation.\n\n**Adapter installed but silent.** Run\n`pnpm exec vitest run src/channels/imessage-registration.test.ts` — red means\nthe barrel import or the package install drifted, so re-run the Apply steps.\nIf green, confirm the backend connected (hosted:\n`grep \"Photon channel connected\" logs/nanoclaw.log`), restart the service\n(`bash setup/lib/restart.sh`), then check `logs/nanoclaw.error.log`.\n\nMore in [docs/imessage.md](docs.md).\n\n## Upgrading spectrum-ts (hosted)\n\n`spectrum-ts` is pinned exactly because it ships breaking majors. To upgrade,\nread the [release notes](https://github.com/photon-hq/spectrum-ts/releases) for\nevery version between the pins, bump the pin, reconcile\n`src/channels/imessage.ts` against the new typings, then `pnpm run build` and\n`pnpm exec vitest run src/channels/imessage.test.ts`. See\n[docs/imessage.md](docs.md).","author":"@nanocoai","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-imessage","license":"MIT","category":"writing","lang":"en","tokens":3297,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"apply-fixtures.json","size":847,"sha256":"fcad68d49f51ca59f41621bbf63be0200cde94a04eb3ba70453f0b2bb87da53b"},{"path":"docs.md","size":13252,"sha256":"ef592f5c9d047ae7ed0c50711687c26a5fdfdf6ab2fd249c3393ba4f16a9409a"},{"path":"REMOVE.md","size":2006,"sha256":"f0780a8240d5d57baba9a22887abcbcdb1ba6df139a087f69c16b9d1613e19d6"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["app.photon.codes","photon.codes","spectrum.photon.codes"]}}