From abfdfc8985fef84ca378bf647bd5757c271ea1d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 18:24:14 +0000 Subject: [PATCH] Stop requiring a hardcoded Telnyx outbound voice profile name. Use the profile attached to the PhoneZero TeXML app (any name, any whitelist). Create a profile only when the account has none. Co-authored-by: Bryan @ FnFirst --- .cursor-plugin/marketplace.json | 2 +- AGENTS.md | 12 +- README.md | 8 +- docs/SETUP.md | 9 +- .../phonezero-grok/.grok-plugin/plugin.json | 2 +- .../commands/setup-phone-calling.md | 2 +- .../skills/phonezero-telnyx-canary/SKILL.md | 4 +- plugins/phonezero/.cursor-plugin/plugin.json | 2 +- plugins/phonezero/scripts/xai-mcp.mjs | 2 +- plugins/phonezero/skills/_template/SKILL.md | 2 +- .../phonezero/skills/book-restaurant/SKILL.md | 2 +- .../skills/confirm-business-hours/SKILL.md | 2 +- .../skills/phonezero-runtime/SKILL.md | 8 +- plugins/phonezero/texml/bridge.xml | 7 +- scripts/provision.sh | 153 ++++++++++++++---- 15 files changed, 159 insertions(+), 58 deletions(-) diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index b0d327b..3b884b3 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -5,7 +5,7 @@ }, "metadata": { "description": "Phone skills for voice tasks (restaurant booking first). Need: Telnyx KYC + one US number, xAI team with ZDR off, API keys, Voice Agent Builder.", - "version": "0.4.1" + "version": "0.4.2" }, "plugins": [ { diff --git a/AGENTS.md b/AGENTS.md index e6704fe..7d1bf90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,26 +97,26 @@ Use that `organization_id`. (Developer curl `GET /v2/whoami` is the same value. 2. **`PHONEZERO_TEXML_APP_ID`** — `invoke_api_endpoint` with `endpoint_name` `list_texml_applications` and args: ```json -{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name}" } +{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name, outbound}" } ``` If a row named `PhoneZero` exists, use its `id`. If not, create it in **7**. 3. **From** — `get_call_config`. If `from_wired` is false, Telnyx `list_phone_numbers` for the DID on the PhoneZero TeXML app. -4. **Destinations** — read Telnyx, do not invent a PhoneZero field. `invoke_api_endpoint` `list_outbound_voice_profiles` args: +4. **Destinations** — the outbound voice profile **attached to the PhoneZero TeXML app**, whatever it is named. Do **not** invent a PhoneZero field. Do **not** filter by `PhoneZero US-only`. Read `outbound.outbound_voice_profile_id` from the TeXML app (step 2). Then `invoke_api_endpoint` `list_outbound_voice_profiles` args: ```json -{ "filter": { "name": { "contains": "PhoneZero" } }, "jq_filter": ".data[] | select(.name==\"PhoneZero US-only\") | {id, name, whitelisted_destinations}" } +{ "jq_filter": ".data[] | {id, name, whitelisted_destinations}" } ``` -Show the codes in chat: “Telnyx outbound voice profile **PhoneZero US-only** currently allows: … . Change this in Telnyx Mission Control → Voice → Outbound voice profiles (or ask me to PATCH). It is not a PhoneZero plugin setting.” Only PATCH if they ask to add/remove countries. Do not give legal advice. +Pick the row whose `id` matches that profile id. Show the codes in chat: “Telnyx outbound voice profile **{name}** currently allows: … . Change this in Telnyx Mission Control → Voice → Outbound voice profiles (or ask me to PATCH). It is not a PhoneZero plugin setting.” If the TeXML app has no profile yet: same list, no name filter — one profile → use it; several → ask which; none → create in **7**. Only PATCH if they ask to add/remove countries. Do not give legal advice. ### 7. Provision only if missing -If profile **PhoneZero US-only**, TeXML app **PhoneZero**, and the DID is already attached: skip create. Do **not** overwrite an existing `whitelisted_destinations` unless they asked to change countries. Approve each credentialed write. Never echo keys. +If a TeXML app **PhoneZero** already has an outbound voice profile attached and the DID is on that app: skip create. Use that profile’s name and whitelist as-is. Do **not** overwrite an existing `whitelisted_destinations` unless they asked to change countries. Approve each credentialed write. Never echo keys. -If something is missing, skill **Setup** via MCP names (not REST path names): `list_outbound_voice_profiles` / `create_outbound_voice_profiles` (name `PhoneZero US-only`, `traffic_type=conversational`, `service_plan=global`, `usage_payment_method=rate-deck`, `whitelisted_destinations` default `["US"]` on create, `daily_spend_limit="5.00"`, `daily_spend_limit_enabled=true`); `create_texml_applications` / `update_texml_applications` (`voice_url` = `https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml`, verify HTTP 200, `voice_method=get`); `update_phone_numbers` `connection_id` = TeXML app id. +If something is missing, skill **Setup** via MCP names (not REST path names): `list_outbound_voice_profiles` (no name filter) / `create_outbound_voice_profiles` only when they have **no** profile (create-default name `PhoneZero` unless they pick another, `traffic_type=conversational`, `service_plan=global`, `usage_payment_method=rate-deck`, `whitelisted_destinations` default `["US"]` on create, `daily_spend_limit="5.00"`, `daily_spend_limit_enabled=true`); `create_texml_applications` / `update_texml_applications` (`voice_url` = `https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml`, verify HTTP 200, `voice_method=get`, attach the chosen profile); `update_phone_numbers` `connection_id` = TeXML app id. xAI: `list_phone_numbers` → `register_byo_number` if the DID is not `byo_trunk` → `attach_agent` onto **your** DID. Skip Builder if they said already set up. diff --git a/README.md b/README.md index 2843c73..601a636 100644 --- a/README.md +++ b/README.md @@ -76,13 +76,13 @@ Fallback only if stdio will not start: Bearer on `https://api.x.ai/v1` and `/v2` Keep in session. Do not look these up after yes. 1. `TELNYX_ACCOUNT_SID` — `invoke_api_endpoint` `list_billing_groups` args `{ "jq_filter": "[.data[].organization_id] | unique" }` -2. `PHONEZERO_TEXML_APP_ID` — `invoke_api_endpoint` `list_texml_applications` args `{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name}" }` +2. `PHONEZERO_TEXML_APP_ID` — `invoke_api_endpoint` `list_texml_applications` args `{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name, outbound}" }` 3. From — `get_call_config` (last-4 in chat) -4. Destinations — `invoke_api_endpoint` `list_outbound_voice_profiles` (name `PhoneZero US-only` → `whitelisted_destinations`). Show the list. This is **Telnyx** (Mission Control → Voice → Outbound voice profiles), not a PhoneZero field. PATCH only if they ask to add/remove countries. +4. Destinations — the profile **attached to the PhoneZero TeXML app** (any name). `list_texml_applications` → `outbound.outbound_voice_profile_id`, then `invoke_api_endpoint` `list_outbound_voice_profiles` `{ "jq_filter": ".data[] | {id, name, whitelisted_destinations}" }` and pick that `id`. **No name filter.** Show the actual name + codes. This is **Telnyx** (Mission Control → Voice → Outbound voice profiles), not a PhoneZero field. PATCH only if they ask to add/remove countries. ### 7. Provision only if missing -If profile **PhoneZero US-only**, app **PhoneZero**, and the DID is attached: skip create. Do not overwrite an existing whitelist unless they asked. Else skill Setup via MCP names: `list_outbound_voice_profiles` / `create_outbound_voice_profiles` (create default `["US"]`), `create_texml_applications` (`voice_url` = `https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml`, verify 200), `update_phone_numbers` `connection_id`. xAI: `register_byo_number` + `attach_agent` on **your** DID if needed. +If app **PhoneZero** already has an outbound voice profile attached and the DID is on that app: skip create. Use that profile’s name and whitelist as-is. Do not overwrite an existing whitelist unless they asked. Else skill Setup via MCP names: `list_outbound_voice_profiles` (no name filter) / `create_outbound_voice_profiles` only if they have none (create-default name `PhoneZero` unless they pick another; whitelist default `["US"]` on create), `create_texml_applications` (`voice_url` = `https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml`, verify 200, attach the chosen profile), `update_phone_numbers` `connection_id`. xAI: `register_byo_number` + `attach_agent` on **your** DID if needed. ### 8. First call @@ -146,7 +146,7 @@ Keys live in Configure / MCP headers only — not the agent shell. This sample ships with: -- **Destinations:** Telnyx outbound voice profile **PhoneZero US-only**, `whitelisted_destinations` default `US` on create (Mission Control → Voice → Outbound voice profiles) +- **Destinations:** whichever Telnyx outbound voice profile is attached to the PhoneZero TeXML app (`whitelisted_destinations`; default `US` only when PhoneZero **creates** a new profile). Name and country list are the user’s. Mission Control → Voice → Outbound voice profiles. - **AI disclosure:** default **on** in each task (`disclose_ai`) and in the one-time Builder paste - **Spoken name:** `PhoneZero` - **Recording:** dual-channel Telnyx recording on every call; the opener says the call is on a recorded line diff --git a/docs/SETUP.md b/docs/SETUP.md index 9d66ea2..1694c49 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -47,7 +47,7 @@ The Telnyx MCP cannot run until `TELNYX_API_KEY` is saved as a plugin variable. The Configure card must match [`plugins/phonezero/.cursor-plugin/plugin.json`](../plugins/phonezero/.cursor-plugin/plugin.json). Cursor injects those values into the plugin MCP processes only — **not** the agent shell. After save, start a **new** conversation and check Telnyx MCP plus the PhoneZero xAI MCP (`get_call_config`). Never `source ~/.phonezero/env` in chat. Never ask the agent to `curl` with `$XAI_API_KEY`. -**Defaults.** Destinations are the Telnyx profile **PhoneZero US-only** (`whitelisted_destinations`, default `US` on create) — Mission Control → Voice → Outbound voice profiles. PhoneZero has no plugin field for this. AI disclosure defaults on. Calls are recorded. You are solely responsible for lawful use; see [DISCLAIMER.md](../DISCLAIMER.md). +**Defaults.** Destinations are the Telnyx outbound voice profile **attached to the PhoneZero TeXML app** (`whitelisted_destinations`; default `US` only when a new profile is created). Name and country list are yours — Mission Control → Voice → Outbound voice profiles. PhoneZero has no plugin field for this. AI disclosure defaults on. Calls are recorded. You are solely responsible for lawful use; see [DISCLAIMER.md](../DISCLAIMER.md). To add a country (e.g. Japan): change that Telnyx whitelist in Mission Control, or ask the Bot to PATCH `whitelisted_destinations`. Telnyx rejects calls outside that list. @@ -57,7 +57,7 @@ To add a country (e.g. Japan): change that Telnyx whitelist in Mission Control, | `PHONEZERO_FROM_NUMBER` | plugin variable | A | Telnyx US DID (E.164): outbound caller ID and SIP bridge target (`sip:{PHONEZERO_FROM_NUMBER}@sip.voice.x.ai;transport=tls`). | | `XAI_API_KEY` | plugin variable (secret) | A | From a team with **ZDR off**. Injected into the PhoneZero xAI MCP only (Files, collections, STT, phone-numbers). Not in the agent shell. | | Spoken name / disclose | per-task (`phonezero-task.json`) | chat | `spoken_name` and `disclose_ai`. Default PhoneZero / true. Not on the Configure card. `{disclosure_clause}` is pasted once into the Builder prompt. | -| Destinations | Telnyx voice profile **PhoneZero US-only** | Telnyx | `whitelisted_destinations`. Mission Control → Voice → Outbound voice profiles. Not a PhoneZero plugin variable. | +| Destinations | Telnyx voice profile on the PhoneZero TeXML app | Telnyx | `whitelisted_destinations` of whatever profile is attached (`outbound.outbound_voice_profile_id`). Any name, any country list. Mission Control → Voice → Outbound voice profiles. Not a PhoneZero plugin variable. | | `TELNYX_ACCOUNT_SID` | resolved in session | B | TeXML REST account SID. MCP: `list_billing_groups` → `data[].organization_id` (no `whoami` tool). Developer curl: `GET /v2/whoami` → `data.organization_id`. Not on the Configure card. | | `PHONEZERO_TEXML_APP_ID` | resolved in session | B | TeXML application SID. Not on the Configure card. | | `PHONEZERO_XAI_COLLECTION_ID` | resolved in session | B | Collection for `phonezero-task.json` and optional `phonezero-template-*.json`. Find-or-create name `PhoneZero bookings`. | @@ -68,7 +68,7 @@ Account, KYC, and the DID stay manual. Everything after that is API-automatable. 12. **End-user path.** Run `/setup-phone-calling` (or ask *"Set up phone calling."*). The first message is a vendor checklist — stop if anything is missing (including ZDR off). Then the Bot uses the Telnyx hosted MCP (`list_api_endpoints` → `get_api_endpoint_schema` → `invoke_api_endpoint`) and `XAI_API_KEY` for xAI Files + phone-numbers. Approve each credentialed step. It will: - Resolve `TELNYX_ACCOUNT_SID` from Telnyx MCP `list_billing_groups` → `data[].organization_id` (developer curl: `GET /v2/whoami`). - - Find-or-create outbound voice profile **PhoneZero US-only**: `traffic_type=conversational`, `service_plan=global`, `usage_payment_method=rate-deck` (the only accepted combo today; error 10015 otherwise), `whitelisted_destinations` default `["US"]` on create, `daily_spend_limit="5.00"`, `daily_spend_limit_enabled=true`. If the profile exists, keep its current whitelist. PATCH only if the user asks to add or remove countries. + - Resolve the outbound voice profile **already attached** to the PhoneZero TeXML app (any name). If none: use the account’s only profile, or ask which of several to attach, or create one (create-default name `PhoneZero` unless they pick another; `traffic_type=conversational`, `service_plan=global`, `usage_payment_method=rate-deck` — the only accepted combo today; error 10015 otherwise; `whitelisted_destinations` default `["US"]` on create; `daily_spend_limit="5.00"`, `daily_spend_limit_enabled=true`). Keep an existing whitelist and name. PATCH only if the user asks to add or remove countries. - Find-or-create TeXML application **PhoneZero** with `voice_url` = [`texml/inbound.xml`](../texml/inbound.xml) (default raw URL: `https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml`) and `voice_method=get`. **Verify that URL returns HTTP 200 before writing it.** That URL is fetched **only for inbound** calls to the DID. Outbound calls carry inline `Texml` (template [`plugins/phonezero/texml/bridge.xml`](../plugins/phonezero/texml/bridge.xml)). - Attach the DID: `PATCH /v2/phone_numbers/{phone_number_id}` `{"connection_id":""}`. - Register the DID with xAI if it is not already registered: `POST https://api.x.ai/v2/phone-numbers` `{"name":"PhoneZero","phoneNumber":"+1…","origin":"byo_trunk"}`. @@ -79,6 +79,7 @@ Account, KYC, and the DID stay manual. Everything after that is API-automatable. - `XAI_API_KEY` — register the DID with xAI. The SIP bridge target is `PHONEZERO_FROM_NUMBER`. - `PHONEZERO_XAI_AGENT_ID` — attach a Builder agent (step 15) via the fieldMask PATCH. - `PHONEZERO_ALLOWED_COUNTRIES` — developer-only env. If set, writes that ISO list onto the Telnyx profile `whitelisted_destinations`. If unset, create uses `["US"]` and an existing profile is left as-is. Not a plugin variable. + - `PHONEZERO_PROFILE_NAME` — developer-only env. Pin a Telnyx outbound voice profile by name (find or create). If unset: use the profile already attached to the PhoneZero TeXML app, or the only profile on the account. Not a plugin variable. 14. **Create the Voice Agent (once, in Builder).** There is no public create API (`/v1/agents` is not enabled). Preferred: the Bot opens [https://console.x.ai](https://console.x.ai) with your approved session. Fallback: you do this and give the Bot the `agentId`. On the **same ZDR-off team** as the API key: @@ -107,7 +108,7 @@ Account, KYC, and the DID stay manual. Everything after that is API-automatable. - Telnyx auth works (a real `tools/call` succeeds); - `get_call_config` returns From last-4 (or Telnyx `list_phone_numbers` shows the DID); - a TeXML application named PhoneZero exists and the DID's `connection_id` matches it; - - Telnyx outbound voice profile **PhoneZero US-only** `whitelisted_destinations` is listed (change countries there, not on the Configure card); + - the outbound voice profile **attached to the PhoneZero TeXML app** is listed (`name` + `whitelisted_destinations`; change countries there, not on the Configure card); - xAI MCP `ensure_collection` finds **PhoneZero bookings** (403 + "Zero Data Retention" → wrong team); - xAI MCP `list_phone_numbers` shows the DID as `origin=byo_trunk` with an `agentId` (missing agent is a warning — finish step 14–15). 20. **Optional developer path.** [`scripts/setup-check.sh`](../scripts/setup-check.sh) is developer-only. Run it on a **personal machine that is allowed to hold keys**, never on the Bot computer. It verifies Telnyx auth, that the number is on the account, that the TeXML application exists, and that the DID is attached to that app. If `XAI_API_KEY` is set, it also checks xAI BYO registration. It does **not** verify that the agent answers. diff --git a/plugins/phonezero-grok/.grok-plugin/plugin.json b/plugins/phonezero-grok/.grok-plugin/plugin.json index b3164c1..146014d 100644 --- a/plugins/phonezero-grok/.grok-plugin/plugin.json +++ b/plugins/phonezero-grok/.grok-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "phonezero-grok", "displayName": "PhoneZero (Grok)", - "version": "0.4.1", + "version": "0.4.2", "description": "Grok Bot: phone skills (restaurant first). Telnyx hosted HTTP + PhoneZero xAI stdio. Edit Values for keys. Do not click Authenticate on Telnyx.", "author": { "name": "Function1st" diff --git a/plugins/phonezero-grok/commands/setup-phone-calling.md b/plugins/phonezero-grok/commands/setup-phone-calling.md index 4e24e1e..a1a21ea 100644 --- a/plugins/phonezero-grok/commands/setup-phone-calling.md +++ b/plugins/phonezero-grok/commands/setup-phone-calling.md @@ -12,7 +12,7 @@ After the vendor gate, **ask in chat and wait** before wiring. Do not silently k - Spoken name (default PhoneZero) — what the callee hears - AI disclaimer ON or OFF — they may turn it OFF -**Do not ask for destination countries.** Read them from Telnyx after the HTTP MCP is proven: `list_outbound_voice_profiles` → profile **PhoneZero US-only** → `whitelisted_destinations`. Show the list. That setting is Telnyx Mission Control → Voice → Outbound voice profiles. PATCH only if they ask to add or remove countries. Do not give legal advice. +**Do not ask for destination countries.** Read them from Telnyx after the HTTP MCP is proven: the outbound voice profile **attached to the PhoneZero TeXML app** (any name) → `whitelisted_destinations`. `list_outbound_voice_profiles` with **no name filter**; pick the id on that TeXML app. Show the actual name + codes. That setting is Telnyx Mission Control → Voice → Outbound voice profiles. PATCH only if they ask to add or remove countries. Do not give legal advice. Do not search Telnyx MCP for `whoami`. After Telnyx HTTP + xAI stdio are proven, resolve session ids immediately: diff --git a/plugins/phonezero-grok/skills/phonezero-telnyx-canary/SKILL.md b/plugins/phonezero-grok/skills/phonezero-telnyx-canary/SKILL.md index ed67437..4823802 100644 --- a/plugins/phonezero-grok/skills/phonezero-telnyx-canary/SKILL.md +++ b/plugins/phonezero-grok/skills/phonezero-telnyx-canary/SKILL.md @@ -14,8 +14,8 @@ Computer-use. Drive the UI. Do these steps **in order**. Same playbook: repo-roo 5. If `put_task` / `put_booking` is missing: add stdio `xai` from [`plugins/phonezero/mcp.json`](../../../phonezero/mcp.json) with **real** env (not `${…}`). Then `get_call_config` + `ensure_collection`. If they said xAI is already set up, do not open the Builder. Ignore the wizard number. If they turned disclaimer OFF and Builder already exists, or the agent still searches `phonezero-booking.json`, re-paste `prompts/voice-agent.md` and `prompts/end_call.md` once. 6. Session ids **now**, before any plan: - `TELNYX_ACCOUNT_SID` = `invoke_api_endpoint` `list_billing_groups` args `{ "jq_filter": "[.data[].organization_id] | unique" }` - - `PHONEZERO_TEXML_APP_ID` = `invoke_api_endpoint` `list_texml_applications` args `{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name}" }` + - `PHONEZERO_TEXML_APP_ID` = `invoke_api_endpoint` `list_texml_applications` args `{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name, outbound}" }` - From = `get_call_config` - - Destinations = `list_outbound_voice_profiles` → **PhoneZero US-only** → `whitelisted_destinations`. Show them. This is Telnyx Mission Control → Voice → Outbound voice profiles, not a PhoneZero field. PATCH only if they ask to change countries. + - Destinations = TeXML app `outbound.outbound_voice_profile_id` → `list_outbound_voice_profiles` (no name filter) → that row’s `name` + `whitelisted_destinations`. Show them. This is Telnyx Mission Control → Voice → Outbound voice profiles, not a PhoneZero field. PATCH only if they ask to change countries. 7. Provision only if profile / TeXML app / DID attach is missing. Do not overwrite an existing whitelist unless they asked. 8. On yes: `put_task` (or `put_booking` alias) → `calls_accounts_texml_calls` (session ids, do not look up SID again) → poll → recordings → `transcribe` → classify → `delete_booking` (live brief only). diff --git a/plugins/phonezero/.cursor-plugin/plugin.json b/plugins/phonezero/.cursor-plugin/plugin.json index 4292ce3..218e326 100644 --- a/plugins/phonezero/.cursor-plugin/plugin.json +++ b/plugins/phonezero/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "phonezero", "displayName": "PhoneZero", - "version": "0.4.1", + "version": "0.4.2", "description": "Phone skills for voice tasks (restaurant booking first). Need: Telnyx KYC + one US number, xAI team with ZDR off, API keys, Voice Agent Builder. Then /setup-phone-calling.", "author": { "name": "Function1st" diff --git a/plugins/phonezero/scripts/xai-mcp.mjs b/plugins/phonezero/scripts/xai-mcp.mjs index b4cda62..05a0cfb 100755 --- a/plugins/phonezero/scripts/xai-mcp.mjs +++ b/plugins/phonezero/scripts/xai-mcp.mjs @@ -808,7 +808,7 @@ async function handle(msg) { ok(id, { protocolVersion: params?.protocolVersion || "2024-11-05", capabilities: { tools: {} }, - serverInfo: { name: "phonezero-xai", version: "0.4.1" }, + serverInfo: { name: "phonezero-xai", version: "0.4.2" }, }); return; } diff --git a/plugins/phonezero/skills/_template/SKILL.md b/plugins/phonezero/skills/_template/SKILL.md index 862a84c..3491a3d 100644 --- a/plugins/phonezero/skills/_template/SKILL.md +++ b/plugins/phonezero/skills/_template/SKILL.md @@ -16,7 +16,7 @@ Required fields (edit these): | Field | Rules | |---|---| | Callee name | As they will recognize it. | -| Callee phone | E.164. Confirm. Country must be on the Telnyx **PhoneZero US-only** whitelist. | +| Callee phone | E.164. Confirm. Country must be on the Telnyx profile attached to the PhoneZero TeXML app. | | Callback | E.164. Default to the user's phone; confirm it. | Add only what the voice agent needs. Fail closed after one clarifying turn if the task stays vague. diff --git a/plugins/phonezero/skills/book-restaurant/SKILL.md b/plugins/phonezero/skills/book-restaurant/SKILL.md index eb8e880..e571299 100644 --- a/plugins/phonezero/skills/book-restaurant/SKILL.md +++ b/plugins/phonezero/skills/book-restaurant/SKILL.md @@ -26,7 +26,7 @@ Do not dial until every required field is known. Fail closed after one clarifyin | Field | Rules | |---|---| | Restaurant name | As the host will recognize it. | -| Restaurant phone | E.164. If you only have a name, look the number up, show it, and get confirmation. Reject numbers whose country is not in the Telnyx **PhoneZero US-only** `whitelisted_destinations`. `+1` covers Canada and Caribbean NANP too — confirm the actual country, ask if unsure, refuse on no. | +| Restaurant phone | E.164. If you only have a name, look the number up, show it, and get confirmation. Reject numbers whose country is not in the Telnyx profile attached to the PhoneZero TeXML app (`whitelisted_destinations`). `+1` covers Canada and Caribbean NANP too — confirm the actual country, ask if unsure, refuse on no. | | Date | Concrete calendar date. | | Preferred time | The first ask. | | Window start–end | Inclusive acceptable range on that date. Concatenate into `{window}` (e.g. `6:30 PM to 8:00 PM`). | diff --git a/plugins/phonezero/skills/confirm-business-hours/SKILL.md b/plugins/phonezero/skills/confirm-business-hours/SKILL.md index 4076b4c..60d546e 100644 --- a/plugins/phonezero/skills/confirm-business-hours/SKILL.md +++ b/plugins/phonezero/skills/confirm-business-hours/SKILL.md @@ -12,7 +12,7 @@ Collect who to call and what we need confirmed. No OpenTable step. Hand off to [ | Field | Rules | |---|---| | Business name | As they will recognize it. | -| Business phone | E.164. Look up if needed, show it, get confirmation. Country must be on the Telnyx **PhoneZero US-only** whitelist. | +| Business phone | E.164. Look up if needed, show it, get confirmation. Country must be on the Telnyx profile attached to the PhoneZero TeXML app. | | Callback | E.164. Default to the user's phone; confirm it. | | Expected hours (optional) | What we think the hours are, or a specific question (“open Saturday?”, “today’s holiday hours?”). | diff --git a/plugins/phonezero/skills/phonezero-runtime/SKILL.md b/plugins/phonezero/skills/phonezero-runtime/SKILL.md index 8f1f966..28c46e3 100644 --- a/plugins/phonezero/skills/phonezero-runtime/SKILL.md +++ b/plugins/phonezero/skills/phonezero-runtime/SKILL.md @@ -7,7 +7,7 @@ description: PhoneZero call runtime — setup, plan-first dial, Telnyx poll, xAI Shared outbound loop for every phone skill. First-party skills (`book-restaurant`, `confirm-business-hours`) and Cursor-local skills collect a task, then **hand off here** to dial. Grok Bot: if no shipped skill matches, interview into a `phonezero-task` (this file, Ad-hoc section). Do not ask Grok users to paste a `SKILL.md` or write `~/.cursor/skills`. -Destinations must be in the Telnyx outbound voice profile **PhoneZero US-only** `whitelisted_destinations` (ISO 3166-1 alpha-2). Read that list via Telnyx MCP — it is **not** a PhoneZero plugin variable. Never place a call to a country not on that list. Never place bulk or multi-destination calls. Never auto-dial. +Destinations must be in the Telnyx outbound voice profile **attached to the PhoneZero TeXML app** (`whitelisted_destinations`, ISO 3166-1 alpha-2). Read that list via Telnyx MCP — it is **not** a PhoneZero plugin variable. The profile may have **any name** and any country list the user set. Never filter by a hardcoded name such as `PhoneZero US-only`. Never place a call to a country not on that list. Never place bulk or multi-destination calls. Never auto-dial. **Defaults (not compliance advice).** Destinations default `US` on a new Telnyx profile. Disclose defaults on (per-task `disclose_ai`, and the one-time Builder `{disclosure_clause}`). Every call is recorded (dual-channel); the opener says so. Spoken name defaults to PhoneZero per task. Task JSON is English; after pickup the voice agent speaks the answerer’s language. The person using this sample is solely responsible for complying with all applicable laws and vendor terms — see repo [DISCLAIMER.md](../../../../DISCLAIMER.md). Do not give legal guidance in chat. @@ -29,7 +29,7 @@ Before collecting a task or touching Telnyx, verify these variables are present. | `PHONEZERO_XAI_COLLECTION_ID` | **Not on the Configure card.** Find-or-create collection name `PhoneZero bookings`. Attach it to the Builder agent (knowledge / file search). | | Spoken name / disclose | **Not on the Configure card.** Per-task: `spoken_name` and `disclose_ai` in `phonezero-task.json`. Default PhoneZero / true if they do not say otherwise. `{disclosure_clause}` is substituted once when pasting the Builder prompt. | -Call-time required: working Telnyx MCP, working PhoneZero xAI MCP, a From number from `get_call_config` or Telnyx list. Resolve `TELNYX_ACCOUNT_SID` and `PHONEZERO_TEXML_APP_ID` **before** the call plan — not after `put_task`. SID: `invoke_api_endpoint` `list_billing_groups` `{ "jq_filter": "[.data[].organization_id] | unique" }`. TeXML id: `invoke_api_endpoint` `list_texml_applications` `{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name}" }`. Destinations: `invoke_api_endpoint` `list_outbound_voice_profiles` `{ "filter": { "name": { "contains": "PhoneZero" } }, "jq_filter": ".data[] | select(.name==\"PhoneZero US-only\") | {id, name, whitelisted_destinations}" }`. Spoken name / disclose come from this call’s collect (defaults PhoneZero / true). Collection via xAI MCP `ensure_collection`. +Call-time required: working Telnyx MCP, working PhoneZero xAI MCP, a From number from `get_call_config` or Telnyx list. Resolve `TELNYX_ACCOUNT_SID` and `PHONEZERO_TEXML_APP_ID` **before** the call plan — not after `put_task`. SID: `invoke_api_endpoint` `list_billing_groups` `{ "jq_filter": "[.data[].organization_id] | unique" }`. TeXML id: `invoke_api_endpoint` `list_texml_applications` `{ "filter": { "friendly_name": "PhoneZero" }, "jq_filter": ".data[] | {id, friendly_name, outbound}" }`. Destinations: that app’s `outbound.outbound_voice_profile_id`, then `invoke_api_endpoint` `list_outbound_voice_profiles` `{ "jq_filter": ".data[] | {id, name, whitelisted_destinations}" }` and pick the matching `id`. **No name filter.** Spoken name / disclose come from this call’s collect (defaults PhoneZero / true). Collection via xAI MCP `ensure_collection`. If Telnyx MCP or xAI MCP is missing / 401s, or From cannot be resolved: **stop. Do not dial.** Tell the user to re-save Plugins → Configure and start a **new** conversation, then `/setup-phone-calling`. Never paste keys in chat. Never `source ~/.phonezero/env`. An old chat not seeing new MCP tools is not a failure. @@ -63,10 +63,10 @@ If any of those are missing, **stop**. - Fallback (no plugin, Cursor IDE): `"telnyx": {"command":"npx","args":["-y","@telnyx/mcp"],"env":{"TELNYX_API_KEY":"${env:TELNYX_API_KEY}"}}`. Never the literal key in the file or chat. - **Verify with a `tools/call`, never the tool count.** Call `list_api_endpoints`. If `get_call_config.from_wired` is false, take From from Telnyx `list_phone_numbers` (PhoneZero TeXML DID). -**Keys first.** Required on the Configure card: `TELNYX_API_KEY`, `PHONEZERO_FROM_NUMBER`, `XAI_API_KEY`. Spoken name and disclose are **not** on the card — ask in chat (Grok: the `AGENTS.md` call-settings card; Cursor: per task, default PhoneZero / true) and write them into `phonezero-task.json`. Destinations are **not** on the card. Read destinations from Telnyx (`list_outbound_voice_profiles` → `PhoneZero US-only` → `whitelisted_destinations`) and show them. PATCH that profile only if they ask to add/remove countries (Mission Control → Voice → Outbound voice profiles is the same setting). Account SID, TeXML app id, and collection id are **not** on the card. If Telnyx MCP or the PhoneZero xAI MCP is unwired, send the user to Plugins → Configure and a **new** conversation. Then, field-for-field: +**Keys first.** Required on the Configure card: `TELNYX_API_KEY`, `PHONEZERO_FROM_NUMBER`, `XAI_API_KEY`. Spoken name and disclose are **not** on the card — ask in chat (Grok: the `AGENTS.md` call-settings card; Cursor: per task, default PhoneZero / true) and write them into `phonezero-task.json`. Destinations are **not** on the card. Read destinations from Telnyx (the profile attached to the PhoneZero TeXML app → `whitelisted_destinations`) and show the **actual profile name** plus the codes. PATCH that profile only if they ask to add/remove countries (Mission Control → Voice → Outbound voice profiles is the same setting). Account SID, TeXML app id, and collection id are **not** on the card. If Telnyx MCP or the PhoneZero xAI MCP is unwired, send the user to Plugins → Configure and a **new** conversation. Then, field-for-field: 1. `TELNYX_ACCOUNT_SID` via Telnyx MCP `invoke_api_endpoint` `list_billing_groups` (`data[].organization_id`). Do not search the catalog for `whoami`. -2. Find-or-create outbound voice profile name `PhoneZero US-only`: `traffic_type=conversational`, `service_plan=global`, `usage_payment_method=rate-deck`, `whitelisted_destinations` default `["US"]` **on create only**, `daily_spend_limit="5.00"`, `daily_spend_limit_enabled=true` (`POST /v2/outbound_voice_profiles`; any other combo → Telnyx error 10015). **This Telnyx profile is the destination enforcement** — Telnyx rejects calls outside `whitelisted_destinations`. If the profile already exists, keep its current whitelist. `PATCH` only when the user asks to add or remove countries (e.g. add `JP`). Change the same list in Telnyx Mission Control → Voice → Outbound voice profiles. +2. Outbound voice profile — **do not require a specific name.** Resolve in this order: (a) the profile already attached to the PhoneZero TeXML app (`outbound.outbound_voice_profile_id`); (b) if they have exactly one outbound voice profile, use it; (c) if they have several, list `{id, name, whitelisted_destinations}` and ask which to attach; (d) if they have none, create one. Create defaults: name `PhoneZero` unless they pick another, `traffic_type=conversational`, `service_plan=global`, `usage_payment_method=rate-deck`, `whitelisted_destinations` default `["US"]` **on create only**, `daily_spend_limit="5.00"`, `daily_spend_limit_enabled=true` (`POST /v2/outbound_voice_profiles`; any other combo → Telnyx error 10015). **This Telnyx profile is the destination enforcement** — Telnyx rejects calls outside `whitelisted_destinations`. Keep an existing whitelist and name. `PATCH` only when the user asks to add or remove countries. They may name the profile anything and set any region list in Telnyx Mission Control → Voice → Outbound voice profiles. 3. Find-or-create TeXML app name `PhoneZero`: `voice_url` = the public raw URL of `texml/inbound.xml` (default `https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml`; verify it returns HTTP 200 before writing it), `voice_method=get`, `outbound.outbound_voice_profile_id` = that profile. 4. `PATCH /v2/phone_numbers/{phone_number_id}` `{"connection_id":""}`. 5. Register the DID with xAI (idempotent: `GET https://api.x.ai/v2/phone-numbers` first): `POST https://api.x.ai/v2/phone-numbers` `{"name":"PhoneZero","phoneNumber":"{PHONEZERO_FROM_NUMBER}","origin":"byo_trunk"}`. diff --git a/plugins/phonezero/texml/bridge.xml b/plugins/phonezero/texml/bridge.xml index ebd7acd..38ae61c 100644 --- a/plugins/phonezero/texml/bridge.xml +++ b/plugins/phonezero/texml/bridge.xml @@ -8,9 +8,10 @@ At call time the caller substitutes: {RESTAURANT_E164} — callee PSTN E.164 inside - (must be on the Telnyx PhoneZero US-only - voice-profile whitelisted_destinations; - enforcement is Telnyx, not this XML) + (must be on the Telnyx outbound voice + profile attached to the PhoneZero TeXML + app — any name; enforcement is Telnyx, + not this XML) Task facts are NOT spoken here. Upload phonezero-task.json to the xAI collection (scripts/put-booking-file.sh) before this dial. The agent diff --git a/scripts/provision.sh b/scripts/provision.sh index d0bf978..4dc733f 100755 --- a/scripts/provision.sh +++ b/scripts/provision.sh @@ -34,7 +34,7 @@ set -euo pipefail DEFAULT_INBOUND_XML_URL="https://raw.githubusercontent.com/function1st/PhoneZero/main/texml/inbound.xml" -PROFILE_NAME="PhoneZero US-only" +DEFAULT_CREATE_PROFILE_NAME="PhoneZero" APP_NAME="PhoneZero" usage() { @@ -59,6 +59,12 @@ Optional: list onto the Telnyx profile whitelist. If unset: create uses ["US"]; existing profile is not overwritten. Not a plugin var. + PHONEZERO_PROFILE_NAME developer-only. Pin a Telnyx outbound + voice profile by name (find or create). + If unset: use the profile already + attached to the PhoneZero TeXML app, + or the only profile on the account. + Do not assume a hardcoded name. PHONEZERO_INBOUND_XML_URL TeXML app voice_url (inbound reject page) XAI_API_KEY if set, register the DID with xAI (byo_trunk) PHONEZERO_XAI_AGENT_ID if set (requires XAI_API_KEY), attach this @@ -194,6 +200,47 @@ for item in (d.get("data") or []): ' } +# First data[]. whose id equals WANT_ID. Empty if none. +json_data_field_by_id() { + FIELD="$1" WANT_ID="$2" python3 -c ' +import json,os,sys +d=json.load(sys.stdin) +field=os.environ["FIELD"] +want=os.environ["WANT_ID"] +for item in (d.get("data") or []): + if str(item.get("id") or "") == want: + print(str(item.get(field) or "")) + raise SystemExit(0) +' +} + +# outbound.outbound_voice_profile_id on the TeXML app named WANT. Empty if none. +json_texml_outbound_profile_id() { + WANT="$1" python3 -c ' +import json,os,sys +d=json.load(sys.stdin) +want=os.environ["WANT"] +for item in (d.get("data") or []): + if str(item.get("friendly_name") or "") != want: + continue + outbound=item.get("outbound") or {} + rid=str(outbound.get("outbound_voice_profile_id") or "") + if rid: + print(rid) + raise SystemExit(0) +' +} + +# data[].id when the list has exactly one row with an id. Empty otherwise. +json_unique_data_id() { + python3 -c ' +import json,sys +rows=[item for item in (json.load(sys.stdin).get("data") or []) if item.get("id")] +if len(rows)==1: + print(rows[0]["id"]) +' +} + json_data_id() { python3 -c ' import json,sys @@ -297,7 +344,7 @@ fi if [ -z "${PHONEZERO_INBOUND_XML_URL:-}" ]; then PHONEZERO_INBOUND_XML_URL="$DEFAULT_INBOUND_XML_URL" fi -export PROFILE_NAME APP_NAME +export APP_NAME export PHONEZERO_INBOUND_XML_URL export PHONEZERO_FROM_NUMBER if [ -n "${PHONEZERO_XAI_AGENT_ID:-}" ]; then @@ -316,14 +363,31 @@ echo resolve_account_sid +# --- TeXML applications (needed to find the attached profile) --------- +# GET /v2/texml_applications +# https://developers.telnyx.com/api-reference/texml-applications +APP_ID="" +CODE="$(telnyx_list "https://api.telnyx.com/v2/texml_applications" "${WORKDIR}/apps.json")" || true +if [ "$CODE" != "200" ]; then + echo "error: GET /v2/texml_applications HTTP ${CODE}" >&2 + python3 -c 'import sys; print(sys.stdin.read()[:400])' <"${WORKDIR}/apps.json" >&2 + exit 1 +fi +APP_ID="$(json_find_data_id friendly_name "$APP_NAME" <"${WORKDIR}/apps.json" || true)" +ATTACHED_PROFILE_ID="" +if [ -n "$APP_ID" ]; then + ATTACHED_PROFILE_ID="$(json_texml_outbound_profile_id "$APP_NAME" <"${WORKDIR}/apps.json" || true)" +fi + # --- Outbound voice profile ------------------------------------------- # GET /v2/outbound_voice_profiles # POST /v2/outbound_voice_profiles # PATCH /v2/outbound_voice_profiles/{id} (whitelist drift after widen) # https://developers.telnyx.com/api-reference/outbound-voice-profiles -# The Telnyx voice profile whitelisted_destinations is destination -# enforcement. Do not overwrite an existing list unless the developer -# explicitly set PHONEZERO_ALLOWED_COUNTRIES (not a plugin variable). +# Use the profile attached to the PhoneZero TeXML app (any name). Do not +# require a hardcoded name. Create only when the account has none. +# Do not overwrite an existing list unless the developer explicitly set +# PHONEZERO_ALLOWED_COUNTRIES (not a plugin variable). WANT_WHITELIST_PATCH=0 if [ -n "${PHONEZERO_ALLOWED_COUNTRIES:-}" ]; then WANT_WHITELIST_PATCH=1 @@ -336,25 +400,51 @@ else echo "destination allowlist: create default ${WHITELIST_JSON}; existing Telnyx profile left as-is" fi -PROFILE_ID="" CODE="$(telnyx_list "https://api.telnyx.com/v2/outbound_voice_profiles" "${WORKDIR}/profiles.json")" || true if [ "$CODE" != "200" ]; then echo "error: GET /v2/outbound_voice_profiles HTTP ${CODE}" >&2 python3 -c 'import sys; print(sys.stdin.read()[:400])' <"${WORKDIR}/profiles.json" >&2 exit 1 fi -PROFILE_ID="$(json_find_data_id name "$PROFILE_NAME" <"${WORKDIR}/profiles.json" || true)" + +PROFILE_ID="" +PROFILE_NAME="" +PROFILE_HOW="" +if [ -n "${PHONEZERO_PROFILE_NAME:-}" ]; then + PROFILE_NAME="$PHONEZERO_PROFILE_NAME" + PROFILE_ID="$(json_find_data_id name "$PROFILE_NAME" <"${WORKDIR}/profiles.json" || true)" + if [ -n "$PROFILE_ID" ]; then + PROFILE_HOW="name pin" + fi +elif [ -n "$ATTACHED_PROFILE_ID" ]; then + PROFILE_ID="$ATTACHED_PROFILE_ID" + PROFILE_NAME="$(json_data_field_by_id name "$PROFILE_ID" <"${WORKDIR}/profiles.json" || true)" + if [ -n "$PROFILE_NAME" ]; then + PROFILE_HOW="attached to TeXML app ${APP_NAME}" + else + echo "warning: TeXML app ${APP_NAME} points at outbound profile ${PROFILE_ID}, which is not in the account list — resolving another profile" >&2 + PROFILE_ID="" + fi +fi +if [ -z "$PROFILE_ID" ] && [ -z "${PHONEZERO_PROFILE_NAME:-}" ]; then + PROFILE_ID="$(json_unique_data_id <"${WORKDIR}/profiles.json" || true)" + if [ -n "$PROFILE_ID" ]; then + PROFILE_NAME="$(json_data_field_by_id name "$PROFILE_ID" <"${WORKDIR}/profiles.json" || true)" + PROFILE_HOW="only profile on the account" + fi +fi + if [ -n "$PROFILE_ID" ]; then - echo "found outbound voice profile ${PROFILE_NAME} (${PROFILE_ID})" + echo "found outbound voice profile ${PROFILE_NAME:-?} (${PROFILE_ID}) [${PROFILE_HOW}]" DRIFT="$( - WANT_NAME="$PROFILE_NAME" WANT_JSON="$WHITELIST_JSON" python3 -c ' + WANT_ID="$PROFILE_ID" WANT_JSON="$WHITELIST_JSON" python3 -c ' import json, os, sys d = json.load(sys.stdin) -want_name = os.environ["WANT_NAME"] +want_id = os.environ["WANT_ID"] want = json.loads(os.environ["WANT_JSON"]) want_set = set(want) for item in (d.get("data") or []): - if str(item.get("name") or "") != want_name: + if str(item.get("id") or "") != want_id: continue have = [str(x).upper() for x in (item.get("whitelisted_destinations") or [])] have_set = set(have) @@ -373,12 +463,12 @@ print(",".join(sorted(want_set))) DRIFT_FLAG="$(printf '%s\n' "$DRIFT" | sed -n '1p')" if [ "$WANT_WHITELIST_PATCH" -eq 0 ]; then HAVE_WL="$( - WANT_NAME="$PROFILE_NAME" python3 -c ' + WANT_ID="$PROFILE_ID" python3 -c ' import json, os, sys d = json.load(sys.stdin) -want_name = os.environ["WANT_NAME"] +want_id = os.environ["WANT_ID"] for item in (d.get("data") or []): - if str(item.get("name") or "") != want_name: + if str(item.get("id") or "") != want_id: continue have = [str(x).upper() for x in (item.get("whitelisted_destinations") or [])] print(",".join(have) if have else "") @@ -411,6 +501,24 @@ print(json.dumps({"whitelisted_destinations": json.loads(os.environ["WHITELIST_J echo "outbound voice profile whitelist matches ${WHITELIST_JSON}" fi else + PROFILE_COUNT="$( + python3 -c 'import json,sys; print(len([i for i in (json.load(sys.stdin).get("data") or []) if i.get("id")]))' <"${WORKDIR}/profiles.json" + )" + if [ "$PROFILE_COUNT" -gt 1 ] && [ -z "${PHONEZERO_PROFILE_NAME:-}" ]; then + echo "error: ${PROFILE_COUNT} outbound voice profiles on the account and none attached to TeXML app ${APP_NAME}." >&2 + echo "Set PHONEZERO_PROFILE_NAME to pick one, or attach a profile to that TeXML app." >&2 + python3 -c ' +import json,sys +for item in (json.load(sys.stdin).get("data") or []): + name=item.get("name") or "" + rid=item.get("id") or "" + dest=item.get("whitelisted_destinations") or [] + print(f" {name} ({rid}) dest={dest}") +' <"${WORKDIR}/profiles.json" >&2 + exit 1 + fi + PROFILE_NAME="${PHONEZERO_PROFILE_NAME:-$DEFAULT_CREATE_PROFILE_NAME}" + export PROFILE_NAME python3 -c ' import json,os print(json.dumps({ @@ -424,7 +532,7 @@ print(json.dumps({ })) ' >"${WORKDIR}/profile.body" if [ "$DRY_RUN" -eq 1 ]; then - echo "would POST /v2/outbound_voice_profiles" + echo "would POST /v2/outbound_voice_profiles (name ${PROFILE_NAME})" python3 -c 'import sys; print(sys.stdin.read())' <"${WORKDIR}/profile.body" else CODE="$(telnyx_http POST "https://api.telnyx.com/v2/outbound_voice_profiles" "${WORKDIR}/profile.json" "${WORKDIR}/profile.body")" || true @@ -437,7 +545,7 @@ print(json.dumps({ echo "created outbound voice profile ${PROFILE_NAME} (${PROFILE_ID})" fi fi -export PROFILE_ID +export PROFILE_ID PROFILE_NAME # --- inbound.xml voice_url must fetch (404 until the file is on main) -- INBOUND_CODE="$( @@ -458,19 +566,10 @@ if [ "$INBOUND_CODE" != "200" ]; then fi # --- TeXML application ------------------------------------------------ -# GET /v2/texml_applications # POST /v2/texml_applications # PATCH /v2/texml_applications/{id} # https://developers.telnyx.com/api-reference/texml-applications -APP_ID="" -CODE="$(telnyx_list "https://api.telnyx.com/v2/texml_applications" "${WORKDIR}/apps.json")" || true -if [ "$CODE" != "200" ]; then - echo "error: GET /v2/texml_applications HTTP ${CODE}" >&2 - python3 -c 'import sys; print(sys.stdin.read()[:400])' <"${WORKDIR}/apps.json" >&2 - exit 1 -fi -APP_ID="$(json_find_data_id friendly_name "$APP_NAME" <"${WORKDIR}/apps.json" || true)" - +# apps.json / APP_ID already loaded above (needed to find the attached profile). PROFILE_ID="${PROFILE_ID:-}" python3 -c ' import json,os @@ -763,4 +862,4 @@ echo echo "Spoken name and disclose are per-task (chat), not Plugins → Configure." echo "Enter TELNYX_API_KEY as a plugin variable (backend-held)." echo "Enter XAI_API_KEY via Grok Bot's secure secret request flow." -echo "Destination countries are the Telnyx outbound voice profile PhoneZero US-only (Mission Control → Voice → Outbound voice profiles), not a plugin field." +echo "Destination countries are the Telnyx outbound voice profile attached to the PhoneZero TeXML app (${PROFILE_NAME:-any name}; Mission Control → Voice → Outbound voice profiles), not a plugin field."