Skip to content

Commit 573ac80

Browse files
authored
feat(tendlc)!: command-tree cutover and number reads (PR 5 of 5) (#41)
* feat(tendlc): add phone number service methods * feat(tendlc): add band tendlc number list, get, and history * fix(tendlc): add number list --campaign-id-contains, correct filter/projection docs Re-measurement showed campaignId[contains] filters correctly (the earlier zero-match probe used a campaign with no assigned numbers) and the list projection has two shapes, not a fixed five keys (assigned numbers carry three extra fields). status remains confirmed dead under every operator and value, so --status stays absent. * docs(tendlc): correct the phone number projection and filter comments Two claims in ListPhoneNumbers' doc comment were wrong, both from probe design rather than API behaviour. The projection is conditional, not fixed: 16 of 23 records carry five keys, the 7 assigned to a campaign carry three more. The original claim came from a single record fetched with limit=1. campaignId[contains] works correctly. The original probe filtered on a campaign with no assigned numbers, got zero results, and read the empty set as a broken filter. Re-tested against a campaign with three numbers, it returns three. status genuinely does not filter under any operator, including a value that matches nothing on the account. * feat(tendlc)!: remove the legacy campaigns, numbers, and number commands Deletes band tendlc campaigns, campaigns numbers, numbers, and the flat number <tn> command, ending the deprecation window from the two previous PRs where the legacy and new command trees coexisted deliberately. No aliases or shims: v0.3.0-beta status and no substantial customer traffic justify a clean break here. number.go's three subcommands (list/get/history) now attach to a plain numberCmd parent registered directly on Cmd, matching brandCmd/campaignCmd, instead of the numberGetCmd node that lived in the now-deleted numbers.go. extractData and filterNumbers in helpers.go existed only to serve the deleted commands and are removed along with their tests. Also folds in a carried-over test assertion: number list must not gain an exact-match --campaign-id flag, since campaignId[eq] is silently ignored by the API and returns every record — the same anti-pattern already avoided by brand list and campaign list. * fix(tendlc)!: reject stray args on tendlc's dispatcher commands band tendlc campaigns, band tendlc numbers, and band tendlc number <tn> -- all removed in the previous commit -- exited 0 with a help dump instead of failing, because cobra checks Runnable() before it ever consults Args, and none of Cmd/brandCmd/campaignCmd/numberCmd/vettingCmd had a RunE. A stray token that used to be a real, now-deleted command was indistinguishable from a successful help request. Gives each of those five dispatcher commands Args: cobra.NoArgs plus a trivial RunE (return cmd.Help()) so NoArgs actually runs: a bare invocation still prints help and exits 0, but a trailing token matching no subcommand now exits non-zero. customer-profile has the same latent shape but is out of scope for this branch. * test: make the doc-contract parser distinguish subcommands from positionals * test: close the fully-resolves-but-rejects-args blind spot in the doc-contract parser Adds a third gate that calls the resolved command's real cobra Args validator against the documented arguments, catching stale references that resolve completely (so the Use-string heuristic sees no remainder to judge) but whose command no longer accepts what follows it. * docs: retire the deleted commands and drop every doc-contract suppression The parser fix landed with five knownDriftCommands entries: four for real drift the deletion left behind, one for a shell comment that parsed as a command. All five are now unnecessary. The six stale lines are rewritten to the new tree, the numbers block no longer advertises a --status filter the API silently ignores, and the parser skips shell comments inside fenced blocks — prose that mentions a command mid-sentence is not an invocation. knownDriftCommands is empty. Verified by planting three shapes of stale reference and confirming each is caught: a deleted subcommand, a deleted positional form, and a bare argument on a parent that takes none. * feat(tendlc): retry once when a PUT rejects fields we can safely drop brand update and campaign update build a full-replacement PUT body by stripping a known list of read-only keys from the resource the API just returned. That only works because production currently accepts read-only fields it does not use. If that is ever tightened to a 400, both commands break the same day, since the strip lists cannot enumerate every field the API might start rejecting. Add putReplaceWithReadOnlyRetry, shared by UpdateBrand and UpdateCampaign: on a 400 whose error source.POINTER values name top-level fields present in the outgoing body, strip exactly those fields and retry once, noting the drop on stderr. Any other 400, or a retry that also fails, surfaces untouched/original. No loop, no backoff, no mutation of the shared strip lists. * docs: document band tendlc number and finish the command-tree cutover * docs: replace two real phone numbers with reserved-range placeholders Pre-existing on main, in the band number list example. This repo is public and the 919 numbers are real Bandwidth TNs; the rest of the docs already use the reserved 555-0100 block. * fix(tendlc): never let the PUT retry drop a field the caller set * fix(tendlc): the PUT retry may only drop fields the CLI does not model An independent adversarial pass found the previous guard insufficient. It protected fields the caller changed in this invocation, but an unchanged field still holds real data: a brand with a stored website that no longer passes validation would have that website dropped from the retry body by an unrelated --display-name update, and a full-replacement PUT nulls it. neverDrop is now built from the entire update flag surface, so the retry can only ever drop a field the CLI does not model at all — which is the only case it was designed for. The invariant is stated in putretry.go. Also: trailing punctuation no longer bypasses the doc-contract parser, so a documented 'band tendlc campaigns,' is caught rather than abstained on; a port-in example shows two distinct numbers again; and a caller-id moves into the reserved range. * test(cmd): normalize CRLF before parsing docs in the doc-contract gate The doc-contract parser is line-oriented and splits on "\n". On a Windows checkout the files land with CRLF, so the trailing "\r" survives the split and glues itself to the last token on every line. That broke the gate two ways. A command token became "get\r", which fails commandTokenRe, so the path resolved one token short and the test reported `band tendlc campaign` rejecting "get". And the lone "\" shell line-continuation marker became "\\\r", which no longer matched the documented abstain rule for it, so multi-line examples were parsed as if the continuation backslash were a real positional argument. Four AGENTS.md examples failed this way on windows-latest only. Normalize once at the read site rather than defending against "\r" at each token check downstream. Verified: passes under LF, passes under simulated CRLF, and still catches a planted `band tendlc brandz list` under CRLF -- the normalization does not neuter the gate.
1 parent 43b9b5c commit 573ac80

25 files changed

Lines changed: 2236 additions & 465 deletions

AGENTS.md

Lines changed: 169 additions & 41 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Manage phone numbers, voice calls, and messaging from your terminal. No dashboard clicking, no API wrangling — just straightforward commands that get things done.
44

55
```sh
6-
band call create --from +19195551234 --to +15559876543 --app-id abc-123 --answer-url https://example.com/answer
6+
band call create --from +15555550100 --to +15559876543 --app-id abc-123 --answer-url https://example.com/answer
77
```
88

99
Built for humans, but agent-native from day one — every command supports `--plain` for flat JSON, `--if-not-exists` for safe retries, and `--wait` for async operations. If you're building an AI agent that provisions phone numbers or makes calls, this is the interface.
@@ -83,7 +83,7 @@ No TTY required. Accounts are auto-discovered from the OAuth2 token.
8383
You can sign up for a Bandwidth Build trial account from the CLI:
8484

8585
```sh
86-
band account register --phone +19195551234 --email you@example.com --first-name Jane --last-name Doe
86+
band account register --phone +15555550100 --email you@example.com --first-name Jane --last-name Doe
8787
```
8888

8989
You'll be prompted to accept the [Bandwidth Build Terms of Service](https://www.bandwidth.com/legal/build-terms-of-service/) before registration proceeds. For scripted usage, pass `--accept-tos`.
@@ -138,7 +138,7 @@ Search for available numbers, then order one:
138138

139139
```sh
140140
band number search --area-code 919 --quantity 1
141-
band number order +19195551234 --subaccount <subaccount-id> --wait
141+
band number order +15555550100 --subaccount <subaccount-id> --wait
142142
```
143143

144144
The `--wait` flag blocks until the number is active, so you don't have to poll.
@@ -149,7 +149,7 @@ Phone numbers don't do anything on their own — you need to tell Bandwidth how
149149

150150
```sh
151151
band vcp create --name "My VCP" --app-id <your-app-id>
152-
band vcp assign <vcp-id> +19195551234
152+
band vcp assign <vcp-id> +15555550100
153153
```
154154

155155
Now when someone calls that number, Bandwidth routes the call to your application's callback URL.
@@ -164,7 +164,7 @@ If `vcp create` fails with a 403, your account uses the older sub-account model
164164
165165
```sh
166166
band call create \
167-
--from +19195551234 \
167+
--from +15555550100 \
168168
--to +15559876543 \
169169
--app-id <your-app-id> \
170170
--answer-url https://your-server.example.com/answer
@@ -196,7 +196,7 @@ The CLI can generate BXML for you locally. No API calls, no auth required — it
196196
band bxml speak "Thanks for calling. How can we help?"
197197
band bxml speak --voice julie "Press 1 for sales."
198198
band bxml gather --url https://example.com/gather --max-digits 1 --prompt "Press a key"
199-
band bxml transfer +19195551234 --caller-id +19195550000
199+
band bxml transfer +15555550100 --caller-id +15555550101
200200
band bxml record --url https://example.com/done --max-duration 60
201201
band bxml raw '<SpeakSentence>Hello</SpeakSentence>' # validate and pretty-print XML
202202
```
@@ -276,8 +276,8 @@ If you're sending from a standard 10-digit local number, it must be assigned to
276276
You can check registration status with `band tendlc`:
277277

278278
```sh
279-
band tendlc number +19195551234 --plain # check a specific number
280-
band tendlc campaigns --plain # list campaigns on your account
279+
band tendlc number get +15555550100 --plain # check a specific number
280+
band tendlc campaign list --plain # list campaigns on your account
281281
```
282282

283283
These two commands are for **import** customers (accounts that register campaigns through TCR and import them to Bandwidth) — campaign registration for them still happens in the Bandwidth App; see [dev.bandwidth.com](https://dev.bandwidth.com/docs/messaging/campaign-management/) for the full guide. **Direct** customers register brands and campaigns via `band tendlc brand create` and `band tendlc campaign create` (see [AGENTS.md](AGENTS.md#10dlc-campaigns) for the create requirement tree). Once you have a campaign either way, assign numbers to it with `band tnoption assign`.
@@ -306,9 +306,9 @@ A fresh UP account typically has one sub-account and one location already create
306306
```sh
307307
band number list # list your numbers
308308
band number search --area-code 919 --quantity 5 # search available numbers
309-
band number order +19195551234 --subaccount <subaccount-id> --wait # order (blocks until active)
310-
band number activate +19195551234 --voice-inbound --wait # turn on inbound voice
311-
band number release +19195551234 # release a number
309+
band number order +15555550100 --subaccount <subaccount-id> --wait # order (blocks until active)
310+
band number activate +15555550100 --voice-inbound --wait # turn on inbound voice
311+
band number release +15555550100 # release a number
312312
```
313313

314314
### Messaging
@@ -334,7 +334,7 @@ band message media upload image.png # prints media URL to stdout
334334
### Calls
335335

336336
```sh
337-
band call create --from +19195551234 --to +15559876543 --app-id abc-123 --answer-url https://example.com/answer
337+
band call create --from +15555550100 --to +15559876543 --app-id abc-123 --answer-url https://example.com/answer
338338
band call get <call-id> # check state
339339
band call hangup <call-id> # hang up
340340
band call update <call-id> --redirect-url <url> # redirect active call
@@ -359,7 +359,7 @@ band subaccount create --name "My Subaccount"
359359
band location create --subaccount <subaccount-id> --name "My Location"
360360
band app create --name "My Voice App" --type voice --callback-url https://your-server.example.com/callbacks
361361
band number search --area-code 919 --quantity 1
362-
band number order +19195551234 --subaccount <subaccount-id> --wait
362+
band number order +15555550100 --subaccount <subaccount-id> --wait
363363
```
364364

365365
Sub-accounts (formerly known as sites) are the top-level container. Locations (formerly known as SIP peers) sit inside sub-accounts and define where numbers get routed. The flow is: sub-account → location → application → number.
@@ -477,9 +477,9 @@ Sub-accounts (formerly known as sites) are the top-level container. Locations (f
477477
| `band tnoption get <id>` | Check the status of a TN Option Order |
478478
| `band tnoption list` | List TN Option Orders (filter by `--status`, `--tn`) |
479479

480-
### 10DLC brands, vettings, and campaigns (direct customers)
480+
### 10DLC brands, vettings, campaigns, and numbers
481481

482-
`band tendlc brand`, `band tendlc vetting`, and `band tendlc campaign` register and manage 10DLC brands and campaigns for accounts that register directly with TCR (not through import). Requires the Registration Center feature and Campaign Management role — check with `band tendlc status --plain`. A brand needs a customer profile first (`band customer-profile create`); see [AGENTS.md](AGENTS.md#10dlc-brands) for the full flag matrix, `--wait` semantics, and exit codes, and [AGENTS.md](AGENTS.md#10dlc-campaigns) for the campaign create requirement tree, the `imported` update branch, and the operational trap around editing a non-terminal campaign.
482+
`band tendlc brand`, `band tendlc vetting`, and `band tendlc campaign` register and manage 10DLC brands and campaigns for accounts that register directly with TCR (not through import); `band tendlc number` looks up phone number registration status and works for direct and import customers alike. Requires the Registration Center feature and Campaign Management role — check with `band tendlc status --plain`. A brand needs a customer profile first (`band customer-profile create`); see [AGENTS.md](AGENTS.md#10dlc-brands) for the full flag matrix, `--wait` semantics, and exit codes, [AGENTS.md](AGENTS.md#10dlc-campaigns) for the campaign create requirement tree, the `imported` update branch, and the operational trap around editing a non-terminal campaign, and [AGENTS.md](AGENTS.md#10dlc-numbers) for the conditional list projection and the `get` 404 caveat.
483483

484484
| Command | What it does |
485485
|---------|-------------|
@@ -504,6 +504,9 @@ Sub-accounts (formerly known as sites) are the top-level container. Locations (f
504504
| `band tendlc campaign update <id>` | Update a campaign (read-modify-write; imported campaigns accept only `--campaign-name`) |
505505
| `band tendlc campaign deactivate <id>` | Permanently deactivate a campaign (`--confirm` required; irreversible) |
506506
| `band tendlc campaign nudge <id> --intent <intent>` | Ask TCR to re-evaluate a campaign (not billable, no `--confirm`) |
507+
| `band tendlc number list` | List 10DLC phone number registrations (filter by `--campaign-id-contains`; no `--status` filter — the API silently ignores it, so filter client-side) |
508+
| `band tendlc number get <tn>` | Get one phone number's registration record (may 404 on some accounts even for numbers `list` returns) |
509+
| `band tendlc number history <tn>` | Show a phone number's activity log |
507510

508511
### SIP trunk authentication
509512

0 commit comments

Comments
 (0)