Skip to main content
Glama
AravDharnikota

VoiceOS Instagram Integration

README.md
# VoiceOS Instagram Integration

Run your Instagram account by voice from the Mac notch. Ask how the account is doing, read comments and DMs, and publish a photo or a carousel by dropping it on the notch and saying what to caption it.

> **Business or Creator account required.** Instagram's API does not expose insights, comments, DMs, or publishing on a personal account — this is Meta's rule, not ours, and there is no way around it.
>
> To convert: Instagram app → your profile → **☰ menu** → **Settings and privacy** → **Account type and tools** → **Switch to professional account**. Pick **Creator** or **Business**, then follow the prompts. It is free, it is reversible, and it does not make your account public if it was private. Reconnect this integration afterwards.

---

## Setup

Six steps. Steps 3 and 4 are only needed if you want to **publish**; reading works without them.

### 1. Install dependencies

```sh
cd instagram
bun install
```

### 2. Connect Instagram through Composio

Composio is the auth and API transport this integration runs on.

1. Get an API key from the [Composio dashboard](https://app.composio.dev).
2. Add **Instagram** as an app in your Composio project. That creates the auth config the connect flow needs.

You approve the actual Instagram OAuth in step 6 — nothing to do here yet.

### 3. Create the photo relay bucket (Cloudflare R2)

Instagram never accepts image bytes. Meta fetches a **public URL** instead, with its own crawler. So a photo you drop on the notch is uploaded to your own R2 bucket, handed to Instagram as a link, and deleted seconds later.

In the [Cloudflare dashboard](https://dash.cloudflare.com) → **R2**:

1. **Create a bucket.**
2. Open it → **Settings** → **Public Development URL** → **Enable**. Copy that URL. The bucket must be public or Meta cannot fetch the photo.
3. **Manage API Tokens** → **Create API token**, scoped to that one bucket, with Object Read & Write. The secret is shown **once** — copy it now.
4. Optional but recommended: add a lifecycle rule to delete objects after 1 day. The integration deletes each photo itself; this is the backstop for the rare miss.

*Skip this whole step if you only want to read.* `account_pulse`, `post_insights`, `activity`, and `dm_thread` all work without a bucket. Only `create_post` and `schedule_post` need one.

### 4. Give the server the keys

Create a `.env` file in this folder:

```sh
COMPOSIO_API_KEY=

# Cloudflare R2 — publishing only, leave blank if you are read-only
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=
R2_PUBLIC_URL=
```

`R2_ACCOUNT_ID` is on the Cloudflare **R2 → Overview** page, top right. `R2_PUBLIC_URL` is the Public Development URL from step 3.

If VoiceOS prompts you for these as setup fields, that injection wins and the file is only a fallback for running the server standalone.

### 5. Install into VoiceOS

> **Quit VoiceOS first.** It holds `config.json` in memory and rewrites it on exit, so anything written while it is running is silently thrown away — with no error at all. The installer refuses to run if it sees VoiceOS alive.

```sh
osascript -e 'quit app "VoiceOS"'
python3 install-into-voiceos.py
open -a VoiceOS
```

That copies this folder into `~/Library/Application Support/VoiceOS/custom-mcps/`, carries the keys from step 4 across, and registers it. A plain `cp` is **not** enough — VoiceOS also needs two entries in `config.json` (one saying how to launch the server, one carrying the manifest), and writing those is most of what the script does. It backs up `config.json` first.

| Command | Does |
|---|---|
| `python3 install-into-voiceos.py --check` | Report what is installed. Changes nothing, safe while VoiceOS runs. |
| `python3 install-into-voiceos.py --update` | Re-copy after editing the source. The edit → test loop. |
| `python3 install-into-voiceos.py --update --deps` | Also refresh `node_modules`, after adding a dependency. |
| `python3 install-into-voiceos.py --remove` | Unregister and delete the installed copy. |

`--update` re-derives `confirmTools` from the manifest every time. That matters more than it looks: it is the list VoiceOS uses to decide which tools need a confirmation card, and a stale entry left over from a rename would let a post publish with **no card at all**.

### 6. Connect your account

Say *"How's my Instagram doing?"* If Instagram isn't linked yet you get a **Connect Instagram** card with an OAuth link. Approve it once and you're set.

If it says the account is **personal**, go back to the box at the top of this page.

---

## Tools

| Tool | Does | Try saying | Confirms first? |
|---|---|---|---|
| `instagram_account_pulse` | Profile, follower and post counts, recent reach and profile views, plus a grid of your latest posts | *"How's my Instagram doing?"* · *"How many followers do I have?"* | No |
| `instagram_post_insights` | Everything on one post: likes, comments, shares, saves, reach, impressions, and the image | *"How's my latest post doing?"* | No |
| `instagram_activity` | New comments on your posts, recent DMs, and any scheduled post that failed or is still queued | *"What's new on Instagram?"* · *"Did my scheduled post go out?"* | No |
| `instagram_dm_thread` | Recent messages with one person, and marks them seen | *"Show me my messages with Jonah"* · *"Did Kai reply?"* | No |
| `instagram_create_post` | Publishes a photo or carousel you dropped on the notch, with a caption you speak or one written for you | *"Post this picture on Instagram"* | **Yes** |
| `instagram_schedule_post` | Queues that same post to publish later, up to 24 hours out | *"Schedule this for tomorrow at 9am"* | **Yes** |

Drop the photos onto the notch and speak in the same breath — *"post these two with a caption about the hackathon"*. Both writing tools show you the photos, the caption, and (for a schedule) the exact time on a card before anything goes live.

---

## How your photos are handled

Worth reading once, because one step surprises people.

1. The photo is converted to JPEG on your Mac with `sips` (built into macOS). Instagram accepts nothing else.
2. It is uploaded to **your** R2 bucket under a random unguessable name, and is publicly readable for a few seconds. This is unavoidable: Meta's crawler is anonymous and cannot log in, so a public URL is the only way Instagram will take a photo.
3. Instagram fetches it and publishes the post.
4. The file is deleted from the bucket — on success **and** on failure, in a `finally` block. The lifecycle rule from step 3 is the backstop.

The bucket is yours. Nothing is stored on anyone else's server, and this integration keeps no copy of your photos.

**Scheduled posts run on your Mac**, not on Instagram's servers — Instagram has no scheduling API. A macOS launchd timer wakes at the minute you named and publishes then. So the Mac has to be on and awake. If it was shut when the post was due, the post is recorded as **missed** rather than published hours late, and `instagram_activity` tells you next time you ask.

---

## Not in v1

Cut deliberately, so you know before you try:

- **Sending DMs.** Meta blocks API DM sends through Composio's shared Instagram app — it returns "outside the allowed window" errors even with the 24-hour window provably open. The integration **reads** DMs but cannot send them. Reply in the Instagram app.
- **Replying to comments.** Same transport limitation.
- **Video and Reels.** Photos and photo carousels only. Video publishing needs a resumable upload path this build does not have.
- **Stories.** Not exposed by the toolkit.
- **Scheduling further than 24 hours out.** The ceiling is deliberate: every extra hour is another way a deferred job rots where nobody can see it — the photo gets deleted, a key is rotated, the connection is revoked.
- **Reading other accounts.** Your connected account only.

---

## Troubleshooting

| Symptom | Cause |
|---|---|
| *"Instagram isn't supported yet"* or the tools do not appear | The install did not register. Run `--check`; if it reports "not installed", re-run the installer **with VoiceOS quit**. |
| Everything returns the Connect card | Token expired, or the connection was revoked. Approve the OAuth link on the card again. |
| Publishing says the relay isn't set up | One of the five `R2_*` values is missing or blank. |
| Publishing fails with "Instagram rejected that image" | Wrong aspect ratio (Instagram allows 4:5 to 1.91:1), or over 8 MB after conversion. |
| A scheduled post never happened | Ask *"what's new on Instagram?"* — a failed or missed post is reported there with the reason. |

---

## Development

```sh
bun install
bun test          # 224 unit and failure-injection tests
bunx tsc --noEmit -p tsconfig.json
```

Three rules the tests exist to protect, worth knowing before editing:

- **stdout is the MCP wire.** One `console.log` in shipped code and VoiceOS cannot parse the JSON-RPC stream, so the integration silently vanishes from routing until the app restarts. Everything logs through `console.error`; `stdoutGuard.ts` is the first import in `server.ts` and rebinds the console for dependencies that do not.
- **Never build a shell string from a file path.** Photos come from the user dragging a file onto the notch. `execFile(cmd, [args])` only — a file named `holiday.png; rm -rf ~` is one opaque argument to `sips`, and `test/media-paths.test.ts` asserts it.
- **Tool names and descriptions must match the manifest exactly**, in both directions. `server.ts` and `voiceos.integration.json` are two copies of one contract.

`confirmations/post_composer.html` is the source of truth for the pre-publish card; the manifest carries a copy of it as a string. Edit the HTML and the copy must be regenerated, or the card shown before an irreversible publish is the stale one.