diff --git a/.claude/skills/packetdial/SKILL.md b/.claude/skills/packetdial/SKILL.md index 550386f0..5019ad59 100644 --- a/.claude/skills/packetdial/SKILL.md +++ b/.claude/skills/packetdial/SKILL.md @@ -1,6 +1,6 @@ --- name: packetdial -description: "Manage the ACG PacketDial/OITVOIP hosted VoIP via the NetSapiens API (pbx.packetdial.com). List/inspect domains, users, devices, DIDs, resellers; pull CDRs; provision domains/users/SIP/numbers (writes gated --confirm; read-only default). Triggers: packetdial, oitvoip, netsapiens, voip domain/user/extension, provision phone, add did, CDR. Live production PBX." +description: "Manage the ACG PacketDial/OITVOIP hosted VoIP via the NetSapiens API v2 (pbx.packetdial.com). List/inspect domains, users, devices, DIDs, call queues, time frames, sites, auto-attendants, contacts, and domain billing/limits; pull CDRs; provision domains/users/SIP/numbers (writes gated --confirm; read-only default). Reseller-scoped key live + vaulted. Triggers: packetdial, oitvoip, netsapiens, voip domain/user/extension, call queue, auto attendant, provision phone, add did, CDR. Live production PBX." --- @@ -23,35 +23,23 @@ every write (create / update / delete) is gated behind `--confirm`. - Live Swagger UI: `https://pbx.packetdial.com/ns-api/openapi` - Vendor docs: https://docs.ns-api.com/ (login) and https://voipdocs.io/oitvoip-access-platform-apis -## Credentials — ONE-TIME SETUP (not yet provisioned) +## Credentials — PROVISIONED + LIVE-VERIFIED (2026-06-22) -As of this skill's creation **no API key exists yet** — the vault entry -`msp-tools/oitvoip.sops.yaml` is empty/absent, so every command will fail with a -clear "No credentials found" error until you do this once: +The reseller API key is vaulted at **`msp-tools/oitvoip.sops.yaml`** → +`credentials.api_key` (static `nsr_` bearer key). Live-verified via `ns.py whoami`: +key-id `nsr_hSGUB5Wo`, **user-scope Reseller** (`91912.service`), `user/domain: *` +(sees every domain under the ACG reseller territory), **`readonly: no`** (read-write — +so the `--confirm` gate on writes matters), `can-create-keys: no`. -1. Log into `pbx.packetdial.com` -> **Admin > API Keys** and create a - reseller-scoped key (prefix `nsr_`). If self-service key creation is not - available, reply to **Darwin Escaro (OITVOIP)** for reseller OAuth client - credentials. -2. Store it in the SOPS vault. Preferred (static bearer key): - ``` - # msp-tools/oitvoip.sops.yaml - credentials: - api_key: nsr_xxxxxxxxxxxxxxxx - ``` - Or, for OAuth2 password-grant credentials: - ``` - credentials: - client_id: - client_secret: - username: - password: - ``` -3. That's it — the client auto-detects which shape is present. +The client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env → +`PACKETDIAL_CLIENT_ID`+friends env → vault `credentials.api_key` → vault OAuth fields +(`client_id`/`client_secret`/`username`/`password`). Env overrides exist for quick +testing without touching the vault. -The client never hardcodes secrets. Resolution order: `PACKETDIAL_API_KEY` env --> `PACKETDIAL_CLIENT_ID`+friends env -> vault `credentials.api_key` -> vault -OAuth fields. Env overrides exist for quick testing without touching the vault. +> The key's own docs reference `https://api.ucaasnetwork.com/ns-api/...`; the skill +> targets `pbx.packetdial.com/ns-api/v2` and the key works there directly. They are the +> same NetSapiens platform behind two white-label hostnames — if a future endpoint 403s +> on `pbx.`, override with `PACKETDIAL_API_BASE_URL=https://api.ucaasnetwork.com/ns-api/v2`. ## Running the CLI @@ -69,10 +57,19 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py user bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py phones # SIP devices registered in a domain bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dids # phone numbers (DIDs) on a domain bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py devices +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py callqueues # ACD call queues (agents, live queued count) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py timeframes # time-based routing (business hours / holidays) +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py sites # multi-site definitions +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py autoattendants # IVR menus +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py contacts # shared address book +bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py billing # limits + current usage counts bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain --start 2026-06-01 --end 2026-06-02 bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py resellers ``` +All read wrappers above are **live-verified** against the production reseller key +(2026-06-22). + ## Writes (gated) Every mutating command prints a `[DRY RUN]` line and exits non-zero unless you @@ -97,6 +94,31 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET domains/acme/users bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw POST domains/acme/users --body '{...}' --confirm ``` +## API capability map (RTFM — spec v44.4.10, mapped 2026-06-22) + +The live OpenAPI spec is **NetSapiens API v2 v44.4.10** — **239 paths / 354 operations** +(GET 139 · POST 87 · PUT 68 · DELETE 50 · PATCH 10). Almost everything is nested under +`/domains/{domain}/...`. What the platform exposes, and how this skill surfaces it: + +| Resource (`/domains/{domain}/…`) | Ops | Skill wrapper | Notes | +|---|---|---|---| +| `users` (+ `/devices`, answerrules, voicemail, contacts under a user) | R/W, biggest surface | `users`, `user`, `devices`, `create/update/delete-user` | per-user devices live at `users/{u}/devices` | +| `phonenumbers` (DIDs + dial-rule routing) | R/W | `dids`, `create-did` | | +| `phones` (SIP devices, MAC provisioning) | R/W | `phones`, `create-phone` | | +| `callqueues` (ACD: agents, dispatch, live stats) | R/W + PATCH | `callqueues` (read) | writes via `raw` | +| `timeframes` (business-hours / holiday schedules) | R/W | `timeframes` (read) | writes via `raw` | +| `autoattendants` (IVR menus) | R/W | `autoattendants` (read) | | +| `sites` (multi-site) | R/W | `sites` (read) | | +| `contacts` (domain address book) | R/W | `contacts` (read) | | +| `addresses` (E911) · `moh` (music on hold) · `dialplans` · `msg`/`smsnumbers` (SMS) · `connections` · `number-filters` | R/W | `raw` | not yet wrapped | +| `cdrs` · `calls` · `recordings` · `transcriptions` · `queuedcall` | read | `cdrs`; rest via `raw` | call data | +| `billing` (limits + current counts) | read | `billing` | per-domain quota snapshot | +| Top-level: `domains`, `resellers`, `apikeys`, `subscriptions`, `routes`, `configurations`, `certificates`, `templates`, `jwt`, `tokens` | R/W | `domains`/`domain`/`resellers`; rest via `raw` | platform/admin | + +**Everything not wrapped is reachable via `raw `** (non-GET gated by +`--confirm`). The local spec copy used for this map: `.claude/tmp/ns-openapi.json` (refetch +from `pbx.packetdial.com/ns-api/webroot/openapi/openapi.json`). + ## Standard provisioning flow (new customer) 1. `create-domain` -> dial plan auto-generates diff --git a/.claude/skills/packetdial/references/api.md b/.claude/skills/packetdial/references/api.md index 9aaf62f4..a1969f1b 100644 --- a/.claude/skills/packetdial/references/api.md +++ b/.claude/skills/packetdial/references/api.md @@ -80,3 +80,9 @@ exact request/response schema of any specific path. `msp-tools/oitvoip.sops.yaml` was the open TODO. - 2026-06-02: `packetdial` skill created wrapping the v2 API (read-by-default, gated writes). Confirmed `voip.` is portal-only and `pbx.` is the API host. +- 2026-06-22: **Reseller API key provisioned + vaulted** (`msp-tools/oitvoip.sops.yaml`, + key-id `nsr_hSGUB5Wo`, scope Reseller `91912.service`, read-write). Live-verified end to + end. RTFM pass over the v2 spec (v44.4.10, 239 paths / 354 ops) — capability map added to + SKILL.md. Added live-verified read wrappers: `callqueues`, `timeframes`, `sites`, + `contacts`, `autoattendants`, `billing`. Reseller territory = 3 domains today + (arizonacomputerguru + two `*.91912.service`). diff --git a/.claude/skills/packetdial/scripts/ns.py b/.claude/skills/packetdial/scripts/ns.py index 3f5f2e5d..f9cd3699 100644 --- a/.claude/skills/packetdial/scripts/ns.py +++ b/.claude/skills/packetdial/scripts/ns.py @@ -97,6 +97,12 @@ def main(argv=None) -> int: sp = sub.add_parser("phones", help="phones (devices) in a domain"); sp.add_argument("domain") sp = sub.add_parser("dids", help="phone numbers in a domain"); sp.add_argument("domain") sp = sub.add_parser("devices", help="devices for a user"); sp.add_argument("domain"); sp.add_argument("user") + sp = sub.add_parser("callqueues", help="ACD call queues in a domain"); sp.add_argument("domain") + sp = sub.add_parser("timeframes", help="time-based routing schedules in a domain"); sp.add_argument("domain") + sp = sub.add_parser("sites", help="multi-site definitions in a domain"); sp.add_argument("domain") + sp = sub.add_parser("contacts", help="shared/domain contacts"); sp.add_argument("domain") + sp = sub.add_parser("autoattendants", help="auto-attendants (IVR) in a domain"); sp.add_argument("domain") + sp = sub.add_parser("billing", help="domain limits + current usage counts"); sp.add_argument("domain") sub.add_parser("resellers", help="list resellers") sp = sub.add_parser("cdrs", help="call detail records") sp.add_argument("--domain"); sp.add_argument("--start"); sp.add_argument("--end") @@ -138,6 +144,18 @@ def main(argv=None) -> int: _emit(client.phonenumbers(args.domain)) elif args.cmd == "devices": _emit(client.devices(args.domain, args.user)) + elif args.cmd == "callqueues": + _emit(client.callqueues(args.domain)) + elif args.cmd == "timeframes": + _emit(client.timeframes(args.domain)) + elif args.cmd == "sites": + _emit(client.sites(args.domain)) + elif args.cmd == "contacts": + _emit(client.contacts(args.domain)) + elif args.cmd == "autoattendants": + _emit(client.autoattendants(args.domain)) + elif args.cmd == "billing": + _emit(client.billing(args.domain)) elif args.cmd == "resellers": _emit(client.resellers()) elif args.cmd == "cdrs": diff --git a/.claude/skills/packetdial/scripts/ns_client.py b/.claude/skills/packetdial/scripts/ns_client.py index f91c75c0..6274e628 100644 --- a/.claude/skills/packetdial/scripts/ns_client.py +++ b/.claude/skills/packetdial/scripts/ns_client.py @@ -351,6 +351,31 @@ class NetSapiensClient: def subscriptions(self) -> Any: return self.request("GET", "subscriptions") + # --- per-domain feature resources (live-verified shapes, 2026-06-22) --- + def callqueues(self, domain: str) -> Any: + """ACD call queues in a domain (agents, dispatch type, live queued count).""" + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/callqueues") + + def timeframes(self, domain: str) -> Any: + """Time-based routing schedules (business hours / holidays) for a domain.""" + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/timeframes") + + def sites(self, domain: str) -> Any: + """Multi-site definitions within a domain.""" + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/sites") + + def contacts(self, domain: str) -> Any: + """Shared/domain contacts (address book).""" + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/contacts") + + def autoattendants(self, domain: str) -> Any: + """Auto-attendants (IVR menus) in a domain.""" + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/autoattendants") + + def billing(self, domain: str) -> Any: + """Domain billing/limits snapshot: max + current counts (users, queues, AAs, calls).""" + return self.request("GET", f"domains/{urllib.parse.quote(domain)}/billing") + # ====================================================================== # WRITE METHODS (gated — the CLI requires --confirm before calling these) # ======================================================================