@agent-native/core 0.68.3 → 0.70.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 (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +54 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/changelog/parse.ts +245 -0
  5. package/corpus/core/src/cli/changelog.ts +199 -0
  6. package/corpus/core/src/cli/index.ts +19 -0
  7. package/corpus/core/src/cli/recap.ts +24 -1
  8. package/corpus/core/src/cli/skills.ts +72 -45
  9. package/corpus/core/src/client/CommandMenu.tsx +183 -75
  10. package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
  11. package/corpus/core/src/client/analytics.ts +181 -0
  12. package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
  13. package/corpus/core/src/client/index.ts +11 -0
  14. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
  15. package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
  16. package/corpus/core/src/extensions/web-search-tool.ts +45 -4
  17. package/corpus/core/src/mcp/build-server.ts +1 -1
  18. package/corpus/core/src/mcp/embed-app.ts +112 -6
  19. package/corpus/core/src/secrets/register-framework-secrets.ts +7 -0
  20. package/corpus/core/src/server/attribution.ts +211 -0
  21. package/corpus/core/src/server/auth.ts +6 -0
  22. package/corpus/core/src/server/better-auth-instance.ts +49 -5
  23. package/corpus/core/src/server/builder-browser.ts +27 -1
  24. package/corpus/core/src/server/core-routes-plugin.ts +58 -0
  25. package/corpus/core/src/server/google-oauth.ts +19 -6
  26. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  27. package/corpus/core/src/styles/agent-conversation.css +21 -0
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  30. package/corpus/templates/analytics/CHANGELOG.md +10 -0
  31. package/corpus/templates/analytics/actions/install-dashboard-template.ts +2 -2
  32. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
  33. package/corpus/templates/analytics/actions/update-dashboard.ts +5 -3
  34. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
  35. package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -2
  36. package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
  37. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -213
  38. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -3
  39. package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
  40. package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
  41. package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
  44. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
  45. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
  46. package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
  47. package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
  48. package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
  49. package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
  50. package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
  51. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
  52. package/corpus/templates/analytics/server/db/index.ts +1 -1
  53. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
  54. package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
  55. package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
  56. package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
  57. package/corpus/templates/assets/CHANGELOG.md +10 -0
  58. package/corpus/templates/assets/app/root.tsx +7 -1
  59. package/corpus/templates/assets/app/routes/settings.tsx +6 -0
  60. package/corpus/templates/brain/CHANGELOG.md +10 -0
  61. package/corpus/templates/brain/app/root.tsx +7 -1
  62. package/corpus/templates/brain/app/routes/settings.tsx +8 -1
  63. package/corpus/templates/calendar/CHANGELOG.md +10 -0
  64. package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
  65. package/corpus/templates/calendar/app/root.tsx +7 -1
  66. package/corpus/templates/chat/CHANGELOG.md +10 -0
  67. package/corpus/templates/chat/app/root.tsx +7 -1
  68. package/corpus/templates/clips/CHANGELOG.md +10 -0
  69. package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
  70. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
  71. package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
  72. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +72 -20
  73. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +69 -2
  74. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
  75. package/corpus/templates/clips/app/lib/recorder-preferences.ts +67 -0
  76. package/corpus/templates/clips/app/root.tsx +7 -1
  77. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
  78. package/corpus/templates/clips/app/routes/_app.trash.tsx +21 -0
  79. package/corpus/templates/clips/app/routes/record.tsx +92 -8
  80. package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
  81. package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
  85. package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
  86. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  87. package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
  88. package/corpus/templates/clips/desktop/src/styles.css +49 -0
  89. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  90. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +67 -1
  91. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
  92. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
  93. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +51 -1
  94. package/corpus/templates/clips/shared/share-attribution.ts +79 -0
  95. package/corpus/templates/content/CHANGELOG.md +10 -0
  96. package/corpus/templates/content/app/root.tsx +7 -1
  97. package/corpus/templates/content/vitest.config.ts +3 -0
  98. package/corpus/templates/design/CHANGELOG.md +10 -0
  99. package/corpus/templates/design/actions/generate-design.ts +4 -0
  100. package/corpus/templates/design/app/root.tsx +7 -1
  101. package/corpus/templates/design/app/routes/settings.tsx +10 -2
  102. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  103. package/corpus/templates/dispatch/app/root.tsx +7 -1
  104. package/corpus/templates/forms/CHANGELOG.md +10 -0
  105. package/corpus/templates/forms/app/root.tsx +7 -1
  106. package/corpus/templates/macros/CHANGELOG.md +10 -0
  107. package/corpus/templates/macros/app/root.tsx +7 -1
  108. package/corpus/templates/mail/CHANGELOG.md +10 -0
  109. package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
  110. package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
  111. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
  112. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
  113. package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
  114. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
  115. package/corpus/templates/plan/AGENTS.md +4 -0
  116. package/corpus/templates/plan/CHANGELOG.md +10 -0
  117. package/corpus/templates/plan/actions/view-screen.ts +5 -0
  118. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
  119. package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
  120. package/corpus/templates/plan/app/root.tsx +7 -1
  121. package/corpus/templates/plan/shared/share-attribution.ts +72 -0
  122. package/corpus/templates/slides/CHANGELOG.md +10 -0
  123. package/corpus/templates/slides/app/root.tsx +7 -1
  124. package/corpus/templates/videos/CHANGELOG.md +4 -151
  125. package/corpus/templates/videos/app/root.tsx +7 -1
  126. package/dist/changelog/parse.d.ts +70 -0
  127. package/dist/changelog/parse.d.ts.map +1 -0
  128. package/dist/changelog/parse.js +200 -0
  129. package/dist/changelog/parse.js.map +1 -0
  130. package/dist/cli/changelog.d.ts +4 -0
  131. package/dist/cli/changelog.d.ts.map +1 -0
  132. package/dist/cli/changelog.js +166 -0
  133. package/dist/cli/changelog.js.map +1 -0
  134. package/dist/cli/index.js +18 -0
  135. package/dist/cli/index.js.map +1 -1
  136. package/dist/cli/recap.d.ts.map +1 -1
  137. package/dist/cli/recap.js +22 -1
  138. package/dist/cli/recap.js.map +1 -1
  139. package/dist/cli/skills.d.ts +3 -3
  140. package/dist/cli/skills.d.ts.map +1 -1
  141. package/dist/cli/skills.js +72 -45
  142. package/dist/cli/skills.js.map +1 -1
  143. package/dist/client/CommandMenu.d.ts +17 -1
  144. package/dist/client/CommandMenu.d.ts.map +1 -1
  145. package/dist/client/CommandMenu.js +49 -13
  146. package/dist/client/CommandMenu.js.map +1 -1
  147. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  148. package/dist/client/agent-chat-adapter.js +43 -6
  149. package/dist/client/agent-chat-adapter.js.map +1 -1
  150. package/dist/client/analytics.d.ts +18 -0
  151. package/dist/client/analytics.d.ts.map +1 -1
  152. package/dist/client/analytics.js +177 -0
  153. package/dist/client/analytics.js.map +1 -1
  154. package/dist/client/changelog/Changelog.d.ts +47 -0
  155. package/dist/client/changelog/Changelog.d.ts.map +1 -0
  156. package/dist/client/changelog/Changelog.js +138 -0
  157. package/dist/client/changelog/Changelog.js.map +1 -0
  158. package/dist/client/index.d.ts +2 -1
  159. package/dist/client/index.d.ts.map +1 -1
  160. package/dist/client/index.js +2 -1
  161. package/dist/client/index.js.map +1 -1
  162. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  163. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  164. package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
  165. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  166. package/dist/client/settings/useBuilderStatus.d.ts +2 -0
  167. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  168. package/dist/client/settings/useBuilderStatus.js +58 -10
  169. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  170. package/dist/extensions/web-search-tool.d.ts +5 -3
  171. package/dist/extensions/web-search-tool.d.ts.map +1 -1
  172. package/dist/extensions/web-search-tool.js +36 -4
  173. package/dist/extensions/web-search-tool.js.map +1 -1
  174. package/dist/mcp/build-server.js +1 -1
  175. package/dist/mcp/build-server.js.map +1 -1
  176. package/dist/mcp/embed-app.d.ts.map +1 -1
  177. package/dist/mcp/embed-app.js +112 -6
  178. package/dist/mcp/embed-app.js.map +1 -1
  179. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  180. package/dist/secrets/register-framework-secrets.js +6 -0
  181. package/dist/secrets/register-framework-secrets.js.map +1 -1
  182. package/dist/server/attribution.d.ts +83 -0
  183. package/dist/server/attribution.d.ts.map +1 -0
  184. package/dist/server/attribution.js +178 -0
  185. package/dist/server/attribution.js.map +1 -0
  186. package/dist/server/auth.d.ts.map +1 -1
  187. package/dist/server/auth.js +7 -0
  188. package/dist/server/auth.js.map +1 -1
  189. package/dist/server/better-auth-instance.d.ts +9 -1
  190. package/dist/server/better-auth-instance.d.ts.map +1 -1
  191. package/dist/server/better-auth-instance.js +32 -2
  192. package/dist/server/better-auth-instance.js.map +1 -1
  193. package/dist/server/builder-browser.d.ts +4 -0
  194. package/dist/server/builder-browser.d.ts.map +1 -1
  195. package/dist/server/builder-browser.js +20 -1
  196. package/dist/server/builder-browser.js.map +1 -1
  197. package/dist/server/core-routes-plugin.d.ts +25 -0
  198. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  199. package/dist/server/core-routes-plugin.js +38 -0
  200. package/dist/server/core-routes-plugin.js.map +1 -1
  201. package/dist/server/google-oauth.d.ts.map +1 -1
  202. package/dist/server/google-oauth.js +16 -2
  203. package/dist/server/google-oauth.js.map +1 -1
  204. package/dist/shared/mcp-embed-headers.js +1 -1
  205. package/dist/shared/mcp-embed-headers.js.map +1 -1
  206. package/dist/styles/agent-conversation.css +21 -0
  207. package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  208. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  209. package/package.json +1 -1
  210. package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
  211. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
  212. /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
@@ -195,6 +195,8 @@ const BUILDER_STATE_PARAM = "_an_state";
195
195
  const BUILDER_SIGNUP_SOURCE_PARAM = "signupSource";
196
196
  const BUILDER_AGENT_NATIVE_FLOW_PARAM = "agentNativeFlow";
197
197
  const BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM = "agentNativeConnectSource";
198
+ const BUILDER_AGENT_NATIVE_APP_PARAM = "agentNativeApp";
199
+ const BUILDER_AGENT_NATIVE_TEMPLATE_PARAM = "agentNativeTemplate";
198
200
  const BUILDER_SIGNUP_SOURCE = "agent-native";
199
201
  const STATUS_CONNECT_URL_TTL_MS = 9 * 60 * 1000;
200
202
 
@@ -224,42 +226,102 @@ function inferBuilderConnectTrackingFlow(source: string | undefined): string {
224
226
  return "connect_llm";
225
227
  }
226
228
 
229
+ function normalizeTrackingSlug(
230
+ value: string | null | undefined,
231
+ ): string | null {
232
+ const trimmed = value?.trim();
233
+ if (!trimmed) return null;
234
+ const unscoped = trimmed.startsWith("@")
235
+ ? (trimmed.split("/").pop() ?? trimmed)
236
+ : trimmed;
237
+ const slug = unscoped
238
+ .toLowerCase()
239
+ .replace(/[^a-z0-9]+/g, "-")
240
+ .replace(/^-+|-+$/g, "");
241
+ return slug || null;
242
+ }
243
+
244
+ function inferBuilderConnectTrackingIdentity(options: {
245
+ app?: string;
246
+ template?: string;
247
+ }): { app: string | null; template: string | null } {
248
+ const env = (import.meta.env as Record<string, string | undefined>) ?? {};
249
+ const app =
250
+ normalizeTrackingSlug(options.app) ??
251
+ normalizeTrackingSlug(env.VITE_AGENT_NATIVE_APP) ??
252
+ (typeof window !== "undefined"
253
+ ? normalizeTrackingSlug(window.location.hostname.split(".")[0])
254
+ : null);
255
+ const template =
256
+ normalizeTrackingSlug(options.template) ??
257
+ normalizeTrackingSlug(env.VITE_AGENT_NATIVE_TEMPLATE) ??
258
+ normalizeTrackingSlug(env.VITE_APP_TEMPLATE) ??
259
+ (app?.startsWith("agent-native-")
260
+ ? normalizeTrackingSlug(app.slice("agent-native-".length))
261
+ : app && app !== "localhost"
262
+ ? app
263
+ : null);
264
+
265
+ return { app, template };
266
+ }
267
+
268
+ function applyBuilderConnectTrackingParams(
269
+ params: URLSearchParams,
270
+ tracking: {
271
+ source?: string | null;
272
+ flow: string;
273
+ app?: string | null;
274
+ template?: string | null;
275
+ },
276
+ ) {
277
+ params.set(BUILDER_SIGNUP_SOURCE_PARAM, BUILDER_SIGNUP_SOURCE);
278
+ params.set(BUILDER_AGENT_NATIVE_FLOW_PARAM, tracking.flow);
279
+ if (tracking.source) {
280
+ params.set(BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM, tracking.source);
281
+ }
282
+ if (tracking.app) {
283
+ params.set(BUILDER_AGENT_NATIVE_APP_PARAM, tracking.app);
284
+ }
285
+ if (tracking.template) {
286
+ params.set(BUILDER_AGENT_NATIVE_TEMPLATE_PARAM, tracking.template);
287
+ }
288
+ }
289
+
227
290
  export function withBuilderConnectTrackingParams(
228
291
  url: string,
229
- options: { source?: string; flow?: string } = {},
292
+ options: {
293
+ source?: string;
294
+ flow?: string;
295
+ app?: string;
296
+ template?: string;
297
+ } = {},
230
298
  ): string {
231
299
  const source = cleanTrackingParam(options.source);
232
300
  const flow =
233
301
  cleanTrackingParam(options.flow) ??
234
302
  inferBuilderConnectTrackingFlow(source ?? undefined);
303
+ const { app, template } = inferBuilderConnectTrackingIdentity(options);
235
304
  const origin =
236
305
  typeof window !== "undefined" ? window.location.origin : "http://localhost";
237
306
 
238
307
  try {
239
308
  const parsed = new URL(url, origin);
240
- parsed.searchParams.set(BUILDER_SIGNUP_SOURCE_PARAM, BUILDER_SIGNUP_SOURCE);
241
- parsed.searchParams.set(BUILDER_AGENT_NATIVE_FLOW_PARAM, flow);
242
- if (source) {
243
- parsed.searchParams.set(
244
- BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM,
245
- source,
246
- );
247
- }
309
+ applyBuilderConnectTrackingParams(parsed.searchParams, {
310
+ source,
311
+ flow,
312
+ app,
313
+ template,
314
+ });
248
315
 
249
316
  const redirectUrl = parsed.searchParams.get("redirect_url");
250
317
  if (redirectUrl) {
251
318
  const parsedRedirect = new URL(redirectUrl);
252
- parsedRedirect.searchParams.set(
253
- BUILDER_SIGNUP_SOURCE_PARAM,
254
- BUILDER_SIGNUP_SOURCE,
255
- );
256
- parsedRedirect.searchParams.set(BUILDER_AGENT_NATIVE_FLOW_PARAM, flow);
257
- if (source) {
258
- parsedRedirect.searchParams.set(
259
- BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM,
260
- source,
261
- );
262
- }
319
+ applyBuilderConnectTrackingParams(parsedRedirect.searchParams, {
320
+ source,
321
+ flow,
322
+ app,
323
+ template,
324
+ });
263
325
  parsed.searchParams.set("redirect_url", parsedRedirect.toString());
264
326
  }
265
327
 
@@ -7,13 +7,15 @@
7
7
  * 1. Brave Search API (BRAVE_SEARCH_API_KEY)
8
8
  * 2. Tavily (TAVILY_API_KEY)
9
9
  * 3. Exa (EXA_API_KEY)
10
- * 4. Builder.io (connected Builder credentials)
10
+ * 4. Firecrawl (FIRECRAWL_API_KEY)
11
+ * 5. Builder.io (connected Builder credentials)
11
12
  *
12
13
  * The first configured backend wins. If none is configured, the tool
13
14
  * returns a helpful message telling the user which keys to add.
14
15
  *
15
- * Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, or
16
- * EXA_API_KEY via app secrets settings or environment variables.
16
+ * Connect Builder.io or register BRAVE_SEARCH_API_KEY, TAVILY_API_KEY,
17
+ * EXA_API_KEY, or FIRECRAWL_API_KEY via app secrets settings or environment
18
+ * variables.
17
19
  */
18
20
 
19
21
  import type { ActionEntry } from "../agent/production-agent.js";
@@ -222,6 +224,40 @@ async function searchExa(
222
224
  }));
223
225
  }
