@carrierllc/mcp 0.2.19 → 0.2.20

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.
Files changed (41) hide show
  1. package/README.md +22 -1
  2. package/dist/chunk-NHQDO2E7.js +461 -0
  3. package/dist/chunk-NHQDO2E7.js.map +1 -0
  4. package/dist/cli.js +822 -105
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.js +144 -315
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/plugin/carrier/commands/storefront.md +24 -12
  10. package/plugin/carrier/skills/carrier-operations/SKILL.md +4 -1
  11. package/templates/storefront/{next.config.ts → next.config.mjs} +2 -6
  12. package/templates/storefront/public/brand/logo.svg +13 -0
  13. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +12 -12
  14. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +5 -5
  15. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +29 -29
  16. package/templates/storefront/src/app/checkout/success/page.tsx +1 -1
  17. package/templates/storefront/src/app/contact/page.tsx +33 -25
  18. package/templates/storefront/src/app/dashboard/page.tsx +7 -4
  19. package/templates/storefront/src/app/globals.css +72 -82
  20. package/templates/storefront/src/app/help/page.tsx +22 -17
  21. package/templates/storefront/src/app/layout.tsx +15 -4
  22. package/templates/storefront/src/app/page.tsx +16 -7
  23. package/templates/storefront/src/app/shop/ShopClient.tsx +7 -3
  24. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +1 -1
  25. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +16 -16
  26. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +1 -1
  27. package/templates/storefront/src/brand.config.ts +3 -3
  28. package/templates/storefront/src/components/faq/FaqSection.tsx +23 -26
  29. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +37 -22
  30. package/templates/storefront/src/components/landing/FinalCTA.tsx +28 -0
  31. package/templates/storefront/src/components/landing/HeroSection.tsx +57 -24
  32. package/templates/storefront/src/components/landing/HowItWorks.tsx +44 -0
  33. package/templates/storefront/src/components/landing/PlanCard.tsx +58 -28
  34. package/templates/storefront/src/components/nav/StorefrontNav.tsx +59 -24
  35. package/templates/storefront/src/components/theme/BrandStyles.tsx +8 -0
  36. package/templates/storefront/src/components/theme/ThemeToggle.tsx +27 -0
  37. package/templates/storefront/src/components/theme/clerk-appearance.ts +33 -35
  38. package/templates/storefront/src/components/theme/theme-provider.tsx +25 -32
  39. package/templates/storefront/src/components/theme/theme-script.tsx +4 -7
  40. package/templates/storefront/src/lib/complete-email-sign-up.ts +3 -3
  41. package/templates/storefront/src/vendor/ui/brand.ts +6 -6
package/dist/cli.js CHANGED
@@ -1,9 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CARRIER_VERSION,
4
+ OCS_MAX_USAGE_WINDOW_DAYS,
5
+ clampUsagePeriod,
6
+ generateStorefrontLogo,
7
+ lastNDaysPeriod
8
+ } from "./chunk-NHQDO2E7.js";
2
9
 
3
10
  // src/cli/index.ts
4
11
  import { Command } from "commander";
5
- import * as p2 from "@clack/prompts";
6
- import pc3 from "picocolors";
12
+ import * as p3 from "@clack/prompts";
13
+ import pc4 from "picocolors";
7
14
  import { resolve as resolve2 } from "path";
8
15
 
9
16
  // src/cli/lib/brand.ts
@@ -224,12 +231,12 @@ async function replaceInTree(dir, subs) {
224
231
  }
225
232
  return touched;
226
233
  }
