@agent-native/core 0.20.9 → 0.22.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 (196) 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 +18 -3
  16. package/dist/cli/connect.d.ts.map +1 -1
  17. package/dist/cli/connect.js +619 -19
  18. package/dist/cli/connect.js.map +1 -1
  19. package/dist/client/AgentPanel.d.ts.map +1 -1
  20. package/dist/client/AgentPanel.js +6 -2
  21. package/dist/client/AgentPanel.js.map +1 -1
  22. package/dist/client/AssistantChat.d.ts.map +1 -1
  23. package/dist/client/AssistantChat.js +13 -6
  24. package/dist/client/AssistantChat.js.map +1 -1
  25. package/dist/client/NewWorkspaceAppFlow.js +1 -1
  26. package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
  27. package/dist/client/agent-chat.d.ts.map +1 -1
  28. package/dist/client/agent-chat.js +13 -8
  29. package/dist/client/agent-chat.js.map +1 -1
  30. package/dist/client/agent-sidebar-state.d.ts +2 -0
  31. package/dist/client/agent-sidebar-state.d.ts.map +1 -1
  32. package/dist/client/agent-sidebar-state.js +40 -0
  33. package/dist/client/agent-sidebar-state.js.map +1 -1
  34. package/dist/client/code-agent-chat-adapter.js +1 -0
  35. package/dist/client/code-agent-chat-adapter.js.map +1 -1
  36. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  37. package/dist/client/conversation/AgentConversation.js +3 -2
  38. package/dist/client/conversation/AgentConversation.js.map +1 -1
  39. package/dist/client/conversation/code-agent-transcript.js +1 -0
  40. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  41. package/dist/client/conversation/types.d.ts +2 -0
  42. package/dist/client/conversation/types.d.ts.map +1 -1
  43. package/dist/client/conversation/types.js.map +1 -1
  44. package/dist/client/index.d.ts +1 -0
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +1 -0
  47. package/dist/client/index.js.map +1 -1
  48. package/dist/client/mcp-apps/McpAppRenderer.d.ts +10 -0
  49. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -0
  50. package/dist/client/mcp-apps/McpAppRenderer.js +301 -0
  51. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -0
  52. package/dist/client/sse-event-processor.d.ts +3 -0
  53. package/dist/client/sse-event-processor.d.ts.map +1 -1
  54. package/dist/client/sse-event-processor.js +2 -0
  55. package/dist/client/sse-event-processor.js.map +1 -1
  56. package/dist/client/use-db-sync.d.ts +5 -5
  57. package/dist/client/use-db-sync.d.ts.map +1 -1
  58. package/dist/client/use-db-sync.js +15 -5
  59. package/dist/client/use-db-sync.js.map +1 -1
  60. package/dist/client/use-db-sync.spec.d.ts +2 -0
  61. package/dist/client/use-db-sync.spec.d.ts.map +1 -0
  62. package/dist/client/use-db-sync.spec.js +80 -0
  63. package/dist/client/use-db-sync.spec.js.map +1 -0
  64. package/dist/code-agents/transcript-normalizer.d.ts +2 -0
  65. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  66. package/dist/code-agents/transcript-normalizer.js +17 -0
  67. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  68. package/dist/db/client.d.ts.map +1 -1
  69. package/dist/db/client.js +29 -21
  70. package/dist/db/client.js.map +1 -1
  71. package/dist/extensions/actions.d.ts.map +1 -1
  72. package/dist/extensions/actions.js +62 -3
  73. package/dist/extensions/actions.js.map +1 -1
  74. package/dist/extensions/content-patch.d.ts +71 -0
  75. package/dist/extensions/content-patch.d.ts.map +1 -0
  76. package/dist/extensions/content-patch.js +251 -0
  77. package/dist/extensions/content-patch.js.map +1 -0
  78. package/dist/extensions/routes.js +6 -1
  79. package/dist/extensions/routes.js.map +1 -1
  80. package/dist/extensions/store.d.ts +4 -4
  81. package/dist/extensions/store.d.ts.map +1 -1
  82. package/dist/extensions/store.js +14 -18
  83. package/dist/extensions/store.js.map +1 -1
  84. package/dist/index.browser.d.ts +1 -1
  85. package/dist/index.browser.d.ts.map +1 -1
  86. package/dist/index.browser.js +1 -1
  87. package/dist/index.browser.js.map +1 -1
  88. package/dist/index.d.ts +1 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +1 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/mcp/build-server.d.ts +3 -0
  93. package/dist/mcp/build-server.d.ts.map +1 -1
  94. package/dist/mcp/build-server.js +207 -8
  95. package/dist/mcp/build-server.js.map +1 -1
  96. package/dist/mcp/oauth-route.d.ts +22 -0
  97. package/dist/mcp/oauth-route.d.ts.map +1 -0
  98. package/dist/mcp/oauth-route.js +618 -0
  99. package/dist/mcp/oauth-route.js.map +1 -0
  100. package/dist/mcp/oauth-store.d.ts +89 -0
  101. package/dist/mcp/oauth-store.d.ts.map +1 -0
  102. package/dist/mcp/oauth-store.js +391 -0
  103. package/dist/mcp/oauth-store.js.map +1 -0
  104. package/dist/mcp/oauth-token.d.ts +28 -0
  105. package/dist/mcp/oauth-token.d.ts.map +1 -0
  106. package/dist/mcp/oauth-token.js +83 -0
  107. package/dist/mcp/oauth-token.js.map +1 -0
  108. package/dist/mcp/server.d.ts.map +1 -1
  109. package/dist/mcp/server.js +5 -2
  110. package/dist/mcp/server.js.map +1 -1
  111. package/dist/mcp/stdio.d.ts +2 -2
  112. package/dist/mcp/stdio.d.ts.map +1 -1
  113. package/dist/mcp/stdio.js +26 -8
  114. package/dist/mcp/stdio.js.map +1 -1
  115. package/dist/mcp-client/app-result.d.ts +40 -0
  116. package/dist/mcp-client/app-result.d.ts.map +1 -0
  117. package/dist/mcp-client/app-result.js +19 -0
  118. package/dist/mcp-client/app-result.js.map +1 -0
  119. package/dist/mcp-client/index.d.ts +5 -2
  120. package/dist/mcp-client/index.d.ts.map +1 -1
  121. package/dist/mcp-client/index.js +201 -25
  122. package/dist/mcp-client/index.js.map +1 -1
  123. package/dist/mcp-client/manager.d.ts +16 -0
  124. package/dist/mcp-client/manager.d.ts.map +1 -1
  125. package/dist/mcp-client/manager.js +58 -1
  126. package/dist/mcp-client/manager.js.map +1 -1
  127. package/dist/mcp-client/routes.d.ts +4 -1
  128. package/dist/mcp-client/routes.d.ts.map +1 -1
  129. package/dist/mcp-client/routes.js +159 -0
  130. package/dist/mcp-client/routes.js.map +1 -1
  131. package/dist/scripts/dev/shell.d.ts.map +1 -1
  132. package/dist/scripts/dev/shell.js +24 -1
  133. package/dist/scripts/dev/shell.js.map +1 -1
  134. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  135. package/dist/server/agent-chat-plugin.js +3 -2
  136. package/dist/server/agent-chat-plugin.js.map +1 -1
  137. package/dist/server/auth.d.ts.map +1 -1
  138. package/dist/server/auth.js +14 -8
  139. package/dist/server/auth.js.map +1 -1
  140. package/dist/server/builder-browser.d.ts +6 -0
  141. package/dist/server/builder-browser.d.ts.map +1 -1
  142. package/dist/server/builder-browser.js +15 -0
  143. package/dist/server/builder-browser.js.map +1 -1
  144. package/dist/server/core-routes-plugin.d.ts +5 -4
  145. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  146. package/dist/server/core-routes-plugin.js +17 -2
  147. package/dist/server/core-routes-plugin.js.map +1 -1
  148. package/dist/styles/agent-conversation.css +53 -0
  149. package/dist/templates/default/.agents/skills/actions/SKILL.md +193 -72
  150. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +88 -38
  151. package/dist/templates/default/AGENTS.md +3 -3
  152. package/dist/templates/default/actions/hello.ts +13 -20
  153. package/dist/templates/default/actions/navigate.ts +19 -51
  154. package/dist/templates/default/actions/view-screen.ts +16 -33
  155. package/dist/templates/default/app/hooks/use-navigation-state.ts +13 -3
  156. package/dist/templates/default/app/lib/tab-id.ts +1 -0
  157. package/dist/templates/default/app/root.tsx +2 -1
  158. package/dist/templates/default/app/routes/_index.tsx +11 -0
  159. package/dist/templates/default/package.json +2 -1
  160. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +9 -1
  161. package/dist/templates/workspace-core/AGENTS.md +8 -0
  162. package/dist/templates/workspace-root/AGENTS.md +7 -0
  163. package/dist/vite/client.d.ts.map +1 -1
  164. package/dist/vite/client.js +2 -2
  165. package/dist/vite/client.js.map +1 -1
  166. package/docs/content/actions.md +26 -3
  167. package/docs/content/authentication.md +16 -1
  168. package/docs/content/client.md +11 -8
  169. package/docs/content/context-awareness.md +2 -3
  170. package/docs/content/creating-templates.md +2 -2
  171. package/docs/content/external-agents.md +106 -19
  172. package/docs/content/faq.md +2 -2
  173. package/docs/content/key-concepts.md +31 -23
  174. package/docs/content/mcp-clients.md +1 -1
  175. package/docs/content/mcp-protocol.md +65 -27
  176. package/docs/content/template-starter.md +3 -3
  177. package/docs/content/what-is-agent-native.md +4 -2
  178. package/package.json +3 -1
  179. package/src/templates/default/.agents/skills/actions/SKILL.md +193 -72
  180. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +88 -38
  181. package/src/templates/default/AGENTS.md +3 -3
  182. package/src/templates/default/actions/hello.ts +13 -20
  183. package/src/templates/default/actions/navigate.ts +19 -51
  184. package/src/templates/default/actions/view-screen.ts +16 -33
  185. package/src/templates/default/app/hooks/use-navigation-state.ts +13 -3
  186. package/src/templates/default/app/lib/tab-id.ts +1 -0
  187. package/src/templates/default/app/root.tsx +2 -1
  188. package/src/templates/default/app/routes/_index.tsx +11 -0
  189. package/src/templates/default/package.json +2 -1
  190. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +9 -1
  191. package/src/templates/workspace-core/AGENTS.md +8 -0
  192. package/src/templates/workspace-root/AGENTS.md +7 -0
  193. package/dist/templates/default/server/routes/api/hello.get.ts +0 -5
  194. package/dist/templates/default/shared/api.ts +0 -6
  195. package/src/templates/default/server/routes/api/hello.get.ts +0 -5
  196. package/src/templates/default/shared/api.ts +0 -6
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * `agent-native connect <url>` — wire your local Claude Code / Codex / Cowork
3
- * to a DEPLOYED agent-native app using a browser device-code flow. No token
4
- * copying: open the verification URL, approve in the browser, and the minted
5
- * HTTP MCP server entry is written into your client config(s) idempotently.
3
+ * to a DEPLOYED agent-native app. OAuth-capable clients receive a standard
4
+ * remote MCP URL entry and authenticate in the host. Fallback clients use the
5
+ * browser device-code flow: open the verification URL, approve in the browser,
6
+ * and the minted HTTP MCP server entry is written idempotently.
6
7
  *
