@agent-native/core 0.99.1 → 0.99.3

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 (170) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/a2a-protocol.mdx +42 -0
  4. package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  5. package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  6. package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  7. package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  8. package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  9. package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  10. package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  11. package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  12. package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  13. package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/production-agent.ts +12 -38
  16. package/corpus/core/src/audit/config.ts +0 -1
  17. package/corpus/core/src/chat-threads/store.ts +57 -41
  18. package/corpus/core/src/client/AssistantChat.tsx +1 -2
  19. package/corpus/core/src/client/chat/message-components.tsx +15 -3
  20. package/corpus/core/src/client/chat/tool-call-display.tsx +18 -1
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +5 -9
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +0 -133
  23. package/corpus/core/src/client/index.ts +7 -0
  24. package/corpus/core/src/client/settings/DemoModeSection.tsx +9 -65
  25. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  26. package/corpus/core/src/client/use-db-sync.ts +3 -15
  27. package/corpus/core/src/client/use-demo-mode-status.ts +16 -30
  28. package/corpus/core/src/demo/browser-state.ts +47 -0
  29. package/corpus/core/src/demo/fetch-interceptor.ts +12 -59
  30. package/corpus/core/src/deploy/workspace-deploy.ts +2 -2
  31. package/corpus/core/src/server/action-discovery.ts +0 -1
  32. package/corpus/core/src/server/core-routes-plugin.ts +0 -30
  33. package/corpus/core/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  34. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +48 -7
  35. package/corpus/templates/analytics/AGENTS.md +4 -4
  36. package/corpus/templates/analytics/actions/compose-dashboard.ts +12 -1
  37. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +15 -4
  38. package/corpus/templates/analytics/actions/mutate-dashboard.ts +2 -1
  39. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -0
  40. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -27
  41. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -1
  42. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +21 -0
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +14 -0
  44. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +31 -12
  45. package/corpus/templates/analytics/changelog/2026-07-13-dashboard-charts-now-honor-the-selected-time-range-by-defaul.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-13-keep-real-authorized-identities-in-analytics-incident-lookup.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-13-the-analytics-agents-tab-is-available-again-from-the-main-na.md +6 -0
  48. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  49. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +4 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-time-scope.ts +170 -0
  51. package/corpus/templates/analytics/server/lib/error-capture.ts +6 -62
  52. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +20 -5
  53. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +1 -1
  54. package/corpus/templates/analytics/server/lib/session-replay.ts +5 -111
  55. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -0
  56. package/corpus/templates/assets/app/routes/library.tsx +3 -4
  57. package/corpus/templates/assets/changelog/2026-07-13-internal-library-links-open-in-new-tabs.md +6 -0
  58. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +43 -40
  59. package/corpus/templates/calendar/app/pages/ManageBookingPage.tsx +6 -8
  60. package/corpus/templates/calendar/changelog/2026-07-13-booking-links-and-previews-open-in-new-tabs.md +6 -0
  61. package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +4 -7
  62. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +19 -9
  63. package/corpus/templates/clips/app/routes/share.$shareId.tsx +5 -3
  64. package/corpus/templates/clips/changelog/2026-07-13-recording-links-open-in-new-tabs.md +6 -0
  65. package/corpus/templates/content/app/components/editor/ContentReferencePreview.tsx +6 -8
  66. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +5 -3
  67. package/corpus/templates/content/app/components/editor/database/FormView.tsx +6 -9
  68. package/corpus/templates/content/changelog/2026-07-13-document-links-open-in-new-tabs.md +6 -0
  69. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +33 -16
  70. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -1
  71. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +4 -4
  72. package/corpus/templates/design/app/pages/DesignSystems.tsx +8 -10
  73. package/corpus/templates/design/app/pages/Index.tsx +6 -1
  74. package/corpus/templates/design/app/pages/Present.tsx +12 -6
  75. package/corpus/templates/design/changelog/2026-07-13-design-navigation-links-open-in-new-tabs.md +6 -0
  76. package/corpus/templates/design/changelog/2026-07-13-design-session-replays-include-preview-iframes.md +6 -0
  77. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -11
  78. package/corpus/templates/forms/app/pages/FormsListPage.tsx +6 -8
  79. package/corpus/templates/forms/changelog/2026-07-13-form-navigation-links-open-in-new-tabs.md +6 -0
  80. package/corpus/templates/mail/app/pages/DraftQueuePage.tsx +3 -8
  81. package/corpus/templates/mail/changelog/2026-07-13-settings-links-open-in-new-tabs.md +6 -0
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +13 -37
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/audit/config.d.ts.map +1 -1
  86. package/dist/audit/config.js +0 -1
  87. package/dist/audit/config.js.map +1 -1
  88. package/dist/chat-threads/store.d.ts.map +1 -1
  89. package/dist/chat-threads/store.js +47 -33
  90. package/dist/chat-threads/store.js.map +1 -1
  91. package/dist/client/AssistantChat.d.ts.map +1 -1
  92. package/dist/client/AssistantChat.js +1 -1
  93. package/dist/client/AssistantChat.js.map +1 -1
  94. package/dist/client/chat/message-components.d.ts.map +1 -1
  95. package/dist/client/chat/message-components.js +9 -3
  96. package/dist/client/chat/message-components.js.map +1 -1
  97. package/dist/client/chat/tool-call-display.d.ts +3 -1
  98. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  99. package/dist/client/chat/tool-call-display.js +10 -2
  100. package/dist/client/chat/tool-call-display.js.map +1 -1
  101. package/dist/client/extensions/EmbeddedExtension.js +2 -6
  102. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  103. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  104. package/dist/client/extensions/ExtensionViewer.js +2 -59
  105. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  106. package/dist/client/index.d.ts +2 -0
  107. package/dist/client/index.d.ts.map +1 -1
  108. package/dist/client/index.js +2 -0
  109. package/dist/client/index.js.map +1 -1
  110. package/dist/client/settings/DemoModeSection.d.ts +1 -13
  111. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  112. package/dist/client/settings/DemoModeSection.js +5 -52
  113. package/dist/client/settings/DemoModeSection.js.map +1 -1
  114. package/dist/client/settings/SettingsPanel.js +1 -1
  115. package/dist/client/settings/SettingsPanel.js.map +1 -1
  116. package/dist/client/use-db-sync.d.ts.map +1 -1
  117. package/dist/client/use-db-sync.js +3 -7
  118. package/dist/client/use-db-sync.js.map +1 -1
  119. package/dist/client/use-demo-mode-status.d.ts +3 -4
  120. package/dist/client/use-demo-mode-status.d.ts.map +1 -1
  121. package/dist/client/use-demo-mode-status.js +8 -23
  122. package/dist/client/use-demo-mode-status.js.map +1 -1
  123. package/dist/collab/awareness.d.ts +2 -2
  124. package/dist/collab/awareness.d.ts.map +1 -1
  125. package/dist/collab/routes.d.ts +2 -2
  126. package/dist/collab/struct-routes.d.ts +1 -1
  127. package/dist/demo/browser-state.d.ts +10 -0
  128. package/dist/demo/browser-state.d.ts.map +1 -0
  129. package/dist/demo/browser-state.js +49 -0
  130. package/dist/demo/browser-state.js.map +1 -0
  131. package/dist/demo/fetch-interceptor.d.ts +2 -7
  132. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  133. package/dist/demo/fetch-interceptor.js +12 -56
  134. package/dist/demo/fetch-interceptor.js.map +1 -1
  135. package/dist/deploy/workspace-deploy.js +2 -2
  136. package/dist/deploy/workspace-deploy.js.map +1 -1
  137. package/dist/notifications/routes.d.ts +2 -2
  138. package/dist/observability/routes.d.ts +5 -5
  139. package/dist/resources/handlers.d.ts +3 -3
  140. package/dist/secrets/routes.d.ts +9 -9
  141. package/dist/server/action-discovery.d.ts.map +1 -1
  142. package/dist/server/action-discovery.js +0 -1
  143. package/dist/server/action-discovery.js.map +1 -1
  144. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  145. package/dist/server/core-routes-plugin.js +0 -27
  146. package/dist/server/core-routes-plugin.js.map +1 -1
  147. package/dist/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  148. package/docs/content/a2a-protocol.mdx +42 -0
  149. package/docs/content/locales/ar-SA/a2a-protocol.mdx +42 -0
  150. package/docs/content/locales/de-DE/a2a-protocol.mdx +42 -0
  151. package/docs/content/locales/es-ES/a2a-protocol.mdx +42 -0
  152. package/docs/content/locales/fr-FR/a2a-protocol.mdx +42 -0
  153. package/docs/content/locales/hi-IN/a2a-protocol.mdx +42 -0
  154. package/docs/content/locales/ja-JP/a2a-protocol.mdx +42 -0
  155. package/docs/content/locales/ko-KR/a2a-protocol.mdx +42 -0
  156. package/docs/content/locales/pt-BR/a2a-protocol.mdx +42 -0
  157. package/docs/content/locales/zh-CN/a2a-protocol.mdx +42 -0
  158. package/docs/content/locales/zh-TW/a2a-protocol.mdx +42 -0
  159. package/package.json +1 -1
  160. package/src/templates/workspace-core/.agents/skills/native-navigation/SKILL.md +67 -0
  161. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +0 -23
  162. package/corpus/core/src/demo/config.ts +0 -57
  163. package/dist/demo/actions/toggle-demo-mode.d.ts +0 -8
  164. package/dist/demo/actions/toggle-demo-mode.d.ts.map +0 -1
  165. package/dist/demo/actions/toggle-demo-mode.js +0 -21
  166. package/dist/demo/actions/toggle-demo-mode.js.map +0 -1
  167. package/dist/demo/config.d.ts +0 -9
  168. package/dist/demo/config.d.ts.map +0 -1
  169. package/dist/demo/config.js +0 -58
  170. package/dist/demo/config.js.map +0 -1
