@carrierllc/mcp 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  verifyStorefront,
40
40
  walletScreen,
41
41
  which
42
- } from "./chunk-QZBALDKD.js";
42
+ } from "./chunk-CQ6EOLA7.js";
43
43
  import {
44
44
  copyTree,
45
45
  exists,
@@ -94,6 +94,7 @@ function installedPluginDir() {
94
94
  // src/cli/lib/urls.ts
95
95
  import { spawn } from "child_process";
96
96
  var MCP_URL = "https://mcp.carrier.llc/mcp";
97
+ var API_URL = "https://api.carrier.llc";
97
98
  var MCP_HOME = "https://mcp.carrier.llc";
98
99
  var SIGN_UP_URL = "https://accounts.carrier.llc/sign-up";
99
100
  var SIGN_IN_URL = "https://accounts.carrier.llc/sign-in";
@@ -683,6 +684,9 @@ function isExpired(creds, skewMs = 6e4) {
683
684
  if (!creds.expiresAt) return false;
684
685
  return Date.now() >= creds.expiresAt - skewMs;
685
686
  }
687
+ function redact(text3) {
688
+ return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
689
+ }
686
690
 
687
691
  // src/cli/lib/auth.ts
688
692
  var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
@@ -2276,251 +2280,1633 @@ var CLI_DOMAINS = [
2276
2280
  ]
2277
2281
  }
2278
2282
  ]