7
8
  * agent-native connect <url> [--client all|claude-code|claude-code-cli|
8
9
  * codex|cowork] [--scope user|project]
@@ -31,12 +32,15 @@ import os from "node:os";
31
32
  import { spawn } from "node:child_process";
32
33
  import path from "node:path";
33
34
  import { findWorkspaceRoot } from "../mcp/workspace-resolve.js";
34
- import { CLIENTS, writeHttpEntryForClient, } from "./mcp-config-writers.js";
35
- import { visibleTemplates } from "./templates-meta.js";
35
+ import { CLIENTS, configPathFor, writeCodexBlock, writeHttpEntryForClient, writeJsonMcpEntry, } from "./mcp-config-writers.js";
36
+ import { TEMPLATES, visibleTemplates } from "./templates-meta.js";
36
37
  const DEVICE_START_PATH = "/_agent-native/mcp/connect/device/start";
37
38
  const DEVICE_POLL_PATH = "/_agent-native/mcp/connect/device/poll";
39
+ const MCP_PATH = "/_agent-native/mcp";
38
40
  const SERVER_NAME_PREFIX = "agent-native";
39
41
  const CONNECT_PREFERENCES_VERSION = 1;
42
+ const CONNECT_PROFILES_VERSION = 1;
43
+ const DEFAULT_DEV_GATEWAY = "http://127.0.0.1:8080";
40
44
  const CLIENT_LABELS = {
41
45
  "claude-code": "Claude Code",
42
46
  "claude-code-cli": "Claude Code CLI",
@@ -49,6 +53,10 @@ const CLIENT_HINTS = {
49
53
  codex: "~/.codex/config.toml",
50
54
  cowork: "~/.cowork/mcp.json",
51
55
  };
56
+ const REMOTE_MCP_OAUTH_CLIENTS = new Set([
57
+ "claude-code",
58
+ "claude-code-cli",
59
+ ]);
52
60
  function logOut(msg) {
53
61
  process.stdout.write(`${msg}\n`);
54
62
  }
@@ -74,6 +82,16 @@ export function parseConnectArgs(argv) {
74
82
  let v;
75
83
  if (a === "--all")
76
84
  out.all = true;
85
+ else if ((v = eat("--apps")) !== undefined)
86
+ out.apps = v;
87
+ else if ((v = eat("--gateway")) !== undefined)
88
+ out.gateway = v;
89
+ else if ((v = eat("--gateway-url")) !== undefined)
90
+ out.gateway = v;
91
+ else if ((v = eat("--port")) !== undefined)
92
+ out.port = Number(v);
93
+ else if ((v = eat("--owner-email")) !== undefined)
94
+ out.ownerEmail = v;
77
95
  else if ((v = eat("--client")) !== undefined) {
78
96
  out.client = v;
79
97
  out.clientExplicit = true;
@@ -84,8 +102,12 @@ export function parseConnectArgs(argv) {
84
102
  out.name = v;
85
103
  else if ((v = eat("--token")) !== undefined)
86
104
  out.token = v;
87
- else if (!a.startsWith("-") && !out.url)
88
- out.url = a;
105
+ else if (!a.startsWith("-") && !out.url) {
106
+ if (!out.mode && (a === "dev" || a === "prod"))
107
+ out.mode = a;
108
+ else
109
+ out.url = a;
110
+ }
89
111
  }
90
112
  return out;
91
113
  }
@@ -290,6 +312,12 @@ async function resolveHostedAppsFromPrompt(deps) {
290
312
  function clientArgForDeviceFlow(clients) {
291
313
  return clients.length === 1 ? clients[0] : "all";
292
314
  }
315
+ export function supportsRemoteMcpOAuth(client) {
316
+ return REMOTE_MCP_OAUTH_CLIENTS.has(client);
317
+ }
318
+ function clientLabelList(clients) {
319
+ return clients.map((client) => CLIENT_LABELS[client]).join(", ");
320
+ }
293
321
  /** Derive an app slug from a deployed origin, e.g. mail.agent-native.com → mail. */
294
322
  function appSlugFromUrl(url) {
295
323
  try {
@@ -361,6 +389,61 @@ function responseMessage(json, fallback) {
361
389
  : "";
362
390
  return message.trim() || fallback;
363
391
  }
392
+ function stripMcpPath(baseUrl) {
393
+ const parsed = new URL(baseUrl);
394
+ const pathname = parsed.pathname.replace(/\/+$/, "");
395
+ if (pathname === MCP_PATH || pathname.endsWith(MCP_PATH)) {
396
+ parsed.pathname = pathname.slice(0, -MCP_PATH.length) || "/";
397
+ parsed.search = "";
398
+ parsed.hash = "";
399
+ return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "");
400
+ }
401
+ return baseUrl;
402
+ }
403
+ function mcpUrlForBaseUrl(baseUrl) {
404
+ const parsed = new URL(baseUrl);
405
+ const pathname = parsed.pathname.replace(/\/+$/, "");
406
+ if (pathname === MCP_PATH || pathname.endsWith(MCP_PATH)) {
407
+ parsed.pathname = pathname;
408
+ parsed.search = "";
409
+ parsed.hash = "";
410
+ return `${parsed.origin}${parsed.pathname}`;
411
+ }
412
+ return `${baseUrl.replace(/\/+$/, "")}${MCP_PATH}`;
413
+ }
414
+ async function validateOAuthMcpServer(baseUrl, mcpUrl, deps) {
415
+ const fetchImpl = deps.fetchImpl ?? fetch;
416
+ const metadataUrl = `${baseUrl}/.well-known/oauth-protected-resource`;
417
+ const controller = new AbortController();
418
+ const timeout = setTimeout(() => controller.abort(), 15_000);
419
+ try {
420
+ const response = await fetchImpl(metadataUrl, {
421
+ method: "GET",
422
+ headers: { accept: "application/json" },
423
+ signal: controller.signal,
424
+ });
425
+ if (!response.ok) {
426
+ logErr(` Could not validate OAuth MCP support at ${metadataUrl} ` +
427
+ `(HTTP ${response.status}).`);
428
+ return false;
429
+ }
430
+ const metadata = (await response.json().catch(() => null));
431
+ if (metadata?.resource !== mcpUrl) {
432
+ logErr(` ${metadataUrl} did not advertise the expected MCP resource ` +
433
+ `${mcpUrl}.`);
434
+ return false;
435
+ }
436
+ return true;
437
+ }
438
+ catch (err) {
439
+ logErr(` Could not reach ${metadataUrl} (${err?.message ?? err}). ` +
440
+ `Check the URL and your network.`);
441
+ return false;
442
+ }
443
+ finally {
444
+ clearTimeout(timeout);
445
+ }
446
+ }
364
447
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
365
448
  /**
366
449
  * Run the device-code flow against `baseUrl` and return the approved grant.
@@ -483,13 +566,477 @@ export function writeConfigs(clients, serverName, mcpUrl, token, scope, baseDir
483
566
  }
484
567
  return written;
485
568
  }
569
+ export function connectProfilesPath() {
570
+ return path.join(os.homedir(), ".agent-native", "connect-profiles.json");
571
+ }
572
+ function readConnectProfiles(file) {
573
+ try {
574
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
575
+ if (parsed && typeof parsed === "object") {
576
+ return {
577
+ version: Number(parsed.version) || CONNECT_PROFILES_VERSION,
578
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined,
579
+ prodEntries: parsed.prodEntries && typeof parsed.prodEntries === "object"
580
+ ? parsed.prodEntries
581
+ : {},
582
+ };
583
+ }
584
+ }
585
+ catch {
586
+ // no saved profiles yet
587
+ }
588
+ return { version: CONNECT_PROFILES_VERSION, prodEntries: {} };
589
+ }
590
+ function writeConnectProfiles(file, profiles) {
591
+ profiles.version = CONNECT_PROFILES_VERSION;
592
+ profiles.updatedAt = new Date().toISOString();
593
+ fs.mkdirSync(path.dirname(file), { recursive: true });
594
+ fs.writeFileSync(file, JSON.stringify(profiles, null, 2) + "\n", "utf-8");
595
+ }
596
+ function savedProfileEntry(profiles, serverName, client, file) {
597
+ return profiles.prodEntries?.[serverName]?.[client]?.[file];
598
+ }
599
+ function setSavedProfileEntry(profiles, serverName, client, file, entry) {
600
+ profiles.prodEntries ??= {};
601
+ profiles.prodEntries[serverName] ??= {};
602
+ profiles.prodEntries[serverName][client] ??= {};
603
+ profiles.prodEntries[serverName][client][file] = entry;
604
+ }
605
+ function readJsonMcpServerEntry(file, serverName) {
606
+ try {
607
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
608
+ const entry = parsed?.mcpServers?.[serverName];
609
+ return entry && typeof entry === "object" ? entry : undefined;
610
+ }
611
+ catch {
612
+ return undefined;
613
+ }
614
+ }
615
+ function tomlQuoteForRead(s) {
616
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
617
+ }
618
+ function codexHeadersForRead(name) {
619
+ const headers = [`[mcp_servers.${tomlQuoteForRead(name)}]`];
620
+ if (/^[A-Za-z0-9_-]+$/.test(name))
621
+ headers.push(`[mcp_servers.${name}]`);
622
+ return headers;
623
+ }
624
+ function readCodexMcpBlock(file, serverName) {
625
+ let content = "";
626
+ try {
627
+ content = fs.readFileSync(file, "utf-8");
628
+ }
629
+ catch {
630
+ return undefined;
631
+ }
632
+ const headers = new Set(codexHeadersForRead(serverName));
633
+ const lines = content.split(/\r?\n/);
634
+ for (let i = 0; i < lines.length; i++) {
635
+ if (!headers.has(lines[i].trim()))
636
+ continue;
637
+ const block = [lines[i]];
638
+ i++;
639
+ while (i < lines.length && !/^\s*\[/.test(lines[i])) {
640
+ block.push(lines[i]);
641
+ i++;
642
+ }
643
+ return block.join("\n").replace(/\n*$/, "") + "\n";
644
+ }
645
+ return undefined;
646
+ }
647
+ function readCurrentMcpEntry(client, serverName, baseDir, scope) {
648
+ const file = configPathFor(client, baseDir, scope);
649
+ if (client === "codex") {
650
+ const block = readCodexMcpBlock(file, serverName);
651
+ return {
652
+ file,
653
+ saved: block
654
+ ? { kind: "codex", block, savedAt: new Date().toISOString() }
655
+ : undefined,
656
+ };
657
+ }
658
+ const entry = readJsonMcpServerEntry(file, serverName);
659
+ return {
660
+ file,
661
+ saved: entry
662
+ ? { kind: "json", entry, savedAt: new Date().toISOString() }
663
+ : undefined,
664
+ };
665
+ }
666
+ function writeSavedMcpEntry(client, file, serverName, saved) {
667
+ if (client === "codex") {
668
+ if (saved.kind !== "codex")
669
+ return;
670
+ writeCodexBlock(file, serverName, saved.block);
671
+ return;
672
+ }
673
+ if (saved.kind !== "json")
674
+ return;
675
+ writeJsonMcpEntry(file, serverName, saved.entry);
676
+ }
677
+ function unescapeTomlString(value) {
678
+ return value.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
679
+ }
680
+ function parseCodexHeaders(block) {
681
+ const line = block
682
+ .split(/\r?\n/)
683
+ .find((candidate) => /^\s*http_headers\s*=/.test(candidate));
684
+ if (!line)
685
+ return {};
686
+ const match = line.match(/\{(.*)\}/);
687
+ if (!match)
688
+ return {};
689
+ const headers = {};
690
+ const pairRe = /"((?:\\.|[^"])*)"\s*=\s*"((?:\\.|[^"])*)"/g;
691
+ let pair;
692
+ while ((pair = pairRe.exec(match[1]))) {
693
+ headers[unescapeTomlString(pair[1])] = unescapeTomlString(pair[2]);
694
+ }
695
+ return headers;
696
+ }
697
+ function savedEntryUrl(saved) {
698
+ if (!saved)
699
+ return undefined;
700
+ if (saved.kind === "json") {
701
+ return typeof saved.entry.url === "string" ? saved.entry.url : undefined;
702
+ }
703
+ const match = saved.block.match(/^\s*url\s*=\s*"((?:\\.|[^"])*)"/m);
704
+ return match ? unescapeTomlString(match[1]) : undefined;
705
+ }
706
+ function savedEntryHeaders(saved) {
707
+ if (!saved)
708
+ return {};
709
+ if (saved.kind === "json") {
710
+ const headers = saved.entry.headers;
711
+ return headers && typeof headers === "object"
712
+ ? Object.fromEntries(Object.entries(headers)
713
+ .filter((entry) => {
714
+ return typeof entry[1] === "string";
715
+ })
716
+ .map(([key, value]) => [key, value]))
717
+ : {};
718
+ }
719
+ return parseCodexHeaders(saved.block);
720
+ }
721
+ function isLoopbackMcpUrl(value) {
722
+ if (!value)
723
+ return false;
724
+ try {
725
+ const url = new URL(value);
726
+ return (url.hostname === "localhost" ||
727
+ url.hostname === "127.0.0.1" ||
728
+ url.hostname === "::1" ||
729
+ url.hostname.startsWith("127."));
730
+ }
731
+ catch {
732
+ return false;
733
+ }
734
+ }
735
+ function decodeJwtSub(authHeader) {
736
+ if (!authHeader?.startsWith("Bearer "))
737
+ return undefined;
738
+ const token = authHeader.slice("Bearer ".length);
739
+ const [, payload] = token.split(".");
740
+ if (!payload)
741
+ return undefined;
742
+ try {
743
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
744
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
745
+ const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
746
+ return typeof parsed.sub === "string" && parsed.sub.includes("@")
747
+ ? parsed.sub
748
+ : undefined;
749
+ }
750
+ catch {
751
+ return undefined;
752
+ }
753
+ }
754
+ function ownerEmailFromEntry(saved) {
755
+ const headers = savedEntryHeaders(saved);
756
+ return (headers["X-Agent-Native-Owner-Email"] || decodeJwtSub(headers.Authorization));
757
+ }
758
+ function readEnvFile(file) {
759
+ try {
760
+ return fs.readFileSync(file, "utf-8");
761
+ }
762
+ catch {
763
+ return "";
764
+ }
765
+ }
766
+ function readEnvValue(content, key) {
767
+ let found;
768
+ for (const line of content.split(/\r?\n/)) {
769
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/i);
770
+ if (match?.[1] === key) {
771
+ found = match[2].replace(/^["']|["']$/g, "");
772
+ }
773
+ }
774
+ return found;
775
+ }
776
+ function workspaceEnvContent(baseDir) {
777
+ return (readEnvFile(path.join(baseDir, ".env.local")) +
778
+ "\n" +
779
+ readEnvFile(path.join(baseDir, ".env")));
780
+ }
781
+ function localAccessToken(baseDir) {
782
+ const content = workspaceEnvContent(baseDir);
783
+ const single = readEnvValue(content, "ACCESS_TOKEN");
784
+ if (single)
785
+ return single;
786
+ const multi = readEnvValue(content, "ACCESS_TOKENS");
787
+ return multi
788
+ ?.split(",")
789
+ .map((token) => token.trim())
790
+ .find(Boolean);
791
+ }
792
+ function localA2ASecret(baseDir) {
793
+ return (process.env.A2A_SECRET ||
794
+ readEnvValue(workspaceEnvContent(baseDir), "A2A_SECRET"));
795
+ }
796
+ async function mintLocalA2AToken(ownerEmail, baseDir) {
797
+ const secret = ownerEmail ? localA2ASecret(baseDir) : undefined;
798
+ if (!secret)
799
+ return undefined;
800
+ const jose = await import("jose");
801
+ return new jose.SignJWT({ sub: ownerEmail })
802
+ .setProtectedHeader({ alg: "HS256" })
803
+ .setIssuer("agent-native-connect-dev")
804
+ .setIssuedAt()
805
+ .setExpirationTime("30d")
806
+ .sign(new TextEncoder().encode(secret));
807
+ }
808
+ async function devHeadersForApp(params) {
809
+ const ownerEmail = params.ownerEmail ||
810
+ process.env.AGENT_NATIVE_OWNER_EMAIL ||
811
+ ownerEmailFromEntry(params.sourceEntry);
812
+ const headers = {};
813
+ const accessToken = localAccessToken(params.baseDir);
814
+ const a2aToken = accessToken
815
+ ? undefined
816
+ : await mintLocalA2AToken(ownerEmail, params.baseDir);
817
+ if (accessToken || a2aToken) {
818
+ headers.Authorization = `Bearer ${accessToken || a2aToken}`;
819
+ }
820
+ if (ownerEmail) {
821
+ headers["X-Agent-Native-Owner-Email"] = ownerEmail;
822
+ }
823
+ return Object.keys(headers).length ? headers : undefined;
824
+ }
825
+ function connectableApps(includeHidden = false) {
826
+ const source = includeHidden ? TEMPLATES : visibleTemplates();
827
+ return source
828
+ .filter((template) => typeof template.prodUrl === "string")
829
+ .map((template) => ({
830
+ name: template.name,
831
+ label: template.label,
832
+ url: template.prodUrl,
833
+ core: !!template.core,
834
+ }));
835
+ }
836
+ function profileDefaultApps() {
837
+ const core = connectableApps(false).filter((app) => app.core);
838
+ return core.length ? core : connectableApps(false);
839
+ }
840
+ function parseAppsList(value) {
841
+ return (value ?? "")
842
+ .split(",")
843
+ .map((app) => app.trim())
844
+ .filter(Boolean);
845
+ }
846
+ async function resolveProfileApps(parsed, deps) {
847
+ const allVisible = connectableApps(false);
848
+ const allIncludingHidden = connectableApps(true);
849
+ if (parsed.apps) {
850
+ const requested = parseAppsList(parsed.apps);
851
+ if (requested.includes("all"))
852
+ return allVisible;
853
+ const byName = new Map(allIncludingHidden.map((app) => [app.name, app]));
854
+ const unknown = requested.filter((name) => !byName.has(name));
855
+ if (unknown.length) {
856
+ throw new Error(`Unknown app(s): ${unknown.join(", ")}. Known apps: ${allIncludingHidden
857
+ .map((app) => app.name)
858
+ .join(", ")}`);
859
+ }
860
+ return requested.map((name) => byName.get(name));
861
+ }
862
+ if (parsed.all)
863
+ return allVisible;
864
+ if (shouldPrompt(deps)) {
865
+ const prompt = deps.promptHostedApps ?? promptForHostedApps;
866
+ const initialApps = profileDefaultApps().map((app) => app.name);
867
+ const selectedNames = normalizeHostedAppNames(await prompt({ apps: allVisible, initialApps }), allVisible);
868
+ if (selectedNames.length === 0)
869
+ return [];
870
+ const selected = new Set(selectedNames);
871
+ return allVisible.filter((app) => selected.has(app.name));
872
+ }
873
+ return profileDefaultApps();
874
+ }
875
+ function defaultDevGateway() {
876
+ if (process.env.WORKSPACE_GATEWAY_URL)
877
+ return process.env.WORKSPACE_GATEWAY_URL;
878
+ const port = process.env.WORKSPACE_PORT || process.env.PORT;
879
+ return port ? `http://127.0.0.1:${port}` : DEFAULT_DEV_GATEWAY;
880
+ }
881
+ function normalizeDevGateway(parsed) {
882
+ const raw = parsed.gateway ||
883
+ (Number.isFinite(parsed.port) && parsed.port
884
+ ? `http://127.0.0.1:${parsed.port}`
885
+ : defaultDevGateway());
886
+ const normalized = normalizeUrl(raw);
887
+ return normalized.replace(/\/+$/, "");
888
+ }
889
+ async function gatewayAppUrls(gatewayUrl, deps) {
890
+ const out = new Map();
891
+ const fetchImpl = deps.fetchImpl ?? fetch;
892
+ try {
893
+ const response = await fetchImpl(`${gatewayUrl}/_workspace/apps`, {
894
+ signal: AbortSignal.timeout(1200),
895
+ });
896
+ if (!response.ok)
897
+ return out;
898
+ const apps = (await response.json());
899
+ if (!Array.isArray(apps))
900
+ return out;
901
+ for (const app of apps) {
902
+ if (!app || typeof app !== "object")
903
+ continue;
904
+ const id = app.id;
905
+ const url = app.url;
906
+ if (typeof id === "string" && typeof url === "string") {
907
+ out.set(id, normalizeUrl(url));
908
+ }
909
+ }
910
+ }
911
+ catch {
912
+ // The gateway may not be running yet; still write deterministic dev URLs.
913
+ }
914
+ return out;
915
+ }
916
+ function devMcpUrl(app, gatewayUrl, gatewayUrls) {
917
+ const base = gatewayUrls.get(app.name) ?? `${gatewayUrl}/${app.name}`;
918
+ return `${base.replace(/\/+$/, "")}/_agent-native/mcp`;
919
+ }
920
+ function serverNameForApp(app) {
921
+ return `${SERVER_NAME_PREFIX}-${app.name}`;
922
+ }
923
+ async function connectDevProfile(parsed, clients, deps) {
924
+ const apps = await resolveProfileApps(parsed, deps);
925
+ if (!apps || apps.length === 0)
926
+ return true;
927
+ const baseDir = projectBaseDir();
928
+ const scope = parsed.scope === "project" ? "project" : "user";
929
+ const gatewayUrl = normalizeDevGateway(parsed);
930
+ const gatewayUrls = await gatewayAppUrls(gatewayUrl, deps);
931
+ const profilesFile = deps.profilesFile ?? connectProfilesPath();
932
+ const profiles = readConnectProfiles(profilesFile);
933
+ const rows = [];
934
+ const ownerWarnings = new Set();
935
+ for (const app of apps) {
936
+ const serverName = serverNameForApp(app);
937
+ const mcpUrl = devMcpUrl(app, gatewayUrl, gatewayUrls);
938
+ for (const client of clients) {
939
+ const current = readCurrentMcpEntry(client, serverName, baseDir, scope);
940
+ const backup = savedProfileEntry(profiles, serverName, client, current.file);
941
+ if (current.saved && !isLoopbackMcpUrl(savedEntryUrl(current.saved))) {
942
+ setSavedProfileEntry(profiles, serverName, client, current.file, current.saved);
943
+ }
944
+ const sourceEntry = current.saved && !isLoopbackMcpUrl(savedEntryUrl(current.saved))
945
+ ? current.saved
946
+ : backup;
947
+ const headers = await devHeadersForApp({
948
+ ownerEmail: parsed.ownerEmail,
949
+ sourceEntry,
950
+ baseDir,
951
+ });
952
+ if (!headers?.["X-Agent-Native-Owner-Email"]) {
953
+ ownerWarnings.add(app.name);
954
+ }
955
+ const file = writeHttpEntryForClient(client, serverName, mcpUrl, undefined, baseDir, scope, headers);
956
+ rows.push({
957
+ app: app.name,
958
+ client,
959
+ status: "dev",
960
+ file,
961
+ });
962
+ }
963
+ }
964
+ writeConnectProfiles(profilesFile, profiles);
965
+ logOut("");
966
+ logOut(` Switched ${apps.length} app(s) to dev via ${gatewayUrl}`);
967
+ for (const row of rows) {
968
+ logOut(` ${row.app.padEnd(12)} ${row.client.padEnd(18)} ${row.file}`);
969
+ }
970
+ if (ownerWarnings.size) {
971
+ logOut("");
972
+ logOut(` Tip: pass --owner-email <you@example.com> if local tools look sparse ` +
973
+ `for ${Array.from(ownerWarnings).join(", ")}.`);
974
+ }
975
+ logOut("");
976
+ logOut(" Restart your coding agent to pick up the dev MCP servers.");
977
+ return true;
978
+ }
979
+ async function connectProdProfile(parsed, clients, deps) {
980
+ const apps = await resolveProfileApps(parsed, deps);
981
+ if (!apps || apps.length === 0)
982
+ return true;
983
+ const baseDir = projectBaseDir();
984
+ const scope = parsed.scope === "project" ? "project" : "user";
985
+ const profilesFile = deps.profilesFile ?? connectProfilesPath();
986
+ const profiles = readConnectProfiles(profilesFile);
987
+ const restored = [];
988
+ const missing = [];
989
+ for (const app of apps) {
990
+ const serverName = serverNameForApp(app);
991
+ for (const client of clients) {
992
+ const file = configPathFor(client, baseDir, scope);
993
+ const saved = savedProfileEntry(profiles, serverName, client, file);
994
+ if (!saved) {
995
+ missing.push({ app: app.name, client });
996
+ continue;
997
+ }
998
+ writeSavedMcpEntry(client, file, serverName, saved);
999
+ restored.push({ app: app.name, client, file });
1000
+ }
1001
+ }
1002
+ logOut("");
1003
+ if (restored.length) {
1004
+ logOut(` Restored ${restored.length} production MCP entr${restored.length === 1 ? "y" : "ies"}.`);
1005
+ for (const row of restored) {
1006
+ logOut(` ${row.app.padEnd(12)} ${row.client.padEnd(18)} ${row.file}`);
1007
+ }
1008
+ }
1009
+ if (missing.length) {
1010
+ logOut("");
1011
+ logOut(" No saved production entry for:");
1012
+ for (const row of missing) {
1013
+ const app = apps.find((candidate) => candidate.name === row.app);
1014
+ logOut(` ${row.app.padEnd(12)} ${row.client.padEnd(18)} ` +
1015
+ `run: agent-native connect ${app?.url ?? "<url>"} --client ${row.client}`);
1016
+ }
1017
+ }
1018
+ logOut("");
1019
+ logOut(" Restart your coding agent to pick up the production MCP servers.");
1020
+ return missing.length === 0;
1021
+ }
486
1022
  // ---------------------------------------------------------------------------
487
1023
  // Single-app connect
488
1024
  // ---------------------------------------------------------------------------
489
1025
  async function connectOne(rawUrl, parsed, clients, deps) {
490
- const baseUrl = normalizeUrl(rawUrl);
1026
+ const normalizedUrl = normalizeUrl(rawUrl);
1027
+ const baseUrl = stripMcpPath(normalizedUrl);
1028
+ const normalizedMcpUrl = mcpUrlForBaseUrl(normalizedUrl);
491
1029
  const appSlug = appSlugFromUrl(baseUrl);
492
1030
  const scope = parsed.scope === "user" ? "user" : "project";
1031
+ const baseDir = projectBaseDir();
1032
+ const allWritten = [];
1033
+ const oauthClients = parsed.token
1034
+ ? []
1035
+ : clients.filter((client) => supportsRemoteMcpOAuth(client));
1036
+ const deviceFlowClients = parsed.token
1037
+ ? clients
1038
+ : clients.filter((client) => !supportsRemoteMcpOAuth(client));
1039
+ const oauthMigrations = [];
493
1040
  let token;
494
1041
  let mcpUrl;
495
1042
  let serverName;
@@ -497,13 +1044,18 @@ async function connectOne(rawUrl, parsed, clients, deps) {
497
1044
  if (parsed.token) {
498
1045
  // No-browser fallback: skip the device flow entirely.
499
1046
  token = parsed.token;
500
- mcpUrl = `${baseUrl}/_agent-native/mcp`;
1047
+ mcpUrl = normalizedMcpUrl;
501
1048
  serverName = parsed.name ?? defaultServerName(baseUrl);
502
1049
  logOut("");
503
1050
  logOut(` Using supplied --token for ${baseUrl} (skipping browser flow).`);
504
1051
  }
1052
+ else if (deviceFlowClients.length === 0) {
1053
+ token = undefined;
1054
+ mcpUrl = normalizedMcpUrl;
1055
+ serverName = parsed.name ?? defaultServerName(baseUrl);
1056
+ }
505
1057
  else {
506
- const grant = await runDeviceFlow(baseUrl, appSlug, clientArgForDeviceFlow(clients), deps);
1058
+ const grant = await runDeviceFlow(baseUrl, appSlug, clientArgForDeviceFlow(deviceFlowClients), deps);
507
1059
  if (!grant)
508
1060
  return { ok: false };
509
1061
  token = grant.token;
@@ -511,15 +1063,40 @@ async function connectOne(rawUrl, parsed, clients, deps) {
511
1063
  serverName = parsed.name ?? grant.serverName ?? defaultServerName(baseUrl);
512
1064
  headers = grant.headers;
513
1065
  }
514
- const written = writeConfigs(clients, serverName, mcpUrl, token, scope, undefined, headers);
1066
+ if (oauthClients.length > 0 && !parsed.token) {
1067
+ if (!(await validateOAuthMcpServer(baseUrl, mcpUrl, deps))) {
1068
+ return { ok: false };
1069
+ }
1070
+ }
1071
+ if (deviceFlowClients.length > 0) {
1072
+ allWritten.push(...writeConfigs(deviceFlowClients, serverName, mcpUrl, token, scope, baseDir, headers));
1073
+ }
1074
+ if (oauthClients.length > 0) {
1075
+ for (const client of oauthClients) {
1076
+ const current = readCurrentMcpEntry(client, serverName, baseDir, scope);
1077
+ const currentHeaders = savedEntryHeaders(current.saved);
1078
+ if (typeof currentHeaders.Authorization === "string") {
1079
+ oauthMigrations.push(client);
1080
+ }
1081
+ }
1082
+ allWritten.push(...writeConfigs(oauthClients, serverName, mcpUrl, undefined, scope, baseDir, undefined));
1083
+ }
515
1084
  logOut("");
516
- logOut(` Connected "${serverName}" → ${mcpUrl}`);
517
- for (const w of written) {
1085
+ logOut(` Configured "${serverName}" → ${mcpUrl}`);
1086
+ for (const w of allWritten) {
518
1087
  logOut(` ${w.client.padEnd(18)} ${w.file}`);
519
1088
  }
1089
+ if (oauthClients.length > 0 && !parsed.token) {
1090
+ logOut("");
1091
+ if (oauthMigrations.length > 0) {
1092
+ logOut(` Replaced legacy bearer headers for ${clientLabelList(oauthMigrations)}; it will reconnect with standard MCP OAuth.`);
1093
+ }
1094
+ logOut(` ${clientLabelList(oauthClients)}: wrote URL-only MCP config (no bearer headers).`);
1095
+ logOut(" Next: restart Claude Code, run /mcp, and choose Authenticate.");
1096
+ }
520
1097
  logOut("");
521
1098
  logOut(" Restart your coding agent to pick up the new MCP server.");
522
- return { ok: true, serverName, files: written.map((w) => w.file) };
1099
+ return { ok: true, serverName, files: allWritten.map((w) => w.file) };
523
1100
  }
524
1101
  // ---------------------------------------------------------------------------
525
1102
  // --all : connect every first-party hosted app
@@ -580,11 +1157,15 @@ Usage:
580
1157
  (mail.agent-native.com, calendar.agent-native.com, and friends).
581
1158
 
582
1159
  agent-native connect <url> [--client <c>] [--scope user|project] [--name <n>]
583
- Browser device-code flow. Prints a code, opens the verification URL,
584
- polls until approved, then writes the HTTP MCP entry into your
585
- selected client config(s). With no --client, opens a brief picker
586
- preselected from ~/.agent-native/connect.json, or all clients on first
587
- run. Idempotent re-running replaces the same entry.
1160
+ Writes the HTTP MCP entry into your selected client config(s). Claude
1161
+ Code / Claude Code CLI use standard remote MCP OAuth: restart Claude,
1162
+ run /mcp, and choose Authenticate. Codex / Cowork use the browser
1163
+ device-code fallback: the command prints a code, opens the verification
1164
+ URL, polls until approved, then writes bearer headers. With no --client,
1165
+ opens a brief picker preselected from ~/.agent-native/connect.json, or
1166
+ all clients on first run. Idempotent — re-running replaces the same entry.
1167
+ Re-running over an older Claude bearer entry upgrades it to URL-only
1168
+ OAuth config and prompts you to authenticate with /mcp.
588
1169
 
589
1170
  agent-native connect <url> --token <token>
590
1171
  No-browser fallback. Skip the device flow and write the entry with
@@ -593,6 +1174,14 @@ Usage:
593
1174
  agent-native connect --all [--client <c>] [--scope user|project]
594
1175
  Connect every first-party hosted app at once.
595
1176
 
1177
+ Developer:
1178
+ agent-native connect dev [--apps mail,calendar] [--client <c>]
1179
+ Switch selected first-party MCP entries to a local dev-lazy gateway.
1180
+ Defaults to ${DEFAULT_DEV_GATEWAY}; override with --gateway or --port.
1181
+
1182
+ agent-native connect prod [--apps mail,calendar] [--client <c>]
1183
+ Restore production MCP entries saved before the dev switch.
1184
+
596
1185
  Clients: all (default), claude-code, claude-code-cli, codex, cowork
597
1186
  Scope: user (default, ~/.claude.json) or project (.mcp.json)`;
598
1187
  /**
@@ -610,6 +1199,17 @@ export async function runConnect(args, deps = {}) {
610
1199
  }
611
1200
  const parsed = parseConnectArgs(args);
612
1201
  try {
1202
+ if (parsed.mode) {
1203
+ const clients = await resolveConnectClients(parsed, deps);
1204
+ if (!clients)
1205
+ return;
1206
+ const ok = parsed.mode === "dev"
1207
+ ? await connectDevProfile(parsed, clients, deps)
1208
+ : await connectProdProfile(parsed, clients, deps);
1209
+ if (!ok)
1210
+ process.exitCode = 1;
1211
+ return;
1212
+ }
613
1213
  if (parsed.all) {
614
1214
  const clients = await resolveConnectClients(parsed, deps);
615
1215
  if (!clients)