Files
claudetools/.claude/skills/packetdial/SKILL.md
Mike Swanson f42f9c2419 packetdial: add gated call-queue + time-frame edit wrappers (live-verified)
Added write wrappers, each tested create→update→delete on the arizonacomputerguru test
domain (sanctioned, non-production):
- call queues: create-callqueue, update-callqueue, delete-callqueue + add-agent /
  update-agent / remove-agent
- time frames: create-timeframe, update-timeframe, delete-timeframe (body-discriminated —
  same path, server selects the op from the body; wrappers pass --body verbatim)

All behind --confirm (gate verified: DRY RUN refuses without it). SKILL.md documents the
bodies + the days-of-week-needs-array gotcha + names ACG as the test bed; capability map
+ api.md history updated. No production objects touched; no test leftovers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:44:30 -07:00

10 KiB

name, description
name description
packetdial 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.

PacketDial / NetSapiens (OITVOIP) Skill

Standalone CLI client for the NetSapiens SNAPsolution API v2 that backs ACG's hosted-VoIP offering through OITVOIP / PacketDial. Read-only by default; every write (create / update / delete) is gated behind --confirm.

The two hostnames (important)

Host What it is API?
voip.packetdial.com Customer-facing white-label portal / UC & fax dashboard (e.g. Cascades fax account 28598). Login-gated UI. No
pbx.packetdial.com Reseller PBX platform — NetSapiens v44.4. Yes — this skill targets it

Credentials — PROVISIONED + LIVE-VERIFIED (2026-06-22)

The reseller API key is vaulted at msp-tools/oitvoip.sops.yamlcredentials.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.

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 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

This machine's Python launcher is py (per identity.json); python / python3 also work. Run from the scripts dir so the two modules resolve.

cd C:/claudetools/.claude/skills/packetdial/scripts

bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py status                 # API version + authenticated key identity
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domains                # list all domains
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py domain <domain>        # one domain's config
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py users <domain>         # users / extensions in a domain
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py user <domain> <user>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py phones <domain>        # SIP devices registered in a domain
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py dids <domain>          # phone numbers (DIDs) on a domain
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py devices <domain> <user>
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py callqueues <domain>     # ACD call queues (agents, live queued count)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py timeframes <domain>     # time-based routing (business hours / holidays)
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py sites <domain>          # multi-site definitions
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py autoattendants <domain> # IVR menus
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py contacts <domain>       # shared address book
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py billing <domain>        # limits + current usage counts
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py cdrs --domain <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 pass --confirm. Bodies are raw JSON matching the NetSapiens v2 schema.

bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-domain --body '{"domain":"acme","description":"Acme Inc"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-user acme --body '{"user":"101","name-first-name":"Jane"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-phone acme --body '{...}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py create-did acme --body '{"phonenumber":"15205551234"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py update-user acme 101 --body '{"name-last-name":"Doe"}' --confirm
bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py delete-user acme 101 --confirm

Call queue edits (live-verified create→update→delete, 2026-06-22)

# create — body MUST include synchronous + callqueue (the queue extension/id)
ns.py create-callqueue acme --body '{"synchronous":"yes","callqueue":"8000","description":"Support"}' --confirm
ns.py update-callqueue acme 8000 --body '{"description":"Support (2nd shift)","callqueue-dispatch-type":"Round-robin"}' --confirm
ns.py delete-callqueue acme 8000 --confirm
# agents on a queue
ns.py add-agent    acme 8000 --body '{"callqueue-agent-id":"101@acme"}' --confirm
ns.py update-agent acme 8000 101@acme --body '{"callqueue-agent-dispatch-order-ordinal":1}' --confirm
ns.py remove-agent acme 8000 101@acme --confirm

Time frame edits (live-verified create→update→delete, 2026-06-22)

Timeframes are body-discriminated — same path, the server picks the operation from the body. The id arg is the timeframe-id (from ns.py timeframes <domain>).

# create: timeframe-type = always | days-of-week | specific-dates | holiday | custom
ns.py create-timeframe acme --body '{"synchronous":"yes","timeframe-name":"After Hours","timeframe-type":"always"}' --confirm
# update: the body carries the type-specific array OR a type-convert:
ns.py update-timeframe acme <timeframe-id> --body '{"update-only":"yes","timeframe-days-of-week-array":[...]}' --confirm
ns.py update-timeframe acme <timeframe-id> --body '{"type":"days-of-week"}' --confirm   # convert type
# delete: no body = delete the whole timeframe; {"child_id":N} = delete one entry
ns.py delete-timeframe acme <timeframe-id> --confirm

Gotcha: a days-of-week (or other entry-bearing) timeframe rejects creation without its entry array (HTTP 400 "...should have at most 1 days of week entry") — create always first and convert, or include the array in the create body. The arizonacomputerguru domain is the sanctioned test bed (not in production use) for exercising these writes safely.

Raw escape hatch (any of the 239 v2 paths)

The named commands cover the common surface; for anything else, hit the path directly. Non-GET methods still require --confirm.

bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py raw GET  domains/acme/users/101/answerrules
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.10239 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 + create/update/delete-callqueue, add/update/remove-agent full CRUD wrapped (verified)
timeframes (business-hours / holiday schedules) R/W timeframes + create/update/delete-timeframe body-discriminated; CRUD wrapped (verified)
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 <METHOD> <path> (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
  2. create-user per extension
  3. create-phone per SIP device (MAC-provisioned)
  4. create-did to attach DIDs and route them to users
  5. Log the work back to the Syncro ticket

Notes

  • This is the LIVE production reseller PBX. A bad create-domain or delete-user affects real customers — confirm the target domain first with a read command before any write.
  • CDR queries can be large; always pass --start/--end and a --limit.
  • Reference detail (auth shapes, full endpoint inventory) lives in references/api.md.