@agent-native/core 0.90.3 → 0.90.4

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 (191) hide show
  1. package/bin/agent-native.js +25 -3
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +9 -0
  4. package/corpus/core/bin/agent-native.js +25 -3
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/engine/index.ts +1 -0
  7. package/corpus/core/src/agent/engine/registry.ts +63 -2
  8. package/corpus/core/src/cli/create.ts +35 -0
  9. package/corpus/core/src/cli/design-connect.ts +248 -0
  10. package/corpus/core/src/cli/index.ts +84 -7
  11. package/corpus/core/src/cli/workspacify.ts +34 -0
  12. package/corpus/core/src/client/history/VersionHistoryPanel.tsx +77 -47
  13. package/corpus/core/src/client/history/index.ts +3 -0
  14. package/corpus/core/src/client/history/use-history.ts +28 -0
  15. package/corpus/core/src/client/index.ts +8 -0
  16. package/corpus/core/src/client/review/index.ts +5 -0
  17. package/corpus/core/src/client/review/use-review.ts +40 -2
  18. package/corpus/core/src/history/actions/create-resource-version.ts +11 -8
  19. package/corpus/core/src/history/index.ts +0 -1
  20. package/corpus/core/src/history/registry.ts +24 -12
  21. package/corpus/core/src/history/store.ts +84 -53
  22. package/corpus/core/src/review/actions/consume-review-feedback.ts +15 -5
  23. package/corpus/core/src/review/actions/delete-review-comment.ts +15 -3
  24. package/corpus/core/src/review/actions/resolve-review-thread.ts +12 -5
  25. package/corpus/core/src/review/index.ts +0 -5
  26. package/corpus/core/src/review/registry.ts +24 -12
  27. package/corpus/core/src/review/store.ts +10 -7
  28. package/corpus/core/src/server/agent-chat-plugin.ts +6 -0
  29. package/corpus/core/src/server/credential-provider.ts +13 -3
  30. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +11 -11
  31. package/corpus/templates/assets/actions/generate-image.ts +64 -28
  32. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +1 -1
  33. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +91 -74
  34. package/corpus/templates/assets/server/lib/generation.ts +29 -4
  35. package/corpus/templates/assets/server/lib/image-processing.ts +131 -0
  36. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +24 -2
  37. package/corpus/templates/calendar/AGENTS.md +3 -0
  38. package/corpus/templates/calendar/actions/create-event.ts +8 -34
  39. package/corpus/templates/calendar/actions/event-action-helpers.ts +52 -0
  40. package/corpus/templates/calendar/actions/manage-event-draft.ts +7 -34
  41. package/corpus/templates/calendar/actions/update-event.ts +10 -23
  42. package/corpus/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +47 -0
  43. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +23 -0
  44. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +59 -2
  45. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +40 -1
  46. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +34 -1
  47. package/corpus/templates/calendar/app/hooks/use-events.ts +6 -0
  48. package/corpus/templates/calendar/app/i18n/zh-TW.ts +4 -0
  49. package/corpus/templates/calendar/app/i18n-data.ts +40 -0
  50. package/corpus/templates/calendar/changelog/2026-07-07-you-can-mark-event-guests-as-optional-while-inviting-them-or.md +6 -0
  51. package/corpus/templates/calendar/server/handlers/events.ts +1 -0
  52. package/corpus/templates/calendar/server/lib/google-calendar.ts +4 -0
  53. package/corpus/templates/calendar/server/plugins/agent-chat.ts +1 -1
  54. package/corpus/templates/calendar/shared/api.ts +2 -0
  55. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +29 -3
  56. package/corpus/templates/clips/actions/generate-workflow.ts +19 -8
  57. package/corpus/templates/clips/actions/get-clips-ai-prefs.ts +36 -0
  58. package/corpus/templates/clips/actions/lib/clips-ai-prefs.ts +28 -0
  59. package/corpus/templates/clips/actions/regenerate-chapters.ts +21 -7
  60. package/corpus/templates/clips/actions/regenerate-summary.ts +26 -8
  61. package/corpus/templates/clips/actions/regenerate-title.ts +44 -7
  62. package/corpus/templates/clips/actions/update-clips-ai-prefs.ts +50 -0
  63. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +7 -2
  64. package/corpus/templates/clips/app/hooks/use-auto-title.ts +25 -10
  65. package/corpus/templates/clips/app/i18n/ar-SA.ts +28 -6
  66. package/corpus/templates/clips/app/i18n/de-DE.ts +28 -6
  67. package/corpus/templates/clips/app/i18n/en-US.ts +29 -6
  68. package/corpus/templates/clips/app/i18n/es-ES.ts +29 -6
  69. package/corpus/templates/clips/app/i18n/fr-FR.ts +30 -6
  70. package/corpus/templates/clips/app/i18n/hi-IN.ts +26 -6
  71. package/corpus/templates/clips/app/i18n/ja-JP.ts +27 -6
  72. package/corpus/templates/clips/app/i18n/ko-KR.ts +28 -6
  73. package/corpus/templates/clips/app/i18n/pt-BR.ts +28 -6
  74. package/corpus/templates/clips/app/i18n/zh-CN.ts +26 -6
  75. package/corpus/templates/clips/app/i18n/zh-TW.ts +25 -6
  76. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +73 -0
  77. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -0
  78. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +70 -2
  79. package/corpus/templates/clips/changelog/2026-07-07-ai-tools-can-include-the-full-video-not-just-the-transcript-.md +6 -0
  80. package/corpus/templates/clips/changelog/2026-07-07-clips-desktop-now-surfaces-meetings-and-dictate-directly-in-.md +6 -0
  81. package/corpus/templates/clips/changelog/2026-07-07-meeting-notes-now-explain-exactly-how-to-start-granola-style.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-07-07-meeting-start-alerts-now-include-separate-start-notes-and-jo.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-07-07-recordings-start-more-predictably-and-newly-saved-clips-refr.md +6 -0
  84. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  85. package/corpus/templates/clips/desktop/src/app.tsx +435 -105
  86. package/corpus/templates/clips/desktop/src/lib/recorder.ts +27 -21
  87. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +23 -22
  88. package/corpus/templates/clips/desktop/src/styles.css +167 -2
  89. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1 -1
  90. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -7
  91. package/corpus/templates/clips/desktop/src-tauri/src/tray_meetings.rs +5 -4
  92. package/corpus/templates/clips/server/routes/_agent-native/clips/user-prefs.put.ts +16 -8
  93. package/corpus/templates/clips/shared/clips-ai-prefs.ts +81 -0
  94. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +16 -1
  95. package/corpus/templates/design/actions/add-localhost-screens.ts +1 -0
  96. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +345 -11
  97. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +89 -14
  98. package/corpus/templates/design/app/components/design/bridge-security.ts +7 -1
  99. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -1
  100. package/corpus/templates/design/app/i18n/zh-TW.ts +3 -0
  101. package/corpus/templates/design/app/i18n-data.ts +41 -0
  102. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-uses-live-app-iframes-for-click-to-edit-pr.md +6 -0
  103. package/dist/agent/engine/index.d.ts +1 -1
  104. package/dist/agent/engine/index.d.ts.map +1 -1
  105. package/dist/agent/engine/index.js +1 -1
  106. package/dist/agent/engine/index.js.map +1 -1
  107. package/dist/agent/engine/registry.d.ts +14 -0
  108. package/dist/agent/engine/registry.d.ts.map +1 -1
  109. package/dist/agent/engine/registry.js +56 -2
  110. package/dist/agent/engine/registry.js.map +1 -1
  111. package/dist/cli/create.d.ts.map +1 -1
  112. package/dist/cli/create.js +31 -0
  113. package/dist/cli/create.js.map +1 -1
  114. package/dist/cli/design-connect.d.ts.map +1 -1
  115. package/dist/cli/design-connect.js +186 -0
  116. package/dist/cli/design-connect.js.map +1 -1
  117. package/dist/cli/index.js +76 -9
  118. package/dist/cli/index.js.map +1 -1
  119. package/dist/cli/workspacify.d.ts.map +1 -1
  120. package/dist/cli/workspacify.js +30 -0
  121. package/dist/cli/workspacify.js.map +1 -1
  122. package/dist/client/history/VersionHistoryPanel.d.ts +1 -1
  123. package/dist/client/history/VersionHistoryPanel.d.ts.map +1 -1
  124. package/dist/client/history/VersionHistoryPanel.js +12 -5
  125. package/dist/client/history/VersionHistoryPanel.js.map +1 -1
  126. package/dist/client/history/index.d.ts +1 -1
  127. package/dist/client/history/index.d.ts.map +1 -1
  128. package/dist/client/history/index.js +1 -1
  129. package/dist/client/history/index.js.map +1 -1
  130. package/dist/client/history/use-history.d.ts +12 -0
  131. package/dist/client/history/use-history.d.ts.map +1 -1
  132. package/dist/client/history/use-history.js +6 -0
  133. package/dist/client/history/use-history.js.map +1 -1
  134. package/dist/client/index.d.ts +2 -2
  135. package/dist/client/index.d.ts.map +1 -1
  136. package/dist/client/index.js +2 -2
  137. package/dist/client/index.js.map +1 -1
  138. package/dist/client/review/index.d.ts +1 -1
  139. package/dist/client/review/index.d.ts.map +1 -1
  140. package/dist/client/review/index.js +1 -1
  141. package/dist/client/review/index.js.map +1 -1
  142. package/dist/client/review/use-review.d.ts +23 -0
  143. package/dist/client/review/use-review.d.ts.map +1 -1
  144. package/dist/client/review/use-review.js +8 -0
  145. package/dist/client/review/use-review.js.map +1 -1
  146. package/dist/collab/awareness.d.ts +2 -2
  147. package/dist/collab/awareness.d.ts.map +1 -1
  148. package/dist/collab/routes.d.ts +1 -1
  149. package/dist/history/actions/create-resource-version.js +7 -4
  150. package/dist/history/actions/create-resource-version.js.map +1 -1
  151. package/dist/history/index.d.ts +1 -1
  152. package/dist/history/index.d.ts.map +1 -1
  153. package/dist/history/index.js +1 -1
  154. package/dist/history/index.js.map +1 -1
  155. package/dist/history/registry.d.ts.map +1 -1
  156. package/dist/history/registry.js +14 -12
  157. package/dist/history/registry.js.map +1 -1
  158. package/dist/history/store.d.ts.map +1 -1
  159. package/dist/history/store.js +74 -49
  160. package/dist/history/store.js.map +1 -1
  161. package/dist/observability/routes.d.ts +3 -3
  162. package/dist/progress/routes.d.ts +1 -1
  163. package/dist/review/actions/consume-review-feedback.d.ts +1 -0
  164. package/dist/review/actions/consume-review-feedback.js +8 -2
  165. package/dist/review/actions/consume-review-feedback.js.map +1 -1
  166. package/dist/review/actions/delete-review-comment.d.ts +2 -1
  167. package/dist/review/actions/delete-review-comment.js +11 -3
  168. package/dist/review/actions/delete-review-comment.js.map +1 -1
  169. package/dist/review/actions/resolve-review-thread.d.ts +2 -1
  170. package/dist/review/actions/resolve-review-thread.js +5 -2
  171. package/dist/review/actions/resolve-review-thread.js.map +1 -1
  172. package/dist/review/index.d.ts +1 -1
  173. package/dist/review/index.d.ts.map +1 -1
  174. package/dist/review/index.js +1 -1
  175. package/dist/review/index.js.map +1 -1
  176. package/dist/review/registry.d.ts.map +1 -1
  177. package/dist/review/registry.js +14 -12
  178. package/dist/review/registry.js.map +1 -1
  179. package/dist/review/store.d.ts +3 -3
  180. package/dist/review/store.d.ts.map +1 -1
  181. package/dist/review/store.js +7 -4
  182. package/dist/review/store.js.map +1 -1
  183. package/dist/secrets/routes.d.ts +9 -9
  184. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  185. package/dist/server/agent-chat-plugin.js +5 -0
  186. package/dist/server/agent-chat-plugin.js.map +1 -1
  187. package/dist/server/credential-provider.d.ts +2 -0
  188. package/dist/server/credential-provider.d.ts.map +1 -1
  189. package/dist/server/credential-provider.js +13 -3
  190. package/dist/server/credential-provider.js.map +1 -1
  191. package/package.json +1 -1