224
226
 
227
+ async function searchFirecrawl(
228
+ query: string,
229
+ count: number,
230
+ apiKey: string,
231
+ ): Promise<WebSearchResult[]> {
232
+ const res = await fetch("https://api.firecrawl.dev/v2/search", {
233
+ method: "POST",
234
+ headers: {
235
+ "Content-Type": "application/json",
236
+ Authorization: `Bearer ${apiKey}`,
237
+ },
238
+ body: JSON.stringify({
239
+ query,
240
+ limit: count,
241
+ }),
242
+ signal: AbortSignal.timeout(15_000),
243
+ });
244
+ if (!res.ok) {
245
+ throw new Error(`Firecrawl error ${res.status}: ${await res.text()}`);
246
+ }
247
+ // v2 groups results by source ({ data: { web: [...] } }); older shapes
248
+ // return a flat array ({ data: [...] }). Accept both defensively.
249
+ type FirecrawlResult = { title?: string; url?: string; description?: string };
250
+ const data = (await res.json()) as {
251
+ data?: FirecrawlResult[] | { web?: FirecrawlResult[] };
252
+ };
253
+ const results = Array.isArray(data.data) ? data.data : (data.data?.web ?? []);
254
+ return results.map((r) => ({
255
+ title: r.title ?? "",
256
+ url: r.url ?? "",
257
+ snippet: r.description ?? "",
258
+ }));
259
+ }
260
+
225
261
  async function searchBuilderManaged(
226
262
  query: string,
227
263
  count: number,
@@ -287,7 +323,7 @@ export function createWebSearchToolEntry(
287
323
  "web-search": {
288
324
  tool: {
289
325
  description:
290
- "Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, or EXA_API_KEY.",
326
+ "Search the public web. Use to find API documentation, endpoints, current information, or any topic. Returns ranked results from BYOK providers or a grounded Builder-managed summary. Follow up with web-request or provider-api-docs using responseMode:'markdown' or responseMode:'matches' to fetch clean text, links, or compact snippets from promising URLs. Requires either Connect Builder.io or one of: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, EXA_API_KEY, or FIRECRAWL_API_KEY.",
291
327
  parameters: {
292
328
  type: "object" as const,
293
329
  properties: {
@@ -318,6 +354,7 @@ export function createWebSearchToolEntry(
318
354
  const braveKey = await resolveSearchKey("BRAVE_SEARCH_API_KEY", opts);
319
355
  const tavilyKey = await resolveSearchKey("TAVILY_API_KEY", opts);
320
356
  const exaKey = await resolveSearchKey("EXA_API_KEY", opts);
357
+ const firecrawlKey = await resolveSearchKey("FIRECRAWL_API_KEY", opts);
321
358
  const builderCredentials = await resolveBuilderSearchCredentials(opts);
322
359
 
323
360
  let results: WebSearchResult[];
@@ -334,6 +371,9 @@ export function createWebSearchToolEntry(
334
371
  } else if (exaKey) {
335
372
  results = await searchExa(query, count, exaKey);
336
373
  backend = "Exa";
374
+ } else if (firecrawlKey) {
375
+ results = await searchFirecrawl(query, count, firecrawlKey);
376
+ backend = "Firecrawl";
337
377
  } else if (builderCredentials) {
338
378
  managedText = await searchBuilderManaged(
339
379
  query,
@@ -350,6 +390,7 @@ export function createWebSearchToolEntry(
350
390
  " • BRAVE_SEARCH_API_KEY — https://brave.com/search/api/",
351
391
  " • TAVILY_API_KEY — https://tavily.com/",
352
392
  " • EXA_API_KEY — https://exa.ai/",
393
+ " • FIRECRAWL_API_KEY — https://firecrawl.dev/",
353
394
  ].join("\n");
354
395
  }
355
396
  } catch (err: unknown) {
@@ -708,7 +708,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
708
708
 
709
709
  // ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
710
710
  // when the shared shell changes in a way that must invalidate host caches.
711
- const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v43";
711
+ const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v45";
712
712
 
713
713
  function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
714
714
  const app = safeUiSegment(config.appId ?? config.name, "agent-native");
@@ -455,6 +455,49 @@ export function embedApp(
455
455
  };
456
456
  }
457
457
 
458
+ function externalOpenUrlForAppUrl(appUrl) {
459
+ if (openUrl) return openUrl;
460
+ try {
461
+ const url = new URL(appUrl.href);
462
+ url.searchParams.delete("embedded");
463
+ url.searchParams.delete("__an_embed_token");
464
+ url.searchParams.delete(chatBridgeParam);
465
+ return url.toString();
466
+ } catch (_err) {
467
+ return "";
468
+ }
469
+ }
470
+
471
+ function installExternalOpenControl(appUrl) {
472
+ const href = externalOpenUrlForAppUrl(appUrl);
473
+ if (!href || !document.body || document.getElementById("agent-native-external-open-control")) {
474
+ return;
475
+ }
476
+ const host = document.createElement("div");
477
+ host.id = "agent-native-external-open-control";
478
+ host.style.position = "fixed";
479
+ host.style.top = "10px";
480
+ host.style.right = "10px";
481
+ host.style.zIndex = "2147483647";
482
+ host.style.pointerEvents = "auto";
483
+ const root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host;
484
+ root.innerHTML =
485
+ '<style>' +
486
+ ':host{all:initial;}' +
487
+ 'button{box-sizing:border-box;min-height:30px;border:1px solid color-mix(in srgb, CanvasText 18%, Canvas);border-radius:7px;background:Canvas;color:CanvasText;box-shadow:0 6px 22px color-mix(in srgb, CanvasText 14%, transparent);cursor:pointer;font:700 12px ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:0 10px;white-space:nowrap;}' +
488
+ 'button:hover{background:color-mix(in srgb, CanvasText 6%, Canvas);}' +
489
+ '</style>' +
490
+ '<button type="button">Open in new tab</button>';
491
+ const button = root.querySelector("button");
492
+ if (button) {
493
+ button.addEventListener("click", (event) => {
494
+ event.preventDefault();
495
+ void openHostLink({ url: href });
496
+ });
497
+ }
498
+ document.body.appendChild(host);
499
+ }
500
+
458
501
  function installReactRefreshPreambleFallback() {
459
502
  window.__vite_plugin_react_preamble_installed__ = true;
460
503
  if (typeof window.$RefreshReg$ !== "function") {
@@ -795,6 +838,7 @@ export function embedApp(
795
838
  base.href = config.baseHref;
796
839
  document.head.prepend(base);
797
840
  importChildren(parsed.body, document.body);
841
+ installExternalOpenControl(appUrl);
798
842
  for (const script of scripts) {
799
843
  if (isRunnableClassicScript(script)) runClassicScript(script);
800
844
  }
@@ -987,6 +1031,42 @@ export function embedApp(
987
1031
  }
988
1032
  }
989
1033
 
1034
+ function renderAppLaunchError(message) {
1035
+ clearFrameReadyTimer();
1036
+ clearFrameLoadTimer();
1037
+ appFrame = null;
1038
+ const fallbackCopy = openUrl
1039
+ ? "The inline MCP app could not load in this chat host. You can open the same app route in a new tab or retry the inline load."
1040
+ : "The inline MCP app could not load in this chat host.";
1041
+ const copyHtml = message
1042
+ ? '<div>' + esc(message) + '</div><div>' + esc(fallbackCopy) + '</div>'
1043
+ : esc(fallbackCopy);
1044
+ stage.innerHTML =
1045
+ '<div class="fallback">' +
1046
+ '<div class="fallback-title">App did not load</div>' +
1047
+ '<div class="fallback-copy">' + copyHtml + '</div>' +
1048
+ '<div class="fallback-actions">' +
1049
+ '<button type="button" data-fallback-open>Open in new tab</button>' +
1050
+ '<button type="button" data-fallback-retry>Retry</button>' +
1051
+ '</div>' +
1052
+ (openUrl ? '<a class="fallback-url" href="' + esc(openUrl) + '" target="_blank" rel="noreferrer">' + esc(openUrl) + '</a>' : '') +
1053
+ '</div>';
1054
+ const fallbackOpen = stage.querySelector("[data-fallback-open]");
1055
+ const fallbackRetry = stage.querySelector("[data-fallback-retry]");
1056
+ if (fallbackOpen) {
1057
+ fallbackOpen.disabled = !openUrl;
1058
+ fallbackOpen.onclick = () => {
1059
+ if (openUrl) void openHostLink({ url: openUrl });
1060
+ };
1061
+ }
1062
+ if (fallbackRetry) {
1063
+ fallbackRetry.onclick = () => {
1064
+ startedFor = "";
1065
+ void launchEmbed();
1066
+ };
1067
+ }
1068
+ }
1069
+
990
1070
  async function openFallbackExternal() {
991
1071
  if (!openUrl) return;
992
1072
  let url = withChatBridgeParam(openUrl);
@@ -1051,12 +1131,29 @@ export function embedApp(
1051
1131
  return true;
1052
1132
  }
1053
1133
 
1134
+ // We have connected to a standards-track MCP Apps host (Codex, Cursor,
1135
+ // Claude over the SDK, our own renderer, …) through the postMessage
1136
+ // \`ui/*\` bridge rather than ChatGPT's \`window.openai\` global. These hosts
1137
+ // render the resource in a strict sandboxed iframe (typically
1138
+ // \`sandbox="allow-scripts"\`, opaque origin). Self-navigating that iframe to
1139
+ // the real app origin tears down the host bridge and loses the opaque-origin
1140
+ // auth context, which shows up as a permanent / flashing loading state.
1141
+ // Transplanting the app document into the shell keeps the bridge alive and
1142
+ // works under the opaque origin via embed-token auth, exactly like Claude.
1143
+ function isNativeMcpAppsBridgeHost() {
1144
+ return !!app && !openAiBridge;
1145
+ }
1146
+
1054
1147
  function shouldTransplantAppDocument() {
1055
1148
  const render = renderModeSource();
1056
1149
  const mode = render.mode;
1150
+ if (mode === "iframe" || mode === "nested" || render.frame === "iframe" || render.nested) {
1151
+ return false;
1152
+ }
1057
1153
  return (
1058
1154
  isClaudeMcpContentHost() ||
1059
1155
  isChatGptSandboxHost() ||
1156
+ isNativeMcpAppsBridgeHost() ||
1060
1157
  mode === "transplant" ||
1061
1158
  render.frame === "transplant"
1062
1159
  );
@@ -1074,7 +1171,7 @@ export function embedApp(
1074
1171
  try {
1075
1172
  const host = window.location.hostname || "";
1076
1173
  const appParam = new URL(window.location.href).searchParams.get("app");
1077
- return /^[^.]+\\.web-sandbox\\.oaiusercontent\\.com$/i.test(host) || appParam === "chatgpt";
1174
+ return /^(?:[^.]+\\.)?web-sandbox\\.oaiusercontent\\.com$/i.test(host) || appParam === "chatgpt";
1078
1175
  } catch {
1079
1176
  return false;
1080
1177
  }
@@ -1297,7 +1394,7 @@ export function embedApp(
1297
1394
  async function launchEmbed() {
1298
1395
  const launchUrl = openStartUrl || openUrl;
1299
1396
  if (!launchUrl) {
1300
- setMessage("Open link was not available.");
1397
+ renderAppLaunchError("Open link was not available.");
1301
1398
  return;
1302
1399
  }
1303
1400
  if (!wantsEmbed()) {
@@ -1328,7 +1425,7 @@ export function embedApp(
1328
1425
  const data = parseToolResult(result);
1329
1426
  if (typeof data.startUrl !== "string" || !data.startUrl) {
1330
1427
  startedFor = "";
1331
- setMessage(data.error || "This app can be opened, but not embedded from this MCP server.");
1428
+ renderAppLaunchError(data.error || "This app can be opened, but not embedded from this MCP server.");
1332
1429
  return;
1333
1430
  }
1334
1431
  const startUrl = withChatBridgeParam(data.startUrl);
@@ -1345,7 +1442,7 @@ export function embedApp(
1345
1442
  }
1346
1443
  } catch (err) {
1347
1444
  startedFor = "";
1348
- setMessage(err && err.message ? err.message : "Could not launch embedded app.");
1445
+ renderAppLaunchError(err && err.message ? err.message : "Could not launch embedded app.");
1349
1446
  }
1350
1447
  }
1351
1448
 
@@ -1395,6 +1492,9 @@ export function embedApp(
1395
1492
  try {
1396
1493
  if (new URLSearchParams(window.location.search).get("app") === "chatgpt") return true;
1397
1494
  } catch (_err) {}
1495
+ try {
1496
+ if (/^(?:[^.]+\\.)?web-sandbox\\.oaiusercontent\\.com$/i.test(window.location.hostname || "")) return true;
1497
+ } catch (_err) {}
1398
1498
  try {
1399
1499
  return /chatgpt/i.test(String(navigator.userAgent || ""));
1400
1500
  } catch (_err) {
@@ -1578,8 +1678,13 @@ export function embedApp(
1578
1678
  async openLink(params) {
1579
1679
  const url = typeof (params && params.url) === "string" ? params.url : "";
1580
1680
  if (!url) return { isError: true };
1581
- window.open(url, "_blank", "noopener,noreferrer");
1582
- return { ok: true };
1681
+ try {
1682
+ return await rpcRequest("ui/open-link", { url }, wrapperRequestTimeoutMs);
1683
+ } catch (err) {
1684
+ console.warn("[agent-native] MCP host open-link request failed", err);
1685
+ }
1686
+ const opened = window.open(url, "_blank", "noopener,noreferrer");
1687
+ return opened ? { ok: true } : { isError: true };
1583
1688
  },
1584
1689
  async requestDisplayMode(params) {
1585
1690
  return await rpcRequest("ui/request-display-mode", objectValue(params));
@@ -1615,6 +1720,7 @@ export function embedApp(
1615
1720
  return;
1616
1721
  }
1617
1722
  if (
1723
+ message.method === "ui/notifications/host-context-changed" ||
1618
1724
  message.method === "ui/notifications/host-context" ||
1619
1725
  message.method === "ui/notifications/context"
1620
1726
  ) {
@@ -47,6 +47,13 @@ export function registerFrameworkSecrets(): void {
47
47
  "Enables the web-search agent tool via Exa. Used as fallback when Brave and Tavily are not set and before Builder-managed search.",
48
48
  docsUrl: "https://exa.ai/",
49
49
  },
50
+ {
51
+ key: "FIRECRAWL_API_KEY",
52
+ label: "Firecrawl API Key",
53
+ description:
54
+ "Enables the web-search agent tool via Firecrawl. Used as fallback when Brave, Tavily, and Exa are not set and before Builder-managed search.",
55
+ docsUrl: "https://firecrawl.dev/",
56
+ },
50
57
  ];
51
58
 
52
59
  for (const entry of webSearchKeys) {
@@ -0,0 +1,211 @@
1
+ /**
2
+ * First-touch referral attribution — server side.
3
+ *
4
+ * The browser captures an anonymous visitor's *first* landing context (referral
5
+ * source, UTM params, referring host, landing path) and persists it across the
6
+ * signup boundary in a first-party cookie named `an_ft` (see
7
+ * `client/analytics.ts`). On signup, the auth hook reads that cookie off the
8
+ * request and enriches the canonical server-side `signup` event so we can
9
+ * measure where new users came from and how apps spread (virality).
10
+ *
11
+ * This module is intentionally pure and dependency-free so it is trivially
12
+ * unit-testable and can never throw into the signup path. The single hard rule:
13
+ * parsing untrusted cookie input must NEVER throw — every accessor is defensive.
14
+ */
15
+
16
+ /**
17
+ * The decoded first-touch attribution object. Mirrors the compact JSON the
18
+ * client writes into the `an_ft` cookie / `an_attribution` localStorage key.
19
+ * Every field is optional — the client omits empty fields to keep the cookie
20
+ * small, and a malformed/absent cookie yields `null`.
21
+ */
22
+ export interface FirstTouchAttribution {
23
+ /** Referral source bucket, e.g. "clip_share", "plan_share". */
24
+ ref?: string;
25
+ /** Referrer's stable user id (the clip/plan owner who shared the link). */
26
+ via?: string;
27
+ utm_source?: string;
28
+ utm_medium?: string;
29
+ utm_campaign?: string;
30
+ utm_content?: string;
31
+ utm_term?: string;
32
+ /** `window.location.pathname` of the first page the visitor landed on. */
33
+ landing_path?: string;
34
+ /** Host of `document.referrer` (scrubbed; host only, never a full URL). */
35
+ landing_referrer?: string;
36
+ /** ISO timestamp of when the visitor first landed. */
37
+ landed_at?: string;
38
+ }
39
+
40
+ /** Cookie name written by the client (non-HttpOnly; non-sensitive). */
41
+ export const FIRST_TOUCH_COOKIE_NAME = "an_ft";
42
+
43
+ const STRING_FIELDS: Array<keyof FirstTouchAttribution> = [
44
+ "ref",
45
+ "via",
46
+ "utm_source",
47
+ "utm_medium",
48
+ "utm_campaign",
49
+ "utm_content",
50
+ "utm_term",
51
+ "landing_path",
52
+ "landing_referrer",
53
+ "landed_at",
54
+ ];
55
+
56
+ /**
57
+ * Parse a raw `Cookie:` header into a flat name→value map. Tolerates missing
58
+ * input, extra whitespace, `=` inside values, and malformed pairs. Never throws.
59
+ */
60
+ export function parseCookieHeader(
61
+ cookieHeader: string | null | undefined,
62
+ ): Record<string, string> {
63
+ const out: Record<string, string> = {};
64
+ if (!cookieHeader || typeof cookieHeader !== "string") return out;
65
+ for (const part of cookieHeader.split(";")) {
66
+ const eq = part.indexOf("=");
67
+ if (eq === -1) continue;
68
+ const name = part.slice(0, eq).trim();
69
+ if (!name) continue;
70
+ const rawValue = part.slice(eq + 1).trim();
71
+ // First write wins so a duplicate cookie name can't clobber the first.
72
+ if (name in out) continue;
73
+ out[name] = rawValue;
74
+ }
75
+ return out;
76
+ }
77
+
78
+ /**
79
+ * Decode a single cookie value into a `FirstTouchAttribution`. The value is the
80
+ * URL-encoded compact JSON written by the client. Returns `null` for empty,
81
+ * malformed, or non-object input. Only known string fields are copied through,
82
+ * each clamped to a sane max length as a defense against an oversized cookie.
83
+ */
84
+ export function decodeFirstTouchValue(
85
+ value: string | null | undefined,
86
+ ): FirstTouchAttribution | null {
87
+ if (!value || typeof value !== "string") return null;
88
+ let decoded = value;
89
+ try {
90
+ decoded = decodeURIComponent(value);
91
+ } catch {
92
+ // Not valid percent-encoding — fall back to the raw value and let the JSON
93
+ // parse below decide whether it's usable.
94
+ decoded = value;
95
+ }
96
+ let parsed: unknown;
97
+ try {
98
+ parsed = JSON.parse(decoded);
99
+ } catch {
100
+ return null;
101
+ }
102
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
103
+ return null;
104
+ }
105
+ const source = parsed as Record<string, unknown>;
106
+ const result: FirstTouchAttribution = {};
107
+ let any = false;
108
+ for (const field of STRING_FIELDS) {
109
+ const raw = source[field];
110
+ if (typeof raw === "string" && raw.length > 0) {
111
+ result[field] = raw.slice(0, 120);
112
+ any = true;
113
+ }
114
+ }
115
+ return any ? result : null;
116
+ }
117
+
118
+ /**
119
+ * Read the `an_ft` first-touch attribution out of a raw `Cookie:` header.
120
+ * Returns `null` when the cookie is absent or unparseable. Never throws.
121
+ */
122
+ export function readFirstTouchAttribution(
123
+ cookieHeader: string | null | undefined,
124
+ ): FirstTouchAttribution | null {
125
+ try {
126
+ const cookies = parseCookieHeader(cookieHeader);
127
+ return decodeFirstTouchValue(cookies[FIRST_TOUCH_COOKIE_NAME]);
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function isExternalReferrerHost(host: string | undefined): boolean {
134
+ const trimmed = host?.trim();
135
+ return !!trimmed && trimmed.length > 0;
136
+ }
137
+
138
+ /**
139
+ * Derive the `referral_source` bucket from first-touch attribution per the
140
+ * contract:
141
+ * 1. explicit `ref` wins;
142
+ * 2. landing path under `/share/` => "clip_share";
143
+ * 3. landing path that looks like a public plan page
144
+ * (contains `/p/`, `/plan/`, or `/share-plan/`) => "plan_share";
145
+ * 4. a non-empty external referring host => "external";
146
+ * 5. otherwise => "direct".
147
+ */
148
+ export function deriveReferralSource(ft: FirstTouchAttribution | null): string {
149
+ if (ft?.ref && ft.ref.trim()) return ft.ref.trim();
150
+ const path = ft?.landing_path ?? "";
151
+ if (path.startsWith("/share/")) return "clip_share";
152
+ if (
153
+ path.includes("/p/") ||
154
+ path.includes("/plan/") ||
155
+ path.includes("/share-plan/")
156
+ ) {
157
+ return "plan_share";
158
+ }
159
+ if (isExternalReferrerHost(ft?.landing_referrer)) return "external";
160
+ return "direct";
161
+ }
162
+
163
+ /**
164
+ * Compute the snake_case signup-event properties from first-touch attribution.
165
+ * Returns a clean object with `undefined` values omitted, ready to merge into
166
+ * the `signup` track call. Always sets `referral_source` (defaults to "direct").
167
+ *
168
+ * Pure and total — given any (or no) input it returns a well-formed object and
169
+ * never throws.
170
+ */
171
+ export function deriveSignupAttribution(
172
+ ft: FirstTouchAttribution | null,
173
+ ): Record<string, string> {
174
+ const out: Record<string, string> = {
175
+ referral_source: deriveReferralSource(ft),
176
+ };
177
+ if (!ft) return out;
178
+
179
+ const setIf = (key: string, value: string | undefined) => {
180
+ const trimmed = value?.trim();
181
+ if (trimmed) out[key] = trimmed;
182
+ };
183
+
184
+ setIf("referrer_user", ft.via);
185
+ setIf("referral_medium", ft.utm_medium);
186
+ setIf("referral_campaign", ft.utm_campaign);
187
+ setIf("utm_source", ft.utm_source);
188
+ setIf("utm_medium", ft.utm_medium);
189
+ setIf("utm_campaign", ft.utm_campaign);
190
+ setIf("utm_content", ft.utm_content);
191
+ setIf("utm_term", ft.utm_term);
192
+ setIf("first_touch_path", ft.landing_path);
193
+ setIf("landing_referrer", ft.landing_referrer);
194
+
195
+ return out;
196
+ }
197
+
198
+ /**
199
+ * Convenience: read the cookie header and derive signup attribution in one
200
+ * call. Never throws; falls back to `{ referral_source: "direct" }` on any
201
+ * error.
202
+ */
203
+ export function signupAttributionFromCookieHeader(
204
+ cookieHeader: string | null | undefined,
205
+ ): Record<string, string> {
206
+ try {
207
+ return deriveSignupAttribution(readFirstTouchAttribution(cookieHeader));
208
+ } catch {
209
+ return { referral_source: "direct" };
210
+ }
211
+ }
@@ -1796,6 +1796,12 @@ function createAuthGuardFn(): (
1796
1796
  // route tree, no per-user data.
1797
1797
  if (p === "/__manifest") return;
1798
1798
  if (p === "/_agent-native/speculation-rules.json") return;
1799
+ // Liveness probe: always public so uptime monitors and the keep-warm cron
1800
+ // can reach the DB-warmup route without a session. It exposes no per-user
1801
+ // data (just ok/db/ms) and runs a trivial `SELECT 1`. Without this bypass
1802
+ // the gate below 401s anonymous /_agent-native/* requests before any DB
1803
+ // query, so the database would never get warmed.
1804
+ if (p === "/_agent-native/health") return;
1799
1805
  if (isPublicPath(normalizedUrl, publicPaths)) return;
1800
1806
  if (shouldBypassAuthForBuilderConnect(event, p)) return;
1801
1807
  if (isPublicWorkspacePageRequest(event, p, config)) {