@agent-native/core 0.7.1 → 0.7.2

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 (242) hide show
  1. package/dist/action.d.ts +8 -0
  2. package/dist/action.d.ts.map +1 -1
  3. package/dist/action.js +18 -0
  4. package/dist/action.js.map +1 -1
  5. package/dist/agent/production-agent.d.ts +9 -0
  6. package/dist/agent/production-agent.d.ts.map +1 -1
  7. package/dist/agent/production-agent.js +148 -36
  8. package/dist/agent/production-agent.js.map +1 -1
  9. package/dist/agent/run-manager.d.ts.map +1 -1
  10. package/dist/agent/run-manager.js +20 -1
  11. package/dist/agent/run-manager.js.map +1 -1
  12. package/dist/agent/run-store.d.ts +14 -0
  13. package/dist/agent/run-store.d.ts.map +1 -1
  14. package/dist/agent/run-store.js +63 -6
  15. package/dist/agent/run-store.js.map +1 -1
  16. package/dist/agent/types.d.ts +2 -0
  17. package/dist/agent/types.d.ts.map +1 -1
  18. package/dist/cli/create.js +1 -1
  19. package/dist/cli/create.js.map +1 -1
  20. package/dist/cli/setup-agents.d.ts.map +1 -1
  21. package/dist/cli/setup-agents.js +0 -2
  22. package/dist/cli/setup-agents.js.map +1 -1
  23. package/dist/cli/templates-meta.d.ts +52 -0
  24. package/dist/cli/templates-meta.d.ts.map +1 -0
  25. package/dist/cli/templates-meta.js +165 -0
  26. package/dist/cli/templates-meta.js.map +1 -0
  27. package/dist/client/AgentPanel.d.ts +5 -1
  28. package/dist/client/AgentPanel.d.ts.map +1 -1
  29. package/dist/client/AgentPanel.js +279 -30
  30. package/dist/client/AgentPanel.js.map +1 -1
  31. package/dist/client/AssistantChat.d.ts +2 -1
  32. package/dist/client/AssistantChat.d.ts.map +1 -1
  33. package/dist/client/AssistantChat.js +172 -40
  34. package/dist/client/AssistantChat.js.map +1 -1
  35. package/dist/client/ConnectBuilderCard.d.ts +21 -0
  36. package/dist/client/ConnectBuilderCard.d.ts.map +1 -0
  37. package/dist/client/ConnectBuilderCard.js +196 -0
  38. package/dist/client/ConnectBuilderCard.js.map +1 -0
  39. package/dist/client/FeedbackButton.d.ts +15 -0
  40. package/dist/client/FeedbackButton.d.ts.map +1 -0
  41. package/dist/client/FeedbackButton.js +72 -0
  42. package/dist/client/FeedbackButton.js.map +1 -0
  43. package/dist/client/IframeEmbed.d.ts +17 -0
  44. package/dist/client/IframeEmbed.d.ts.map +1 -0
  45. package/dist/client/IframeEmbed.js +108 -0
  46. package/dist/client/IframeEmbed.js.map +1 -0
  47. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  48. package/dist/client/MultiTabAssistantChat.js +34 -7
  49. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  50. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  51. package/dist/client/agent-chat-adapter.js +34 -15
  52. package/dist/client/agent-chat-adapter.js.map +1 -1
  53. package/dist/client/agent-chat.d.ts +6 -0
  54. package/dist/client/agent-chat.d.ts.map +1 -1
  55. package/dist/client/agent-chat.js +7 -0
  56. package/dist/client/agent-chat.js.map +1 -1
  57. package/dist/client/composer/MentionPopover.d.ts.map +1 -1
  58. package/dist/client/composer/MentionPopover.js +27 -24
  59. package/dist/client/composer/MentionPopover.js.map +1 -1
  60. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  61. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  62. package/dist/client/composer/TiptapComposer.js +21 -4
  63. package/dist/client/composer/TiptapComposer.js.map +1 -1
  64. package/dist/client/embed.d.ts +28 -0
  65. package/dist/client/embed.d.ts.map +1 -0
  66. package/dist/client/embed.js +50 -0
  67. package/dist/client/embed.js.map +1 -0
  68. package/dist/client/index.d.ts +4 -1
  69. package/dist/client/index.d.ts.map +1 -1
  70. package/dist/client/index.js +4 -1
  71. package/dist/client/index.js.map +1 -1
  72. package/dist/client/integrations/IntegrationsPanel.js +2 -2
  73. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  74. package/dist/client/onboarding/OnboardingBanner.js +2 -2
  75. package/dist/client/onboarding/OnboardingBanner.js.map +1 -1
  76. package/dist/client/onboarding/OnboardingPanel.d.ts.map +1 -1
  77. package/dist/client/onboarding/OnboardingPanel.js +139 -52
  78. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  79. package/dist/client/onboarding/SetupButton.d.ts.map +1 -1
  80. package/dist/client/onboarding/SetupButton.js +13 -3
  81. package/dist/client/onboarding/SetupButton.js.map +1 -1
  82. package/dist/client/org/TeamPage.d.ts.map +1 -1
  83. package/dist/client/org/TeamPage.js +12 -7
  84. package/dist/client/org/TeamPage.js.map +1 -1
  85. package/dist/client/resources/ResourceEditor.d.ts.map +1 -1
  86. package/dist/client/resources/ResourceEditor.js +57 -2
  87. package/dist/client/resources/ResourceEditor.js.map +1 -1
  88. package/dist/client/resources/ResourceTree.d.ts +5 -1
  89. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  90. package/dist/client/resources/ResourceTree.js +17 -10
  91. package/dist/client/resources/ResourceTree.js.map +1 -1
  92. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  93. package/dist/client/resources/ResourcesPanel.js +12 -10
  94. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  95. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  96. package/dist/client/settings/AgentsSection.js +6 -3
  97. package/dist/client/settings/AgentsSection.js.map +1 -1
  98. package/dist/client/settings/ComingSoonSection.js +1 -1
  99. package/dist/client/settings/ComingSoonSection.js.map +1 -1
  100. package/dist/client/settings/LLMSection.d.ts.map +1 -1
  101. package/dist/client/settings/LLMSection.js +1 -1
  102. package/dist/client/settings/LLMSection.js.map +1 -1
  103. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  104. package/dist/client/settings/SettingsPanel.js +16 -23
  105. package/dist/client/settings/SettingsPanel.js.map +1 -1
  106. package/dist/client/settings/SettingsSection.js +2 -2
  107. package/dist/client/settings/SettingsSection.js.map +1 -1
  108. package/dist/client/settings/UsageSection.d.ts +2 -0
  109. package/dist/client/settings/UsageSection.d.ts.map +1 -0
  110. package/dist/client/settings/UsageSection.js +70 -0
  111. package/dist/client/settings/UsageSection.js.map +1 -0
  112. package/dist/client/use-action.d.ts.map +1 -1
  113. package/dist/client/use-action.js +67 -4
  114. package/dist/client/use-action.js.map +1 -1
  115. package/dist/client/use-db-sync.d.ts +25 -2
  116. package/dist/client/use-db-sync.d.ts.map +1 -1
  117. package/dist/client/use-db-sync.js +62 -1
  118. package/dist/client/use-db-sync.js.map +1 -1
  119. package/dist/client/use-dev-mode.d.ts.map +1 -1
  120. package/dist/client/use-dev-mode.js +16 -1
  121. package/dist/client/use-dev-mode.js.map +1 -1
  122. package/dist/db/client.d.ts +12 -0
  123. package/dist/db/client.d.ts.map +1 -1
  124. package/dist/db/client.js +89 -2
  125. package/dist/db/client.js.map +1 -1
  126. package/dist/db/create-get-db.d.ts +11 -0
  127. package/dist/db/create-get-db.d.ts.map +1 -1
  128. package/dist/db/create-get-db.js +47 -3
  129. package/dist/db/create-get-db.js.map +1 -1
  130. package/dist/db/migrations.d.ts.map +1 -1
  131. package/dist/db/migrations.js +62 -5
  132. package/dist/db/migrations.js.map +1 -1
  133. package/dist/index.d.ts +1 -0
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +2 -0
  136. package/dist/index.js.map +1 -1
  137. package/dist/jobs/scheduler.d.ts.map +1 -1
  138. package/dist/jobs/scheduler.js +7 -2
  139. package/dist/jobs/scheduler.js.map +1 -1
  140. package/dist/oauth-tokens/google-refresh.d.ts +31 -0
  141. package/dist/oauth-tokens/google-refresh.d.ts.map +1 -0
  142. package/dist/oauth-tokens/google-refresh.js +115 -0
  143. package/dist/oauth-tokens/google-refresh.js.map +1 -0
  144. package/dist/oauth-tokens/index.d.ts +1 -0
  145. package/dist/oauth-tokens/index.d.ts.map +1 -1
  146. package/dist/oauth-tokens/index.js +1 -0
  147. package/dist/oauth-tokens/index.js.map +1 -1
  148. package/dist/onboarding/default-steps.d.ts.map +1 -1
  149. package/dist/onboarding/default-steps.js +57 -18
  150. package/dist/onboarding/default-steps.js.map +1 -1
  151. package/dist/org/context.js +1 -1
  152. package/dist/org/handlers.d.ts.map +1 -1
  153. package/dist/org/handlers.js +35 -10
  154. package/dist/org/handlers.js.map +1 -1
  155. package/dist/org/plugin.d.ts.map +1 -1
  156. package/dist/org/plugin.js +37 -22
  157. package/dist/org/plugin.js.map +1 -1
  158. package/dist/resources/handlers.js +1 -1
  159. package/dist/resources/handlers.js.map +1 -1
  160. package/dist/resources/metadata.d.ts.map +1 -1
  161. package/dist/resources/metadata.js +2 -2
  162. package/dist/resources/metadata.js.map +1 -1
  163. package/dist/resources/store.d.ts.map +1 -1
  164. package/dist/resources/store.js +27 -1
  165. package/dist/resources/store.js.map +1 -1
  166. package/dist/scripts/db/patch.d.ts.map +1 -1
  167. package/dist/scripts/db/patch.js +273 -11
  168. package/dist/scripts/db/patch.js.map +1 -1
  169. package/dist/server/action-discovery.d.ts.map +1 -1
  170. package/dist/server/action-discovery.js +2 -0
  171. package/dist/server/action-discovery.js.map +1 -1
  172. package/dist/server/action-routes.d.ts.map +1 -1
  173. package/dist/server/action-routes.js +26 -0
  174. package/dist/server/action-routes.js.map +1 -1
  175. package/dist/server/agent-chat-plugin.d.ts +12 -0
  176. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  177. package/dist/server/agent-chat-plugin.js +495 -48
  178. package/dist/server/agent-chat-plugin.js.map +1 -1
  179. package/dist/server/agent-discovery.js +2 -2
  180. package/dist/server/agent-discovery.js.map +1 -1
  181. package/dist/server/agent-teams.d.ts.map +1 -1
  182. package/dist/server/agent-teams.js +21 -58
  183. package/dist/server/agent-teams.js.map +1 -1
  184. package/dist/server/auth.d.ts +6 -0
  185. package/dist/server/auth.d.ts.map +1 -1
  186. package/dist/server/auth.js +284 -41
  187. package/dist/server/auth.js.map +1 -1
  188. package/dist/server/better-auth-instance.d.ts +1 -1
  189. package/dist/server/better-auth-instance.d.ts.map +1 -1
  190. package/dist/server/better-auth-instance.js +70 -4
  191. package/dist/server/better-auth-instance.js.map +1 -1
  192. package/dist/server/builder-browser.d.ts +21 -0
  193. package/dist/server/builder-browser.d.ts.map +1 -1
  194. package/dist/server/builder-browser.js +67 -4
  195. package/dist/server/builder-browser.js.map +1 -1
  196. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  197. package/dist/server/core-routes-plugin.js +122 -4
  198. package/dist/server/core-routes-plugin.js.map +1 -1
  199. package/dist/server/desktop-sso.d.ts +30 -0
  200. package/dist/server/desktop-sso.d.ts.map +1 -0
  201. package/dist/server/desktop-sso.js +74 -0
  202. package/dist/server/desktop-sso.js.map +1 -0
  203. package/dist/server/email.d.ts +23 -0
  204. package/dist/server/email.d.ts.map +1 -0
  205. package/dist/server/email.js +105 -0
  206. package/dist/server/email.js.map +1 -0
  207. package/dist/server/framework-request-handler.d.ts.map +1 -1
  208. package/dist/server/framework-request-handler.js +29 -0
  209. package/dist/server/framework-request-handler.js.map +1 -1
  210. package/dist/server/google-oauth.d.ts.map +1 -1
  211. package/dist/server/google-oauth.js +19 -3
  212. package/dist/server/google-oauth.js.map +1 -1
  213. package/dist/server/local-migration.d.ts +9 -0
  214. package/dist/server/local-migration.d.ts.map +1 -1
  215. package/dist/server/local-migration.js +44 -14
  216. package/dist/server/local-migration.js.map +1 -1
  217. package/dist/server/oauth-helpers.d.ts +2 -0
  218. package/dist/server/oauth-helpers.d.ts.map +1 -1
  219. package/dist/server/oauth-helpers.js +2 -0
  220. package/dist/server/oauth-helpers.js.map +1 -1
  221. package/dist/server/onboarding-html.d.ts +6 -0
  222. package/dist/server/onboarding-html.d.ts.map +1 -1
  223. package/dist/server/onboarding-html.js +229 -25
  224. package/dist/server/onboarding-html.js.map +1 -1
  225. package/dist/server/poll.d.ts.map +1 -1
  226. package/dist/server/poll.js +48 -0
  227. package/dist/server/poll.js.map +1 -1
  228. package/dist/templates/default/.agents/skills/inline-embeds/SKILL.md +88 -0
  229. package/dist/usage/store.d.ts +74 -12
  230. package/dist/usage/store.d.ts.map +1 -1
  231. package/dist/usage/store.js +210 -44
  232. package/dist/usage/store.js.map +1 -1
  233. package/dist/vite/client.d.ts.map +1 -1
  234. package/dist/vite/client.js +55 -0
  235. package/dist/vite/client.js.map +1 -1
  236. package/docs/content/deployment.md +59 -2
  237. package/docs/content/resources.md +9 -9
  238. package/docs/content/workspace-management.md +2 -2
  239. package/package.json +3 -3
  240. package/src/templates/default/.agents/skills/inline-embeds/SKILL.md +88 -0
  241. /package/dist/templates/workspace-core/{skills → .agents/skills}/company-policies/SKILL.md +0 -0
  242. /package/src/templates/workspace-core/{skills → .agents/skills}/company-policies/SKILL.md +0 -0
