⏳ This skill is pending AI review.
Scores will appear once the review pipeline completes.
call-me
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.
Choose how to use this skill
You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.
1. Native installer
This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.
Do not guess an installer command or replace an existing version without reviewing the diff.
2. Complete package recommended
Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.
No complete ProSkills package is published for this listing yet.3. Prompt-only
Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.
Need only the instruction file?
Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.
No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.
// RATINGS
// README
callme — your AI can call you
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 for iPhone, or on Google Play 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
claude mcp add --transport http call-me https://callmemcp.com/mcp
opencode — add to opencode.json (or ~/.config/opencode/opencode.json):
{
"mcp": {
"call-me": {
"type": "remote",
"url": "https://callmemcp.com/mcp",
"enabled": true
}
}
}
Codex CLI — use the local paired MCP server
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
gemini extensions install https://github.com/radres/call-me
or, for the MCP server alone:
gemini mcp add --transport http call-me https://callmemcp.com/mcp
Grok
In the Grok app: grok.com/connectors → New
Connector → Custom → https://callmemcp.com/mcp. In Grok Build:
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.
{
"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):
{
"mcpServers": {
"call-me": { "url": "https://callmemcp.com/mcp" }
}
}
Or install the plugin in plugin/, which adds a skill telling the
agent when to call and when to text. It is a standard
Agent Plugin 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):
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"}'
{"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):
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:
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.
// HOW IT'S BUILT
// FILES
5 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.
skill/SKILL.md18 KB5e2db4471b5dupstream ↗
--- 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.skill/agents/openai.yaml184 Bb7a0fa3726acupstream ↗
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."skill/aiphone6 B2c083c36bf52upstream ↗
Contents are not inlined in the catalog. Open the upstream file.
skill/callme23 KB0e16976632abupstream ↗
#!/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;; esacREADME.mdupstream ↗
Contents are not inlined in the catalog. Open the upstream file.
Plain-text copy, no login: /skill-bundles/radres-call-me-skill.md