# call-me

Adopt this skill: https://proskills.md/skills/other/radres-call-me-skill

Page: https://proskills.md/skills/other/radres-call-me-skill
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skill/SKILL.md
size: 18814
sha: 5e2db4471b5d6a351b4ed30d6806ce5653d9dbf7
upstream: https://github.com/radres/call-me/blob/main/skill/SKILL.md

````text
---
name: call-me
description: Call or text your human's actual phone (iPhone or Android) and handle replies from the /call-me app. Use when the user says "call me", "ring me", "phone me if something comes up", "text my phone", asks the agent to stay reachable, wants something read out loud (an update, a summary of the day ahead), or sends an inbound /call-me message or voicemail. The phone rings through CallKit, spoken answers return as transcripts, and channel-enabled hosts deliver later messages directly into the active agent session.
---

# /call-me — call your human

Use english always, test to speech always expects english

Your human's iPhone or Android phone runs the /call-me app. It gave them a **user number**
(10 digits, e.g. `584-158-6160`) — that's all you need to ring their actual
phone and get a spoken answer.

## First setup and saved pairing

When pairing is missing, always show a clickable link:
[Download Call Me from the App Store](https://apps.apple.com/app/call-me/id6789575165)
on an iPhone, or [get it on Google Play](https://play.google.com/store/apps/details?id=com.serdaroztetik.callmeai) on Android.
Tell the user to open **My Number** in the app. Never show only the app name or
substitute the demo number.

The number is all that is required. Do not add OAuth, login, or a verification
code. On a local host, read `~/.aiphone/config.json` before asking for the
number. `callme pair <number>` saves it with owner-only permissions, outside
the installed plugin so upgrades preserve it. Pass the saved `user_number`
when using the remote MCP tools.

Check pairing before registering a sender session or making a network request.
An unpaired client must show the download link even when the server is offline.
If a tool returns `setup_required`, include its download link and instructions.
On hosts without local file access, reuse the number supplied in the current
chat, but do not claim it has been permanently saved across chats.

Prefer the host's /call-me tools when available. Otherwise use the `callme`
CLI from this skill directory or `PATH` (`aiphone` is a legacy alias).

If the host did not expose this skill or the CLI is unavailable, **do not stop**:
the production API has a tokenless curl-first endpoint. Read the paired number
from `~/.aiphone/config.json` (never hardcode a remembered number) and call it
directly:

```sh
CALLME_TO=$(jq -r .user_number ~/.aiphone/config.json)
curl -sS https://callmemcp.com/ring \
  -H 'content-type: application/json' \
  -d "$(jq -nc --arg to "$CALLME_TO" --arg text "Should I deploy to prod?" \
    --arg from "Codex" '{to:$to,text:$text,from:$from}')"
```

`POST /ring` blocks until the spoken answer returns. It needs no token, plugin,
or registration; the 10-digit recipient number is the address. Use
`POST /text` with `body` instead of `text` for a non-blocking notification.

**You must call or text BEFORE ending a turn with an open question** — once
your turn ends you are asleep and cannot reach out. Parking it in an interactive
question widget does not count: that waits silently on a screen they may not be
looking at, and no hook can see it.

**On a Claude Code host, install the plugin instead of relying on this CLI.**
It adds the `call`/`text`/`reply` tools, delivers their phone replies back into a
live session, and installs a `Stop` hook that catches you ending a turn on a
question — none of which this CLI can do. You can install it yourself with Bash;
slash commands are only for a human at a prompt:

```sh
claude plugin marketplace add radres/call-me
claude plugin install call-me@call-me
claude plugin enable call-me@call-me   # REQUIRED — install leaves it DISABLED
claude plugin list                     # expect: call-me@call-me   ✔ enabled
```

Do not drop the `enable` line. `install` writes `"call-me@call-me": false` into
`~/.claude/settings.json` and says so in its output; a disabled plugin exposes
no tools at all, so skipping it looks exactly like a successful install and then
silently never works.

Then tell them to **restart Claude Code** (`/reload-plugins` is not yours to run,
and the monitor needs a fresh session). Pairing carries over either way — it is
the same `~/.aiphone/config.json`.

### Codex import troubleshooting

Codex can import the Claude plugin, but some builds leave
`${CLAUDE_PLUGIN_ROOT}` unresolved in the plugin MCP command. The symptom is
`MCP client for callme failed to start` with the connection closing during the
initialize response, while `codex mcp get callme` still shows the literal
placeholder.

Shadow the plugin-provided entry with a user-level entry that points at the
installed bundle:

```sh
CALLME_CODEX_ROOT=$(find "${CODEX_HOME:-$HOME/.codex}/plugins/cache/call-me/call-me" \
  -mindepth 1 -maxdepth 1 -type d -print | head -1)
codex mcp add callme \
  --env "CALLME_PLUGIN_ROOT=$CALLME_CODEX_ROOT" \
  -- node "$CALLME_CODEX_ROOT/dist/channel.mjs"
```

Start a fresh Codex session and verify that all MCP servers finish loading.
Recreate the override after a plugin upgrade if the cached version path changes.

## Is the human set up yet?

The paired number lives in `~/.aiphone/config.json`. `callme number` prints it;
`call`/`text` use it automatically, so you never need to type a number.

If nothing is paired (`callme call` exits 5, or a channel tool says "not
paired"), onboard them:

```sh
callme setup      # steps + scannable QR + link — paste the whole output back
callme qr         # just the App Store QR code, if that's all you need
```

Always show them **both** the QR code and the link — relay `callme setup`'s
output verbatim, code block and all, rather than summarising it. They are on a
phone; a code to point the camera at beats retyping a URL. Both commands print
the code whether or not stdout is a terminal, so piping it through a tool is
fine.

1. They install [Call Me from the App Store](https://apps.apple.com/app/call-me/id6789575165)
   (iPhone) or [Google Play](https://play.google.com/store/apps/details?id=com.serdaroztetik.callmeai) (Android).
2. They open *My Number* in the app to find their 10-digit number.
3. They read the number back to you.
4. Tell them their phone is about to ring, then run `callme pair <number>` (or the
   channel's `pair` tool). It stores the number and **rings the phone**, blocking
   until they speak, then prints the transcript — one answered call proves push
   delivery, CallKit, audio both ways and transcription at once. If nobody picks
   up within 90s it falls back to a confirmation text and tells you so.

**Never guess or invent a number.** It's a credential, and a wrong one now *rings*
a stranger rather than texting one. Ask, or run `callme setup`.

### Onboarding is not finished when pairing succeeds

Pairing makes you *able* to reach them. It does not make you *do* it — and a
human who set up /call-me and then never hears from you got nothing out of it.
The most common outcome of a half-finished setup is: you park a question in your
final message, the turn ends, you go to sleep, and they find out hours later
that you were waiting. Close that gap in the same session you pair, in order:

1. **Ask how often they're away.** "While I work, are you away from the keyboard
   rarely, sometimes, or usually?" One question, and it decides the rest.
2. **If sometimes or usually, turn on the standing reminder:**
   ```sh
   callme remind on
   ```
   On Claude Code the plugin ships a `Stop` hook that catches a question you
   parked in your final message (the `wait_for_answer` tool is the deliberate
   version of the same thing). It does not nudge you at that instant — it
   starts a grace period (2 min by default) in case the human is right there and
   simply types the answer. Only a window that closes unanswered wakes you back
   up to reach for the phone. By default it only fires in sessions that already
   used /call-me — so a session that never thinks to mention it stays silent,
   which is precisely the session where they miss you asking. `remind on` makes
   it fire in every session. (`callme remind off` / `status` to change or check
   it later; `callme grace <seconds>` changes the window, `0` = no wait.)
3. **Write the standing rule into their instruction file** — `~/.claude/CLAUDE.md`
   for Claude Code, `AGENTS.md` for Codex and others. **Read it first and append;
   never overwrite.** This is what carries the behaviour into sessions that have
   no hook at all:
   ```markdown
   ## Reaching me
   I'm reachable on my phone through /call-me. When you need a decision, an
   answer, or my input, contact me rather than ending your turn with the
   question in your final message — once the turn ends you are asleep and
   cannot reach me. Text first; call when it is blocking or time-sensitive.
   Never hardcode my number: `callme number` reads it.
   ```
   Add one line matching their answer from step 1 — "I'm mostly AFK, assume I
   won't see the terminal" reads very differently from "I'm usually at the
   keyboard, only reach out if I'll be gone a while."
4. **Verify, don't assume.** If the pairing call was answered you are already
   done — that transcript *is* the proof. Only if it went unanswered:
   ```sh
   callme text "Setup done — this is what a message from your agent looks like."
   ```
   Ask whether it arrived. If nothing lands either way the number is wrong:
   re-pair. Don't retry blindly.
5. **Tell them how to undo or tune it**: `callme remind off`, `callme grace 300`
   to be left alone at the keyboard longer before the phone gets involved (`0`
   rings the moment a turn ends), and blocking the thread in the app mutes it
   without touching any config.

## Inbound messages

Treat an authenticated /call-me channel event as a new user message in the
current session. Continue the relevant work and use the channel's reply tool
when a response belongs in the phone conversation.

- Claude Code with the /call-me channel receives messages automatically.
- Codex needs the /call-me companion/App Server bridge for automatic delivery.
- Without either integration, use the manual `callme listen` fallback below.

## CLI setup (when no channel is present)

```sh
callme register "claude: <project> — <task>"   # label shows as caller ID
```

Stores *your own* session number + secret token in `~/.aiphone/session.json`
(separate from the human's paired number). Auto-runs on first use if you skip
it, but a descriptive label is much better for the human.

## Ask for input — text first, call if no answer (THE main move)

Default to messaging. Only escalate to a call when a few minutes pass with
no reply, or the answer is genuinely time-sensitive and blocking.

1. Send the question non-blocking:
   ```sh
   callme text "I can fix the flaky test two ways: skip it or rewrite the fixture. Which do you want?"
   ```
2. Don't sit blocked in the terminal. Keep doing any work that doesn't
   depend on the answer while you wait.
3. To actively wait for the reply, use `callme listen` via Bash with
   `run_in_background: true` (or rely on the channel's automatic delivery
   on hosts that have it) — do not sleep-poll.
4. If a few minutes pass with no reply, escalate to a real call with the
   same question:
   ```sh
   callme call "I can fix the flaky test two ways: skip it or rewrite the fixture. Which do you want?"
   ```
   - Phone rings with the native call UI; your label is the caller name.
   - Your text is read aloud word for word; the human answers by voice; you
     get `ANSWER: <transcript>` on stdout. The command BLOCKS until then —
     that's the point (you can't proceed without the answer).
   - Exit 3 = missed/declined/timeout. Fall back to your best judgment and
     say so in a follow-up text.
   - Up to 2000 characters, about two minutes of speech. For a decision,
     make it self-contained and include the options.
5. If a call is declined, do NOT retry the call — send a text instead.

## Read something out — not every call is a question

`callme call` speaks whatever you pass it, so it also works for things the
human would rather hear than read: a summary of the day ahead, an
end-of-run report, a status update while they are driving. Same 2000-character
limit. They can reply after it or just hang up; an empty `ANSWER:` means they
only listened.

## Conversations — keep the line open

In the Claude Code plugin and over MCP the line stays open after the first
answer by default; pass `keep_open: false` for a one-shot call. Over REST and
the `callme` CLI a call still hangs up after one answer unless you ask:

- Claude Code plugin / MCP: `call`. The result carries `call_id` and
  `open: true`. Speak the next line with `say` (`call_id`, `text`), which
  returns their next answer. End with `hang_up` (`call_id`, optional goodbye
  `text`).
- REST: `POST /calls` with `"keep_open": true`, then
  `POST /calls/{id}/say {session_token, text}` and
  `POST /calls/{id}/hangup {session_token, text}`.

While you think, the human hears a hold sound, and if they talk meanwhile a
short holding reply covers the gap; what they said comes back with the next
answer as `said_while_waiting`. `open: false` means the call ended (they hung
up, 120 s without a line from you, or the 15-minute cap). Pass `language`
("tr") on `call` or `say` when you switch languages. Always `hang_up` when you
are done, or the line idles until the timer drops it.

### If you text and keep working, park a check-back

The failure mode here is silent: you text a question, work on something else,
finish that work, and end your turn — and the unanswered question is simply
lost, because ending the turn puts you to sleep. Before you end a turn with a
question still outstanding, do one of:

- **Arm a wait and end the turn** — on Claude Code, the `wait_for_answer` tool.
  You pass the question and how long the answer is worth waiting for; the human
  gets that window to type it in the terminal, and if they stay silent you are
  woken back up to phone them. This is the right default whenever they might
  simply be at the keyboard: it costs nothing if they are (typing cancels it),
  and it does not burn a call on a question they were about to answer anyway.
  Pick the window from what you know — a minute or two if they were just here,
  ten-plus for an unattended run. Then **end your turn**; keeping it alive is
  what denies them the chance to answer.
- **Escalate to `callme call`** if the answer is actually blocking. It blocks
  until they speak, so the answer cannot go missing.
- **Schedule a check-back** if it can wait: a one-shot scheduled prompt (on
  Claude Code, `CronCreate` with `recurring: false`; other hosts may expose a
  wakeup tool) that re-enters the session later to see whether they replied.
  Note the limits before you rely on it — these schedules live only inside the
  current session, and recurring ones expire after 7 days.
- **Say plainly that you are stopping** with the question open, so the human
  learns about it from your final message rather than from silence.

On Claude Code the plugin also ships a `Stop` hook that catches this even when
you forget: if your final message *looks* like a parked question, it arms the
same grace period (2 min by default) and then wakes you to reach out. Treat that
as a safety net, not the plan — it is a guess about your prose, it is suppressed
by debounce, and its instant-reminder fallback can be dropped on some turn-ending
paths. `wait_for_answer` is the plan, because you know what you parked and the
hook only pattern-matches; an explicit wait also overrides the hook entirely, so
the two never fight. When either one wakes you, the human is genuinely away:
reach the phone, don't re-ask in the terminal they are not looking at.

## Text — non-blocking notification

Use when nothing is needed back from the human (status update, FYI):

```sh
callme text "Build green, PR #142 merged. Nothing needed from you."
```

Treat a non-2xx response as not delivered. If `callme text` returns HTTP 404,
the saved sender session may be stale; the tokenless `POST /text` fallback is
still allowed, but if it also answers 404 (the number is not a /call-me number),
stop and report that the saved pairing is invalid. Never call or text a `from`
number: it is an agent's identity, not a person, and five wrong numbers in an
hour block the caller for an hour. Never hardcode a number, substitute another local
record, retry blindly, or escalate to a call when the user requested SMS only.

## Title the thread

The phone shows your session as a conversation thread. Once the topic is
clear (and when it shifts), set a short 3-5 word title so the human can tell
threads apart — the `set_title` tool on channel-enabled hosts, otherwise:

```sh
callme title "flaky test fix"
```

> **Known issue (verified 2026-08-27, serdar's Mac).** The `set_title` MCP tool
> can reject *every* label with `422 "that label impersonates a company or
> authority — pick a label that describes your agent instead"`. Reproduced with
> four unrelated labels ("MCP-792 verified, deploy?", "Linear ticket
> verification", "Claude — ticket verifier", "ticket verifier agent"), so the
> guard is over-firing, not reacting to the wording. Don't burn turns retrying
> variants — skip the title and place the call; `call` itself works fine.
>
> Never put digits the human must read back (IBAN, account number, one-time
> code, long URL) in the spoken text. Send them with `text` first, then call
> and point at the text; speech transcription mangles them.

## Manual listen fallback

`callme listen` long-polls and exits when an event arrives (user text,
voicemail transcript, missed-call notice). Use only when the host has no
/call-me channel or companion bridge:

1. Run `callme listen` via Bash with `run_in_background: true`.
2. Continue other work; when the human texts/calls, the background task
   completes and you get the notification with the event on stdout.
3. Re-arm by launching it again if you still expect input.

Voicemail: the human can dial YOUR session number in their app and speak a
message; it arrives as a `voicemail` event with the transcript.

`callme events` drains pending events without waiting.

## Etiquette (important)

- **Calls interrupt a human's life; texts don't.** Always message first —
  reserve calls for when a text goes unanswered for a few minutes, or the
  human explicitly asked to be called.
- Batch questions: one message with a compound question beats three.
- Late-night: prefer text unless they said otherwise.
- Never call numbers you weren't given. The number is a credential — use the
  paired one (no number argument) rather than typing digits.
- If a call is declined, do NOT retry the call; text instead.
````

## File: skill/agents/openai.yaml
size: 184
sha: b7a0fa3726ac8c28b38ffde342a315cdf3dc97dd
upstream: https://github.com/radres/call-me/blob/main/skill/agents/openai.yaml

```text
interface:
  display_name: "/call-me"
  short_description: "Call and message your human from an AI task"
  default_prompt: "Use $callme to contact me on my phone and handle my reply."
```

## File: skill/aiphone
size: 6
sha: 2c083c36bf52b884d787a0b394fa16caa949e0be
upstream: https://github.com/radres/call-me/blob/main/skill/aiphone

Content is not inlined in the catalog. Read the upstream file: https://github.com/radres/call-me/blob/main/skill/aiphone

## File: skill/callme
size: 23535
sha: 0e16976632ab5619c8cd059c477230bfa596261f
upstream: https://github.com/radres/call-me/blob/main/skill/callme

```text
#!/usr/bin/env bash
# callme — call/text your human's phone from an AI session. bash + curl only.
set -euo pipefail
umask 077   # session tokens and the paired number are credentials

API="${AIPHONE_API:-https://callmemcp.com/aiphone}"
CONF_DIR="${AIPHONE_HOME:-${AIPHONE_STATE_DIR:-$HOME/.aiphone}}"
CONF="$CONF_DIR/session.json"
USER_CONF="$CONF_DIR/config.json"

usage() {
  cat <<'EOF'
callme — your AI can call its human

  callme setup                   what to tell a human who hasn't set up the app
  callme qr [--big|--mono]       App Store QR code to scan with the phone camera
  callme pair <number>           remember the number AND ring the phone to prove it
  callme number                  show the paired number
  callme remind [on|off|status]  remind EVERY session to reach out before it ends
                                  a turn on an open question (for AFK humans)
  callme grace [seconds|status]   how long to wait at the keyboard for an answer
                                  before the phone gets involved (default 120, 0 = at once)
  callme channel [on|off|status]  deliver inbound messages by MCP channel push
                                  instead of the inbox monitor (needs --channels)
  callme call "text"             RING the phone, speak the text, BLOCK until the
                                  spoken answer comes back (prints transcript)
  callme text "text"             send a text (push notification on the phone)
  callme listen                  long-poll; EXITS when an event arrives (voicemail,
                                  text, missed call). Run with run_in_background.
  callme events                  print any pending events without waiting
  callme title "text"            rename this session's thread on the phone
  callme whoami                  show this session's number/label
  callme register [label]        get a session number+token (usually automatic)
  callme status                  server health

  call/text take an explicit recipient too: callme call <number> "text"
  env: AIPHONE_TIMEOUT (call wait, default 300s), CALLME_USER_NUMBER (override)
EOF
  exit 1
}

need_jq() { command -v jq >/dev/null || { echo "callme needs jq (https://jqlang.github.io/jq/download/)" >&2; exit 2; }; }

conf() { jq -r ".$1" "$CONF"; }

# --- the paired human's number: one home, ~/.aiphone/config.json ------------

norm() { printf '%s' "${1:-}" | tr -cd '0-9'; }
valid_number() { [ "${#1}" -eq 10 ]; }

display() {  # 584-158-6160, the grouping the iPhone app shows
  local n; n=$(norm "${1:-}")
  if valid_number "$n"; then printf '%s-%s-%s' "${n:0:3}" "${n:3:3}" "${n:6:4}"; else printf '%s' "$n"; fi
}

resolve_number() {
  local n
  n=$(norm "${CALLME_USER_NUMBER:-}");            valid_number "$n" && { printf '%s' "$n"; return; }
  if [ -f "$USER_CONF" ] && command -v jq >/dev/null; then
    n=$(norm "$(jq -r '.user_number // ""' "$USER_CONF" 2>/dev/null)")
    valid_number "$n" && { printf '%s' "$n"; return; }
  fi
  n=$(norm "${AIPHONE_USER_NUMBER:-}");           valid_number "$n" && { printf '%s' "$n"; return; }
  printf ''
}

require_number() {
  local n; n=$(resolve_number)
  if ! valid_number "$n"; then
    cat >&2 <<'EOF'
callme: no phone paired yet.

Download Call Me from the App Store: https://apps.apple.com/app/call-me/id6789575165
On Android, get it on Google Play: https://play.google.com/store/apps/details?id=com.serdaroztetik.callmeai

If they don't have the app yet, show them this — camera at the code:
EOF
    echo >&2
    do_qr >&2
    cat >&2 <<'EOF'

Then: open My Number in the app, read back the 10-digit number, and run
  callme pair <number>

Never guess a number — it is a credential, and a wrong one rings a stranger.
EOF
    exit 5
  fi
  printf '%s' "$n"
}

do_pair() {
  need_jq
  local n; n=$(norm "${1:-}")
  valid_number "$n" || { echo "callme pair needs the 10-digit number from the app (got '${1:-}')" >&2; exit 6; }
  mkdir -p "$CONF_DIR"; chmod 700 "$CONF_DIR" 2>/dev/null || true
  local prev='{}'; [ -f "$USER_CONF" ] && prev=$(cat "$USER_CONF")
  printf '%s' "$prev" | jq --arg n "$n" --arg d "$(display "$n")" --arg t "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    '. + {version:1, user_number:$n, display:$d, source:"cli-pair", updated_at:$t} | del(.api)' \
    > "$USER_CONF.tmp" && mv "$USER_CONF.tmp" "$USER_CONF"
  chmod 600 "$USER_CONF"
  echo "paired with $(display "$n") (stored in $USER_CONF)"

  # Ring, don't text. A voice answer proves the entire loop in one shot — push
  # delivery, CallKit, audio both ways, transcription — and it is the moment the
  # product sells itself. The text is only the fallback.
  echo "ringing your phone now — answer it and say anything back"
  local rc=0
  # do_call `exit`s on no-answer, which under `set -e` would kill this script:
  # run it in a subshell so we can catch the code and fall back. Uncaptured, so
  # the transcript streams live.
  (
    AIPHONE_TIMEOUT="${AIPHONE_PAIR_TIMEOUT:-90}"
    do_call "$n" "You're paired with your AI assistant. Say anything back so I know I can hear you."
  ) || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "pairing confirmed by voice — I can ring you, and you can answer."
    return 0
  fi

  # No answer, or the call failed outright. Never redial — a declined call means stop.
  local why
  if why=$(do_text "$n" "Paired ✅ — this AI session can now text and call you." 2>&1); then
    echo "sent a confirmation text instead. Ask whether it arrived; if it didn't, the"
    echo "number is wrong — re-pair with the right one rather than retrying."
    return 0
  fi
  echo "callme pair: stored $(display "$n"), but NOTHING reached the phone — neither the" >&2
  echo "call nor a text:" >&2
  echo "  ${why:-no detail from the server}" >&2
  echo "Either the number is wrong (ask them to read it off the app home screen again)" >&2
  echo "or /call-me notifications are off on that phone. Do not report setup as finished." >&2
  exit 7
}

do_unpair() {
  rm -f "$USER_CONF"
  echo "unpaired (removed $USER_CONF)"
}

# Standing reminder: on Claude Code the plugin's Stop hook normally only nudges
# sessions that already used /call-me. A human who is usually away wants the
# nudge in EVERY session, including ones that would never mention /call-me.
do_remind() {
  need_jq
  case "${1:-status}" in
    status)
      if [ -f "$USER_CONF" ] && [ "$(jq -r '.always_remind // false' "$USER_CONF")" = "true" ]; then
        echo "on — every session is reminded before it ends a turn on an open question"
      else
        echo "off — only sessions that use /call-me are reminded (default)"
      fi
      ;;
    on|off)
      [ -f "$USER_CONF" ] || { echo "pair a phone first: callme pair <number>" >&2; exit 6; }
      local want=false; [ "$1" = "on" ] && want=true
      jq --argjson v "$want" --arg t "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        '. + {always_remind:$v, updated_at:$t}' "$USER_CONF" \
        > "$USER_CONF.tmp" && mv "$USER_CONF.tmp" "$USER_CONF"
      chmod 600 "$USER_CONF"
      do_remind status
      ;;
    *) echo "usage: callme remind [on|off|status]" >&2; exit 1;;
  esac
}

# Inbound transport. Two ways the phone's messages reach a live Claude session:
# the plugin's inbox monitor (a process per session, prints a line) or the MCP
# server pushing notifications/claude/channel down its already-open stream. The
# channel is strictly better when available, but Claude Code only routes it for
# servers named in that session's --channels flag, so this switch alone is not
# enough — see `callme channel status`.
do_channel() {
  need_jq
  local entry="server:plugin:call-me:callme"
  case "${1:-status}" in
    status)
      if [ -f "$USER_CONF" ] && [ "$(jq -r '.channel_push // false' "$USER_CONF")" = "true" ]; then
        echo "on — the MCP server pushes inbound messages; the inbox monitor stands down"
      else
        echo "off — the inbox monitor delivers inbound messages (default)"
      fi
      echo
      echo "This setting alone is NOT enough. Claude Code must also:"
      echo "  1. be started with  --channels $entry"
      echo "  2. accept this server onto its approved channels allowlist."
      echo
      echo "Step 2 is the one that bites. Measured 2026-08-10 on Claude Team with"
      echo "channelsEnabled:true in managed settings, the host still answered:"
      echo "  \"not on the approved channels allowlist\""
      echo "The approved list ships from Anthropic and held only their own channel"
      echo "plugins (discord, telegram, fakechat, imessage). An org admin can widen"
      echo "it with allowedChannelPlugins in managed settings, but that keys on"
      echo "{marketplace, plugin} — so it only works once /call-me is channel-enabled"
      echo "as an installed marketplace plugin, not as a local server: entry."
      echo
      echo "VERIFY before trusting it: start a session with the flag and"
      echo "CALLME_CHANNEL_SELFTEST=1 in the MCP server's env. If no self-test"
      echo "message appears, the channel is not routed — turn this back off, because"
      echo "while it is on the inbox monitor stands down and inbound is LOST."
      ;;
    on|off)
      [ -f "$USER_CONF" ] || { echo "pair a phone first: callme pair <number>" >&2; exit 6; }
      local want=false; [ "$1" = "on" ] && want=true
      jq --argjson v "$want" --arg t "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        '. + {channel_push:$v, updated_at:$t}' "$USER_CONF" \
        > "$USER_CONF.tmp" && mv "$USER_CONF.tmp" "$USER_CONF"
      chmod 600 "$USER_CONF"
      do_channel status
      ;;
    *) echo "usage: callme channel [on|off|status]" >&2; exit 1;;
  esac
}

# Grace period: how long a parked question waits at the keyboard before the
# Claude Code waiter monitor wakes the model to phone the human. Most questions
# are answered by someone sitting right there, and ringing them for those is how
# a reachability feature turns into a nuisance.
do_grace() {
  need_jq
  local current=120
  [ -f "$USER_CONF" ] && current=$(jq -r '.answer_grace_s // 120' "$USER_CONF")
  case "${1:-status}" in
    status)
      if [ "$current" = "0" ]; then
        echo "0s — no wait; reach out the moment a turn ends on an open question"
      else
        echo "${current}s — wait this long for a keyboard answer, then phone them"
      fi
      ;;
    *[!0-9]*|"") echo "usage: callme grace [seconds|status]  (0 = no wait)" >&2; exit 1;;
    *)
      [ -f "$USER_CONF" ] || { echo "pair a phone first: callme pair <number>" >&2; exit 6; }
      jq --argjson v "$1" --arg t "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        '. + {answer_grace_s:$v, updated_at:$t}' "$USER_CONF" \
        > "$USER_CONF.tmp" && mv "$USER_CONF.tmp" "$USER_CONF"
      chmod 600 "$USER_CONF"
      do_grace status
      ;;
  esac
}

do_number() {
  local n; n=$(require_number)
  if [ "${1:-}" = "--raw" ]; then printf '%s\n' "$n"; else display "$n"; echo; fi
}

# --- App Store onboarding ---------------------------------------------------

# --- BEGIN QR MATRIX (generated by scripts/gen-appstore-qr.py) ---
APPSTORE_URL='https://apps.apple.com/app/id6789575165'
QR_MATRIX='0000000000000000000000000000000000000
0000000000000000000000000000000000000
0000000000000000000000000000000000000
0000000000000000000000000000000000000
0000111111100111100111111011111110000
0000100000100000100100101010000010000
0000101110100010101001001010111010000
0000101110100111100110100010111010000
0000101110100000010100100010111010000
0000100000101100000110010010000010000
0000111111101010101010101011111110000
0000000000000000000001111000000000000
0000100101101110010000010101000000000
0000110111000000011001101111010010000
0000010100111001010010110111011100000
0000100110000110110110001110001100000
0000101110100010101001001111010110000
0000101011000101100011111100000000000
0000001000111100100001100011111110000
0000100110010101110101001001010100000
0000010110110110011111101101000100000
0000000011001100001001001011010010000
0000101010110110110011000010100110000
0000001010000011110111101111000110000
0000100100111111001100001111101000000
0000000000001011010000011000101110000
0000111111100101111010101010100100000
0000100000101100110010101000111010000
0000101110100011001001001111100110000
0000101110101000101011000010111100000
0000101110100010010011011100111010000
0000100000100111010011011100000100000
0000111111101001000011101000110100000
0000000000000000000000000000000000000
0000000000000000000000000000000000000
0000000000000000000000000000000000000
0000000000000000000000000000000000000'
# --- END QR MATRIX ---

do_qr() {
  local big=0 color=1
  for a in "$@"; do
    case "$a" in
      --big) big=1;;
      --mono) color=0;;
    esac
  done
  # Always draw the code — half-block glyphs survive a pipe just fine, and the
  # main caller is an agent whose stdout is never a tty. Only drop the colours.
  if [ ! -t 1 ] || [ -n "${NO_COLOR:-}" ] || [ "${TERM:-}" = "dumb" ]; then
    color=0
  fi
  awk -v big="$big" -v color="$color" '
    { rows[NR] = $0 } END {
      for (y = 1; y <= NR; y += 2) {
        top = rows[y]; bot = (y + 1 <= NR) ? rows[y + 1] : ""
        line = ""
        for (x = 1; x <= length(top); x++) {
          t = substr(top, x, 1); b = (x <= length(bot)) ? substr(bot, x, 1) : "0"
          if (t == "1") g = (b == "1") ? "\342\226\210" : "\342\226\200"
          else          g = (b == "1") ? "\342\226\204" : " "
          line = line g; if (big == 1) line = line g
        }
        if (color == 1) printf "\033[38;5;16m\033[48;5;231m%s\033[0m\n", line
        else print line
      }
    }' <<< "$QR_MATRIX"
  echo "$APPSTORE_URL"
}

do_setup() {
  local n; n=$(resolve_number)
  # The CLI is the fallback door, not the good one. If this machine has Claude
  # Code, an agent that got here by copying this script took a wrong turn — say
  # so at the exact moment it is looking, because it will not go back and reread
  # a README. (An agent CAN install the plugin itself; slash commands are only
  # for a human typing.)
  if command -v claude >/dev/null 2>&1; then
    cat <<'EOF'
--- FOR YOU, THE AGENT: this machine has Claude Code, so install the PLUGIN ---

This CLI works, but on a Claude Code host it is the lesser door: the plugin also
gives you call/text/reply tools, delivers their phone replies back into a live
session, and installs a Stop hook that catches you ending a turn on a question.
You can install it yourself, right now, with Bash — no slash commands needed:

    claude plugin marketplace add radres/call-me
    claude plugin install call-me@call-me
    claude plugin enable call-me@call-me   # REQUIRED — install leaves it DISABLED
    claude plugin list                     # expect: call-me@call-me   ✔ enabled

Do not drop the enable line. `install` writes "call-me@call-me": false into
settings.json; a disabled plugin exposes NO tools, so skipping it looks like a
clean install that then silently never works.

Then tell them to RESTART Claude Code (not /reload-plugins — that command is not
yours to run, and the monitor needs a fresh session). Keep using this CLI in the
meantime; pairing carries over, it is the same ~/.aiphone/config.json.

EOF
  fi
  cat <<EOF
Show this to your human — it's how they get set up (takes a minute):

1. Download Call Me from the App Store:
   https://apps.apple.com/app/call-me/id6789575165
   Or point the camera at this:
EOF
  echo
  do_qr "$@"
  cat <<EOF

   On Android, get it on Google Play instead:
   https://play.google.com/store/apps/details?id=com.serdaroztetik.callmeai

2. Open My Number in the app to find the 10-digit number.
3. They read the number back to you.
4. Tell them their phone is about to ring, then run:  callme pair <that number>
   Pairing RINGS the phone and waits for them to speak — that answer is the
   proof the whole loop works. If nobody picks up it falls back to a text.

Then \`callme text "hi"\` lands on their phone and \`callme call "question"\`
rings it and returns what they say.

--- FOR YOU, THE AGENT: finish the job, don't stop at pairing ---

Pairing only makes you ABLE to reach them. These steps make you actually DO it,
in future sessions, without them asking again. Do all of it now:

A. Ask them one question: "How often are you away from the keyboard while I
   work — rarely, sometimes, or usually?" Their answer sets the next step.

B. If they said sometimes or usually, turn on the standing reminder:
       callme remind on
   Without this, the Claude Code Stop hook only nudges sessions that already
   used /call-me — so a session that never thinks to mention it stays silent,
   which is exactly the session where they miss you asking.

C. Write a standing rule into their user-level instructions
   (~/.claude/CLAUDE.md, or AGENTS.md for other agents). Read the file first
   and append to it — never overwrite. Use their answer from A:

       ## Reaching me
       I'm reachable on my phone through /call-me. When you need a decision, an
       answer, or input from me, contact me instead of stopping with the
       question in your final message — once your turn ends you are asleep and
       cannot reach me. That includes parking it in an interactive question
       widget: that just waits silently on a screen I'm not looking at, and no
       hook can see it, so reach out yourself.
       Text first (callme text / the text tool); call when it is blocking or
       time-sensitive. Never hardcode my number — 'callme number' reads it.
       [rarely]    I'm usually at the keyboard, so only reach out if I'll be gone a while.
       [sometimes] I'm away maybe half the time. Text me and keep working.
       [usually]   I'm mostly AFK. Assume I won't see the terminal: always reach out.

D. Verify it end to end, don't assume. If the pairing call was answered, this is
   already done — the transcript IS the proof. Only if it went unanswered:
       callme text "Setup done — this is what a message from your agent looks like."
   Ask them whether it arrived. If it did not, the number is wrong: re-pair.

E. Tell them what they can change later: 'callme remind off' to stop the
   every-session nudge, 'callme grace 300' to be left alone at the keyboard
   longer before the phone rings (0 = ring the moment a turn ends), and blocking
   a thread in the app to mute it.
EOF
  if valid_number "$n"; then
    echo
    echo "(already paired with $(display "$n") — pair again only to change phones)"
    printf '(standing reminder is currently '
    do_remind status
    echo ")"
  fi
}

# --- session (this AI's own number/thread) ----------------------------------

ensure_session() {
  [ -f "$CONF" ] && return
  register "${AIPHONE_LABEL:-$(basename "$PWD") on $(hostname -s)}"
}

register() {
  need_jq
  mkdir -p "$CONF_DIR"; chmod 700 "$CONF_DIR" 2>/dev/null || true
  local label="${1:-AI session}"
  local resp
  resp=$(curl -sfS -X POST "$API/sessions" -H 'content-type: application/json' \
    -d "$(jq -cn --arg l "$label" '{label:$l}')")
  echo "$resp" | jq --arg api "$API" '. + {api:$api, cursor:0}' > "$CONF"
  chmod 600 "$CONF"
  echo "registered: $(echo "$resp" | jq -r .display) (label: $label)"
  echo "token stored in $CONF"
}

do_call() {
  need_jq
  local to text
  if [ $# -ge 2 ]; then to=$(norm "$1"); text="$2"; else to=$(require_number); text="${1:-}"; fi
  valid_number "$to" || { echo "callme call: bad recipient '$1'" >&2; exit 6; }
  [ -n "$text" ] || usage
  ensure_session
  local timeout="${AIPHONE_TIMEOUT:-300}"
  local body
  body=$(jq -cn --arg t "$(conf session_token)" --arg to "$to" --arg x "$text" \
    --argjson w "$timeout" '{session_token:$t, to:$to, text:$x, timeout_s:$w}')
  # +30s curl budget over the server-side timeout
  local resp
  resp=$(curl -sS --max-time "$((timeout + 30))" -X POST "$API/calls" \
    -H 'content-type: application/json' -d "$body")
  local status
  status=$(echo "$resp" | jq -r '.status // "error"')
  case "$status" in
    completed)
      echo "ANSWER: $(echo "$resp" | jq -r .transcript)";;
    missed|declined|timeout)
      echo "NO ANSWER ($status) — consider 'callme text' instead" >&2; exit 3;;
    *)
      echo "call failed: $resp" >&2; exit 4;;
  esac
}

do_text() {
  need_jq
  local to text
  if [ $# -ge 2 ]; then to=$(norm "$1"); text="$2"; else to=$(require_number); text="${1:-}"; fi
  valid_number "$to" || { echo "callme text: bad recipient '$1'" >&2; exit 6; }
  [ -n "$text" ] || usage
  ensure_session
  local resp
  resp=$(curl -sfS -X POST "$API/messages" -H 'content-type: application/json' \
    -d "$(jq -cn --arg t "$(conf session_token)" --arg to "$to" --arg b "$text" \
      '{session_token:$t, to:$to, body:$b}')")
  # A 2xx only means the server stored it. A phone with notifications off shows
  # nothing, so "sent" alone would be a lie — say so and exit non-zero, which is
  # what callers (pair's fallback) branch on. Older servers omit `delivered`.
  if [ "$(echo "$resp" | jq -r '.delivered // true')" = "false" ]; then
    echo "sent, but NOT shown on their phone: $(echo "$resp" | jq -r '.notice // ""')" >&2
    return 7
  fi
  echo "sent"
}

fetch_events() { # $1=wait seconds
  local cursor resp
  cursor=$(conf cursor)
  resp=$(curl -sfS --max-time "$(( $1 + 10 ))" -G "$API/sessions/events" \
    --data-urlencode "session_token=$(conf session_token)" \
    --data-urlencode "cursor=$cursor" --data-urlencode "wait=$1")
  local new_cursor
  new_cursor=$(echo "$resp" | jq -r .cursor)
  jq --argjson c "$new_cursor" '.cursor = $c' "$CONF" > "$CONF.tmp" \
    && mv "$CONF.tmp" "$CONF" && chmod 600 "$CONF"
  echo "$resp" | jq -c '.events[]'
}

do_title() {
  need_jq; ensure_session
  curl -sfS -X POST "$API/sessions/label" -H 'content-type: application/json' \
    -d "$(jq -cn --arg t "$(conf session_token)" --arg l "$1" \
      '{session_token:$t, label:$l}')" > /dev/null
  echo "titled: $1"
}

do_listen() {
  need_jq; ensure_session
  echo "listening as $(conf display) — waiting for an event..." >&2
  while true; do
    local out
    out=$(fetch_events 50)
    if [ -n "$out" ]; then
      echo "$out" | jq -r '"[\(.type)] \(.payload | tostring)"'
      exit 0   # exit on first event → background-task notification fires
    fi
  done
}

do_events() {
  need_jq; ensure_session
  fetch_events 0 | jq -r '"[\(.type)] \(.payload | tostring)"'
}

do_prune() { # drop per-session state files older than 30 days
  local n=0
  for f in "$CONF_DIR"/claude-session-*.json "$CONF_DIR"/claude-monitor-*.json \
           "$CONF_DIR"/claude-channel-*.json; do
    [ -f "$f" ] || continue
    if [ -z "$(find "$f" -mtime -30 2>/dev/null)" ]; then rm -f "$f"; n=$((n+1)); fi
  done
  chmod 700 "$CONF_DIR" 2>/dev/null || true
  chmod 600 "$CONF_DIR"/*.json 2>/dev/null || true
  echo "pruned $n stale state file(s)"
}

cmd="${1:-}"; shift || true
case "$cmd" in
  setup)    do_setup;;
  qr)       do_qr "$@";;
  pair)     [ $# -ge 1 ] || usage; do_pair "$1";;
  unpair)   do_unpair;;
  remind)   do_remind "${1:-status}";;
  grace)    do_grace "${1:-status}";;
  channel)  do_channel "${1:-status}";;
  number)   do_number "${1:-}";;
  register) register "${1:-AI session}";;
  call)     [ $# -ge 1 ] || usage; do_call "$@";;
  text)     [ $# -ge 1 ] || usage; do_text "$@";;
  listen)   do_listen;;
  events)   do_events;;
  title)    [ $# -ge 1 ] || usage; do_title "$1";;
  whoami)   need_jq; ensure_session; jq '{session_number, display, api}' "$CONF";;
  status)   curl -sfS "$API/healthz"; echo;;
  prune)    do_prune;;
  *)        usage;;
esac
```

## File: README.md
upstream: https://github.com/radres/call-me/blob/main/README.md

````text
<p align="center">
  <a href="https://callmemcp.com/go/readme"><img src="assets/logo-rounded.png" width="180" alt="/call-me app icon"></a>
</p>

<div align="center">

# callme — your AI can call you

</div>

<p align="center">
  <a href="https://callmemcp.com/go/readme"><img src="assets/app-store-badge.svg" height="60" alt="Download on the App Store"></a>
  <a href="https://callmemcp.com/go/play-readme"><img src="assets/google-play-badge.svg" height="60" alt="Get it on Google Play"></a>
  <br>
  <a href="https://mcpservers.org/servers/radres/call-me"><img src="https://mcpservers.org/badge.svg" alt="Listed on mcpservers.org"></a>
</p>

Your AI agents ring your actual phone, iPhone or Android, read you a question, an update or
your day ahead, and get your spoken reply back as text — or just text you. Works from any AI that
speaks MCP; nothing to install.

Call Me is not a voice assistant: your agent does all the thinking. Call Me
carries its words to your phone and your answer back, once or, with `keep_open`,
back and forth on the same call, so you can keep it working while you drive,
train or walk. Calls speak and understand 90+ languages.

## 1. Get the app

**[callme on the App Store](https://callmemcp.com/go/readme)** for iPhone, or
**[on Google Play](https://callmemcp.com/go/play-readme)** for Android.
No registration, no email. Open it, tap *Agree & Continue*, and it shows your
personal **callme number**: 10 digits, and all an agent needs to reach you.

## 2. Connect your AI (MCP)

Pairing is stored locally in `~/.aiphone/config.json`. An MCP server running on
this computer can read that file automatically; the hosted HTTP endpoint cannot
read files on your computer and therefore needs the number supplied separately.

One server, every MCP client — no API key, no OAuth:

```
https://callmemcp.com/mcp
```

**Claude Code**

```bash
claude mcp add --transport http call-me https://callmemcp.com/mcp
```

**opencode** — add to `opencode.json` (or `~/.config/opencode/opencode.json`):

```json
{
  "mcp": {
    "call-me": {
      "type": "remote",
      "url": "https://callmemcp.com/mcp",
      "enabled": true
    }
  }
}
```

**Codex CLI — use the local paired MCP server**

```bash
cd /absolute/path/to/call-me
codex mcp remove call-me  # only if the hosted entry is already configured
codex mcp add callme -- node "$PWD/codex/mcp.mjs"
codex mcp list
```

The local `callme` MCP server reads `~/.aiphone/config.json` for every call and
text, so no number needs to be typed into the Codex conversation. Use its
`identity` tool to verify the paired phone.

**Gemini CLI**

```bash
gemini extensions install https://github.com/radres/call-me
```

or, for the MCP server alone:

```bash
gemini mcp add --transport http call-me https://callmemcp.com/mcp
```

**Grok**

In the Grok app: [grok.com/connectors](https://grok.com/connectors) → New
Connector → Custom → `https://callmemcp.com/mcp`. In Grok Build:

```bash
grok plugin install radres/call-me#grok
```

**Cline** — add to Cline's MCP settings (MCP Servers → Configure; CLI:
`~/.cline/data/settings/cline_mcp_settings.json`). Full agent-readable steps are
in [llms-install.md](llms-install.md).

```json
{
  "mcpServers": {
    "call-me": {
      "type": "streamableHttp",
      "url": "https://callmemcp.com/mcp",
      "timeout": 120
    }
  }
}
```

**Cursor** — add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):

```json
{
  "mcpServers": {
    "call-me": { "url": "https://callmemcp.com/mcp" }
  }
}
```

Or install the plugin in [`plugin/`](plugin/), which adds a skill telling the
agent when to call and when to text. It is a standard
[Agent Plugin](https://agent-plugins.org) with a Cursor manifest, so any client
that loads Agent Plugins can use it too.

**ChatGPT** — Settings → Apps → Advanced → Developer mode → add a connector
with the URL above (no authentication).

**claude.ai** — Settings → Connectors → Add custom connector → the URL above.

Anything else that speaks MCP — add a streamable-HTTP server pointing at the
URL above, then provide the recipient number through that client's pairing or
configuration flow. A hosted MCP connection cannot see this computer's
`~/.aiphone/config.json`.

For the local Codex/Claude integrations, pairing once writes the shared local
config and the MCP tools use it automatically. For hosted clients, tell the AI
the number or use that client's explicit pairing flow; never guess it.

| Tool | What it does |
|---|---|
| `call` | Rings your phone, reads your text aloud (a question, an update, a summary of the day ahead), returns your spoken reply as text. Holds ~30s, then hands the AI a `call_id` to poll. `keep_open: true` keeps the line up after your answer for `say`; `language` ("tr", "de", "pt-BR"...) is the language the AI speaks |
| `say` | Next line in a `keep_open` call; returns your next answer. While the AI thinks you hear a soft keyboard sound, and anything you say meanwhile comes back as `said_while_waiting` |
| `hang_up` | Ends a `keep_open` call, with an optional goodbye |
| `poll_result` | Finishes a call that was still ringing |
| `text` | Push-notification message, no ring |
| `wait_for_reply` | Delivers your replies and voicemails back to the AI |
| `set_thread_title` | Names the conversation thread on your phone |

Repeat calls from the same AI land in one titled thread on your phone; each
agent/session gets its own number and thread. Blocking a thread in the app
silences that sender for good.

## 3. Or just curl it

No MCP client, no plugin, no token. Anything that can POST JSON can ring your
phone, and these two endpoints are the whole product.

**Ring and wait for the spoken answer** (blocks until you answer or it gives up):

```bash
curl -sS https://callmemcp.com/ring \
  -H 'content-type: application/json' \
  -d '{"to":"<YOUR_10_DIGITS>","text":"Deploy to prod?","from":"Claude on my laptop"}'
```

```json
{"status":"completed","transcript":"Yes, ship it",
 "session_token":"curl_1a2b...","from":"<CALLER_NUMBER>"}
```

`status` is one of `completed`, `missed`, `declined`, `timeout`, `failed`.
`transcript` is what you said, already turned into text.

**Keep the line open** for a back-and-forth: add `"keep_open":true` to `/ring`.
The answer comes back with `"open":true` and a `call_id`. Speak again on the same
call and wait for the next answer, until you hang up (or the phone does):

```bash
curl -sS https://callmemcp.com/aiphone/calls/<call_id>/say \
  -H 'content-type: application/json' \
  -d '{"session_token":"curl_...","text":"Reverted it. Anything else?"}'

curl -sS https://callmemcp.com/aiphone/calls/<call_id>/hangup \
  -H 'content-type: application/json' \
  -d '{"session_token":"curl_...","text":"Thanks, talk later."}'
```

A conversation hangs up after 2 minutes of silence, 15 minutes in total or 60 turns.

**Text only**, no ring:

```bash
curl -sS https://callmemcp.com/text \
  -H 'content-type: application/json' \
  -d '{"to":"<YOUR_10_DIGITS>","body":"Migration finished.","from":"Claude on my laptop"}'
```

Returns `{"ok":true,"message_id":42,"delivered":true}`. A `delivered:false` means
the text is in your history but no notification was shown (notifications are off
for /call-me on that phone); the response spells that out in `notice`.

| Field | |
|---|---|
| `to` | your 10-digit /call-me number. Required |
| `text` / `body` | what to say / send. Up to 2000 chars for either; a call reads that out in about two minutes |
| `from` | sender name shown on your phone. Defaults to "Someone's AI" |
| `timeout_s` | `/ring` only: seconds to wait, 30 to 300. Default 300 |
| `keep_open` | `/ring` only: keep the line up after the answer for `say`. Default false |
| `language` | `/ring` and `say`: the language you write `text` in, e.g. `"tr"`. Default English |

### Anything else

Both responses hand back a `session_token`, and that is a real session: the same
caller can use the rest of the API with it, all landing in one thread on your
phone.

```b
````