@@ -1,4 +1,5 @@
1
1
  import { runWithRequestContext, getRequestOrgId } from "./request-context.js";
2
+ import { getSetting, putSetting } from "../settings/store.js";
2
3
  import { getH3App } from "./framework-request-handler.js";
3
4
  import { createProductionAgentHandler, runAgentLoop, actionsToEngineTools, getActiveRunForThreadAsync, abortRun, subscribeToRun, } from "../agent/production-agent.js";
4
5
  import { resolveEngine, createAnthropicEngine } from "../agent/engine/index.js";
@@ -28,9 +29,10 @@ async function lazyFs() {
28
29
  * Wraps a core CLI script (that writes to console.log) as a ActionEntry
29
30
  * by capturing stdout.
30
31
  */
31
- function wrapCliScript(tool, cliDefault) {
32
+ function wrapCliScript(tool, cliDefault, opts) {
32
33
  return {
33
34
  tool,
35
+ ...(opts?.readOnly ? { readOnly: true } : {}),
34
36
  run: async (args) => {
35
37
  const cliArgs = [];
36
38
  for (const [k, v] of Object.entries(args)) {
@@ -72,6 +74,254 @@ function wrapCliScript(tool, cliDefault) {
72
74
  },
73
75
  };
74
76
  }
77
+ /**
78
+ * Creates the `refresh-screen` tool. Writes a bump to `application_state`
79
+ * under a well-known key; the client's `useDbSync` watches for this and
80
+ * invalidates react-query caches so the on-screen UI re-fetches its data
81
+ * without a full page reload.
82
+ *
83
+ * This is the standard way for the agent to say "the data on the screen
84
+ * just changed, please refresh it" — e.g. after editing a dashboard config,
85
+ * updating a form schema, or mutating a row that the current view renders.
86
+ */
87
+ function createRefreshScreenEntry() {
88
+ return {
89
+ "refresh-screen": {
90
+ // Writes __screen_refresh__ to application_state, which emits its own
91
+ // distinct `screen-refresh` poll event. Don't double-emit a generic
92
+ // `action` event on top of that.
93
+ readOnly: true,
94
+ tool: {
95
+ description: "Manually refresh the user's current screen. The framework ALREADY auto-refreshes after any successful mutating action tool call (template actions, db-exec, db-patch) — you do NOT need to call this after a normal action. Use it only when (a) you mutated data via a path the framework can't detect (e.g. a direct write to an external system the app mirrors), or (b) you want to pass a `scope` hint so the UI narrows which queries to refetch. The UI re-fetches its queries without a full page reload.",
96
+ parameters: {
97
+ type: "object",
98
+ properties: {
99
+ scope: {
100
+ type: "string",
101
+ description: "Optional hint describing what changed (e.g. 'dashboard', 'form', 'settings'). Templates may use it to narrow which queries to invalidate; if omitted, all queries are invalidated.",
102
+ },
103
+ },
104
+ },
105
+ },
106
+ run: async (args) => {
107
+ const { writeAppState } = await import("../application-state/script-helpers.js");
108
+ const nonce = Date.now();
109
+ const scope = typeof args?.scope === "string" ? args.scope : undefined;
110
+ await writeAppState(SCREEN_REFRESH_KEY, {
111
+ nonce,
112
+ ...(scope ? { scope } : {}),
113
+ });
114
+ return `refreshed${scope ? ` (scope: ${scope})` : ""}`;
115
+ },
116
+ },
117
+ };
118
+ }
119
+ /** Well-known application-state key used by the refresh-screen tool. */
120
+ const SCREEN_REFRESH_KEY = "__screen_refresh__";
121
+ /**
122
+ * Creates the `set-search-params` / `set-url-path` tools. Writes a one-shot
123
+ * URL command to application_state; the client's URLSync component applies
124
+ * it via react-router (no full page reload) and then deletes the command.
125
+ *
126
+ * This is how the agent edits URL state — filter query params, route
127
+ * changes, hash — without needing a per-template navigate action. The
128
+ * current URL is visible to the agent via the auto-injected `<current-url>`
129
+ * block, which includes parsed search params.
130
+ */
131
+ function createUrlTools() {
132
+ return {
133
+ "set-search-params": {
134
+ // Writes __set_url__ to application_state, which the app-state watcher
135
+ // already surfaces as a poll event. No need to double-emit.
136
+ readOnly: true,
137
+ tool: {
138
+ description: "Update the URL query string on the user's current page. Use this to change dashboard/list filters, search terms, or any other state the app stores in `?foo=bar` style query params. One-shot — the UI applies it in ~1s without a page reload. See the current URL + parsed search params in the auto-injected `<current-url>` block. Keys are the exact query param names as they appear in the URL (e.g. `f_pubDateStart`, not just `pubDateStart`). Set a value to null or empty string to clear that param. By default merges over existing params — pass `merge: false` to replace them all.",
139
+ parameters: {
140
+ type: "object",
141
+ properties: {
142
+ params: {
143
+ type: "object",
144
+ description: 'Map of query param → value. Each value is a string, or null/"" to clear. Example: {"f_pubDateStart": null, "f_cadence": "MONTH"}.',
145
+ },
146
+ merge: {
147
+ type: "string",
148
+ description: '"true" (default) merges over existing params; "false" replaces them entirely.',
149
+ enum: ["true", "false"],
150
+ },
151
+ },
152
+ required: ["params"],
153
+ },
154
+ },
155
+ run: async (args) => {
156
+ const params = (args?.params ?? {});
157
+ const merge = args?.merge !== "false";
158
+ const { writeAppState } = await import("../application-state/script-helpers.js");
159
+ await writeAppState("__set_url__", {
160
+ searchParams: params,
161
+ mergeSearchParams: merge,
162
+ });
163
+ const keys = Object.keys(params);
164
+ return `set-search-params: ${keys.length} key${keys.length === 1 ? "" : "s"}${merge ? "" : " (replace)"}`;
165
+ },
166
+ },
167
+ "set-url-path": {
168
+ // Same as set-search-params — writes application_state, already emits
169
+ // via the app-state watcher.
170
+ readOnly: true,
171
+ tool: {
172
+ description: "Navigate the user to a different pathname, optionally also setting search params. For most template-specific routing prefer the template's `navigate` action if it exists — this is the generic fallback. One-shot, applied by the client without a page reload.",
173
+ parameters: {
174
+ type: "object",
175
+ properties: {
176
+ pathname: {
177
+ type: "string",
178
+ description: "New URL pathname (e.g. '/adhoc/weekly').",
179
+ },
180
+ params: {
181
+ type: "object",
182
+ description: 'Optional query params to set alongside the path change. String values set, null/"" clears.',
183
+ },
184
+ merge: {
185
+ type: "string",
186
+ description: '"true" (default) merges over existing params; "false" starts fresh.',
187
+ enum: ["true", "false"],
188
+ },
189
+ },
190
+ required: ["pathname"],
191
+ },
192
+ },
193
+ run: async (args) => {
194
+ const pathname = String(args?.pathname ?? "");
195
+ if (!pathname.startsWith("/")) {
196
+ return "Error: pathname must start with '/'.";
197
+ }
198
+ const params = (args?.params ?? {});
199
+ const merge = args?.merge !== "false";
200
+ const { writeAppState } = await import("../application-state/script-helpers.js");
201
+ await writeAppState("__set_url__", {
202
+ pathname,
203
+ searchParams: params,
204
+ mergeSearchParams: merge,
205
+ });
206
+ return `set-url-path: ${pathname}`;
207
+ },
208
+ },
209
+ };
210
+ }
211
+ /**
212
+ * Creates db-* tools (db-query, db-exec, db-patch, db-schema) as native tools.
213
+ * These let the agent read and write the app's own SQL database. Scoping to
214
+ * the current user/org is enforced automatically in production via temp views.
215
+ *
216
+ * In dev mode template actions are invoked via shell and the agent can call
217
+ * `pnpm action db-query ...` — but in production there is no shell, so these
218
+ * must be registered as native tools for the agent to reach the app DB at all.
219
+ */
220
+ async function createDbScriptEntries() {
221
+ try {
222
+ const [schemaMod, queryMod, execMod, patchMod] = await Promise.all([
223
+ import("../scripts/db/schema.js"),
224
+ import("../scripts/db/query.js"),
225
+ import("../scripts/db/exec.js"),
226
+ import("../scripts/db/patch.js"),
227
+ ]);
228
+ return {
229
+ "db-schema": wrapCliScript({
230
+ description: "Show the app's SQL schema — all tables, columns, types, indexes, and foreign keys. Use this to understand the data model before querying.",
231
+ parameters: {
232
+ type: "object",
233
+ properties: {
234
+ format: {
235
+ type: "string",
236
+ description: 'Output format: "json" or "text" (default: text)',
237
+ enum: ["json", "text"],
238
+ },
239
+ },
240
+ },
241
+ }, schemaMod.default, { readOnly: true }),
242
+ "db-query": wrapCliScript({
243
+ description: "Read from the app's SQL database. Runs a SELECT (or WITH/EXPLAIN/PRAGMA) against the app's own tables — settings, application_state, and all template tables. Results are automatically scoped to the current user/org; DO NOT add `WHERE owner_email = ...` yourself. This queries the APP DATABASE — not any external data source.",
244
+ parameters: {
245
+ type: "object",
246
+ properties: {
247
+ sql: {
248
+ type: "string",
249
+ description: "SELECT query to run, e.g. \"SELECT key, value FROM settings WHERE key LIKE 'sql-dashboard-%'\"",
250
+ },
251
+ format: {
252
+ type: "string",
253
+ description: 'Output format: "json" or "text" (default: text)',
254
+ enum: ["json", "text"],
255
+ },
256
+ limit: {
257
+ type: "string",
258
+ description: "Append LIMIT N if the query doesn't already have one",
259
+ },
260
+ },
261
+ required: ["sql"],
262
+ },
263
+ }, queryMod.default, { readOnly: true }),
264
+ "db-exec": wrapCliScript({
265
+ description: "Write to the app's SQL database. Runs INSERT / UPDATE / DELETE against the app's own tables. Writes are automatically scoped to the current user/org, and `owner_email` / `org_id` are auto-injected on INSERT. Use this to update rows in the settings table (e.g. edit a dashboard config stored under `o:<orgId>:sql-dashboard-<id>`). This writes to the APP DATABASE — not any external data source.",
266
+ parameters: {
267
+ type: "object",
268
+ properties: {
269
+ sql: {
270
+ type: "string",
271
+ description: "INSERT / UPDATE / DELETE statement. Use parameterized placeholders (?) if possible.",
272
+ },
273
+ },
274
+ required: ["sql"],
275
+ },
276
+ }, execMod.default),
277
+ "db-patch": wrapCliScript({
278
+ description: "Surgical patch on a large text/JSON column in the app's SQL database. Two modes: (1) text find/replace via `find`/`replace`/`edits` — best for small edits to documents, slide HTML, etc. (2) structural JSON ops via `json-ops` — STRONGLY PREFERRED when the column is JSON (dashboard configs, form schemas, slide decks) because it avoids all the brace/quote/comma surgery that text find/replace requires. Use `json-ops` to set/remove values at a JSON Pointer path, or to move/insert array items — e.g. reorder dashboard panels, add a filter, rename a field. Targets exactly one row (narrow `where` by primary key). Same per-user/org scoping as db-exec.",
279
+ parameters: {
280
+ type: "object",
281
+ properties: {
282
+ table: {
283
+ type: "string",
284
+ description: "Table name (e.g. 'settings')",
285
+ },
286
+ column: {
287
+ type: "string",
288
+ description: "Text/JSON column to patch (e.g. 'value' for settings)",
289
+ },
290
+ where: {
291
+ type: "string",
292
+ description: "WHERE clause that matches exactly one row (e.g. \"key = 'o:org1:sql-dashboard-foo'\")",
293
+ },
294
+ find: {
295
+ type: "string",
296
+ description: "Text mode: substring to find. Must match EXACTLY ONE occurrence by default (like Claude Code's Edit tool). If 0 matches, you get 'NOT FOUND'. If >1 matches, you get surrounding context for each match — widen `find` with unique context and retry. Use `all: \"true\"` to replace every occurrence.",
297
+ },
298
+ replace: {
299
+ type: "string",
300
+ description: "Text mode: replacement substring",
301
+ },
302
+ edits: {
303
+ type: "string",
304
+ description: 'Text mode batch: JSON array of {find, replace} pairs. Same uniqueness rule applies to each `find`. Example: \'[{"find":"a","replace":"b"}]\'',
305
+ },
306
+ "json-ops": {
307
+ type: "string",
308
+ description: 'JSON mode: JSON array of structural ops. Each op is {op, path, value?, from?}. `op` is one of "set", "remove", "insert", "move", "move-before". `path` / `from` use JSON Pointer ("/panels/3/title"). Examples — reorder: \'[{"op":"move","from":"/panels/7","path":"/panels/1"}]\'; edit field: \'[{"op":"set","path":"/panels/0/title","value":"New"}]\'; delete filter: \'[{"op":"remove","path":"/filters/2"}]\'; add panel: \'[{"op":"insert","path":"/panels/0","value":{"id":"p","title":"..."}}]\'. Much safer than text find/replace for JSON columns.',
309
+ },
310
+ all: {
311
+ type: "string",
312
+ description: 'Text mode: set to "true" to replace every occurrence of each `find` (default requires exactly one match)',
313
+ enum: ["true"],
314
+ },
315
+ },
316
+ required: ["table", "column", "where"],
317
+ },
318
+ }, patchMod.default),
319
+ };
320
+ }
321
+ catch {
322
+ return {};
323
+ }
324
+ }
75
325
  /**
76
326
  * Creates the docs-search tool so agents can look up framework documentation.
77
327
  * Docs are bundled in @agent-native/core and read via fs at runtime.
@@ -100,7 +350,7 @@ async function createDocsScriptEntries() {
100
350
  },
101
351
  },
102
352
  },
103
- }, mod.default),
353
+ }, mod.default, { readOnly: true }),
104
354
  };
105
355
  }
106
356
  catch {
@@ -342,6 +592,32 @@ async function createCallAgentScriptEntry(selfAppId) {
342
592
  }
343
593
  function createBuilderBrowserTool(deps) {
344
594
  return {
595
+ "connect-builder": {
596
+ tool: {
597
+ description: "Render a Builder.io card inline in the chat. Call this IMMEDIATELY — no exploration, no planning — whenever (a) the user asks to add a feature, change the UI, edit code, create a component, add a route, add an integration, fix a bug in the app itself, or anything else that requires source-file edits while in hosted/production mode, OR (b) the user needs Builder for LLM access, browser automation, or any other Builder-gated capability. If Builder is already connected, the card shows a 'Send to Builder' button that hands the work off to Builder's cloud agent and returns a branch URL. When you call this for a code-change request, pass the user's request verbatim as the `prompt` arg so the card can forward it to Builder unchanged.",
598
+ parameters: {
599
+ type: "object",
600
+ properties: {
601
+ prompt: {
602
+ type: "string",
603
+ description: "The user's feature / change request, verbatim. Forwarded to Builder's cloud agent when the user clicks Send. Omit only for generic 'connect Builder' requests that aren't tied to a specific code change.",
604
+ },
605
+ },
606
+ },
607
+ },
608
+ run: async (args) => {
609
+ const configured = !!(process.env.BUILDER_PRIVATE_KEY && process.env.BUILDER_PUBLIC_KEY);
610
+ const prompt = typeof args?.prompt === "string" ? args.prompt : "";
611
+ return JSON.stringify({
612
+ kind: "connect-builder-card",
613
+ configured,
614
+ builderEnabled: !!process.env.ENABLE_BUILDER,
615
+ connectUrl: getBuilderBrowserConnectUrl(deps.getOrigin()),
616
+ orgName: process.env.BUILDER_ORG_NAME || null,
617
+ prompt,
618
+ });
619
+ },
620
+ },
345
621
  "get-browser-connection": {
346
622
  tool: {
347
623
  description: "Provision a Builder-backed browser session and return browser websocket connection details. If Builder browser access is not configured yet, this returns setup guidance instead.",
@@ -616,11 +892,12 @@ const FRAMEWORK_CORE = `
616
892
  ### Core Rules
617
893
 
618
894
  1. **Data lives in SQL** — All app state is in a SQL database (could be SQLite, Postgres, Turso, or Cloudflare D1 — never assume which). Use the available database tools.
619
- 2. **Context awareness** — The user's current screen state is automatically included in each message as a \`<current-screen>\` block. Use it to understand what the user is looking at. You can still call \`view-screen\` for a more detailed snapshot if needed, but you should NOT need to call it before every action.
895
+ 2. **Context awareness** — The user's current screen state is automatically included in each message as a \`<current-screen>\` block, and the current URL (path + search params) as a \`<current-url>\` block. Use both to understand what the user is looking at filters, search terms, and other URL-driven state live in \`<current-url>\`'s \`searchParams\`, NOT in the settings table. To change URL state (e.g. toggle a filter, clear a query string), use the \`set-search-params\` or \`set-url-path\` tools — never try to edit URL state by writing to settings or application_state directly.
620
896
  3. **Navigate the UI** — Use the \`navigate\` tool to switch views, open items, or focus elements for the user.
621
897
  4. **Application state** — Ephemeral UI state (drafts, selections, navigation) lives in \`application_state\`. Use \`readAppState\`/\`writeAppState\` to read and write it. When you write state, the UI updates automatically.
622
- 5. **Memory** — Use the structured memory system to persist knowledge across sessions. Use \`save-memory\` proactively when you learn preferences, corrections, or project context. Update shared AGENTS.md for instructions that should apply to all users.
623
- 6. **Security** — Always use \`defineAction\` with a Zod \`schema:\` for input validation. Never construct SQL with string concatenation use parameterized queries via db-query/db-exec. Never use \`dangerouslySetInnerHTML\`, \`innerHTML\`, or \`eval()\`. Never expose secrets in responses or source code. Every table with user data must have \`owner_email\`.
898
+ 5. **Screen refresh is automatic after action calls** — The framework auto-emits a refresh event after any successful mutating tool call (template actions like \`log-meal\`, \`update-form\`, \`edit-document\`, and the \`db-exec\` / \`db-patch\` tools). The UI re-fetches its queries without a full page reload. You do NOT need to call \`refresh-screen\` after an action — it's already handled. Only call \`refresh-screen\` explicitly when (a) you mutated data via a path the framework can't detect (e.g. writing directly to an external system whose results the app mirrors), or (b) you want to pass a \`scope\` hint so the UI narrows which queries to refetch. Do NOT tell the user to reload the page.
899
+ 6. **Memory** — Use the structured memory system to persist knowledge across sessions. Use \`save-memory\` proactively when you learn preferences, corrections, or project context. Update shared AGENTS.md for instructions that should apply to all users.
900
+ 7. **Security** — Always use \`defineAction\` with a Zod \`schema:\` for input validation. Never construct SQL with string concatenation — use parameterized queries via db-query/db-exec. Never use \`dangerouslySetInnerHTML\`, \`innerHTML\`, or \`eval()\`. Never expose secrets in responses or source code. Every table with user data must have \`owner_email\`.
624
901
 
625
902
  ### Resources
626
903
 
@@ -634,6 +911,37 @@ When the user gives instructions that should apply to all users/sessions, update
634
911
 
635
912
  When the user says "show me", "go to", "open", "switch to", or similar navigation language, ALWAYS use the \`navigate\` action to update the UI. The user expects to SEE the result in the main app, not just read it in chat. Navigate first, then fetch/display data.
636
913
 
914
+ ### Inline Embeds
915
+
916
+ You can embed an interactive view inline in your chat reply by writing an \`embed\` fenced code block. The chat renderer swaps the fence for a sandboxed iframe pointing at a route inside this app.
917
+
918
+ Syntax:
919
+
920
+ \`\`\`\`
921
+ \`\`\`embed
922
+ src: /some/path?param=value
923
+ aspect: 16/9
924
+ title: Optional label
925
+ \`\`\`
926
+ \`\`\`\`
927
+
928
+ Keys:
929
+ - \`src\` (required) — **must be a same-origin path starting with \`/\`**. Cross-origin URLs are blocked by the renderer. No \`javascript:\` or \`data:\` URLs.
930
+ - \`aspect\` (optional) — one of \`16/9\` (default), \`4/3\`, \`3/2\`, \`2/1\`, \`21/9\`, \`1/1\`.
931
+ - \`title\` (optional) — accessible label / hover tooltip.
932
+ - \`height\` (optional) — fixed pixel height when aspect ratio isn't a good fit.
933
+
934
+ **When to reach for it:**
935
+ - Showing a chart, visualization, or map that benefits from being live/interactive.
936
+ - Previewing a specific item (a thread, a doc, a record) inline with your explanation.
937
+ - Anything where a screenshot-sized static image would undersell the result.
938
+
939
+ **When NOT to use it:**
940
+ - For simple prose answers, tables, or plain data — those should stay as markdown.
941
+ - For external sites — the renderer blocks cross-origin iframes.
942
+
943
+ Which routes are renderable as embeds is template-specific — the app's \`AGENTS.md\` will list them. If no embeddable routes exist in this template, don't emit \`embed\` fences.
944
+
637
945
  ### Chat History
638
946
 
639
947
  You can search and restore previous chat conversations:
@@ -682,11 +990,15 @@ When the user asks for something recurring ("every morning", "daily at 9am", "we
682
990
 
683
991
  Job instructions should be self-contained — include which actions to call, what conditions to check, and what to do with results. The agent executing the job has access to all the same tools you do.
684
992
 
993
+ ### Connecting Builder.io
994
+
995
+ When the user asks to connect Builder.io, needs Builder for LLM access / browser automation, or you hit a "Builder not configured" error, call the \`connect-builder\` tool. It renders a one-click Connect card inline in the chat — do NOT write out multi-step setup instructions yourself (no "Option 1 / Option 2", no terminal commands). Just call the tool and let the card handle the rest.
996
+
685
997
  ### Browser Access
686
998
 
687
999
  Use \`get-browser-connection\` when you need a real browser session backed by Builder. It returns websocket connection details for a provisioned browser session.
688
1000
 
689
- - If the tool says Builder is not configured, tell the user to connect Builder from the workspace Resources panel.
1001
+ - If the tool says Builder is not configured, call \`connect-builder\` to show the user a Connect card.
690
1002
  - Reuse a stable \`sessionId\` when you want to reconnect to the same browser session.
691
1003
  - Include proxy parameters when you need the browser to reach a local dev server through Builder's browser connection flow.
692
1004
 
@@ -743,20 +1055,22 @@ The agent and the UI are equal partners — everything the UI can do, you can do
743
1055
 
744
1056
  **In production mode, you operate through registered actions exposed as tools.** These are your capabilities — use them to read data, take actions, and help the user. You cannot edit source code or access the filesystem directly. Your tools are the app's API.
745
1057
 
746
- ### Code Changes Not Available
1058
+ ### Code Changes Not Available — Call \`connect-builder\` Immediately
747
1059
 
748
- When the user asks you to change the UI, modify code, add features, fix bugs in the app itself, change styles, or do anything that requires editing source files you MUST respond with this exact message (replacing APP_NAME with the current app name from the URL hostname):
1060
+ When the user asks you to change the UI, modify code, add a feature, fix a bug in the app itself, change styles, add a hook, create a component, add a route, add an integration, or anything else that requires editing source files you MUST take exactly these steps, in order:
749
1061
 
750
- > This app is running in hosted mode and I can't make code changes here. To customize this app:
751
- >
752
- > **Option 1:** Connect to [Builder.io](https://www.builder.io/m/agent-native) for a full development environment
753
- >
754
- > **Option 2:** Clone and run locally:
755
- > \`\`\`
756
- > npx agent-native create APP_NAME
757
- > \`\`\`
1062
+ 1. Call the \`connect-builder\` tool, passing the user's full request verbatim as the \`prompt\` argument. This renders an inline card. If Builder is connected, the card hands the prompt off to Builder's cloud agent on one click and returns a branch URL; otherwise it shows a connect / waitlist flow. Either way, Builder writes the code — no local setup, no clone, no install.
1063
+ 2. After the card renders, write a single short sentence telling the user to click Connect Builder to have this built for them in the cloud, and mention they can also clone the app locally with \`npx agent-native create <app-name>\` if they'd rather work offline.
758
1064
 
759
- Do NOT attempt to work around this limitation. Do NOT say "I can't do that" without providing the options above. Always include both the Builder.io link and the CLI command.
1065
+ **Hard rules do NOT break these:**
1066
+ - Do NOT read source files, list directories, or explore the codebase. You have no filesystem tools and don't need to look at code to recommend Builder.
1067
+ - Do NOT write an implementation plan. Do NOT write code in your response. Do NOT describe which files to create or modify. Builder will figure that out in its sandbox.
1068
+ - Do NOT save plans, specs, or code to \`resource-write\`. Resources are for app data, not implementation plans the user didn't ask for.
1069
+ - Do NOT spawn sub-agents (\`spawn-task\`) to design, plan, or research the feature. The answer is always: call \`connect-builder\`, say one sentence, stop. Sub-agents inherit these same rules and have no code-editing tools either.
1070
+ - Do NOT say "I don't have a request-code-change tool" or list what tools you lack. Just call \`connect-builder\`.
1071
+ - Do NOT wait for the user to ask "how" — call \`connect-builder\` the moment a code change is requested.
1072
+
1073
+ Builder.io is the recommended path because it's one-click and runs in the cloud. Local dev is a fallback for users who want to work offline.
760
1074
  ${FRAMEWORK_CORE}`;
761
1075
  const DEV_FRAMEWORK_PROMPT = `## Agent-Native Framework — Development Mode
762
1076
 
@@ -850,12 +1164,15 @@ async function loadResourcesForPrompt(owner) {
850
1164
  * from the environment so scheduler/A2A/HTTP call sites all see whatever
851
1165
  * org was just resolved for this request.
852
1166
  */
853
- async function buildSchemaBlock(owner, hasRawDbTools) {
1167
+ async function buildSchemaBlock(owner, _legacyHasRawDbTools) {
1168
+ // db-* tools are always registered (see createDbScriptEntries), in both dev
1169
+ // and prod. The legacy boolean is kept for call-site compatibility but
1170
+ // ignored — always advertise the tools to the agent.
854
1171
  try {
855
1172
  return await loadSchemaPromptBlock({
856
1173
  owner,
857
1174
  orgId: getRequestOrgId() ?? null,
858
- hasRawDbTools,
1175
+ hasRawDbTools: true,
859
1176
  });
860
1177
  }
861
1178
  catch {
@@ -1045,6 +1362,29 @@ export function createAgentChatPlugin(options) {
1045
1362
  const canToggle = (env === "development" || env === "test") &&
1046
1363
  process.env.AGENT_MODE !== "production";
1047
1364
  const routePath = options?.path ?? "/_agent-native/agent-chat";
1365
+ // Mutable mode flag — persisted to the `settings` table so a user who
1366
+ // toggles to "Production" stays in prod mode across server restarts.
1367
+ // Hoisted here (before any tool-registry / handler closures are built)
1368
+ // so every runtime decision point can close over it and see live changes
1369
+ // when the user toggles the Environment dropdown.
1370
+ const AGENT_MODE_SETTING_KEY = "agent-chat.mode";
1371
+ let currentDevMode = canToggle;
1372
+ if (canToggle) {
1373
+ try {
1374
+ const persisted = await getSetting(AGENT_MODE_SETTING_KEY);
1375
+ if (persisted && typeof persisted.devMode === "boolean") {
1376
+ currentDevMode = persisted.devMode;
1377
+ }
1378
+ }
1379
+ catch {
1380
+ // Settings table may not be ready yet — fall back to default.
1381
+ }
1382
+ }
1383
+ // Every closure that picks between dev/prod tools, prompts, or handlers
1384
+ // at request time should call this getter instead of reading `canToggle`.
1385
+ // `canToggle` means "this environment allows toggling" (static); this
1386
+ // function means "the user currently has dev mode ON" (live).
1387
+ const isDevMode = () => currentDevMode;
1048
1388
  // Initialize MCP client (connects to user-configured local MCP servers).
1049
1389
  // Graceful-degrade: any failure yields zero MCP tools and agent-chat keeps
1050
1390
  // working as before. No-op outside Node runtimes.
@@ -1092,9 +1432,12 @@ export function createAgentChatPlugin(options) {
1092
1432
  const templateScripts = typeof rawActions === "function"
1093
1433
  ? await rawActions()
1094
1434
  : (rawActions ?? {});
1095
- // Resource, chat, docs, and cross-agent scripts are available in both prod and dev modes
1435
+ // Resource, chat, docs, db, and cross-agent scripts are available in both prod and dev modes
1096
1436
  const resourceScripts = await createResourceScriptEntries();
1097
1437
  const docsScripts = await createDocsScriptEntries();
1438
+ const dbScripts = await createDbScriptEntries();
1439
+ const refreshScreenTool = createRefreshScreenEntry();
1440
+ const urlTools = createUrlTools();
1098
1441
  const engineScripts = await createAgentEngineScriptEntries();
1099
1442
  const chatScripts = {
1100
1443
  ...(await createChatScriptEntries()),
@@ -1253,6 +1596,9 @@ export function createAgentChatPlugin(options) {
1253
1596
  ...templateScripts,
1254
1597
  ...resourceScripts,
1255
1598
  ...docsScripts,
1599
+ ...dbScripts,
1600
+ ...refreshScreenTool,
1601
+ ...urlTools,
1256
1602
  ...chatScripts,
1257
1603
  ...callAgentScript,
1258
1604
  ...browserTools,
@@ -1328,12 +1674,13 @@ export function createAgentChatPlugin(options) {
1328
1674
  apiKey: options?.apiKey,
1329
1675
  });
1330
1676
  // Use the same handler (dev or prod) that the interactive chat uses
1331
- const handler = canToggle && devHandler ? devHandler : prodHandler;
1677
+ const devActive = isDevMode();
1678
+ const handler = devActive && devHandler ? devHandler : prodHandler;
1332
1679
  // Build the same system prompt the interactive agent uses
1333
1680
  const owner = userEmail || "local@localhost";
1334
1681
  const resources = await loadResourcesForPrompt(owner);
1335
- const schemaBlock = await buildSchemaBlock(owner, canToggle);
1336
- const systemPrompt = canToggle
1682
+ const schemaBlock = await buildSchemaBlock(owner, devActive);
1683
+ const systemPrompt = devActive
1337
1684
  ? devPrompt + resources + schemaBlock
1338
1685
  : basePrompt + resources + schemaBlock;
1339
1686
  const model = options?.model ??
@@ -1342,7 +1689,7 @@ export function createAgentChatPlugin(options) {
1342
1689
  // to prevent infinite recursive A2A loops (agent calling itself).
1343
1690
  // In dev mode, template actions are invoked via shell (not native tools),
1344
1691
  // so they're omitted from the tool registry — see allScripts comment.
1345
- const a2aActions = canToggle
1692
+ const a2aActions = devActive
1346
1693
  ? {
1347
1694
  ...resourceScripts,
1348
1695
  ...docsScripts,
@@ -1354,6 +1701,9 @@ export function createAgentChatPlugin(options) {
1354
1701
  ...templateScripts,
1355
1702
  ...resourceScripts,
1356
1703
  ...docsScripts,
1704
+ ...dbScripts,
1705
+ ...refreshScreenTool,
1706
+ ...urlTools,
1357
1707
  ...chatScripts,
1358
1708
  ...browserTools,
1359
1709
  };
@@ -1438,7 +1788,8 @@ export function createAgentChatPlugin(options) {
1438
1788
  (canToggle ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001");
1439
1789
  // Same actions as A2A — without call-agent to prevent loops.
1440
1790
  // In dev mode, template actions go through shell, not native tools.
1441
- const mcpActions = canToggle
1791
+ const devActiveMcp = isDevMode();
1792
+ const mcpActions = devActiveMcp
1442
1793
  ? {
1443
1794
  ...resourceScripts,
1444
1795
  ...docsScripts,
@@ -1449,12 +1800,15 @@ export function createAgentChatPlugin(options) {
1449
1800
  ...templateScripts,
1450
1801
  ...resourceScripts,
1451
1802
  ...docsScripts,
1803
+ ...dbScripts,
1804
+ ...refreshScreenTool,
1805
+ ...urlTools,
1452
1806
  ...chatScripts,
1453
1807
  };
1454
1808
  const mcpTools = actionsToEngineTools(mcpActions);
1455
1809
  const resources = await loadResourcesForPrompt("local@localhost");
1456
- const schemaBlock = await buildSchemaBlock("local@localhost", canToggle);
1457
- const systemPrompt = canToggle
1810
+ const schemaBlock = await buildSchemaBlock("local@localhost", devActiveMcp);
1811
+ const systemPrompt = devActiveMcp
1458
1812
  ? devPrompt + resources + schemaBlock
1459
1813
  : basePrompt + resources + schemaBlock;
1460
1814
  let accumulatedText = "";
@@ -1560,6 +1914,7 @@ export function createAgentChatPlugin(options) {
1560
1914
  // requests for different threads don't clobber each other.
1561
1915
  const _runSendByThread = new Map();
1562
1916
  let _currentRunOwner = "local@localhost";
1917
+ let _currentRunUserApiKey;
1563
1918
  let _currentRunThreadId = "";
1564
1919
  let _currentRunSystemPrompt = basePrompt;
1565
1920
  // Default to Haiku in production mode to manage costs for hosted apps
@@ -1568,7 +1923,7 @@ export function createAgentChatPlugin(options) {
1568
1923
  const teamTools = createTeamTools({
1569
1924
  getOwner: () => _currentRunOwner,
1570
1925
  getSystemPrompt: () => _currentRunSystemPrompt,
1571
- getActions: () => canToggle
1926
+ getActions: () => isDevMode()
1572
1927
  ? {
1573
1928
  // Sub-agents spawned in dev mode also invoke template actions
1574
1929
  // via shell, so omit them from the native tool registry.
@@ -1581,10 +1936,18 @@ export function createAgentChatPlugin(options) {
1581
1936
  ...templateScripts,
1582
1937
  ...resourceScripts,
1583
1938
  ...docsScripts,
1939
+ ...dbScripts,
1940
+ ...refreshScreenTool,
1941
+ ...urlTools,
1584
1942
  ...chatScripts,
1585
1943
  },
1586
1944
  getEngine: () => createAnthropicEngine({
1587
- apiKey: options?.apiKey ?? process.env.ANTHROPIC_API_KEY,
1945
+ // Sub-agents must inherit the parent run's resolved key so a
1946
+ // BYO-key user can't bypass the free-tier check on the parent
1947
+ // run and then have spawn-task delegations bill the platform key.
1948
+ apiKey: _currentRunUserApiKey ??
1949
+ options?.apiKey ??
1950
+ process.env.ANTHROPIC_API_KEY,
1588
1951
  }),
1589
1952
  getModel: () => resolvedModel,
1590
1953
  getParentThreadId: () => _currentRunThreadId,
@@ -1606,6 +1969,9 @@ export function createAgentChatPlugin(options) {
1606
1969
  ...templateScripts,
1607
1970
  ...resourceScripts,
1608
1971
  ...docsScripts,
1972
+ ...dbScripts,
1973
+ ...refreshScreenTool,
1974
+ ...urlTools,
1609
1975
  ...chatScripts,
1610
1976
  ...callAgentScript,
1611
1977
  ...teamTools,
@@ -1616,15 +1982,30 @@ export function createAgentChatPlugin(options) {
1616
1982
  // Always build the production handler (includes resource tools + call-agent + team tools)
1617
1983
  // In production mode (!canToggle), enable usage tracking and limits
1618
1984
  const isHostedProd = !canToggle;
1985
+ const resolveExtraContext = async (event, owner) => {
1986
+ if (!options?.extraContext)
1987
+ return "";
1988
+ try {
1989
+ const extra = await options.extraContext(event, owner);
1990
+ return extra ? `\n\n${extra}` : "";
1991
+ }
1992
+ catch (err) {
1993
+ console.warn("[agent-chat] extraContext threw:", err instanceof Error ? err.message : err);
1994
+ return "";
1995
+ }
1996
+ };
1619
1997
  const prodHandler = createProductionAgentHandler({
1620
1998
  actions: prodActions,
1621
1999
  systemPrompt: async (event) => {
1622
2000
  _currentRequestOrigin = getOrigin(event);
1623
2001
  const owner = await getOwnerFromEvent(event);
1624
2002
  _currentRunOwner = owner;
2003
+ const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
2004
+ _currentRunUserApiKey = await getOwnerAnthropicApiKey(owner);
1625
2005
  const resources = await loadResourcesForPrompt(owner);
1626
2006
  const schemaBlock = await buildSchemaBlock(owner, false);
1627
- _currentRunSystemPrompt = basePrompt + resources + schemaBlock;
2007
+ const extra = await resolveExtraContext(event, owner);
2008
+ _currentRunSystemPrompt = basePrompt + resources + schemaBlock + extra;
1628
2009
  return _currentRunSystemPrompt;
1629
2010
  },
1630
2011
  model: options?.model ??
@@ -1670,9 +2051,12 @@ export function createAgentChatPlugin(options) {
1670
2051
  _currentRequestOrigin = getOrigin(event);
1671
2052
  const owner = await getOwnerFromEvent(event);
1672
2053
  _currentRunOwner = owner;
2054
+ const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
2055
+ _currentRunUserApiKey = await getOwnerAnthropicApiKey(owner);
1673
2056
  const resources = await loadResourcesForPrompt(owner);
1674
2057
  const schemaBlock = await buildSchemaBlock(owner, true);
1675
- _currentRunSystemPrompt = devPrompt + resources + schemaBlock;
2058
+ const extra = await resolveExtraContext(event, owner);
2059
+ _currentRunSystemPrompt = devPrompt + resources + schemaBlock + extra;
1676
2060
  return _currentRunSystemPrompt;
1677
2061
  },
1678
2062
  model: options?.model,
@@ -1693,8 +2077,8 @@ export function createAgentChatPlugin(options) {
1693
2077
  const mentionProviders = typeof rawProviders === "function"
1694
2078
  ? await rawProviders()
1695
2079
  : (rawProviders ?? {});
1696
- // Mutable mode flag starts in dev if environment allows
1697
- let currentDevMode = canToggle;
2080
+ // currentDevMode + persistence were hoisted to the top of this function
2081
+ // so every closure built below can close over the live flag.
1698
2082
  // Mount mode endpoint — GET returns current mode, POST toggles it (localhost only)
1699
2083
  getH3App(nitroApp).use(`${routePath}/mode`, defineEventHandler(async (event) => {
1700
2084
  if (getMethod(event) === "POST") {
@@ -1713,12 +2097,24 @@ export function createAgentChatPlugin(options) {
1713
2097
  else {
1714
2098
  currentDevMode = !currentDevMode;
1715
2099
  }
2100
+ try {
2101
+ await putSetting(AGENT_MODE_SETTING_KEY, {
2102
+ devMode: currentDevMode,
2103
+ });
2104
+ }
2105
+ catch {
2106
+ // Persistence is best-effort — in-memory flag still applies for
2107
+ // the lifetime of this process even if the settings write fails.
2108
+ }
1716
2109
  return { devMode: currentDevMode, canToggle };
1717
2110
  }
1718
2111
  return { devMode: currentDevMode, canToggle };
1719
2112
  }));
1720
- // Mount save-key BEFORE the prefix handler so it isn't shadowed
1721
- // Only functional in Node.js environments (writes to .env file)
2113
+ // Mount save-key BEFORE the prefix handler so it isn't shadowed.
2114
+ // Persists the user's key per-owner in the SQL settings table so it
2115
+ // survives across serverless invocations (where mutating process.env
2116
+ // and writing .env are both no-ops). Also updates process.env and
2117
+ // .env when running locally for fast pickup by other handlers.
1722
2118
  getH3App(nitroApp).use(`${routePath}/save-key`, defineEventHandler(async (event) => {
1723
2119
  if (getMethod(event) !== "POST") {
1724
2120
  setResponseStatus(event, 405);
@@ -1731,19 +2127,66 @@ export function createAgentChatPlugin(options) {
1731
2127
  return { error: "API key is required" };
1732
2128
  }
1733
2129
  const trimmedKey = key.trim();
1734
- try {
1735
- const path = await import("path");
1736
- const { upsertEnvFile } = await import("./create-server.js");
1737
- const envPath = path.join(process.cwd(), ".env");
1738
- await upsertEnvFile(envPath, [
1739
- { key: "ANTHROPIC_API_KEY", value: trimmedKey },
1740
- ]);
2130
+ // Persist per-owner so the key survives cold starts in serverless
2131
+ // and so the user's key isn't shared across users on multi-tenant
2132
+ // hosted deployments. We require a real authenticated owner here —
2133
+ // `local@localhost` is the unauthenticated fallback and must never
2134
+ // become the shared key bucket on hosted deployments.
2135
+ const ownerEmail = await getOwnerFromEvent(event);
2136
+ if (isHostedProd && (!ownerEmail || ownerEmail === "local@localhost")) {
2137
+ setResponseStatus(event, 401);
2138
+ return { error: "Authentication required" };
1741
2139
  }
1742
- catch {
1743
- // Edge runtime — can't write .env, but can still update process.env
2140
+ if (ownerEmail && ownerEmail !== "local@localhost") {
2141
+ try {
2142
+ await putSetting(`user-anthropic-api-key:${ownerEmail}`, {
2143
+ key: trimmedKey,
2144
+ });
2145
+ // Verify the write actually landed — some managed DB drivers
2146
+ // swallow errors on degraded connections. Without this the
2147
+ // client sees "saved", reloads, and the usage-limit card
2148
+ // re-appears on the next message because the key isn't
2149
+ // really persisted.
2150
+ const check = await getSetting(`user-anthropic-api-key:${ownerEmail}`);
2151
+ if (!check ||
2152
+ typeof check.key !== "string" ||
2153
+ check.key !== trimmedKey) {
2154
+ throw new Error("settings write did not persist");
2155
+ }
2156
+ }
2157
+ catch (err) {
2158
+ if (isHostedProd) {
2159
+ console.error("[agent-chat] save-key persistence failed:", err instanceof Error ? err.message : err);
2160
+ setResponseStatus(event, 500);
2161
+ return {
2162
+ error: "Failed to persist API key. Please try again or contact support.",
2163
+ };
2164
+ }
2165
+ // Local dev falls through to the env-file path below.
2166
+ }
2167
+ }
2168
+ // In hosted/multi-tenant mode we deliberately do NOT touch
2169
+ // process.env or .env: the per-owner SQL lookup above is the
2170
+ // single source of truth, and overwriting the shared env key
2171
+ // would leak one tenant's credentials into every subsequent
2172
+ // request that hit the same warm instance without its own key.
2173
+ if (!isHostedProd) {
2174
+ try {
2175
+ const path = await import("path");
2176
+ const { upsertEnvFile } = await import("./create-server.js");
2177
+ const envPath = path.join(process.cwd(), ".env");
2178
+ await upsertEnvFile(envPath, [
2179
+ { key: "ANTHROPIC_API_KEY", value: trimmedKey },
2180
+ ]);
2181
+ }
2182
+ catch {
2183
+ // Edge runtime — can't write .env, but can still update process.env
2184
+ }
2185
+ // Update process.env so the agent works immediately in the
2186
+ // current local-dev invocation; the SQL persist above covers
2187
+ // future invocations.
2188
+ process.env.ANTHROPIC_API_KEY = trimmedKey;
1744
2189
  }
1745
- // Update process.env so the agent works immediately
1746
- process.env.ANTHROPIC_API_KEY = trimmedKey;
1747
2190
  return { ok: true };
1748
2191
  }));
1749
2192
  // Mount file search endpoint
@@ -2069,7 +2512,7 @@ export function createAgentChatPlugin(options) {
2069
2512
  setResponseStatus(event, 405);
2070
2513
  return { error: "Method not allowed" };
2071
2514
  }
2072
- await getOwnerFromEvent(event);
2515
+ const ownerEmail = await getOwnerFromEvent(event);
2073
2516
  const body = await readBody(event);
2074
2517
  const message = body?.message;
2075
2518
  if (!message || typeof message !== "string") {
@@ -2078,7 +2521,11 @@ export function createAgentChatPlugin(options) {
2078
2521
  }
2079
2522
  // Strip mention markup: @[Name|type] → @Name
2080
2523
  const cleanMessage = message.replace(/@\[([^\]|]+)\|[^\]]*\]/g, "@$1");
2081
- const apiKey = process.env.ANTHROPIC_API_KEY;
2524
+ // Mirror the chat-run resolution so BYO-key users have title
2525
+ // generation billed to their own key instead of the platform key.
2526
+ const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
2527
+ const userApiKey = await getOwnerAnthropicApiKey(ownerEmail);
2528
+ const apiKey = userApiKey ?? process.env.ANTHROPIC_API_KEY;
2082
2529
  if (!apiKey) {
2083
2530
  // Fallback: truncate the message
2084
2531
  return { title: cleanMessage.trim().slice(0, 60) };