@agent-native/core 0.20.9 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/action.d.ts +61 -0
  2. package/dist/action.d.ts.map +1 -1
  3. package/dist/action.js +14 -0
  4. package/dist/action.js.map +1 -1
  5. package/dist/agent/production-agent.d.ts +4 -0
  6. package/dist/agent/production-agent.d.ts.map +1 -1
  7. package/dist/agent/production-agent.js +19 -7
  8. package/dist/agent/production-agent.js.map +1 -1
  9. package/dist/agent/types.d.ts +2 -0
  10. package/dist/agent/types.d.ts.map +1 -1
  11. package/dist/agent/types.js.map +1 -1
  12. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  13. package/dist/cli/code-agent-executor.js +1 -0
  14. package/dist/cli/code-agent-executor.js.map +1 -1
  15. package/dist/cli/connect.d.ts +13 -0
  16. package/dist/cli/connect.d.ts.map +1 -1
  17. package/dist/cli/connect.js +492 -4
  18. package/dist/cli/connect.js.map +1 -1
  19. package/dist/client/AssistantChat.d.ts.map +1 -1
  20. package/dist/client/AssistantChat.js +6 -5
  21. package/dist/client/AssistantChat.js.map +1 -1
  22. package/dist/client/code-agent-chat-adapter.js +1 -0
  23. package/dist/client/code-agent-chat-adapter.js.map +1 -1
  24. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  25. package/dist/client/conversation/AgentConversation.js +3 -2
  26. package/dist/client/conversation/AgentConversation.js.map +1 -1
  27. package/dist/client/conversation/code-agent-transcript.js +1 -0
  28. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  29. package/dist/client/conversation/types.d.ts +2 -0
  30. package/dist/client/conversation/types.d.ts.map +1 -1
  31. package/dist/client/conversation/types.js.map +1 -1
  32. package/dist/client/index.d.ts +1 -0
  33. package/dist/client/index.d.ts.map +1 -1
  34. package/dist/client/index.js +1 -0
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/mcp-apps/McpAppRenderer.d.ts +10 -0
  37. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -0
  38. package/dist/client/mcp-apps/McpAppRenderer.js +296 -0
  39. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -0
  40. package/dist/client/sse-event-processor.d.ts +3 -0
  41. package/dist/client/sse-event-processor.d.ts.map +1 -1
  42. package/dist/client/sse-event-processor.js +2 -0
  43. package/dist/client/sse-event-processor.js.map +1 -1
  44. package/dist/code-agents/transcript-normalizer.d.ts +2 -0
  45. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  46. package/dist/code-agents/transcript-normalizer.js +17 -0
  47. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  48. package/dist/db/client.d.ts.map +1 -1
  49. package/dist/db/client.js +22 -20
  50. package/dist/db/client.js.map +1 -1
  51. package/dist/index.browser.d.ts +1 -1
  52. package/dist/index.browser.d.ts.map +1 -1
  53. package/dist/index.browser.js +1 -1
  54. package/dist/index.browser.js.map +1 -1
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/mcp/build-server.d.ts.map +1 -1
  60. package/dist/mcp/build-server.js +154 -4
  61. package/dist/mcp/build-server.js.map +1 -1
  62. package/dist/mcp/stdio.d.ts +2 -2
  63. package/dist/mcp/stdio.d.ts.map +1 -1
  64. package/dist/mcp/stdio.js +26 -8
  65. package/dist/mcp/stdio.js.map +1 -1
  66. package/dist/mcp-client/app-result.d.ts +40 -0
  67. package/dist/mcp-client/app-result.d.ts.map +1 -0
  68. package/dist/mcp-client/app-result.js +19 -0
  69. package/dist/mcp-client/app-result.js.map +1 -0
  70. package/dist/mcp-client/index.d.ts +5 -2
  71. package/dist/mcp-client/index.d.ts.map +1 -1
  72. package/dist/mcp-client/index.js +185 -23
  73. package/dist/mcp-client/index.js.map +1 -1
  74. package/dist/mcp-client/manager.d.ts +16 -0
  75. package/dist/mcp-client/manager.d.ts.map +1 -1
  76. package/dist/mcp-client/manager.js +58 -1
  77. package/dist/mcp-client/manager.js.map +1 -1
  78. package/dist/mcp-client/routes.d.ts +4 -1
  79. package/dist/mcp-client/routes.d.ts.map +1 -1
  80. package/dist/mcp-client/routes.js +146 -0
  81. package/dist/mcp-client/routes.js.map +1 -1
  82. package/dist/styles/agent-conversation.css +53 -0
  83. package/docs/content/actions.md +25 -2
  84. package/docs/content/external-agents.md +62 -8
  85. package/docs/content/key-concepts.md +1 -1
  86. package/docs/content/mcp-clients.md +1 -1
  87. package/docs/content/mcp-protocol.md +16 -11
  88. package/package.json +2 -1