2279
- }
2280
- ];
2281
-
2282
- // src/cli/lib/capability.ts
2283
- import * as p2 from "@clack/prompts";
2284
- import pc3 from "picocolors";
2285
- var MAX_PRETTY_CHARS = 8e3;
2286
- function optionKey(flags) {
2287
- const long = flags.split(/[ ,|]+/).find((t) => t.startsWith("--"));
2288
- if (!long) throw new Error(`Option "${flags}" has no long flag`);
2289
- return long.replace(/^--/, "").split("-").map((part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
2290
- }
2291
- function setArg(target, path, value) {
2292
- const parts = path.split(".");
2293
- let node = target;
2294
- for (const part of parts.slice(0, -1)) {
2295
- const next = node[part];
2296
- if (typeof next !== "object" || next === null) node[part] = {};
2297
- node = node[part];
2298
- }
2299
- node[parts[parts.length - 1]] = value;
2300
- }
2301
- function coerce(opt, raw) {
2302
- if (opt.type === "boolean") {
2303
- if (raw === true) return { value: true };
2304
- const text3 = String(raw).toLowerCase();
2305
- if (text3 === "true") return { value: true };
2306
- if (text3 === "false") return { value: false };
2307
- return { error: `${opt.flags} expects true or false, got "${String(raw)}"` };
2308
- }
2309
- if (opt.type === "number") {
2310
- const n = Number(raw);
2311
- if (!Number.isFinite(n)) return { error: `${opt.flags} expects a number, got "${String(raw)}"` };
2312
- return { value: n };
2313
- }
2314
- return { value: String(raw) };
2315
- }
2316
- function buildToolArgs(cmd, opts) {
2317
- const args = {};
2318
- const errors = [];
2319
- const notes = [];
2320
- for (const opt of cmd.options) {
2321
- const raw = opts[optionKey(opt.flags)];
2322
- if (raw === void 0 || raw === false) {
2323
- if (opt.required) errors.push(`Missing required ${opt.flags}`);
2324
- continue;
2325
- }
2326
- const { value, error } = coerce(opt, raw);
2327
- if (error) errors.push(error);
2328
- else setArg(args, opt.arg, value);
2329
- }
2330
- if (cmd.requireOneOf && !cmd.requireOneOf.some((name) => args[name] !== void 0)) {
2331
- const flags = cmd.options.filter((opt) => cmd.requireOneOf.includes(opt.arg)).map((opt) => opt.flags);
2332
- errors.push(`Pass one of: ${flags.join(", ")}`);
2333
- }
2334
- if (cmd.usageWindow) {
2335
- const start = args.startDate;
2336
- const end = args.endDate;
2337
- if (!start && !end) {
2338
- const window = lastNDaysPeriod();
2339
- args.startDate = window.start;
2340
- args.endDate = window.end;
2341
- notes.push(`Window defaulted to ${window.start} \u2026 ${window.end} (last ${OCS_MAX_USAGE_WINDOW_DAYS} days).`);
2342
- } else if (!start || !end) {
2343
- errors.push("Pass both --start and --end, or neither for the last 7 days");
2344
- } else {
2345
- const clamped = clampUsagePeriod(start, end);
2346
- if (clamped.start !== start) {
2347
- notes.push(
2348
- `Start moved to ${clamped.start} \u2014 OCS rejects windows wider than ${OCS_MAX_USAGE_WINDOW_DAYS} days.`
2349
- );
2283
+ },
2284
+ {
2285
+ name: "api-keys",
2286
+ summary: "Mint Carrier API keys (ak_\u2026) for headless and CI use",
2287
+ commands: [
2288
+ {
2289
+ name: "create",
2290
+ transport: "rest",
2291
+ restMethod: "POST",
2292
+ restPath: "/v1/api-keys",
2293
+ summary: "Create an org API key. The secret is shown once and never stored by the CLI.",
2294
+ options: [
2295
+ {
2296
+ flags: "--name <name>",
2297
+ description: "Label for the key, max 64 characters",
2298
+ arg: "name",
2299
+ type: "string",
2300
+ required: true
2301
+ },
2302
+ {
2303
+ flags: "--description <text>",
2304
+ description: "Optional note, max 256 characters",
2305
+ arg: "description",
2306
+ type: "string"
2307
+ },
2308
+ {
2309
+ flags: "--seconds-until-expiration <n>",
2310
+ description: "Expiry in seconds, 60 to 31536000. Omit for a key that does not expire.",
2311
+ arg: "secondsUntilExpiration",
2312
+ type: "number"
2313
+ }
2314
+ ]
2350
2315
  }
2351
- args.startDate = clamped.start;
2352
- args.endDate = clamped.end;
2353
- }
2354
- }
2355
- if (cmd.write) args.dry_run = true;
2356
- return { args, errors, notes };
2357
- }
2358
- function formatJsonOutput(text3) {
2359
- try {
2360
- JSON.parse(text3);
2361
- return text3;
2362
- } catch {
2363
- return JSON.stringify(text3);
2364
- }
2365
- }
2366
- async function runCapability(domain, cmd, opts) {
2367
- const label = `${domain.name} ${cmd.name}`;
2368
- const quiet = opts.json === true;
2369
- const { args, errors, notes } = buildToolArgs(cmd, opts);
2370
- if (cmd.write && opts.commit === true) delete args.dry_run;
2371
- if (errors.length) {
2372
- const message = withNextStep(`Cannot run \`carrier ${label}\`.`, [
2373
- ...errors,
2374
- `See every flag: carrier ${label} --help`
2375
- ]);
2376
- if (quiet) console.error(message);
2377
- else p2.log.error(message);
2378
- process.exitCode = 1;
2379
- return;
2380
- }
2381
- const dryRun = cmd.write === true && args.dry_run === true;
2382
- const warnings = dryRun ? [...notes, "Dry run \u2014 nothing changes. Re-run with --commit to apply."] : notes;
2383
- for (const note4 of warnings) {
2384
- if (quiet) console.error(note4);
2385
- else p2.log.info(pc3.dim(note4));
2386
- }
2387
- const s = quiet ? null : p2.spinner();
2388
- s?.start(`Calling ${cmd.tool}`);
2389
- const result = await callMcpTool(cmd.tool, args, {
2390
- errorHints: [
2391
- `Check the arguments: carrier ${label} --help`,
2392
- 'Or describe the task in words: carrier ask "\u2026"'
2393
2316
  ]
2394
- });
2395
- s?.stop(result.ok ? "Done" : "Failed");
2396
- if (!result.ok) {
2397
- if (quiet) console.error(result.text);
2398
- else p2.log.error(result.text);
2399
- process.exitCode = 1;
2400
- return;
2401
- }
2402
- if (quiet) {
2403
- process.stdout.write(`${formatJsonOutput(result.text)}
2404
- `);
2405
- return;
2406
- }
2407
- const body = result.text.length > MAX_PRETTY_CHARS ? `${result.text.slice(0, MAX_PRETTY_CHARS)}
2408
- \u2026truncated. Re-run with --json for the whole response.` : result.text;
2409
- p2.note(body, cmd.tool);
2410
- }
2411
-
2412
- // src/cli/index.ts
2413
- var VERSION = CARRIER_VERSION;
2414
- function header() {
2415
- p3.intro(`${pc4.bold(pc4.yellow("\u25C6 carrier"))} ${pc4.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
2416
- }
2417
- function ok(msg) {
2418
- p3.log.success(pc4.green(msg));
2419
- }
2420
- function info(msg) {
2421
- p3.log.info(msg);
2422
- }
2423
- function fail(msg, next) {
2424
- p3.log.error(msg);
2425
- p3.note(next.map((s) => `\u2192 ${s}`).join("\n"), "What to do next");
2426
- }
2427
- async function promptBrand(seed) {
2428
- const name = await p3.text({
2429
- message: "Brand name",
2430
- placeholder: seed.name,
2431
- defaultValue: seed.name
2432
- });
2433
- if (p3.isCancel(name)) process.exit(0);
2434
- const domain = await p3.text({
2435
- message: "Domain",
2436
- placeholder: seed.domain,
2437
- defaultValue: seed.domain
2438
- });
2439
- if (p3.isCancel(domain)) process.exit(0);
2440
- const accent = await p3.text({
2441
- message: "Accent color (hex)",
2442
- placeholder: seed.colors.accent,
2443
- defaultValue: seed.colors.accent
2444
- });
2445
- if (p3.isCancel(accent)) process.exit(0);
2446
- const supportEmail = await p3.text({
2447
- message: "Support email",
2448
- placeholder: `support@${domain}`,
2449
- defaultValue: `support@${domain}`
2450
- });
2451
- if (p3.isCancel(supportEmail)) process.exit(0);
2452
- const accentDark = deriveAccentDark(accent, seed);
2453
- const isCarrier = name === CARRIER_BRAND.name && domain === CARRIER_BRAND.domain;
2454
- return {
2455
- ...seed,
2456
- name,
2457
- legalName: name,
2458
- tagline: isCarrier ? seed.tagline : `Mobile connectivity by ${name}.`,
2459
- domain,
2460
- supportEmail,
2461
- supportUrl: `https://${domain}/help`,
2462
- supportWhatsapp: isCarrier ? seed.supportWhatsapp : "",
2463
- social: isCarrier ? seed.social : {},
2464
- colors: { ...seed.colors, accent, accentDark }
2465
- };
2466
- }
2467
- async function doPluginInstall() {
2468
- const s = p3.spinner();
2469
- s.start("Installing Carrier Claude Code plugin + zero-cred MCP");
2470
- let r;
2471
- try {
2472
- r = await installPlugin();
2473
- } catch (e) {
2474
- s.stop("Install failed");
2475
- fail(String(e instanceof Error ? e.message : e), [
2476
- "Reinstall the package: npm i -g @carrierllc/mcp (or npx @carrierllc/mcp)",
2477
- "Or finish manually with the commands under `carrier plugin install` help",
2478
- `Sign up anytime: ${SIGN_UP_URL}`
2479
- ]);
2480
- return;
2481
- }
2482
- s.stop("Plugin staged");
2483
- ok(`Plugin \u2192 ${r.copiedTo}`);
2484
- info(
2485
- `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")}`
2486
- );
2487
- p3.note(installAuthGuidance().join("\n"), "OAuth on first use (Entry A)");
2488
- if (!r.claudeFound || r.notes.length) {
2489
- p3.note(manualCommands().join("\n"), "Finish wiring (run in your terminal)");
2490
- if (r.notes.length) info(pc4.dim(r.notes.join("\n")));
2491
- p3.note(
2492
- [
2493
- "You can still create an account now:",
2494
- ` carrier open signup`,
2495
- "Then open Claude and talk to your fleet \u2014 browser OAuth completes auth."
2496
- ].join("\n"),
2497
- "Next"
2498
- );
2499
- } else {
2500
- p3.note(
2501
- [
2502
- "Open Claude Code and say something like:",
2503
- ' "Show my fleet health"',
2504
- "First call opens the browser for sign-in / sign-up. No token paste.",
2505
- "",
2506
- "More prompts: carrier examples"
2507
- ].join("\n"),
2508
- "Talk to your fleet"
2509
- );
2510
- }
2511
- }
2512
- async function doSiteCreate(target, brand) {
2513
- const s = p3.spinner();
2514
- s.start(`Scaffolding ${brand.name} storefront \u2192 ${target}`);
2515
- try {
2516
- await scaffoldStorefront(target, brand);
2517
- } catch (e) {
2518
- s.stop("Scaffold failed");
2519
- fail(String(e instanceof Error ? e.message : e), [
2520
- "Pick a free directory: carrier site create ./my-storefront",
2521
- "Ensure the package templates shipped with @carrierllc/mcp",
2522
- "Still stuck? carrier open console"
2523
- ]);
2317
+ },
2318
+ // <generated:catalog-domains>
2319
+ // Generated from the MCP catalog by scripts/generate-domains.mjs. Do not edit by hand:
2320
+ // rebuild the server, re-run the generator, and review the diff.
2321
+ {
2322
+ name: "billing",
2323
+ summary: "Carrier plan credits, spend thresholds and rate-limit state",
2324
+ commands: [
2325
+ {
2326
+ name: "events",
2327
+ tool: "billing_events",
2328
+ summary: "View recent billing threshold events: notifications, invoice triggers, hard cap alerts, and overage pauses.",
2329
+ options: [
2330
+ {
2331
+ flags: "--limit <n>",
2332
+ description: "Maximum number of events to return (default: 20, max: 50).",
2333
+ arg: "limit",
2334
+ type: "number"
2335
+ }
2336
+ ]
2337
+ },
2338
+ {
2339
+ name: "configure",
2340
+ tool: "configure_billing",
2341
+ summary: "Configure your billing preferences: enable/disable overages, set billing thresholds (bill shock prevention), configure notification percentages, and set hard spending caps.",
2342
+ requireConfirm: true,
2343
+ options: [
2344
+ {
2345
+ flags: "--overages-enabled <bool>",
2346
+ description: "Enable or disable auto-billed overages when monthly credits are exhausted.",
2347
+ arg: "overages_enabled",
2348
+ type: "boolean"
2349
+ },
2350
+ {
2351
+ flags: "--billing-threshold-cents <n>",
2352
+ description: "Amount in cents at which an invoice is automatically generated (e.g., 10000 = $100).",
2353
+ arg: "billing_threshold_cents",
2354
+ type: "number"
2355
+ },
2356
+ {
2357
+ flags: "--hard-cap-cents <n>",
2358
+ description: "Maximum overage spend in cents before overages are paused (0 = no cap).",
2359
+ arg: "hard_cap_cents",
2360
+ type: "number"
2361
+ },
2362
+ {
2363
+ flags: "--auto-pause-on-cap <bool>",
2364
+ description: "Whether to automatically pause overages when hard cap is reached.",
2365
+ arg: "auto_pause_on_cap",
2366
+ type: "boolean"
2367
+ },
2368
+ {
2369
+ flags: "--notify-at-pct <list>",
2370
+ description: "Percentage thresholds at which to send notifications (e.g., [50, 80, 95, 100]). Comma-separated.",
2371
+ arg: "notify_at_pct",
2372
+ type: "number[]"
2373
+ },
2374
+ {
2375
+ flags: "--notification-email <email>",
2376
+ description: "Email address for billing threshold notifications.",
2377
+ arg: "notification_email",
2378
+ type: "string"
2379
+ },
2380
+ {
2381
+ flags: "--notification-phone <phone>",
2382
+ description: "E.164 phone number for SMS billing threshold notifications via Twilio (e.g. +14155552671).",
2383
+ arg: "notification_phone",
2384
+ type: "string"
2385
+ },
2386
+ {
2387
+ flags: "--webhook-url <url>",
2388
+ description: "Webhook URL for billing threshold events.",
2389
+ arg: "webhook_url",
2390
+ type: "string"
2391
+ }
2392
+ ]
2393
+ },
2394
+ {
2395
+ name: "credits",
2396
+ tool: "credit_balance",
2397
+ summary: "Read how much of this Carrier MCP plan is left: credits remaining, daily free credits, overage state, volume discount tier, and billing threshold state.",
2398
+ options: []
2399
+ },
2400
+ {
2401
+ name: "plans",
2402
+ tool: "pricing_plans",
2403
+ summary: "View all available Carrier MCP pricing plans with credit allotments, features, overage rates, and volume discount tiers.",
2404
+ options: []
2405
+ },
2406
+ {
2407
+ name: "projection",
2408
+ tool: "usage_projection",
2409
+ summary: "Project your credit usage and costs for the remainder of the billing period based on current consumption rate.",
2410
+ options: [
2411
+ {
2412
+ flags: "--days-to-project <n>",
2413
+ description: "Number of days to project forward (default: remaining days in month).",
2414
+ arg: "days_to_project",
2415
+ type: "number"
2416
+ }
2417
+ ]
2418
+ },
2419
+ {
2420
+ name: "rate-limit",
2421
+ tool: "rate_limit_status",
2422
+ summary: "Inspect Bridge4IP OCS rate-limit governor state \u2014 bucket fill levels, per-endpoint limits, and 80% alert thresholds.",
2423
+ options: [
2424
+ {
2425
+ flags: "--endpoint <endpoint>",
2426
+ description: "OCS method name in camelCase (e.g. 'subscriberUsageOverPeriod').",
2427
+ arg: "endpoint",
2428
+ type: "string"
2429
+ }
2430
+ ]
2431
+ }
2432
+ ]
2433
+ },
2434
+ {
2435
+ name: "wallet",
2436
+ summary: "The Carrier prepaid wallet: balance and top-ups",
2437
+ commands: [
2438
+ {
2439
+ name: "balance",
2440
+ tool: "wallet_balance",
2441
+ summary: "Read the caller organisation's Carrier prepaid wallet: current balance and auto-top-up settings.",
2442
+ options: []
2443
+ },
2444
+ {
2445
+ name: "topup",
2446
+ tool: "wallet_topup_checkout",
2447
+ summary: "Create a Stripe Checkout link so a human can top up the Carrier prepaid wallet by card.",
2448
+ options: [
2449
+ {
2450
+ flags: "--pack <pack>",
2451
+ description: "Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount.",
2452
+ arg: "pack",
2453
+ type: "string"
2454
+ }
2455
+ ]
2456
+ },
2457
+ {
2458
+ name: "auto-topup",
2459
+ tool: "wallet_auto_topup",
2460
+ summary: "Charge the organisation's saved card off-session, right now, and credit the Carrier prepaid wallet with the pack.",
2461
+ requireConfirm: true,
2462
+ options: [
2463
+ {
2464
+ flags: "--pack-cents <n>",
2465
+ description: "Override pack amount in EUR cents (must match a catalog pack).",
2466
+ arg: "pack_cents",
2467
+ type: "number"
2468
+ }
2469
+ ]
2470
+ }
2471
+ ]
2472
+ },
2473
+ {
2474
+ name: "stripe",
2475
+ summary: "Money in the operator's connected Stripe account",
2476
+ commands: [
2477
+ {
2478
+ name: "balance",
2479
+ tool: "stripe_connect_balance",
2480
+ summary: "Read the money sitting in the operator's connected Stripe account: available and pending, split per currency.",
2481
+ options: []
2482
+ },
2483
+ {
2484
+ name: "payouts",
2485
+ tool: "stripe_connect_payouts",
2486
+ summary: "List money Stripe has already sent from the operator's connected account to their bank, newest first.",
2487
+ options: [
2488
+ {
2489
+ flags: "--limit <n>",
2490
+ description: "Number of payouts to return. Default 10.",
2491
+ arg: "limit",
2492
+ type: "number"
2493
+ },
2494
+ {
2495
+ flags: "--status <status>",
2496
+ description: "Filter by payout status. One of: pending, paid, failed, canceled, in_transit.",
2497
+ arg: "status",
2498
+ type: "string"
2499
+ }
2500
+ ]
2501
+ },
2502
+ {
2503
+ name: "disputes",
2504
+ tool: "stripe_connect_dispute_list",
2505
+ summary: "List chargebacks customers have raised with their bank against the operator's connected Stripe account.",
2506
+ options: [
2507
+ {
2508
+ flags: "--limit <n>",
2509
+ description: "limit (number). Default 10.",
2510
+ arg: "limit",
2511
+ type: "number"
2512
+ },
2513
+ {
2514
+ flags: "--status <status>",
2515
+ description: "Filter by dispute status (e.g. needs_response, under_review).",
2516
+ arg: "status",
2517
+ type: "string"
2518
+ }
2519
+ ]
2520
+ },
2521
+ {
2522
+ name: "refund",
2523
+ tool: "stripe_connect_refund",
2524
+ summary: "Refund money to a customer on one charge in the operator's connected Stripe account.",
2525
+ options: [
2526
+ {
2527
+ flags: "--charge-id <id>",
2528
+ description: "Stripe charge ID (ch_...).",
2529
+ arg: "charge_id",
2530
+ type: "string",
2531
+ required: true
2532
+ },
2533
+ {
2534
+ flags: "--amount-cents <n>",
2535
+ description: "Partial refund amount in cents.",
2536
+ arg: "amount_cents",
2537
+ type: "number"
2538
+ },
2539
+ {
2540
+ flags: "--reason <reason>",
2541
+ description: "reason (string). One of: duplicate, fraudulent, requested_by_customer.",
2542
+ arg: "reason",
2543
+ type: "string"
2544
+ },
2545
+ {
2546
+ flags: "--confirm-token <token>",
2547
+ description: "Confirmation token from previous call.",
2548
+ arg: "confirm_token",
2549
+ type: "string"
2550
+ }
2551
+ ]
2552
+ }
2553
+ ]
2554
+ },
2555
+ {
2556
+ name: "radar",
2557
+ summary: "Stripe Radar fraud reviews and value lists",
2558
+ commands: [
2559
+ {
2560
+ name: "reviews",
2561
+ tool: "radar_review_list",
2562
+ summary: "List the Stripe Radar fraud reviews waiting on a manual approve/decline decision, newest first.",
2563
+ options: [
2564
+ {
2565
+ flags: "--open-only <bool>",
2566
+ description: "If true, only returns open (undecided) reviews. Default true.",
2567
+ arg: "open_only",
2568
+ type: "boolean"
2569
+ },
2570
+ {
2571
+ flags: "--limit <n>",
2572
+ description: "limit (number). Default 10.",
2573
+ arg: "limit",
2574
+ type: "number"
2575
+ }
2576
+ ]
2577
+ },
2578
+ {
2579
+ name: "approve",
2580
+ tool: "radar_review_approve",
2581
+ summary: "Clear a Stripe Radar fraud review so the held charge is captured and the money is taken.",
2582
+ options: [
2583
+ {
2584
+ flags: "--review-id <id>",
2585
+ description: "Stripe Radar review ID (prv_...).",
2586
+ arg: "review_id",
2587
+ type: "string",
2588
+ required: true
2589
+ },
2590
+ {
2591
+ flags: "--confirm-token <token>",
2592
+ description: "confirm token (string).",
2593
+ arg: "confirm_token",
2594
+ type: "string"
2595
+ }
2596
+ ]
2597
+ },
2598
+ {
2599
+ name: "decline",
2600
+ tool: "radar_review_decline",
2601
+ summary: "Close a Stripe Radar fraud review as fraudulent, so the held charge is blocked and the customer is never billed.",
2602
+ options: [
2603
+ {
2604
+ flags: "--review-id <id>",
2605
+ description: "review id (string).",
2606
+ arg: "review_id",
2607
+ type: "string",
2608
+ required: true
2609
+ },
2610
+ {
2611
+ flags: "--confirm-token <token>",
2612
+ description: "confirm token (string).",
2613
+ arg: "confirm_token",
2614
+ type: "string"
2615
+ }
2616
+ ]
2617
+ },
2618
+ {
2619
+ name: "rule-toggle",
2620
+ tool: "radar_rule_toggle",
2621
+ summary: "Report a Stripe Radar rule's state. Stripe exposes no API to change it, so this changes nothing.",
2622
+ options: [
2623
+ {
2624
+ flags: "--rule-id <id>",
2625
+ description: "Stripe Radar rule ID.",
2626
+ arg: "rule_id",
2627
+ type: "string",
2628
+ required: true
2629
+ },
2630
+ {
2631
+ flags: "--enabled <bool>",
2632
+ description: "true = enable, false = disable.",
2633
+ arg: "enabled",
2634
+ type: "boolean",
2635
+ required: true
2636
+ }
2637
+ ]
2638
+ },
2639
+ {
2640
+ name: "value-list add",
2641
+ tool: "radar_value_list_add",
2642
+ summary: "Add one value to an existing Stripe Radar value list, changing how every future charge is scored.",
2643
+ options: [
2644
+ {
2645
+ flags: "--value-list-id <id>",
2646
+ description: "Stripe Radar value list ID (rsl_...).",
2647
+ arg: "value_list_id",
2648
+ type: "string",
2649
+ required: true
2650
+ },
2651
+ {
2652
+ flags: "--value <value>",
2653
+ description: "The value to add (email, IP address, country code, etc.).",
2654
+ arg: "value",
2655
+ type: "string",
2656
+ required: true
2657
+ },
2658
+ {
2659
+ flags: "--confirm-token <token>",
2660
+ description: "confirm token (string).",
2661
+ arg: "confirm_token",
2662
+ type: "string"
2663
+ }
2664
+ ]
2665
+ }
2666
+ ]
2667
+ },
2668
+ {
2669
+ name: "greenzone",
2670
+ summary: "The Bridge4IP Greenzone whitelist",
2671
+ commands: [
2672
+ {
2673
+ name: "list",
2674
+ tool: "greenzone_whitelist_list",
2675
+ summary: "List current Greenzone whitelist entries from the KV cache.",
2676
+ options: [
2677
+ {
2678
+ flags: "--from-portal <bool>",
2679
+ description: "When true, return Kapture steps to read live portal state instead of KV cache.",
2680
+ arg: "from_portal",
2681
+ type: "boolean"
2682
+ }
2683
+ ]
2684
+ },
2685
+ {
2686
+ name: "add",
2687
+ tool: "greenzone_whitelist_add",
2688
+ summary: "Add a host or IP to the Greenzone whitelist. Needs Kapture running in the operator's own Chrome \u2014 without it the dry run still returns the portal steps to follow by hand.",
2689
+ write: true,
2690
+ options: [
2691
+ {
2692
+ flags: "--host <host>",
2693
+ description: "Hostname to whitelist (e.g. 'example.com').",
2694
+ arg: "host",
2695
+ type: "string"
2696
+ },
2697
+ {
2698
+ flags: "--ip <ip>",
2699
+ description: "IP address or CIDR to whitelist (e.g. '203.0.113.0/24').",
2700
+ arg: "ip",
2701
+ type: "string"
2702
+ }
2703
+ ]
2704
+ },
2705
+ {
2706
+ name: "remove",
2707
+ tool: "greenzone_whitelist_remove",
2708
+ summary: "Remove a host or IP from the Greenzone whitelist. Needs Kapture running in the operator's own Chrome \u2014 without it the dry run still returns the portal steps to follow by hand.",
2709
+ write: true,
2710
+ options: [
2711
+ {
2712
+ flags: "--host <host>",
2713
+ description: "Hostname to remove from whitelist.",
2714
+ arg: "host",
2715
+ type: "string"
2716
+ },
2717
+ {
2718
+ flags: "--ip <ip>",
2719
+ description: "IP address or CIDR to remove from whitelist.",
2720
+ arg: "ip",
2721
+ type: "string"
2722
+ }
2723
+ ]
2724
+ }
2725
+ ]
2726
+ },
2727
+ {
2728
+ name: "agent",
2729
+ summary: "Steel browsing agents: one-off tasks and cron schedules",
2730
+ commands: [
2731
+ {
2732
+ name: "ask",
2733
+ tool: "ui_agent_ask",
2734
+ summary: "Send a plain-language task to a Steel browsing agent, which drives a real browser on the open web \u2014 navigating pages, searching via Tavily, extracting content, filling forms outside the OCS portal.",
2735
+ write: true,
2736
+ options: [
2737
+ {
2738
+ flags: "--prompt <prompt>",
2739
+ description: "Natural-language task description for the Steel browsing agent.",
2740
+ arg: "prompt",
2741
+ type: "string",
2742
+ required: true
2743
+ },
2744
+ {
2745
+ flags: "--max-steps <n>",
2746
+ description: "Maximum agent steps (1\u201330, default 15).",
2747
+ arg: "max_steps",
2748
+ type: "number"
2749
+ }
2750
+ ]
2751
+ },
2752
+ {
2753
+ name: "status",
2754
+ tool: "ui_agent_status",
2755
+ summary: "Poll the status of a Steel browser agent task dispatched by ui_agent_ask or any ui_* tool.",
2756
+ options: [
2757
+ {
2758
+ flags: "--task-id <id>",
2759
+ description: "Steel task ID returned by ui_agent_ask or any ui_* tool dispatch.",
2760
+ arg: "task_id",
2761
+ type: "string",
2762
+ required: true
2763
+ }
2764
+ ]
2765
+ },
2766
+ {
2767
+ name: "pending",
2768
+ tool: "ui_agent_list_pending",
2769
+ summary: "Returns all Steel browser automation tasks that are currently paused waiting for human input (stop_reason 'ask').",
2770
+ options: []
2771
+ },
2772
+ {
2773
+ name: "reply",
2774
+ tool: "ui_agent_reply",
2775
+ summary: "Resumes a Steel browser automation task that paused with stop_reason 'ask'.",
2776
+ options: [
2777
+ {
2778
+ flags: "--task-id <id>",
2779
+ description: "Steel task ID to resume (from ui_agent_list_pending).",
2780
+ arg: "task_id",
2781
+ type: "string",
2782
+ required: true
2783
+ },
2784
+ {
2785
+ flags: "--reply <reply>",
2786
+ description: "Your answer to the agent's question (e.g. a 2FA code or confirmation).",
2787
+ arg: "reply",
2788
+ type: "string",
2789
+ required: true
2790
+ }
2791
+ ]
2792
+ },
2793
+ {
2794
+ name: "usage",
2795
+ tool: "ui_agent_usage",
2796
+ summary: "Returns an estimated Anthropic API cost for Steel agent tasks dispatched this calendar month.",
2797
+ options: []
2798
+ },
2799
+ {
2800
+ name: "schedule list",
2801
+ tool: "ui_agent_schedule_list",
2802
+ summary: "Lists all Steel recurring schedules stored in KV.",
2803
+ options: []
2804
+ },
2805
+ {
2806
+ name: "schedule create",
2807
+ tool: "ui_agent_schedule_create",
2808
+ summary: "Creates a recurring Steel agent run on a cron schedule.",
2809
+ requireConfirm: true,
2810
+ options: [
2811
+ {
2812
+ flags: "--name <name>",
2813
+ description: "Human-readable name for this schedule.",
2814
+ arg: "name",
2815
+ type: "string",
2816
+ required: true
2817
+ },
2818
+ {
2819
+ flags: "--cron <cron>",
2820
+ description: "Standard 5-field cron expression (minute hour day month weekday).",
2821
+ arg: "cron",
2822
+ type: "string",
2823
+ required: true
2824
+ },
2825
+ {
2826
+ flags: "--prompt-template <template>",
2827
+ description: "Agent prompt/task template the Steel agent will execute on each run.",
2828
+ arg: "prompt_template",
2829
+ type: "string",
2830
+ required: true
2831
+ }
2832
+ ]
2833
+ },
2834
+ {
2835
+ name: "schedule pause",
2836
+ tool: "ui_agent_schedule_pause",
2837
+ summary: "Pauses an active Steel recurring schedule.",
2838
+ requireConfirm: true,
2839
+ options: [
2840
+ {
2841
+ flags: "--schedule-id <id>",
2842
+ description: "ID of the schedule to pause.",
2843
+ arg: "schedule_id",
2844
+ type: "string",
2845
+ required: true
2846
+ }
2847
+ ]
2848
+ },
2849
+ {
2850
+ name: "schedule resume",
2851
+ tool: "ui_agent_schedule_resume",
2852
+ summary: "Resumes a paused Steel recurring schedule.",
2853
+ requireConfirm: true,
2854
+ options: [
2855
+ {
2856
+ flags: "--schedule-id <id>",
2857
+ description: "ID of the schedule to resume.",
2858
+ arg: "schedule_id",
2859
+ type: "string",
2860
+ required: true
2861
+ }
2862
+ ]
2863
+ },
2864
+ {
2865
+ name: "schedule delete",
2866
+ tool: "ui_agent_schedule_delete",
2867
+ summary: "Permanently deletes a Steel recurring schedule.",
2868
+ requireConfirm: true,
2869
+ options: [
2870
+ {
2871
+ flags: "--schedule-id <id>",
2872
+ description: "ID of the schedule to delete.",
2873
+ arg: "schedule_id",
2874
+ type: "string",
2875
+ required: true
2876
+ }
2877
+ ]
2878
+ }
2879
+ ]
2880
+ },
2881
+ {
2882
+ name: "portal",
2883
+ summary: "OCS portal changes that only exist in the web UI, driven by Kapture",
2884
+ commands: [
2885
+ {
2886
+ name: "account create",
2887
+ tool: "ui_create_account",
2888
+ summary: "Creates a new sub-account under the reseller via the OCS web dashboard.",
2889
+ write: true,
2890
+ options: [
2891
+ {
2892
+ flags: "--name <name>",
2893
+ description: "Name for the new account.",
2894
+ arg: "name",
2895
+ type: "string",
2896
+ required: true
2897
+ },
2898
+ {
2899
+ flags: "--description <description>",
2900
+ description: "Optional description.",
2901
+ arg: "description",
2902
+ type: "string"
2903
+ },
2904
+ {
2905
+ flags: "--initial-balance <n>",
2906
+ description: "Initial balance in account currency (default 0).",
2907
+ arg: "initial_balance",
2908
+ type: "number"
2909
+ }
2910
+ ]
2911
+ },
2912
+ {
2913
+ name: "steering build",
2914
+ tool: "ui_build_steering_list",
2915
+ summary: "Change which mobile operators sit on a steering list that already exists, by driving the OCS web dashboard \u2014 the OCS REST API cannot edit list membership.",
2916
+ write: true,
2917
+ options: [
2918
+ {
2919
+ flags: "--steering-list-id <n>",
2920
+ description: "ID of the steering list to modify.",
2921
+ arg: "steering_list_id",
2922
+ type: "number",
2923
+ required: true
2924
+ },
2925
+ {
2926
+ flags: "--add-operators <list>",
2927
+ description: "MCC-MNC codes to add (e.g. ['20801', '26201']). Comma-separated.",
2928
+ arg: "add_operators",
2929
+ type: "string[]"
2930
+ },
2931
+ {
2932
+ flags: "--remove-operators <list>",
2933
+ description: "MCC-MNC codes to remove. Comma-separated.",
2934
+ arg: "remove_operators",
2935
+ type: "string[]"
2936
+ },
2937
+ {
2938
+ flags: "--operator-type <type>",
2939
+ description: 'Whether operators are priority or excluded. One of: priority, excluded. Default "priority".',
2940
+ arg: "operator_type",
2941
+ type: "string"
2942
+ }
2943
+ ]
2944
+ },
2945
+ {
2946
+ name: "steering create",
2947
+ tool: "ui_create_steering_list",
2948
+ summary: "Create a new, empty network steering list (OPLMN preference set) by driving the OCS web dashboard, because the OCS REST API cannot create one.",
2949
+ write: true,
2950
+ options: [
2951
+ {
2952
+ flags: "--name <name>",
2953
+ description: "Name for the new steering list.",
2954
+ arg: "name",
2955
+ type: "string",
2956
+ required: true
2957
+ },
2958
+ {
2959
+ flags: "--description <description>",
2960
+ description: "Optional description for the steering list.",
2961
+ arg: "description",
2962
+ type: "string"
2963
+ }
2964
+ ]
2965
+ },
2966
+ {
2967
+ name: "steering set-account",
2968
+ tool: "ui_set_account_steering_list",
2969
+ summary: "Assigns or removes a steering list at the account level via the OCS web dashboard.",
2970
+ write: true,
2971
+ options: [
2972
+ {
2973
+ flags: "--account-id <n>",
2974
+ description: "Account ID to assign the steering list to.",
2975
+ arg: "account_id",
2976
+ type: "number",
2977
+ required: true
2978
+ },
2979
+ {
2980
+ flags: "--steering-list-id <n>",
2981
+ description: "Steering list ID to assign (0 to remove/unset).",
2982
+ arg: "steering_list_id",
2983
+ type: "number",
2984
+ required: true
2985
+ }
2986
+ ]
2987
+ },
2988
+ {
2989
+ name: "destinations create",
2990
+ tool: "ui_create_destination_list",
2991
+ summary: "Creates a new destination list (named set of phone number prefixes for MOC call permissions) via the OCS web dashboard.",
2992
+ write: true,
2993
+ options: [
2994
+ {
2995
+ flags: "--name <name>",
2996
+ description: "Name for the new destination list.",
2997
+ arg: "name",
2998
+ type: "string",
2999
+ required: true
3000
+ },
3001
+ {
3002
+ flags: "--prefixes <list>",
3003
+ description: "Phone number prefixes to include (e.g. ['+31', '+49']). Comma-separated.",
3004
+ arg: "prefixes",
3005
+ type: "string[]"
3006
+ },
3007
+ {
3008
+ flags: "--description <description>",
3009
+ description: "Optional description.",
3010
+ arg: "description",
3011
+ type: "string"
3012
+ }
3013
+ ]
3014
+ },
3015
+ {
3016
+ name: "destinations edit",
3017
+ tool: "ui_edit_destination_list",
3018
+ summary: "Edits an existing destination list via the OCS web dashboard.",
3019
+ write: true,
3020
+ options: [
3021
+ {
3022
+ flags: "--destination-list-id <n>",
3023
+ description: "ID of the destination list to edit.",
3024
+ arg: "destination_list_id",
3025
+ type: "number",
3026
+ required: true
3027
+ },
3028
+ {
3029
+ flags: "--add-prefixes <list>",
3030
+ description: "Prefixes to add. Comma-separated.",
3031
+ arg: "add_prefixes",
3032
+ type: "string[]"
3033
+ },
3034
+ {
3035
+ flags: "--remove-prefixes <list>",
3036
+ description: "Prefixes to remove. Comma-separated.",
3037
+ arg: "remove_prefixes",
3038
+ type: "string[]"
3039
+ },
3040
+ {
3041
+ flags: "--new-name <name>",
3042
+ description: "Rename the destination list.",
3043
+ arg: "new_name",
3044
+ type: "string"
3045
+ }
3046
+ ]
3047
+ },
3048
+ {
3049
+ name: "destinations delete",
3050
+ tool: "ui_delete_destination_list",
3051
+ summary: "Delete a destination list \u2014 a named set of phone-number prefixes \u2014 by driving the OCS web dashboard.",
3052
+ write: true,
3053
+ options: [
3054
+ {
3055
+ flags: "--destination-list-id <n>",
3056
+ description: "ID of the destination list to delete.",
3057
+ arg: "destination_list_id",
3058
+ type: "number",
3059
+ required: true
3060
+ }
3061
+ ]
3062
+ },
3063
+ {
3064
+ name: "zones edit",
3065
+ tool: "ui_edit_location_zone",
3066
+ summary: "Edits an existing location zone via the OCS web dashboard. `create_location_zone` is available via API; edit is UI-only.",
3067
+ write: true,
3068
+ options: [
3069
+ {
3070
+ flags: "--zone-id <n>",
3071
+ description: "ID of the location zone to edit.",
3072
+ arg: "zone_id",
3073
+ type: "number",
3074
+ required: true
3075
+ },
3076
+ {
3077
+ flags: "--new-name <name>",
3078
+ description: "Rename the location zone.",
3079
+ arg: "new_name",
3080
+ type: "string"
3081
+ },
3082
+ {
3083
+ flags: "--add-countries <list>",
3084
+ description: "ISO country codes to add (e.g. ['NL', 'DE']). Comma-separated.",
3085
+ arg: "add_countries",
3086
+ type: "string[]"
3087
+ },
3088
+ {
3089
+ flags: "--remove-countries <list>",
3090
+ description: "ISO country codes to remove. Comma-separated.",
3091
+ arg: "remove_countries",
3092
+ type: "string[]"
3093
+ }
3094
+ ]
3095
+ },
3096
+ {
3097
+ name: "zones delete",
3098
+ tool: "ui_delete_location_zone",
3099
+ summary: "Delete a location zone by driving the OCS web dashboard, because the OCS REST API can create a zone (`create_location_zone`) but not remove one.",
3100
+ write: true,
3101
+ options: [
3102
+ {
3103
+ flags: "--zone-id <n>",
3104
+ description: "ID of the location zone to delete.",
3105
+ arg: "zone_id",
3106
+ type: "number",
3107
+ required: true
3108
+ }
3109
+ ]
3110
+ },
3111
+ {
3112
+ name: "templates delete",
3113
+ tool: "ui_delete_package_template",
3114
+ summary: "Remove a package template from the reseller's product catalogue for good, by driving the OCS web dashboard \u2014 the OCS REST API cannot delete a template.",
3115
+ write: true,
3116
+ options: [
3117
+ {
3118
+ flags: "--template-id <n>",
3119
+ description: "ID of the package template to delete.",
3120
+ arg: "template_id",
3121
+ type: "number",
3122
+ required: true
3123
+ }
3124
+ ]
3125
+ }
3126
+ ]
3127
+ },
3128
+ {
3129
+ name: "events",
3130
+ summary: "Buffered OCS event history per subscriber",
3131
+ commands: [
3132
+ {
3133
+ name: "ocs",
3134
+ tool: "list_recent_ocs_events",
3135
+ summary: "Return the last N OCS events buffered for a given ICCID.",
3136
+ options: [
3137
+ {
3138
+ flags: "--iccid <iccid>",
3139
+ description: "ICCID, 19 or 20 digits, ITU-T E.118 format.",
3140
+ arg: "iccid",
3141
+ type: "string",
3142
+ required: true
3143
+ },
3144
+ {
3145
+ flags: "--limit <n>",
3146
+ description: "Max events to return, newest first. Default 20.",
3147
+ arg: "limit",
3148
+ type: "number"
3149
+ },
3150
+ {
3151
+ flags: "--event-types <list>",
3152
+ description: "Filter to specific event types. Comma-separated.",
3153
+ arg: "event_types",
3154
+ type: "string[]"
3155
+ },
3156
+ {
3157
+ flags: "--since <since>",
3158
+ description: "ISO-8601 timestamp; only events after this point.",
3159
+ arg: "since",
3160
+ type: "string"
3161
+ }
3162
+ ]
3163
+ },
3164
+ {
3165
+ name: "countries",
3166
+ tool: "subscriber_country_history",
3167
+ summary: "Return cross-border movement history for a subscriber from Relay LU events.",
3168
+ options: [
3169
+ {
3170
+ flags: "--subscriber-id <id>",
3171
+ description: "Subscriber ICCID (19\u201320 digits, ITU-T E.118).",
3172
+ arg: "subscriberId",
3173
+ type: "string",
3174
+ required: true
3175
+ },
3176
+ {
3177
+ flags: "--limit <n>",
3178
+ description: "Maximum number of events to return, newest first (default 20, max 50). Default 20.",
3179
+ arg: "limit",
3180
+ type: "number"
3181
+ }
3182
+ ]
3183
+ },
3184
+ {
3185
+ name: "depletion",
3186
+ tool: "subscriber_depletion_events",
3187
+ summary: "Look up bundle depletion events for a subscriber (or list recent fleet-wide depletions).",
3188
+ options: [
3189
+ {
3190
+ flags: "--subscriber-id <id>",
3191
+ description: "OCS subscriber ID to look up.",
3192
+ arg: "subscriberId",
3193
+ type: "string"
3194
+ },
3195
+ {
3196
+ flags: "--since <since>",
3197
+ description: "ISO-8601 timestamp \u2014 only return events at or after this time (for single-subscriber lookup).",
3198
+ arg: "since",
3199
+ type: "string"
3200
+ }
3201
+ ]
3202
+ }
3203
+ ]
3204
+ },
3205
+ {
3206
+ name: "panels",
3207
+ summary: "MCP UI surfaces. A terminal sees the underlying data, not the panel.",
3208
+ commands: [
3209
+ {
3210
+ name: "fleet",
3211
+ tool: "fleet_health_app",
3212
+ summary: "Fleet overview as a UI panel. Prefer `carrier intelligence fleet-health` in a terminal; use --json to read the panel's data.",
3213
+ options: [
3214
+ {
3215
+ flags: "--account-id <n>",
3216
+ description: "Filter to a specific account (omit for all).",
3217
+ arg: "accountId",
3218
+ type: "number"
3219
+ }
3220
+ ]
3221
+ },
3222
+ {
3223
+ name: "topup",
3224
+ tool: "balance_topup_form",
3225
+ summary: "Adjust the OCS account balance behind one subscriber. Previews unless --preview false.",
3226
+ requireConfirm: true,
3227
+ options: [
3228
+ {
3229
+ flags: "--iccid <iccid>",
3230
+ description: "The subscriber ICCID for the account lookup.",
3231
+ arg: "iccid",
3232
+ type: "string",
3233
+ required: true
3234
+ },
3235
+ {
3236
+ flags: "--delta <n>",
3237
+ description: "Amount to add (positive) or deduct (negative) from the account balance.",
3238
+ arg: "delta",
3239
+ type: "number",
3240
+ required: true
3241
+ },
3242
+ {
3243
+ flags: "--preview <bool>",
3244
+ description: "If true, return preview without writing. Default true.",
3245
+ arg: "preview",
3246
+ type: "boolean"
3247
+ }
3248
+ ]
3249
+ },
3250
+ {
3251
+ name: "provision",
3252
+ tool: "provision_esim_wizard",
3253
+ summary: "Step through the provisioning wizard. Pass --step confirm to execute; every earlier step only reads.",
3254
+ options: [
3255
+ {
3256
+ flags: "--step <step>",
3257
+ description: "Current wizard step. One of: init, select-package, preview, confirm.",
3258
+ arg: "step",
3259
+ type: "string",
3260
+ required: true
3261
+ },
3262
+ {
3263
+ flags: "--wizard-id <id>",
3264
+ description: "Wizard session ID (absent on init).",
3265
+ arg: "wizardId",
3266
+ type: "string"
3267
+ },
3268
+ {
3269
+ flags: "--subscriber-iccid <iccid>",
3270
+ description: "Subscriber ICCID (required for select-package).",
3271
+ arg: "subscriber_iccid",
3272
+ type: "string"
3273
+ },
3274
+ {
3275
+ flags: "--package-template-id <n>",
3276
+ description: "Package template ID (required for preview).",
3277
+ arg: "package_template_id",
3278
+ type: "number"
3279
+ }
3280
+ ]
3281
+ }
3282
+ ]
3283
+ },
3284
+ {
3285
+ name: "env",
3286
+ summary: "This environment: credentials, catalog and machine-readable context",
3287
+ commands: [
3288
+ {
3289
+ name: "info",
3290
+ tool: "environment_info",
3291
+ summary: "View your Carrier environment configuration \u2014 active organization, reseller details, connected services, and deployment environment.",
3292
+ options: []
3293
+ },
3294
+ {
3295
+ name: "credentials",
3296
+ tool: "credential_status",
3297
+ summary: "Report on the stored eSIMVault credential without changing it: whether a token is present, whether it is encrypted, how old it is, and whether rotation is recommended (over 90 days).",
3298
+ options: []
3299
+ },
3300
+ {
3301
+ name: "rotate",
3302
+ tool: "rotate_credentials",
3303
+ summary: "Replace the stored eSIMVault API token with one you supply.",
3304
+ options: [
3305
+ {
3306
+ flags: "--new-token <token>",
3307
+ description: "New eSIMVault API token to encrypt and store.",
3308
+ arg: "new_token",
3309
+ type: "string",
3310
+ required: true
3311
+ },
3312
+ {
3313
+ flags: "--confirm <bool>",
3314
+ description: "Confirm rotation \u2014 this will replace the current token immediately.",
3315
+ arg: "confirm",
3316
+ type: "boolean",
3317
+ required: true
3318
+ }
3319
+ ]
3320
+ },
3321
+ {
3322
+ name: "catalog",
3323
+ tool: "service_catalog",
3324
+ summary: "Browse the Carrier service catalog \u2014 discover available services, their requirements, endpoints, and documentation.",
3325
+ options: [
3326
+ {
3327
+ flags: "--category <category>",
3328
+ description: "Filter services by category. One of: connectivity, analytics, marketplace, coverage, billing, integration, dashboard.",
3329
+ arg: "category",
3330
+ type: "string"
3331
+ },
3332
+ {
3333
+ flags: "--tier <tier>",
3334
+ description: "Filter services accessible at this tier level. One of: free, pro, enterprise.",
3335
+ arg: "tier",
3336
+ type: "string"
3337
+ }
3338
+ ]
3339
+ },
3340
+ {
3341
+ name: "llm-context",
3342
+ tool: "llm_context",
3343
+ summary: "Generate a comprehensive LLM context document describing your Carrier environment, available tools, current tier, usage patterns, and best practices.",
3344
+ options: [
3345
+ {
3346
+ flags: "--format <format>",
3347
+ description: "Output format for the context document (default: markdown). One of: markdown, json, yaml.",
3348
+ arg: "format",
3349
+ type: "string"
3350
+ },
3351
+ {
3352
+ flags: "--include-examples <bool>",
3353
+ description: "Include example tool invocations (default: true).",
3354
+ arg: "include_examples",
3355
+ type: "boolean"
3356
+ }
3357
+ ]
3358
+ },
3359
+ {
3360
+ name: "describe",
3361
+ tool: "carrier_ask_describe",
3362
+ summary: "Get full documentation for any registered Carrier MCP tool: description, parameters, 2-3 example invocations, required scope, destructive flag, and guidance.",
3363
+ options: [
3364
+ {
3365
+ flags: "--tool-name <name>",
3366
+ description: "The exact MCP tool name to describe (e.g. 'assign_package', 'hlr_set_bitrate').",
3367
+ arg: "tool_name",
3368
+ type: "string",
3369
+ required: true
3370
+ }
3371
+ ]
3372
+ }
3373
+ ]
3374
+ },
3375
+ {
3376
+ name: "brand",
3377
+ summary: "Storefront brand assets",
3378
+ commands: [
3379
+ {
3380
+ name: "logo",
3381
+ tool: "generate_storefront_logo",
3382
+ summary: "Generate a storefront logo (SVG always; PNG when an image key is configured on the Carrier worker).",
3383
+ options: [
3384
+ {
3385
+ flags: "--name <name>",
3386
+ description: "Brand / shop name, e.g. Bananas.",
3387
+ arg: "name",
3388
+ type: "string",
3389
+ required: true
3390
+ },
3391
+ {
3392
+ flags: "--accent <accent>",
3393
+ description: "Accent hex, e.g. #FF8A00.",
3394
+ arg: "accent",
3395
+ type: "string"
3396
+ },
3397
+ {
3398
+ flags: "--tagline <tagline>",
3399
+ description: "Optional tagline for AI-enhanced marks.",
3400
+ arg: "tagline",
3401
+ type: "string"
3402
+ }
3403
+ ]
3404
+ }
3405
+ ]
3406
+ }
3407
+ // </generated:catalog-domains>
3408
+ ];
3409
+ function capabilityTarget(cmd) {
3410
+ return cmd.transport === "rest" ? `${cmd.restMethod} ${cmd.restPath}` : cmd.tool;
3411
+ }
3412
+
3413
+ // src/cli/lib/capability.ts
3414
+ import * as p2 from "@clack/prompts";
3415
+ import pc3 from "picocolors";
3416
+
3417
+ // src/cli/lib/rest.ts
3418
+ var DEFAULT_ERROR_HINTS2 = [
3419
+ "Re-check the arguments: carrier <domain> <command> --help",
3420
+ "Or open Claude and ask there with full tool routing"
3421
+ ];
3422
+ function resolveApiKey() {
3423
+ return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
3424
+ }
3425
+ function ocsTokenOnly() {
3426
+ return Boolean(
3427
+ process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
3428
+ );
3429
+ }
3430
+ function applyPath(path, args) {
3431
+ const rest = { ...args };
3432
+ const missing = [];
3433
+ const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
3434
+ const value = rest[name];
3435
+ if (value === void 0 || value === null || value === "") {
3436
+ missing.push(name);
3437
+ return `{${name}}`;
3438
+ }
3439
+ delete rest[name];
3440
+ return encodeURIComponent(String(value));
3441
+ });
3442
+ return { path: filled, rest, missing };
3443
+ }
3444
+ function queryString(args) {
3445
+ const params = new URLSearchParams();
3446
+ for (const [key, value] of Object.entries(args)) {
3447
+ if (value === void 0 || value === null) continue;
3448
+ params.set(key, String(value));
3449
+ }
3450
+ const qs = params.toString();
3451
+ return qs ? `?${qs}` : "";
3452
+ }
3453
+ function bodyText(raw) {
3454
+ const trimmed = raw.trim();
3455
+ if (!trimmed) return "";
3456
+ try {
3457
+ const parsed = JSON.parse(trimmed);
3458
+ if (typeof parsed.error === "string") return parsed.error;
3459
+ if (typeof parsed.message === "string") return parsed.message;
3460
+ return trimmed;
3461
+ } catch {
3462
+ return trimmed;
3463
+ }
3464
+ }
3465
+ async function callRestEndpoint(method, path, args, opts = {}) {
3466
+ const token = resolveApiKey() ?? await getAccessToken();
3467
+ if (!token) {
3468
+ return {
3469
+ ok: false,
3470
+ text: withNextStep(
3471
+ ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
3472
+ [
3473
+ "Sign in from this terminal: carrier login",
3474
+ "No account yet? carrier open signup",
3475
+ "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
3476
+ ]
3477
+ )
3478
+ };
3479
+ }
3480
+ const { path: resolvedPath, rest, missing } = applyPath(path, args);
3481
+ if (missing.length) {
3482
+ return {
3483
+ ok: false,
3484
+ text: withNextStep(`Cannot build ${method} ${path}.`, [
3485
+ `Missing path value(s): ${missing.join(", ")}`,
3486
+ "See every flag: carrier <domain> <command> --help"
3487
+ ])
3488
+ };
3489
+ }
3490
+ const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
3491
+ const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
3492
+ const headers = {
3493
+ Authorization: `Bearer ${token}`,
3494
+ Accept: "application/json",
3495
+ "User-Agent": `carrier-cli/${CARRIER_VERSION}`
3496
+ };
3497
+ if (sendsBody) headers["Content-Type"] = "application/json";
3498
+ if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
3499
+ const timeoutMs = mcpTimeoutMs();
3500
+ const controller = new AbortController();
3501
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3502
+ try {
3503
+ const res = await fetch(url, {
3504
+ method,
3505
+ headers,
3506
+ signal: controller.signal,
3507
+ body: sendsBody ? JSON.stringify(rest) : void 0
3508
+ });
3509
+ const raw = await res.text();
3510
+ if (res.status === 202) {
3511
+ let confirmToken;
3512
+ let expires;
3513
+ let message = "";
3514
+ try {
3515
+ const parsed = JSON.parse(raw);
3516
+ confirmToken = parsed.confirm_token;
3517
+ expires = parsed.expires_in_seconds;
3518
+ message = parsed.message ?? "";
3519
+ } catch {
3520
+ }
3521
+ return {
3522
+ ok: false,
3523
+ text: withNextStep(
3524
+ message || `${method} ${path} needs an explicit confirmation.`,
3525
+ confirmToken ? [
3526
+ `Re-run the same command with: --confirm-token ${confirmToken}`,
3527
+ `The token is single-use and expires in ${expires ?? 120}s.`
3528
+ ] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
3529
+ )
3530
+ };
3531
+ }
3532
+ if (res.status === 401 || res.status === 403) {
3533
+ return {
3534
+ ok: false,
3535
+ text: withNextStep(
3536
+ `Carrier API returned ${res.status} (auth rejected).`,
3537
+ [
3538
+ "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
3539
+ "Or sign in from this terminal: carrier login",
3540
+ ...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
3541
+ ]
3542
+ )
3543
+ };
3544
+ }
3545
+ if (!res.ok) {
3546
+ return {
3547
+ ok: false,
3548
+ text: withNextStep(
3549
+ redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
3550
+ opts.errorHints ?? DEFAULT_ERROR_HINTS2
3551
+ )
3552
+ };
3553
+ }
3554
+ return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
3555
+ } catch (e) {
3556
+ const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
3557
+ if (aborted) {
3558
+ const secs = Math.round(timeoutMs / 1e3);
3559
+ return {
3560
+ ok: false,
3561
+ text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
3562
+ `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
3563
+ "Confirm the service is up: check network access to https://api.carrier.llc"
3564
+ ])
3565
+ };
3566
+ }
3567
+ const msg = e instanceof Error ? e.message : String(e);
3568
+ return {
3569
+ ok: false,
3570
+ text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
3571
+ "Verify outbound HTTPS to api.carrier.llc",
3572
+ "Use the interactive Claude path while offline from this machine"
3573
+ ])
3574
+ };
3575
+ } finally {
3576
+ clearTimeout(timer);
3577
+ }
3578
+ }
3579
+
3580
+ // src/cli/lib/capability.ts
3581
+ var MAX_PRETTY_CHARS = 8e3;
3582
+ function optionKey(flags) {
3583
+ const long = flags.split(/[ ,|]+/).find((t) => t.startsWith("--"));
3584
+ if (!long) throw new Error(`Option "${flags}" has no long flag`);
3585
+ return long.replace(/^--/, "").split("-").map((part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
3586
+ }
3587
+ function setArg(target, path, value) {
3588
+ const parts = path.split(".");
3589
+ let node = target;
3590
+ for (const part of parts.slice(0, -1)) {
3591
+ const next = node[part];
3592
+ if (typeof next !== "object" || next === null) node[part] = {};
3593
+ node = node[part];
3594
+ }
3595
+ node[parts[parts.length - 1]] = value;
3596
+ }
3597
+ function coerce(opt, raw) {
3598
+ if (opt.type === "boolean") {
3599
+ if (raw === true) return { value: true };
3600
+ const text3 = String(raw).toLowerCase();
3601
+ if (text3 === "true") return { value: true };
3602
+ if (text3 === "false") return { value: false };
3603
+ return { error: `${opt.flags} expects true or false, got "${String(raw)}"` };
3604
+ }
3605
+ if (opt.type === "number") {
3606
+ const n = Number(raw);
3607
+ if (!Number.isFinite(n)) return { error: `${opt.flags} expects a number, got "${String(raw)}"` };
3608
+ return { value: n };
3609
+ }
3610
+ if (opt.type === "string[]" || opt.type === "number[]") {
3611
+ const items = String(raw).split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3612
+ if (!items.length) return { error: `${opt.flags} expects a comma-separated list, got "${String(raw)}"` };
3613
+ if (opt.type === "string[]") return { value: items };
3614
+ const numbers = items.map(Number);
3615
+ const bad = items.find((_, i) => !Number.isFinite(numbers[i]));
3616
+ if (bad !== void 0) return { error: `${opt.flags} expects numbers, got "${bad}"` };
3617
+ return { value: numbers };
3618
+ }
3619
+ return { value: String(raw) };
3620
+ }
3621
+ function buildToolArgs(cmd, opts) {
3622
+ const args = {};
3623
+ const errors = [];
3624
+ const notes = [];
3625
+ for (const opt of cmd.options) {
3626
+ const raw = opts[optionKey(opt.flags)];
3627
+ if (raw === void 0 || raw === false) {
3628
+ if (opt.required) errors.push(`Missing required ${opt.flags}`);
3629
+ continue;
3630
+ }
3631
+ const { value, error } = coerce(opt, raw);
3632
+ if (error) errors.push(error);
3633
+ else setArg(args, opt.arg, value);
3634
+ }
3635
+ if (cmd.requireOneOf && !cmd.requireOneOf.some((name) => args[name] !== void 0)) {
3636
+ const flags = cmd.options.filter((opt) => cmd.requireOneOf.includes(opt.arg)).map((opt) => opt.flags);
3637
+ errors.push(`Pass one of: ${flags.join(", ")}`);
3638
+ }
3639
+ if (cmd.usageWindow) {
3640
+ const start = args.startDate;
3641
+ const end = args.endDate;
3642
+ if (!start && !end) {
3643
+ const window = lastNDaysPeriod();
3644
+ args.startDate = window.start;
3645
+ args.endDate = window.end;
3646
+ notes.push(`Window defaulted to ${window.start} \u2026 ${window.end} (last ${OCS_MAX_USAGE_WINDOW_DAYS} days).`);
3647
+ } else if (!start || !end) {
3648
+ errors.push("Pass both --start and --end, or neither for the last 7 days");
3649
+ } else {
3650
+ const clamped = clampUsagePeriod(start, end);
3651
+ if (clamped.start !== start) {
3652
+ notes.push(
3653
+ `Start moved to ${clamped.start} \u2014 OCS rejects windows wider than ${OCS_MAX_USAGE_WINDOW_DAYS} days.`
3654
+ );
3655
+ }
3656
+ args.startDate = clamped.start;
3657
+ args.endDate = clamped.end;
3658
+ }
3659
+ }
3660
+ if (cmd.write && cmd.transport !== "rest") args.dry_run = true;
3661
+ return { args, errors, notes };
3662
+ }
3663
+ function formatJsonOutput(text3) {
3664
+ try {
3665
+ JSON.parse(text3);
3666
+ return text3;
3667
+ } catch {
3668
+ return JSON.stringify(text3);
3669
+ }
3670
+ }
3671
+ async function runCapability(domain, cmd, opts) {
3672
+ const label = `${domain.name} ${cmd.name}`;
3673
+ const quiet = opts.json === true;
3674
+ const { args, errors, notes } = buildToolArgs(cmd, opts);
3675
+ if (cmd.write && opts.commit === true) delete args.dry_run;
3676
+ if (cmd.requireConfirm && opts.confirm !== true) {
3677
+ errors.push(`${capabilityTarget(cmd)} has no preview mode. Pass --confirm to run it for real.`);
3678
+ }
3679
+ if (errors.length) {
3680
+ const message = withNextStep(`Cannot run \`carrier ${label}\`.`, [
3681
+ ...errors,
3682
+ `See every flag: carrier ${label} --help`
3683
+ ]);
3684
+ if (quiet) console.error(message);
3685
+ else p2.log.error(message);
3686
+ process.exitCode = 1;
3687
+ return;
3688
+ }
3689
+ if (cmd.transport === "rest" && cmd.write && opts.commit !== true) {
3690
+ const preview = [
3691
+ `${cmd.restMethod} ${cmd.restPath}`,
3692
+ JSON.stringify(args, null, 2),
3693
+ "",
3694
+ "Dry run \u2014 nothing sent. Re-run with --commit to apply."
3695
+ ].join("\n");
3696
+ if (quiet) process.stdout.write(`${formatJsonOutput(preview)}
3697
+ `);
3698
+ else p2.note(preview, `carrier ${label}`);
3699
+ return;
3700
+ }
3701
+ const dryRun = cmd.write === true && args.dry_run === true;
3702
+ const warnings = dryRun ? [...notes, "Dry run \u2014 nothing changes. Re-run with --commit to apply."] : notes;
3703
+ for (const note4 of warnings) {
3704
+ if (quiet) console.error(note4);
3705
+ else p2.log.info(pc3.dim(note4));
3706
+ }
3707
+ const target = capabilityTarget(cmd);
3708
+ const errorHints = [
3709
+ `Check the arguments: carrier ${label} --help`,
3710
+ 'Or describe the task in words: carrier ask "\u2026"'
3711
+ ];
3712
+ const s = quiet ? null : p2.spinner();
3713
+ s?.start(`Calling ${target}`);
3714
+ const result = cmd.transport === "rest" ? await callRestEndpoint(cmd.restMethod, cmd.restPath, args, {
3715
+ errorHints,
3716
+ confirmToken: opts.confirmToken
3717
+ }) : await callMcpTool(cmd.tool, args, { errorHints });
3718
+ s?.stop(result.ok ? "Done" : "Failed");
3719
+ if (!result.ok) {
3720
+ if (quiet) console.error(result.text);
3721
+ else p2.log.error(result.text);
3722
+ process.exitCode = 1;
3723
+ return;
3724
+ }
3725
+ if (quiet) {
3726
+ process.stdout.write(`${formatJsonOutput(result.text)}
3727
+ `);
3728
+ return;
3729
+ }
3730
+ const body = result.text.length > MAX_PRETTY_CHARS ? `${result.text.slice(0, MAX_PRETTY_CHARS)}
3731
+ \u2026truncated. Re-run with --json for the whole response.` : result.text;
3732
+ p2.note(body, target);
3733
+ }
3734
+ function parseArgValue(text3) {
3735
+ if (text3 === "true") return true;
3736
+ if (text3 === "false") return false;
3737
+ if (text3 === "null") return null;
3738
+ if (/^[[{"]/.test(text3)) {
3739
+ try {
3740
+ return JSON.parse(text3);
3741
+ } catch {
3742
+ return text3;
3743
+ }
3744
+ }
3745
+ if (/^-?\d+(?:\.\d+)?$/.test(text3)) {
3746
+ const n = Number(text3);
3747
+ if (Number.isFinite(n) && String(n) === text3) return n;
3748
+ }
3749
+ return text3;
3750
+ }
3751
+ async function runRawTool(name, pairs, opts) {
3752
+ const quiet = opts.json === true;
3753
+ const args = {};
3754
+ const errors = [];
3755
+ for (const pair of pairs) {
3756
+ const eq = pair.indexOf("=");
3757
+ if (eq < 1) {
3758
+ errors.push(`--arg ${pair} is not key=value`);
3759
+ continue;
3760
+ }
3761
+ setArg(args, pair.slice(0, eq), parseArgValue(pair.slice(eq + 1)));
3762
+ }
3763
+ if (errors.length) {
3764
+ const message = withNextStep(`Cannot call \`${name}\`.`, [
3765
+ ...errors,
3766
+ "Arguments look like: --arg iccid=8931000000000000000 --arg limit=20"
3767
+ ]);
3768
+ if (quiet) console.error(message);
3769
+ else p2.log.error(message);
3770
+ process.exitCode = 1;
3771
+ return;
3772
+ }
3773
+ const s = quiet ? null : p2.spinner();
3774
+ s?.start(`Calling ${name}`);
3775
+ const result = await callMcpTool(name, args, {
3776
+ errorHints: [
3777
+ "List every tool this server registers: carrier env catalog",
3778
+ 'Or describe the task in words: carrier ask "\u2026"'
3779
+ ]
3780
+ });
3781
+ s?.stop(result.ok ? "Done" : "Failed");
3782
+ if (!result.ok) {
3783
+ if (quiet) console.error(result.text);
3784
+ else p2.log.error(result.text);
3785
+ process.exitCode = 1;
3786
+ return;
3787
+ }
3788
+ if (quiet) {
3789
+ process.stdout.write(`${formatJsonOutput(result.text)}
3790
+ `);
3791
+ return;
3792
+ }
3793
+ const body = result.text.length > MAX_PRETTY_CHARS ? `${result.text.slice(0, MAX_PRETTY_CHARS)}
3794
+ \u2026truncated. Re-run with --json for the whole response.` : result.text;
3795
+ p2.note(body, name);
3796
+ }
3797
+
3798
+ // src/cli/index.ts
3799
+ var VERSION = CARRIER_VERSION;
3800
+ function header() {
3801
+ p3.intro(`${pc4.bold(pc4.yellow("\u25C6 carrier"))} ${pc4.dim("\xB7 the Stripe of telecom \u2014 CLI v" + VERSION)}`);
3802
+ }
3803
+ function ok(msg) {
3804
+ p3.log.success(pc4.green(msg));
3805
+ }
3806
+ function info(msg) {
3807
+ p3.log.info(msg);
3808
+ }
3809
+ function fail(msg, next) {
3810
+ p3.log.error(msg);
3811
+ p3.note(next.map((s) => `\u2192 ${s}`).join("\n"), "What to do next");
3812
+ }
3813
+ async function promptBrand(seed) {
3814
+ const name = await p3.text({
3815
+ message: "Brand name",
3816
+ placeholder: seed.name,
3817
+ defaultValue: seed.name
3818
+ });
3819
+ if (p3.isCancel(name)) process.exit(0);
3820
+ const domain = await p3.text({
3821
+ message: "Domain",
3822
+ placeholder: seed.domain,
3823
+ defaultValue: seed.domain
3824
+ });
3825
+ if (p3.isCancel(domain)) process.exit(0);
3826
+ const accent = await p3.text({
3827
+ message: "Accent color (hex)",
3828
+ placeholder: seed.colors.accent,
3829
+ defaultValue: seed.colors.accent
3830
+ });
3831
+ if (p3.isCancel(accent)) process.exit(0);
3832
+ const supportEmail = await p3.text({
3833
+ message: "Support email",
3834
+ placeholder: `support@${domain}`,
3835
+ defaultValue: `support@${domain}`
3836
+ });
3837
+ if (p3.isCancel(supportEmail)) process.exit(0);
3838
+ const accentDark = deriveAccentDark(accent, seed);
3839
+ const isCarrier = name === CARRIER_BRAND.name && domain === CARRIER_BRAND.domain;
3840
+ return {
3841
+ ...seed,
3842
+ name,
3843
+ legalName: name,
3844
+ tagline: isCarrier ? seed.tagline : `Mobile connectivity by ${name}.`,
3845
+ domain,
3846
+ supportEmail,
3847
+ supportUrl: `https://${domain}/help`,
3848
+ supportWhatsapp: isCarrier ? seed.supportWhatsapp : "",
3849
+ social: isCarrier ? seed.social : {},
3850
+ colors: { ...seed.colors, accent, accentDark }
3851
+ };
3852
+ }
3853
+ async function doPluginInstall() {
3854
+ const s = p3.spinner();
3855
+ s.start("Installing Carrier Claude Code plugin + zero-cred MCP");
3856
+ let r;
3857
+ try {
3858
+ r = await installPlugin();
3859
+ } catch (e) {
3860
+ s.stop("Install failed");
3861
+ fail(String(e instanceof Error ? e.message : e), [
3862
+ "Reinstall the package: npm i -g @carrierllc/mcp (or npx @carrierllc/mcp)",
3863
+ "Or finish manually with the commands under `carrier plugin install` help",
3864
+ `Sign up anytime: ${SIGN_UP_URL}`
3865
+ ]);
3866
+ return;
3867
+ }
3868
+ s.stop("Plugin staged");
3869
+ ok(`Plugin \u2192 ${r.copiedTo}`);
3870
+ info(
3871
+ `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")}`
3872
+ );
3873
+ p3.note(installAuthGuidance().join("\n"), "OAuth on first use (Entry A)");
3874
+ if (!r.claudeFound || r.notes.length) {
3875
+ p3.note(manualCommands().join("\n"), "Finish wiring (run in your terminal)");
3876
+ if (r.notes.length) info(pc4.dim(r.notes.join("\n")));
3877
+ p3.note(
3878
+ [
3879
+ "You can still create an account now:",
3880
+ ` carrier open signup`,
3881
+ "Then open Claude and talk to your fleet \u2014 browser OAuth completes auth."
3882
+ ].join("\n"),
3883
+ "Next"
3884
+ );
3885
+ } else {
3886
+ p3.note(
3887
+ [
3888
+ "Open Claude Code and say something like:",
3889
+ ' "Show my fleet health"',
3890
+ "First call opens the browser for sign-in / sign-up. No token paste.",
3891
+ "",
3892
+ "More prompts: carrier examples"
3893
+ ].join("\n"),
3894
+ "Talk to your fleet"
3895
+ );
3896
+ }
3897
+ }
3898
+ async function doSiteCreate(target, brand) {
3899
+ const s = p3.spinner();
3900
+ s.start(`Scaffolding ${brand.name} storefront \u2192 ${target}`);
3901
+ try {
3902
+ await scaffoldStorefront(target, brand);
3903
+ } catch (e) {
3904
+ s.stop("Scaffold failed");
3905
+ fail(String(e instanceof Error ? e.message : e), [
3906
+ "Pick a free directory: carrier site create ./my-storefront",
3907
+ "Ensure the package templates shipped with @carrierllc/mcp",
3908
+ "Still stuck? carrier open console"
3909
+ ]);
2524
3910
  throw e;
2525
3911
  }
2526
3912
  s.stop("Storefront scaffolded");
@@ -3085,10 +4471,16 @@ for (const domain of CLI_DOMAINS) {
3085
4471
  }
3086
4472
  parent = nested;
3087
4473
  }
3088
- const sub = parent.command(parts[parts.length - 1]).description(cmd.write ? `${cmd.summary} Dry run unless --commit.` : cmd.summary);
4474
+ const sub = parent.command(parts[parts.length - 1]).description(
4475
+ cmd.write ? `${cmd.summary} Dry run unless --commit.` : cmd.requireConfirm ? `${cmd.summary} Refuses without --confirm.` : cmd.summary
4476
+ );
3089
4477
  for (const opt of cmd.options) sub.option(opt.flags, opt.description);
3090
4478
  sub.option("--json", "Print the raw tool response on stdout and nothing else");
3091
4479
  if (cmd.write) sub.option("--commit", "Apply the change instead of previewing it");
4480
+ if (cmd.transport === "rest") {
4481
+ sub.option("--confirm-token <token>", "Token from a prior 202, to confirm this call");
4482
+ }
4483
+ if (cmd.requireConfirm) sub.option("--confirm", "Required. This call takes effect immediately.");
3092
4484
  sub.action(async (opts) => {
3093
4485
  if (opts.json !== true) header();
3094
4486
  await runCapability(domain, cmd, opts);
@@ -3096,6 +4488,16 @@ for (const domain of CLI_DOMAINS) {
3096
4488
  });
3097
4489
  }
3098
4490
  }
4491
+ program.command("tool <name>").description("Call one MCP tool directly, by name, with raw arguments").option(
4492
+ "--arg <key=value>",
4493
+ "One tool argument. Repeat for more. Dots nest: context.iccid=8931\u2026",
4494
+ (value, prev) => [...prev, value],
4495
+ []
4496
+ ).option("--json", "Print the raw tool response on stdout and nothing else").action(async (name, o) => {
4497
+ if (o.json !== true) header();
4498
+ await runRawTool(name, o.arg, o);
4499
+ if (o.json !== true) p3.outro("");
4500
+ });
3099
4501
  program.command("dash [domain]").description(`Render a status screen in the terminal: ${DASH_DOMAINS.join(" | ")}`).option("--json", "Emit the screen model as JSON instead of drawing it").option("--no-color", "Plain text, safe to pipe").option("--width <n>", "Terminal width override").action(async (domain, o) => {
3100
4502
  const target = domain ?? "fleet";
3101
4503
  if (!isDashDomain(target)) {