@f-o-h/cli 0.1.37 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ AI-operator provisioning CLI for Front Of House.
4
4
 
5
5
  Public mirror: https://github.com/iiko38/front-of-house-cli
6
6
 
7
- Current published baseline: `@f-o-h/cli@0.1.37`
7
+ Current published baseline: `@f-o-h/cli@0.1.39`
8
8
 
9
9
  This mirror is a generated release artifact. The private product monorepo is not
10
10
  published here, and no open-source license is granted unless stated separately.
@@ -87,6 +87,9 @@ read-only by default; pass `--mutation-mode ensure` or `--repair` only when you
87
87
  explicitly want proof to ensure missing widget state. Use `--strict` in
88
88
  automation when holds should fail the command, and `--mission voice` or
89
89
  `--require-phone` when a voice/contact number is mandatory for the demo.
90
+ Use `--contact-path byon` when the proof is meant to validate a
91
+ customer-owned/BYON phone route; missing BYON config then reports
92
+ `byon_voice_number_not_configured` instead of suggesting FOH number purchase.
90
93
 
91
94
  For mass AI-agent evals and repeated demo rehearsals, keep setup on the free
92
95
  scaffold lane:
@@ -99,6 +102,10 @@ FOH_CLI_SPEND_POLICY=no_spend foh setup --org <org-id> --agent-template <templat
99
102
  buying one. `--phone-mode skip` bypasses the phone step. `--phone-mode purchase`
100
103
  is the explicit paid contact path and is fail-closed when
101
104
  `FOH_CLI_SPEND_POLICY=no_spend` is set.
105
+
106
+ If managed-number provisioning is blocked by account/provider capacity or empty
107
+ reserve inventory, proof reports `provider_capacity_blocked`; fix capacity or
108
+ switch to BYON rather than retrying blindly.
102
109
 
103
110
  The CLI defaults to the production API at `https://api.frontofhouse.okii.uk`.
104
111
 