@@ -1,9 +1,6 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
 
3
- import {
4
- ensureDemoModeFetchInterceptor,
5
- refreshDemoModeFetchInterceptor,
6
- } from "../demo/fetch-interceptor.js";
3
+ import { ensureDemoModeFetchInterceptor } from "../demo/fetch-interceptor.js";
7
4
  import { agentNativePath } from "./api-path.js";
8
5
  import { getBrowserTabId } from "./browser-tab-id.js";
9
6
  import {
@@ -314,15 +311,6 @@ class SyncTransport {
314
311
  // -------------------------------------------------------------------------
315
312
 
316
313
  private fan(events: SyncEvent[], version: number | undefined): void {
317
- if (
318
- events.some(
319
- (event) =>
320
- event.source === "app-state" &&
321
- (event.key === "demo-mode" || event.key === "*"),
322
- )
323
- ) {
324
- void refreshDemoModeFetchInterceptor();
325
- }
326
314
  for (const sub of this.subscribers.values()) {
327
315
  sub.onEvents(events, version);
328
316
  }
@@ -544,8 +532,8 @@ class SyncTransport {
544
532
  };
545
533
 
546
534
  private start(): void {
547
- // Universal demo-mode redaction for the UI. Idempotent + browser-only +
548
- // a no-op until demo mode is on. Lives here because every template root
535
+ // Universal browser-local demo-mode presentation redaction. Idempotent and
536
+ // a no-op until the local preference is on. Lives here because every root
549
537
  // already mounts useDbSync, so this needs zero per-template wiring.
550
538
  ensureEmbedAuthFetchInterceptor();
551
539
  ensureDemoModeFetchInterceptor();
@@ -1,44 +1,30 @@
1
- import { useQuery } from "@tanstack/react-query";
1
+ import { useSyncExternalStore } from "react";
2
2
 
3
- import { agentNativePath } from "./api-path.js";
4
- import { useChangeVersion } from "./use-change-version.js";
5
-
6
- interface DemoModeStatusResponse {
7
- enabled?: boolean;
8
- forced?: boolean;
9
- }
3
+ import {
4
+ getBrowserDemoModeEnabled,
5
+ subscribeToBrowserDemoMode,
6
+ } from "../demo/browser-state.js";
10
7
 
11
8
  export interface DemoModeStatus {
12
9
  enabled: boolean;
13
- forced: boolean;
10
+ forced: false;
14
11
  isLoading: boolean;
15
12
  }
16
13
 
17
- const DEMO_STATUS_URL = agentNativePath("/_agent-native/demo/status");
18
-
19
14
  /**
20
- * Reads the effective Demo mode status for the current user. The shared
21
- * change-version stream advances after Demo mode application state changes,
22
- * so mounted consumers refresh without polling or duplicating the route call.
15
+ * Reads the browser-local Demo mode presentation preference. This deliberately
16
+ * has no backend request: the server and agent always operate on real data.
23
17
  */
24
18
  export function useDemoModeStatus(): DemoModeStatus {
25
- const demoModeVersion = useChangeVersion("app-state:demo-mode");
26
-
27
- const { data, isLoading } = useQuery({
28
- queryKey: ["agent-native", "demo-mode", demoModeVersion],
29
- queryFn: async () => {
30
- const res = await fetch(DEMO_STATUS_URL, {
31
- credentials: "same-origin",
32
- });
33
- if (!res.ok) return null;
34
- return (await res.json()) as DemoModeStatusResponse | null;
35
- },
36
- staleTime: Infinity,
37
- });
19
+ const enabled = useSyncExternalStore(
20
+ subscribeToBrowserDemoMode,
21
+ getBrowserDemoModeEnabled,
22
+ () => false,
23
+ );
38
24
 
39
25
  return {
40
- enabled: data?.enabled === true,
41
- forced: data?.forced === true,
42
- isLoading,
26
+ enabled,
27
+ forced: false,
28
+ isLoading: false,
43
29
  };
44
30
  }
@@ -0,0 +1,47 @@
1
+ /** Browser-local Demo mode state.
2
+ *
3
+ * Demo mode is a presentation preference for this browser only. It must not
4
+ * be persisted in application state or consulted by backend actions.
5
+ */
6
+ export const DEMO_MODE_STORAGE_KEY = "agent-native:demo-mode";
7
+
8
+ const DEMO_MODE_CHANGE_EVENT = "agent-native:demo-mode-change";
9
+
10
+ export function getBrowserDemoModeEnabled(): boolean {
11
+ if (typeof window === "undefined") return false;
12
+ try {
13
+ return window.localStorage.getItem(DEMO_MODE_STORAGE_KEY) === "true";
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ export function setBrowserDemoModeEnabled(enabled: boolean): void {
20
+ if (typeof window === "undefined") return;
21
+ try {
22
+ if (enabled) {
23
+ window.localStorage.setItem(DEMO_MODE_STORAGE_KEY, "true");
24
+ } else {
25
+ window.localStorage.removeItem(DEMO_MODE_STORAGE_KEY);
26
+ }
27
+ } catch {
28
+ // A blocked or unavailable localStorage should not break the settings UI.
29
+ }
30
+ window.dispatchEvent(new Event(DEMO_MODE_CHANGE_EVENT));
31
+ }
32
+
33
+ export function subscribeToBrowserDemoMode(callback: () => void): () => void {
34
+ if (typeof window === "undefined") return () => undefined;
35
+
36
+ const onStorage = (event: StorageEvent) => {
37
+ if (event.key === DEMO_MODE_STORAGE_KEY || event.key === null) callback();
38
+ };
39
+ const onChange = () => callback();
40
+
41
+ window.addEventListener("storage", onStorage);
42
+ window.addEventListener(DEMO_MODE_CHANGE_EVENT, onChange);
43
+ return () => {
44
+ window.removeEventListener("storage", onStorage);
45
+ window.removeEventListener(DEMO_MODE_CHANGE_EVENT, onChange);
46
+ };
47
+ }
@@ -1,4 +1,3 @@
1
- import { agentNativePath } from "../client/api-path.js";
2
1
  /**
3
2
  * Client-side demo-mode redaction.
4
3
  *
@@ -14,8 +13,8 @@ import { agentNativePath } from "../client/api-path.js";
14
13
  * can only ever post-process JSON the app already parses for display, so it
15
14
  * physically cannot break auth, SSE streams, SSR HTML, or binary downloads.
16
15
  *
17
- * The agent is handled separately and in-process (its action tool results are
18
- * redacted in `production-agent.ts`), so it doesn't depend on this at all.
16
+ * The agent transport is intentionally skipped because it carries the real
17
+ * backend results; this interceptor is only a browser presentation transform.
19
18
  *
20
19
  * Scope intentionally narrow:
21
20
  * - Only same-document `GET` requests are redacted. Mutation responses
@@ -23,20 +22,18 @@ import { agentNativePath } from "../client/api-path.js";
23
22
  * typed isn't echoed back as fake data mid-demo.
24
23
  * - Only `application/json` 2xx bodies. Streams (`text/event-stream`),
25
24
  * HTML, and binary are skipped by content-type.
26
- * - Framework infra endpoints (poll, events, the demo-status endpoint
27
- * itself) are skipped — no PII and avoids self-recursion.
25
+ * - Framework infra endpoints (poll and events) are skipped.
28
26
  * - Any error during interception falls back to the original response.
29
27
  */
28
+ import { getBrowserDemoModeEnabled } from "./browser-state.js";
30
29
  import { redactDemoData } from "./redact.js";
31
30
 
32
- const STATUS_PATH = agentNativePath("/_agent-native/demo/status");
33
31
  const SKIP_SUBSTRINGS = [
34
- "/_agent-native/demo/status",
35
32
  "/_agent-native/poll",
36
33
  "/_agent-native/events",
37
- // Never touch agent transport. The agent already gets in-process
38
- // redaction of its tool results; faking its own transcript adds no demo
39
- // value and must stay clear of the tool_use/tool_result protocol. Covers
34
+ // Never touch agent transport. The agent receives real tool results; faking
35
+ // its own transcript adds no demo value and must stay clear of the
36
+ // tool_use/tool_result protocol. Covers
40
37
  // "/_agent-native/agent" (stream) and "/_agent-native/agent-chat"
41
38
  // (thread history) and any sub-paths.
42
39
  "/_agent-native/agent",
@@ -73,16 +70,6 @@ export function shouldSkipDemoResponseRedaction(url: string): boolean {
73
70
  }
74
71
 
75
72
  let installed = false;
76
- let demoEnabled = false;
77
- let originalFetch: typeof fetch | null = null;
78
- let refreshPromise: Promise<void> | null = null;
79
-
80
- // Set once the first demo-status check completes. We DO NOT block requests on
81
- // it — if status isn't known yet a response is simply passed through
82
- // un-redacted (a brief first-paint window) rather than delaying transport.
83
- // Injecting latency into early GETs previously risked the agent's streaming
84
- // reconnect logic; transport safety wins over redacting the first paint.
85
- let firstStatusDone = false;
86
73
 
87
74
  /** Reject after `ms` so a misclassified streaming body can never hang. */
88
75
  function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
@@ -114,49 +101,17 @@ function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
114
101
  return m.toUpperCase();
115
102
  }
116
103
 
117
- async function refreshDemoFlag(): Promise<void> {
118
- const f = originalFetch ?? fetch;
119
- try {
120
- const res = await f(STATUS_PATH, { credentials: "same-origin" });
121
- if (!res.ok) return;
122
- const json = (await res.json()) as {
123
- enabled?: boolean;
124
- forced?: boolean;
125
- } | null;
126
- demoEnabled = json?.enabled === true || json?.forced === true;
127
- } catch {
128
- // Status endpoint unreachable — leave the last known value.
129
- } finally {
130
- firstStatusDone = true;
131
- }
132
- }
133
-
134
- /** Refresh demo mode after the shared DB-sync stream reports a real change. */
135
- export function refreshDemoModeFetchInterceptor(): Promise<void> {
136
- if (!installed) return Promise.resolve();
137
- if (!refreshPromise) {
138
- refreshPromise = refreshDemoFlag().finally(() => {
139
- refreshPromise = null;
140
- });
141
- }
142
- return refreshPromise;
143
- }
144
-
145
104
  /**
146
- * Install the demo-mode fetch interceptor and read demo status once. Later
147
- * changes are driven by the shared DB-sync transport instead of a separate
148
- * fixed polling loop.
105
+ * Install the browser-local demo-mode fetch interceptor.
149
106
  * Idempotent and browser-only — safe to call from any hook that runs in
150
- * every template root (we call it from `useDbSync`). A no-op until demo
151
- * mode is actually on.
107
+ * every template root (we call it from `useDbSync`).
152
108
  */
153
109
  export function ensureDemoModeFetchInterceptor(): void {
154
110
  if (typeof window === "undefined") return;
155
111
  if (installed) return;
156
112
  installed = true;
157
113
 
158
- originalFetch = window.fetch.bind(window);
159
- const base = originalFetch;
114
+ const base = window.fetch.bind(window);
160
115
 
161
116
  window.fetch = async function patchedFetch(
162
117
  input: RequestInfo | URL,
@@ -164,11 +119,11 @@ export function ensureDemoModeFetchInterceptor(): void {
164
119
  ): Promise<Response> {
165
120
  const res = await base(input, init);
166
121
 
167
- // Fast path: anything that isn't a demo-enabled, plain GET returns the
122
+ // Fast path: anything that isn't a browser-local-demo, plain GET returns the
168
123
  // ORIGINAL response with zero body work and zero extra awaits — when
169
124
  // demo mode is off this wrapper is byte-for-byte native fetch, so it
170
125
  // cannot influence agent/run/stream transport.
171
- if (!demoEnabled || !firstStatusDone) return res;
126
+ if (!getBrowserDemoModeEnabled()) return res;
172
127
  if (methodOf(input, init) !== "GET") return res;
173
128
  if (!res.ok) return res;
174
129
 
@@ -217,6 +172,4 @@ export function ensureDemoModeFetchInterceptor(): void {
217
172
  return res;
218
173
  }
219
174
  };
220
-
221
- void refreshDemoModeFetchInterceptor();
222
175
  }
@@ -416,7 +416,7 @@ function writeCloudflareRoutingManifest(distDir: string, apps: string[]): void {
416
416
  const dispatch = apps
417
417
  .map(
418
418
  (a) =>
419
- ` if (pathname === "/${a}" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
419
+ ` if (pathname === "/${a}" || pathname === "/${a}.data" || pathname.startsWith("/${a}/")) return ${moduleIdent(a)}.fetch(requestForMountedApp(request, "/${a}"), env, ctx);`,
420
420
  )
421
421
  .join("\n");
422
422
  const dispatchRootFrameworkRoutes = apps.includes("dispatch")
@@ -880,7 +880,7 @@ function patchNetlifyFunctionEntry(
880
880
  const pathConfig =
881
881
  app === "dispatch"
882
882
  ? ["/_agent-native/*", "/.well-known/*", `${basePath}/*`]
883
- : [basePath, `${basePath}/*`];
883
+ : [basePath, `${basePath}.data`, `${basePath}/*`];
884
884
  const normalizeBasePathHelper =
885
885
  app === "dispatch"
886
886
  ? ""
@@ -599,7 +599,6 @@ export async function mergeCoreSharingActions(
599
599
  "change-appearance",
600
600
  () => import("../appearance/actions/change-appearance.js"),
601
601
  ],
602
- ["toggle-demo-mode", () => import("../demo/actions/toggle-demo-mode.js")],
603
602
  // Audit log — read surface (who changed what, when, agent vs human).
604
603
  [
605
604
  "list-audit-events",
@@ -1205,36 +1205,6 @@ export function createCoreRoutesPlugin(
1205
1205
  }),
1206
1206
  );
1207
1207
 
1208
- // Demo-mode status — read by the client fetch interceptor and the
1209
- // Demo mode settings toggle. `forced` reflects the DEMO_MODE env (a
1210
- // hosted demo deployment); `enabled` ORs that with the per-user
1211
- // application_state toggle. No request-context dependency: the env case
1212
- // needs nothing, and the per-user case resolves the session straight
1213
- // off the request cookie via getSession(event).
1214
- getH3App(nitroApp).use(
1215
- `${P}/demo/status`,
1216
- defineEventHandler(async (event) => {
1217
- const { isDemoModeForced } = await import("../demo/config.js");
1218
- const forced = isDemoModeForced();
1219
- if (forced) return { enabled: true, forced: true };
1220
- try {
1221
- const session = await getSession(event);
1222
- const email = session?.email;
1223
- if (!email) return { enabled: false, forced: false };
1224
- const { appStateGet } =
1225
- await import("../application-state/store.js");
1226
- const state = await appStateGet(email, "demo-mode");
1227
- return {
1228
- enabled:
1229
- (state as { enabled?: boolean } | null)?.enabled === true,
1230
- forced: false,
1231
- };
1232
- } catch {
1233
- return { enabled: false, forced: false };
1234
- }
1235
- }),
1236
- );
1237
-
1238
1208
  // Polling
1239
1209
  getH3App(nitroApp).use(`${P}/poll`, createPollHandler());
1240
1210
 
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: native-navigation
3
+ description: >-
4
+ Use when adding or reviewing UI navigation so internal routes preserve
5
+ normal SPA behavior plus Cmd/Ctrl-click and middle-click new-tab semantics.
6
+ scope: dev
7
+ ---
8
+
9
+ # Native Navigation
10
+
11
+ ## Rule
12
+
13
+ Pure navigation must render a real React Router `<Link>` for same-SPA routes,
14
+ a native `<a href>` for external/public or explicit new-tab destinations, or a
15
+ shadcn `Button asChild` wrapper around one. Do not use a button whose only job
16
+ is to call `navigate()`, `window.open()`, or `window.location`.
17
+
18
+ ## Why
19
+
20
+ Browser modifier-click, middle-click, keyboard activation, accessibility, and
21
+ normal client-side routing come from real links. Imperative JavaScript
22
+ navigation hides the destination and blocks those browser semantics.
23
+
24
+ ## How
25
+
26
+ ```tsx
27
+ import { Link } from "react-router";
28
+
29
+ <Link to={`/items/${item.id}`}>...</Link>
30
+
31
+ <Button asChild>
32
+ <Link to="/settings">Settings</Link>
33
+ </Button>
34
+
35
+ <a href={previewUrl} target="_blank" rel="noopener noreferrer">
36
+ Preview
37
+ </a>
38
+ ```
39
+
40
+ - Use `Link` for same-SPA internal routes.
41
+ - Use `<a>` when the URL is external, crosses an app/public boundary, is an
42
+ auth or sign-in destination, or intentionally opens in a new tab.
43
+ - Preserve query strings and hashes in `to` or `href`.
44
+ - For cards with nested actions, link only the non-interactive card region or
45
+ use a transparent Link overlay; never nest links or buttons.
46
+ - If selection mode changes click behavior, prevent default only for an
47
+ unmodified primary click and preserve modified clicks.
48
+
49
+ Keep imperative navigation for post-mutation redirects, OAuth and popup flows,
50
+ downloads, media/blob URLs, playback, keyboard shortcuts, and stateful tabs.
51
+
52
+ ## Review
53
+
54
+ Search for `onClick={() => navigate`, `window.open`, `window.location`,
55
+ `onAuxClick`, and buttons whose only purpose is routing. For each candidate,
56
+ confirm whether it is pure navigation or a stateful workflow before changing
57
+ it.
58
+
59
+ Verify the rendered DOM exposes an `href`, then test normal click, Cmd/Ctrl-click,
60
+ middle-click, keyboard Enter, and nested controls where applicable. Run oxfmt
61
+ and focused app tests or typechecks for the touched package.
62
+
63
+ ## Related Skills
64
+
65
+ - `client-side-routing`
66
+ - `frontend-design`
67
+ - `shadcn-ui`
@@ -58,6 +58,23 @@ once — do not switch to db-patch or raw SQL.
58
58
 
59
59
  Do not use `app-db` as a dashboard source. For first-party events collected through `/track`, use `source: "first-party"` or the `query-agent-native-analytics` action rather than raw internal `db-query`.
60
60
 
61
+ AI-generated first-party panels are dashboard-time-bound by default. Set
62
+ `config.timeScope` to `"dashboard"` and include the matching dashboard time
63
+ filter in the SQL. The allowed values are:
64
+
65
+ - `dashboard`: use the dashboard-selected time range; the default for ordinary metrics.
66
+ - `fixed-window`: use an explicit bounded window independent of the dashboard filter.
67
+ - `cohort-history`: use the bounded history of an explicitly defined cohort.
68
+ - `all-time`: scan all available history; use only when the user requests it and
69
+ put `all-time`, `lifetime`, or `historical` in the title or description.
70
+
71
+ `{{timeRange}}` requires an explicit matching `filters` entry with
72
+ `id: "timeRange"` and `type: "select"`. `{{<id>Start}}` and `{{<id>End}}`
73
+ require a matching `filters` entry with that id and `type: "date-range"`.
74
+ Do not rely on undeclared time variables. Server validation rejects unbound
75
+ first-party SQL, so declare the filter or choose an explicit non-dashboard
76
+ scope before saving.
77
+
61
78
  ## Creating A Dashboard
62
79
 
63
80
  When the user asks for a dashboard:
@@ -154,7 +171,8 @@ Notes:
154
171
  "source": "first-party",
155
172
  "chartType": "metric",
156
173
  "width": 1,
157
- "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'click'",
174
+ "config": { "timeScope": "dashboard" },
175
+ "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'click' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
158
176
  },
159
177
  {
160
178
  "id": "kpi-signups",
@@ -162,7 +180,8 @@ Notes:
162
180
  "source": "first-party",
163
181
  "chartType": "metric",
164
182
  "width": 1,
165
- "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'signup'",
183
+ "config": { "timeScope": "dashboard" },
184
+ "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'signup' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
166
185
  },
167
186
  {
168
187
  "id": "kpi-active",
@@ -170,7 +189,8 @@ Notes:
170
189
  "source": "first-party",
171
190
  "chartType": "metric",
172
191
  "width": 1,
173
- "sql": "SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events",
192
+ "config": { "timeScope": "dashboard" },
193
+ "sql": "SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
174
194
  },
175
195
  // Section header switches the grid to 2 columns for the panels below it.
176
196
  {
@@ -186,7 +206,8 @@ Notes:
186
206
  "source": "first-party",
187
207
  "chartType": "line",
188
208
  "width": 2,
189
- "sql": "SELECT DATE(timestamp) AS date, COUNT(*) AS value FROM analytics_events WHERE timestamp >= '{{dateStart}}' AND timestamp < '{{dateEnd}}' GROUP BY 1 ORDER BY 1",
209
+ "config": { "timeScope": "dashboard" },
210
+ "sql": "SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY 1 ORDER BY 1",
190
211
  },
191
212
  ],
192
213
  }
@@ -196,6 +217,13 @@ Notes:
196
217
 
197
218
  `filters[]` defines dashboard-wide controls. Filter values are available in panel SQL through `{{var}}` interpolation. Date ranges emit `{{<id>Start}}` and `{{<id>End}}`.
198
219
 
220
+ For dashboard-time-bound first-party SQL, use `config.timeScope: "dashboard"`
221
+ and a predicate that consumes the declared filter, such as
222
+ `event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'`. A
223
+ `{{timeRange}}` token must have a matching select filter and SQL branches for
224
+ its options; date variables must have a matching date-range filter. The server
225
+ rejects unbound first-party SQL during dashboard validation.
226
+
199
227
  **Filter ids must be unique.** Two filters with the same `id` collide on the same URL param, so changing one visibly updates the other in the UI. The dashboard save endpoint rejects duplicates with a 400.
200
228
 
201
229
  **Use `type: "date-range"` for paired start/end dates.** Don't add two `type: "date"` filters labeled "Start" and "End" — even with distinct ids, that ships the wrong UX. A single date-range filter renders as the "from … to …" pair and exposes both halves to SQL via `{{<id>Start}}` / `{{<id>End}}`.
@@ -244,6 +272,17 @@ type DashboardPatch = {
244
272
  variables?: Record<string, string>;
245
273
  };
246
274
 
275
+ type PanelTimeScope =
276
+ | "dashboard"
277
+ | "fixed-window"
278
+ | "cohort-history"
279
+ | "all-time";
280
+
281
+ type PanelConfig = Record<string, unknown> & {
282
+ // Use "dashboard" for AI-generated first-party panels by default.
283
+ timeScope?: PanelTimeScope;
284
+ };
285
+
247
286
  type PanelPatch = {
248
287
  title?: string;
249
288
  sql?: string;
@@ -268,7 +307,7 @@ type PanelPatch = {
268
307
  width?: number;
269
308
  columns?: number;
270
309
  tab?: string;
271
- config?: Record<string, unknown>;
310
+ config?: PanelConfig;
272
311
  description?: string;
273
312
  };
274
313
 
@@ -346,7 +385,9 @@ dashboard
346
385
  source: "first-party",
347
386
  chartType: "metric",
348
387
  width: 1,
349
- sql: "SELECT COUNT(*) AS value FROM analytics_events",
388
+ config: { timeScope: "dashboard" },
389
+ // Assumes filters includes { id: "date", type: "date-range", default: "30d" }.
390
+ sql: "SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
350
391
  })
351
392
  .atTop();
352
393
  ```
@@ -441,7 +482,7 @@ For a **first-party analytics** dashboard, prefer `compose-dashboard` over hand-
441
482
  - **Never hand-author large first-party configs panel-by-panel.** Call `compose-dashboard` with the metric keys instead.
442
483
  - Unknown metric keys are skipped and reported in `unknownMetrics` (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in `invalidMetrics`.
443
484
  - By default (no `overwrite`), composing into an existing dashboard APPENDS the new panels and skips ids already present. `overwrite: true` replaces the whole config.
444
- - Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides.
485
+ - Each metric accepts an optional per-metric `window` of `'30d' | '90d' | 'all'` (only affects windowed virality/time metrics) and `title` / `chartType` / `width` overrides. Request `'all'` only when the user asks for all-time coverage, and describe it as full available history.
445
486
  - Returns `{ dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, skippedExistingIds }` — report `panelCount` as proof-of-done.
446
487
 
447
488
  Available metric keys: `total-signups`, `signups-over-time`, `signups-by-template`, `sessions-by-app`, `sessions-over-time`, `replay-sessions`, `replay-chunks-over-time`, `recent-replay-sessions`, `signed-in-vs-anon`, `total-template-clicks`, `total-demo-clicks`, `total-cli-copies`, `template-interest-over-time`, `clicks-by-template`, `demo-clicks-by-template`, `cli-copies-by-template`, `cli-copies-over-time`, `pageviews-over-time`, `top-referrer-domains`, `referred-signups-30d`, `viral-signup-share-30d`, `clip-share-signups-30d`, `signups-by-referral-source`, `referred-signups-over-time`, `top-referrers`, `share-funnel-30d`, `viral-participation-rate-90d`, `viral-coefficient-90d`, `activated-referrers-90d`.
@@ -119,11 +119,11 @@ details live in `.agents/skills/`.
119
119
  without raw replay events or storage references. Do not add replay blob or
120
120
  dashboard mutation actions to this catalog without an explicit security
121
121
  review.
122
- - While Demo mode is enabled, session and error identities display anonymized
123
- (`anonymous@builder.io`), so incident lookups must filter by the stable
122
+ - Demo mode is a browser-local presentation preference. Analytics backend
123
+ actions, agent/MCP reads, session replays, and error issues always return real
124
+ authorized identities, so incident lookups should use the actual
124
125
  `userId`/email parameter (e.g. `userId: "user@example.com"`) or recording/
125
- issue id — never by matching emails read back from previously displayed
126
- output.
126
+ issue id.
127
127
  - Analytics keeps its direct MCP surface explicitly curated, so external
128
128
  agents should use `ask_app` for multi-step investigation and changes. The
129
129
  six incident reads above are bounded, user/org-scoped fallback tools for
@@ -11,6 +11,7 @@ import {
11
11
  } from "@agent-native/core/server";
12
12
  import { z } from "zod";
13
13
 
14
+ import { validateFirstPartyDashboardTimeScope } from "../server/lib/dashboard-time-scope";
14
15
  import {
15
16
  getDashboard,
16
17
  upsertDashboard,
@@ -138,7 +139,7 @@ export default defineAction({
138
139
  "If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped); with `overwrite: true` the config is replaced. " +
139
140
  "Returns { dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
140
141
  `Available metric keys: ${METRIC_KEYS.join(", ")}. ` +
141
- "Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics).",
142
+ "Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics). Catalog panels are time-scoped by construction; custom first-party SQL added later must use `{{timeRange}}` or declare `config.timeScope`.",
142
143
  schema: z.object({
143
144
  dashboardId: z
144
145
  .string()
@@ -200,6 +201,7 @@ export default defineAction({
200
201
  const unknownMetrics: string[] = [];
201
202
  const invalidMetrics: Array<{ metric: string; reason: string }> = [];
202
203
  const composedPanels: ComposedPanel[] = [];
204
+ const catalogDashboard = withFirstPartyDashboardFilters({ filters: [] });
203
205
 
204
206
  for (const req of requests) {
205
207
  const panel = buildPanel(req.metric, {
@@ -220,6 +222,15 @@ export default defineAction({
220
222
  // rest of the dashboard still builds. (Catalog SQL is known-good, so this
221
223
  // is a defensive net, e.g. if a future window/override produces bad SQL.)
222
224
  try {
225
+ const timeScopeError = validateFirstPartyDashboardTimeScope(
226
+ panel,
227
+ catalogDashboard,
228
+ 0,
229
+ );
230
+ if (timeScopeError) {
231
+ invalidMetrics.push({ metric: req.metric, reason: timeScopeError });
232
+ continue;
233
+ }
223
234
  validateFirstPartyAnalyticsSql(panel.sql);
224
235
  } catch (e: any) {
225
236
  invalidMetrics.push({
@@ -32,6 +32,17 @@ type DashboardPatch = {
32
32
  parentId?: string;
33
33
  };
34
34
 
35
+ type PanelTimeScope =
36
+ | "dashboard"
37
+ | "fixed-window"
38
+ | "cohort-history"
39
+ | "all-time";
40
+
41
+ type PanelConfig = Record<string, unknown> & {
42
+ /** Use "dashboard" for AI-generated first-party panels by default. */
43
+ timeScope?: PanelTimeScope;
44
+ };
45
+
35
46
  type PanelPatch = {
36
47
  title?: string;
37
48
  sql?: string;
@@ -40,7 +51,7 @@ type PanelPatch = {
40
51
  width?: number;
41
52
  columns?: number;
42
53
  tab?: string;
43
- config?: Record<string, unknown>;
54
+ config?: PanelConfig;
44
55
  description?: string; // shorthand for config.description
45
56
  };
46
57
 
@@ -112,9 +123,9 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
112
123
  'dashboard.panelsMatching({"source":"first-party"}).setWidth(2);',
113
124
  'dashboard.panel("retention").setConfigPath("yAxis.format","percent");',
114
125
  'dashboard.section("retention-activity-section").append(["repeat-users","retention-over-time"]);',
115
- 'dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atTop();',
116
- 'dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"sql":"SELECT date, COUNT(*) AS value FROM analytics_events GROUP BY date ORDER BY date"}).nextTo("retention-over-time");',
117
- 'dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"sql":"SELECT name, COUNT(*) AS value FROM analytics_events GROUP BY name"}).atRow(2);',
126
+ `dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'"}).atTop();`,
127
+ `dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY event_date ORDER BY event_date"}).nextTo("retention-over-time");`,
128
+ `dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"config":{"timeScope":"dashboard"},"sql":"SELECT event_name AS name, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY event_name"}).atRow(2);`,
118
129
  'dashboard.insertPanel({"id":"pipeline-widget","title":"Pipeline Widget","chartType":"extension","width":3,"config":{"extensionId":"<existing-extension-id>"}}).atBottom();',
119
130
  ] as const;
120
131
 
@@ -122,6 +122,7 @@ const apiHelp =
122
122
  "Subjects: dashboard.set, dashboard.panel, dashboard.panels, dashboard.panelsMatching, dashboard.section, dashboard.insertPanel. " +
123
123
  "Selection methods: moveToTop, moveToBottom, moveBefore, moveAfter, moveToIndex, moveNextTo, moveToRow, remove, set, setTitle, setSql, setWidth, setConfig, setConfigPath, duplicate. " +
124
124
  "Inserted panels support atTop, atBottom, before, after, atIndex, nextTo, atRow, atRowStart, and atRowEnd. Use nextTo(panelId) or atRow(rowNumber) for visible row placement. " +
125
+ "AI-generated first-party panels are dashboard-time-bound by default: set config.timeScope to dashboard and include a matching dashboard time filter in SQL. Allowed values are dashboard, fixed-window, cohort-history, and all-time; use all-time only when the user requests full available history and put all-time, lifetime, or historical in the title or description. A {{timeRange}} token requires the timeRange select filter; {{<id>Start}}/{{<id>End}} require a matching date-range filter. Server validation rejects unbound first-party SQL. " +
125
126
  `Examples: ${DASHBOARD_MUTATION_EXAMPLES.slice(0, 5).join(" ")}`;
126
127
 
127
128
  function resolveScope() {
@@ -198,7 +199,7 @@ export default defineAction({
198
199
  "Apply general SQL dashboard edits through a small typed mutation API in ONE atomic save. " +
199
200
  "Prefer this for dashboard layout and panel edits: move panels by id, edit titles/SQL/width/config, remove panels, duplicate panels, insert panels, or patch dashboard fields. " +
200
201
  "For user placement requests like 'second row' or 'next to return rates', use row-aware placement such as `dashboard.insertPanel(...).nextTo(\"retention-over-time\")` or `.atRow(2)`, then verify rendered rows from `get-sql-dashboard.layout.groups`. " +
201
- "This is code-shaped but not arbitrary code execution: the server parses the allowed dashboard methods, validates the resulting config with the same invariants as update-dashboard, saves once, syncs collab, and returns compact proof. " +
202
+ "This is code-shaped but not arbitrary code execution: the server parses the allowed dashboard methods, validates the resulting config with the same invariants as update-dashboard, saves once, syncs collab, and returns compact proof. First-party SQL must be explicitly time-bound as described in the API help; server validation rejects unbound first-party SQL. " +
202
203
  "The main code argument is a string, so it avoids brittle JSON-pointer indexes and native-array serialization issues. " +
203
204
  `Common example: ${DASHBOARD_MUTATION_EXAMPLES[0]}`,
204
205
  schema: z.object({