227
- async function exists(p3) {
228
- return existsSync2(p3);
234
+ async function exists(p4) {
235
+ return existsSync2(p4);
229
236
  }
230
- async function isDir(p3) {
237
+ async function isDir(p4) {
231
238
  try {
232
- return (await stat(p3)).isDirectory();
239
+ return (await stat(p4)).isDirectory();
233
240
  } catch {
234
241
  return false;
235
242
  }
@@ -446,8 +453,27 @@ ${socialBody}
446
453
  await writeFile(path, src);
447
454
  }
448
455
 
449
- // src/cli/lib/site.ts
456
+ // src/cli/lib/logo.ts
457
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
450
458
  import { join as join5 } from "path";
459
+ async function writeStorefrontLogo(target, brand) {
460
+ const logo = await generateStorefrontLogo({
461
+ name: brand.name,
462
+ accent: brand.colors.accent,
463
+ tagline: brand.tagline
464
+ });
465
+ const dir = join5(target, "public", "brand");
466
+ await mkdir2(dir, { recursive: true });
467
+ const svgPath = join5(dir, "logo.svg");
468
+ await writeFile2(svgPath, logo.svg, "utf8");
469
+ if (logo.pngBase64) {
470
+ await writeFile2(join5(dir, "logo.png"), Buffer.from(logo.pngBase64, "base64"));
471
+ }
472
+ return { path: svgPath, source: logo.source };
473
+ }
474
+
475
+ // src/cli/lib/site.ts
476
+ import { join as join6 } from "path";
451
477
  function slug(s) {
452
478
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "storefront";
453
479
  }
@@ -462,7 +488,7 @@ async function buildSite(target) {
462
488
  return r.ok;
463
489
  }
464
490
  async function loadStorefrontBrand(target, overrides) {
465
- const configPath = join5(target, "src", "brand.config.ts");
491
+ const configPath = join6(target, "src", "brand.config.ts");
466
492
  if (!await exists(configPath)) {
467
493
  return { ...CARRIER_BRAND, ...overrides };
468
494
  }
@@ -491,7 +517,7 @@ async function loadStorefrontBrand(target, overrides) {
491
517
  }
492
518
  async function deploySite(target, brand) {
493
519
  const projectName = slug(brand.name);
494
- const workerBundle = join5(target, ".open-next", "worker.js");
520
+ const workerBundle = join6(target, ".open-next", "worker.js");
495
521
  if (!await exists(workerBundle)) {
496
522
  return {
497
523
  ok: false,
@@ -510,7 +536,7 @@ async function deploySite(target, brand) {
510
536
 
511
537
  // src/cli/lib/status.ts
512
538
  import { homedir as homedir2 } from "os";
513
- import { join as join6 } from "path";
539
+ import { join as join7 } from "path";
514
540
  function detectEnvToken() {
515
541
  const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
516
542
  if (api.startsWith("ak_") || api.length > 0) {
@@ -555,10 +581,10 @@ function scanMcpServers(servers) {
555
581
  }
556
582
  async function probeMcpFromConfig() {
557
583
  const candidates = [
558
- join6(homedir2(), ".claude.json"),
559
- join6(claudeHome(), "settings.json"),
560
- join6(claudeHome(), ".mcp.json"),
561
- join6(process.cwd(), ".mcp.json")
584
+ join7(homedir2(), ".claude.json"),
585
+ join7(claudeHome(), "settings.json"),
586
+ join7(claudeHome(), ".mcp.json"),
587
+ join7(process.cwd(), ".mcp.json")
562
588
  ];
563
589
  let best = null;
564
590
  let configSeen = false;
@@ -743,6 +769,10 @@ import * as p from "@clack/prompts";
743
769
  import pc2 from "picocolors";
744
770
 
745
771
  // src/cli/lib/ask.ts
772
+ var DEFAULT_ERROR_HINTS = [
773
+ "Re-check the arguments: carrier <domain> <command> --help",
774
+ "Or open Claude and ask there with full tool routing"
775
+ ];
746
776
  function parseRpcBody(raw) {
747
777
  const trimmed = raw.trim();
748
778
  if (!trimmed) return {};
@@ -753,7 +783,7 @@ function parseRpcBody(raw) {
753
783
  if (!dataLine) return {};
754
784
  return JSON.parse(dataLine.slice(5).trim());
755
785
  }
756
- async function carrierAsk(intent) {
786
+ async function callMcpTool(tool, args, opts = {}) {
757
787
  const token = resolveCliToken();
758
788
  if (!token) {
759
789
  return {
@@ -761,7 +791,8 @@ async function carrierAsk(intent) {
761
791
  text: withNextStep("No headless token in the environment.", [
762
792
  "Interactive path: open Claude and say your intent \u2014 OAuth runs on first use.",
763
793
  "Or export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys.",
764
- 'Then retry: carrier ask "show fleet health"'
794
+ 'Then retry: carrier ask "show fleet health"',
795
+ "Or run a discrete command: carrier subscribers list --account-id 123"
765
796
  ])
766
797
  };
767
798
  }
@@ -781,7 +812,7 @@ async function carrierAsk(intent) {
781
812
  params: {
782
813
  protocolVersion: "2024-11-05",
783
814
  capabilities: {},
784
- clientInfo: { name: "carrier-cli", version: "0.2.19" }
815
+ clientInfo: { name: "carrier-cli", version: CARRIER_VERSION }
785
816
  }
786
817
  })
787
818
  });
@@ -819,7 +850,7 @@ async function carrierAsk(intent) {
819
850
  jsonrpc: "2.0",
820
851
  id: 2,
821
852
  method: "tools/call",
822
- params: { name: "carrier_ask", arguments: { intent } }
853
+ params: { name: tool, arguments: args }
823
854
  })
824
855
  });
825
856
  if (!res.ok) {
@@ -836,14 +867,11 @@ async function carrierAsk(intent) {
836
867
  if (json.error?.message) {
837
868
  return {
838
869
  ok: false,
839
- text: withNextStep(json.error.message, [
840
- "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
841
- "Or open Claude and ask there with full tool routing"
842
- ])
870
+ text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
843
871
  };
844
872
  }
845
873
  const parts = json.result?.content ?? [];
846
- const text3 = parts.map((p3) => p3.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
874
+ const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
847
875
  return { ok: !json.result?.isError, text: text3 };
848
876
  } catch (e) {
849
877
  const msg = e instanceof Error ? e.message : String(e);
@@ -856,6 +884,18 @@ async function carrierAsk(intent) {
856
884
  };
857
885
  }
858
886
  }
887
+ async function carrierAsk(intent) {
888
+ return callMcpTool(
889
+ "carrier_ask",
890
+ { intent },
891
+ {
892
+ errorHints: [
893
+ "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
894
+ "Or use a discrete command: carrier --help lists every domain"
895
+ ]
896
+ }
897
+ );
898
+ }
859
899
 
860
900
  // src/cli/lib/home.ts
861
901
  function mark(ok2) {
@@ -996,46 +1036,666 @@ async function runHome(opts) {
996
1036
  }
997
1037
  }
998
1038
 
1039
+ // src/cli/lib/domains.ts
1040
+ var ICCID = {
1041
+ flags: "--iccid <iccid>",
1042
+ description: "Subscriber ICCID",
1043
+ arg: "iccid",
1044
+ type: "string",
1045
+ required: true
1046
+ };
1047
+ var ACCOUNT_ID = (description) => ({
1048
+ flags: "--account-id <id>",
1049
+ description,
1050
+ arg: "accountId",
1051
+ type: "number"
1052
+ });
1053
+ var RESELLER_ID = {
1054
+ flags: "--reseller-id <id>",
1055
+ description: "Reseller ID (omit for the token owner's reseller)",
1056
+ arg: "resellerId",
1057
+ type: "number"
1058
+ };
1059
+ var PACKAGE_ID = {
1060
+ flags: "--package-id <id>",
1061
+ description: "Package ID from `carrier packages list`",
1062
+ arg: "packageId",
1063
+ type: "number",
1064
+ required: true
1065
+ };
1066
+ var TEMPLATE_ID = {
1067
+ flags: "--template-id <id>",
1068
+ description: "Template ID from `carrier templates list`",
1069
+ arg: "templateId",
1070
+ type: "number",
1071
+ required: true
1072
+ };
1073
+ var PERIOD = [
1074
+ {
1075
+ flags: "--start <date>",
1076
+ description: "Start date YYYY-MM-DD (default: 7 days ago)",
1077
+ arg: "startDate",
1078
+ type: "string"
1079
+ },
1080
+ {
1081
+ flags: "--end <date>",
1082
+ description: "End date YYYY-MM-DD, max 7 days from start (default: today)",
1083
+ arg: "endDate",
1084
+ type: "string"
1085
+ }
1086
+ ];
1087
+ var CLI_DOMAINS = [
1088
+ {
1089
+ name: "subscribers",
1090
+ summary: "Look up, inspect and change subscribers.",
1091
+ commands: [
1092
+ {
1093
+ name: "list",
1094
+ tool: "list_subscribers",
1095
+ summary: "List subscribers. OCS needs at least one of account/iccid/msisdn/imsi.",
1096
+ options: [
1097
+ ACCOUNT_ID("Filter by account ID"),
1098
+ { flags: "--iccid <iccid>", description: "Filter by ICCID", arg: "iccid", type: "string" },
1099
+ { flags: "--msisdn <msisdn>", description: "Filter by MSISDN", arg: "msisdn", type: "string" },
1100
+ { flags: "--imsi <imsi>", description: "Filter by IMSI", arg: "imsi", type: "string" },
1101
+ { flags: "--status <status>", description: "Filter by status, e.g. ACTIVE", arg: "status", type: "string" },
1102
+ { flags: "--limit <n>", description: "Max rows to return", arg: "limit", type: "number" },
1103
+ { flags: "--offset <n>", description: "Pagination offset", arg: "offset", type: "number" }
1104
+ ]
1105
+ },
1106
+ {
1107
+ name: "get",
1108
+ tool: "get_subscriber",
1109
+ summary: "Full record for one subscriber by ICCID or MSISDN.",
1110
+ options: [
1111
+ { flags: "--iccid <iccid>", description: "Subscriber ICCID", arg: "iccid", type: "string" },
1112
+ { flags: "--msisdn <msisdn>", description: "Subscriber MSISDN", arg: "msisdn", type: "string" },
1113
+ {
1114
+ flags: "--gz-counter",
1115
+ description: "Include the green-zone byte counter",
1116
+ arg: "with_gz_counter",
1117
+ type: "boolean"
1118
+ }
1119
+ ]
1120
+ },
1121
+ {
1122
+ name: "location",
1123
+ tool: "get_subscriber_location",
1124
+ summary: "Last-known location from the most recent cell tower usage.",
1125
+ options: [ICCID]
1126
+ },
1127
+ {
1128
+ name: "usage",
1129
+ tool: "subscriber_usage",
1130
+ summary: "Daily data, voice and SMS usage over a window of up to 7 days.",
1131
+ options: [ICCID, ...PERIOD],
1132
+ usageWindow: true
1133
+ },
1134
+ {
1135
+ name: "events",
1136
+ tool: "subscriber_network_events",
1137
+ summary: "Network attach/detach events over a window of up to 7 days.",
1138
+ options: [ICCID, ...PERIOD],
1139
+ usageWindow: true
1140
+ },
1141
+ {
1142
+ name: "active-period",
1143
+ tool: "subscriber_active_period",
1144
+ summary: "First and last usage dates for one subscriber.",
1145
+ options: [ICCID]
1146
+ },
1147
+ {
1148
+ name: "esim-status",
1149
+ tool: "esim_status_per_account",
1150
+ summary: "eSIM state counts per account: active, suspended, inventory.",
1151
+ options: [ACCOUNT_ID("Report on a single account"), RESELLER_ID]
1152
+ },
1153
+ {
1154
+ name: "set-status",
1155
+ tool: "modify_subscriber_status",
1156
+ summary: "Change a subscriber's status.",
1157
+ options: [
1158
+ ICCID,
1159
+ {
1160
+ flags: "--status <status>",
1161
+ description: "New status, e.g. ACTIVE or SUSPENDED",
1162
+ arg: "status",
1163
+ type: "string",
1164
+ required: true
1165
+ }
1166
+ ],
1167
+ write: true
1168
+ },
1169
+ {
1170
+ name: "set-balance",
1171
+ tool: "modify_subscriber_balance",
1172
+ summary: "Add to or replace a subscriber's balance.",
1173
+ options: [
1174
+ ICCID,
1175
+ {
1176
+ flags: "--amount <amount>",
1177
+ description: "Amount to add (adapt) or set to (set)",
1178
+ arg: "amount",
1179
+ type: "number",
1180
+ required: true
1181
+ },
1182
+ {
1183
+ flags: "--mode <mode>",
1184
+ description: "'adapt' adds or subtracts, 'set' replaces",
1185
+ arg: "mode",
1186
+ type: "string",
1187
+ required: true
1188
+ }
1189
+ ],
1190
+ write: true
1191
+ }
1192
+ ]
1193
+ },
1194
+ {
1195
+ name: "packages",
1196
+ summary: "Packages assigned to a subscriber.",
1197
+ commands: [
1198
+ {
1199
+ name: "list",
1200
+ tool: "list_subscriber_packages",
1201
+ summary: "Packages on one subscriber, with allowance and expiry.",
1202
+ options: [ICCID]
1203
+ },
1204
+ {
1205
+ name: "assign",
1206
+ tool: "assign_package",
1207
+ summary: "Assign a one-time package from a template.",
1208
+ options: [
1209
+ { ...ICCID, required: false },
1210
+ {
1211
+ flags: "--template-id <id>",
1212
+ description: "Package template ID from `carrier templates list`",
1213
+ arg: "packageTemplateId",
1214
+ type: "number",
1215
+ required: true
1216
+ },
1217
+ {
1218
+ flags: "--account-id <id>",
1219
+ description: "Auto-pick a free eSIM from this account instead of using --iccid",
1220
+ arg: "account_for_subs",
1221
+ type: "number"
1222
+ }
1223
+ ],
1224
+ write: true,
1225
+ requireOneOf: ["iccid", "account_for_subs"]
1226
+ },
1227
+ {
1228
+ name: "set-status",
1229
+ tool: "modify_package_status",
1230
+ summary: "Activate or deactivate one package without deleting it.",
1231
+ options: [
1232
+ ICCID,
1233
+ PACKAGE_ID,
1234
+ {
1235
+ flags: "--status <status>",
1236
+ description: "New package status, e.g. ACTIVE or INACTIVE",
1237
+ arg: "status",
1238
+ type: "string",
1239
+ required: true
1240
+ }
1241
+ ],
1242
+ write: true
1243
+ },
1244
+ {
1245
+ name: "set-expiry",
1246
+ tool: "modify_package_expiry",
1247
+ summary: "Move a package's expiry date. Pass an absolute date or a day count.",
1248
+ options: [
1249
+ ICCID,
1250
+ PACKAGE_ID,
1251
+ {
1252
+ flags: "--expires <date>",
1253
+ description: "Absolute expiry, ISO 8601 (e.g. 2026-06-01)",
1254
+ arg: "expirationDate",
1255
+ type: "string"
1256
+ },
1257
+ {
1258
+ flags: "--validity-days <n>",
1259
+ description: "Days from now until expiry (instead of --expires)",
1260
+ arg: "validity_days",
1261
+ type: "number"
1262
+ }
1263
+ ],
1264
+ write: true
1265
+ },
1266
+ {
1267
+ name: "set-limits",
1268
+ tool: "modify_package_limits",
1269
+ summary: "Change data, voice or SMS ceilings on an assigned package.",
1270
+ options: [
1271
+ ICCID,
1272
+ PACKAGE_ID,
1273
+ {
1274
+ flags: "--limits <json>",
1275
+ description: `New limits as JSON, e.g. '{"dataLimit":5368709120}'`,
1276
+ arg: "limits",
1277
+ type: "string",
1278
+ required: true
1279
+ }
1280
+ ],
1281
+ write: true
1282
+ },
1283
+ {
1284
+ name: "delete",
1285
+ tool: "delete_subscriber_package",
1286
+ summary: "Permanently remove one package from a subscriber.",
1287
+ options: [ICCID, PACKAGE_ID],
1288
+ write: true
1289
+ }
1290
+ ]
1291
+ },
1292
+ {
1293
+ name: "templates",
1294
+ summary: "The package template catalog.",
1295
+ commands: [
1296
+ {
1297
+ name: "list",
1298
+ tool: "list_package_templates",
1299
+ summary: "Browse package templates available for assignment.",
1300
+ options: [ACCOUNT_ID("Filter templates visible to one account")]
1301
+ },
1302
+ {
1303
+ name: "create",
1304
+ tool: "create_package_template",
1305
+ summary: "Create a package template from a JSON configuration.",
1306
+ options: [
1307
+ {
1308
+ flags: "--template <json>",
1309
+ description: `Template config as JSON, e.g. '{"name":"Europe 5GB","dataLimit":5368709120}'`,
1310
+ arg: "template",
1311
+ type: "string",
1312
+ required: true
1313
+ }
1314
+ ],
1315
+ write: true
1316
+ },
1317
+ {
1318
+ name: "set-core",
1319
+ tool: "modify_template_core",
1320
+ summary: "Change name, limits, price, validity or zone on a template.",
1321
+ options: [
1322
+ TEMPLATE_ID,
1323
+ {
1324
+ flags: "--changes <json>",
1325
+ description: `Core fields as JSON, e.g. '{"name":"Europe 5GB"}'`,
1326
+ arg: "changes",
1327
+ type: "string",
1328
+ required: true
1329
+ }
1330
+ ],
1331
+ write: true
1332
+ },
1333
+ {
1334
+ name: "set-recurring",
1335
+ tool: "modify_template_recurring",
1336
+ summary: "Change auto-renewal settings on a template.",
1337
+ options: [
1338
+ TEMPLATE_ID,
1339
+ {
1340
+ flags: "--changes <json>",
1341
+ description: `Recurring fields as JSON, e.g. '{"periodicity":"monthly"}'`,
1342
+ arg: "changes",
1343
+ type: "string",
1344
+ required: true
1345
+ }
1346
+ ],
1347
+ write: true
1348
+ },
1349
+ {
1350
+ name: "set-throttling",
1351
+ tool: "modify_template_throttling",
1352
+ summary: "Change throttling thresholds. Applies to existing packages too.",
1353
+ options: [
1354
+ TEMPLATE_ID,
1355
+ {
1356
+ flags: "--changes <json>",
1357
+ description: `Throttling fields as JSON, e.g. '{"throttlingActive":true}'`,
1358
+ arg: "changes",
1359
+ type: "string",
1360
+ required: true
1361
+ }
1362
+ ],
1363
+ write: true
1364
+ }
1365
+ ]
1366
+ },
1367
+ {
1368
+ name: "zones",
1369
+ summary: "Location zones and destination lists.",
1370
+ commands: [
1371
+ {
1372
+ name: "list",
1373
+ tool: "list_detailed_location_zones",
1374
+ summary: "Location zones with countries and operators. Prefer this over `elements`.",
1375
+ options: [RESELLER_ID]
1376
+ },
1377
+ {
1378
+ name: "elements",
1379
+ tool: "list_location_zones",
1380
+ summary: "Raw zone elements. Upstream OCS returns malformed rows for some zones.",
1381
+ options: [
1382
+ {
1383
+ flags: "--zone-id <id>",
1384
+ description: "Filter to one location zone",
1385
+ arg: "locationZoneId",
1386
+ type: "number"
1387
+ }
1388
+ ]
1389
+ },
1390
+ {
1391
+ name: "create",
1392
+ tool: "create_location_zone",
1393
+ summary: "Create a location zone from a JSON configuration.",
1394
+ options: [
1395
+ {
1396
+ flags: "--zone <json>",
1397
+ description: `Zone config as JSON, e.g. '{"name":"Europe","countries":["NL","DE"]}'`,
1398
+ arg: "zone",
1399
+ type: "string",
1400
+ required: true
1401
+ }
1402
+ ],
1403
+ write: true
1404
+ },
1405
+ {
1406
+ name: "destinations",
1407
+ tool: "list_destination_lists",
1408
+ summary: "Destination list catalog for voice and SMS packages.",
1409
+ options: [RESELLER_ID]
1410
+ },
1411
+ {
1412
+ name: "prefixes",
1413
+ tool: "list_destination_prefixes",
1414
+ summary: "Dialling prefixes inside one destination list.",
1415
+ options: [
1416
+ {
1417
+ flags: "--destination-list-id <id>",
1418
+ description: "Destination list ID from `carrier zones destinations`",
1419
+ arg: "destinationListId",
1420
+ type: "number"
1421
+ }
1422
+ ]
1423
+ }
1424
+ ]
1425
+ },
1426
+ {
1427
+ name: "steering",
1428
+ summary: "Network steering lists and operator preference.",
1429
+ commands: [
1430
+ {
1431
+ name: "list",
1432
+ tool: "list_steering_lists",
1433
+ summary: "Steering lists configured for this reseller.",
1434
+ options: [RESELLER_ID]
1435
+ },
1436
+ {
1437
+ name: "assign",
1438
+ tool: "modify_subscriber_steering_list",
1439
+ summary: "Assign a steering list to a subscriber. Follow with `push`.",
1440
+ options: [
1441
+ ICCID,
1442
+ {
1443
+ flags: "--list-id <id>",
1444
+ description: "Steering list ID from `carrier steering list`",
1445
+ arg: "steeringListId",
1446
+ type: "number",
1447
+ required: true
1448
+ }
1449
+ ],
1450
+ write: true
1451
+ },
1452
+ {
1453
+ name: "push",
1454
+ tool: "push_steering_to_subscriber",
1455
+ summary: "Push the assigned operator preference list to the device now.",
1456
+ options: [ICCID],
1457
+ write: true
1458
+ }
1459
+ ]
1460
+ },
1461
+ {
1462
+ name: "intelligence",
1463
+ summary: "Composite reports over the fleet.",
1464
+ commands: [
1465
+ {
1466
+ name: "fleet-health",
1467
+ tool: "fleet_health",
1468
+ summary: "eSIM counts, low-balance accounts and what needs attention.",
1469
+ options: [ACCOUNT_ID("Report on a single account")]
1470
+ },
1471
+ {
1472
+ name: "diagnose",
1473
+ tool: "diagnose_subscriber",
1474
+ summary: "Why one subscriber is offline, throttled or failing to attach.",
1475
+ options: [ICCID]
1476
+ },
1477
+ {
1478
+ name: "anomalies",
1479
+ tool: "detect_usage_anomalies",
1480
+ summary: "Usage spikes and burn rates that exhaust the package early.",
1481
+ options: [ICCID]
1482
+ },
1483
+ {
1484
+ name: "churn",
1485
+ tool: "churn_risk",
1486
+ summary: "Churn risk score with contributing factors.",
1487
+ options: [ICCID]
1488
+ },
1489
+ {
1490
+ name: "optimize",
1491
+ tool: "optimize_package",
1492
+ summary: "Better-fitting package for one subscriber, with the saving.",
1493
+ options: [ICCID]
1494
+ },
1495
+ {
1496
+ name: "coverage",
1497
+ tool: "audit_network_coverage",
1498
+ summary: "Networks subscribers actually attach to, against your steering lists.",
1499
+ options: [
1500
+ ACCOUNT_ID("Report on a single account"),
1501
+ { flags: "--limit <n>", description: "Subscribers to sample (default 50)", arg: "limit", type: "number" }
1502
+ ]
1503
+ },
1504
+ {
1505
+ name: "high-cost",
1506
+ tool: "high_cost_subscribers",
1507
+ summary: "Subscribers whose consumption erodes the margin.",
1508
+ options: [
1509
+ ACCOUNT_ID("Report on a single account"),
1510
+ { flags: "--limit <n>", description: "Subscribers to analyse (default 100)", arg: "limit", type: "number" },
1511
+ {
1512
+ flags: "--threshold-pct <n>",
1513
+ description: "Usage percentage that flags a subscriber (default 80)",
1514
+ arg: "thresholdPct",
1515
+ type: "number"
1516
+ }
1517
+ ]
1518
+ },
1519
+ {
1520
+ name: "marketing",
1521
+ tool: "marketing_intelligence",
1522
+ summary: "Where subscribers and revenue concentrate, and which markets grow.",
1523
+ options: [ACCOUNT_ID("Report on a single account")]
1524
+ },
1525
+ {
1526
+ name: "country-entry",
1527
+ tool: "detect_country_entry",
1528
+ summary: "Country a subscriber is in now, optionally diffed against the last known one.",
1529
+ options: [
1530
+ { ...ICCID, arg: "subscriber.iccid" },
1531
+ {
1532
+ flags: "--expected-country <iso2>",
1533
+ description: "Last known ISO 3166-1 alpha-2 country, e.g. NL",
1534
+ arg: "expectedCountry",
1535
+ type: "string"
1536
+ }
1537
+ ]
1538
+ }
1539
+ ]
1540
+ }
1541
+ ];
1542
+
1543
+ // src/cli/lib/capability.ts
1544
+ import * as p2 from "@clack/prompts";
1545
+ import pc3 from "picocolors";
1546
+ var MAX_PRETTY_CHARS = 8e3;
1547
+ function optionKey(flags) {
1548
+ const long = flags.split(/[ ,|]+/).find((t) => t.startsWith("--"));
1549
+ if (!long) throw new Error(`Option "${flags}" has no long flag`);
1550
+ return long.replace(/^--/, "").split("-").map((part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
1551
+ }
1552
+ function setArg(target, path, value) {
1553
+ const parts = path.split(".");
1554
+ let node = target;
1555
+ for (const part of parts.slice(0, -1)) {
1556
+ const next = node[part];
1557
+ if (typeof next !== "object" || next === null) node[part] = {};
1558
+ node = node[part];
1559
+ }
1560
+ node[parts[parts.length - 1]] = value;
1561
+ }
1562
+ function coerce(opt, raw) {
1563
+ if (opt.type === "boolean") return { value: raw === true };
1564
+ if (opt.type === "number") {
1565
+ const n = Number(raw);
1566
+ if (!Number.isFinite(n)) return { error: `${opt.flags} expects a number, got "${String(raw)}"` };
1567
+ return { value: n };
1568
+ }
1569
+ return { value: String(raw) };
1570
+ }
1571
+ function buildToolArgs(cmd, opts) {
1572
+ const args = {};
1573
+ const errors = [];
1574
+ const notes = [];
1575
+ for (const opt of cmd.options) {
1576
+ const raw = opts[optionKey(opt.flags)];
1577
+ if (raw === void 0 || raw === false) {
1578
+ if (opt.required) errors.push(`Missing required ${opt.flags}`);
1579
+ continue;
1580
+ }
1581
+ const { value, error } = coerce(opt, raw);
1582
+ if (error) errors.push(error);
1583
+ else setArg(args, opt.arg, value);
1584
+ }
1585
+ if (cmd.requireOneOf && !cmd.requireOneOf.some((name) => args[name] !== void 0)) {
1586
+ const flags = cmd.options.filter((opt) => cmd.requireOneOf.includes(opt.arg)).map((opt) => opt.flags);
1587
+ errors.push(`Pass one of: ${flags.join(", ")}`);
1588
+ }
1589
+ if (cmd.usageWindow) {
1590
+ const start = args.startDate;
1591
+ const end = args.endDate;
1592
+ if (!start && !end) {
1593
+ const window = lastNDaysPeriod();
1594
+ args.startDate = window.start;
1595
+ args.endDate = window.end;
1596
+ notes.push(`Window defaulted to ${window.start} \u2026 ${window.end} (last ${OCS_MAX_USAGE_WINDOW_DAYS} days).`);
1597
+ } else if (!start || !end) {
1598
+ errors.push("Pass both --start and --end, or neither for the last 7 days");
1599
+ } else {
1600
+ const clamped = clampUsagePeriod(start, end);
1601
+ if (clamped.start !== start) {
1602
+ notes.push(
1603
+ `Start moved to ${clamped.start} \u2014 OCS rejects windows wider than ${OCS_MAX_USAGE_WINDOW_DAYS} days.`
1604
+ );
1605
+ }
1606
+ args.startDate = clamped.start;
1607
+ args.endDate = clamped.end;
1608
+ }
1609
+ }
1610
+ if (cmd.write) args.dry_run = true;
1611
+ return { args, errors, notes };
1612
+ }
1613
+ async function runCapability(domain, cmd, opts) {
1614
+ const label = `${domain.name} ${cmd.name}`;
1615
+ const quiet = opts.json === true;
1616
+ const { args, errors, notes } = buildToolArgs(cmd, opts);
1617
+ if (cmd.write && opts.commit === true) delete args.dry_run;
1618
+ if (errors.length) {
1619
+ const message = withNextStep(`Cannot run \`carrier ${label}\`.`, [
1620
+ ...errors,
1621
+ `See every flag: carrier ${label} --help`
1622
+ ]);
1623
+ if (quiet) console.error(message);
1624
+ else p2.log.error(message);
1625
+ process.exitCode = 1;
1626
+ return;
1627
+ }
1628
+ const dryRun = cmd.write === true && args.dry_run === true;
1629
+ const warnings = dryRun ? [...notes, "Dry run \u2014 nothing changes. Re-run with --commit to apply."] : notes;
1630
+ for (const note4 of warnings) {
1631
+ if (quiet) console.error(note4);
1632
+ else p2.log.info(pc3.dim(note4));
1633
+ }
1634
+ const s = quiet ? null : p2.spinner();
1635
+ s?.start(`Calling ${cmd.tool}`);
1636
+ const result = await callMcpTool(cmd.tool, args, {
1637
+ errorHints: [
1638
+ `Check the arguments: carrier ${label} --help`,
1639
+ 'Or describe the task in words: carrier ask "\u2026"'
1640
+ ]
1641
+ });
1642
+ s?.stop(result.ok ? "Done" : "Failed");
1643
+ if (!result.ok) {
1644
+ if (quiet) console.error(result.text);
1645
+ else p2.log.error(result.text);
1646
+ process.exitCode = 1;
1647
+ return;
1648
+ }
1649
+ if (quiet) {
1650
+ process.stdout.write(`${result.text}
1651
+ `);
1652
+ return;
1653
+ }
1654
+ const body = result.text.length > MAX_PRETTY_CHARS ? `${result.text.slice(0, MAX_PRETTY_CHARS)}
1655
+ \u2026truncated. Re-run with --json for the whole response.` : result.text;
1656
+ p2.note(body, cmd.tool);
1657
+ }
1658
+
999
1659
  // src/cli/index.ts
1000
- var VERSION = "0.2.19";
1660
+ var VERSION = CARRIER_VERSION;
1001
1661
  function header() {
1002
- p2.intro(`${pc3.bold(pc3.yellow("\u25C6 carrier"))} ${pc3.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
1662
+ p3.intro(`${pc4.bold(pc4.yellow("\u25C6 carrier"))} ${pc4.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
1003
1663
  }
1004
1664
  function ok(msg) {
1005
- p2.log.success(pc3.green(msg));
1665
+ p3.log.success(pc4.green(msg));
1006
1666
  }
1007
1667
  function info(msg) {
1008
- p2.log.info(msg);
1668
+ p3.log.info(msg);
1009
1669
  }
1010
1670
  function fail(msg, next) {
1011
- p2.log.error(msg);
1012
- p2.note(next.map((s) => `\u2192 ${s}`).join("\n"), "What to do next");
1671
+ p3.log.error(msg);
1672
+ p3.note(next.map((s) => `\u2192 ${s}`).join("\n"), "What to do next");
1013
1673
  }
1014
1674
  async function promptBrand(seed) {
1015
- const name = await p2.text({
1675
+ const name = await p3.text({
1016
1676
  message: "Brand name",
1017
1677
  placeholder: seed.name,
1018
1678
  defaultValue: seed.name
1019
1679
  });
1020
- if (p2.isCancel(name)) process.exit(0);
1021
- const domain = await p2.text({
1680
+ if (p3.isCancel(name)) process.exit(0);
1681
+ const domain = await p3.text({
1022
1682
  message: "Domain",
1023
1683
  placeholder: seed.domain,
1024
1684
  defaultValue: seed.domain
1025
1685
  });
1026
- if (p2.isCancel(domain)) process.exit(0);
1027
- const accent = await p2.text({
1686
+ if (p3.isCancel(domain)) process.exit(0);
1687
+ const accent = await p3.text({
1028
1688
  message: "Accent color (hex)",
1029
1689
  placeholder: seed.colors.accent,
1030
1690
  defaultValue: seed.colors.accent
1031
1691
  });
1032
- if (p2.isCancel(accent)) process.exit(0);
1033
- const supportEmail = await p2.text({
1692
+ if (p3.isCancel(accent)) process.exit(0);
1693
+ const supportEmail = await p3.text({
1034
1694
  message: "Support email",
1035
1695
  placeholder: `support@${domain}`,
1036
1696
  defaultValue: `support@${domain}`
1037
1697
  });
1038
- if (p2.isCancel(supportEmail)) process.exit(0);
1698
+ if (p3.isCancel(supportEmail)) process.exit(0);
1039
1699
  const accentDark = deriveAccentDark(accent, seed);
1040
1700
  const isCarrier = name === CARRIER_BRAND.name && domain === CARRIER_BRAND.domain;
1041
1701
  return {
@@ -1052,7 +1712,7 @@ async function promptBrand(seed) {
1052
1712
  };
1053
1713
  }
1054
1714
  async function doPluginInstall() {
1055
- const s = p2.spinner();
1715
+ const s = p3.spinner();
1056
1716
  s.start("Installing Carrier Claude Code plugin + zero-cred MCP");
1057
1717
  let r;
1058
1718
  try {
@@ -1069,13 +1729,13 @@ async function doPluginInstall() {
1069
1729
  s.stop("Plugin staged");
1070
1730
  ok(`Plugin \u2192 ${r.copiedTo}`);
1071
1731
  info(
1072
- `MCP: ${r.mcpAdded ? pc3.green("registered") : pc3.yellow("manual")} \xB7 marketplace: ${r.marketplaceAdded ? pc3.green("added") : pc3.yellow("manual")} \xB7 install: ${r.pluginInstalled ? pc3.green("done") : pc3.yellow("manual")}`
1732
+ `MCP: ${r.mcpAdded ? pc4.green("registered") : pc4.yellow("manual")} \xB7 marketplace: ${r.marketplaceAdded ? pc4.green("added") : pc4.yellow("manual")} \xB7 install: ${r.pluginInstalled ? pc4.green("done") : pc4.yellow("manual")}`
1073
1733
  );
1074
- p2.note(installAuthGuidance().join("\n"), "OAuth on first use (Entry A)");
1734
+ p3.note(installAuthGuidance().join("\n"), "OAuth on first use (Entry A)");
1075
1735
  if (!r.claudeFound || r.notes.length) {
1076
- p2.note(manualCommands().join("\n"), "Finish wiring (run in your terminal)");
1077
- if (r.notes.length) info(pc3.dim(r.notes.join("\n")));
1078
- p2.note(
1736
+ p3.note(manualCommands().join("\n"), "Finish wiring (run in your terminal)");
1737
+ if (r.notes.length) info(pc4.dim(r.notes.join("\n")));
1738
+ p3.note(
1079
1739
  [
1080
1740
  "You can still create an account now:",
1081
1741
  ` carrier open signup`,
@@ -1084,7 +1744,7 @@ async function doPluginInstall() {
1084
1744
  "Next"
1085
1745
  );
1086
1746
  } else {
1087
- p2.note(
1747
+ p3.note(
1088
1748
  [
1089
1749
  "Open Claude Code and say something like:",
1090
1750
  ' "Show my fleet health"',
@@ -1097,7 +1757,7 @@ async function doPluginInstall() {
1097
1757
  }
1098
1758
  }
1099
1759
  async function doSiteCreate(target, brand) {
1100
- const s = p2.spinner();
1760
+ const s = p3.spinner();
1101
1761
  s.start(`Scaffolding ${brand.name} storefront \u2192 ${target}`);
1102
1762
  try {
1103
1763
  await scaffoldStorefront(target, brand);
@@ -1112,15 +1772,25 @@ async function doSiteCreate(target, brand) {
1112
1772
  }
1113
1773
  s.stop("Storefront scaffolded");
1114
1774
  ok(`Created ${target} (white-labeled: ${brand.name}, accent ${brand.colors.accent})`);
1775
+ const logoSpin = p3.spinner();
1776
+ logoSpin.start("Generating storefront logo");
1777
+ try {
1778
+ const logo = await writeStorefrontLogo(target, brand);
1779
+ logoSpin.stop(`Logo ready (${logo.source})`);
1780
+ ok(logo.path);
1781
+ } catch (e) {
1782
+ logoSpin.stop("Logo skipped");
1783
+ info(pc4.yellow(String(e instanceof Error ? e.message : e)));
1784
+ }
1115
1785
  }
1116
1786
  async function maybeBuildDeploy(target, brand, opts) {
1117
1787
  if (opts.install) {
1118
- const s = p2.spinner();
1788
+ const s = p3.spinner();
1119
1789
  s.start("Installing storefront dependencies");
1120
1790
  const oki = await installDeps(target);
1121
1791
  s.stop(oki ? "Dependencies installed" : "Dependency install reported errors");
1122
1792
  if (!oki) {
1123
- p2.note(
1793
+ p3.note(
1124
1794
  [
1125
1795
  `cd ${target} && pnpm install`,
1126
1796
  "Fix any Node/pnpm version issues, then retry build"
@@ -1131,12 +1801,12 @@ async function maybeBuildDeploy(target, brand, opts) {
1131
1801
  }
1132
1802
  }
1133
1803
  if (opts.build) {
1134
- const s = p2.spinner();
1804
+ const s = p3.spinner();
1135
1805
  s.start("Building storefront (next build)");
1136
1806
  const okb = await buildSite(target);
1137
1807
  s.stop(okb ? "Build succeeded" : "Build failed \u2014 see output above");
1138
1808
  if (!okb) {
1139
- p2.note(
1809
+ p3.note(
1140
1810
  [`cd ${target}`, "pnpm build", "Check env keys in .env.local if the build mentions Clerk"].join("\n"),
1141
1811
  "Next"
1142
1812
  );
@@ -1144,13 +1814,13 @@ async function maybeBuildDeploy(target, brand, opts) {
1144
1814
  }
1145
1815
  }
1146
1816
  if (opts.deploy) {
1147
- const s = p2.spinner();
1817
+ const s = p3.spinner();
1148
1818
  s.start("Deploying to Cloudflare Workers");
1149
1819
  const r = await deploySite(target, brand);
1150
1820
  s.stop(r.ok ? `Deployed: ${r.projectName}` : "Deploy skipped");
1151
1821
  if (!r.ok && r.reason) {
1152
- info(pc3.yellow(r.reason));
1153
- p2.note(
1822
+ info(pc4.yellow(r.reason));
1823
+ p3.note(
1154
1824
  [
1155
1825
  "Install wrangler and login: npx wrangler login",
1156
1826
  `Then: carrier site deploy ${target}`
@@ -1162,26 +1832,31 @@ async function maybeBuildDeploy(target, brand, opts) {
1162
1832
  }
1163
1833
  async function interactiveSiteCreate() {
1164
1834
  const brand = await promptBrand(CARRIER_BRAND);
1165
- const dir = await p2.text({
1835
+ const dir = await p3.text({
1166
1836
  message: "Output directory",
1167
1837
  placeholder: "./storefront",
1168
1838
  defaultValue: "./storefront"
1169
1839
  });
1170
- if (p2.isCancel(dir)) return;
1840
+ if (p3.isCancel(dir)) return;
1171
1841
  const target = resolve2(dir);
1172
1842
  if (await exists(target)) {
1173
- const go = await p2.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1174
- if (p2.isCancel(go) || !go) {
1175
- p2.outro("Stopped. Re-run with a different directory.");
1843
+ const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1844
+ if (p3.isCancel(go) || !go) {
1845
+ p3.outro("Stopped. Re-run with a different directory.");
1176
1846
  return;
1177
1847
  }
1178
1848
  }
1179
1849
  await doSiteCreate(target, brand);
1180
- p2.outro(pc3.green(`Scaffolded. cd ${dir} && pnpm install && pnpm dev`));
1850
+ p3.outro(pc4.green(`Scaffolded. cd ${dir} && pnpm install && pnpm dev`));
1181
1851
  }
1182
1852
  var program = new Command();
1183
1853
  program.name("carrier").description(
1184
- "Carrier CLI \u2014 clear TUI for non-developers: status, OAuth-ready MCP install, storefront scaffold, and fleet NL helpers."
1854
+ [
1855
+ "Carrier CLI \u2014 status, OAuth-ready MCP install, storefront scaffold, and fleet control.",
1856
+ "",
1857
+ "Domains: " + CLI_DOMAINS.map((d) => d.name).join(", "),
1858
+ 'Anything they do not cover: carrier ask "\u2026"'
1859
+ ].join("\n")
1185
1860
  ).version(VERSION).action(async () => {
1186
1861
  header();
1187
1862
  await runHome({
@@ -1196,12 +1871,12 @@ program.command("init").description("Interactive home: status, install plugin+MC
1196
1871
  const brand = CARRIER_BRAND;
1197
1872
  const target = resolve2(o.dir);
1198
1873
  if (await exists(target)) {
1199
- info(pc3.yellow(`${target} exists \u2014 writing into it (--yes).`));
1874
+ info(pc4.yellow(`${target} exists \u2014 writing into it (--yes).`));
1200
1875
  }
1201
1876
  await doSiteCreate(target, brand);
1202
1877
  await maybeBuildDeploy(target, brand, { install: true, build: true, deploy: false });
1203
- p2.note(oauthFirstUseNote(), "Auth");
1204
- p2.note(
1878
+ p3.note(oauthFirstUseNote(), "Auth");
1879
+ p3.note(
1205
1880
  [
1206
1881
  `cd ${o.dir}`,
1207
1882
  `Edit src/brand.config.ts to re-brand anytime`,
@@ -1211,7 +1886,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
1211
1886
  ].join("\n"),
1212
1887
  "Next"
1213
1888
  );
1214
- p2.outro(pc3.green("Done. Your connectivity business is wired."));
1889
+ p3.outro(pc4.green("Done. Your connectivity business is wired."));
1215
1890
  return;
1216
1891
  }
1217
1892
  if (o.full) {
@@ -1221,14 +1896,14 @@ program.command("init").description("Interactive home: status, install plugin+MC
1221
1896
  const brand = await promptBrand(CARRIER_BRAND);
1222
1897
  const target = resolve2(o.dir);
1223
1898
  if (await exists(target)) {
1224
- const go = await p2.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1225
- if (p2.isCancel(go) || !go) {
1226
- p2.outro("Stopped. Re-run with --dir <new path>.");
1899
+ const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1900
+ if (p3.isCancel(go) || !go) {
1901
+ p3.outro("Stopped. Re-run with --dir <new path>.");
1227
1902
  return;
1228
1903
  }
1229
1904
  }
1230
1905
  await doSiteCreate(target, brand);
1231
- const next = await p2.select({
1906
+ const next = await p3.select({
1232
1907
  message: "Roll it out now?",
1233
1908
  options: [
1234
1909
  { value: "build", label: "Install deps + build" },
@@ -1237,14 +1912,14 @@ program.command("init").description("Interactive home: status, install plugin+MC
1237
1912
  ],
1238
1913
  initialValue: "build"
1239
1914
  });
1240
- if (p2.isCancel(next)) process.exit(0);
1915
+ if (p3.isCancel(next)) process.exit(0);
1241
1916
  await maybeBuildDeploy(target, brand, {
1242
1917
  install: next !== "none",
1243
1918
  build: next !== "none",
1244
1919
  deploy: next === "deploy"
1245
1920
  });
1246
- p2.note(oauthFirstUseNote(), "Auth");
1247
- p2.note(
1921
+ p3.note(oauthFirstUseNote(), "Auth");
1922
+ p3.note(
1248
1923
  [
1249
1924
  `cd ${o.dir}`,
1250
1925
  `Edit src/brand.config.ts to re-brand anytime`,
@@ -1253,7 +1928,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
1253
1928
  ].join("\n"),
1254
1929
  "Next"
1255
1930
  );
1256
- p2.outro(pc3.green("Done. Your connectivity business is wired."));
1931
+ p3.outro(pc4.green("Done. Your connectivity business is wired."));
1257
1932
  return;
1258
1933
  }
1259
1934
  await runHome({
@@ -1262,14 +1937,14 @@ program.command("init").description("Interactive home: status, install plugin+MC
1262
1937
  const brand = await promptBrand(CARRIER_BRAND);
1263
1938
  const target = resolve2(o.dir);
1264
1939
  if (await exists(target)) {
1265
- const go = await p2.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1266
- if (p2.isCancel(go) || !go) {
1267
- p2.log.info("Skipped storefront. Pick Install or Exit from the menu, or re-run with --dir.");
1940
+ const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1941
+ if (p3.isCancel(go) || !go) {
1942
+ p3.log.info("Skipped storefront. Pick Install or Exit from the menu, or re-run with --dir.");
1268
1943
  return;
1269
1944
  }
1270
1945
  }
1271
1946
  await doSiteCreate(target, brand);
1272
- const next = await p2.select({
1947
+ const next = await p3.select({
1273
1948
  message: "Roll it out now?",
1274
1949
  options: [
1275
1950
  { value: "build", label: "Install deps + build" },
@@ -1278,13 +1953,13 @@ program.command("init").description("Interactive home: status, install plugin+MC
1278
1953
  ],
1279
1954
  initialValue: "build"
1280
1955
  });
1281
- if (p2.isCancel(next)) return;
1956
+ if (p3.isCancel(next)) return;
1282
1957
  await maybeBuildDeploy(target, brand, {
1283
1958
  install: next !== "none",
1284
1959
  build: next !== "none",
1285
1960
  deploy: next === "deploy"
1286
1961
  });
1287
- p2.note(
1962
+ p3.note(
1288
1963
  [
1289
1964
  `cd ${o.dir}`,
1290
1965
  `pnpm dev`,
@@ -1299,25 +1974,25 @@ var plugin = program.command("plugin").description("Manage the Carrier Claude Co
1299
1974
  plugin.command("install").description("Install/register the Carrier plugin + zero-cred MCP (OAuth on first use).").action(async () => {
1300
1975
  header();
1301
1976
  await doPluginInstall();
1302
- p2.outro(pc3.green("Plugin ready. Restart Claude Code, then talk to your fleet."));
1977
+ p3.outro(pc4.green("Plugin ready. Restart Claude Code, then talk to your fleet."));
1303
1978
  });
1304
1979
  plugin.command("status").description("Show plugin + MCP registration + auth next steps.").action(async () => {
1305
1980
  header();
1306
1981
  const st = await gatherStatus();
1307
1982
  printStatus(st);
1308
- p2.note(oauthFirstUseNote(), "Auth");
1309
- p2.outro("");
1983
+ p3.note(oauthFirstUseNote(), "Auth");
1984
+ p3.outro("");
1310
1985
  });
1311
1986
  program.command("status").description("Show MCP / plugin / auth status and next steps.").action(async () => {
1312
1987
  header();
1313
- const s = p2.spinner();
1988
+ const s = p3.spinner();
1314
1989
  s.start("Checking status");
1315
1990
  const st = await gatherStatus();
1316
1991
  s.stop("Done");
1317
- p2.note(formatStatusBlock(st), "Status");
1318
- p2.note(formatNextSteps(st), "Next steps");
1319
- p2.note(oauthFirstUseNote(), "Auth");
1320
- p2.outro("");
1992
+ p3.note(formatStatusBlock(st), "Status");
1993
+ p3.note(formatNextSteps(st), "Next steps");
1994
+ p3.note(oauthFirstUseNote(), "Auth");
1995
+ p3.outro("");
1321
1996
  });
1322
1997
  var openCmd = program.command("open").description("Open Carrier account / product URLs in your browser.");
1323
1998
  for (const [name, url, desc] of [
@@ -1332,17 +2007,17 @@ for (const [name, url, desc] of [
1332
2007
  const r = await openUrl(url);
1333
2008
  if (r.ok) ok(r.hint);
1334
2009
  else {
1335
- p2.log.warn(r.hint);
1336
- p2.note(url, "Open this URL");
2010
+ p3.log.warn(r.hint);
2011
+ p3.note(url, "Open this URL");
1337
2012
  }
1338
- p2.note(accountLinksNote(), "Account links");
1339
- p2.outro("");
2013
+ p3.note(accountLinksNote(), "Account links");
2014
+ p3.outro("");
1340
2015
  });
1341
2016
  }
1342
2017
  program.command("examples").description("Print natural-language fleet prompts for Claude / MCP.").action(async () => {
1343
2018
  header();
1344
- p2.note(formatNlExamples(), "Talk to fleet \u2014 paste into Claude");
1345
- p2.note(
2019
+ p3.note(formatNlExamples(), "Talk to fleet \u2014 paste into Claude");
2020
+ p3.note(
1346
2021
  [
1347
2022
  "After `carrier plugin install` (or this menu \u2192 Install):",
1348
2023
  " 1. Open Claude Code",
@@ -1353,9 +2028,21 @@ program.command("examples").description("Print natural-language fleet prompts fo
1353
2028
  ].join("\n"),
1354
2029
  "How"
1355
2030
  );
1356
- p2.outro("");
2031
+ p3.note(
2032
+ [
2033
+ "With CARRIER_API_KEY set, every capability also has its own command:",
2034
+ ...CLI_DOMAINS.map((d) => ` carrier ${d.name} --help`.padEnd(34) + pc4.dim(d.summary)),
2035
+ "",
2036
+ " --json raw response on stdout, ready for jq",
2037
+ " --commit apply a change instead of previewing it"
2038
+ ].join("\n"),
2039
+ "Domain commands"
2040
+ );
2041
+ p3.outro("");
1357
2042
  });
1358
- program.command("ask").description('Optional headless NL: carrier ask "show fleet health" (needs CARRIER_API_KEY or OCS token).').argument("<intent...>", "Natural-language intent").action(async (parts) => {
2043
+ program.command("ask").description(
2044
+ 'Natural-language fallback for whatever the domain commands miss: carrier ask "show fleet health" (needs CARRIER_API_KEY or OCS token).'
2045
+ ).argument("<intent...>", "Natural-language intent").action(async (parts) => {
1359
2046
  header();
1360
2047
  const intent = parts.join(" ").trim();
1361
2048
  if (!intent) {
@@ -1366,19 +2053,33 @@ program.command("ask").description('Optional headless NL: carrier ask "show flee
1366
2053
  process.exitCode = 1;
1367
2054
  return;
1368
2055
  }
1369
- const s = p2.spinner();
2056
+ const s = p3.spinner();
1370
2057
  s.start("Asking Carrier MCP\u2026");
1371
2058
  const result = await carrierAsk(intent);
1372
2059
  s.stop(result.ok ? "Done" : "Failed");
1373
2060
  if (result.ok) {
1374
- p2.note(result.text.slice(0, 6e3), "Answer");
1375
- p2.outro("");
2061
+ p3.note(result.text.slice(0, 6e3), "Answer");
2062
+ p3.outro("");
1376
2063
  } else {
1377
- p2.log.error(result.text);
1378
- p2.outro(pc3.yellow("See next steps above."));
2064
+ p3.log.error(result.text);
2065
+ p3.outro(pc4.yellow("See next steps above."));
1379
2066
  process.exitCode = 1;
1380
2067
  }
1381
2068
  });
2069
+ for (const domain of CLI_DOMAINS) {
2070
+ const group = program.command(domain.name).description(domain.summary);
2071
+ for (const cmd of domain.commands) {
2072
+ const sub = group.command(cmd.name).description(cmd.write ? `${cmd.summary} Dry run unless --commit.` : cmd.summary);
2073
+ for (const opt of cmd.options) sub.option(opt.flags, opt.description);
2074
+ sub.option("--json", "Print the raw tool response on stdout and nothing else");
2075
+ if (cmd.write) sub.option("--commit", "Apply the change instead of previewing it");
2076
+ sub.action(async (opts) => {
2077
+ if (opts.json !== true) header();
2078
+ await runCapability(domain, cmd, opts);
2079
+ if (opts.json !== true) p3.outro("");
2080
+ });
2081
+ }
2082
+ }
1382
2083
  var site = program.command("site").description("Scaffold and deploy a white-labeled storefront.");
1383
2084
  site.command("create [dir]").description("Scaffold a white-labeled storefront from the Mango template.").option("--name <name>", "Brand name").option("--domain <domain>", "Domain").option("--accent <hex>", "Accent color").option("--yes", "Carrier defaults, no prompts").action(async (dir, o) => {
1384
2085
  header();
@@ -1408,9 +2109,9 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
1408
2109
  }
1409
2110
  const target = resolve2(dir ?? "./storefront");
1410
2111
  if (await exists(target)) {
1411
- const go = await p2.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
1412
- if (p2.isCancel(go) || !go) {
1413
- p2.outro("Stopped. Re-run with a different directory.");
2112
+ const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
2113
+ if (p3.isCancel(go) || !go) {
2114
+ p3.outro("Stopped. Re-run with a different directory.");
1414
2115
  return;
1415
2116
  }
1416
2117
  }
@@ -1420,7 +2121,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
1420
2121
  process.exitCode = 1;
1421
2122
  return;
1422
2123
  }
1423
- p2.outro(pc3.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
2124
+ p3.outro(pc4.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
1424
2125
  });
1425
2126
  site.command("deploy [dir]").description("Build + deploy a storefront to Cloudflare Workers.").option("--name <name>", "Cloudflare Worker name (defaults from brand)").action(async (dir, o) => {
1426
2127
  header();
@@ -1436,12 +2137,28 @@ site.command("deploy [dir]").description("Build + deploy a storefront to Cloudfl
1436
2137
  process.exitCode = 1;
1437
2138
  return;
1438
2139
  }
1439
- p2.outro("");
2140
+ p3.outro("");
2141
+ });
2142
+ site.command("logo [dir]").description("Generate a storefront logo (SVG always; PNG if an image key is on this box or Carrier).").action(async (dir) => {
2143
+ header();
2144
+ const target = resolve2(dir ?? "./storefront");
2145
+ try {
2146
+ const brand = await loadStorefrontBrand(target);
2147
+ const logo = await writeStorefrontLogo(target, brand);
2148
+ ok(`Logo (${logo.source}): ${logo.path}`);
2149
+ } catch (e) {
2150
+ fail(String(e instanceof Error ? e.message : e), [
2151
+ "Scaffold first: carrier site create"
2152
+ ]);
2153
+ process.exitCode = 1;
2154
+ return;
2155
+ }
2156
+ p3.outro("");
1440
2157
  });
1441
2158
  program.parseAsync(process.argv).catch((e) => {
1442
- console.error(pc3.red(String(e instanceof Error ? e.message : e)));
2159
+ console.error(pc4.red(String(e instanceof Error ? e.message : e)));
1443
2160
  console.error(
1444
- pc3.dim(
2161
+ pc4.dim(
1445
2162
  [
1446
2163
  "",
1447
2164
  "What to do next:",