@carrierllc/mcp 0.2.17 → 0.2.19

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 (30) hide show
  1. package/README.md +21 -1
  2. package/dist/cli.js +893 -80
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +213 -36
  5. package/dist/index.js.map +1 -1
  6. package/package.json +10 -9
  7. package/plugin/.claude-plugin/marketplace.json +2 -2
  8. package/plugin/carrier/.claude-plugin/plugin.json +1 -1
  9. package/plugin/carrier/README.md +31 -4
  10. package/plugin/carrier/agents/carrier-billing-auditor.md +1 -1
  11. package/plugin/carrier/commands/billing.md +8 -1
  12. package/plugin/carrier/commands/provision.md +18 -8
  13. package/plugin/carrier/commands/wallet.md +31 -0
  14. package/plugin/carrier/skills/carrier-operations/SKILL.md +12 -5
  15. package/templates/storefront/package-lock.json +12722 -0
  16. package/templates/storefront/package.json +3 -3
  17. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +125 -0
  18. package/templates/storefront/src/app/activate/[orderId]/page.tsx +16 -12
  19. package/templates/storefront/src/app/api/checkout/claim/route.ts +30 -0
  20. package/templates/storefront/src/app/api/checkout/guest/route.ts +91 -0
  21. package/templates/storefront/src/app/api/profile/phone/route.ts +40 -0
  22. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +71 -13
  23. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +415 -0
  24. package/templates/storefront/src/app/checkout/success/page.tsx +59 -0
  25. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +199 -0
  26. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +18 -3
  27. package/templates/storefront/src/lib/checkout-order-claim.ts +141 -0
  28. package/templates/storefront/src/lib/complete-email-sign-up.ts +50 -0
  29. package/templates/storefront/src/lib/verify-checkout-session.ts +70 -0
  30. package/templates/storefront/src/middleware.ts +6 -0
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import * as p from "@clack/prompts";
6
- import pc from "picocolors";
5
+ import * as p2 from "@clack/prompts";
6
+ import pc3 from "picocolors";
7
7
  import { resolve as resolve2 } from "path";
8
8
 
9
9
  // src/cli/lib/brand.ts
@@ -224,12 +224,12 @@ async function replaceInTree(dir, subs) {
224
224
  }
225
225
  return touched;
226
226
  }
