@carrierllc/mcp 0.2.20 → 0.3.1

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/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  clampUsagePeriod,
6
6
  generateStorefrontLogo,
7
7
  lastNDaysPeriod
8
- } from "./chunk-NHQDO2E7.js";
8
+ } from "./chunk-TABEJ7SM.js";
9
9
 
10
10
  // src/cli/index.ts
11
11
  import { Command } from "commander";
@@ -773,6 +773,22 @@ var DEFAULT_ERROR_HINTS = [
773
773
  "Re-check the arguments: carrier <domain> <command> --help",
774
774
  "Or open Claude and ask there with full tool routing"
775
775
  ];
776
+ var DEFAULT_MCP_TIMEOUT_MS = 45e3;
777
+ function mcpTimeoutMs() {
778
+ const parsed = Number(process.env.CARRIER_MCP_TIMEOUT_MS);
779
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MCP_TIMEOUT_MS;
780
+ }
781
+ var McpTimeoutError = class extends Error {
782
+ constructor(timeoutMs) {
783
+ super(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
784
+ this.timeoutMs = timeoutMs;
785
+ this.name = "McpTimeoutError";
786
+ }
787
+ timeoutMs;
788
+ };
789
+ function isTimeoutish(err) {
790
+ return err instanceof McpTimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
791
+ }
776
792
  function parseRpcBody(raw) {
777
793
  const trimmed = raw.trim();
778
794
  if (!trimmed) return {};
@@ -801,79 +817,113 @@ async function callMcpTool(tool, args, opts = {}) {
801
817
  "Content-Type": "application/json",
802
818
  Accept: "application/json, text/event-stream"
803
819
  };
820
+ const timeoutMs = mcpTimeoutMs();
821
+ const controller = new AbortController();
822
+ let deadlineTimer;
823
+ const deadline = new Promise((_resolve, reject) => {
824
+ deadlineTimer = setTimeout(() => {
825
+ controller.abort();
826
+ reject(new McpTimeoutError(timeoutMs));
827
+ }, timeoutMs);
828
+ });
829
+ const signal = controller.signal;
804
830
  try {
805
- const initRes = await fetch(MCP_URL, {
806
- method: "POST",
807
- headers,
808
- body: JSON.stringify({
809
- jsonrpc: "2.0",
810
- id: 1,
811
- method: "initialize",
812
- params: {
813
- protocolVersion: "2024-11-05",
814
- capabilities: {},
815
- clientInfo: { name: "carrier-cli", version: CARRIER_VERSION }
831
+ return await Promise.race([
832
+ (async () => {
833
+ const initRes = await fetch(MCP_URL, {
834
+ method: "POST",
835
+ headers,
836
+ signal,
837
+ body: JSON.stringify({
838
+ jsonrpc: "2.0",
839
+ id: 1,
840
+ method: "initialize",
841
+ params: {
842
+ protocolVersion: "2024-11-05",
843
+ capabilities: {},
844
+ clientInfo: { name: "carrier-cli", version: CARRIER_VERSION }
845
+ }
846
+ })
847
+ });
848
+ if (initRes.status === 401 || initRes.status === 403) {
849
+ return {
850
+ ok: false,
851
+ text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
852
+ "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
853
+ "Or complete OCS onboarding: https://app.carrier.llc/onboarding",
854
+ "Interactive: use Claude + OAuth instead of a headless key"
855
+ ])
856
+ };
816
857
  }
817
- })
818
- });
819
- if (initRes.status === 401 || initRes.status === 403) {
820
- return {
821
- ok: false,
822
- text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
823
- "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
824
- "Or complete OCS onboarding: https://app.carrier.llc/onboarding",
825
- "Interactive: use Claude + OAuth instead of a headless key"
826
- ])
827
- };
828
- }
829
- const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
830
- await initRes.arrayBuffer().catch(() => void 0);
831
- if (!sessionId) {
832
- return {
833
- ok: false,
834
- text: withNextStep("MCP initialize did not return a session id.", [
835
- "Check network access to https://mcp.carrier.llc/mcp",
836
- "Retry later, or use Claude with the registered MCP (OAuth path)"
837
- ])
838
- };
839
- }
840
- const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
841
- await fetch(MCP_URL, {
842
- method: "POST",
843
- headers: sessionHeaders,
844
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
845
- }).catch(() => void 0);
846
- const res = await fetch(MCP_URL, {
847
- method: "POST",
848
- headers: sessionHeaders,
849
- body: JSON.stringify({
850
- jsonrpc: "2.0",
851
- id: 2,
852
- method: "tools/call",
853
- params: { name: tool, arguments: args }
854
- })
855
- });
856
- if (!res.ok) {
857
- const snippet = (await res.text().catch(() => "")).slice(0, 240);
858
- return {
859
- ok: false,
860
- text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
861
- "Check network access to https://mcp.carrier.llc/mcp",
862
- "Retry later, or use Claude with the registered MCP (OAuth path)"
863
- ])
864
- };
865
- }
866
- const json = parseRpcBody(await res.text());
867
- if (json.error?.message) {
858
+ const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
859
+ await initRes.arrayBuffer().catch(() => void 0);
860
+ if (!sessionId) {
861
+ return {
862
+ ok: false,
863
+ text: withNextStep("MCP initialize did not return a session id.", [
864
+ "Check network access to https://mcp.carrier.llc/mcp",
865
+ "Retry later, or use Claude with the registered MCP (OAuth path)"
866
+ ])
867
+ };
868
+ }
869
+ const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
870
+ await fetch(MCP_URL, {
871
+ method: "POST",
872
+ headers: sessionHeaders,
873
+ signal,
874
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
875
+ }).catch(() => void 0);
876
+ const res = await fetch(MCP_URL, {
877
+ method: "POST",
878
+ headers: sessionHeaders,
879
+ signal,
880
+ body: JSON.stringify({
881
+ jsonrpc: "2.0",
882
+ id: 2,
883
+ method: "tools/call",
884
+ params: { name: tool, arguments: args }
885
+ })
886
+ });
887
+ if (!res.ok) {
888
+ const snippet = (await res.text().catch(() => "")).slice(0, 240);
889
+ return {
890
+ ok: false,
891
+ text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
892
+ "Check network access to https://mcp.carrier.llc/mcp",
893
+ "Retry later, or use Claude with the registered MCP (OAuth path)"
894
+ ])
895
+ };
896
+ }
897
+ const json = parseRpcBody(await res.text());
898
+ if (json.error?.message) {
899
+ return {
900
+ ok: false,
901
+ text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
902
+ };
903
+ }
904
+ const parts = json.result?.content ?? [];
905
+ const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
906
+ return { ok: !json.result?.isError, text: text3 };
907
+ })(),
908
+ deadline
909
+ ]);
910
+ } catch (e) {
911
+ if (isTimeoutish(e)) {
912
+ const secs = Math.round(timeoutMs / 1e3);
868
913
  return {
869
914
  ok: false,
870
- text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
915
+ text: withNextStep(
916
+ `MCP call to ${tool} timed out after ${secs}s (no response).`,
917
+ [
918
+ "The tool is likely returning more data than the MCP transport can deliver \u2014 see issue #663",
919
+ "Narrow the request (a specific --account-id / --iccid returns far less than a whole-fleet call)",
920
+ `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
921
+ "Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
922
+ "Or open Claude and ask there with full tool routing"
923
+ ]
924
+ )
871
925
  };
872
926
  }
873
- const parts = json.result?.content ?? [];
874
- const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
875
- return { ok: !json.result?.isError, text: text3 };
876
- } catch (e) {
877
927
  const msg = e instanceof Error ? e.message : String(e);
878
928
  return {
879
929
  ok: false,
@@ -882,6 +932,8 @@ async function callMcpTool(tool, args, opts = {}) {
882
932
  "Use the interactive Claude path while offline from this machine"
883
933
  ])
884
934
  };
935
+ } finally {
936
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
885
937
  }
886
938
  }
887
939
  async function carrierAsk(intent) {
@@ -1188,6 +1240,169 @@ var CLI_DOMAINS = [
1188
1240
  }
1189
1241
  ],
1190
1242
  write: true
1243
+ },
1244
+ {
1245
+ name: "sim-status show",
1246
+ tool: "get_sim_provider_status",
1247
+ summary: "SIM/eSIM status at the SIM provider level, distinct from OCS status.",
1248
+ options: [ICCID]
1249
+ },
1250
+ {
1251
+ name: "sim-status set",
1252
+ tool: "change_sim_status",
1253
+ summary: "Change the SIM/eSIM status at the SIM provider level. DELETED is irreversible.",
1254
+ options: [
1255
+ ICCID,
1256
+ {
1257
+ flags: "--status <status>",
1258
+ description: "New SIM status: ENABLED, DISABLED or DELETED (irreversible)",
1259
+ arg: "simStatus",
1260
+ type: "string",
1261
+ required: true
1262
+ }
1263
+ ],
1264
+ write: true
1265
+ },
1266
+ {
1267
+ name: "phone-number set",
1268
+ tool: "affect_subscriber_phone_number",
1269
+ summary: "Assign a phone number (MSISDN) to a subscriber.",
1270
+ options: [
1271
+ ICCID,
1272
+ {
1273
+ flags: "--phone-number <e164>",
1274
+ description: "E.164 phone number to assign, e.g. +31612345678",
1275
+ arg: "phone_number",
1276
+ type: "string",
1277
+ required: true
1278
+ },
1279
+ {
1280
+ flags: "--phone-type <type>",
1281
+ description: "'fake' for test/dev MSISDNs, 'real' for production",
1282
+ arg: "phone_type",
1283
+ type: "string",
1284
+ required: true
1285
+ }
1286
+ ],
1287
+ write: true
1288
+ },
1289
+ {
1290
+ name: "contact set",
1291
+ tool: "modify_subscriber_contact_info",
1292
+ summary: "Update contact details on a subscriber. Omitted fields stay unchanged.",
1293
+ options: [
1294
+ ICCID,
1295
+ { flags: "--first-name <name>", description: "First name", arg: "firstName", type: "string" },
1296
+ { flags: "--last-name <name>", description: "Last name", arg: "lastName", type: "string" },
1297
+ { flags: "--company <name>", description: "Company name", arg: "company", type: "string" },
1298
+ { flags: "--email <email>", description: "Email address", arg: "email", type: "string" },
1299
+ { flags: "--phone-number <number>", description: "Phone number", arg: "phoneNumber", type: "string" }
1300
+ ],
1301
+ write: true
1302
+ },
1303
+ {
1304
+ name: "mobile-plan set",
1305
+ tool: "modify_subscriber_mobile_plan",
1306
+ summary: "Change the mobile pricing plan a subscriber is billed under.",
1307
+ options: [
1308
+ ICCID,
1309
+ {
1310
+ flags: "--plan-id <id>",
1311
+ description: "Mobile pricing plan ID",
1312
+ arg: "mobile_plan_id",
1313
+ type: "number",
1314
+ required: true
1315
+ }
1316
+ ],
1317
+ write: true
1318
+ },
1319
+ {
1320
+ name: "voip-plan set",
1321
+ tool: "modify_subscriber_voip_plan",
1322
+ summary: "Change the VoIP pricing plan assigned to a subscriber.",
1323
+ options: [
1324
+ ICCID,
1325
+ {
1326
+ flags: "--plan-id <id>",
1327
+ description: "VoIP pricing plan ID",
1328
+ arg: "voip_plan_id",
1329
+ type: "number",
1330
+ required: true
1331
+ }
1332
+ ],
1333
+ write: true
1334
+ },
1335
+ {
1336
+ name: "traffic set",
1337
+ tool: "set_subscriber_traffic_restrictions",
1338
+ summary: "Enable or disable data, calls and SMS per traffic type. Omit a flag to leave it unchanged.",
1339
+ options: [
1340
+ ICCID,
1341
+ { flags: "--data <bool>", description: "Allow data traffic (true or false)", arg: "dataAllowed", type: "boolean" },
1342
+ { flags: "--moc <bool>", description: "Allow outbound calls (true or false)", arg: "mocAllowed", type: "boolean" },
1343
+ { flags: "--mtc <bool>", description: "Allow inbound calls (true or false)", arg: "mtcAllowed", type: "boolean" },
1344
+ { flags: "--sms <bool>", description: "Allow outbound SMS (true or false)", arg: "smsMoAllowed", type: "boolean" }
1345
+ ],
1346
+ write: true
1347
+ },
1348
+ {
1349
+ name: "bitrate show",
1350
+ tool: "hlr_get_bitrate",
1351
+ summary: "Current HLR-level bandwidth cap for a subscriber.",
1352
+ options: [ICCID]
1353
+ },
1354
+ {
1355
+ name: "bitrate set",
1356
+ tool: "hlr_set_bitrate",
1357
+ summary: "Set an HLR-level bandwidth cap. Pass a numeric bps value or an OCS level.",
1358
+ options: [
1359
+ ICCID,
1360
+ {
1361
+ flags: "--bitrate <bps>",
1362
+ description: "Max bitrate in bits per second (0 removes the limit)",
1363
+ arg: "bitrate",
1364
+ type: "number"
1365
+ },
1366
+ {
1367
+ flags: "--bitrate-string <level>",
1368
+ description: "OCS bitrate level, e.g. KB_256, KB_1024, UNLIMITED (instead of --bitrate)",
1369
+ arg: "bitrate_string",
1370
+ type: "string"
1371
+ }
1372
+ ],
1373
+ write: true,
1374
+ requireOneOf: ["bitrate", "bitrate_string"]
1375
+ },
1376
+ {
1377
+ name: "greenzone reset",
1378
+ tool: "reset_subscriber_gz_counter",
1379
+ summary: "Reset the Green Zone byte counter to zero. Irreversible.",
1380
+ options: [ICCID],
1381
+ write: true
1382
+ },
1383
+ {
1384
+ name: "cell-location",
1385
+ tool: "get_subscriber_location_by_cell_id",
1386
+ summary: "Resolve a raw cell tower tuple to coordinates (Bridge4IP GeoSense).",
1387
+ options: [
1388
+ {
1389
+ flags: "--radio-type <type>",
1390
+ description: "Radio type: 2G, 3G, 4G, 5G or NB-IoT",
1391
+ arg: "radio_type",
1392
+ type: "string",
1393
+ required: true
1394
+ },
1395
+ { flags: "--mcc <mcc>", description: "Mobile Country Code", arg: "mcc", type: "number", required: true },
1396
+ { flags: "--mnc <mnc>", description: "Mobile Network Code", arg: "mnc", type: "number", required: true },
1397
+ { flags: "--lac <lac>", description: "Location Area Code", arg: "lac", type: "number", required: true },
1398
+ { flags: "--cell-id <id>", description: "Cell tower ID (strongly recommended)", arg: "cell_id", type: "number" },
1399
+ {
1400
+ flags: "--signal-strength <dbm>",
1401
+ description: "Signal strength in dBm, e.g. -89 (improves accuracy)",
1402
+ arg: "signal_strength",
1403
+ type: "number"
1404
+ }
1405
+ ]
1191
1406
  }
1192
1407
  ]
1193
1408
  },
@@ -1280,6 +1495,87 @@ var CLI_DOMAINS = [
1280
1495
  ],
1281
1496
  write: true
1282
1497
  },
1498
+ {
1499
+ name: "assign-recurring",
1500
+ tool: "assign_recurring_package",
1501
+ summary: "Assign an auto-renewing package from a recurring-enabled template.",
1502
+ options: [
1503
+ ICCID,
1504
+ {
1505
+ flags: "--template-id <id>",
1506
+ description: "Recurring package template ID from `carrier templates list`",
1507
+ arg: "packageTemplateId",
1508
+ type: "number",
1509
+ required: true
1510
+ },
1511
+ {
1512
+ flags: "--activate-at-first-use",
1513
+ description: "Activate on first network usage (instead of --start-time)",
1514
+ arg: "activation_at_first_use",
1515
+ type: "boolean"
1516
+ },
1517
+ {
1518
+ flags: "--start-time <iso>",
1519
+ description: "Scheduled activation, ISO 8601 UTC (instead of --activate-at-first-use)",
1520
+ arg: "start_time_utc",
1521
+ type: "string"
1522
+ }
1523
+ ],
1524
+ write: true
1525
+ },
1526
+ {
1527
+ name: "recurring set",
1528
+ tool: "stop_resume_recurring_package",
1529
+ summary: "Pause or restart the auto-renewal of a recurring package.",
1530
+ options: [
1531
+ ICCID,
1532
+ PACKAGE_ID,
1533
+ {
1534
+ flags: "--action <action>",
1535
+ description: "'stop' halts future renewals, 'resume' re-enables them",
1536
+ arg: "action",
1537
+ type: "string",
1538
+ required: true
1539
+ }
1540
+ ],
1541
+ write: true
1542
+ },
1543
+ {
1544
+ name: "active-period set",
1545
+ tool: "modify_subscriber_package_active_period",
1546
+ summary: "Move the start and/or end of a package's active period.",
1547
+ options: [
1548
+ ICCID,
1549
+ {
1550
+ flags: "--package-id <id>",
1551
+ description: "Package ID from `carrier packages list`",
1552
+ arg: "package_id",
1553
+ type: "number",
1554
+ required: true
1555
+ },
1556
+ {
1557
+ flags: "--start <date>",
1558
+ description: "New start date, ISO 8601 (e.g. 2026-09-01)",
1559
+ arg: "start_date",
1560
+ type: "string"
1561
+ },
1562
+ {
1563
+ flags: "--end <date>",
1564
+ description: "New end date, ISO 8601 (e.g. 2026-09-30)",
1565
+ arg: "end_date",
1566
+ type: "string"
1567
+ }
1568
+ ],
1569
+ write: true,
1570
+ requireOneOf: ["start_date", "end_date"]
1571
+ },
1572
+ {
1573
+ name: "clean",
1574
+ tool: "clean_all_packages",
1575
+ summary: "Remove ALL packages from a subscriber. Irreversible.",
1576
+ options: [ICCID],
1577
+ write: true
1578
+ },
1283
1579
  {
1284
1580
  name: "delete",
1285
1581
  tool: "delete_subscriber_package",
@@ -1402,6 +1698,28 @@ var CLI_DOMAINS = [
1402
1698
  ],
1403
1699
  write: true
1404
1700
  },
1701
+ {
1702
+ name: "network-profile set",
1703
+ tool: "change_network_profile_of_location_zone",
1704
+ summary: "Attach or change the network profile on an existing location zone.",
1705
+ options: [
1706
+ {
1707
+ flags: "--zone-id <id>",
1708
+ description: "Location zone ID from `carrier zones list`",
1709
+ arg: "location_zone_id",
1710
+ type: "number",
1711
+ required: true
1712
+ },
1713
+ {
1714
+ flags: "--network-profile-id <id>",
1715
+ description: "Network profile ID from `carrier network-profiles list`",
1716
+ arg: "network_profile_id",
1717
+ type: "number",
1718
+ required: true
1719
+ }
1720
+ ],
1721
+ write: true
1722
+ },
1405
1723
  {
1406
1724
  name: "destinations",
1407
1725
  tool: "list_destination_lists",
@@ -1458,6 +1776,193 @@ var CLI_DOMAINS = [
1458
1776
  }
1459
1777
  ]
1460
1778
  },
1779
+ {
1780
+ name: "accounts",
1781
+ summary: "Reseller accounts: balances and subscriber moves.",
1782
+ commands: [
1783
+ {
1784
+ name: "list",
1785
+ tool: "list_reseller_accounts",
1786
+ summary: "Accounts under a reseller with name, balance and type.",
1787
+ options: [RESELLER_ID]
1788
+ },
1789
+ {
1790
+ name: "balance set",
1791
+ tool: "modify_account_balance",
1792
+ summary: "Add to or replace an account's monetary balance.",
1793
+ options: [
1794
+ { ...ACCOUNT_ID("Account ID from `carrier accounts list`"), required: true },
1795
+ {
1796
+ flags: "--amount <amount>",
1797
+ description: "Amount to add (adapt) or set to (set)",
1798
+ arg: "amount",
1799
+ type: "number",
1800
+ required: true
1801
+ },
1802
+ {
1803
+ flags: "--mode <mode>",
1804
+ description: "'adapt' adds or subtracts, 'set' replaces",
1805
+ arg: "mode",
1806
+ type: "string",
1807
+ required: true
1808
+ }
1809
+ ],
1810
+ write: true
1811
+ },
1812
+ {
1813
+ name: "move-subscribers",
1814
+ tool: "move_subscriber_range_to_account",
1815
+ summary: "Move a contiguous ICCID range of subscribers to another account.",
1816
+ options: [
1817
+ {
1818
+ flags: "--iccid-from <iccid>",
1819
+ description: "Start ICCID of the range (inclusive)",
1820
+ arg: "iccidFrom",
1821
+ type: "string",
1822
+ required: true
1823
+ },
1824
+ {
1825
+ flags: "--iccid-to <iccid>",
1826
+ description: "End ICCID of the range (inclusive)",
1827
+ arg: "iccidTo",
1828
+ type: "string",
1829
+ required: true
1830
+ },
1831
+ { ...ACCOUNT_ID("Target account ID from `carrier accounts list`"), required: true }
1832
+ ],
1833
+ write: true
1834
+ }
1835
+ ]
1836
+ },
1837
+ {
1838
+ name: "reseller",
1839
+ summary: "The reseller the token belongs to.",
1840
+ commands: [
1841
+ {
1842
+ name: "show",
1843
+ tool: "get_reseller_info",
1844
+ summary: "Full reseller record: balance, pricing plans, contact and traffic config.",
1845
+ options: [RESELLER_ID]
1846
+ }
1847
+ ]
1848
+ },
1849
+ {
1850
+ name: "tariff",
1851
+ summary: "Wholesale tariffs: mobile and VoIP rates.",
1852
+ commands: [
1853
+ {
1854
+ name: "show",
1855
+ tool: "get_tariff",
1856
+ summary: "Per-country wholesale rates for data, voice and SMS.",
1857
+ options: [RESELLER_ID]
1858
+ },
1859
+ {
1860
+ name: "voip",
1861
+ tool: "list_subscriber_voip_tariff",
1862
+ summary: "VoIP tariff catalog for a reseller.",
1863
+ options: [
1864
+ {
1865
+ flags: "--reseller-id <id>",
1866
+ description: "Reseller ID (omit for the token owner's reseller)",
1867
+ arg: "reseller_id",
1868
+ type: "number"
1869
+ }
1870
+ ]
1871
+ },
1872
+ {
1873
+ name: "voip-rules",
1874
+ tool: "list_voip_tariff_rule",
1875
+ summary: "Rate rules inside one VoIP plan.",
1876
+ options: [
1877
+ {
1878
+ flags: "--voip-plan-id <id>",
1879
+ description: "VoIP plan ID from `carrier tariff voip`",
1880
+ arg: "voip_plan_id",
1881
+ type: "number",
1882
+ required: true
1883
+ }
1884
+ ]
1885
+ }
1886
+ ]
1887
+ },
1888
+ {
1889
+ name: "sms",
1890
+ summary: "Mobile-terminated SMS to subscribers.",
1891
+ commands: [
1892
+ {
1893
+ name: "send",
1894
+ tool: "send_sms",
1895
+ summary: "Send an MT SMS to one subscriber.",
1896
+ options: [
1897
+ ICCID,
1898
+ {
1899
+ flags: "--msisdn <msisdn>",
1900
+ description: "Target MSISDN in E.164 format",
1901
+ arg: "msisdn",
1902
+ type: "string",
1903
+ required: true
1904
+ },
1905
+ {
1906
+ flags: "--message <text>",
1907
+ description: "SMS text (160 chars GSM-7; 70 with emoji or non-Latin scripts)",
1908
+ arg: "message",
1909
+ type: "string",
1910
+ required: true
1911
+ },
1912
+ {
1913
+ flags: "--sender <sender>",
1914
+ description: "Sender ID or number shown on the device",
1915
+ arg: "sender",
1916
+ type: "string"
1917
+ }
1918
+ ],
1919
+ write: true
1920
+ }
1921
+ ]
1922
+ },
1923
+ {
1924
+ name: "sponsors",
1925
+ summary: "Sponsor networks backing the eSIM profiles.",
1926
+ commands: [
1927
+ {
1928
+ name: "list",
1929
+ tool: "list_sponsors",
1930
+ summary: "Sponsor carriers available to this reseller.",
1931
+ options: [RESELLER_ID]
1932
+ }
1933
+ ]
1934
+ },
1935
+ {
1936
+ name: "network-profiles",
1937
+ summary: "Roaming configuration profiles for zones and provisioning.",
1938
+ commands: [
1939
+ {
1940
+ name: "list",
1941
+ tool: "list_network_profiles",
1942
+ summary: "Network profiles available to this reseller.",
1943
+ options: []
1944
+ }
1945
+ ]
1946
+ },
1947
+ {
1948
+ name: "webhooks",
1949
+ summary: "Bridge4IP webhook and relay flag status.",
1950
+ commands: [
1951
+ {
1952
+ name: "show",
1953
+ tool: "carrier_webhook_config",
1954
+ summary: "Current relay flags (LU, Gy, VoIP, calls+SMS) and notification webhooks.",
1955
+ options: [
1956
+ {
1957
+ flags: "--reseller-id <id>",
1958
+ description: "Reseller ID (omit for the token owner's reseller)",
1959
+ arg: "reseller_id",
1960
+ type: "number"
1961
+ }
1962
+ ]
1963
+ }
1964
+ ]
1965
+ },
1461
1966
  {
1462
1967
  name: "intelligence",
1463
1968
  summary: "Composite reports over the fleet.",
@@ -1560,7 +2065,13 @@ function setArg(target, path, value) {
1560
2065
  node[parts[parts.length - 1]] = value;
1561
2066
  }
1562
2067
  function coerce(opt, raw) {
1563
- if (opt.type === "boolean") return { value: raw === true };
2068
+ if (opt.type === "boolean") {
2069
+ if (raw === true) return { value: true };
2070
+ const text3 = String(raw).toLowerCase();
2071
+ if (text3 === "true") return { value: true };
2072
+ if (text3 === "false") return { value: false };
2073
+ return { error: `${opt.flags} expects true or false, got "${String(raw)}"` };
2074
+ }
1564
2075
  if (opt.type === "number") {
1565
2076
  const n = Number(raw);
1566
2077
  if (!Number.isFinite(n)) return { error: `${opt.flags} expects a number, got "${String(raw)}"` };
@@ -2068,8 +2579,21 @@ program.command("ask").description(
2068
2579
  });
2069
2580
  for (const domain of CLI_DOMAINS) {
2070
2581
  const group = program.command(domain.name).description(domain.summary);
2582
+ const subgroups = /* @__PURE__ */ new Map();
2071
2583
  for (const cmd of domain.commands) {
2072
- const sub = group.command(cmd.name).description(cmd.write ? `${cmd.summary} Dry run unless --commit.` : cmd.summary);
2584
+ const parts = cmd.name.split(" ");
2585
+ let parent = group;
2586
+ let prefix = "";
2587
+ for (const part of parts.slice(0, -1)) {
2588
+ prefix = prefix ? `${prefix} ${part}` : part;
2589
+ let nested = subgroups.get(prefix);
2590
+ if (!nested) {
2591
+ nested = parent.command(part).description(`${part} subcommands`);
2592
+ subgroups.set(prefix, nested);
2593
+ }
2594
+ parent = nested;
2595
+ }
2596
+ const sub = parent.command(parts[parts.length - 1]).description(cmd.write ? `${cmd.summary} Dry run unless --commit.` : cmd.summary);
2073
2597
  for (const opt of cmd.options) sub.option(opt.flags, opt.description);
2074
2598
  sub.option("--json", "Print the raw tool response on stdout and nothing else");
2075
2599
  if (cmd.write) sub.option("--commit", "Apply the change instead of previewing it");