package/dist/foh.js CHANGED
@@ -9927,7 +9927,12 @@ function envelopeOk(status) {
9927
9927
  function dedupeCommands(commands = []) {
9928
9928
  return Array.from(new Set(commands.map((command) => String(command || "").trim()).filter(Boolean)));
9929
9929
  }
9930
+ function defaultSafeToRetry(status) {
9931
+ return !(status === "fail" || status === "blocked" || status === "hold");
9932
+ }
9930
9933
  function cliEnvelope(input) {
9934
+ const extra = input.extra ?? {};
9935
+ const artifacts = input.artifacts ?? {};
9931
9936
  return {
9932
9937
  schema_version: input.schemaVersion ?? "foh_cli_envelope.v1",
9933
9938
  ok: envelopeOk(input.status),
@@ -9936,9 +9941,13 @@ function cliEnvelope(input) {
9936
9941
  summary: input.summary,
9937
9942
  ids: input.ids ?? {},
9938
9943
  checks: input.checks ?? [],
9939
- artifacts: input.artifacts ?? {},
9944
+ artifacts,
9945
+ spend_class: input.spendClass ?? extra.spend_class ?? null,
9946
+ safe_to_retry: input.safeToRetry ?? extra.safe_to_retry ?? defaultSafeToRetry(input.status),
9947
+ proof_artifacts: input.proofArtifacts ?? extra.proof_artifacts ?? artifacts.proof_bundle ?? artifacts.proof_report ?? null,
9948
+ operator_note: input.operatorNote ?? extra.operator_note ?? null,
9940
9949
  next_commands: dedupeCommands(input.nextCommands),
9941
- ...input.extra ?? {}
9950
+ ...extra
9942
9951
  };
9943
9952
  }
9944
9953
  function reasonCodeFromStep(step, fallback = "cli_command_failed") {
@@ -32755,7 +32764,7 @@ var StdioServerTransport = class {
32755
32764
  };
32756
32765
 
32757
32766
  // src/lib/cli-version.ts
32758
- var CLI_VERSION = "0.1.37";
32767
+ var CLI_VERSION = "0.1.39";
32759
32768
 
32760
32769
  // src/commands/mcp-serve.ts
32761
32770
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -37173,6 +37182,11 @@ function normalizeMutationMode(raw, repair) {
37173
37182
  const value = String(raw || "read-only").trim().toLowerCase();
37174
37183
  return value === "ensure" ? "ensure" : "read-only";
37175
37184
  }
37185
+ function normalizeContactPath(raw) {
37186
+ const value = String(raw || "auto").trim().toLowerCase();
37187
+ if (value === "managed" || value === "byon") return value;
37188
+ return "auto";
37189
+ }
37176
37190
  function agentIdFromList(response) {
37177
37191
  const agents = Array.isArray(response.agents) ? response.agents : [];
37178
37192
  const usable = agents.filter((agent) => typeof agent.id === "string" && agent.id.trim());
@@ -37186,10 +37200,41 @@ function firstUsableOrgId(response) {
37186
37200
  const usable = orgs.map((org) => org && typeof org === "object" ? org : {}).map((org) => String(org.org_id ?? org.id ?? "").trim()).filter(Boolean);
37187
37201
  return { orgId: usable.length === 1 ? usable[0] : void 0, count: usable.length };
37188
37202
  }
37203
+ function readStringField(record2, keys) {
37204
+ for (const key of keys) {
37205
+ const value = record2[key];
37206
+ if (typeof value === "string" && value.trim()) return value.trim();
37207
+ }
37208
+ return "";
37209
+ }
37210
+ function isProviderCapacityBlocked(onboarding) {
37211
+ const code = readStringField(onboarding, [
37212
+ "provisioning_reason_code",
37213
+ "provisioning_error_code",
37214
+ "reason_code",
37215
+ "code"
37216
+ ]).toLowerCase();
37217
+ if ([
37218
+ "provider_capacity_blocked",
37219
+ "twilio_subaccount_limit_reached",
37220
+ "reserve_pool_exhausted",
37221
+ "global_safety_limit_reached"
37222
+ ].includes(code)) {
37223
+ return true;
37224
+ }
37225
+ const message = readStringField(onboarding, [
37226
+ "provisioning_error",
37227
+ "provisioning_message",
37228
+ "error",
37229
+ "message"
37230
+ ]).toLowerCase();
37231
+ return /maximum number of subaccounts|subaccount limit|reserve[- ]number pool|reserve pool exhausted|global safety limit/.test(message);
37232
+ }
37189
37233
  function registerProve(program3) {
37190
- program3.command("prove").description("Produce one setup/runtime proof bundle for an agent").option("--agent <id>", "Agent ID to prove").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in cert loop (0-5)", "1").option("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--mutation-mode <mode>", "Proof mutation mode: read-only or ensure", "read-only").option("--repair", "Alias for --mutation-mode ensure").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Skip simulation certification check").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--out <path>", "Write signed proof report JSON to this path").option("--strict", "Exit non-zero unless all non-skipped checks pass").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
37234
+ program3.command("prove").description("Produce one setup/runtime proof bundle for an agent").option("--agent <id>", "Agent ID to prove").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in cert loop (0-5)", "1").option("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--contact-path <mode>", "Voice contact path: auto, managed, or byon", "auto").option("--mutation-mode <mode>", "Proof mutation mode: read-only or ensure", "read-only").option("--repair", "Alias for --mutation-mode ensure").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Skip simulation certification check").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--out <path>", "Write signed proof report JSON to this path").option("--strict", "Exit non-zero unless all non-skipped checks pass").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
37191
37235
  const checks = [];
37192
37236
  const mission = normalizeMission(opts.mission);
37237
+ const contactPath = normalizeContactPath(opts.contactPath);
37193
37238
  const mutationMode = normalizeMutationMode(opts.mutationMode, Boolean(opts.repair));
37194
37239
  const ctx = {
37195
37240
  tokenPresent: false,
@@ -37291,18 +37336,41 @@ function registerProve(program3) {
37291
37336
  if (phoneNumber) {
37292
37337
  checks.push(pass("contact_channel", "Contact phone number is provisioned.", {
37293
37338
  phone_number_present: true,
37294
- provisioning_status: provisioningStatus
37339
+ provisioning_status: provisioningStatus,
37340
+ contact_path: contactPath
37341
+ }));
37342
+ } else if (contactPath === "byon" && (opts.requirePhone || mission === "voice")) {
37343
+ checks.push(hold("contact_channel", "byon_voice_number_not_configured", "BYON/customer-owned voice contact path was requested, but no contact phone is configured for this org.", `foh provision status --org ${ctx.orgId} --json`, {
37344
+ provisioning_status: provisioningStatus,
37345
+ mission,
37346
+ contact_path: contactPath,
37347
+ spend_policy: resolveCliSpendPolicy(),
37348
+ spend_class: "customer_owned",
37349
+ safe_to_retry: false,
37350
+ operator_note: "Attach or configure the customer-owned voice number, then rerun proof. This path should not buy a FOH-owned number."
37351
+ }));
37352
+ } else if (provisioningStatus === "failed" && isProviderCapacityBlocked(onboarding) && (opts.requirePhone || mission === "voice")) {
37353
+ checks.push(hold("contact_channel", "provider_capacity_blocked", "Phone/contact provisioning is blocked by provider/account capacity or empty reserve inventory.", `foh provision status --org ${ctx.orgId} --json`, {
37354
+ provisioning_status: provisioningStatus,
37355
+ mission,
37356
+ contact_path: contactPath,
37357
+ spend_policy: resolveCliSpendPolicy(),
37358
+ spend_class: "paid_foh",
37359
+ safe_to_retry: false,
37360
+ operator_note: "Do not retry blindly. Resolve provider/account capacity or use BYON/customer-owned contact path."
37295
37361
  }));
37296
37362
  } else if (provisioningStatus === "failed" && (opts.requirePhone || mission === "voice")) {
37297
37363
  checks.push(hold("contact_channel", "contact_phone_provisioning_failed", "Phone/contact provisioning failed for this org.", `foh provision status --org ${ctx.orgId} --json`, {
37298
37364
  provisioning_status: provisioningStatus,
37299
37365
  mission,
37366
+ contact_path: contactPath,
37300
37367
  spend_policy: resolveCliSpendPolicy()
37301
37368
  }));
37302
37369
  } else if (isNoSpendPolicy() && (opts.requirePhone || mission === "voice")) {
37303
37370
  checks.push(hold("contact_channel", "voice_contact_expected_no_spend_hold", "No phone/contact number is provisioned; this is expected in no-spend eval mode unless a BYON/customer-approved phone path exists.", `foh provision status --org ${ctx.orgId} --json`, {
37304
37371
  provisioning_status: provisioningStatus,
37305
37372
  mission,
37373
+ contact_path: contactPath,
37306
37374
  spend_policy: resolveCliSpendPolicy(),
37307
37375
  spend_class: "free",
37308
37376
  safe_to_retry: true,
@@ -37312,6 +37380,7 @@ function registerProve(program3) {
37312
37380
  checks.push(hold("contact_channel", "contact_phone_missing", "No phone/contact number is provisioned for this org.", `foh provision buy --org ${ctx.orgId} --json`, {
37313
37381
  provisioning_status: provisioningStatus,
37314
37382
  mission,
37383
+ contact_path: contactPath,
37315
37384
  spend_policy: resolveCliSpendPolicy()
37316
37385
  }));
37317
37386
  } else {
@@ -37464,6 +37533,7 @@ function registerProve(program3) {
37464
37533
  org_id: ctx.orgId ?? null,
37465
37534
  agent_id: ctx.agentId ?? null,
37466
37535
  mission,
37536
+ contact_path: contactPath,
37467
37537
  mutation_mode: mutationMode,
37468
37538
  widget_public_key_present: Boolean(ctx.widgetPublicKey),
37469
37539
  conversation_id: ctx.conversationId ?? null,
@@ -39196,6 +39266,12 @@ function classifyRun(input) {
39196
39266
  if (hasCommandReason(new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i"))) {
39197
39267
  return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
39198
39268
  }
39269
+ if (hasCommandReason(/provider_capacity_blocked/i)) {
39270
+ return { status: "hold", reasonCode: "provider_capacity_blocked" };
39271
+ }
39272
+ if (hasCommandReason(/byon_voice_number_not_configured/i)) {
39273
+ return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
39274
+ }
39199
39275
  if (hasCommandReason(/contact_phone_provisioning_failed/i)) {
39200
39276
  return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
39201
39277
  }
@@ -39230,12 +39306,18 @@ ${stderr}`;
39230
39306
  if (/ENV_NETWORK_DNS_BLOCK|Could not resolve host|npm ping.*timeout|NO_EXECUTABLE_INSTALL/i.test(combined)) {
39231
39307
  return { status: "hold", reasonCode: "codex_network_dns_blocked" };
39232
39308
  }
39233
- if (/contact_phone_provisioning_failed/i.test(combined)) {
39234
- return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
39235
- }
39236
39309
  if (new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i").test(combined)) {
39237
39310
  return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
39238
39311
  }
39312
+ if (/provider_capacity_blocked/i.test(combined)) {
39313
+ return { status: "hold", reasonCode: "provider_capacity_blocked" };
39314
+ }
39315
+ if (/byon_voice_number_not_configured/i.test(combined)) {
39316
+ return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
39317
+ }
39318
+ if (/contact_phone_provisioning_failed/i.test(combined)) {
39319
+ return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
39320
+ }
39239
39321
  if (/voice_contact_expected_no_spend_hold/i.test(combined)) {
39240
39322
  return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
39241
39323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f-o-h/cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -3,7 +3,7 @@
3
3
  "$id": "https://frontofhouse.okii.uk/schemas/cli-envelope.schema.json",
4
4
  "title": "FOH CLI Envelope",
5
5
  "type": "object",
6
- "required": ["schema_version", "ok", "status", "reason_code", "summary", "ids", "checks", "artifacts", "next_commands"],
6
+ "required": ["schema_version", "ok", "status", "reason_code", "summary", "ids", "checks", "artifacts", "spend_class", "safe_to_retry", "proof_artifacts", "operator_note", "next_commands"],
7
7
  "properties": {
8
8
  "schema_version": { "type": "string" },
9
9
  "ok": { "type": "boolean" },
@@ -13,6 +13,10 @@
13
13
  "ids": { "type": "object" },
14
14
  "checks": { "type": "array" },
15
15
  "artifacts": { "type": "object" },
16
+ "spend_class": { "type": ["string", "null"] },
17
+ "safe_to_retry": { "type": "boolean" },
18
+ "proof_artifacts": { "type": ["object", "string", "null"] },
19
+ "operator_note": { "type": ["string", "null"] },
16
20
  "next_commands": {
17
21
  "type": "array",
18
22
  "items": { "type": "string" }