227
- async function exists(p2) {
228
- return existsSync2(p2);
227
+ async function exists(p3) {
228
+ return existsSync2(p3);
229
229
  }
230
- async function isDir(p2) {
230
+ async function isDir(p3) {
231
231
  try {
232
- return (await stat(p2)).isDirectory();
232
+ return (await stat(p3)).isDirectory();
233
233
  } catch {
234
234
  return false;
235
235
  }
@@ -242,10 +242,37 @@ function run(cmd, args, opts = {}) {
242
242
  const child = spawn(cmd, args, { cwd: opts.cwd, shell: false });
243
243
  let stdout = "";
244
244
  let stderr = "";
245
+ let settled = false;
246
+ const finish = (result) => {
247
+ if (settled) return;
248
+ settled = true;
249
+ resolve3(result);
250
+ };
251
+ let timer;
252
+ if (opts.timeoutMs && opts.timeoutMs > 0) {
253
+ timer = setTimeout(() => {
254
+ try {
255
+ child.kill("SIGTERM");
256
+ } catch {
257
+ }
258
+ finish({
259
+ ok: false,
260
+ code: null,
261
+ stdout,
262
+ stderr: stderr || `timeout after ${opts.timeoutMs}ms`
263
+ });
264
+ }, opts.timeoutMs);
265
+ }
245
266
  child.stdout?.on("data", (d) => stdout += d.toString());
246
267
  child.stderr?.on("data", (d) => stderr += d.toString());
247
- child.on("error", () => resolve3({ ok: false, code: null, stdout, stderr }));
248
- child.on("close", (code) => resolve3({ ok: code === 0, code, stdout, stderr }));
268
+ child.on("error", () => {
269
+ if (timer) clearTimeout(timer);
270
+ finish({ ok: false, code: null, stdout, stderr });
271
+ });
272
+ child.on("close", (code) => {
273
+ if (timer) clearTimeout(timer);
274
+ finish({ ok: code === 0, code, stdout, stderr });
275
+ });
249
276
  });
250
277
  }
251
278
  function runInherit(cmd, args, opts = {}) {
@@ -261,9 +288,41 @@ async function which(bin) {
261
288
  return r.ok && r.stdout.trim().length > 0;
262
289
  }
263
290
 
291
+ // src/cli/lib/urls.ts
292
+ import { spawn as spawn2 } from "child_process";
293
+ var MCP_URL = "https://mcp.carrier.llc/mcp";
294
+ var MCP_HOME = "https://mcp.carrier.llc";
295
+ var SIGN_UP_URL = "https://accounts.carrier.llc/sign-up";
296
+ var SIGN_IN_URL = "https://accounts.carrier.llc/sign-in";
297
+ var CONSOLE_URL = "https://app.carrier.llc";
298
+ var ONBOARDING_URL = "https://app.carrier.llc/onboarding";
299
+ async function openUrl(url) {
300
+ const platform = process.platform;
301
+ if (platform === "darwin") {
302
+ const r = await run("open", [url]);
303
+ return r.ok ? { ok: true, hint: `Opened ${url}` } : { ok: false, hint: `Could not open browser. Visit:
304
+ ${url}` };
305
+ }
306
+ if (platform === "win32") {
307
+ const r = await run("cmd", ["/c", "start", "", url]);
308
+ return r.ok ? { ok: true, hint: `Opened ${url}` } : { ok: false, hint: `Could not open browser. Visit:
309
+ ${url}` };
310
+ }
311
+ for (const bin of ["xdg-open", "open"]) {
312
+ const r = await run(bin, [url]);
313
+ if (r.ok) return { ok: true, hint: `Opened ${url}` };
314
+ }
315
+ try {
316
+ spawn2("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
317
+ return { ok: true, hint: `Launched browser for ${url}` };
318
+ } catch {
319
+ return { ok: false, hint: `Could not open browser. Visit:
320
+ ${url}` };
321
+ }
322
+ }
323
+
264
324
  // src/cli/lib/plugin.ts
265
325
  var MCP_NAME = "carrier";
266
- var MCP_URL = "https://mcp.carrier.llc/mcp";
267
326
  async function installPlugin() {
268
327
  const marketplace = pluginSourceDir();
269
328
  const pluginRoot = pluginManifestDir();
@@ -311,6 +370,15 @@ function manualCommands() {
311
370
  `claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`
312
371
  ];
313
372
  }
373
+ function installAuthGuidance() {
374
+ return [
375
+ `MCP URL (zero credentials): ${MCP_URL}`,
376
+ "OAuth on first use: when Claude first calls Carrier, your browser opens",
377
+ " Clerk sign-in / sign-up (Google, GitHub, or email). No API token to paste.",
378
+ "Stuck on auth? claude mcp auth carrier",
379
+ "Headless/CI only: org API key (ak_\u2026) from https://app.carrier.llc \u2192 Settings \u2192 API Keys"
380
+ ];
381
+ }
314
382
 
315
383
  // src/cli/lib/whitelabel.ts
316
384
  import { join as join4 } from "path";
@@ -440,42 +508,534 @@ async function deploySite(target, brand) {
440
508
  return r.ok ? { ok: true, projectName } : { ok: false, projectName, reason: "wrangler deploy failed \u2014 run `wrangler login` then retry `carrier site deploy`." };
441
509
  }
442
510
 
511
+ // src/cli/lib/status.ts
512
+ import { homedir as homedir2 } from "os";
513
+ import { join as join6 } from "path";
514
+ function detectEnvToken() {
515
+ const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
516
+ if (api.startsWith("ak_") || api.length > 0) {
517
+ return { present: true, kind: "api-key" };
518
+ }
519
+ const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim() || "";
520
+ if (ocs) return { present: true, kind: "ocs-token" };
521
+ return { present: false, kind: "none" };
522
+ }
523
+ function rankHit(name, url) {
524
+ let rank = 0;
525
+ if (name.toLowerCase() === MCP_NAME) rank += 100;
526
+ else if (/^carrier[-_]?/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 40;
527
+ else if (/carrier/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 20;
528
+ else if (/carrier/i.test(name)) rank += 5;
529
+ if (url === MCP_URL || url.includes("https://mcp.carrier.llc/mcp")) rank += 50;
530
+ else if (/mcp\.carrier\.llc/i.test(url)) rank += 25;
531
+ return rank;
532
+ }
533
+ function scanMcpServers(servers) {
534
+ if (!servers || typeof servers !== "object") return null;
535
+ let best = null;
536
+ for (const [name, entry] of Object.entries(servers)) {
537
+ if (!/carrier/i.test(name) && !(typeof entry?.url === "string" && /carrier\.llc/i.test(entry.url))) {
538
+ continue;
539
+ }
540
+ const url = typeof entry?.url === "string" ? entry.url : "";
541
+ const rank = rankHit(name, url);
542
+ if (rank < 20 && !url.includes("mcp.carrier.llc")) {
543
+ continue;
544
+ }
545
+ const urlMatches = url === MCP_URL || url.includes("https://mcp.carrier.llc/mcp");
546
+ const hit = {
547
+ registered: true,
548
+ urlMatches,
549
+ detail: url ? `${name}: ${url}` : `${name}: ${entry?.command ?? "configured"}`,
550
+ rank
551
+ };
552
+ if (!best || hit.rank > best.rank) best = hit;
553
+ }
554
+ return best;
555
+ }
556
+ async function probeMcpFromConfig() {
557
+ const candidates = [
558
+ join6(homedir2(), ".claude.json"),
559
+ join6(claudeHome(), "settings.json"),
560
+ join6(claudeHome(), ".mcp.json"),
561
+ join6(process.cwd(), ".mcp.json")
562
+ ];
563
+ let best = null;
564
+ let configSeen = false;
565
+ for (const file of candidates) {
566
+ if (!await exists(file)) continue;
567
+ try {
568
+ const json = JSON.parse(await readFile(file, "utf8"));
569
+ configSeen = true;
570
+ const direct = scanMcpServers(json.mcpServers);
571
+ if (direct && (!best || direct.rank > best.rank)) best = direct;
572
+ if (json.projects) {
573
+ for (const proj of Object.values(json.projects)) {
574
+ const hit = scanMcpServers(proj?.mcpServers);
575
+ if (hit && (!best || hit.rank > best.rank)) best = hit;
576
+ }
577
+ }
578
+ } catch {
579
+ }
580
+ }
581
+ if (!best) return { hit: null, configSeen };
582
+ return {
583
+ hit: { registered: best.registered, urlMatches: best.urlMatches, detail: best.detail },
584
+ configSeen
585
+ };
586
+ }
587
+ async function probeMcpRegistration() {
588
+ const { hit, configSeen } = await probeMcpFromConfig();
589
+ if (hit) return hit;
590
+ if (configSeen) {
591
+ return {
592
+ registered: false,
593
+ urlMatches: false,
594
+ detail: "carrier not registered (no production MCP URL in Claude config)"
595
+ };
596
+ }
597
+ const claudeFound = await which("claude");
598
+ if (!claudeFound) {
599
+ return {
600
+ registered: false,
601
+ urlMatches: false,
602
+ detail: "claude CLI not on PATH"
603
+ };
604
+ }
605
+ const r = await run("claude", ["mcp", "list"], { timeoutMs: 5e3 });
606
+ const out = `${r.stdout}
607
+ ${r.stderr}`;
608
+ if (!r.ok && !out.trim()) {
609
+ return { registered: false, urlMatches: false, detail: "claude mcp list failed or timed out" };
610
+ }
611
+ if (/timeout after/i.test(r.stderr) && !/carrier/i.test(out)) {
612
+ return {
613
+ registered: false,
614
+ urlMatches: false,
615
+ detail: "claude mcp list timed out \u2014 run `claude mcp list` manually"
616
+ };
617
+ }
618
+ const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
619
+ const carrierLine = lines.find((l) => new RegExp(`^${MCP_NAME}\\s*:`, "i").test(l)) ?? lines.find((l) => /carrier/i.test(l) && /mcp\.carrier\.llc/i.test(l));
620
+ if (!carrierLine) {
621
+ return { registered: false, urlMatches: false, detail: "carrier not in claude mcp list" };
622
+ }
623
+ const urlMatches = carrierLine.includes(MCP_URL) || /mcp\.carrier\.llc\/mcp/i.test(carrierLine);
624
+ return { registered: true, urlMatches, detail: carrierLine.slice(0, 200) };
625
+ }
626
+ function buildNextSteps(s) {
627
+ const steps = [];
628
+ if (!s.claudeFound) {
629
+ steps.push("Install Claude Code (https://claude.ai/code), then re-run: carrier plugin install");
630
+ } else if (!s.pluginInstalled) {
631
+ steps.push("Install the Carrier plugin: carrier plugin install");
632
+ } else if (!s.mcpRegistered) {
633
+ steps.push(`Register the zero-cred MCP URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
634
+ } else if (!s.mcpUrlMatches) {
635
+ steps.push(`Update MCP to production URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
636
+ }
637
+ if (s.mcpRegistered || s.pluginInstalled) {
638
+ steps.push(
639
+ "Talk to your fleet in Claude \u2014 first tool call opens Clerk OAuth (Google / GitHub / email). No API token needed."
640
+ );
641
+ } else {
642
+ steps.push("Create a free account (browser): carrier open signup");
643
+ steps.push("Or sign in: carrier open signin");
644
+ }
645
+ if (!s.tokenPresent) {
646
+ steps.push(
647
+ "Optional headless key: Console \u2192 Settings \u2192 API Keys (ak_\u2026) then export CARRIER_API_KEY=ak_\u2026 for `carrier ask`."
648
+ );
649
+ } else {
650
+ steps.push('Try a natural-language fleet query: carrier ask "show fleet health"');
651
+ }
652
+ steps.push("Scaffold a white-label storefront anytime: carrier site create");
653
+ return steps;
654
+ }
655
+ async function gatherStatus() {
656
+ const [plugin2, mcp, claudeFound] = await Promise.all([
657
+ pluginStatus(),
658
+ probeMcpRegistration(),
659
+ which("claude")
660
+ ]);
661
+ const token = detectEnvToken();
662
+ let authMode = "none";
663
+ if (token.kind === "api-key") authMode = "api-key";
664
+ else if (token.kind === "ocs-token") authMode = "ocs-token";
665
+ else if (mcp.registered || plugin2.installed) authMode = "oauth-first-use";
666
+ const base = {
667
+ pluginInstalled: plugin2.installed,
668
+ pluginVersion: plugin2.version,
669
+ pluginDir: plugin2.dir,
670
+ claudeFound,
671
+ mcpRegistered: mcp.registered,
672
+ mcpUrlMatches: mcp.urlMatches,
673
+ mcpDetail: mcp.detail,
674
+ tokenPresent: token.present,
675
+ tokenKind: token.kind,
676
+ authMode
677
+ };
678
+ return { ...base, nextSteps: buildNextSteps(base) };
679
+ }
680
+ function resolveCliToken() {
681
+ const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
682
+ if (api) return api;
683
+ const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim() || "";
684
+ return ocs || null;
685
+ }
686
+
687
+ // src/cli/lib/guidance.ts
688
+ import pc from "picocolors";
689
+ var FLEET_NL_EXAMPLES = [
690
+ { label: "Fleet health", prompt: "Show my fleet health \u2014 accounts, eSIMs, low-balance alerts" },
691
+ { label: "Subscriber lookup", prompt: "Look up subscriber ICCID 8944\u2026 and diagnose connectivity" },
692
+ { label: "Usage anomalies", prompt: "Detect usage anomalies and burn-rate risks this week" },
693
+ { label: "Issue from inventory", prompt: "Issue 3 free eSIMs from inventory on my account and assign the starter package" },
694
+ { label: "Billing check", prompt: "What's my platform credit balance and Stripe Connect payout status?" },
695
+ { label: "Wallet balance", prompt: "What's my managed prepaid wallet balance and auto-top-up status?" },
696
+ { label: "Churn risk", prompt: "List high churn-risk subscribers with retention ideas" }
697
+ ];
698
+ function oauthFirstUseNote() {
699
+ return [
700
+ "Entry A \u2014 zero credentials up front",
701
+ ` MCP URL: ${MCP_URL}`,
702
+ " Auth: OAuth on first use (Clerk \u2014 Google, GitHub, or email)",
703
+ " Token: none required in Claude / Cursor / Windsurf",
704
+ "",
705
+ "When you first talk to Carrier in your MCP client, the browser opens",
706
+ "sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
707
+ `(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
708
+ ].join("\n");
709
+ }
710
+ function formatStatusBlock(st) {
711
+ const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
712
+ const authLabel = st.authMode === "oauth-first-use" ? pc.cyan("OAuth on first use (no token needed)") : st.authMode === "api-key" ? pc.green("org API key in env (CARRIER_API_KEY)") : st.authMode === "ocs-token" ? pc.green("OCS token in env") : pc.yellow("not ready \u2014 install MCP or open sign-up");
713
+ return [
714
+ `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
715
+ `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
716
+ `MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
717
+ `MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
718
+ `Auth path: ${authLabel}`,
719
+ `Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
720
+ ].join("\n");
721
+ }
722
+ function formatNextSteps(st) {
723
+ return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
724
+ }
725
+ function formatNlExamples() {
726
+ return FLEET_NL_EXAMPLES.map((e) => ` \u2022 ${pc.bold(e.label)}: "${e.prompt}"`).join("\n");
727
+ }
728
+ function accountLinksNote() {
729
+ return [
730
+ `Sign up: ${SIGN_UP_URL}`,
731
+ `Sign in: ${SIGN_IN_URL}`,
732
+ `Console: ${CONSOLE_URL}`,
733
+ `Onboarding: ${ONBOARDING_URL}`,
734
+ `MCP home: https://mcp.carrier.llc`
735
+ ].join("\n");
736
+ }
737
+ function withNextStep(errorMsg, next) {
738
+ return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
739
+ }
740
+
741
+ // src/cli/lib/home.ts
742
+ import * as p from "@clack/prompts";
743
+ import pc2 from "picocolors";
744
+
745
+ // src/cli/lib/ask.ts
746
+ function parseRpcBody(raw) {
747
+ const trimmed = raw.trim();
748
+ if (!trimmed) return {};
749
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
750
+ return JSON.parse(trimmed);
751
+ }
752
+ const dataLine = trimmed.split("\n").map((l) => l.trim()).find((l) => l.startsWith("data:"));
753
+ if (!dataLine) return {};
754
+ return JSON.parse(dataLine.slice(5).trim());
755
+ }
756
+ async function carrierAsk(intent) {
757
+ const token = resolveCliToken();
758
+ if (!token) {
759
+ return {
760
+ ok: false,
761
+ text: withNextStep("No headless token in the environment.", [
762
+ "Interactive path: open Claude and say your intent \u2014 OAuth runs on first use.",
763
+ "Or export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys.",
764
+ 'Then retry: carrier ask "show fleet health"'
765
+ ])
766
+ };
767
+ }
768
+ const headers = {
769
+ Authorization: `Bearer ${token}`,
770
+ "Content-Type": "application/json",
771
+ Accept: "application/json, text/event-stream"
772
+ };
773
+ try {
774
+ const initRes = await fetch(MCP_URL, {
775
+ method: "POST",
776
+ headers,
777
+ body: JSON.stringify({
778
+ jsonrpc: "2.0",
779
+ id: 1,
780
+ method: "initialize",
781
+ params: {
782
+ protocolVersion: "2024-11-05",
783
+ capabilities: {},
784
+ clientInfo: { name: "carrier-cli", version: "0.2.19" }
785
+ }
786
+ })
787
+ });
788
+ if (initRes.status === 401 || initRes.status === 403) {
789
+ return {
790
+ ok: false,
791
+ text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
792
+ "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
793
+ "Or complete OCS onboarding: https://app.carrier.llc/onboarding",
794
+ "Interactive: use Claude + OAuth instead of a headless key"
795
+ ])
796
+ };
797
+ }
798
+ const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
799
+ await initRes.arrayBuffer().catch(() => void 0);
800
+ if (!sessionId) {
801
+ return {
802
+ ok: false,
803
+ text: withNextStep("MCP initialize did not return a session id.", [
804
+ "Check network access to https://mcp.carrier.llc/mcp",
805
+ "Retry later, or use Claude with the registered MCP (OAuth path)"
806
+ ])
807
+ };
808
+ }
809
+ const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
810
+ await fetch(MCP_URL, {
811
+ method: "POST",
812
+ headers: sessionHeaders,
813
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
814
+ }).catch(() => void 0);
815
+ const res = await fetch(MCP_URL, {
816
+ method: "POST",
817
+ headers: sessionHeaders,
818
+ body: JSON.stringify({
819
+ jsonrpc: "2.0",
820
+ id: 2,
821
+ method: "tools/call",
822
+ params: { name: "carrier_ask", arguments: { intent } }
823
+ })
824
+ });
825
+ if (!res.ok) {
826
+ const snippet = (await res.text().catch(() => "")).slice(0, 240);
827
+ return {
828
+ ok: false,
829
+ text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
830
+ "Check network access to https://mcp.carrier.llc/mcp",
831
+ "Retry later, or use Claude with the registered MCP (OAuth path)"
832
+ ])
833
+ };
834
+ }
835
+ const json = parseRpcBody(await res.text());
836
+ if (json.error?.message) {
837
+ return {
838
+ 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
+ ])
843
+ };
844
+ }
845
+ 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);
847
+ return { ok: !json.result?.isError, text: text3 };
848
+ } catch (e) {
849
+ const msg = e instanceof Error ? e.message : String(e);
850
+ return {
851
+ ok: false,
852
+ text: withNextStep(`Could not reach MCP: ${msg}`, [
853
+ "Verify outbound HTTPS to mcp.carrier.llc",
854
+ "Use the interactive Claude path while offline from this machine"
855
+ ])
856
+ };
857
+ }
858
+ }
859
+
860
+ // src/cli/lib/home.ts
861
+ function mark(ok2) {
862
+ return ok2 ? pc2.green("\u25CF") : pc2.yellow("\u25CB");
863
+ }
864
+ function printStatus(st) {
865
+ p.note(formatStatusBlock(st), "Status");
866
+ p.note(formatNextSteps(st), "Next steps");
867
+ }
868
+ async function showHomeStatus() {
869
+ const s = p.spinner();
870
+ s.start("Checking Claude plugin + MCP registration");
871
+ const st = await gatherStatus();
872
+ s.stop("Status ready");
873
+ printStatus(st);
874
+ return st;
875
+ }
876
+ async function openAndReport(url, label) {
877
+ const r = await openUrl(url);
878
+ if (r.ok) p.log.success(r.hint);
879
+ else {
880
+ p.log.warn(r.hint);
881
+ p.note(url, label);
882
+ }
883
+ }
884
+ async function runHome(opts) {
885
+ let st = await showHomeStatus();
886
+ p.note(oauthFirstUseNote(), "How auth works");
887
+ for (; ; ) {
888
+ const hasToken = !!resolveCliToken();
889
+ const choice = await p.select({
890
+ message: "What do you want to do?",
891
+ options: [
892
+ {
893
+ value: "refresh",
894
+ label: `${mark(st.mcpRegistered && st.pluginInstalled)} Refresh status`,
895
+ hint: "Re-check plugin + MCP"
896
+ },
897
+ {
898
+ value: "install",
899
+ label: `${mark(st.pluginInstalled && st.mcpRegistered)} Install / re-register plugin + MCP`,
900
+ hint: "Zero-cred URL \xB7 OAuth on first use"
901
+ },
902
+ {
903
+ value: "signup",
904
+ label: "Open sign-up",
905
+ hint: "Create a Carrier account (Google / GitHub / email)"
906
+ },
907
+ {
908
+ value: "signin",
909
+ label: "Open sign-in",
910
+ hint: "Existing account"
911
+ },
912
+ {
913
+ value: "console",
914
+ label: "Open console",
915
+ hint: CONSOLE_URL
916
+ },
917
+ {
918
+ value: "onboarding",
919
+ label: "Open onboarding (link OCS / managed setup)",
920
+ hint: ONBOARDING_URL
921
+ },
922
+ {
923
+ value: "ask",
924
+ label: hasToken ? "Ask the fleet (carrier ask)" : "Ask the fleet (needs CARRIER_API_KEY)",
925
+ hint: hasToken ? "Uses env token" : "Optional headless path"
926
+ },
927
+ {
928
+ value: "site",
929
+ label: "Scaffold white-label storefront",
930
+ hint: "carrier site create"
931
+ },
932
+ { value: "exit", label: "Exit" }
933
+ ]
934
+ });
935
+ if (p.isCancel(choice) || choice === "exit") {
936
+ p.outro(pc2.dim("Run `carrier` anytime for this menu. Talk to your fleet in Claude."));
937
+ return;
938
+ }
939
+ switch (choice) {
940
+ case "refresh":
941
+ st = await showHomeStatus();
942
+ break;
943
+ case "install":
944
+ await opts.onInstall();
945
+ p.note(oauthFirstUseNote(), "OAuth on first use");
946
+ st = await showHomeStatus();
947
+ break;
948
+ case "signup":
949
+ await openAndReport(SIGN_UP_URL, "Sign-up URL");
950
+ p.log.info("After sign-up, return here or open Claude and talk to Carrier \u2014 OAuth may already be done.");
951
+ break;
952
+ case "signin":
953
+ await openAndReport(SIGN_IN_URL, "Sign-in URL");
954
+ break;
955
+ case "console":
956
+ await openAndReport(CONSOLE_URL, "Console");
957
+ break;
958
+ case "onboarding":
959
+ await openAndReport(ONBOARDING_URL, "Onboarding");
960
+ p.log.info("Link OCS credentials (BYO) or finish managed setup, then use Claude MCP.");
961
+ break;
962
+ case "ask": {
963
+ if (!resolveCliToken()) {
964
+ p.log.warn("No CARRIER_API_KEY / OCS token in env.");
965
+ p.note(
966
+ [
967
+ "Interactive (recommended): install MCP, open Claude, ask in plain English.",
968
+ "Headless: Console \u2192 Settings \u2192 API Keys \u2192 export CARRIER_API_KEY=ak_\u2026",
969
+ "",
970
+ "Example prompts:",
971
+ ...FLEET_NL_EXAMPLES.slice(0, 3).map((e) => ` carrier ask "${e.prompt}"`)
972
+ ].join("\n"),
973
+ "How to ask"
974
+ );
975
+ break;
976
+ }
977
+ const intent = await p.text({
978
+ message: "What should Carrier do?",
979
+ placeholder: "show fleet health"
980
+ });
981
+ if (p.isCancel(intent) || !String(intent).trim()) break;
982
+ const spin = p.spinner();
983
+ spin.start("Asking Carrier MCP\u2026");
984
+ const result = await carrierAsk(String(intent).trim());
985
+ spin.stop(result.ok ? "Answer" : "Could not complete ask");
986
+ if (result.ok) p.note(result.text.slice(0, 4e3), "carrier ask");
987
+ else p.log.error(result.text);
988
+ break;
989
+ }
990
+ case "site":
991
+ await opts.onSiteCreate();
992
+ return;
993
+ default:
994
+ break;
995
+ }
996
+ }
997
+ }
998
+
443
999
  // src/cli/index.ts