@@ -1,17 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, statSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  const binDir = dirname(fileURLToPath(import.meta.url));
9
9
  const distEntry = join(binDir, "../dist/cli/index.js");
10
+ const sourceEntry = join(binDir, "../src/cli/index.ts");
11
+ const freshnessChecks = [
12
+ [sourceEntry, distEntry],
13
+ [
14
+ join(binDir, "../src/cli/design-connect.ts"),
15
+ join(binDir, "../dist/cli/design-connect.js"),
16
+ ],
17
+ ];
10
18
 
11
- if (existsSync(distEntry)) {
19
+ function shouldUseSourceFallback() {
20
+ if (!existsSync(sourceEntry)) return false;
21
+ if (!existsSync(distEntry)) return true;
22
+ try {
23
+ return freshnessChecks.some(
24
+ ([source, dist]) =>
25
+ existsSync(source) &&
26
+ existsSync(dist) &&
27
+ statSync(source).mtimeMs > statSync(dist).mtimeMs,
28
+ );
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ if (!shouldUseSourceFallback() && existsSync(distEntry)) {
12
35
  await import(pathToFileURL(distEntry).href);
13
36
  } else {
14
- const sourceEntry = join(binDir, "../src/cli/index.ts");
15
37
  if (!existsSync(sourceEntry)) {
16
38
  console.error(
17
39
  "agent-native CLI build output is missing. Run `pnpm --filter @agent-native/core build` and try again.",
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2144
31
- - template files: 4806
31
+ - template files: 4817
@@ -1,5 +1,14 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.90.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 9f396bc: Fail closed for unregistered History/Review resource types, allocate unique version numbers atomically, verify resolve/consume update counts, and prefer server snapshots over client-supplied ones.
8
+ - 9f396bc: Add live visual-edit bridge support for localhost apps so Design can render editable live iframes instead of static HTML snapshots.
9
+ - 9f396bc: Expire sticky provider auth-failure markers, skip rejected deploy env keys during engine auto-detect, and clear failure markers from the legacy save-key path.
10
+ - 9f396bc: Keep React Router build packages installed for scaffolded app builds and report missing build dependencies before spawning the CLI.
11
+
3
12
  ## 0.90.3
4
13
 
5
14
  ### Patch Changes
@@ -1,17 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, statSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  const binDir = dirname(fileURLToPath(import.meta.url));
9
9
  const distEntry = join(binDir, "../dist/cli/index.js");
10
+ const sourceEntry = join(binDir, "../src/cli/index.ts");
11
+ const freshnessChecks = [
12
+ [sourceEntry, distEntry],
13
+ [
14
+ join(binDir, "../src/cli/design-connect.ts"),
15
+ join(binDir, "../dist/cli/design-connect.js"),
16
+ ],
17
+ ];
10
18
 
11
- if (existsSync(distEntry)) {
19
+ function shouldUseSourceFallback() {
20
+ if (!existsSync(sourceEntry)) return false;
21
+ if (!existsSync(distEntry)) return true;
22
+ try {
23
+ return freshnessChecks.some(
24
+ ([source, dist]) =>
25
+ existsSync(source) &&
26
+ existsSync(dist) &&
27
+ statSync(source).mtimeMs > statSync(dist).mtimeMs,
28
+ );
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ if (!shouldUseSourceFallback() && existsSync(distEntry)) {
12
35
  await import(pathToFileURL(distEntry).href);
13
36
  } else {
14
- const sourceEntry = join(binDir, "../src/cli/index.ts");
15
37
  if (!existsSync(sourceEntry)) {
16
38
  console.error(
17
39
  "agent-native CLI build output is missing. Run `pnpm --filter @agent-native/core build` and try again.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.90.3",
3
+ "version": "0.90.4",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -25,6 +25,7 @@ export {
25
25
  getStoredModelForEngine,
26
26
  normalizeModelForEngine,
27
27
  detectEngineFromEnv,
28
+ detectEngineFromEnvForRequest,
28
29
  detectEngineFromUserSecrets,
29
30
  isAgentEngineSettingConfigured,
30
31
  isAgentEnginePackageInstalled,
@@ -156,6 +156,10 @@ function assertAgentEnginePackageInstalled(entry: AgentEngineEntry): void {
156
156
  * on the first pass, so an explicit provider key (ANTHROPIC_API_KEY etc.)
157
157
  * is picked instead. Builder is still used as the fallback when no other
158
158
  * provider key is set.
159
+ *
160
+ * This sync helper is for CLI/status callers that cannot await settings. Prefer
161
+ * {@link detectEngineFromEnvForRequest} at request time so sticky auth-failure
162
+ * markers can skip rejected deploy keys.
159
163
  */
160
164
  export function detectEngineFromEnv(): AgentEngineEntry | null {
161
165
  const preferByo = /^(1|true)$/i.test(
@@ -196,6 +200,59 @@ export function detectEngineFromEnv(): AgentEngineEntry | null {
196
200
  return null;
197
201
  }
198
202
 
203
+ async function envKeyUsableForEntry(
204
+ entry: AgentEngineEntry,
205
+ key: string,
206
+ ): Promise<boolean> {
207
+ if (
208
+ !(
209
+ canUseDeployCredentialFallbackForRequest(key) &&
210
+ !!readDeployCredentialEnv(key)
211
+ )
212
+ ) {
213
+ return false;
214
+ }
215
+ if (entry.name === "builder") {
216
+ return true;
217
+ }
218
+ const value = readDeployCredentialEnv(key);
219
+ if (!value) return false;
220
+ return !(await getProviderCredentialAuthFailure({ key, value }));
221
+ }
222
+
223
+ async function hasUsableEnvKeys(entry: AgentEngineEntry): Promise<boolean> {
224
+ if (!isAgentEnginePackageInstalled(entry)) return false;
225
+ if (entry.requiredEnvVars.length === 0) return false;
226
+ for (const key of entry.requiredEnvVars) {
227
+ if (!(await envKeyUsableForEntry(entry, key))) return false;
228
+ }
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * Request-aware env auto-detect. Same priority as {@link detectEngineFromEnv},
234
+ * but skips provider keys that currently have an auth-failure marker so a
235
+ * rejected deploy key does not permanently win selection and leave chat stuck
236
+ * on `missing_credentials`.
237
+ */
238
+ export async function detectEngineFromEnvForRequest(): Promise<AgentEngineEntry | null> {
239
+ const preferByo = /^(1|true)$/i.test(
240
+ process.env.AGENT_ENGINE_PREFER_BYO_KEY ?? "",
241
+ );
242
+
243
+ if (preferByo) {
244
+ for (const entry of _registry.values()) {
245
+ if (entry.name === "builder") continue;
246
+ if (await hasUsableEnvKeys(entry)) return entry;
247
+ }
248
+ }
249
+
250
+ for (const entry of _registry.values()) {
251
+ if (await hasUsableEnvKeys(entry)) return entry;
252
+ }
253
+ return null;
254
+ }
255
+
199
256
  function shouldTraceEngineDetection(): boolean {
200
257
  return /^(1|true)$/i.test(
201
258
  process.env.AGENT_NATIVE_DEBUG_AGENT_ENGINE_DETECT ??
@@ -399,6 +456,9 @@ async function engineCreateConfigForEntry(
399
456
  * AND an API key for it is reachable via the engine's required env vars.
400
457
  * Inline keys on the global settings row are ignored; see
401
458
  * `isAgentEngineSettingConfigured`.
459
+ *
460
+ * Sync helper for CLI/status. Prefer {@link isStoredEngineUsableForRequest}
461
+ * so sticky auth-failure markers are respected.
402
462
  */
403
463
  export function isStoredEngineUsable(
404
464
  stored: unknown,
@@ -618,8 +678,9 @@ export async function resolveEngine(
618
678
  }
619
679
 
620
680
  // 8. Auto-detect from any provider env var — so just dropping a key in
621
- // .env works without also setting AGENT_ENGINE.
622
- const detected = detectEngineFromEnv();
681
+ // .env works without also setting AGENT_ENGINE. Skip keys with active
682
+ // auth-failure markers so a rejected deploy key cannot permanently win.
683
+ const detected = await detectEngineFromEnvForRequest();
623
684
  if (detected) {
624
685
  return detected.create(await engineCreateConfigForEntry(detected, apiKey));
625
686
  }
@@ -27,6 +27,12 @@ const STANDALONE_EXACT_DEPENDENCY_OVERRIDES: Record<string, string> = {
27
27
  "@react-router/fs-routes": "8.1.0",
28
28
  "react-router": "8.1.0",
29
29
  };
30
+ const REACT_ROUTER_BUILD_DEPENDENCIES = [
31
+ "@react-router/dev",
32
+ "@react-router/fs-routes",
33
+ "react-router",
34
+ "vite",
35
+ ] as const;
30
36
  const SENTRY_MINIMUM_RELEASE_AGE_EXCLUDES = ['"@sentry/*"'];
31
37
  const FIRST_PARTY_TARBALL_SYMLINK_EXCLUDES = [
32
38
  "*/CLAUDE.md",
@@ -1045,6 +1051,7 @@ function postProcessStandalone(
1045
1051
  // under pnpm 10+ without prompting for `pnpm approve-builds`.
1046
1052
  pkg.dependencies = pkg.dependencies ?? {};
1047
1053
  pkg.dependencies.postgres ??= POSTGRES_DEPENDENCY_VERSION;
1054
+ ensureReactRouterBuildDependencies(pkg);
1048
1055
 
1049
1056
  const requiredBuilt = ["better-sqlite3", "esbuild", "node-pty"];
1050
1057
  if (!pkg.pnpm || typeof pkg.pnpm !== "object") {
@@ -1105,6 +1112,34 @@ function postProcessStandalone(
1105
1112
  setupAgentSymlinks(targetDir);
1106
1113
  }
1107
1114
 
1115
+ function ensureReactRouterBuildDependencies(pkg: Record<string, any>): void {
1116
+ const allDeps = {
1117
+ ...pkg.dependencies,
1118
+ ...pkg.devDependencies,
1119
+ ...pkg.peerDependencies,
1120
+ };
1121
+ if (
1122
+ !allDeps["@react-router/dev"] &&
1123
+ !allDeps["react-router"] &&
1124
+ !allDeps["@react-router/fs-routes"]
1125
+ ) {
1126
+ return;
1127
+ }
1128
+
1129
+ pkg.dependencies = pkg.dependencies ?? {};
1130
+ for (const key of REACT_ROUTER_BUILD_DEPENDENCIES) {
1131
+ const existing =
1132
+ pkg.dependencies[key] ??
1133
+ pkg.devDependencies?.[key] ??
1134
+ pkg.peerDependencies?.[key];
1135
+ if (!existing) continue;
1136
+ pkg.dependencies[key] =
1137
+ STANDALONE_EXACT_DEPENDENCY_OVERRIDES[key] ?? existing;
1138
+ delete pkg.devDependencies?.[key];
1139
+ delete pkg.peerDependencies?.[key];
1140
+ }
1141
+ }
1142
+
1108
1143
  function fixStandaloneTsconfig(targetDir: string, templateName?: string): void {
1109
1144
  const tsconfigPath = path.join(targetDir, "tsconfig.json");
1110
1145
  if (!fs.existsSync(tsconfigPath)) return;
@@ -507,6 +507,47 @@ function sendJson(
507
507
  res.end(`${JSON.stringify(body, null, 2)}\n`);
508
508
  }
509
509
 
510
+ function sendText(
511
+ res: ServerResponse,
512
+ statusCode: number,
513
+ body: string,
514
+ contentType: string,
515
+ ) {
516
+ res.writeHead(statusCode, {
517
+ "content-type": contentType,
518
+ "access-control-allow-origin": "*",
519
+ "access-control-allow-methods": "GET, POST, OPTIONS",
520
+ "access-control-allow-headers": "content-type, x-bridge-token",
521
+ "access-control-allow-private-network": "true",
522
+ });
523
+ res.end(body);
524
+ }
525
+
526
+ function sendBytes(
527
+ res: ServerResponse,
528
+ statusCode: number,
529
+ body: Buffer,
530
+ headers: Headers,
531
+ ) {
532
+ const responseHeaders: Record<string, string> = {
533
+ "access-control-allow-origin": "*",
534
+ "access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
535
+ "access-control-allow-headers": "content-type, x-bridge-token",
536
+ "access-control-allow-private-network": "true",
537
+ };
538
+ for (const name of [
539
+ "content-type",
540
+ "cache-control",
541
+ "etag",
542
+ "last-modified",
543
+ ]) {
544
+ const value = headers.get(name);
545
+ if (value) responseHeaders[name] = value;
546
+ }
547
+ res.writeHead(statusCode, responseHeaders);
548
+ res.end(body);
549
+ }
550
+
510
551
  function sameOrigin(a: string, b: string): boolean {
511
552
  try {
512
553
  return new URL(a).origin === new URL(b).origin;
@@ -531,6 +572,23 @@ function resolvePreviewSnapshotUrl(
531
572
  return parsed.toString();
532
573
  }
533
574
 
575
+ function resolvePreviewProxyUrl(
576
+ devServerUrl: string,
577
+ requestUrl: string | undefined,
578
+ ): string {
579
+ const base = normalizeHttpUrl(devServerUrl);
580
+ const parsedRequest = new URL(requestUrl ?? "/", base);
581
+ const parsed = new URL(
582
+ `${parsedRequest.pathname}${parsedRequest.search}`,
583
+ `${base}/`,
584
+ );
585
+ parsed.hash = "";
586
+ if (!sameOrigin(parsed.toString(), base)) {
587
+ throw new Error("Proxy URL must stay on the connected dev server.");
588
+ }
589
+ return parsed.toString();
590
+ }
591
+
534
592
  async function fetchPreviewSnapshot(
535
593
  devServerUrl: string,
536
594
  targetUrl: string,
@@ -579,6 +637,79 @@ async function fetchPreviewSnapshot(
579
637
  }
580
638
  }
581
639
 
640
+ async function fetchPreviewProxyResource(
641
+ devServerUrl: string,
642
+ targetUrl: string,
643
+ redirects = 0,
644
+ ): Promise<{
645
+ url: string;
646
+ status: number;
647
+ headers: Headers;
648
+ body: Buffer;
649
+ }> {
650
+ if (redirects > 5) {
651
+ throw new Error("Too many redirects while proxying preview resource.");
652
+ }
653
+ const controller = new AbortController();
654
+ const timeout = setTimeout(() => controller.abort(), 10_000);
655
+ try {
656
+ const response = await fetch(targetUrl, {
657
+ method: "GET",
658
+ redirect: "manual",
659
+ signal: controller.signal,
660
+ });
661
+ const location = response.headers.get("location");
662
+ if (location && response.status >= 300 && response.status < 400) {
663
+ const redirected = new URL(location, targetUrl);
664
+ redirected.hash = "";
665
+ if (!sameOrigin(redirected.toString(), devServerUrl)) {
666
+ throw new Error("Proxy redirect left the connected dev server.");
667
+ }
668
+ return fetchPreviewProxyResource(
669
+ devServerUrl,
670
+ redirected.toString(),
671
+ redirects + 1,
672
+ );
673
+ }
674
+ return {
675
+ url: response.url || targetUrl,
676
+ status: response.status,
677
+ headers: response.headers,
678
+ body: Buffer.from(await response.arrayBuffer()),
679
+ };
680
+ } finally {
681
+ clearTimeout(timeout);
682
+ }
683
+ }
684
+
685
+ function addLiveEditBaseHref(html: string, href: string): string {
686
+ const escapedHref = href.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
687
+ const baseTag = `<base href="${escapedHref}">`;
688
+ if (/<base\b/i.test(html)) return html;
689
+ if (/<head\b[^>]*>/i.test(html)) {
690
+ return html.replace(/<head\b[^>]*>/i, (match) => `${match}${baseTag}`);
691
+ }
692
+ if (/<html\b[^>]*>/i.test(html)) {
693
+ return html.replace(
694
+ /<html\b[^>]*>/i,
695
+ (match) => `${match}<head>${baseTag}</head>`,
696
+ );
697
+ }
698
+ return `<!DOCTYPE html><html><head>${baseTag}<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${html}</body></html>`;
699
+ }
700
+
701
+ function injectLiveEditBridge(html: string, targetUrl: string, script: string) {
702
+ const withBase = addLiveEditBaseHref(html, targetUrl);
703
+ if (!script) return withBase;
704
+ if (withBase.includes("</body>")) {
705
+ return withBase.replace("</body>", `${script}</body>`);
706
+ }
707
+ if (withBase.includes("</html>")) {
708
+ return withBase.replace("</html>", `${script}</html>`);
709
+ }
710
+ return `${withBase}${script}`;
711
+ }
712
+
582
713
  /** Read the full request body as a UTF-8 string. */
583
714
  async function readRequestBody(req: IncomingMessage): Promise<string> {
584
715
  return new Promise<string>((resolve, reject) => {
@@ -1046,6 +1177,7 @@ export async function startDesignConnectBridge(
1046
1177
  // an unauthenticated caller cannot read it. The server-side grant action
1047
1178
  // obtains it out-of-band (via the exported bridge reference).
1048
1179
  const bridgeToken = crypto.randomBytes(32).toString("hex");
1180
+ let liveEditBridgeScript = "";
1049
1181
 
1050
1182
  const server = http.createServer(
1051
1183
  (req: IncomingMessage, res: ServerResponse) => {
@@ -1077,6 +1209,95 @@ export async function startDesignConnectBridge(
1077
1209
  sendJson(res, 200, { ok: true, source: manifest.source });
1078
1210
  return;
1079
1211
  }
1212
+ if (pathname === "/live-edit-bridge") {
1213
+ if (req.method !== "POST") {
1214
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
1215
+ return;
1216
+ }
1217
+ const tokenHeader = req.headers["x-bridge-token"];
1218
+ const providedToken =
1219
+ typeof tokenHeader === "string" ? tokenHeader : "";
1220
+ let tokenValid = false;
1221
+ try {
1222
+ tokenValid =
1223
+ providedToken.length === bridgeToken.length &&
1224
+ crypto.timingSafeEqual(
1225
+ Buffer.from(providedToken, "utf8"),
1226
+ Buffer.from(bridgeToken, "utf8"),
1227
+ );
1228
+ } catch {
1229
+ tokenValid = false;
1230
+ }
1231
+ if (!tokenValid) {
1232
+ sendJson(res, 401, {
1233
+ ok: false,
1234
+ error: "invalid or missing bridge token",
1235
+ });
1236
+ return;
1237
+ }
1238
+ void (async () => {
1239
+ try {
1240
+ const raw = await readRequestBody(req);
1241
+ const body = JSON.parse(raw) as Record<string, unknown>;
1242
+ const script =
1243
+ typeof body["script"] === "string" ? body["script"] : "";
1244
+ if (!script.includes("agent-native:editor-chrome-ready")) {
1245
+ sendJson(res, 400, {
1246
+ ok: false,
1247
+ error: "script must install the Agent Native editor bridge",
1248
+ });
1249
+ return;
1250
+ }
1251
+ liveEditBridgeScript = script;
1252
+ sendJson(res, 200, { ok: true });
1253
+ } catch (err: unknown) {
1254
+ sendJson(res, 400, {
1255
+ ok: false,
1256
+ error: err instanceof Error ? err.message : String(err),
1257
+ });
1258
+ }
1259
+ })();
1260
+ return;
1261
+ }
1262
+ if (pathname === "/live-edit") {
1263
+ if (req.method !== "GET") {
1264
+ sendJson(res, 405, { ok: false, error: "method not allowed" });
1265
+ return;
1266
+ }
1267
+ void (async () => {
1268
+ try {
1269
+ const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
1270
+ const targetUrl = resolvePreviewSnapshotUrl(
1271
+ manifest.devServerUrl,
1272
+ requestUrl.searchParams.get("url") ??
1273
+ requestUrl.searchParams.get("path"),
1274
+ );
1275
+ const snapshot = await fetchPreviewSnapshot(
1276
+ manifest.devServerUrl,
1277
+ targetUrl,
1278
+ );
1279
+ const html = injectLiveEditBridge(
1280
+ snapshot.html,
1281
+ new URL("/", manifest.bridgeUrl).toString(),
1282
+ liveEditBridgeScript,
1283
+ );
1284
+ sendText(
1285
+ res,
1286
+ snapshot.status >= 400 ? snapshot.status : 200,
1287
+ html,
1288
+ snapshot.contentType.includes("html")
1289
+ ? snapshot.contentType
1290
+ : "text/html; charset=utf-8",
1291
+ );
1292
+ } catch (err: unknown) {
1293
+ sendJson(res, 400, {
1294
+ ok: false,
1295
+ error: err instanceof Error ? err.message : String(err),
1296
+ });
1297
+ }
1298
+ })();
1299
+ return;
1300
+ }
1080
1301
  if (pathname === "/snapshot") {
1081
1302
  if (req.method !== "GET") {
1082
1303
  sendJson(res, 405, { ok: false, error: "method not allowed" });
@@ -1356,6 +1577,33 @@ export async function startDesignConnectBridge(
1356
1577
  return;
1357
1578
  }
1358
1579
 
1580
+ if (req.method === "GET" || req.method === "HEAD") {
1581
+ void (async () => {
1582
+ try {
1583
+ const targetUrl = resolvePreviewProxyUrl(
1584
+ manifest.devServerUrl,
1585
+ req.url,
1586
+ );
1587
+ const proxied = await fetchPreviewProxyResource(
1588
+ manifest.devServerUrl,
1589
+ targetUrl,
1590
+ );
1591
+ sendBytes(
1592
+ res,
1593
+ proxied.status >= 400 ? proxied.status : 200,
1594
+ req.method === "HEAD" ? Buffer.alloc(0) : proxied.body,
1595
+ proxied.headers,
1596
+ );
1597
+ } catch (err: unknown) {
1598
+ sendJson(res, 400, {
1599
+ ok: false,
1600
+ error: err instanceof Error ? err.message : String(err),
1601
+ });
1602
+ }
1603
+ })();
1604
+ return;
1605
+ }
1606
+
1359
1607
  sendJson(res, 404, { ok: false, error: "not found" });
1360
1608
  },
1361
1609
  );
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { execSync, spawn } from "child_process";
4
4
  import fs from "fs";
5
+ import { createRequire } from "module";
5
6
  import path from "path";
6
7
  import { fileURLToPath } from "url";
7
8
 
@@ -253,11 +254,20 @@ function handleScaffoldImportError(err: any): never {
253
254
  throw err;
254
255
  }
255
256
 
257
+ function findBinUpwards(binName: string): string | undefined {
258
+ let dir = process.cwd();
259
+ for (let i = 0; i < 20; i++) {
260
+ const candidate = path.join(dir, "node_modules", ".bin", binName);
261
+ if (fs.existsSync(candidate)) return candidate;
262
+ const parent = path.dirname(dir);
263
+ if (parent === dir) break;
264
+ dir = parent;
265
+ }
266
+ return undefined;
267
+ }
268
+
256
269
  function findViteBin(): string {
257
- // Look for vite in node_modules/.bin
258
- const localVite = path.resolve("node_modules/.bin/vite");
259
- if (fs.existsSync(localVite)) return localVite;
260
- return "vite"; // fallback to PATH
270
+ return findBinUpwards("vite") ?? "vite";
261
271
  }
262
272
 
263
273
  function findTsxBin(): string {
@@ -277,9 +287,7 @@ function findTypeScriptCompilerBin(): string {
277
287
  }
278
288
 
279
289
  function findReactRouterBin(): string {
280
- const localBin = path.resolve("node_modules/.bin/react-router");
281
- if (fs.existsSync(localBin)) return localBin;
282
- return "react-router";
290
+ return findBinUpwards("react-router") ?? "react-router";
283
291
  }
284
292
 
285
293
  /** Check if the project uses React Router framework mode (has react-router.config.ts) */
@@ -290,6 +298,73 @@ function isReactRouterFramework(): boolean {
290
298
  );
291
299
  }
292
300
 
301
+ function canResolveFromProject(specifier: string): boolean {
302
+ try {
303
+ const requireFromProject = createRequire(path.resolve("package.json"));
304
+ requireFromProject.resolve(specifier);
305
+ return true;
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ function projectUsesFsRoutes(): boolean {
312
+ for (const file of [
313
+ path.resolve("app/routes.ts"),
314
+ path.resolve("app/routes.tsx"),
315
+ path.resolve("routes.ts"),
316
+ path.resolve("routes.tsx"),
317
+ ]) {
318
+ try {
319
+ if (
320
+ fs.existsSync(file) &&
321
+ fs.readFileSync(file, "utf-8").includes("@react-router/fs-routes")
322
+ ) {
323
+ return true;
324
+ }
325
+ } catch {}
326
+ }
327
+ return false;
328
+ }
329
+
330
+ function validateReactRouterBuildDependencies(): void {
331
+ const required: Array<[packageName: string, specifier: string]> = [
332
+ ["react-router", "react-router"],
333
+ ["@react-router/dev", "@react-router/dev/vite"],
334
+ ["vite", "vite"],
335
+ ];
336
+ if (projectUsesFsRoutes()) {
337
+ required.push(["@react-router/fs-routes", "@react-router/fs-routes"]);
338
+ }
339
+
340
+ const missing = required
341
+ .filter(([, specifier]) => !canResolveFromProject(specifier))
342
+ .map(([packageName]) => packageName);
343
+
344
+ if (missing.length === 0) return;
345
+
346
+ const packageManager = fs.existsSync(path.resolve("pnpm-lock.yaml"))
347
+ ? "pnpm"
348
+ : fs.existsSync(path.resolve("yarn.lock"))
349
+ ? "yarn"
350
+ : "npm";
351
+ const installCommand =
352
+ packageManager === "pnpm"
353
+ ? `pnpm add ${missing.join(" ")}`
354
+ : packageManager === "yarn"
355
+ ? `yarn add ${missing.join(" ")}`
356
+ : `npm install ${missing.join(" ")}`;
357
+
358
+ console.error(
359
+ [
360
+ "React Router framework mode requires build packages that are not installed from this app root.",
361
+ `Missing: ${missing.join(", ")}`,
362
+ `Install them with: ${installCommand}`,
363
+ ].join("\n"),
364
+ );
365
+ process.exit(1);
366
+ }
367
+
293
368
  function isWorkspaceRoot(): boolean {
294
369
  const pkgPath = path.resolve("package.json");
295
370
  if (!fs.existsSync(pkgPath)) return false;
@@ -488,6 +563,7 @@ switch (command) {
488
563
  // continuation only runs on success.
489
564
  (async () => {
490
565
  if (isReactRouterFramework()) {
566
+ validateReactRouterBuildDependencies();
491
567
  const rr = findReactRouterBin();
492
568
  console.log("Building (React Router framework mode)...");
493
569
  await runBuildStep(rr, ["build"], { label: "react-router-build" });
@@ -592,6 +668,7 @@ switch (command) {
592
668
  // Run TypeScript type checking
593
669
  // React Router framework mode generates route types first
594
670
  if (isReactRouterFramework()) {
671
+ validateReactRouterBuildDependencies();
595
672
  const rr = findReactRouterBin();
596
673
  try {
597
674
  execSync(`${rr} typegen`, { stdio: "inherit" });