@carrierllc/mcp 0.13.29 → 0.13.31

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
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  CARRIER_BRAND,
4
4
  CARRIER_VERSION,
5
+ OCS_LIST_KEYS,
5
6
  OCS_MAX_USAGE_WINDOW_DAYS,
6
7
  TARGET_IDS,
7
8
  billingScreen,
@@ -13,6 +14,7 @@ import {
13
14
  deriveAccentDark,
14
15
  deriveAccentLight,
15
16
  deriveAccentPaletteSubs,
17
+ extractSubscriberStatus,
16
18
  fleetScreen,
17
19
  formatProbes,
18
20
  generateStorefrontLogo,
@@ -22,6 +24,7 @@ import {
22
24
  lastNDaysPeriod,
23
25
  loadStorefrontBrand,
24
26
  mergeEnvLocal,
27
+ package_default,
25
28
  packagesScreen,
26
29
  probeAll,
27
30
  provisionClerk,
@@ -35,12 +38,13 @@ import {
35
38
  slug,
36
39
  storefrontClerkUrls,
37
40
  storefrontScreen,
41
+ subscriberIccid,
38
42
  subscribersScreen,
39
43
  usageScreen,
40
44
  verifyStorefront,
41
45
  walletScreen,
42
46
  which
43
- } from "./chunk-LJPYVILK.js";
47
+ } from "./chunk-EHJEAI4V.js";
44
48
  import {
45
49
  copyTree,
46
50
  exists,
@@ -54,7 +58,7 @@ import {
54
58
  import { Command } from "commander";
55
59
  import * as p3 from "@clack/prompts";
56
60
  import pc4 from "picocolors";
57
- import { resolve as resolve2 } from "path";
61
+ import { resolve as resolve4 } from "path";
58
62
 
59
63
  // src/cli/lib/plugin.ts
60
64
  import { join as join2 } from "path";
@@ -66,7 +70,7 @@ import { existsSync } from "fs";
66
70
  import { homedir } from "os";
67
71
  function packageRoot() {
68
72
  const here = dirname(fileURLToPath(import.meta.url));
69
- for (const candidate of [resolve(here, ".."), resolve(here, "..", "..")]) {
73
+ for (const candidate of [resolve(here, ".."), resolve(here, "..", ".."), resolve(here, "..", "..", "..")]) {
70
74
  if (existsSync(join(candidate, "plugin")) && existsSync(join(candidate, "templates"))) {
71
75
  return candidate;
72
76
  }
@@ -219,7 +223,7 @@ async function scaffoldStorefront(target, brand) {
219
223
  await patchBrandConfig(join3(target, "src", "brand.config.ts"), brand);
220
224
  const envBody = existingEnv ? preserveClerkFromEnv(existingEnv, renderEnv(brand)) : renderEnv(brand);
221
225
  await writeFile(envPath, envBody);
222
- await writeFile(join3(target, ".npmrc"), "ignore-workspace-root-check=true\n");
226
+ await writeFile(join3(target, ".npmrc"), "# Standalone template installs use pnpm --ignore-workspace explicitly.\n");
223
227
  }
224
228
  async function patchBrandConfig(path, brand) {
225
229
  if (!await exists(path)) return;
@@ -277,13 +281,24 @@ async function writeStorefrontLogo(target, brand) {
277
281
  }
278
282
 
279
283
  // src/cli/lib/auth.ts
280
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
284
+ import { createHash as createHash2, randomBytes, timingSafeEqual } from "crypto";
281
285
  import { createServer } from "http";
282
286
 
287
+ // src/cli/lib/mcp-oauth.ts
288
+ import { createHash } from "crypto";
289
+ import { open as open2, lstat, realpath } from "fs/promises";
290
+ import { constants as constants2 } from "fs";
291
+ import { join as join6, resolve as resolve2, dirname as dirname2 } from "path";
292
+ import { homedir as homedir3, userInfo } from "os";
293
+ import { spawn as spawn2, execFile } from "child_process";
294
+ import { promisify } from "util";
295
+
283
296
  // src/cli/lib/credentials.ts
284
297
  import { homedir as homedir2 } from "os";
285
298
  import { join as join5 } from "path";
286
- import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
299
+ import { chmod, mkdir as mkdir2, rm, rename, open } from "fs/promises";
300
+ import { constants, rmSync } from "fs";
301
+ import { randomUUID } from "crypto";
287
302
  function carrierHome() {
288
303
  return process.env.CARRIER_HOME?.trim() || join5(homedir2(), ".carrier");
289
304
  }
@@ -292,7 +307,15 @@ function credentialsPath() {
292
307
  }
293
308
  async function readCredentials() {
294
309
  try {
295
- const raw = await readFile2(credentialsPath(), "utf8");
310
+ const handle = await open(credentialsPath(), constants.O_RDONLY | constants.O_NOFOLLOW);
311
+ let raw;
312
+ try {
313
+ const stat = await handle.stat();
314
+ if (!stat.isFile() || stat.size > 1024 * 1024 || process.platform !== "win32" && stat.mode & 63 || process.getuid && stat.uid !== process.getuid()) return null;
315
+ raw = await handle.readFile("utf8");
316
+ } finally {
317
+ await handle.close();
318
+ }
296
319
  const parsed = JSON.parse(raw);
297
320
  if (!parsed?.accessToken || typeof parsed.accessToken !== "string") return null;
298
321
  return parsed;
@@ -305,9 +328,56 @@ async function writeCredentials(creds) {
305
328
  await mkdir2(dir, { recursive: true, mode: 448 });
306
329
  await chmod(dir, 448).catch(() => void 0);
307
330
  const file = credentialsPath();
308
- await writeFile3(file, `${JSON.stringify(creds, null, 2)}
309
- `, { mode: 384 });
310
- await chmod(file, 384).catch(() => void 0);
331
+ const temporary = join5(dir, `.credentials-${randomUUID()}.json`);
332
+ try {
333
+ const handle = await open(temporary, "wx", 384);
334
+ try {
335
+ await handle.writeFile(`${JSON.stringify({ ...creds, generation: randomUUID() }, null, 2)}
336
+ `);
337
+ await handle.sync();
338
+ } finally {
339
+ await handle.close();
340
+ }
341
+ await rename(temporary, file);
342
+ } finally {
343
+ await rm(temporary, { force: true });
344
+ }
345
+ }
346
+ async function withCredentialLock(action) {
347
+ const dir = carrierHome();
348
+ await mkdir2(dir, { recursive: true, mode: 448 });
349
+ await chmod(dir, 448);
350
+ const lock = join5(dir, "credentials.lock");
351
+ const deadline = Date.now() + 31e4;
352
+ while (true) {
353
+ try {
354
+ await mkdir2(lock, { mode: 448 });
355
+ break;
356
+ } catch (error) {
357
+ if (error.code !== "EEXIST") throw error;
358
+ if (Date.now() >= deadline) throw new Error("Carrier credential owner is busy; retry or recover its abandoned lock.");
359
+ await new Promise((resolve5) => setTimeout(resolve5, 25));
360
+ }
361
+ }
362
+ let released = false;
363
+ const release = () => {
364
+ if (released) return;
365
+ released = true;
366
+ rmSync(lock, { recursive: true, force: true });
367
+ };
368
+ const onSignal = () => {
369
+ release();
370
+ process.exit(1);
371
+ };
372
+ process.once("SIGINT", onSignal);
373
+ process.once("SIGTERM", onSignal);
374
+ try {
375
+ return await action();
376
+ } finally {
377
+ process.removeListener("SIGINT", onSignal);
378
+ process.removeListener("SIGTERM", onSignal);
379
+ if (!released) await rm(lock, { recursive: true });
380
+ }
311
381
  }
312
382
  async function clearCredentials() {
313
383
  await rm(credentialsPath(), { force: true });
@@ -316,11 +386,403 @@ function isExpired(creds, skewMs = 6e4) {
316
386
  if (!creds.expiresAt) return false;
317
387
  return Date.now() >= creds.expiresAt - skewMs;
318
388
  }
389
+ function redactCredential(value, token) {
390
+ if (typeof value === "string") return value.split(token).join("***");
391
+ if (Array.isArray(value)) return value.map((item) => redactCredential(item, token));
392
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key.split(token).join("***"), redactCredential(item, token)]));
393
+ return value;
394
+ }
319
395
  function redact(text3) {
320
- return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
396
+ return text3.replace(/\bclerk_[^\s:"<>]+:[^\s:"<>]+:[^\s"<>]+/g, "clerk_***").replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
397
+ }
398
+
399
+ // src/cli/lib/mcp-oauth.ts
400
+ var CLAUDE_OWNER_VERSIONS = package_default.carrierCompatibility.claudeOAuthOwnerVersions;
401
+ function usableOAuth(creds) {
402
+ if (creds.issuer !== MCP_HOME || !Number.isFinite(creds.expiresAt) || creds.expiresAt <= 0) return false;
403
+ if (Object.hasOwn(creds, "resource") && creds.resource !== MCP_URL) return false;
404
+ if (typeof creds.scope !== "string" || !creds.scope.split(/\s+/).includes("read")) return false;
405
+ if (typeof creds.accessToken !== "string" || !creds.accessToken || /[\s\0]/.test(creds.accessToken)) return false;
406
+ const account = process.env.CARRIER_OAUTH_ACCOUNT?.trim();
407
+ if (account && creds.accessToken.split(":")[0] !== `clerk_${account}`) return false;
408
+ return true;
409
+ }
410
+ async function privateText(file, privateMode = true) {
411
+ try {
412
+ const handle = await open2(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
413
+ try {
414
+ const stat = await handle.stat();
415
+ if (!stat.isFile() || stat.size > 1024 * 1024 || privateMode && process.platform !== "win32" && stat.mode & 63 || process.getuid && stat.uid !== process.getuid()) return null;
416
+ return await handle.readFile("utf8");
417
+ } finally {
418
+ await handle.close();
419
+ }
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+ async function privateJson(file) {
425
+ try {
426
+ const text3 = await privateText(file);
427
+ return text3 ? JSON.parse(text3) : null;
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+ function oauthBinding(creds, selection) {
433
+ const parts = creds.accessToken.split(":");
434
+ const subject = /^clerk_[^:\s]+:[^:\s]+:[^:\s]+$/.test(creds.accessToken) ? parts.slice(0, 2) : [createHash("sha256").update(creds.accessToken).digest("hex")];
435
+ return createHash("sha256").update(JSON.stringify([selection, creds.issuer, creds.resource, subject])).digest("hex");
436
+ }
437
+ function validImport(creds) {
438
+ if (!/^clerk_[^:\s]+:[^:\s]+:[^:\s]+$/.test(creds.accessToken ?? "")) return null;
439
+ return usableOAuth(creds) && !isExpired(creds) ? creds : null;
440
+ }
441
+ function hermesServerConfig(text3, server) {
442
+ try {
443
+ return JSON.parse(text3)?.mcp_servers?.[server] ?? null;
444
+ } catch {
445
+ }
446
+ let inside = false, selected = false;
447
+ let sectionIndent = -1, serverIndent = -1;
448
+ const entry = {};
449
+ for (const line of text3.split(/\r?\n/)) {
450
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
451
+ if (/\t/.test(line)) return null;
452
+ const match = /^( *)([^:]+):\s*(.*?)\s*$/.exec(line);
453
+ if (!match) {
454
+ if (inside) return null;
455
+ continue;
456
+ }
457
+ const indent = match[1].length, key = match[2], value = match[3];
458
+ if (!inside) {
459
+ if (key === "mcp_servers" && value === "") {
460
+ inside = true;
461
+ sectionIndent = indent;
462
+ }
463
+ continue;
464
+ }
465
+ if (indent <= sectionIndent) break;
466
+ if (serverIndent < 0) serverIndent = indent;
467
+ if (indent === serverIndent) {
468
+ selected = key === server || key === JSON.stringify(server) || key === `'${server}'`;
469
+ if (selected && value) return null;
470
+ continue;
471
+ }
472
+ if (!selected) continue;
473
+ if (!value || /^[&*!>|\[{]/.test(value)) return null;
474
+ if (Object.hasOwn(entry, key)) return null;
475
+ let scalar = value;
476
+ if (value.startsWith('"')) {
477
+ try {
478
+ scalar = JSON.parse(value);
479
+ } catch {
480
+ return null;
481
+ }
482
+ } else if (value.startsWith("'")) {
483
+ if (!value.endsWith("'")) return null;
484
+ scalar = value.slice(1, -1).replace(/''/g, "'");
485
+ } else scalar = value.replace(/\s+#.*$/, "");
486
+ entry[key] = scalar;
487
+ }
488
+ return Object.keys(entry).length ? entry : null;
489
+ }
490
+ async function readHermesOAuth(server) {
491
+ if (!/^[\w-]{1,128}$/.test(server)) return null;
492
+ const home = process.env.HERMES_HOME?.trim() || join6(homedir3(), ".hermes");
493
+ const text3 = await privateText(join6(home, "config.yaml"), false);
494
+ const config = text3 && hermesServerConfig(text3, server);
495
+ if (!config || config.url !== MCP_URL || config.command || config.headers || config.type && config.type !== "http" && config.type !== "streamable-http") return null;
496
+ const dir = join6(home, "mcp-tokens");
497
+ const [token, client, meta] = await Promise.all([privateJson(join6(dir, `${server}.json`)), privateJson(join6(dir, `${server}.client.json`)), privateJson(join6(dir, `${server}.meta.json`))]);
498
+ if (!token || !client || !meta || token.hermes_issuer !== MCP_HOME || meta.issuer !== MCP_HOME) return null;
499
+ if (typeof client.client_id !== "string" || !client.client_id || token.client_id && token.client_id !== client.client_id) return null;
500
+ if (typeof token.token_type !== "string" || token.token_type.toLowerCase() !== "bearer") return null;
501
+ for (const field of ["authorization_endpoint", "token_endpoint"]) {
502
+ try {
503
+ if (new URL(String(meta[field])).origin !== MCP_HOME) return null;
504
+ } catch {
505
+ return null;
506
+ }
507
+ }
508
+ const hasResource = Object.hasOwn(token, "resource");
509
+ if (hasResource && token.resource !== MCP_URL) return null;
510
+ if (typeof token.expires_at !== "number") return null;
511
+ const creds = validImport({ issuer: MCP_HOME, ...hasResource ? { resource: token.resource } : {}, origin: "hermes-mcp", accessToken: token.access_token, expiresAt: token.expires_at * 1e3, scope: token.scope });
512
+ if (creds) creds.binding = oauthBinding(creds, [
513
+ "hermes",
514
+ resolve2(home),
515
+ server,
516
+ config,
517
+ client,
518
+ meta,
519
+ token.organization_id,
520
+ token.org_id,
521
+ token.account_id,
522
+ token.user_id
523
+ ]);
524
+ return creds;
525
+ }
526
+ function configuredClaudeDir() {
527
+ const value = process.env.CLAUDE_CONFIG_DIR;
528
+ return value && value.trim() ? value : void 0;
529
+ }
530
+ function claudeKeychainSelection() {
531
+ const custom = process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL;
532
+ if (custom && !["https://beacon.claude-ai.staging.ant.dev", "https://claude.fedstart.com", "https://claude-staging.fedstart.com"].includes(custom.replace(/\/$/, ""))) return null;
533
+ const configured = (configuredClaudeDir() ?? join6(homedir3(), ".claude")).normalize("NFC");
534
+ const secure = process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
535
+ const home = (secure !== void 0 ? secure || join6(homedir3(), ".claude") : configured).normalize("NFC");
536
+ const isDefault = secure !== void 0 ? !secure : !configuredClaudeDir();
537
+ const suffix = isDefault ? "" : `-${createHash("sha256").update(secure !== void 0 ? secure.normalize("NFC") : configured).digest("hex").slice(0, 8)}`;
538
+ let account;
539
+ try {
540
+ account = process.env.USER || userInfo().username;
541
+ } catch {
542
+ account = "claude-code-user";
543
+ }
544
+ if (!/^[a-zA-Z0-9._-]+$/.test(account)) account = "claude-code-user";
545
+ return { service: `Claude Code${custom ? "-custom-oauth" : ""}-credentials${suffix}`, account, home };
546
+ }
547
+ async function claudeResolvedConfig(server) {
548
+ const configDir = (configuredClaudeDir() ?? join6(homedir3(), ".claude")).normalize("NFC");
549
+ let file = join6(configuredClaudeDir() || homedir3(), `.claude${process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL ? "-custom-oauth" : ""}.json`);
550
+ try {
551
+ await lstat(join6(configDir, ".config.json"));
552
+ file = join6(configDir, ".config.json");
553
+ } catch (e) {
554
+ if (e.code !== "ENOENT") return null;
555
+ }
556
+ const json = await privateJson(file);
557
+ if (!json) return null;
558
+ const object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
559
+ const readOptional = async (path) => {
560
+ try {
561
+ await lstat(path);
562
+ const text3 = await privateText(path, false);
563
+ if (text3 === null) return null;
564
+ const value = JSON.parse(text3);
565
+ return object2(value) ? value : null;
566
+ } catch (e) {
567
+ return e.code === "ENOENT" ? void 0 : null;
568
+ }
569
+ };
570
+ let cwd;
571
+ try {
572
+ cwd = await realpath(process.cwd());
573
+ } catch {
574
+ return null;
575
+ }
576
+ let workspace = cwd;
577
+ for (let dir = cwd; ; ) {
578
+ try {
579
+ await lstat(join6(dir, ".git"));
580
+ const { stdout } = await promisify(execFile)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { timeout: 3e3, maxBuffer: 8192 });
581
+ workspace = await realpath(stdout.trim());
582
+ break;
583
+ } catch (e) {
584
+ if (e.code !== "ENOENT") return null;
585
+ }
586
+ const parent = dirname2(dir);
587
+ if (parent === dir) break;
588
+ dir = parent;
589
+ }
590
+ const managedDir = process.platform === "darwin" ? "/Library/Application Support/ClaudeCode" : "/etc/claude-code";
591
+ for (const path of [join6(managedDir, "managed-mcp.json"), join6(managedDir, "managed-settings.json"), join6(configDir, "managed-mcp.json"), join6(configDir, "managed-settings.json")]) {
592
+ try {
593
+ await lstat(path);
594
+ return null;
595
+ } catch (e) {
596
+ if (e.code !== "ENOENT") return null;
597
+ }
598
+ }
599
+ const policyFields = ["managedMcpServers", "allowedMcpServers", "deniedMcpServers", "mcpServers", "env", "enabledSettingsSources", "allowManagedMcpServersOnly"];
600
+ if (["managedMcpServers", "allowedMcpServers", "deniedMcpServers"].some((k) => Object.hasOwn(json, k))) return null;
601
+ for (const path of [join6(configDir, "settings.json"), join6(workspace, ".claude/settings.json"), join6(workspace, ".claude/settings.local.json")]) {
602
+ const settings = await readOptional(path);
603
+ if (settings === null || settings && policyFields.some((k) => Object.hasOwn(settings, k))) return null;
604
+ }
605
+ const projects = json.projects;
606
+ if (projects !== void 0 && !object2(projects)) return null;
607
+ const local = object2(projects) ? projects[workspace] : void 0;
608
+ if (local !== void 0 && !object2(local)) return null;
609
+ const userServers = json.mcpServers;
610
+ const localServers = object2(local) ? local.mcpServers : void 0;
611
+ if (userServers !== void 0 && !object2(userServers) || localServers !== void 0 && !object2(localServers)) return null;
612
+ let config = object2(userServers) ? userServers[server] : void 0;
613
+ let scope = "user", selectedFile = file;
614
+ const hasLocal = object2(localServers) && Object.hasOwn(localServers, server);
615
+ let project, projectFile;
616
+ const dirs = [];
617
+ for (let dir = cwd; ; ) {
618
+ const parent = dirname2(dir);
619
+ if (parent === dir) break;
620
+ dirs.push(dir);
621
+ dir = parent;
622
+ }
623
+ for (const dir of dirs.reverse()) {
624
+ const path = join6(dir, ".mcp.json"), value = await readOptional(path);
625
+ if (value === null) return null;
626
+ if (!value) continue;
627
+ if (!object2(value.mcpServers)) return null;
628
+ if (Object.hasOwn(value.mcpServers, server)) {
629
+ project = value.mcpServers[server];
630
+ projectFile = path;
631
+ }
632
+ }
633
+ if (projectFile && !hasLocal) {
634
+ const settings = await readOptional(join6(configDir, "settings.json"));
635
+ if (settings === null || !object2(local) || local.hasTrustDialogAccepted !== true) return null;
636
+ const disabled = settings?.disabledMcpjsonServers;
637
+ const enabled = settings?.enabledMcpjsonServers;
638
+ if (disabled !== void 0 && (!Array.isArray(disabled) || disabled.some((v) => typeof v !== "string"))) return null;
639
+ if (enabled !== void 0 && (!Array.isArray(enabled) || enabled.some((v) => typeof v !== "string"))) return null;
640
+ if (Array.isArray(disabled) && disabled.includes(server)) return null;
641
+ if (settings?.enableAllProjectMcpServers !== true && !(Array.isArray(enabled) && enabled.includes(server))) return null;
642
+ for (const path of [join6(workspace, ".claude/settings.json"), join6(workspace, ".claude/settings.local.json")]) {
643
+ const extra = await readOptional(path);
644
+ if (extra === null || extra && ["env", "enabledMcpjsonServers", "disabledMcpjsonServers", "enableAllProjectMcpServers"].some((k) => Object.hasOwn(extra, k))) return null;
645
+ }
646
+ config = project;
647
+ scope = "project";
648
+ selectedFile = projectFile;
649
+ }
650
+ if (hasLocal) {
651
+ config = localServers[server];
652
+ scope = "local";
653
+ }
654
+ if (!object2(config) || config.url !== MCP_URL || config.command || config.type !== void 0 && config.type !== "http" && config.type !== "sse") return null;
655
+ if (config.headers !== void 0 && !object2(config.headers)) return null;
656
+ for (const [key, value] of Object.entries(config.headers ?? {})) {
657
+ if (typeof value !== "string" || /authorization|api[-_]?key|token|cookie/i.test(key) || /\$\{|[\r\n\0]/.test(value)) return null;
658
+ }
659
+ return { config, file: selectedFile, scope, cwd: workspace };
660
+ }
661
+ async function readClaudeKeychain(request) {
662
+ return new Promise((resolveResult) => {
663
+ const child = spawn2(
664
+ "/usr/bin/osascript",
665
+ ["-l", "JavaScript", join6(packageRoot(), "native/claude-keychain.js")],
666
+ { stdio: ["pipe", "ignore", "ignore", "pipe"], env: { PATH: "/usr/bin:/bin" } }
667
+ );
668
+ let text3 = "", failed = false;
669
+ const pipe = child.stdio[3];
670
+ if (!pipe || !("on" in pipe)) {
671
+ child.kill();
672
+ resolveResult({ status: "unavailable" });
673
+ return;
674
+ }
675
+ const timer = setTimeout(() => {
676
+ failed = true;
677
+ child.kill();
678
+ }, 5e3);
679
+ pipe.on("data", (data) => {
680
+ if (text3.length + data.length > 65536) {
681
+ failed = true;
682
+ child.kill();
683
+ } else text3 += data.toString("utf8");
684
+ });
685
+ child.once("error", () => {
686
+ failed = true;
687
+ });
688
+ child.stdin?.on("error", () => {
689
+ failed = true;
690
+ });
691
+ child.once("close", (code) => {
692
+ clearTimeout(timer);
693
+ if (failed || code !== 0) {
694
+ resolveResult({ status: "unavailable" });
695
+ return;
696
+ }
697
+ try {
698
+ const result = JSON.parse(text3);
699
+ if (!["ok", "absent", "absent-entry", "locked", "denied", "malformed", "unavailable"].includes(result.status)) throw new Error();
700
+ resolveResult(result);
701
+ } catch {
702
+ resolveResult({ status: "malformed" });
703
+ }
704
+ });
705
+ child.stdin?.end(JSON.stringify(request));
706
+ });
707
+ }
708
+ async function claudeVersion() {
709
+ try {
710
+ const { stdout } = await promisify(execFile)("claude", ["--version"], { timeout: 3e3, maxBuffer: 1024 });
711
+ const version = stdout.trim().replace(/ \(Claude Code\)$/, "");
712
+ return CLAUDE_OWNER_VERSIONS.includes(version) ? version : null;
713
+ } catch {
714
+ return null;
715
+ }
716
+ }
717
+ async function readClaudeOwner(server, options = {}) {
718
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(server)) return { status: "unsupported-config" };
719
+ const selection = claudeKeychainSelection(), resolved = await claudeResolvedConfig(server);
720
+ if (!selection || !resolved) return { status: "unsupported-config" };
721
+ const hash = createHash("sha256").update(JSON.stringify({ type: resolved.config.type, url: resolved.config.url, headers: resolved.config.headers || {} })).digest("hex").slice(0, 16);
722
+ const key = `${server}|${hash}`, explicitFile = process.env.CARRIER_MCP_OAUTH_FILE;
723
+ let entry;
724
+ let source;
725
+ if ((options.platform ?? process.platform) === "darwin" && !explicitFile) {
726
+ if (!CLAUDE_OWNER_VERSIONS.includes(await (options.version ?? claudeVersion)() ?? "")) return { status: "unsupported-version" };
727
+ const result = await (options.native ?? readClaudeKeychain)({ ...selection, key });
728
+ if (result.status === "ok") {
729
+ if (!result.entry) return { status: "malformed" };
730
+ entry = result.entry;
731
+ source = ["keychain", selection.service, selection.account, selection.home];
732
+ } else if (result.status !== "absent") return { status: result.status };
733
+ }
734
+ if (!entry) {
735
+ const file = explicitFile || join6(selection.home, ".credentials.json");
736
+ const json = await privateJson(file);
737
+ entry = json?.mcpOAuth?.[key];
738
+ source = ["file", resolve2(file)];
739
+ }
740
+ if (!entry || entry.serverName !== server || entry.serverUrl !== MCP_URL) return { status: "absent-entry" };
741
+ const discovery = entry.discoveryState;
742
+ const issuer = entry.issuer ?? discovery?.authorizationServerUrl;
743
+ if (entry.issuer !== void 0 && discovery?.authorizationServerUrl !== void 0 && entry.issuer !== discovery.authorizationServerUrl || entry.resource !== void 0 && entry.resource !== MCP_URL || discovery?.resourceMetadataUrl !== void 0 && discovery.resourceMetadataUrl !== `${MCP_HOME}/.well-known/oauth-protected-resource/mcp`) return { status: "malformed" };
744
+ const creds = validImport({ issuer, resource: MCP_URL, origin: "claude-mcp", accessToken: entry.accessToken, expiresAt: entry.expiresAt, scope: entry.scope });
745
+ if (!creds) return { status: "malformed" };
746
+ creds.binding = oauthBinding(creds, [
747
+ "claude",
748
+ source,
749
+ resolve2(resolved.file),
750
+ resolved.scope,
751
+ resolved.cwd,
752
+ server,
753
+ hash,
754
+ entry.clientId,
755
+ entry.organizationId,
756
+ entry.organization_id,
757
+ entry.orgId,
758
+ entry.accountId,
759
+ discovery?.authorizationServerUrl,
760
+ discovery?.resourceMetadataUrl
761
+ ]);
762
+ return { status: "ok", credentials: creds };
763
+ }
764
+ async function readClaudeOAuth(server) {
765
+ return (await readClaudeOwner(server)).credentials ?? null;
766
+ }
767
+ async function readMcpOAuth(selectedOwner) {
768
+ const server = process.env.CARRIER_MCP_OAUTH_SERVER?.trim() || "carrier";
769
+ const owner = process.env.CARRIER_MCP_OAUTH_OWNER?.trim();
770
+ if (owner && owner !== "hermes" && owner !== "claude") return null;
771
+ if (selectedOwner) {
772
+ if (owner && owner !== selectedOwner || selectedOwner === "hermes" && process.env.CARRIER_MCP_OAUTH_FILE) return null;
773
+ return selectedOwner === "hermes" ? readHermesOAuth(server) : readClaudeOAuth(server);
774
+ }
775
+ if (owner === "hermes") return readHermesOAuth(server);
776
+ if (owner === "claude" || process.env.CARRIER_MCP_OAUTH_FILE) return readClaudeOAuth(server);
777
+ const claude = await readClaudeOwner(server);
778
+ if (["locked", "denied", "malformed", "unavailable", "unsupported-version"].includes(claude.status)) return null;
779
+ const owners = [await readHermesOAuth(server), claude.credentials ?? null].filter((c) => c !== null);
780
+ return owners.length === 1 ? owners[0] : null;
321
781
  }
322
782
 
323
783
  // src/cli/lib/auth.ts
784
+ import { access, writeFile as writeFile3, rm as rm2 } from "fs/promises";
785
+ import { join as join7, resolve as resolve3 } from "path";
324
786
  var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
325
787
  var CLIENT_NAME = "Carrier CLI";
326
788
  var DEFAULT_SCOPE = "read write";
@@ -334,6 +796,11 @@ async function discover(base = MCP_HOME) {
334
796
  if (!meta.authorization_endpoint || !meta.token_endpoint) {
335
797
  throw new Error("OAuth discovery returned no authorization or token endpoint");
336
798
  }
799
+ if (meta.issuer !== MCP_HOME) throw new Error("OAuth issuer does not match Carrier");
800
+ for (const endpoint of [meta.authorization_endpoint, meta.token_endpoint, meta.registration_endpoint].filter(Boolean)) {
801
+ const url = new URL(endpoint);
802
+ if (url.origin !== MCP_HOME || url.username || url.password) throw new Error("OAuth endpoint does not match Carrier");
803
+ }
337
804
  return meta;
338
805
  }
339
806
  function base64url(buf) {
@@ -341,7 +808,7 @@ function base64url(buf) {
341
808
  }
342
809
  function pkcePair() {
343
810
  const verifier = base64url(randomBytes(32));
344
- const challenge = base64url(createHash("sha256").update(verifier).digest());
811
+ const challenge = base64url(createHash2("sha256").update(verifier).digest());
345
812
  return { verifier, challenge };
346
813
  }
347
814
  function sameState(a, b) {
@@ -368,8 +835,7 @@ async function registerClient(meta, redirectUri) {
368
835
  })
369
836
  });
370
837
  if (!res.ok) {
371
- const body = (await res.text().catch(() => "")).slice(0, 200);
372
- throw new Error(`Client registration failed (${res.status}). ${body}`);
838
+ throw new Error(`Client registration failed (${res.status}).`);
373
839
  }
374
840
  const json = await res.json();
375
841
  if (!json.client_id) throw new Error("Client registration returned no client_id");
@@ -400,8 +866,8 @@ async function startCallbackServer() {
400
866
  const state = url.searchParams.get("state") ?? "";
401
867
  if (err) {
402
868
  res.writeHead(400, { "Content-Type": "text/html" });
403
- res.end(page("Sign-in failed", err));
404
- rejectCode?.(new Error(`Authorization server returned "${err}"`));
869
+ res.end(page("Sign-in failed", "Authorization was rejected."));
870
+ rejectCode?.(new Error("Authorization server rejected sign-in."));
405
871
  return;
406
872
  }
407
873
  if (!code || !expected || !sameState(state, expected)) {
@@ -414,24 +880,24 @@ async function startCallbackServer() {
414
880
  res.end(page("You're signed in", "Close this tab and return to your terminal."));
415
881
  resolveCode?.(code);
416
882
  });
417
- let bound = false;
883
+ let bound2 = false;
418
884
  let lastErr;
419
885
  for (const port2 of CALLBACK_PORTS) {
420
886
  try {
421
- await new Promise((resolve3, reject) => {
887
+ await new Promise((resolve5, reject) => {
422
888
  server.once("error", reject);
423
889
  server.listen(port2, "127.0.0.1", () => {
424
890
  server.removeListener("error", reject);
425
- resolve3();
891
+ resolve5();
426
892
  });
427
893
  });
428
- bound = true;
894
+ bound2 = true;
429
895
  break;
430
896
  } catch (e) {
431
897
  lastErr = e;
432
898
  }
433
899
  }
434
- if (!bound) {
900
+ if (!bound2) {
435
901
  throw new Error(
436
902
  `Could not bind a local callback port (tried ${CALLBACK_PORTS.filter(Boolean).join(", ")}): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
437
903
  );
@@ -443,14 +909,14 @@ async function startCallbackServer() {
443
909
  // browser would hold the event loop open and the CLI would never exit.
444
910
  waitForCode: (expectedState, timeoutMs) => {
445
911
  expected = expectedState;
446
- return new Promise((resolve3, reject) => {
912
+ return new Promise((resolve5, reject) => {
447
913
  const timer = setTimeout(
448
914
  () => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
449
915
  timeoutMs
450
916
  );
451
917
  resolveCode = (code) => {
452
918
  clearTimeout(timer);
453
- resolve3(code);
919
+ resolve5(code);
454
920
  };
455
921
  rejectCode = (err) => {
456
922
  clearTimeout(timer);
@@ -475,14 +941,20 @@ async function postToken(meta, body) {
475
941
  });
476
942
  const json = await res.json().catch(() => ({}));
477
943
  if (!res.ok || json.error) {
478
- throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
944
+ throw new Error(`OAuth token exchange rejected (${res.status}).`);
479
945
  }
480
946
  if (!json.access_token) throw new Error("Token endpoint returned no access_token");
947
+ if (Object.hasOwn(json, "resource")) {
948
+ const resources = Array.isArray(json.resource) ? json.resource : [json.resource];
949
+ if (resources.length !== 1 || resources[0] !== MCP_URL) throw new Error("OAuth token resource does not match Carrier.");
950
+ json.resource = MCP_URL;
951
+ }
481
952
  return json;
482
953
  }
483
954
  function store(meta, token, clientId) {
484
955
  return {
485
956
  issuer: meta.issuer,
957
+ ...Object.hasOwn(token, "resource") ? { resource: token.resource } : {},
486
958
  accessToken: token.access_token,
487
959
  refreshToken: token.refresh_token,
488
960
  expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
@@ -492,65 +964,140 @@ function store(meta, token, clientId) {
492
964
  }
493
965
  async function login(opts) {
494
966
  const meta = await discover();
495
- const server = await startCallbackServer();
496
- try {
497
- const existing = await readCredentials();
498
- const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
499
- const { verifier, challenge } = pkcePair();
500
- const state = base64url(randomBytes(16));
501
- const authUrl = new URL(meta.authorization_endpoint);
502
- authUrl.searchParams.set("response_type", "code");
503
- authUrl.searchParams.set("client_id", clientId);
504
- authUrl.searchParams.set("redirect_uri", server.redirectUri);
505
- authUrl.searchParams.set("scope", pickScope(meta));
506
- authUrl.searchParams.set("state", state);
507
- authUrl.searchParams.set("code_challenge", challenge);
508
- authUrl.searchParams.set("code_challenge_method", "S256");
509
- const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
510
- pendingCode.catch(() => void 0);
511
- opts.onUrl?.(authUrl.toString());
512
- await opts.openBrowser(authUrl.toString());
513
- const code = await pendingCode;
514
- const token = await postToken(meta, {
515
- grant_type: "authorization_code",
516
- code,
517
- redirect_uri: server.redirectUri,
518
- client_id: clientId,
519
- code_verifier: verifier
520
- });
521
- await writeCredentials(store(meta, token, clientId));
522
- return { ok: true, message: "Signed in.", scope: token.scope };
523
- } finally {
524
- server.close();
525
- }
967
+ return withCredentialLock(async () => {
968
+ const server = await startCallbackServer();
969
+ try {
970
+ const existing = await readCredentials();
971
+ const clientId = existing?.clientId && existing.issuer === meta.issuer && existing.redirectUri === server.redirectUri ? existing.clientId : await registerClient(meta, server.redirectUri);
972
+ const { verifier, challenge } = pkcePair();
973
+ const state = base64url(randomBytes(16));
974
+ const authUrl = new URL(meta.authorization_endpoint);
975
+ authUrl.searchParams.set("response_type", "code");
976
+ authUrl.searchParams.set("resource", MCP_URL);
977
+ authUrl.searchParams.set("client_id", clientId);
978
+ authUrl.searchParams.set("redirect_uri", server.redirectUri);
979
+ authUrl.searchParams.set("scope", pickScope(meta));
980
+ authUrl.searchParams.set("state", state);
981
+ authUrl.searchParams.set("code_challenge", challenge);
982
+ authUrl.searchParams.set("code_challenge_method", "S256");
983
+ const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
984
+ pendingCode.catch(() => void 0);
985
+ opts.onUrl?.(authUrl.toString());
986
+ await opts.openBrowser(authUrl.toString());
987
+ const code = await pendingCode;
988
+ const token = await postToken(meta, {
989
+ grant_type: "authorization_code",
990
+ code,
991
+ redirect_uri: server.redirectUri,
992
+ client_id: clientId,
993
+ code_verifier: verifier,
994
+ resource: MCP_URL
995
+ });
996
+ const next = { ...store(meta, token, clientId), redirectUri: server.redirectUri };
997
+ if (!usableOAuth(next)) throw new Error("OAuth endpoint returned an unusable Carrier session.");
998
+ await writeCredentials(next);
999
+ await rm2(join7(carrierHome(), "mcp-oauth-disabled"), { force: true });
1000
+ return { ok: true, message: "Signed in.", scope: token.scope };
1001
+ } finally {
1002
+ server.close();
1003
+ }
1004
+ });
526
1005
  }
527
1006
  function pickScope(meta) {
528
1007
  const supported = meta.scopes_supported ?? [];
529
1008
  if (!supported.length) return DEFAULT_SCOPE;
530
1009
  return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
531
1010
  }
532
- async function getAccessToken() {
1011
+ function inheritedOAuth() {
1012
+ const api = process.env.CARRIER_API_KEY?.trim();
1013
+ if (!api || process.env.CARRIER_OAUTH_HANDOFF !== createHash2("sha256").update(api).digest("hex")) return null;
1014
+ const origin = process.env.CARRIER_OAUTH_ORIGIN, binding = process.env.CARRIER_OAUTH_BINDING;
1015
+ if (!origin || !["native-cli", "hermes-mcp", "claude-mcp"].includes(origin) || !/^[a-f0-9]{64}$/.test(binding ?? "")) return false;
1016
+ return { origin, binding };
1017
+ }
1018
+ function nativeBinding(creds) {
1019
+ return oauthBinding(creds, ["native-cli", resolve3(carrierHome()), creds.clientId]);
1020
+ }
1021
+ async function selectedMcpOAuth(handoff) {
1022
+ if (handoff === false || handoff && handoff.origin === "native-cli") return null;
1023
+ const owner = await readMcpOAuth(handoff ? handoff.origin === "hermes-mcp" ? "hermes" : "claude" : void 0);
1024
+ return owner && (!handoff || owner.binding === handoff.binding) ? owner : null;
1025
+ }
1026
+ async function getAccessTokenLocked() {
1027
+ const handoff = inheritedOAuth();
1028
+ if (handoff === false) return null;
533
1029
  const creds = await readCredentials();
534
- if (!creds) return null;
1030
+ if (creds && handoff && (handoff.origin !== "native-cli" || nativeBinding(creds) !== handoff.binding)) return null;
1031
+ if (!creds) {
1032
+ try {
1033
+ await access(join7(carrierHome(), "credentials.json"));
1034
+ return null;
1035
+ } catch (error) {
1036
+ if (error.code !== "ENOENT") return null;
1037
+ }
1038
+ try {
1039
+ await access(join7(carrierHome(), "mcp-oauth-disabled"));
1040
+ return null;
1041
+ } catch (error) {
1042
+ if (error.code !== "ENOENT") return null;
1043
+ }
1044
+ return (await selectedMcpOAuth(handoff))?.accessToken ?? null;
1045
+ }
1046
+ if (!usableOAuth(creds)) return null;
535
1047
  if (!isExpired(creds)) return creds.accessToken;
536
1048
  if (!creds.refreshToken || !creds.clientId) return null;
537
1049
  try {
538
1050
  const meta = await discover();
1051
+ if (meta.issuer !== creds.issuer) return null;
539
1052
  const token = await postToken(meta, {
540
1053
  grant_type: "refresh_token",
1054
+ resource: MCP_URL,
541
1055
  refresh_token: creds.refreshToken,
542
1056
  client_id: creds.clientId
543
1057
  });
544
1058
  const next = store(meta, token, creds.clientId);
545
1059
  if (!next.refreshToken) next.refreshToken = creds.refreshToken;
1060
+ if (!next.scope) next.scope = creds.scope;
1061
+ if (!Object.hasOwn(next, "resource") && Object.hasOwn(creds, "resource")) next.resource = creds.resource;
1062
+ if (!usableOAuth(next) || handoff && nativeBinding(next) !== handoff.binding) return null;
1063
+ next.redirectUri = creds.redirectUri;
1064
+ const latest = await readCredentials();
1065
+ if (latest?.generation !== creds.generation || latest?.accessToken !== creds.accessToken) return null;
546
1066
  await writeCredentials(next);
547
1067
  return next.accessToken;
548
1068
  } catch {
549
1069
  return null;
550
1070
  }
551
1071
  }
1072
+ function resolveExplicitCredential(opts = {}) {
1073
+ const api = process.env.CARRIER_API_KEY?.trim();
1074
+ const handedOff = api && process.env.CARRIER_OAUTH_HANDOFF === createHash2("sha256").update(api).digest("hex");
1075
+ if (api && !handedOff) return { token: api, kind: "api-key" };
1076
+ const org = process.env.CARRIER_ORG_API_KEY?.trim();
1077
+ if (org) return { token: org, kind: "api-key" };
1078
+ if (opts.allowOcs) {
1079
+ const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim();
1080
+ if (ocs) return { token: ocs, kind: "ocs-token" };
1081
+ }
1082
+ return null;
1083
+ }
1084
+ async function resolveCredential(opts = {}) {
1085
+ const explicit = resolveExplicitCredential(opts);
1086
+ if (explicit) return explicit;
1087
+ return withCredentialLock(async () => {
1088
+ const token = await getAccessTokenLocked();
1089
+ if (!token) return null;
1090
+ const native = await readCredentials();
1091
+ const owner = native ?? await selectedMcpOAuth(inheritedOAuth());
1092
+ if (!owner || owner.accessToken !== token) return null;
1093
+ return { token, kind: "oauth", origin: native ? "native-cli" : owner.origin, expiresAt: owner.expiresAt, binding: native ? nativeBinding(native) : owner.binding };
1094
+ });
1095
+ }
552
1096
  async function logout() {
553
- await clearCredentials();
1097
+ await withCredentialLock(async () => {
1098
+ await writeFile3(join7(carrierHome(), "mcp-oauth-disabled"), "disabled\n", { mode: 384 });
1099
+ await clearCredentials();
1100
+ });
554
1101
  }
555
1102
 
556
1103
  // src/cli/lib/guidance.ts
@@ -572,13 +1119,15 @@ function oauthFirstUseNote() {
572
1119
  " Token: none required in Claude / Cursor / Windsurf",
573
1120
  "",
574
1121
  "When you first talk to Carrier in your MCP client, the browser opens",
575
- "sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
576
- `(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
1122
+ "sign-in/sign-up. After authorize, tools work. Headless: carrier login,",
1123
+ "or reuse compatible Claude Code Carrier OAuth. No manual key export needed.",
1124
+ "To export CARRIER_API_KEY into a child terminal: carrier exec -- bash.",
1125
+ `For unattended CI use an org key from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
577
1126
  ].join("\n");
578
1127
  }
579
1128
  function formatStatusBlock(st) {
580
1129
  const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
581
- 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");
1130
+ const authLabel = st.authMode === "oauth" ? pc.green("OAuth session ready for CLI / Ask the fleet") : 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");
582
1131
  return [
583
1132
  `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
584
1133
  `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
@@ -607,228 +1156,286 @@ function withNextStep(errorMsg, next) {
607
1156
  return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
608
1157
  }
609
1158
 
610
- // src/cli/lib/status.ts
611
- import { homedir as homedir3 } from "os";
612
- import { join as join6 } from "path";
613
-
614
- // src/cli/lib/mcp-url.ts
615
- var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
616
- var PRODUCTION_HOST = "mcp.carrier.llc";
617
- var URL_TOKEN = /https?:\/\/[^\s"'<>)]+/g;
618
- function pathWithoutTrailingSlash(pathname) {
619
- let end = pathname.length;
620
- while (end > 1 && pathname.charCodeAt(end - 1) === 47) end -= 1;
621
- return pathname.slice(0, end);
622
- }
623
- function parseHttpUrl(value) {
624
- try {
625
- const parsed = new URL(value);
626
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
627
- return parsed;
628
- } catch {
629
- return null;
630
- }
631
- }
632
- function isProductionMcpUrl(value) {
633
- if (value === PRODUCTION_MCP_URL) return true;
634
- const parsed = parseHttpUrl(value);
635
- if (!parsed || parsed.protocol !== "https:" || parsed.hostname !== PRODUCTION_HOST) return false;
636
- return pathWithoutTrailingSlash(parsed.pathname) === "/mcp";
637
- }
638
- function isCarrierMcpHost(value) {
639
- const parsed = parseHttpUrl(value);
640
- return parsed?.protocol === "https:" && parsed.hostname === PRODUCTION_HOST;
641
- }
642
- function trimTrailingPunctuation(token) {
643
- let end = token.length;
644
- while (end > 0) {
645
- const code = token.charCodeAt(end - 1);
646
- if (code !== 46 && code !== 44 && code !== 59 && code !== 58) break;
647
- end -= 1;
648
- }
649
- return token.slice(0, end);
650
- }
651
- function lineMentionsProductionMcp(line) {
652
- const tokens = line.match(URL_TOKEN);
653
- if (!tokens) return false;
654
- for (const token of tokens) {
655
- if (isProductionMcpUrl(trimTrailingPunctuation(token))) return true;
656
- }
1159
+ // src/cli/lib/ask-answer.ts
1160
+ var MAX_CHARS = 3800;
1161
+ var MAX_ROWS = 10;
1162
+ var RAW_HINT = 'Use carrier ask "your question" --json for the complete raw result (may contain sensitive data).';
1163
+ function object(value) {
1164
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1165
+ }
1166
+ function measured(value) {
1167
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
1168
+ }
1169
+ function bound(text3, prose = false) {
1170
+ text3 = sanitizeAskText(text3);
1171
+ if (text3.length <= MAX_CHARS) return text3;
1172
+ const notice = `Answer shortened; complete rows or sentences omitted. ${RAW_HINT}`;
1173
+ const budget = MAX_CHARS - notice.length - 1;
1174
+ const units = prose ? text3.match(/[^\n]+?(?:[.!?](?=\s|$)|(?=\n|$))/g) ?? [] : text3.split("\n");
1175
+ const kept = [];
1176
+ let size = 0;
1177
+ for (const unit of units) {
1178
+ const clean = unit.trim();
1179
+ if (size + clean.length + 1 > budget) break;
1180
+ kept.push(clean);
1181
+ size += clean.length + 1;
1182
+ }
1183
+ return [...kept, notice].join("\n");
1184
+ }
1185
+ function safeFacet(key, value) {
1186
+ if (key === "recurring") return value === "true" || value === "false";
1187
+ if (key === "status") return ["active", "inactive", "inventory", "manually suspended", "end of life", "free", "expired", "terminated"].includes(value.toLowerCase());
657
1188
  return false;
658
1189
  }
659
-
660
- // src/cli/lib/status.ts
661
- function detectEnvToken() {
662
- const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
663
- if (api.startsWith("ak_") || api.length > 0) {
664
- return { present: true, kind: "api-key" };
665
- }
666
- const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim() || "";
667
- if (ocs) return { present: true, kind: "ocs-token" };
668
- return { present: false, kind: "none" };
669
- }
670
- function rankHit(name, url) {
671
- let rank = 0;
672
- if (name.toLowerCase() === MCP_NAME) rank += 100;
673
- else if (/^carrier[-_]?/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 40;
674
- else if (/carrier/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 20;
675
- else if (/carrier/i.test(name)) rank += 5;
676
- if (isProductionMcpUrl(url)) rank += 50;
677
- else if (isCarrierMcpHost(url)) rank += 25;
678
- return rank;
679
- }
680
- function scanMcpServers(servers) {
681
- if (!servers || typeof servers !== "object") return null;
682
- let best = null;
683
- for (const [name, entry] of Object.entries(servers)) {
684
- if (!/carrier/i.test(name) && !(typeof entry?.url === "string" && /carrier\.llc/i.test(entry.url))) {
685
- continue;
686
- }
687
- const url = typeof entry?.url === "string" ? entry.url : "";
688
- const rank = rankHit(name, url);
689
- if (rank < 20 && !isCarrierMcpHost(url)) {
690
- continue;
691
- }
692
- const urlMatches = isProductionMcpUrl(url);
693
- const hit = {
694
- registered: true,
695
- urlMatches,
696
- detail: url ? `${name}: ${url}` : `${name}: ${entry?.command ?? "configured"}`,
697
- rank
698
- };
699
- if (!best || hit.rank > best.rank) best = hit;
700
- }
701
- return best;
702
- }
703
- async function probeMcpFromConfig() {
704
- const candidates = [
705
- join6(homedir3(), ".claude.json"),
706
- join6(claudeHome(), "settings.json"),
707
- join6(claudeHome(), ".mcp.json"),
708
- join6(process.cwd(), ".mcp.json")
709
- ];
710
- let best = null;
711
- let configSeen = false;
712
- for (const file of candidates) {
713
- if (!await exists(file)) continue;
714
- try {
715
- const json = JSON.parse(await readFile(file, "utf8"));
716
- configSeen = true;
717
- const direct = scanMcpServers(json.mcpServers);
718
- if (direct && (!best || direct.rank > best.rank)) best = direct;
719
- if (json.projects) {
720
- for (const proj of Object.values(json.projects)) {
721
- const hit = scanMcpServers(proj?.mcpServers);
722
- if (hit && (!best || hit.rank > best.rank)) best = hit;
723
- }
724
- }
725
- } catch {
726
- }
727
- }
728
- if (!best) return { hit: null, configSeen };
729
- return {
730
- hit: { registered: best.registered, urlMatches: best.urlMatches, detail: best.detail },
731
- configSeen
732
- };
733
- }
734
- async function probeMcpRegistration() {
735
- const { hit, configSeen } = await probeMcpFromConfig();
736
- if (hit) return hit;
737
- if (configSeen) {
738
- return {
739
- registered: false,
740
- urlMatches: false,
741
- detail: "carrier not registered (no production MCP URL in Claude config)"
742
- };
743
- }
744
- const claudeFound = await which("claude");
745
- if (!claudeFound) {
746
- return {
747
- registered: false,
748
- urlMatches: false,
749
- detail: "claude CLI not on PATH"
750
- };
751
- }
752
- const r = await run("claude", ["mcp", "list"], { timeoutMs: 5e3 });
753
- const out = `${r.stdout}
754
- ${r.stderr}`;
755
- if (!r.ok && !out.trim()) {
756
- return { registered: false, urlMatches: false, detail: "claude mcp list failed or timed out" };
757
- }
758
- if (/timeout after/i.test(r.stderr) && !/carrier/i.test(out)) {
759
- return {
760
- registered: false,
761
- urlMatches: false,
762
- detail: "claude mcp list timed out \u2014 run `claude mcp list` manually"
763
- };
764
- }
765
- const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
766
- 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));
767
- if (!carrierLine) {
768
- return { registered: false, urlMatches: false, detail: "carrier not in claude mcp list" };
769
- }
770
- const urlMatches = lineMentionsProductionMcp(carrierLine);
771
- return { registered: true, urlMatches, detail: carrierLine.slice(0, 200) };
772
- }
773
- function buildNextSteps(s) {
774
- const steps = [];
775
- if (!s.claudeFound) {
776
- steps.push("Install Claude Code (https://claude.ai/code), then re-run: carrier plugin install");
777
- } else if (!s.pluginInstalled) {
778
- steps.push("Install the Carrier plugin: carrier plugin install");
779
- } else if (!s.mcpRegistered) {
780
- steps.push(`Register the zero-cred MCP URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
781
- } else if (!s.mcpUrlMatches) {
782
- steps.push(`Update MCP to production URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
783
- }
784
- if (s.mcpRegistered || s.pluginInstalled) {
785
- steps.push(
786
- "Talk to your fleet in Claude \u2014 first tool call opens Clerk OAuth (Google / GitHub / email). No API token needed."
787
- );
788
- } else {
789
- steps.push("Create a free account (browser): carrier open signup");
790
- steps.push("Or sign in: carrier open signin");
791
- }
792
- if (!s.tokenPresent) {
793
- steps.push(
794
- "Optional headless key: Console \u2192 Settings \u2192 API Keys (ak_\u2026) then export CARRIER_API_KEY=ak_\u2026 for `carrier ask`."
795
- );
796
- } else {
797
- steps.push('Try a natural-language fleet query: carrier ask "show fleet health"');
798
- }
799
- steps.push("Scaffold a white-label storefront anytime: carrier site create");
800
- return steps;
801
- }
802
- async function gatherStatus() {
803
- const [plugin2, mcp, claudeFound] = await Promise.all([
804
- pluginStatus(),
805
- probeMcpRegistration(),
806
- which("claude")
807
- ]);
808
- const token = detectEnvToken();
809
- let authMode = "none";
810
- if (token.kind === "api-key") authMode = "api-key";
811
- else if (token.kind === "ocs-token") authMode = "ocs-token";
812
- else if (mcp.registered || plugin2.installed) authMode = "oauth-first-use";
813
- const base = {
814
- pluginInstalled: plugin2.installed,
815
- pluginVersion: plugin2.version,
816
- pluginDir: plugin2.dir,
817
- claudeFound,
818
- mcpRegistered: mcp.registered,
819
- mcpUrlMatches: mcp.urlMatches,
820
- mcpDetail: mcp.detail,
821
- tokenPresent: token.present,
822
- tokenKind: token.kind,
823
- authMode
824
- };
825
- return { ...base, nextSteps: buildNextSteps(base) };
826
- }
827
- function resolveCliToken() {
828
- const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
829
- if (api) return api;
830
- const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim() || "";
831
- return ocs || null;
1190
+ function unsupportedJudgement(intent) {
1191
+ return /\b(predict\w*|most likely|least likely|likelihood|probability|forecast\w*|voorspel\w*|waarschijnlijk)\b/i.test(intent) || /\b(who|which|what|wie|welke)\b.*\b(best|worst|beste|slechtste)\b(?=\s*(?:[?!.]|$))/i.test(intent) || /\b(who|which|what|wie|welke)\s+(?:(?:is|are|has|the|subscriber|package|action)\s+)*(best|worst|beste|slechtste)\b/i.test(intent) || /\b(?:recommend\w*|advise)\b/i.test(intent) || /^\s*should\b/i.test(intent) || /\b(will|going to)\b.*\b(cancel|churn|leave|fail)\b/i.test(intent);
1192
+ }
1193
+ function subscriberLine(value) {
1194
+ const row = object(value);
1195
+ if (!row) return void 0;
1196
+ const status = extractSubscriberStatus(row);
1197
+ const sim = object(row.sim);
1198
+ const id = typeof row.subscriberId === "string" || typeof row.subscriberId === "number" ? row.subscriberId : "(identifier not reported)";
1199
+ return `Subscriber ${inline(id)}. Subscriber status: ${inline(status || "not reported")}. SIM inventory status: ${typeof sim?.status === "string" ? inline(sim.status) : "not reported"}.`;
1200
+ }
1201
+ function subscriberStatusAnswer(rows2, summary, intent) {
1202
+ const identities = [...intent.matchAll(/\b(subscriberId|subscriber\s+id|subscriber|ICCID)(?:\s*[:=]\s*|\s+)(?:"([^"\r\n]+)"|'([^'\r\n]+)'|([^\s"'?!,;]+))/gi)].map((m) => {
1203
+ if (m[4]) m[4] = m[4].replace(/[.:)]+$/, "");
1204
+ return m;
1205
+ }).filter((m) => !/^(?:status|statuses|current|is|are|the|list|count|in|for|of|from|their|my|our|all|each|every|now|today|currently|and|or)$/i.test(m[4] ?? ""));
1206
+ if (identities.length) {
1207
+ const tail = intent.slice((identities[0].index ?? 0) + identities[0][0].length);
1208
+ const extraIdentity = /\b(?:and|or)\b/i.test(tail) && !/^\s*[.:)]*\s+and\s+(?:(?:its|their|the)\s+)?(?:ICCID|subscriberId|subscriber\s+id|(?:current\s+)?status)\s*[.?!]*$/i.test(tail);
1209
+ if (identities.length !== 1 || /\b(named|with|whose|and|or)\b/i.test(identities[0][4] ?? "") || extraIdentity) {
1210
+ return "Carrier cannot identify a single subscriber from this unclear request. Specify one exact subscriberId or ICCID; no subscriber was selected.";
1211
+ }
1212
+ const match = identities[0];
1213
+ const id = match[2] ?? match[3] ?? match[4];
1214
+ if (!rows2) return "Subscriber row details were not returned; Carrier cannot determine the requested current status.";
1215
+ const selected = rows2.filter((value) => {
1216
+ const row = object(value);
1217
+ if (!row) return false;
1218
+ return match[1].toLowerCase() === "iccid" ? subscriberIccid(row) === id : (typeof row.subscriberId === "string" || typeof row.subscriberId === "number" && Number.isFinite(row.subscriberId)) && String(row.subscriberId) === id;
1219
+ });
1220
+ if (!selected.length) return "The requested identifier is not present in the returned rows. This does not establish fleet-wide absence.";
1221
+ if (selected.length !== 1) return "Carrier cannot select a single subscriber: that identifier is ambiguous in the returned rows.";
1222
+ return bound(`Current status from the returned row:
1223
+ ${subscriberLine(selected[0])}`);
1224
+ }
1225
+ const fieldList = /^\s*(?:list|show)\b/i.test(intent) && /\b(?:all|each|every)\s+subscribers\b/i.test(intent);
1226
+ if (!fieldList && /\b(subscriberId|subscriber\s+id|ICCID)\b|\b(?:of|for)\s+(?:the\s+)?subscriber\b/i.test(intent)) return "Carrier cannot identify a single subscriber from this unclear request. Specify one exact subscriberId or ICCID; no subscriber was selected.";
1227
+ if (!rows2) return "Subscriber row details were not returned; current statuses are unavailable.";
1228
+ const lines = ["Current subscriber statuses from the returned rows only:"];
1229
+ if (/\b(how many|count|number|total|hoeveel)\b/i.test(intent)) {
1230
+ lines.push(`Carrier reports ${summary.total} subscribers in this result.`);
1231
+ if (rows2.length < summary.total) lines.push(`Only ${rows2.length} of the reported ${summary.total} rows are present in the response; the remaining details are unavailable.`);
1232
+ }
1233
+ if (rows2.length === 0) lines.push("No subscriber rows were returned; no current statuses can be determined.");
1234
+ for (const row of rows2.slice(0, MAX_ROWS)) {
1235
+ const line = subscriberLine(row);
1236
+ if (line) lines.push(line);
1237
+ else lines.push("A returned row has an unsupported shape; its current status is unavailable.");
1238
+ }
1239
+ if (rows2.length < summary.total || summary.moreAvailable === true) lines.push("Current statuses for remaining rows are unavailable; these returned statuses are not a fleet-wide status breakdown.");
1240
+ if (rows2.length > MAX_ROWS) lines.push(`Showing ${MAX_ROWS} of ${rows2.length} returned rows. ${RAW_HINT}`);
1241
+ return bound(lines.join("\n"));
1242
+ }
1243
+ function formatAskAnswer(text3, tool, structured, intent = "") {
1244
+ const countOnly = /\b(how many|count|number of|hoeveel|aantal)\b/i.test(intent) && !/\b(status|list|details?|which|their|lijst)\b/i.test(intent);
1245
+ if (unsupportedJudgement(intent) && !INTELLIGENCE_TOOLS.has(tool)) return `Carrier cannot determine or predict that judgement from this tool result. No such conclusion is supported by the returned records. ${RAW_HINT}`;
1246
+ let value = structured;
1247
+ try {
1248
+ const parsed = JSON.parse(text3);
1249
+ if (structured === void 0) value = parsed;
1250
+ } catch {
1251
+ if (text3.trim() && !/^[\[{]/.test(text3.trim())) return bound(text3, true);
1252
+ if (structured === void 0) return `The tool result could not be read as complete JSON. ${RAW_HINT}`;
1253
+ }
1254
+ const fallback = object(value);
1255
+ if (structured === void 0 && Array.isArray(fallback?.content) && fallback.content.length === 0) {
1256
+ return "No result data was returned.";
1257
+ }
1258
+ const body = object(value);
1259
+ if (tool === "get_subscriber" && /\b(status|statuses)\b/i.test(intent)) {
1260
+ const row = object(body?.subscriber) ?? object(object(body?.data)?.subscriber) ?? object(body?.data) ?? body;
1261
+ if (row && (Object.hasOwn(row, "subscriberId") || Object.hasOwn(row, "status") || Object.hasOwn(row, "sim"))) {
1262
+ return subscriberStatusAnswer([row], { total: 1 }, intent);
1263
+ }
1264
+ }
1265
+ const summary = object(body?.summary);
1266
+ if (measured(summary?.total)) {
1267
+ const data = object(body?.data) ?? body;
1268
+ const collections = [["subscriberList", "subscribers"], ["template", "package templates"], ["prepaidPackageTemplate", "package templates"], ["reseller", "resellers"], ...OCS_LIST_KEYS.packages.map((key) => [key, "packages"]), ["zone", "zones"]];
1269
+ const found = collections.find(([key]) => Array.isArray(data?.[key]));
1270
+ const noun = found?.[1] ?? (tool === "list_subscribers" ? "subscribers" : tool === "list_package_templates" ? "package templates" : tool === "list_subscriber_packages" ? "packages" : "items");
1271
+ const rows2 = found ? data?.[found[0]] : void 0;
1272
+ if (noun === "subscribers" && !countOnly && /\b(status|statuses)\b/i.test(intent)) return subscriberStatusAnswer(rows2, summary, intent);
1273
+ const lines = [`Carrier reports ${summary.total} ${noun} in this result.`];
1274
+ for (const [key, count] of Object.entries(object(summary.nested) ?? {})) {
1275
+ if (key === "accounts" && measured(count)) lines.push(`${count} accounts reported inside the result.`);
1276
+ }
1277
+ if (summary.moreAvailable === true) lines.push("More results are available; this is not a complete fleet total.");
1278
+ if (measured(summary.upstreamTotal)) lines.push(`Upstream reports ${summary.upstreamTotal} matches before paging.`);
1279
+ for (const [key, value2] of Object.entries(object(summary.breakdown) ?? {})) {
1280
+ const counts = object(object(value2)?.counts);
1281
+ const entries = Object.entries(counts ?? {}).filter(([name, n]) => measured(n) && safeFacet(key, name));
1282
+ if (entries.length) lines.push(`${label(key)} (server-reported): ${entries.map(([k, n]) => `${k}: ${n}`).join(", ")}.`);
1283
+ }
1284
+ if (rows2) {
1285
+ if (rows2.length < summary.total) lines.push(`Only ${rows2.length} of the reported ${summary.total} rows are present in the response; the remaining details are unavailable.`);
1286
+ if (countOnly) return bound(lines.join("\n"));
1287
+ if (noun === "subscribers") {
1288
+ for (const value2 of rows2.slice(0, MAX_ROWS)) {
1289
+ const line = subscriberLine(value2);
1290
+ if (line) lines.push(line);
1291
+ }
1292
+ } else if (found?.[0] === "reseller") {
1293
+ const accounts = projectResellerAccounts(rows2);
1294
+ lines.push(...facts(accounts.length ? accounts : rows2));
1295
+ } else if (found?.[0] === "template" || found?.[0] === "prepaidPackageTemplate") {
1296
+ lines.push(...facts(projectPackageTemplates(rows2) ?? rows2));
1297
+ } else lines.push(...facts(rows2));
1298
+ if (rows2.length > MAX_ROWS && noun === "subscribers") lines.push(`Showing ${MAX_ROWS} of ${rows2.length} returned rows. ${RAW_HINT}`);
1299
+ } else lines.push("Row details were not returned.");
1300
+ return bound(lines.join("\n"));
1301
+ }
1302
+ if (value === null) return "No result data was returned.";
1303
+ if (typeof value !== "object") return bound(`Carrier returned: ${String(value)}`);
1304
+ if (tool === "fleet_health_app" && body) {
1305
+ return bound(["Carrier returned these fleet facts:", ...recordFacts(body, FLEET_FIELDS, false)].join("\n"));
1306
+ }
1307
+ return bound(["Carrier returned these facts (not an inferred answer):", ...facts(value)].join("\n"));
1308
+ }
1309
+ var UNSUPPORTED = `Carrier cannot safely interpret unsupported result fields or shapes. ${RAW_HINT}`;
1310
+ var INTELLIGENCE_TOOLS = /* @__PURE__ */ new Set(["optimize_package", "churn_risk", "fleet_health", "fleet_health_app"]);
1311
+ var FLEET_FIELDS = {
1312
+ utilization: "number",
1313
+ totalActive: "number",
1314
+ totalSuspended: "number",
1315
+ totalInventory: "number",
1316
+ totalOther: "number",
1317
+ totalAccounts: "number",
1318
+ lowBalanceCount: "number"
1319
+ };
1320
+ var FIELD_TYPES = {
1321
+ name: "string",
1322
+ description: "string",
1323
+ detail: "string",
1324
+ currency: "string",
1325
+ status: "string",
1326
+ balance: "money",
1327
+ amount: "money",
1328
+ price: "money",
1329
+ dataLimit: "number",
1330
+ dataUsed: "number",
1331
+ expirationDate: "string",
1332
+ startDate: "string",
1333
+ endDate: "string",
1334
+ useddatabyte: "number",
1335
+ pckdatabyte: "number",
1336
+ tsexpirationutc: "string",
1337
+ recurring: "boolean",
1338
+ active: "boolean",
1339
+ enabled: "boolean",
1340
+ id: "scalar",
1341
+ accountId: "scalar",
1342
+ resellerId: "scalar",
1343
+ templateId: "scalar",
1344
+ packageId: "scalar",
1345
+ zoneId: "scalar",
1346
+ subscriberId: "scalar",
1347
+ // Live listPrepaidPackageTemplate rows use these wire names, not templateId.
1348
+ prepaidpackagetemplateid: "scalar",
1349
+ prepaidpackagetemplatename: "string",
1350
+ resellerid: "scalar",
1351
+ locationzoneid: "scalar",
1352
+ // listResellerAccount account rows. packageOnly is a boolean on the live shape.
1353
+ packageOnly: "boolean"
1354
+ };
1355
+ function inline(value) {
1356
+ return sanitizeAskText(String(value)).replace(/[\r\n\t]+/g, " ");
1357
+ }
1358
+ function sanitizeAskText(text3) {
1359
+ return text3.replace(/(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)/g, "").replace(/(?:\x1b[P^_X]|[\x90\x98\x9e\x9f])[\s\S]*?(?:\x1b\\|\x9c|$)/g, "").replace(/(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]/g, "").replace(/\x1b[ -/]*[@-~]/g, "").replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
1360
+ }
1361
+ function label(key) {
1362
+ const words = key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ");
1363
+ return words.charAt(0).toUpperCase() + words.slice(1);
1364
+ }
1365
+ function recordFacts(record2, fields = FIELD_TYPES, warnUnknown = true) {
1366
+ const lines = [];
1367
+ let unsupported = false;
1368
+ for (const [key, value] of Object.entries(record2)) {
1369
+ const expected = Object.hasOwn(fields, key) ? fields[key] : void 0;
1370
+ const valid = expected === "scalar" ? typeof value === "string" || typeof value === "number" && Number.isFinite(value) : expected === "money" ? typeof value === "number" && Number.isFinite(value) || typeof value === "string" && /^[+-]?\d+(?:\.\d+)?$/.test(value) && Number.isFinite(Number(value)) : typeof value === expected && (expected !== "number" || Number.isFinite(value));
1371
+ if (!expected || !valid) {
1372
+ if (warnUnknown || expected) unsupported = true;
1373
+ continue;
1374
+ }
1375
+ lines.push(`${label(key)}: ${typeof value === "boolean" ? String(value) : inline(value)}`);
1376
+ }
1377
+ if (unsupported) lines.push(UNSUPPORTED);
1378
+ return lines.length ? lines : ["No result fields were returned."];
1379
+ }
1380
+ var TEMPLATE_WIRE_FIELDS = ["prepaidpackagetemplateid", "prepaidpackagetemplatename", "resellerid", "locationzoneid"];
1381
+ var ACCOUNT_WIRE_FIELDS = ["id", "name", "balance", "packageOnly"];
1382
+ function projectResellerAccounts(rows2) {
1383
+ const accounts = [];
1384
+ for (const row of rows2) {
1385
+ const reseller = object(row);
1386
+ if (!reseller || !Array.isArray(reseller.account)) continue;
1387
+ for (const item of reseller.account) {
1388
+ const account = object(item);
1389
+ if (!account) continue;
1390
+ const projected = {};
1391
+ if (typeof reseller.id === "number" && Number.isFinite(reseller.id)) projected.resellerId = reseller.id;
1392
+ for (const key of ACCOUNT_WIRE_FIELDS) {
1393
+ if (Object.hasOwn(account, key)) projected[key] = account[key];
1394
+ }
1395
+ accounts.push(projected);
1396
+ }
1397
+ }
1398
+ return accounts;
1399
+ }
1400
+ function projectPackageTemplates(rows2) {
1401
+ const projected = [];
1402
+ for (const row of rows2) {
1403
+ const record2 = object(row);
1404
+ if (!record2) return void 0;
1405
+ const wire = Object.hasOwn(record2, "prepaidpackagetemplateid") || Object.hasOwn(record2, "prepaidpackagetemplatename");
1406
+ if (!wire) return void 0;
1407
+ const next = {};
1408
+ for (const key of TEMPLATE_WIRE_FIELDS) {
1409
+ if (Object.hasOwn(record2, key)) next[key] = record2[key];
1410
+ }
1411
+ projected.push(next);
1412
+ }
1413
+ return projected;
1414
+ }
1415
+ function facts(value) {
1416
+ if (Array.isArray(value)) {
1417
+ if (!value.length) return ["No items were returned."];
1418
+ const lines2 = value.slice(0, MAX_ROWS).flatMap((row, i) => {
1419
+ const record3 = object(row);
1420
+ return record3 ? [`Item ${i + 1}: ${recordFacts(record3).join("; ")}`] : [UNSUPPORTED];
1421
+ });
1422
+ if (value.length > MAX_ROWS) lines2.push(`Showing ${MAX_ROWS} of ${value.length} returned rows. ${RAW_HINT}`);
1423
+ return lines2;
1424
+ }
1425
+ const record2 = object(value);
1426
+ if (!record2) return [UNSUPPORTED];
1427
+ const outer = Object.fromEntries(Object.entries(record2).filter(([key]) => !["account", "result", "data"].includes(key)));
1428
+ const lines = Object.keys(outer).length ? recordFacts(outer) : [];
1429
+ for (const key of ["account", "result", "data"]) {
1430
+ if (!Object.hasOwn(record2, key)) continue;
1431
+ const inner = object(record2[key]);
1432
+ if (inner) lines.push(`${label(key)}:`, ...recordFacts(inner));
1433
+ else if (key === "account" && Array.isArray(record2[key])) {
1434
+ const accounts = record2[key];
1435
+ lines.push(...facts(accounts));
1436
+ } else lines.push(UNSUPPORTED);
1437
+ }
1438
+ return lines.length ? lines : ["No result fields were returned."];
832
1439
  }
833
1440
 
834
1441
  // src/cli/lib/ask.ts
@@ -863,7 +1470,7 @@ function parseRpcBody(raw) {
863
1470
  return JSON.parse(dataLine.slice(5).trim());
864
1471
  }
865
1472
  async function callMcpTool(tool, args, opts = {}) {
866
- const token = resolveCliToken() ?? await getAccessToken();
1473
+ const token = (resolveExplicitCredential({ allowOcs: true }) ?? await resolveCredential({ allowOcs: true }))?.token;
867
1474
  if (!token) {
868
1475
  return {
869
1476
  ok: false,
@@ -890,7 +1497,7 @@ async function callMcpTool(tool, args, opts = {}) {
890
1497
  });
891
1498
  const signal = controller.signal;
892
1499
  try {
893
- return await Promise.race([
1500
+ const result = await Promise.race([
894
1501
  (async () => {
895
1502
  const initRes = await fetch(MCP_URL, {
896
1503
  method: "POST",
@@ -973,6 +1580,7 @@ async function callMcpTool(tool, args, opts = {}) {
973
1580
  })(),
974
1581
  deadline
975
1582
  ]);
1583
+ return redactCredential(result, token);
976
1584
  } catch (e) {
977
1585
  if (isTimeoutish(e)) {
978
1586
  const secs = Math.round(timeoutMs / 1e3);
@@ -990,7 +1598,7 @@ async function callMcpTool(tool, args, opts = {}) {
990
1598
  )
991
1599
  };
992
1600
  }
993
- const msg = e instanceof Error ? e.message : String(e);
1601
+ const msg = redact((e instanceof Error ? e.message : String(e)).split(token).join("***"));
994
1602
  return {
995
1603
  ok: false,
996
1604
  text: withNextStep(`Could not reach MCP: ${msg}`, [
@@ -1019,11 +1627,24 @@ function parseRouting(text3) {
1019
1627
  return void 0;
1020
1628
  }
1021
1629
  }
1022
- async function carrierAsk(intent) {
1630
+ function readableAskError(result) {
1631
+ try {
1632
+ const body = JSON.parse(result.text);
1633
+ const message = body.error ?? body.message;
1634
+ if (typeof message === "string") return { ...result, text: message };
1635
+ } catch {
1636
+ }
1637
+ return result;
1638
+ }
1639
+ async function carrierAsk(intent, opts = {}) {
1640
+ const result = await routeAsk(intent, opts);
1641
+ return opts.json ? result : { ...result, text: sanitizeAskText(result.text) };
1642
+ }
1643
+ async function routeAsk(intent, opts) {
1023
1644
  const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
1024
1645
  if (!routed.ok) return routed;
1025
1646
  const routing = parseRouting(routed.text);
1026
- if (!routing) return routed;
1647
+ if (!routing) return opts.json ? routed : { ...routed, text: formatAskAnswer(routed.text, "carrier_ask", routed.structured, intent) };
1027
1648
  if (!routing.resolved_tool) {
1028
1649
  const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
1029
1650
  return {
@@ -1049,13 +1670,16 @@ async function carrierAsk(intent) {
1049
1670
  return {
1050
1671
  ok: false,
1051
1672
  text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
1052
- `Params: ${JSON.stringify(args)}`,
1673
+ `Params: ${JSON.stringify(args, (_k, v) => typeof v === "string" ? sanitizeAskText(v) : v)}`,
1053
1674
  "Run the matching discrete command so every flag is explicit and reviewable",
1054
1675
  "carrier --help lists every domain"
1055
1676
  ])
1056
1677
  };
1057
1678
  }
1058
- return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
1679
+ const result = await callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
1680
+ if (opts.json) return result;
1681
+ if (!result.ok) return readableAskError(result);
1682
+ return { ...result, text: formatAskAnswer(result.text, tool, result.structured, intent) };
1059
1683
  }
1060
1684
 
1061
1685
  // src/cli/lib/rest.ts
@@ -1063,9 +1687,6 @@ var DEFAULT_ERROR_HINTS2 = [
1063
1687
  "Re-check the arguments: carrier <domain> <command> --help",
1064
1688
  "Or open Claude and ask there with full tool routing"
1065
1689
  ];
1066
- function resolveApiKey() {
1067
- return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
1068
- }
1069
1690
  function ocsTokenOnly() {
1070
1691
  return Boolean(
1071
1692
  process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
@@ -1114,7 +1735,7 @@ function orgNotConnected(raw) {
1114
1735
  }
1115
1736
  }
1116
1737
  async function callRestEndpoint(method, path, args, opts = {}) {
1117
- const token = resolveApiKey() ?? await getAccessToken();
1738
+ const token = (await resolveCredential())?.token;
1118
1739
  if (!token) {
1119
1740
  return {
1120
1741
  ok: false,
@@ -1158,7 +1779,7 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1158
1779
  signal: controller.signal,
1159
1780
  body: sendsBody ? JSON.stringify(rest) : void 0
1160
1781
  });
1161
- const raw = await res.text();
1782
+ const raw = (await res.text()).split(token).join("***");
1162
1783
  if (res.status === 202) {
1163
1784
  let confirmToken;
1164
1785
  let expires;
@@ -1225,7 +1846,7 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1225
1846
  ])
1226
1847
  };
1227
1848
  }
1228
- const msg = e instanceof Error ? e.message : String(e);
1849
+ const msg = (e instanceof Error ? e.message : String(e)).split(token).join("***");
1229
1850
  return {
1230
1851
  ok: false,
1231
1852
  text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
@@ -1248,9 +1869,9 @@ var NAME_TO_CODE = /* @__PURE__ */ new Map();
1248
1869
  for (let first = 65; first <= 90; first += 1) {
1249
1870
  for (let second = 65; second <= 90; second += 1) {
1250
1871
  const code = String.fromCharCode(first, second);
1251
- const label = regionLabel(code);
1252
- if (!label) continue;
1253
- const key = label.toLowerCase();
1872
+ const label2 = regionLabel(code);
1873
+ if (!label2) continue;
1874
+ const key = label2.toLowerCase();
1254
1875
  const current = NAME_TO_CODE.get(key);
1255
1876
  if (!current || PREFERRED.has(code)) NAME_TO_CODE.set(key, code);
1256
1877
  }
@@ -1382,9 +2003,9 @@ function logoMime(bytes) {
1382
2003
  function regionLabel(code) {
1383
2004
  if (!/^[A-Z]{2}$/.test(code)) return void 0;
1384
2005
  try {
1385
- const label = regionNames.of(code);
1386
- if (!label || label.toUpperCase() === code) return void 0;
1387
- return label;
2006
+ const label2 = regionNames.of(code);
2007
+ if (!label2 || label2.toUpperCase() === code) return void 0;
2008
+ return label2;
1388
2009
  } catch {
1389
2010
  return void 0;
1390
2011
  }
@@ -1505,9 +2126,9 @@ function storefrontPlanBody(input) {
1505
2126
  function isStorefrontPlanError(value) {
1506
2127
  return value !== null && "error" in value;
1507
2128
  }
1508
- function parseWholeNumber(value, label) {
2129
+ function parseWholeNumber(value, label2) {
1509
2130
  if (value === void 0) return { ok: true, value: void 0 };
1510
- if (!/^\d+$/.test(value)) return { ok: false, message: `${label} must be a whole number.` };
2131
+ if (!/^\d+$/.test(value)) return { ok: false, message: `${label2} must be a whole number.` };
1511
2132
  return { ok: true, value: Number(value) };
1512
2133
  }
1513
2134
  function overlay(base, input, fields) {
@@ -1734,7 +2355,7 @@ function record(value) {
1734
2355
  }
1735
2356
 
1736
2357
  // src/cli/lib/resend.ts
1737
- import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
2358
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
1738
2359
  import { createServer as createServer2 } from "http";
1739
2360
  var RESEND_API = "https://api.resend.com";
1740
2361
  var RESEND_API_KEYS_URL = "https://resend.com/api-keys";
@@ -1752,7 +2373,7 @@ function base64url2(input) {
1752
2373
  }
1753
2374
  function pkcePair2() {
1754
2375
  const verifier = base64url2(randomBytes2(64));
1755
- return { verifier, challenge: base64url2(createHash2("sha256").update(verifier).digest()) };
2376
+ return { verifier, challenge: base64url2(createHash3("sha256").update(verifier).digest()) };
1756
2377
  }
1757
2378
  function buildAuthorizeUrl(opts) {
1758
2379
  const url = new URL(`${RESEND_API}/oauth/authorize`);
@@ -1900,9 +2521,9 @@ async function startLoopbackServer() {
1900
2521
  error ? { ok: false, reason: `Resend returned "${error}".` } : code ? { ok: true, code, reason: state ?? void 0 } : { ok: false, reason: "Resend sent no authorization code." }
1901
2522
  );
1902
2523
  });
1903
- await new Promise((resolve3, reject) => {
2524
+ await new Promise((resolve5, reject) => {
1904
2525
  server.once("error", reject);
1905
- server.listen(0, REDIRECT_HOST, resolve3);
2526
+ server.listen(0, REDIRECT_HOST, resolve5);
1906
2527
  });
1907
2528
  const port = server.address().port;
1908
2529
  const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
@@ -1910,12 +2531,12 @@ async function startLoopbackServer() {
1910
2531
  redirectUri,
1911
2532
  close: () => server.close(),
1912
2533
  waitForCode(state, timeoutMs) {
1913
- return new Promise((resolve3) => {
2534
+ return new Promise((resolve5) => {
1914
2535
  const done = (r) => {
1915
2536
  clearTimeout(timer);
1916
2537
  resolveCode = null;
1917
2538
  server.close();
1918
- resolve3(r);
2539
+ resolve5(r);
1919
2540
  };
1920
2541
  const timer = setTimeout(
1921
2542
  () => done({ ok: false, reason: "Timed out waiting for the browser." }),
@@ -2189,6 +2810,217 @@ function buildDashScreen(domain, payload) {
2189
2810
  }
2190
2811
  }
2191
2812
 
2813
+ // src/cli/lib/status.ts
2814
+ import { homedir as homedir4 } from "os";
2815
+ import { join as join8 } from "path";
2816
+
2817
+ // src/cli/lib/mcp-url.ts
2818
+ var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
2819
+ var PRODUCTION_HOST = "mcp.carrier.llc";
2820
+ var URL_TOKEN = /https?:\/\/[^\s"'<>)]+/g;
2821
+ function pathWithoutTrailingSlash(pathname) {
2822
+ let end = pathname.length;
2823
+ while (end > 1 && pathname.charCodeAt(end - 1) === 47) end -= 1;
2824
+ return pathname.slice(0, end);
2825
+ }
2826
+ function parseHttpUrl(value) {
2827
+ try {
2828
+ const parsed = new URL(value);
2829
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
2830
+ return parsed;
2831
+ } catch {
2832
+ return null;
2833
+ }
2834
+ }
2835
+ function isProductionMcpUrl(value) {
2836
+ if (value === PRODUCTION_MCP_URL) return true;
2837
+ const parsed = parseHttpUrl(value);
2838
+ if (!parsed || parsed.protocol !== "https:" || parsed.hostname !== PRODUCTION_HOST) return false;
2839
+ return pathWithoutTrailingSlash(parsed.pathname) === "/mcp";
2840
+ }
2841
+ function isCarrierMcpHost(value) {
2842
+ const parsed = parseHttpUrl(value);
2843
+ return parsed?.protocol === "https:" && parsed.hostname === PRODUCTION_HOST;
2844
+ }
2845
+ function trimTrailingPunctuation(token) {
2846
+ let end = token.length;
2847
+ while (end > 0) {
2848
+ const code = token.charCodeAt(end - 1);
2849
+ if (code !== 46 && code !== 44 && code !== 59 && code !== 58) break;
2850
+ end -= 1;
2851
+ }
2852
+ return token.slice(0, end);
2853
+ }
2854
+ function lineMentionsProductionMcp(line) {
2855
+ const tokens = line.match(URL_TOKEN);
2856
+ if (!tokens) return false;
2857
+ for (const token of tokens) {
2858
+ if (isProductionMcpUrl(trimTrailingPunctuation(token))) return true;
2859
+ }
2860
+ return false;
2861
+ }
2862
+
2863
+ // src/cli/lib/status.ts
2864
+ function rankHit(name, url) {
2865
+ let rank = 0;
2866
+ if (name.toLowerCase() === MCP_NAME) rank += 100;
2867
+ else if (/^carrier[-_]?/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 40;
2868
+ else if (/carrier/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 20;
2869
+ else if (/carrier/i.test(name)) rank += 5;
2870
+ if (isProductionMcpUrl(url)) rank += 50;
2871
+ else if (isCarrierMcpHost(url)) rank += 25;
2872
+ return rank;
2873
+ }
2874
+ function scanMcpServers(servers) {
2875
+ if (!servers || typeof servers !== "object") return null;
2876
+ let best = null;
2877
+ for (const [name, entry] of Object.entries(servers)) {
2878
+ if (!/carrier/i.test(name) && !(typeof entry?.url === "string" && /carrier\.llc/i.test(entry.url))) {
2879
+ continue;
2880
+ }
2881
+ const url = typeof entry?.url === "string" ? entry.url : "";
2882
+ const rank = rankHit(name, url);
2883
+ if (rank < 20 && !isCarrierMcpHost(url)) {
2884
+ continue;
2885
+ }
2886
+ const urlMatches = isProductionMcpUrl(url);
2887
+ const hit = {
2888
+ registered: true,
2889
+ urlMatches,
2890
+ detail: url ? `${name}: ${url}` : `${name}: ${entry?.command ?? "configured"}`,
2891
+ rank
2892
+ };
2893
+ if (!best || hit.rank > best.rank) best = hit;
2894
+ }
2895
+ return best;
2896
+ }
2897
+ async function probeMcpFromConfig() {
2898
+ const candidates = [
2899
+ join8(homedir4(), ".claude.json"),
2900
+ join8(claudeHome(), "settings.json"),
2901
+ join8(claudeHome(), ".mcp.json"),
2902
+ join8(process.cwd(), ".mcp.json")
2903
+ ];
2904
+ let best = null;
2905
+ let configSeen = false;
2906
+ for (const file of candidates) {
2907
+ if (!await exists(file)) continue;
2908
+ try {
2909
+ const json = JSON.parse(await readFile(file, "utf8"));
2910
+ configSeen = true;
2911
+ const direct = scanMcpServers(json.mcpServers);
2912
+ if (direct && (!best || direct.rank > best.rank)) best = direct;
2913
+ if (json.projects) {
2914
+ for (const proj of Object.values(json.projects)) {
2915
+ const hit = scanMcpServers(proj?.mcpServers);
2916
+ if (hit && (!best || hit.rank > best.rank)) best = hit;
2917
+ }
2918
+ }
2919
+ } catch {
2920
+ }
2921
+ }
2922
+ if (!best) return { hit: null, configSeen };
2923
+ return {
2924
+ hit: { registered: best.registered, urlMatches: best.urlMatches, detail: best.detail },
2925
+ configSeen
2926
+ };
2927
+ }
2928
+ async function probeMcpRegistration() {
2929
+ const { hit, configSeen } = await probeMcpFromConfig();
2930
+ if (hit) return hit;
2931
+ if (configSeen) {
2932
+ return {
2933
+ registered: false,
2934
+ urlMatches: false,
2935
+ detail: "carrier not registered (no production MCP URL in Claude config)"
2936
+ };
2937
+ }
2938
+ const claudeFound = await which("claude");
2939
+ if (!claudeFound) {
2940
+ return {
2941
+ registered: false,
2942
+ urlMatches: false,
2943
+ detail: "claude CLI not on PATH"
2944
+ };
2945
+ }
2946
+ const r = await run("claude", ["mcp", "list"], { timeoutMs: 5e3 });
2947
+ const out = `${r.stdout}
2948
+ ${r.stderr}`;
2949
+ if (!r.ok && !out.trim()) {
2950
+ return { registered: false, urlMatches: false, detail: "claude mcp list failed or timed out" };
2951
+ }
2952
+ if (/timeout after/i.test(r.stderr) && !/carrier/i.test(out)) {
2953
+ return {
2954
+ registered: false,
2955
+ urlMatches: false,
2956
+ detail: "claude mcp list timed out \u2014 run `claude mcp list` manually"
2957
+ };
2958
+ }
2959
+ const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
2960
+ 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));
2961
+ if (!carrierLine) {
2962
+ return { registered: false, urlMatches: false, detail: "carrier not in claude mcp list" };
2963
+ }
2964
+ const urlMatches = lineMentionsProductionMcp(carrierLine);
2965
+ return { registered: true, urlMatches, detail: carrierLine.slice(0, 200) };
2966
+ }
2967
+ function buildNextSteps(s) {
2968
+ const steps = [];
2969
+ if (!s.claudeFound) {
2970
+ steps.push("Install Claude Code (https://claude.ai/code), then re-run: carrier plugin install");
2971
+ } else if (!s.pluginInstalled) {
2972
+ steps.push("Install the Carrier plugin: carrier plugin install");
2973
+ } else if (!s.mcpRegistered) {
2974
+ steps.push(`Register the zero-cred MCP URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
2975
+ } else if (!s.mcpUrlMatches) {
2976
+ steps.push(`Update MCP to production URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
2977
+ }
2978
+ if (s.mcpRegistered || s.pluginInstalled) {
2979
+ steps.push(
2980
+ "Talk to your fleet in Claude \u2014 first tool call opens Clerk OAuth (Google / GitHub / email). No API token needed."
2981
+ );
2982
+ } else {
2983
+ steps.push("Create a free account (browser): carrier open signup");
2984
+ steps.push("Or sign in: carrier open signin");
2985
+ }
2986
+ if (!s.tokenPresent) {
2987
+ steps.push(
2988
+ "Headless: carrier login, or refresh compatible Carrier OAuth in Claude. Export to a child terminal: carrier exec -- bash."
2989
+ );
2990
+ } else {
2991
+ steps.push('Try a natural-language fleet query: carrier ask "show fleet health"');
2992
+ }
2993
+ steps.push("Scaffold a white-label storefront anytime: carrier site create");
2994
+ return steps;
2995
+ }
2996
+ async function gatherStatus() {
2997
+ const [plugin2, mcp, claudeFound] = await Promise.all([
2998
+ pluginStatus(),
2999
+ probeMcpRegistration(),
3000
+ which("claude")
3001
+ ]);
3002
+ const credential = await resolveCredential({ allowOcs: true });
3003
+ const token = { present: !!credential, kind: credential?.kind ?? "none" };
3004
+ let authMode = "none";
3005
+ if (token.kind === "oauth") authMode = "oauth";
3006
+ else if (token.kind === "api-key") authMode = "api-key";
3007
+ else if (token.kind === "ocs-token") authMode = "ocs-token";
3008
+ else if (mcp.registered || plugin2.installed) authMode = "oauth-first-use";
3009
+ const base = {
3010
+ pluginInstalled: plugin2.installed,
3011
+ pluginVersion: plugin2.version,
3012
+ pluginDir: plugin2.dir,
3013
+ claudeFound,
3014
+ mcpRegistered: mcp.registered,
3015
+ mcpUrlMatches: mcp.urlMatches,
3016
+ mcpDetail: mcp.detail,
3017
+ tokenPresent: token.present,
3018
+ tokenKind: token.kind,
3019
+ authMode
3020
+ };
3021
+ return { ...base, nextSteps: buildNextSteps(base) };
3022
+ }
3023
+
2192
3024
  // src/cli/lib/home.ts
2193
3025
  import * as p from "@clack/prompts";
2194
3026
  import pc2 from "picocolors";
@@ -2207,19 +3039,19 @@ async function showHomeStatus() {
2207
3039
  printStatus(st);
2208
3040
  return st;
2209
3041
  }
2210
- async function openAndReport(url, label) {
3042
+ async function openAndReport(url, label2) {
2211
3043
  const r = await openUrl(url);
2212
3044
  if (r.ok) p.log.success(r.hint);
2213
3045
  else {
2214
3046
  p.log.warn(r.hint);
2215
- p.note(url, label);
3047
+ p.note(url, label2);
2216
3048
  }
2217
3049
  }
2218
3050
  async function runHome(opts) {
2219
3051
  let st = await showHomeStatus();
2220
3052
  p.note(oauthFirstUseNote(), "How auth works");
2221
3053
  for (; ; ) {
2222
- const hasToken = !!resolveCliToken();
3054
+ const hasToken = !!await resolveCredential({ allowOcs: true });
2223
3055
  const choice = await p.select({
2224
3056
  message: "What do you want to do?",
2225
3057
  options: [
@@ -2255,8 +3087,8 @@ async function runHome(opts) {
2255
3087
  },
2256
3088
  {
2257
3089
  value: "ask",
2258
- label: hasToken ? "Ask the fleet (carrier ask)" : "Ask the fleet (needs CARRIER_API_KEY)",
2259
- hint: hasToken ? "Uses env token" : "Optional headless path"
3090
+ label: hasToken ? "Ask the fleet (carrier ask)" : "Ask the fleet (sign-in needed)",
3091
+ hint: hasToken ? "Uses Carrier credential" : "Run carrier login"
2260
3092
  },
2261
3093
  {
2262
3094
  value: "site",
@@ -2294,8 +3126,8 @@ async function runHome(opts) {
2294
3126
  p.log.info("Link OCS credentials (BYO) or finish managed setup, then use Claude MCP.");
2295
3127
  break;
2296
3128
  case "ask": {
2297
- if (!resolveCliToken()) {
2298
- p.log.warn("No CARRIER_API_KEY / OCS token in env.");
3129
+ if (!await resolveCredential({ allowOcs: true })) {
3130
+ p.log.warn("No usable Carrier credential. Run carrier login or refresh Carrier OAuth in Claude.");
2299
3131
  p.note(
2300
3132
  [
2301
3133
  "Interactive (recommended): install MCP, open Claude, ask in plain English.",
@@ -2317,7 +3149,7 @@ async function runHome(opts) {
2317
3149
  spin.start("Asking Carrier MCP\u2026");
2318
3150
  const result = await carrierAsk(String(intent).trim());
2319
3151
  spin.stop(result.ok ? "Answer" : "Could not complete ask");
2320
- if (result.ok) p.note(result.text.slice(0, 4e3), "carrier ask");
3152
+ if (result.ok) p.note(result.text, "carrier ask");
2321
3153
  else p.log.error(result.text);
2322
3154
  break;
2323
3155
  }
@@ -2330,6 +3162,68 @@ async function runHome(opts) {
2330
3162
  }
2331
3163
  }
2332
3164
 
3165
+ // src/cli/lib/credential-exec.ts
3166
+ import { spawn as spawn3 } from "child_process";
3167
+ import { createHash as createHash4 } from "crypto";
3168
+ import { constants as constants3 } from "os";
3169
+ import { writeSync, fstatSync } from "fs";
3170
+ async function credentialHandoff() {
3171
+ try {
3172
+ const stat = fstatSync(3);
3173
+ if (!stat.isFIFO() && !stat.isSocket()) throw new Error("unsafe fd");
3174
+ const credential = await resolveCredential();
3175
+ if (!credential) throw new Error("no credential");
3176
+ const fields = [credential.token, credential.origin ?? "explicit", String(credential.expiresAt ?? 0), credential.kind === "oauth" ? createHash4("sha256").update(credential.token).digest("hex") : "-", credential.binding ?? "-"];
3177
+ if (fields.some((field) => /[\r\n\0]/.test(field))) throw new Error("unsafe field");
3178
+ const data = Buffer.from(`${fields.join("\n")}
3179
+ `);
3180
+ let offset = 0;
3181
+ while (offset < data.length) offset += writeSync(3, data, offset, data.length - offset);
3182
+ return 0;
3183
+ } catch {
3184
+ console.error("Carrier credential handoff unavailable; use the static shell integration with a usable owner session.");
3185
+ return 1;
3186
+ }
3187
+ }
3188
+ async function credentialExec(command, args) {
3189
+ const credential = await resolveCredential();
3190
+ if (!credential) {
3191
+ console.error("No usable Carrier credential. Run carrier login, or refresh Carrier OAuth in Claude. See carrier exec --help.");
3192
+ return 1;
3193
+ }
3194
+ const env = { ...process.env, CARRIER_API_KEY: credential.token };
3195
+ if (credential.kind === "oauth") {
3196
+ env.CARRIER_OAUTH_HANDOFF = createHash4("sha256").update(credential.token).digest("hex");
3197
+ env.CARRIER_OAUTH_ORIGIN = credential.origin;
3198
+ env.CARRIER_OAUTH_BINDING = credential.binding;
3199
+ env.CARRIER_OAUTH_EXPIRES_AT = String(credential.expiresAt);
3200
+ } else {
3201
+ delete env.CARRIER_OAUTH_HANDOFF;
3202
+ delete env.CARRIER_OAUTH_BINDING;
3203
+ delete env.CARRIER_OAUTH_ORIGIN;
3204
+ delete env.CARRIER_OAUTH_EXPIRES_AT;
3205
+ }
3206
+ return new Promise((resolve5) => {
3207
+ const child = spawn3(command, args, { env, stdio: "inherit", shell: false });
3208
+ const forward = (signal) => {
3209
+ child.kill(signal);
3210
+ };
3211
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
3212
+ const handlers = signals.map((signal) => () => forward(signal));
3213
+ signals.forEach((signal, i) => process.on(signal, handlers[i]));
3214
+ const cleanup = () => signals.forEach((signal, i) => process.removeListener(signal, handlers[i]));
3215
+ child.once("error", () => {
3216
+ cleanup();
3217
+ console.error("Could not start the requested command.");
3218
+ resolve5(1);
3219
+ });
3220
+ child.once("close", (code, signal) => {
3221
+ cleanup();
3222
+ resolve5(code ?? (signal ? 128 + (constants3.signals[signal] ?? 1) : 1));
3223
+ });
3224
+ });
3225
+ }
3226
+
2333
3227
  // src/cli/lib/signup.ts
2334
3228
  var DEFAULT_MAX_CODE_ATTEMPTS = 3;
2335
3229
  function parseEnvironment(raw) {
@@ -5143,7 +6037,7 @@ function formatJsonOutput(text3) {
5143
6037
  }
5144
6038
  }
5145
6039
  async function runCapability(domain, cmd, opts) {
5146
- const label = `${domain.name} ${cmd.name}`;
6040
+ const label2 = `${domain.name} ${cmd.name}`;
5147
6041
  const quiet = opts.json === true;
5148
6042
  const { args, errors, notes } = buildToolArgs(cmd, opts);
5149
6043
  if (cmd.write && opts.commit === true) delete args.dry_run;
@@ -5151,9 +6045,9 @@ async function runCapability(domain, cmd, opts) {
5151
6045
  errors.push(`${capabilityTarget(cmd)} has no preview mode. Pass --confirm to run it for real.`);
5152
6046
  }
5153
6047
  if (errors.length) {
5154
- const message = withNextStep(`Cannot run \`carrier ${label}\`.`, [
6048
+ const message = withNextStep(`Cannot run \`carrier ${label2}\`.`, [
5155
6049
  ...errors,
5156
- `See every flag: carrier ${label} --help`
6050
+ `See every flag: carrier ${label2} --help`
5157
6051
  ]);
5158
6052
  if (quiet) console.error(message);
5159
6053
  else p2.log.error(message);
@@ -5169,7 +6063,7 @@ async function runCapability(domain, cmd, opts) {
5169
6063
  ].join("\n");
5170
6064
  if (quiet) process.stdout.write(`${formatJsonOutput(preview)}
5171
6065
  `);
5172
- else p2.note(preview, `carrier ${label}`);
6066
+ else p2.note(preview, `carrier ${label2}`);
5173
6067
  return;
5174
6068
  }
5175
6069
  const dryRun = cmd.write === true && args.dry_run === true;
@@ -5180,7 +6074,7 @@ async function runCapability(domain, cmd, opts) {
5180
6074
  }
5181
6075
  const target = capabilityTarget(cmd);
5182
6076
  const errorHints = [
5183
- `Check the arguments: carrier ${label} --help`,
6077
+ `Check the arguments: carrier ${label2} --help`,
5184
6078
  'Or describe the task in words: carrier ask "\u2026"'
5185
6079
  ];
5186
6080
  const s = quiet ? null : p2.spinner();
@@ -5593,7 +6487,7 @@ async function interactiveSiteCreate() {
5593
6487
  defaultValue: "./storefront"
5594
6488
  });
5595
6489
  if (p3.isCancel(dir)) return;
5596
- const target = resolve2(dir);
6490
+ const target = resolve4(dir);
5597
6491
  if (await exists(target)) {
5598
6492
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5599
6493
  if (p3.isCancel(go) || !go) {
@@ -5605,6 +6499,20 @@ async function interactiveSiteCreate() {
5605
6499
  p3.outro(pc4.green(`Scaffolded. cd ${dir} && pnpm install && pnpm dev`));
5606
6500
  }
5607
6501
  var program = new Command();
6502
+ program.command("credential-handoff").description("Private FD protocol for the explicitly sourced static shell integration; never stdout.").action(async () => {
6503
+ process.exitCode = await credentialHandoff();
6504
+ });
6505
+ program.command("shell-path").description("Print the static integration file path, without credentials or startup edits.").argument("<shell>", "bash or zsh").action((shell) => {
6506
+ if (shell !== "bash" && shell !== "zsh") {
6507
+ console.error("Supported shells: bash, zsh.");
6508
+ process.exitCode = 1;
6509
+ return;
6510
+ }
6511
+ console.log(resolve4(packageRoot(), "shell", `carrier.${shell}`));
6512
+ });
6513
+ program.command("exec").description("Run a command/child shell with CARRIER_API_KEY securely in its environment.").argument("<command>", "Executable to start (use -- before the command)").argument("[args...]", "Arguments passed without shell interpolation").addHelpText("after", "\nExamples: carrier exec -- bash\n carrier exec -- your-headless-job\nOAuth snapshots expire; Carrier commands refresh their owner per call. Exit the child shell after logout. This never changes the existing parent shell or prints a token.").action(async (command, args) => {
6514
+ process.exitCode = await credentialExec(command, args);
6515
+ });
5608
6516
  program.name("carrier").description(
5609
6517
  [
5610
6518
  "Carrier CLI \u2014 status, OAuth-ready MCP install, storefront scaffold, and fleet control.",
@@ -5624,7 +6532,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5624
6532
  if (o.yes) {
5625
6533
  await doPluginInstall();
5626
6534
  const brand = CARRIER_BRAND;
5627
- const target = resolve2(o.dir);
6535
+ const target = resolve4(o.dir);
5628
6536
  if (await exists(target)) {
5629
6537
  info(pc4.yellow(`${target} exists \u2014 writing into it (--yes).`));
5630
6538
  }
@@ -5649,7 +6557,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5649
6557
  printStatus(st);
5650
6558
  await doPluginInstall();
5651
6559
  const brand = await promptBrand(CARRIER_BRAND);
5652
- const target = resolve2(o.dir);
6560
+ const target = resolve4(o.dir);
5653
6561
  if (await exists(target)) {
5654
6562
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5655
6563
  if (p3.isCancel(go) || !go) {
@@ -5690,7 +6598,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5690
6598
  onInstall: doPluginInstall,
5691
6599
  onSiteCreate: async () => {
5692
6600
  const brand = await promptBrand(CARRIER_BRAND);
5693
- const target = resolve2(o.dir);
6601
+ const target = resolve4(o.dir);
5694
6602
  if (await exists(target)) {
5695
6603
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5696
6604
  if (p3.isCancel(go) || !go) {
@@ -5765,7 +6673,7 @@ program.command("login").description("Sign in from this terminal. Opens a browse
5765
6673
  return;
5766
6674
  }
5767
6675
  p3.note(
5768
- [`Scope: ${r.scope ?? "read write"}`, "", "Try it: carrier intelligence fleet-health"].join("\n"),
6676
+ [`Scope: ${r.scope ?? "read write"}`, "", "Try it: carrier intelligence fleet-health", 'Existing shell: source "$(carrier shell-path bash)"; carrier_auth_export', "Child terminal: carrier exec -- bash"].join("\n"),
5769
6677
  "Signed in"
5770
6678
  );
5771
6679
  } catch (e) {
@@ -5980,8 +6888,8 @@ program.command("examples").description("Print natural-language fleet prompts fo
5980
6888
  });
5981
6889
  program.command("ask").description(
5982
6890
  'Natural-language fallback for whatever the domain commands miss: carrier ask "show fleet health" (needs CARRIER_API_KEY or OCS token).'
5983
- ).argument("<intent...>", "Natural-language intent").action(async (parts) => {
5984
- header();
6891
+ ).argument("<intent...>", "Natural-language intent").option("--json", "Print the complete raw tool result (may contain sensitive data)").action(async (parts, opts) => {
6892
+ if (!opts.json) header();
5985
6893
  const intent = parts.join(" ").trim();
5986
6894
  if (!intent) {
5987
6895
  fail("Missing intent.", [
@@ -5991,12 +6899,27 @@ program.command("ask").description(
5991
6899
  process.exitCode = 1;
5992
6900
  return;
5993
6901
  }
5994
- const s = p3.spinner();
5995
- s.start("Asking Carrier MCP\u2026");
5996
- const result = await carrierAsk(intent);
5997
- s.stop(result.ok ? "Done" : "Failed");
6902
+ const s = opts.json ? void 0 : p3.spinner();
6903
+ s?.start("Asking Carrier MCP\u2026");
6904
+ const result = await carrierAsk(intent, opts);
6905
+ s?.stop(result.ok ? "Done" : "Failed");
6906
+ if (opts.json) {
6907
+ if (result.ok) {
6908
+ let raw = result.text;
6909
+ try {
6910
+ JSON.parse(raw);
6911
+ } catch {
6912
+ raw = JSON.stringify(raw);
6913
+ }
6914
+ process.stdout.write(raw + "\n");
6915
+ } else {
6916
+ process.stderr.write(result.text + "\n");
6917
+ process.exitCode = 1;
6918
+ }
6919
+ return;
6920
+ }
5998
6921
  if (result.ok) {
5999
- p3.note(result.text.slice(0, 6e3), "Answer");
6922
+ p3.note(result.text, "Answer");
6000
6923
  p3.outro("");
6001
6924
  } else {
6002
6925
  p3.log.error(result.text);
@@ -6133,7 +7056,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6133
7056
  } else if (!o.yes) {
6134
7057
  brand = await promptBrand(CARRIER_BRAND);
6135
7058
  }
6136
- const target = resolve2(dir ?? "./storefront");
7059
+ const target = resolve4(dir ?? "./storefront");
6137
7060
  if (await exists(target)) {
6138
7061
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
6139
7062
  if (p3.isCancel(go) || !go) {
@@ -6158,7 +7081,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6158
7081
  site.command("deploy [dir]").description("Build + deploy a storefront to whichever host is available on this box.").option("--name <name>", "Project/worker name (defaults from brand)").option("--force", "Deploy under the scaffold default name anyway (acknowledges the overwrite risk)").option(`--target <target>`, `Force a host: ${TARGET_IDS.join(" | ")}`).option("--no-secrets", "Skip staging runtime secrets on the host").option("--clerk", "Provision and configure Clerk before building").option("--no-verify", "Skip the post-deploy check that the site actually serves").option("--autofix-agent", "On an unrecognised failure, print an agent-ready diagnosis bundle").option("--domain", "Route the brand domain to the deployment (Cloudflare provisions DNS + cert)").action(
6159
7082
  async (dir, o) => {
6160
7083
  header();
6161
- const target = resolve2(dir ?? "./storefront");
7084
+ const target = resolve4(dir ?? "./storefront");
6162
7085
  if (o.target && !isTargetId(o.target)) {
6163
7086
  fail(`Unknown target "${o.target}".`, [`Supported: ${TARGET_IDS.join(", ")}`]);
6164
7087
  process.exitCode = 1;
@@ -6208,7 +7131,7 @@ site.command("publish").description("Publish a Carrier-hosted storefront. Does n
6208
7131
  });
6209
7132
  site.command("targets [dir]").description("Show which deploy hosts are installed, logged in, and ready.").action(async (dir) => {
6210
7133
  header();
6211
- const storefront = resolve2(dir ?? "./storefront");
7134
+ const storefront = resolve4(dir ?? "./storefront");
6212
7135
  const statuses = await probeAll(storefront);
6213
7136
  const ranked = rankTargets(statuses);
6214
7137
  const rows2 = statuses.map((s) => {
@@ -6225,7 +7148,7 @@ site.command("targets [dir]").description("Show which deploy hosts are installed
6225
7148
  site.command("clerk [dir]").description("Create or discover a Clerk instance and configure it for the storefront.").option("--name <name>", "Application name (defaults from brand)").option("--production", "Also create a production instance (Platform API only)").option("--no-create", "Never create a new application; only reuse existing keys").option("--url <url>", "Deployed URL to whitelist as an origin/redirect").action(
6226
7149
  async (dir, o) => {
6227
7150
  header();
6228
- const storefront = resolve2(dir ?? "./storefront");
7151
+ const storefront = resolve4(dir ?? "./storefront");
6229
7152
  try {
6230
7153
  const brand = await loadStorefrontBrand(storefront, o.name ? { name: o.name } : void 0);
6231
7154
  const result = await provisionClerk({
@@ -6271,7 +7194,7 @@ site.command("clerk [dir]").description("Create or discover a Clerk instance and
6271
7194
  site.command("email [dir]").description("Connect Resend in the browser so the storefront can email QR codes.").option("--no-browser", "Never open a browser; print the manual steps instead").option("--yes", "Non-interactive: skip rather than wait for a human").option("--timeout <seconds>", "How long to wait for the browser approval", "180").action(
6272
7195
  async (dir, o) => {
6273
7196
  header();
6274
- const storefront = resolve2(dir ?? "./storefront");
7197
+ const storefront = resolve4(dir ?? "./storefront");
6275
7198
  const interactive = process.stdin.isTTY === true && o.yes !== true;
6276
7199
  try {
6277
7200
  const brand = await loadStorefrontBrand(storefront);
@@ -6329,7 +7252,7 @@ site.command("email [dir]").description("Connect Resend in the browser so the st
6329
7252
  );
6330
7253
  site.command("logo [dir]").description("Generate a storefront logo (SVG always; PNG if an image key is on this box or Carrier).").action(async (dir) => {
6331
7254
  header();
6332
- const target = resolve2(dir ?? "./storefront");
7255
+ const target = resolve4(dir ?? "./storefront");
6333
7256
  try {
6334
7257
  const brand = await loadStorefrontBrand(target);
6335
7258
  const logo = await writeStorefrontLogo(target, brand);