444
- var VERSION = "0.2.17";
1000
+ var VERSION = "0.2.19";
445
1001
  function header() {
446
- p.intro(`${pc.bold(pc.yellow("\u25C6 carrier"))} ${pc.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
1002
+ p2.intro(`${pc3.bold(pc3.yellow("\u25C6 carrier"))} ${pc3.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
447
1003
  }
448
1004
  function ok(msg) {
449
- p.log.success(pc.green(msg));
1005
+ p2.log.success(pc3.green(msg));
450
1006
  }
451
1007
  function info(msg) {
452
- p.log.info(msg);
1008
+ p2.log.info(msg);
1009
+ }
1010
+ function fail(msg, next) {
1011
+ p2.log.error(msg);
1012
+ p2.note(next.map((s) => `\u2192 ${s}`).join("\n"), "What to do next");
453
1013
  }
454
1014
  async function promptBrand(seed) {
455
- const name = await p.text({
1015
+ const name = await p2.text({
456
1016
  message: "Brand name",
457
1017
  placeholder: seed.name,
458
1018
  defaultValue: seed.name
459
1019
  });
460
- if (p.isCancel(name)) process.exit(0);
461
- const domain = await p.text({
1020
+ if (p2.isCancel(name)) process.exit(0);
1021
+ const domain = await p2.text({
462
1022
  message: "Domain",
463
1023
  placeholder: seed.domain,
464
1024
  defaultValue: seed.domain
465
1025
  });
466
- if (p.isCancel(domain)) process.exit(0);
467
- const accent = await p.text({
1026
+ if (p2.isCancel(domain)) process.exit(0);
1027
+ const accent = await p2.text({
468
1028
  message: "Accent color (hex)",
469
1029
  placeholder: seed.colors.accent,
470
1030
  defaultValue: seed.colors.accent
471
1031
  });
472
- if (p.isCancel(accent)) process.exit(0);
473
- const supportEmail = await p.text({
1032
+ if (p2.isCancel(accent)) process.exit(0);
1033
+ const supportEmail = await p2.text({
474
1034
  message: "Support email",
475
1035
  placeholder: `support@${domain}`,
476
1036
  defaultValue: `support@${domain}`
477
1037
  });
478
- if (p.isCancel(supportEmail)) process.exit(0);
1038
+ if (p2.isCancel(supportEmail)) process.exit(0);
479
1039
  const accentDark = deriveAccentDark(accent, seed);
480
1040
  const isCarrier = name === CARRIER_BRAND.name && domain === CARRIER_BRAND.domain;
481
1041
  return {
@@ -492,69 +1052,183 @@ async function promptBrand(seed) {
492
1052
  };
493
1053
  }
494
1054
  async function doPluginInstall() {
495
- const s = p.spinner();
496
- s.start("Installing Carrier Claude Code plugin");
497
- const r = await installPlugin();
1055
+ const s = p2.spinner();
1056
+ s.start("Installing Carrier Claude Code plugin + zero-cred MCP");
1057
+ let r;
1058
+ try {
1059
+ r = await installPlugin();
1060
+ } catch (e) {
1061
+ s.stop("Install failed");
1062
+ fail(String(e instanceof Error ? e.message : e), [
1063
+ "Reinstall the package: npm i -g @carrierllc/mcp (or npx @carrierllc/mcp)",
1064
+ "Or finish manually with the commands under `carrier plugin install` help",
1065
+ `Sign up anytime: ${SIGN_UP_URL}`
1066
+ ]);
1067
+ return;
1068
+ }
498
1069
  s.stop("Plugin staged");
499
1070
  ok(`Plugin \u2192 ${r.copiedTo}`);
500
1071
  info(
501
- `MCP: ${r.mcpAdded ? pc.green("registered") : pc.yellow("manual")} \xB7 marketplace: ${r.marketplaceAdded ? pc.green("added") : pc.yellow("manual")} \xB7 install: ${r.pluginInstalled ? pc.green("done") : pc.yellow("manual")}`
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")}`
502
1073
  );
1074
+ p2.note(installAuthGuidance().join("\n"), "OAuth on first use (Entry A)");
503
1075
  if (!r.claudeFound || r.notes.length) {
504
- p.note(manualCommands().join("\n"), "Finish wiring (run in your terminal)");
505
- if (r.notes.length) info(pc.dim(r.notes.join("\n")));
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(
1079
+ [
1080
+ "You can still create an account now:",
1081
+ ` carrier open signup`,
1082
+ "Then open Claude and talk to your fleet \u2014 browser OAuth completes auth."
1083
+ ].join("\n"),
1084
+ "Next"
1085
+ );
1086
+ } else {
1087
+ p2.note(
1088
+ [
1089
+ "Open Claude Code and say something like:",
1090
+ ' "Show my fleet health"',
1091
+ "First call opens the browser for sign-in / sign-up. No token paste.",
1092
+ "",
1093
+ "More prompts: carrier examples"
1094
+ ].join("\n"),
1095
+ "Talk to your fleet"
1096
+ );
506
1097
  }
507
1098
  }
508
1099
  async function doSiteCreate(target, brand) {
509
- const s = p.spinner();
1100
+ const s = p2.spinner();
510
1101
  s.start(`Scaffolding ${brand.name} storefront \u2192 ${target}`);
511
- await scaffoldStorefront(target, brand);
1102
+ try {
1103
+ await scaffoldStorefront(target, brand);
1104
+ } catch (e) {
1105
+ s.stop("Scaffold failed");
1106
+ fail(String(e instanceof Error ? e.message : e), [
1107
+ "Pick a free directory: carrier site create ./my-storefront",
1108
+ "Ensure the package templates shipped with @carrierllc/mcp",
1109
+ "Still stuck? carrier open console"
1110
+ ]);
1111
+ throw e;
1112
+ }
512
1113
  s.stop("Storefront scaffolded");
513
1114
  ok(`Created ${target} (white-labeled: ${brand.name}, accent ${brand.colors.accent})`);
514
1115
  }
515
1116
  async function maybeBuildDeploy(target, brand, opts) {
516
1117
  if (opts.install) {
517
- const s = p.spinner();
1118
+ const s = p2.spinner();
518
1119
  s.start("Installing storefront dependencies");
519
1120
  const oki = await installDeps(target);
520
1121
  s.stop(oki ? "Dependencies installed" : "Dependency install reported errors");
521
- if (!oki) return;
1122
+ if (!oki) {
1123
+ p2.note(
1124
+ [
1125
+ `cd ${target} && pnpm install`,
1126
+ "Fix any Node/pnpm version issues, then retry build"
1127
+ ].join("\n"),
1128
+ "Next"
1129
+ );
1130
+ return;
1131
+ }
522
1132
  }
523
1133
  if (opts.build) {
524
- const s = p.spinner();
1134
+ const s = p2.spinner();
525
1135
  s.start("Building storefront (next build)");
526
1136
  const okb = await buildSite(target);
527
1137
  s.stop(okb ? "Build succeeded" : "Build failed \u2014 see output above");
528
- if (!okb) return;
1138
+ if (!okb) {
1139
+ p2.note(
1140
+ [`cd ${target}`, "pnpm build", "Check env keys in .env.local if the build mentions Clerk"].join("\n"),
1141
+ "Next"
1142
+ );
1143
+ return;
1144
+ }
529
1145
  }
530
1146
  if (opts.deploy) {
531
- const s = p.spinner();
1147
+ const s = p2.spinner();
532
1148
  s.start("Deploying to Cloudflare Workers");
533
1149
  const r = await deploySite(target, brand);
534
1150
  s.stop(r.ok ? `Deployed: ${r.projectName}` : "Deploy skipped");
535
- if (!r.ok && r.reason) info(pc.yellow(r.reason));
1151
+ if (!r.ok && r.reason) {
1152
+ info(pc3.yellow(r.reason));
1153
+ p2.note(
1154
+ [
1155
+ "Install wrangler and login: npx wrangler login",
1156
+ `Then: carrier site deploy ${target}`
1157
+ ].join("\n"),
1158
+ "Next"
1159
+ );
1160
+ }
536
1161
  }
537
1162
  }
538
- var program = new Command();
539
- program.name("carrier").description("Carrier CLI \u2014 install the Claude Code plugin and roll out a white-labeled eSIM storefront.").version(VERSION);
540
- program.command("init").description("Interactive: install the plugin AND scaffold/deploy a storefront.").option("--dir <path>", "Storefront output directory", "./storefront").option("--yes", "Use Carrier defaults, no prompts").action(async (o) => {
541
- header();
542
- await doPluginInstall();
543
- const brand = o.yes ? CARRIER_BRAND : await promptBrand(CARRIER_BRAND);
544
- const target = resolve2(o.dir);
1163
+ async function interactiveSiteCreate() {
1164
+ const brand = await promptBrand(CARRIER_BRAND);
1165
+ const dir = await p2.text({
1166
+ message: "Output directory",
1167
+ placeholder: "./storefront",
1168
+ defaultValue: "./storefront"
1169
+ });
1170
+ if (p2.isCancel(dir)) return;
1171
+ const target = resolve2(dir);
545
1172
  if (await exists(target)) {
546
- const go = await p.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
547
- if (p.isCancel(go) || !go) {
548
- p.outro("Stopped. Re-run with --dir <new path>.");
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.");
549
1176
  return;
550
1177
  }
551
1178
  }
552
1179
  await doSiteCreate(target, brand);
553
- let install = !!o.yes;
554
- let build = !!o.yes;
555
- let deploy = false;
556
- if (!o.yes) {
557
- const next = await p.select({
1180
+ p2.outro(pc3.green(`Scaffolded. cd ${dir} && pnpm install && pnpm dev`));
1181
+ }
1182
+ var program = new Command();
1183
+ program.name("carrier").description(
1184
+ "Carrier CLI \u2014 clear TUI for non-developers: status, OAuth-ready MCP install, storefront scaffold, and fleet NL helpers."
1185
+ ).version(VERSION).action(async () => {
1186
+ header();
1187
+ await runHome({
1188
+ onInstall: doPluginInstall,
1189
+ onSiteCreate: interactiveSiteCreate
1190
+ });
1191
+ });
1192
+ program.command("init").description("Interactive home: status, install plugin+MCP (OAuth on first use), optional storefront.").option("--dir <path>", "Storefront output directory", "./storefront").option("--yes", "Use Carrier defaults, no prompts (plugin + scaffold + build)").option("--full", "After install, continue into storefront scaffold (default interactive path offers both)").action(async (o) => {
1193
+ header();
1194
+ if (o.yes) {
1195
+ await doPluginInstall();
1196
+ const brand = CARRIER_BRAND;
1197
+ const target = resolve2(o.dir);
1198
+ if (await exists(target)) {
1199
+ info(pc3.yellow(`${target} exists \u2014 writing into it (--yes).`));
1200
+ }
1201
+ await doSiteCreate(target, brand);
1202
+ await maybeBuildDeploy(target, brand, { install: true, build: true, deploy: false });
1203
+ p2.note(oauthFirstUseNote(), "Auth");
1204
+ p2.note(
1205
+ [
1206
+ `cd ${o.dir}`,
1207
+ `Edit src/brand.config.ts to re-brand anytime`,
1208
+ `pnpm dev # local preview`,
1209
+ `MCP endpoint: ${MCP_URL}`,
1210
+ 'In Claude: "Show my fleet health" (browser OAuth on first use)'
1211
+ ].join("\n"),
1212
+ "Next"
1213
+ );
1214
+ p2.outro(pc3.green("Done. Your connectivity business is wired."));
1215
+ return;
1216
+ }
1217
+ if (o.full) {
1218
+ const st = await gatherStatus();
1219
+ printStatus(st);
1220
+ await doPluginInstall();
1221
+ const brand = await promptBrand(CARRIER_BRAND);
1222
+ const target = resolve2(o.dir);
1223
+ 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>.");
1227
+ return;
1228
+ }
1229
+ }
1230
+ await doSiteCreate(target, brand);
1231
+ const next = await p2.select({
558
1232
  message: "Roll it out now?",
559
1233
  options: [
560
1234
  { value: "build", label: "Install deps + build" },
@@ -563,34 +1237,147 @@ program.command("init").description("Interactive: install the plugin AND scaffol
563
1237
  ],
564
1238
  initialValue: "build"
565
1239
  });
566
- if (p.isCancel(next)) process.exit(0);
567
- install = next !== "none";
568
- build = next !== "none";
569
- deploy = next === "deploy";
1240
+ if (p2.isCancel(next)) process.exit(0);
1241
+ await maybeBuildDeploy(target, brand, {
1242
+ install: next !== "none",
1243
+ build: next !== "none",
1244
+ deploy: next === "deploy"
1245
+ });
1246
+ p2.note(oauthFirstUseNote(), "Auth");
1247
+ p2.note(
1248
+ [
1249
+ `cd ${o.dir}`,
1250
+ `Edit src/brand.config.ts to re-brand anytime`,
1251
+ `pnpm dev # local preview`,
1252
+ `MCP endpoint: ${MCP_URL}`
1253
+ ].join("\n"),
1254
+ "Next"
1255
+ );
1256
+ p2.outro(pc3.green("Done. Your connectivity business is wired."));
1257
+ return;
570
1258
  }
571
- await maybeBuildDeploy(target, brand, { install, build, deploy });
572
- p.note(
573
- [
574
- `cd ${o.dir}`,
575
- `Edit src/brand.config.ts to re-brand anytime`,
576
- `pnpm dev # local preview`,
577
- `MCP endpoint: ${MCP_URL}`
578
- ].join("\n"),
579
- "Next"
580
- );
581
- p.outro(pc.green("Done. Your connectivity business is wired."));
1259
+ await runHome({
1260
+ onInstall: doPluginInstall,
1261
+ onSiteCreate: async () => {
1262
+ const brand = await promptBrand(CARRIER_BRAND);
1263
+ const target = resolve2(o.dir);
1264
+ 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.");
1268
+ return;
1269
+ }
1270
+ }
1271
+ await doSiteCreate(target, brand);
1272
+ const next = await p2.select({
1273
+ message: "Roll it out now?",
1274
+ options: [
1275
+ { value: "build", label: "Install deps + build" },
1276
+ { value: "deploy", label: "Install + build + deploy to Cloudflare Workers" },
1277
+ { value: "none", label: "Just scaffold \u2014 I'll build later" }
1278
+ ],
1279
+ initialValue: "build"
1280
+ });
1281
+ if (p2.isCancel(next)) return;
1282
+ await maybeBuildDeploy(target, brand, {
1283
+ install: next !== "none",
1284
+ build: next !== "none",
1285
+ deploy: next === "deploy"
1286
+ });
1287
+ p2.note(
1288
+ [
1289
+ `cd ${o.dir}`,
1290
+ `pnpm dev`,
1291
+ `MCP: ${MCP_URL} \xB7 OAuth on first use in Claude`
1292
+ ].join("\n"),
1293
+ "Next"
1294
+ );
1295
+ }
1296
+ });
582
1297
  });
583
1298
  var plugin = program.command("plugin").description("Manage the Carrier Claude Code plugin.");
584
- plugin.command("install").description("Install/register the Carrier plugin + MCP into Claude Code.").action(async () => {
1299
+ plugin.command("install").description("Install/register the Carrier plugin + zero-cred MCP (OAuth on first use).").action(async () => {
585
1300
  header();
586
1301
  await doPluginInstall();
587
- p.outro(pc.green("Plugin ready. Restart Claude Code to load it."));
1302
+ p2.outro(pc3.green("Plugin ready. Restart Claude Code, then talk to your fleet."));
588
1303
  });
589
- plugin.command("status").description("Show plugin install status.").action(async () => {
590
- const st = await pluginStatus();
1304
+ plugin.command("status").description("Show plugin + MCP registration + auth next steps.").action(async () => {
591
1305
  header();
592
- info(st.installed ? pc.green(`Installed v${st.version ?? "?"} \u2192 ${st.dir}`) : pc.yellow(`Not installed (${st.dir})`));
593
- p.outro("");
1306
+ const st = await gatherStatus();
1307
+ printStatus(st);
1308
+ p2.note(oauthFirstUseNote(), "Auth");
1309
+ p2.outro("");
1310
+ });
1311
+ program.command("status").description("Show MCP / plugin / auth status and next steps.").action(async () => {
1312
+ header();
1313
+ const s = p2.spinner();
1314
+ s.start("Checking status");
1315
+ const st = await gatherStatus();
1316
+ s.stop("Done");
1317
+ p2.note(formatStatusBlock(st), "Status");
1318
+ p2.note(formatNextSteps(st), "Next steps");
1319
+ p2.note(oauthFirstUseNote(), "Auth");
1320
+ p2.outro("");
1321
+ });
1322
+ var openCmd = program.command("open").description("Open Carrier account / product URLs in your browser.");
1323
+ for (const [name, url, desc] of [
1324
+ ["signup", SIGN_UP_URL, "Create a Carrier account (Clerk sign-up)"],
1325
+ ["signin", SIGN_IN_URL, "Sign in to Carrier"],
1326
+ ["console", CONSOLE_URL, "Open the Carrier console"],
1327
+ ["onboarding", ONBOARDING_URL, "Open console onboarding (link OCS / managed)"],
1328
+ ["mcp", MCP_HOME, "Open mcp.carrier.llc product page"]
1329
+ ]) {
1330
+ openCmd.command(name).description(desc).action(async () => {
1331
+ header();
1332
+ const r = await openUrl(url);
1333
+ if (r.ok) ok(r.hint);
1334
+ else {
1335
+ p2.log.warn(r.hint);
1336
+ p2.note(url, "Open this URL");
1337
+ }
1338
+ p2.note(accountLinksNote(), "Account links");
1339
+ p2.outro("");
1340
+ });
1341
+ }
1342
+ program.command("examples").description("Print natural-language fleet prompts for Claude / MCP.").action(async () => {
1343
+ header();
1344
+ p2.note(formatNlExamples(), "Talk to fleet \u2014 paste into Claude");
1345
+ p2.note(
1346
+ [
1347
+ "After `carrier plugin install` (or this menu \u2192 Install):",
1348
+ " 1. Open Claude Code",
1349
+ ' 2. Say: "Show my fleet health"',
1350
+ " 3. Browser opens for sign-in/sign-up on first use",
1351
+ "",
1352
+ "Slash commands: /carrier:fleet /carrier:status /carrier:wallet /carrier:onboard"
1353
+ ].join("\n"),
1354
+ "How"
1355
+ );
1356
+ p2.outro("");
1357
+ });
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) => {
1359
+ header();
1360
+ const intent = parts.join(" ").trim();
1361
+ if (!intent) {
1362
+ fail("Missing intent.", [
1363
+ 'carrier ask "show fleet health"',
1364
+ "Or use Claude interactively (no token): carrier plugin install"
1365
+ ]);
1366
+ process.exitCode = 1;
1367
+ return;
1368
+ }
1369
+ const s = p2.spinner();
1370
+ s.start("Asking Carrier MCP\u2026");
1371
+ const result = await carrierAsk(intent);
1372
+ s.stop(result.ok ? "Done" : "Failed");
1373
+ if (result.ok) {
1374
+ p2.note(result.text.slice(0, 6e3), "Answer");
1375
+ p2.outro("");
1376
+ } else {
1377
+ p2.log.error(result.text);
1378
+ p2.outro(pc3.yellow("See next steps above."));
1379
+ process.exitCode = 1;
1380
+ }
594
1381
  });
595
1382
  var site = program.command("site").description("Scaffold and deploy a white-labeled storefront.");
596
1383
  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) => {
@@ -621,24 +1408,50 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
621
1408
  }
622
1409
  const target = resolve2(dir ?? "./storefront");
623
1410
  if (await exists(target)) {
624
- const go = await p.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
625
- if (p.isCancel(go) || !go) {
626
- p.outro("Stopped. Re-run with a different directory.");
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.");
627
1414
  return;
628
1415
  }
629
1416
  }
630
- await doSiteCreate(target, brand);
631
- p.outro(pc.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
1417
+ try {
1418
+ await doSiteCreate(target, brand);
1419
+ } catch {
1420
+ process.exitCode = 1;
1421
+ return;
1422
+ }
1423
+ p2.outro(pc3.green(`Scaffolded. cd ${dir ?? "storefront"} && pnpm install && pnpm dev`));
632
1424
  });
633
1425
  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) => {
634
1426
  header();
635
1427
  const target = resolve2(dir ?? "./storefront");
636
- const brand = await loadStorefrontBrand(target, o.name ? { name: o.name } : void 0);
637
- await maybeBuildDeploy(target, brand, { install: true, build: true, deploy: true });
638
- p.outro("");
1428
+ try {
1429
+ const brand = await loadStorefrontBrand(target, o.name ? { name: o.name } : void 0);
1430
+ await maybeBuildDeploy(target, brand, { install: true, build: true, deploy: true });
1431
+ } catch (e) {
1432
+ fail(String(e instanceof Error ? e.message : e), [
1433
+ "Scaffold first: carrier site create",
1434
+ "Ensure wrangler is logged in for deploy"
1435
+ ]);
1436
+ process.exitCode = 1;
1437
+ return;
1438
+ }
1439
+ p2.outro("");
639
1440
  });
640
1441
  program.parseAsync(process.argv).catch((e) => {
641
- console.error(pc.red(String(e instanceof Error ? e.message : e)));
1442
+ console.error(pc3.red(String(e instanceof Error ? e.message : e)));
1443
+ console.error(
1444
+ pc3.dim(
1445
+ [
1446
+ "",
1447
+ "What to do next:",
1448
+ " \u2192 carrier status",
1449
+ " \u2192 carrier open signup",
1450
+ " \u2192 carrier plugin install",
1451
+ " \u2192 carrier --help"
1452
+ ].join("\n")
1453
+ )
1454
+ );
642
1455
  process.exit(1);
643
1456
  });
644
1457
  //# sourceMappingURL=cli.js.map