@@ -31,12 +31,14 @@ import os from "node:os";
31
31
  import { spawn } from "node:child_process";
32
32
  import path from "node:path";
33
33
  import { findWorkspaceRoot } from "../mcp/workspace-resolve.js";
34
- import { CLIENTS, writeHttpEntryForClient, } from "./mcp-config-writers.js";
35
- import { visibleTemplates } from "./templates-meta.js";
34
+ import { CLIENTS, configPathFor, writeCodexBlock, writeHttpEntryForClient, writeJsonMcpEntry, } from "./mcp-config-writers.js";
35
+ import { TEMPLATES, visibleTemplates } from "./templates-meta.js";
36
36
  const DEVICE_START_PATH = "/_agent-native/mcp/connect/device/start";
37
37
  const DEVICE_POLL_PATH = "/_agent-native/mcp/connect/device/poll";
38
38
  const SERVER_NAME_PREFIX = "agent-native";
39
39
  const CONNECT_PREFERENCES_VERSION = 1;
40
+ const CONNECT_PROFILES_VERSION = 1;
41
+ const DEFAULT_DEV_GATEWAY = "http://127.0.0.1:8080";
40
42
  const CLIENT_LABELS = {
41
43
  "claude-code": "Claude Code",
42
44
  "claude-code-cli": "Claude Code CLI",
@@ -74,6 +76,16 @@ export function parseConnectArgs(argv) {
74
76
  let v;
75
77
  if (a === "--all")
76
78
  out.all = true;
79
+ else if ((v = eat("--apps")) !== undefined)
80
+ out.apps = v;
81
+ else if ((v = eat("--gateway")) !== undefined)
82
+ out.gateway = v;
83
+ else if ((v = eat("--gateway-url")) !== undefined)
84
+ out.gateway = v;
85
+ else if ((v = eat("--port")) !== undefined)
86
+ out.port = Number(v);
87
+ else if ((v = eat("--owner-email")) !== undefined)
88
+ out.ownerEmail = v;
77
89
  else if ((v = eat("--client")) !== undefined) {
78
90
  out.client = v;
79
91
  out.clientExplicit = true;
@@ -84,8 +96,12 @@ export function parseConnectArgs(argv) {
84
96
  out.name = v;
85
97
  else if ((v = eat("--token")) !== undefined)
86
98
  out.token = v;
87
- else if (!a.startsWith("-") && !out.url)
88
- out.url = a;
99
+ else if (!a.startsWith("-") && !out.url) {
100
+ if (!out.mode && (a === "dev" || a === "prod"))
101
+ out.mode = a;
102
+ else
103
+ out.url = a;
104
+ }
89
105
  }
90
106
  return out;
91
107
  }
@@ -483,6 +499,459 @@ export function writeConfigs(clients, serverName, mcpUrl, token, scope, baseDir
483
499
  }
484
500
  return written;
485
501
  }
502
+ export function connectProfilesPath() {
503
+ return path.join(os.homedir(), ".agent-native", "connect-profiles.json");
504
+ }
505
+ function readConnectProfiles(file) {
506
+ try {
507
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
508
+ if (parsed && typeof parsed === "object") {
509
+ return {
510
+ version: Number(parsed.version) || CONNECT_PROFILES_VERSION,
511
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined,
512
+ prodEntries: parsed.prodEntries && typeof parsed.prodEntries === "object"
513
+ ? parsed.prodEntries
514
+ : {},
515
+ };
516
+ }
517
+ }
518
+ catch {
519
+ // no saved profiles yet
520
+ }
521
+ return { version: CONNECT_PROFILES_VERSION, prodEntries: {} };
522
+ }
523
+ function writeConnectProfiles(file, profiles) {
524
+ profiles.version = CONNECT_PROFILES_VERSION;
525
+ profiles.updatedAt = new Date().toISOString();
526
+ fs.mkdirSync(path.dirname(file), { recursive: true });
527
+ fs.writeFileSync(file, JSON.stringify(profiles, null, 2) + "\n", "utf-8");
528
+ }
529
+ function savedProfileEntry(profiles, serverName, client, file) {
530
+ return profiles.prodEntries?.[serverName]?.[client]?.[file];
531
+ }
532
+ function setSavedProfileEntry(profiles, serverName, client, file, entry) {
533
+ profiles.prodEntries ??= {};
534
+ profiles.prodEntries[serverName] ??= {};
535
+ profiles.prodEntries[serverName][client] ??= {};
536
+ profiles.prodEntries[serverName][client][file] = entry;
537
+ }
538
+ function readJsonMcpServerEntry(file, serverName) {
539
+ try {
540
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
541
+ const entry = parsed?.mcpServers?.[serverName];
542
+ return entry && typeof entry === "object" ? entry : undefined;
543
+ }
544
+ catch {
545
+ return undefined;
546
+ }
547
+ }
548
+ function tomlQuoteForRead(s) {
549
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
550
+ }
551
+ function codexHeadersForRead(name) {
552
+ const headers = [`[mcp_servers.${tomlQuoteForRead(name)}]`];
553
+ if (/^[A-Za-z0-9_-]+$/.test(name))
554
+ headers.push(`[mcp_servers.${name}]`);
555
+ return headers;
556
+ }
557
+ function readCodexMcpBlock(file, serverName) {
558
+ let content = "";
559
+ try {
560
+ content = fs.readFileSync(file, "utf-8");
561
+ }
562
+ catch {
563
+ return undefined;
564
+ }
565
+ const headers = new Set(codexHeadersForRead(serverName));
566
+ const lines = content.split(/\r?\n/);
567
+ for (let i = 0; i < lines.length; i++) {
568
+ if (!headers.has(lines[i].trim()))
569
+ continue;
570
+ const block = [lines[i]];
571
+ i++;
572
+ while (i < lines.length && !/^\s*\[/.test(lines[i])) {
573
+ block.push(lines[i]);
574
+ i++;
575
+ }
576
+ return block.join("\n").replace(/\n*$/, "") + "\n";
577
+ }
578
+ return undefined;
579
+ }
580
+ function readCurrentMcpEntry(client, serverName, baseDir, scope) {
581
+ const file = configPathFor(client, baseDir, scope);
582
+ if (client === "codex") {
583
+ const block = readCodexMcpBlock(file, serverName);
584
+ return {
585
+ file,
586
+ saved: block
587
+ ? { kind: "codex", block, savedAt: new Date().toISOString() }
588
+ : undefined,
589
+ };
590
+ }
591
+ const entry = readJsonMcpServerEntry(file, serverName);
592
+ return {
593
+ file,
594
+ saved: entry
595
+ ? { kind: "json", entry, savedAt: new Date().toISOString() }
596
+ : undefined,
597
+ };
598
+ }
599
+ function writeSavedMcpEntry(client, file, serverName, saved) {
600
+ if (client === "codex") {
601
+ if (saved.kind !== "codex")
602
+ return;
603
+ writeCodexBlock(file, serverName, saved.block);
604
+ return;
605
+ }
606
+ if (saved.kind !== "json")
607
+ return;
608
+ writeJsonMcpEntry(file, serverName, saved.entry);
609
+ }
610
+ function unescapeTomlString(value) {
611
+ return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
612
+ }
613
+ function parseCodexHeaders(block) {
614
+ const line = block
615
+ .split(/\r?\n/)
616
+ .find((candidate) => /^\s*http_headers\s*=/.test(candidate));
617
+ if (!line)
618
+ return {};
619
+ const match = line.match(/\{(.*)\}/);
620
+ if (!match)
621
+ return {};
622
+ const headers = {};
623
+ const pairRe = /"((?:\\.|[^"])*)"\s*=\s*"((?:\\.|[^"])*)"/g;
624
+ let pair;
625
+ while ((pair = pairRe.exec(match[1]))) {
626
+ headers[unescapeTomlString(pair[1])] = unescapeTomlString(pair[2]);
627
+ }
628
+ return headers;
629
+ }
630
+ function savedEntryUrl(saved) {
631
+ if (!saved)
632
+ return undefined;
633
+ if (saved.kind === "json") {
634
+ return typeof saved.entry.url === "string" ? saved.entry.url : undefined;
635
+ }
636
+ const match = saved.block.match(/^\s*url\s*=\s*"((?:\\.|[^"])*)"/m);
637
+ return match ? unescapeTomlString(match[1]) : undefined;
638
+ }
639
+ function savedEntryHeaders(saved) {
640
+ if (!saved)
641
+ return {};
642
+ if (saved.kind === "json") {
643
+ const headers = saved.entry.headers;
644
+ return headers && typeof headers === "object"
645
+ ? Object.fromEntries(Object.entries(headers)
646
+ .filter((entry) => {
647
+ return typeof entry[1] === "string";
648
+ })
649
+ .map(([key, value]) => [key, value]))
650
+ : {};
651
+ }
652
+ return parseCodexHeaders(saved.block);
653
+ }
654
+ function isLoopbackMcpUrl(value) {
655
+ if (!value)
656
+ return false;
657
+ try {
658
+ const url = new URL(value);
659
+ return (url.hostname === "localhost" ||
660
+ url.hostname === "127.0.0.1" ||
661
+ url.hostname === "::1" ||
662
+ url.hostname.startsWith("127."));
663
+ }
664
+ catch {
665
+ return false;
666
+ }
667
+ }
668
+ function decodeJwtSub(authHeader) {
669
+ if (!authHeader?.startsWith("Bearer "))
670
+ return undefined;
671
+ const token = authHeader.slice("Bearer ".length);
672
+ const [, payload] = token.split(".");
673
+ if (!payload)
674
+ return undefined;
675
+ try {
676
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
677
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
678
+ const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
679
+ return typeof parsed.sub === "string" && parsed.sub.includes("@")
680
+ ? parsed.sub
681
+ : undefined;
682
+ }
683
+ catch {
684
+ return undefined;
685
+ }
686
+ }
687
+ function ownerEmailFromEntry(saved) {
688
+ const headers = savedEntryHeaders(saved);
689
+ return (headers["X-Agent-Native-Owner-Email"] || decodeJwtSub(headers.Authorization));
690
+ }
691
+ function readEnvFile(file) {
692
+ try {
693
+ return fs.readFileSync(file, "utf-8");
694
+ }
695
+ catch {
696
+ return "";
697
+ }
698
+ }
699
+ function readEnvValue(content, key) {
700
+ let found;
701
+ for (const line of content.split(/\r?\n/)) {
702
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/i);
703
+ if (match?.[1] === key) {
704
+ found = match[2].replace(/^["']|["']$/g, "");
705
+ }
706
+ }
707
+ return found;
708
+ }
709
+ function workspaceEnvContent(baseDir) {
710
+ return (readEnvFile(path.join(baseDir, ".env.local")) +
711
+ "\n" +
712
+ readEnvFile(path.join(baseDir, ".env")));
713
+ }
714
+ function localAccessToken(baseDir) {
715
+ const content = workspaceEnvContent(baseDir);
716
+ const single = readEnvValue(content, "ACCESS_TOKEN");
717
+ if (single)
718
+ return single;
719
+ const multi = readEnvValue(content, "ACCESS_TOKENS");
720
+ return multi
721
+ ?.split(",")
722
+ .map((token) => token.trim())
723
+ .find(Boolean);
724
+ }
725
+ function localA2ASecret(baseDir) {
726
+ return (process.env.A2A_SECRET ||
727
+ readEnvValue(workspaceEnvContent(baseDir), "A2A_SECRET"));
728
+ }
729
+ async function mintLocalA2AToken(ownerEmail, baseDir) {
730
+ const secret = ownerEmail ? localA2ASecret(baseDir) : undefined;
731
+ if (!secret)
732
+ return undefined;
733
+ const jose = await import("jose");
734
+ return new jose.SignJWT({ sub: ownerEmail })
735
+ .setProtectedHeader({ alg: "HS256" })
736
+ .setIssuer("agent-native-connect-dev")
737
+ .setIssuedAt()
738
+ .setExpirationTime("30d")
739
+ .sign(new TextEncoder().encode(secret));
740
+ }
741
+ async function devHeadersForApp(params) {
742
+ const ownerEmail = params.ownerEmail ||
743
+ process.env.AGENT_NATIVE_OWNER_EMAIL ||
744
+ ownerEmailFromEntry(params.sourceEntry);
745
+ const headers = {};
746
+ const accessToken = localAccessToken(params.baseDir);
747
+ const a2aToken = accessToken
748
+ ? undefined
749
+ : await mintLocalA2AToken(ownerEmail, params.baseDir);
750
+ if (accessToken || a2aToken) {
751
+ headers.Authorization = `Bearer ${accessToken || a2aToken}`;
752
+ }
753
+ if (ownerEmail) {
754
+ headers["X-Agent-Native-Owner-Email"] = ownerEmail;
755
+ }
756
+ return Object.keys(headers).length ? headers : undefined;
757
+ }
758
+ function connectableApps(includeHidden = false) {
759
+ const source = includeHidden ? TEMPLATES : visibleTemplates();
760
+ return source
761
+ .filter((template) => typeof template.prodUrl === "string")
762
+ .map((template) => ({
763
+ name: template.name,
764
+ label: template.label,
765
+ url: template.prodUrl,
766
+ core: !!template.core,
767
+ }));
768
+ }
769
+ function profileDefaultApps() {
770
+ const core = connectableApps(false).filter((app) => app.core);
771
+ return core.length ? core : connectableApps(false);
772
+ }
773
+ function parseAppsList(value) {
774
+ return (value ?? "")
775
+ .split(",")
776
+ .map((app) => app.trim())
777
+ .filter(Boolean);
778
+ }
779
+ async function resolveProfileApps(parsed, deps) {
780
+ const allVisible = connectableApps(false);
781
+ const allIncludingHidden = connectableApps(true);
782
+ if (parsed.apps) {
783
+ const requested = parseAppsList(parsed.apps);
784
+ if (requested.includes("all"))
785
+ return allVisible;
786
+ const byName = new Map(allIncludingHidden.map((app) => [app.name, app]));
787
+ const unknown = requested.filter((name) => !byName.has(name));
788
+ if (unknown.length) {
789
+ throw new Error(`Unknown app(s): ${unknown.join(", ")}. Known apps: ${allIncludingHidden
790
+ .map((app) => app.name)
791
+ .join(", ")}`);
792
+ }
793
+ return requested.map((name) => byName.get(name));
794
+ }
795
+ if (parsed.all)
796
+ return allVisible;
797
+ if (shouldPrompt(deps)) {
798
+ const prompt = deps.promptHostedApps ?? promptForHostedApps;
799
+ const initialApps = profileDefaultApps().map((app) => app.name);
800
+ const selectedNames = normalizeHostedAppNames(await prompt({ apps: allVisible, initialApps }), allVisible);
801
+ if (selectedNames.length === 0)
802
+ return [];
803
+ const selected = new Set(selectedNames);
804
+ return allVisible.filter((app) => selected.has(app.name));
805
+ }
806
+ return profileDefaultApps();
807
+ }
808
+ function defaultDevGateway() {
809
+ if (process.env.WORKSPACE_GATEWAY_URL)
810
+ return process.env.WORKSPACE_GATEWAY_URL;
811
+ const port = process.env.WORKSPACE_PORT || process.env.PORT;
812
+ return port ? `http://127.0.0.1:${port}` : DEFAULT_DEV_GATEWAY;
813
+ }
814
+ function normalizeDevGateway(parsed) {
815
+ const raw = parsed.gateway ||
816
+ (Number.isFinite(parsed.port) && parsed.port
817
+ ? `http://127.0.0.1:${parsed.port}`
818
+ : defaultDevGateway());
819
+ const normalized = normalizeUrl(raw);
820
+ return normalized.replace(/\/+$/, "");
821
+ }
822
+ async function gatewayAppUrls(gatewayUrl, deps) {
823
+ const out = new Map();
824
+ const fetchImpl = deps.fetchImpl ?? fetch;
825
+ try {
826
+ const response = await fetchImpl(`${gatewayUrl}/_workspace/apps`, {
827
+ signal: AbortSignal.timeout(1200),
828
+ });
829
+ if (!response.ok)
830
+ return out;
831
+ const apps = (await response.json());
832
+ if (!Array.isArray(apps))
833
+ return out;
834
+ for (const app of apps) {
835
+ if (!app || typeof app !== "object")
836
+ continue;
837
+ const id = app.id;
838
+ const url = app.url;
839
+ if (typeof id === "string" && typeof url === "string") {
840
+ out.set(id, normalizeUrl(url));
841
+ }
842
+ }
843
+ }
844
+ catch {
845
+ // The gateway may not be running yet; still write deterministic dev URLs.
846
+ }
847
+ return out;
848
+ }
849
+ function devMcpUrl(app, gatewayUrl, gatewayUrls) {
850
+ const base = gatewayUrls.get(app.name) ?? `${gatewayUrl}/${app.name}`;
851
+ return `${base.replace(/\/+$/, "")}/_agent-native/mcp`;
852
+ }
853
+ function serverNameForApp(app) {
854
+ return `${SERVER_NAME_PREFIX}-${app.name}`;
855
+ }
856
+ async function connectDevProfile(parsed, clients, deps) {
857
+ const apps = await resolveProfileApps(parsed, deps);
858
+ if (!apps || apps.length === 0)
859
+ return true;
860
+ const baseDir = projectBaseDir();
861
+ const scope = parsed.scope === "project" ? "project" : "user";
862
+ const gatewayUrl = normalizeDevGateway(parsed);
863
+ const gatewayUrls = await gatewayAppUrls(gatewayUrl, deps);
864
+ const profilesFile = deps.profilesFile ?? connectProfilesPath();
865
+ const profiles = readConnectProfiles(profilesFile);
866
+ const rows = [];
867
+ const ownerWarnings = new Set();
868
+ for (const app of apps) {
869
+ const serverName = serverNameForApp(app);
870
+ const mcpUrl = devMcpUrl(app, gatewayUrl, gatewayUrls);
871
+ for (const client of clients) {
872
+ const current = readCurrentMcpEntry(client, serverName, baseDir, scope);
873
+ const backup = savedProfileEntry(profiles, serverName, client, current.file);
874
+ if (current.saved && !isLoopbackMcpUrl(savedEntryUrl(current.saved))) {
875
+ setSavedProfileEntry(profiles, serverName, client, current.file, current.saved);
876
+ }
877
+ const sourceEntry = current.saved && !isLoopbackMcpUrl(savedEntryUrl(current.saved))
878
+ ? current.saved
879
+ : backup;
880
+ const headers = await devHeadersForApp({
881
+ ownerEmail: parsed.ownerEmail,
882
+ sourceEntry,
883
+ baseDir,
884
+ });
885
+ if (!headers?.["X-Agent-Native-Owner-Email"]) {
886
+ ownerWarnings.add(app.name);
887
+ }
888
+ const file = writeHttpEntryForClient(client, serverName, mcpUrl, undefined, baseDir, scope, headers);
889
+ rows.push({
890
+ app: app.name,
891
+ client,
892
+ status: "dev",
893
+ file,
894
+ });
895
+ }
896
+ }
897
+ writeConnectProfiles(profilesFile, profiles);
898
+ logOut("");
899
+ logOut(` Switched ${apps.length} app(s) to dev via ${gatewayUrl}`);
900
+ for (const row of rows) {
901
+ logOut(` ${row.app.padEnd(12)} ${row.client.padEnd(18)} ${row.file}`);
902
+ }
903
+ if (ownerWarnings.size) {
904
+ logOut("");
905
+ logOut(` Tip: pass --owner-email <you@example.com> if local tools look sparse ` +
906
+ `for ${Array.from(ownerWarnings).join(", ")}.`);
907
+ }
908
+ logOut("");
909
+ logOut(" Restart your coding agent to pick up the dev MCP servers.");
910
+ return true;
911
+ }
912
+ async function connectProdProfile(parsed, clients, deps) {
913
+ const apps = await resolveProfileApps(parsed, deps);
914
+ if (!apps || apps.length === 0)
915
+ return true;
916
+ const baseDir = projectBaseDir();
917
+ const scope = parsed.scope === "project" ? "project" : "user";
918
+ const profilesFile = deps.profilesFile ?? connectProfilesPath();
919
+ const profiles = readConnectProfiles(profilesFile);
920
+ const restored = [];
921
+ const missing = [];
922
+ for (const app of apps) {
923
+ const serverName = serverNameForApp(app);
924
+ for (const client of clients) {
925
+ const file = configPathFor(client, baseDir, scope);
926
+ const saved = savedProfileEntry(profiles, serverName, client, file);
927
+ if (!saved) {
928
+ missing.push({ app: app.name, client });
929
+ continue;
930
+ }
931
+ writeSavedMcpEntry(client, file, serverName, saved);
932
+ restored.push({ app: app.name, client, file });
933
+ }
934
+ }
935
+ logOut("");
936
+ if (restored.length) {
937
+ logOut(` Restored ${restored.length} production MCP entr${restored.length === 1 ? "y" : "ies"}.`);
938
+ for (const row of restored) {
939
+ logOut(` ${row.app.padEnd(12)} ${row.client.padEnd(18)} ${row.file}`);
940
+ }
941
+ }
942
+ if (missing.length) {
943
+ logOut("");
944
+ logOut(" No saved production entry for:");
945
+ for (const row of missing) {
946
+ const app = apps.find((candidate) => candidate.name === row.app);
947
+ logOut(` ${row.app.padEnd(12)} ${row.client.padEnd(18)} ` +
948
+ `run: agent-native connect ${app?.url ?? "<url>"} --client ${row.client}`);
949
+ }
950
+ }
951
+ logOut("");
952
+ logOut(" Restart your coding agent to pick up the production MCP servers.");
953
+ return missing.length === 0;
954
+ }
486
955
  // ---------------------------------------------------------------------------
487
956
  // Single-app connect
488
957
  // ---------------------------------------------------------------------------
@@ -593,6 +1062,14 @@ Usage:
593
1062
  agent-native connect --all [--client <c>] [--scope user|project]
594
1063
  Connect every first-party hosted app at once.
595
1064
 
1065
+ Developer:
1066
+ agent-native connect dev [--apps mail,calendar] [--client <c>]
1067
+ Switch selected first-party MCP entries to a local dev-lazy gateway.
1068
+ Defaults to ${DEFAULT_DEV_GATEWAY}; override with --gateway or --port.
1069
+
1070
+ agent-native connect prod [--apps mail,calendar] [--client <c>]
1071
+ Restore production MCP entries saved before the dev switch.
1072
+
596
1073
  Clients: all (default), claude-code, claude-code-cli, codex, cowork
597
1074
  Scope: user (default, ~/.claude.json) or project (.mcp.json)`;
598
1075
  /**
@@ -610,6 +1087,17 @@ export async function runConnect(args, deps = {}) {
610
1087
  }
611
1088
  const parsed = parseConnectArgs(args);
612
1089
  try {
1090
+ if (parsed.mode) {
1091
+ const clients = await resolveConnectClients(parsed, deps);
1092
+ if (!clients)
1093
+ return;
1094
+ const ok = parsed.mode === "dev"
1095
+ ? await connectDevProfile(parsed, clients, deps)
1096
+ : await connectProdProfile(parsed, clients, deps);
1097
+ if (!ok)
1098
+ process.exitCode = 1;
1099
+ return;
1100
+ }
613
1101
  if (parsed.all) {
614
1102
  const clients = await resolveConnectClients(parsed, deps);
615
1103
  if (!clients)