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>
This commit is contained in:
@@ -84,6 +84,39 @@ bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh" ns.py update-user acme 101 --body
|
||||
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)
|
||||
|
||||
```bash
|
||||
# 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>`).
|
||||
|
||||
```bash
|
||||
# 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
|
||||
@@ -105,8 +138,8 @@ The live OpenAPI spec is **NetSapiens API v2 v44.4.10** — **239 paths / 354 op
|
||||
| `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` |
|
||||
| `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) | |
|
||||
|
||||
@@ -86,3 +86,10 @@ exact request/response schema of any specific path.
|
||||
SKILL.md. Added live-verified read wrappers: `callqueues`, `timeframes`, `sites`,
|
||||
`contacts`, `autoattendants`, `billing`. Reseller territory = 3 domains today
|
||||
(arizonacomputerguru + two `*.91912.service`).
|
||||
Added gated WRITE wrappers for call queues (`create/update/delete-callqueue`,
|
||||
`add/update/remove-agent`) and time frames (`create/update/delete-timeframe`), each
|
||||
live-verified create→update→delete on the `arizonacomputerguru` test domain (sanctioned
|
||||
test bed, not in production use). Timeframe writes are body-discriminated (same path; the
|
||||
server selects the op from the body) — wrappers pass `--body` verbatim. Note: entry-bearing
|
||||
timeframe types (days-of-week etc.) reject create without their array — create `always` then
|
||||
convert, or include the array.
|
||||
|
||||
@@ -116,6 +116,19 @@ def main(argv=None) -> int:
|
||||
sp = sub.add_parser("create-phone"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("create-did"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
|
||||
# call queue edits (gated)
|
||||
sp = sub.add_parser("create-callqueue"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("update-callqueue"); sp.add_argument("domain"); sp.add_argument("callqueue"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("delete-callqueue"); sp.add_argument("domain"); sp.add_argument("callqueue"); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("add-agent"); sp.add_argument("domain"); sp.add_argument("callqueue"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("update-agent"); sp.add_argument("domain"); sp.add_argument("callqueue"); sp.add_argument("agent_id"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("remove-agent"); sp.add_argument("domain"); sp.add_argument("callqueue"); sp.add_argument("agent_id"); sp.add_argument("--confirm", action="store_true")
|
||||
|
||||
# timeframe edits (gated) — body-discriminated variants, pass --body verbatim
|
||||
sp = sub.add_parser("create-timeframe"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("update-timeframe"); sp.add_argument("domain"); sp.add_argument("tf_id"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true")
|
||||
sp = sub.add_parser("delete-timeframe"); sp.add_argument("domain"); sp.add_argument("tf_id"); sp.add_argument("--body"); sp.add_argument("--confirm", action="store_true")
|
||||
|
||||
# --- raw escape hatch ---
|
||||
sp = sub.add_parser("raw", help="raw request against any v2 path")
|
||||
sp.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
@@ -190,6 +203,42 @@ def main(argv=None) -> int:
|
||||
_require_confirm(args, "create phone number", f"{args.domain}: {json.dumps(body)}")
|
||||
_emit(client.create_phonenumber(args.domain, body))
|
||||
|
||||
elif args.cmd == "create-callqueue":
|
||||
body = _parse_body(args.body)
|
||||
_require_confirm(args, "create call queue", f"{args.domain}: {json.dumps(body)}")
|
||||
_emit(client.create_callqueue(args.domain, body))
|
||||
elif args.cmd == "update-callqueue":
|
||||
body = _parse_body(args.body)
|
||||
_require_confirm(args, "update call queue", f"{args.domain}/{args.callqueue}: {json.dumps(body)}")
|
||||
_emit(client.update_callqueue(args.domain, args.callqueue, body))
|
||||
elif args.cmd == "delete-callqueue":
|
||||
_require_confirm(args, "DELETE call queue", f"{args.domain}/{args.callqueue}")
|
||||
_emit(client.delete_callqueue(args.domain, args.callqueue))
|
||||
elif args.cmd == "add-agent":
|
||||
body = _parse_body(args.body)
|
||||
_require_confirm(args, "add queue agent", f"{args.domain}/{args.callqueue}: {json.dumps(body)}")
|
||||
_emit(client.add_callqueue_agent(args.domain, args.callqueue, body))
|
||||
elif args.cmd == "update-agent":
|
||||
body = _parse_body(args.body)
|
||||
_require_confirm(args, "update queue agent", f"{args.domain}/{args.callqueue}/{args.agent_id}: {json.dumps(body)}")
|
||||
_emit(client.update_callqueue_agent(args.domain, args.callqueue, args.agent_id, body))
|
||||
elif args.cmd == "remove-agent":
|
||||
_require_confirm(args, "remove queue agent", f"{args.domain}/{args.callqueue}/{args.agent_id}")
|
||||
_emit(client.remove_callqueue_agent(args.domain, args.callqueue, args.agent_id))
|
||||
|
||||
elif args.cmd == "create-timeframe":
|
||||
body = _parse_body(args.body)
|
||||
_require_confirm(args, "create timeframe", f"{args.domain}: {json.dumps(body)}")
|
||||
_emit(client.create_timeframe(args.domain, body))
|
||||
elif args.cmd == "update-timeframe":
|
||||
body = _parse_body(args.body)
|
||||
_require_confirm(args, "update timeframe", f"{args.domain}/{args.tf_id}: {json.dumps(body)}")
|
||||
_emit(client.update_timeframe(args.domain, args.tf_id, body))
|
||||
elif args.cmd == "delete-timeframe":
|
||||
body = _parse_body(args.body) if args.body else None
|
||||
_require_confirm(args, "DELETE timeframe", f"{args.domain}/{args.tf_id}" + (f" {json.dumps(body)}" if body else ""))
|
||||
_emit(client.delete_timeframe(args.domain, args.tf_id, body))
|
||||
|
||||
elif args.cmd == "raw":
|
||||
body = _parse_body(args.body)
|
||||
if args.method != "GET":
|
||||
|
||||
@@ -404,3 +404,51 @@ class NetSapiensClient:
|
||||
return self.request(
|
||||
"POST", f"domains/{urllib.parse.quote(domain)}/phonenumbers", json_body=body
|
||||
)
|
||||
|
||||
# --- call queue writes (live-verified on arizonacomputerguru, 2026-06-22) ---
|
||||
# Create body requires {"synchronous":"yes","callqueue":"<id>"} (+ optional config).
|
||||
def create_callqueue(self, domain: str, body: dict) -> Any:
|
||||
return self.request(
|
||||
"POST", f"domains/{urllib.parse.quote(domain)}/callqueues", json_body=body
|
||||
)
|
||||
|
||||
def update_callqueue(self, domain: str, callqueue: str, body: dict) -> Any:
|
||||
d, c = urllib.parse.quote(domain), urllib.parse.quote(callqueue)
|
||||
return self.request("PUT", f"domains/{d}/callqueues/{c}", json_body=body)
|
||||
|
||||
def delete_callqueue(self, domain: str, callqueue: str) -> Any:
|
||||
d, c = urllib.parse.quote(domain), urllib.parse.quote(callqueue)
|
||||
return self.request("DELETE", f"domains/{d}/callqueues/{c}")
|
||||
|
||||
def add_callqueue_agent(self, domain: str, callqueue: str, body: dict) -> Any:
|
||||
# body requires {"callqueue-agent-id": "<ext>@<domain>"}.
|
||||
d, c = urllib.parse.quote(domain), urllib.parse.quote(callqueue)
|
||||
return self.request("POST", f"domains/{d}/callqueues/{c}/agents", json_body=body)
|
||||
|
||||
def update_callqueue_agent(self, domain: str, callqueue: str, agent_id: str, body: dict) -> Any:
|
||||
d, c, a = (urllib.parse.quote(domain), urllib.parse.quote(callqueue),
|
||||
urllib.parse.quote(agent_id))
|
||||
return self.request("PUT", f"domains/{d}/callqueues/{c}/agents/{a}", json_body=body)
|
||||
|
||||
def remove_callqueue_agent(self, domain: str, callqueue: str, agent_id: str) -> Any:
|
||||
d, c, a = (urllib.parse.quote(domain), urllib.parse.quote(callqueue),
|
||||
urllib.parse.quote(agent_id))
|
||||
return self.request("DELETE", f"domains/{d}/callqueues/{c}/agents/{a}")
|
||||
|
||||
# --- timeframe writes ---
|
||||
# Same path, body-discriminated variants (NetSapiens routes PUT/DELETE by body):
|
||||
# create: {"synchronous":"yes","timeframe-name":"X","timeframe-type":"days-of-week|specific-dates|holiday|custom|always"}
|
||||
# update PUT body carries the type-specific array(s); pass --body verbatim.
|
||||
# delete with no body = delete the whole timeframe; with {"child_id":N} = delete one entry.
|
||||
def create_timeframe(self, domain: str, body: dict) -> Any:
|
||||
return self.request(
|
||||
"POST", f"domains/{urllib.parse.quote(domain)}/timeframes", json_body=body
|
||||
)
|
||||
|
||||
def update_timeframe(self, domain: str, tf_id: str, body: dict) -> Any:
|
||||
d, t = urllib.parse.quote(domain), urllib.parse.quote(tf_id)
|
||||
return self.request("PUT", f"domains/{d}/timeframes/{t}", json_body=body)
|
||||
|
||||
def delete_timeframe(self, domain: str, tf_id: str, body: Optional[dict] = None) -> Any:
|
||||
d, t = urllib.parse.quote(domain), urllib.parse.quote(tf_id)
|
||||
return self.request("DELETE", f"domains/{d}/timeframes/{t}", json_body=body)
|
||||
|
||||
Reference in New Issue
Block a user