From c0122526bb6fc581705b8923f7d3d9a0253c3061 Mon Sep 17 00:00:00 2001 From: michaela-band Date: Mon, 5 Oct 2026 16:50:27 -0400 Subject: [PATCH 1/6] docs(bxml): use reserved example numbers in transfer help Example phone numbers should come from the 555-0100 through 555-0199 block the NANP reserves for fictional use, so a copy-pasted command never references a number that could be assigned to someone. Co-Authored-By: Claude Opus 5 --- cmd/bxml/transfer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/bxml/transfer.go b/cmd/bxml/transfer.go index 7f0a81d..f4978f5 100644 --- a/cmd/bxml/transfer.go +++ b/cmd/bxml/transfer.go @@ -18,8 +18,8 @@ var transferCmd = &cobra.Command{ Use: "transfer ", Short: "Generate a Transfer BXML verb", Long: "Generates a Response containing a Transfer verb to the given phone number. Use --caller-id to set the caller ID presented on the transferred leg.", - Example: ` band bxml transfer +19195551234 - band bxml transfer +19195551234 --caller-id +19198675309`, + Example: ` band bxml transfer +19195550100 + band bxml transfer +19195550100 --caller-id +19195550101`, Args: cobra.ExactArgs(1), RunE: runTransfer, } From cff376ba731e3954358528efac2ed4a6acbf14e9 Mon Sep 17 00:00:00 2001 From: michaela-band Date: Mon, 5 Oct 2026 16:50:27 -0400 Subject: [PATCH 2/6] feat(sample)!: read sample configuration from the environment Samples that need their own configuration (OPENAI_API_KEY, TRANSFER_TO for live-assistant) now read it from the environment only, matching how BW_CLIENT_ID and BW_CLIENT_SECRET already work. Keeping configuration in one place makes the sample runner consistent with the rest of the CLI. The environment fallback already existed but was undiscoverable: both help examples and the missing-value error mentioned only the flags, so there was no way to learn about it from the CLI itself. The error now names each variable it wants and shows a ready-to-run invocation. BREAKING CHANGE: --openai-key and --transfer-to are removed from `band sample run`. Export OPENAI_API_KEY and TRANSFER_TO instead. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 8 ++++-- README.md | 12 ++++++-- cmd/sample/catalog.go | 9 +++--- cmd/sample/run.go | 55 +++++++++++++++++++----------------- cmd/sample/run_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++ cmd/sample/sample.go | 4 +-- 6 files changed, 113 insertions(+), 38 deletions(-) create mode 100644 cmd/sample/run_test.go diff --git a/AGENTS.md b/AGENTS.md index 4880831..bb3317a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,6 +140,8 @@ This is stderr only — it won't break piped output parsing. |----------|---------| | `BW_CLIENT_ID` | OAuth2 client ID | | `BW_CLIENT_SECRET` | OAuth2 client secret | +| `OPENAI_API_KEY` | OpenAI API key for the `live-assistant` sample | +| `TRANSFER_TO` | Transfer destination (E.164) for the `live-assistant` sample | | `BW_ACCOUNT_ID` | Override active account | | `BW_ENVIRONMENT` | API environment: `prod` (default), `test` | | `BW_API_URL` | Override API base URL (overrides environment-based default) | @@ -366,13 +368,15 @@ For full flag/argument reference, use `band --help`. This section cove - **`band sample run` is helpful for demoing what a developer can accomplish with Bandwidth.** It clones a repo, starts a local process, launches ngrok, and blocks for the lifetime of the app. If you need to deploy a sample for a user, use the step-by-step [Agent Workflows](#agent-workflows) for the underlying provisioning. - **`band sample list` is safe for discovery.** Run it to enumerate available samples and supported languages before telling a user which command to run. -- **Credentials are auto-wired.** `band sample run` reads the active band profile and writes `BW_ACCOUNT_ID`, `BW_CLIENT_ID`, and `BW_CLIENT_SECRET` to a `.env` file in the clone directory — the user never needs to copy credentials manually. Extra credentials (like `OPENAI_API_KEY` for the `live-assistant` sample) must be supplied via CLI flags or set as environment variables beforehand. +- **Credentials are auto-wired.** `band sample run` reads the active band profile and writes `BW_ACCOUNT_ID`, `BW_CLIENT_ID`, and `BW_CLIENT_SECRET` to a `.env` file in the clone directory — the user never needs to copy credentials manually. Sample-specific configuration (like `OPENAI_API_KEY` and `TRANSFER_TO` for the `live-assistant` sample) is read from the environment and must be exported before running. There are no flags for these — run the sample without them and the error names each one. - **The `live-assistant` sample is the primary voice AI demo.** It pairs Bandwidth's PSTN infrastructure (dialing, audio streaming, webhook routing) with the OpenAI Realtime API. The command to give a user: ```bash + export OPENAI_API_KEY=sk-... + export TRANSFER_TO=+19195550100 + band sample run live-assistant \ --language python \ - --openai-key sk-... \ --call-to +15559876543 ``` diff --git a/README.md b/README.md index b8257ef..ec8f6f8 100644 --- a/README.md +++ b/README.md @@ -618,6 +618,8 @@ All five share the same filters: `--to`/`--from` (comma-separated E.164), `--dir | `BW_API_URL` | Override the API base URL | | `BW_VOICE_URL` | Override the Voice API base URL | | `BW_MESSAGING_URL` | Override the Messaging API base URL. Messaging is production-only (no test host), so `--environment`/`BW_ENVIRONMENT` does not change it; use this for local proxies or the internal lab. | +| `OPENAI_API_KEY` | OpenAI API key for the `live-assistant` sample (`band sample run`) | +| `TRANSFER_TO` | Transfer destination in E.164 for the `live-assistant` sample (`band sample run`) | --- @@ -702,19 +704,23 @@ Want to see Bandwidth in action without writing any boilerplate? The CLI ships w ```sh band sample list # see what's available -band sample run live-assistant --language python --openai-key sk-... +OPENAI_API_KEY=sk-... TRANSFER_TO=+19195550100 band sample run live-assistant --language python ``` `band sample run` handles everything: clones the GitHub repo, creates a virtual environment, installs dependencies, starts an ngrok tunnel, wires your Bandwidth credentials into a `.env` file, creates or reuses a voice application, and launches the app. Pass `--call-to` and it dials a number automatically once the app is healthy. +Samples that need their own configuration read it from the environment, the same way `BW_CLIENT_ID` and `BW_CLIENT_SECRET` do. Run a sample without them and the CLI tells you which variables it expects. + ### Try the OpenAI live assistant The flagship sample is a real-time AI voice assistant powered by the OpenAI Realtime API. It's the fastest way to see what Bandwidth's infrastructure can do: Bandwidth owns the PSTN layer — dialing, audio streaming, and webhook routing — while your AI provider handles the intelligence. You supply the API key; Bandwidth supplies the phone network. ```sh +export OPENAI_API_KEY=sk-... +export TRANSFER_TO=+19195550100 + band sample run live-assistant \ --language python \ - --openai-key sk-... \ --call-to +15559876543 ``` @@ -732,7 +738,7 @@ band sample run live-assistant \ 5. The Python app starts and is ready to accept calls 6. If you passed `--call-to`, the CLI dials that number as soon as the health check passes — answer and talk to the assistant -**Want to transfer calls?** Pass `--transfer-to +1XXXXXXXXXX` to give the assistant a number to hand callers off to. +**Want to transfer calls?** Set `TRANSFER_TO=+1XXXXXXXXXX` to give the assistant a number to hand callers off to. --- diff --git a/cmd/sample/catalog.go b/cmd/sample/catalog.go index 889cda5..173a2f0 100644 --- a/cmd/sample/catalog.go +++ b/cmd/sample/catalog.go @@ -3,9 +3,8 @@ package sample // envPrompt describes an environment variable that a sample app requires beyond // the standard Bandwidth credentials (which are auto-populated from band auth). type envPrompt struct { - Key string // env var name written to .env - Flag string // CLI flag name (without --) - Description string // shown in --help + Key string // env var name, read from the environment and written to .env + Description string // shown when the variable is missing } // langSetup describes how to install dependencies and run the app for a given language. @@ -41,8 +40,8 @@ var catalog = map[string]*SampleEntry{ "python": "https://github.com/Bandwidth-Samples/openai-live-websockets-python", }, ExtraEnv: []envPrompt{ - {Key: "OPENAI_API_KEY", Flag: "openai-key", Description: "OpenAI API key (must have Realtime API access)"}, - {Key: "TRANSFER_TO", Flag: "transfer-to", Description: "Phone number to transfer calls to (E.164, e.g. +19195551234)"}, + {Key: "OPENAI_API_KEY", Description: "OpenAI API key with Realtime API access"}, + {Key: "TRANSFER_TO", Description: "Phone number to transfer calls to (E.164, e.g. +19195550100)"}, }, Setup: map[string]langSetup{ "python": { diff --git a/cmd/sample/run.go b/cmd/sample/run.go index 2f04a9b..fa86b48 100644 --- a/cmd/sample/run.go +++ b/cmd/sample/run.go @@ -21,11 +21,9 @@ import ( ) var ( - runLanguage string - runCallTo string - runPort int - runOpenAIKey string - runTransferTo string + runLanguage string + runCallTo string + runPort int ) var runCmd = &cobra.Command{ @@ -34,9 +32,14 @@ var runCmd = &cobra.Command{ Long: `Clones the sample app, wires up Bandwidth credentials from your active band profile, starts an ngrok tunnel, and launches the app. +Samples that need their own configuration read it from the environment, the same +way BW_CLIENT_ID and BW_CLIENT_SECRET work. Run the command without them to see +which variables a given sample expects. + Pass --call-to to automatically dial a number once the app is ready.`, - Example: ` band sample run live-assistant --language python --openai-key sk-... - band sample run live-assistant --language python --openai-key sk-... --call-to +13367499393`, + Example: ` band sample run live-assistant --language python + OPENAI_API_KEY=sk-... TRANSFER_TO=+19195550100 band sample run live-assistant --language python + OPENAI_API_KEY=sk-... TRANSFER_TO=+19195550100 band sample run live-assistant --language python --call-to +19195550101`, Args: cobra.ExactArgs(1), RunE: runSample, } @@ -45,8 +48,6 @@ func init() { runCmd.Flags().StringVarP(&runLanguage, "language", "l", "", "Language variant to run (required)") runCmd.Flags().StringVar(&runCallTo, "call-to", "", "Phone number to call after the app is ready (E.164)") runCmd.Flags().IntVar(&runPort, "port", 0, "Local port (default: from catalog)") - runCmd.Flags().StringVar(&runOpenAIKey, "openai-key", "", "OpenAI API key (for AI-powered samples)") - runCmd.Flags().StringVar(&runTransferTo, "transfer-to", "", "Phone number to transfer calls to (E.164)") _ = runCmd.MarkFlagRequired("language") } @@ -83,7 +84,7 @@ func runSample(cmd *cobra.Command, args []string) error { } // ── Extra env var validation ───────────────────────────────────────────── - extraEnv, err := collectExtraEnv(name, entry) + extraEnv, err := collectExtraEnv(name, runLanguage, entry) if err != nil { return err } @@ -241,28 +242,30 @@ func runSample(cmd *cobra.Command, args []string) error { // ── Helpers ────────────────────────────────────────────────────────────────── -// collectExtraEnv maps flag values and falls back to env vars for each extra -// env var the sample requires. Returns an error if a required value is missing. -func collectExtraEnv(name string, entry *SampleEntry) (map[string]string, error) { - flagMap := map[string]string{ - "OPENAI_API_KEY": runOpenAIKey, - "TRANSFER_TO": runTransferTo, - } +// collectExtraEnv reads each extra environment variable the sample requires. +// Values come from the environment rather than flags so that secrets are not +// exposed in the process argument list. Returns an error if any are missing. +func collectExtraEnv(name, lang string, entry *SampleEntry) (map[string]string, error) { result := make(map[string]string) - var missing []string + var missing, missingKeys []string for _, p := range entry.ExtraEnv { - val := flagMap[p.Key] + val := os.Getenv(p.Key) if val == "" { - val = os.Getenv(p.Key) - } - if val == "" { - missing = append(missing, fmt.Sprintf(" --%s (%s)", p.Flag, p.Description)) - } else { - result[p.Key] = val + missing = append(missing, fmt.Sprintf(" %s (%s)", p.Key, p.Description)) + missingKeys = append(missingKeys, p.Key+"=...") + continue } + result[p.Key] = val } if len(missing) > 0 { - return nil, fmt.Errorf("the %q sample requires additional flags:\n%s", name, strings.Join(missing, "\n")) + return nil, fmt.Errorf( + "the %q sample requires these environment variables:\n%s\n\nset them in the environment, for example:\n %s band sample run %s --language %s", + name, + strings.Join(missing, "\n"), + strings.Join(missingKeys, " "), + name, + lang, + ) } return result, nil } diff --git a/cmd/sample/run_test.go b/cmd/sample/run_test.go new file mode 100644 index 0000000..e76df94 --- /dev/null +++ b/cmd/sample/run_test.go @@ -0,0 +1,63 @@ +package sample + +import ( + "strings" + "testing" +) + +func TestCollectExtraEnvReadsEnvironment(t *testing.T) { + t.Setenv("OPENAI_API_KEY", "sk-test") + t.Setenv("TRANSFER_TO", "+19195550100") + + got, err := collectExtraEnv("live-assistant", "python", catalog["live-assistant"]) + if err != nil { + t.Fatalf("collectExtraEnv: %v", err) + } + if got["OPENAI_API_KEY"] != "sk-test" { + t.Errorf("OPENAI_API_KEY = %q, want %q", got["OPENAI_API_KEY"], "sk-test") + } + if got["TRANSFER_TO"] != "+19195550100" { + t.Errorf("TRANSFER_TO = %q, want %q", got["TRANSFER_TO"], "+19195550100") + } +} + +func TestCollectExtraEnvReportsMissingByEnvVarName(t *testing.T) { + t.Setenv("OPENAI_API_KEY", "sk-test") + t.Setenv("TRANSFER_TO", "") + + _, err := collectExtraEnv("live-assistant", "python", catalog["live-assistant"]) + if err == nil { + t.Fatal("expected an error when TRANSFER_TO is unset, got nil") + } + msg := err.Error() + if !strings.Contains(msg, "TRANSFER_TO") { + t.Errorf("error should name the missing variable, got: %s", msg) + } + if strings.Contains(msg, "OPENAI_API_KEY (") { + t.Errorf("error should not list variables that are set, got: %s", msg) + } + // Extra configuration is environment-only; the message must not send users + // back to a flag that no longer exists. + for _, gone := range []string{"--openai-key", "--transfer-to"} { + if strings.Contains(msg, gone) { + t.Errorf("error should not suggest removed flag %s, got: %s", gone, msg) + } + } + // The hint should name the variable that is actually missing. + if !strings.Contains(msg, "TRANSFER_TO=...") { + t.Errorf("hint should show the missing variable, got: %s", msg) + } + if strings.Contains(msg, "OPENAI_API_KEY=...") { + t.Errorf("hint should not include variables that are already set, got: %s", msg) + } +} + +func TestCollectExtraEnvNoRequirements(t *testing.T) { + got, err := collectExtraEnv("voice-gather", "python", catalog["voice-gather"]) + if err != nil { + t.Fatalf("collectExtraEnv: %v", err) + } + if len(got) != 0 { + t.Errorf("got %v, want empty map", got) + } +} diff --git a/cmd/sample/sample.go b/cmd/sample/sample.go index 4ac8f4d..ff7141b 100644 --- a/cmd/sample/sample.go +++ b/cmd/sample/sample.go @@ -13,8 +13,8 @@ wired from your active band profile. Examples: band sample list - band sample run live-assistant --language python --openai-key sk-... - band sample run live-assistant --language python --openai-key sk-... --call-to +19195551234`, + band sample run live-assistant --language python + OPENAI_API_KEY=sk-... TRANSFER_TO=+19195550100 band sample run live-assistant --language python --call-to +19195550101`, } func init() { From 689a530f74e026de519b8fc74cf4c43329cd36f6 Mon Sep 17 00:00:00 2001 From: michaela-band <84408209+michaela-band@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:54:58 -0400 Subject: [PATCH 3/6] Apply suggestion from @michaela-band --- cmd/sample/sample.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sample/sample.go b/cmd/sample/sample.go index ff7141b..c0d8266 100644 --- a/cmd/sample/sample.go +++ b/cmd/sample/sample.go @@ -14,7 +14,7 @@ wired from your active band profile. Examples: band sample list band sample run live-assistant --language python - OPENAI_API_KEY=sk-... TRANSFER_TO=+19195550100 band sample run live-assistant --language python --call-to +19195550101`, +band sample run live-assistant --language python --call-to +19195550101`, } func init() { From f989d0f2ff0d764f6631063441fd585a461b9200 Mon Sep 17 00:00:00 2001 From: michaela-band <84408209+michaela-band@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:55:51 -0400 Subject: [PATCH 4/6] Apply suggestion from @michaela-band --- cmd/sample/sample.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sample/sample.go b/cmd/sample/sample.go index c0d8266..cf8b494 100644 --- a/cmd/sample/sample.go +++ b/cmd/sample/sample.go @@ -14,7 +14,7 @@ wired from your active band profile. Examples: band sample list band sample run live-assistant --language python -band sample run live-assistant --language python --call-to +19195550101`, + band sample run live-assistant --language python --call-to +19195550101` } func init() { From 773149280620f1fb9c451f380fd7d59c0e825fc6 Mon Sep 17 00:00:00 2001 From: michaela-band <84408209+michaela-band@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:56:44 -0400 Subject: [PATCH 5/6] Apply suggestion from @michaela-band --- cmd/sample/sample.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sample/sample.go b/cmd/sample/sample.go index cf8b494..6d3316b 100644 --- a/cmd/sample/sample.go +++ b/cmd/sample/sample.go @@ -14,7 +14,7 @@ wired from your active band profile. Examples: band sample list band sample run live-assistant --language python - band sample run live-assistant --language python --call-to +19195550101` + band sample run live-assistant --language python --call-to +19195550101` } func init() { From 237db122fb62bbb02ed0cf9bfea655fb6032e3ae Mon Sep 17 00:00:00 2001 From: michaela-band Date: Mon, 5 Oct 2026 17:01:13 -0400 Subject: [PATCH 6/6] fix(sample): restore trailing comma in command literal The previous edit to the examples block dropped the comma after the closing backtick, which left the composite literal unterminated and broke the build. Co-Authored-By: Claude Opus 5 --- cmd/sample/sample.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sample/sample.go b/cmd/sample/sample.go index 6d3316b..0e483cb 100644 --- a/cmd/sample/sample.go +++ b/cmd/sample/sample.go @@ -14,7 +14,7 @@ wired from your active band profile. Examples: band sample list band sample run live-assistant --language python - band sample run live-assistant --language python --call-to +19195550101` + band sample run live-assistant --language python --call-to +19195550101`, } func init() {