@agent-native/core 0.80.11 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +47 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/design-connect.ts +419 -19
  5. package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
  6. package/corpus/core/src/client/chat/index.ts +1 -0
  7. package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
  8. package/corpus/core/src/client/chat-view-transition.ts +33 -2
  9. package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
  10. package/corpus/core/src/client/index.ts +1 -0
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
  12. package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
  13. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
  14. package/corpus/core/src/db/client.ts +29 -2
  15. package/corpus/core/src/provider-api/index.ts +29 -0
  16. package/corpus/core/src/server/core-routes-plugin.ts +58 -20
  17. package/corpus/core/src/styles/agent-native.css +54 -0
  18. package/corpus/core/src/usage/store.ts +2 -2
  19. package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
  20. package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
  21. package/corpus/templates/analytics/app/global.css +8 -0
  22. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
  23. package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
  24. package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
  25. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
  26. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
  27. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
  28. package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
  33. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
  34. package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
  35. package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
  36. package/corpus/templates/assets/app/global.css +15 -0
  37. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
  38. package/corpus/templates/assets/app/routes/library.tsx +90 -16
  39. package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
  40. package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
  42. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
  43. package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
  45. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  46. package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
  47. package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
  48. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
  49. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
  50. package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
  51. package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
  52. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
  53. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
  54. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
  55. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
  56. package/corpus/templates/design/AGENTS.md +27 -0
  57. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
  58. package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
  59. package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
  60. package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
  61. package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
  62. package/corpus/templates/design/actions/capture-design-state.ts +27 -4
  63. package/corpus/templates/design/actions/connect-localhost.ts +59 -22
  64. package/corpus/templates/design/actions/export-zip.ts +23 -8
  65. package/corpus/templates/design/actions/generate-design.ts +53 -1
  66. package/corpus/templates/design/actions/get-component-details.ts +5 -19
  67. package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
  68. package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
  69. package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
  70. package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
  71. package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
  72. package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
  73. package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
  74. package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
  75. package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
  76. package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
  77. package/corpus/templates/design/actions/navigate.ts +18 -3
  78. package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
  79. package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
  80. package/corpus/templates/design/actions/provider-api-request.ts +3 -3
  81. package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
  82. package/corpus/templates/design/actions/run-design-audit.ts +53 -4
  83. package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
  84. package/corpus/templates/design/actions/view-screen.ts +1 -1
  85. package/corpus/templates/design/actions/write-local-file.ts +213 -0
  86. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
  87. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
  88. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
  89. package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
  90. package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
  91. package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
  92. package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
  93. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
  94. package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
  95. package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
  96. package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
  97. package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
  98. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
  99. package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
  100. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
  101. package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
  102. package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
  103. package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
  104. package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
  105. package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
  106. package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
  107. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
  108. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
  109. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
  110. package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
  111. package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
  112. package/corpus/templates/design/app/global.css +7 -0
  113. package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
  114. package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
  115. package/corpus/templates/design/app/i18n-data.ts +358 -0
  116. package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
  117. package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
  118. package/corpus/templates/design/app/pages/Index.tsx +5 -1
  119. package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
  120. package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
  121. package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
  122. package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
  123. package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
  124. package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
  125. package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
  126. package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
  127. package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
  128. package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
  129. package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
  130. package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
  131. package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
  132. package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
  133. package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
  134. package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
  135. package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
  136. package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
  137. package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
  138. package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
  143. package/corpus/templates/design/e2e/helpers.ts +168 -65
  144. package/corpus/templates/design/package.json +3 -1
  145. package/corpus/templates/design/server/db/schema.ts +23 -0
  146. package/corpus/templates/design/server/handlers/uploads.ts +22 -1
  147. package/corpus/templates/design/server/lib/provider-api.ts +7 -4
  148. package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
  149. package/corpus/templates/design/server/plugins/db.ts +39 -0
  150. package/corpus/templates/design/server/register-secrets.ts +38 -0
  151. package/corpus/templates/design/shared/board-file.ts +228 -0
  152. package/corpus/templates/design/shared/board-objects.ts +288 -0
  153. package/corpus/templates/design/shared/code-layer.ts +52 -0
  154. package/corpus/templates/design/shared/motion-compiler.ts +58 -7
  155. package/corpus/templates/design/shared/motion-timeline.ts +2 -2
  156. package/corpus/templates/design/tsconfig.json +2 -1
  157. package/corpus/templates/slides/AGENTS.md +9 -0
  158. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
  159. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
  160. package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
  161. package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
  162. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
  163. package/corpus/templates/slides/app/vite-env.d.ts +18 -0
  164. package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
  165. package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
  166. package/corpus/templates/slides/package.json +1 -0
  167. package/corpus/templates/slides/vite.config.ts +1 -0
  168. package/dist/cli/design-connect.d.ts +25 -0
  169. package/dist/cli/design-connect.d.ts.map +1 -1
  170. package/dist/cli/design-connect.js +357 -18
  171. package/dist/cli/design-connect.js.map +1 -1
  172. package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
  173. package/dist/client/ConnectBuilderCard.js +1 -0
  174. package/dist/client/ConnectBuilderCard.js.map +1 -1
  175. package/dist/client/chat/index.d.ts +1 -1
  176. package/dist/client/chat/index.d.ts.map +1 -1
  177. package/dist/client/chat/index.js +1 -1
  178. package/dist/client/chat/index.js.map +1 -1
  179. package/dist/client/chat/run-recovery.d.ts.map +1 -1
  180. package/dist/client/chat/run-recovery.js +5 -5
  181. package/dist/client/chat/run-recovery.js.map +1 -1
  182. package/dist/client/chat-view-transition.d.ts +6 -0
  183. package/dist/client/chat-view-transition.d.ts.map +1 -1
  184. package/dist/client/chat-view-transition.js +23 -2
  185. package/dist/client/chat-view-transition.js.map +1 -1
  186. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  187. package/dist/client/composer/PromptComposer.js +3 -2
  188. package/dist/client/composer/PromptComposer.js.map +1 -1
  189. package/dist/client/index.d.ts +1 -1
  190. package/dist/client/index.d.ts.map +1 -1
  191. package/dist/client/index.js +1 -1
  192. package/dist/client/index.js.map +1 -1
  193. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  194. package/dist/client/org/OrgSwitcher.js +11 -4
  195. package/dist/client/org/OrgSwitcher.js.map +1 -1
  196. package/dist/client/sharing/ShareButton.js +3 -3
  197. package/dist/client/sharing/ShareButton.js.map +1 -1
  198. package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
  199. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  200. package/dist/client/use-agent-chat-home-handoff.js +20 -6
  201. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  202. package/dist/collab/awareness.d.ts +2 -2
  203. package/dist/collab/awareness.d.ts.map +1 -1
  204. package/dist/collab/routes.d.ts +2 -2
  205. package/dist/db/client.d.ts +1 -0
  206. package/dist/db/client.d.ts.map +1 -1
  207. package/dist/db/client.js +28 -2
  208. package/dist/db/client.js.map +1 -1
  209. package/dist/notifications/routes.d.ts +3 -3
  210. package/dist/observability/routes.d.ts +2 -2
  211. package/dist/progress/routes.d.ts +1 -1
  212. package/dist/provider-api/index.d.ts +5 -4
  213. package/dist/provider-api/index.d.ts.map +1 -1
  214. package/dist/provider-api/index.js +28 -0
  215. package/dist/provider-api/index.js.map +1 -1
  216. package/dist/resources/handlers.d.ts +3 -3
  217. package/dist/server/core-routes-plugin.d.ts +25 -0
  218. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  219. package/dist/server/core-routes-plugin.js +43 -18
  220. package/dist/server/core-routes-plugin.js.map +1 -1
  221. package/dist/server/transcribe-voice.d.ts +1 -1
  222. package/dist/styles/agent-native.css +54 -0
  223. package/dist/usage/store.js +2 -2
  224. package/dist/usage/store.js.map +1 -1
  225. package/package.json +1 -1
@@ -0,0 +1,292 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import { executeProviderApiRequest } from "../server/lib/provider-api.js";
5
+
6
+ const FIGMA_FILE_KEY_RE = /^[A-Za-z0-9_-]{8,}$/;
7
+
8
+ const FigmaRenderFormatSchema = z.enum(["svg", "png"]).default("svg");
9
+
10
+ const schemaInput = z
11
+ .object({
12
+ fileUrl: z
13
+ .string()
14
+ .trim()
15
+ .optional()
16
+ .describe(
17
+ "Figma file URL, for example https://www.figma.com/design/<fileKey>/...",
18
+ ),
19
+ fileKey: z
20
+ .string()
21
+ .trim()
22
+ .optional()
23
+ .describe("Figma file key. Used when fileUrl is omitted."),
24
+ query: z
25
+ .string()
26
+ .trim()
27
+ .optional()
28
+ .describe("Optional case-insensitive search over names/descriptions."),
29
+ limit: z.coerce
30
+ .number()
31
+ .int()
32
+ .min(1)
33
+ .max(64)
34
+ .default(48)
35
+ .describe("Maximum number of Figma library assets to return."),
36
+ renderFormat: FigmaRenderFormatSchema.describe(
37
+ "Image format for insertable render URLs.",
38
+ ),
39
+ })
40
+ .refine((value) => value.fileUrl || value.fileKey, {
41
+ message: "Pass fileUrl or fileKey.",
42
+ path: ["fileUrl"],
43
+ });
44
+
45
+ type FigmaProviderEnvelope = {
46
+ response?: {
47
+ ok?: boolean;
48
+ status?: number;
49
+ statusText?: string;
50
+ json?: unknown;
51
+ text?: string;
52
+ };
53
+ };
54
+
55
+ type FigmaAssetKind = "component" | "component_set";
56
+
57
+ type FigmaComponentRecord = {
58
+ key?: unknown;
59
+ file_key?: unknown;
60
+ node_id?: unknown;
61
+ name?: unknown;
62
+ description?: unknown;
63
+ thumbnail_url?: unknown;
64
+ created_at?: unknown;
65
+ updated_at?: unknown;
66
+ containing_frame?: unknown;
67
+ user?: unknown;
68
+ };
69
+
70
+ type FigmaImageResponse = {
71
+ images?: Record<string, string | null | undefined>;
72
+ };
73
+
74
+ function stringValue(value: unknown): string | null {
75
+ return typeof value === "string" && value.trim() ? value.trim() : null;
76
+ }
77
+
78
+ function numberOrDefault(value: number | undefined, fallback: number): number {
79
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
80
+ }
81
+
82
+ export function parseFigmaFileKey(input: string | undefined): string | null {
83
+ const value = input?.trim();
84
+ if (!value) return null;
85
+ if (FIGMA_FILE_KEY_RE.test(value)) return value;
86
+
87
+ try {
88
+ const url = new URL(value);
89
+ const parts = url.pathname.split("/").filter(Boolean);
90
+ for (const segment of ["design", "file", "proto"]) {
91
+ const index = parts.indexOf(segment);
92
+ if (index >= 0) {
93
+ const candidate = parts[index + 1];
94
+ if (candidate && FIGMA_FILE_KEY_RE.test(candidate)) return candidate;
95
+ }
96
+ }
97
+ } catch {
98
+ return null;
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ function figmaNodeUrl(
105
+ fileKey: string,
106
+ nodeId: string,
107
+ fileUrl?: string,
108
+ ): string {
109
+ const nodeParam = nodeId.replace(/:/g, "-");
110
+ try {
111
+ const url = new URL(fileUrl || `https://www.figma.com/design/${fileKey}`);
112
+ url.searchParams.set("node-id", nodeParam);
113
+ return url.href;
114
+ } catch {
115
+ return `https://www.figma.com/design/${fileKey}?node-id=${encodeURIComponent(
116
+ nodeParam,
117
+ )}`;
118
+ }
119
+ }
120
+
121
+ function recordSearchText(record: FigmaComponentRecord): string {
122
+ const frame =
123
+ record.containing_frame &&
124
+ typeof record.containing_frame === "object" &&
125
+ "name" in record.containing_frame
126
+ ? stringValue((record.containing_frame as { name?: unknown }).name)
127
+ : null;
128
+ return [stringValue(record.name), stringValue(record.description), frame]
129
+ .filter(Boolean)
130
+ .join(" ")
131
+ .toLowerCase();
132
+ }
133
+
134
+ function matchesQuery(record: FigmaComponentRecord, query: string | undefined) {
135
+ const normalized = query?.trim().toLowerCase();
136
+ if (!normalized) return true;
137
+ return recordSearchText(record).includes(normalized);
138
+ }
139
+
140
+ function normalizeFigmaRecords(
141
+ value: unknown,
142
+ kind: FigmaAssetKind,
143
+ ): FigmaComponentRecord[] {
144
+ if (!value || typeof value !== "object") return [];
145
+ const meta = (value as { meta?: unknown }).meta;
146
+ const key = kind === "component" ? "components" : "component_sets";
147
+ const records =
148
+ meta && typeof meta === "object"
149
+ ? (meta as Record<string, unknown>)[key]
150
+ : undefined;
151
+ if (!Array.isArray(records)) return [];
152
+ return records.filter(
153
+ (record): record is FigmaComponentRecord =>
154
+ !!record && typeof record === "object",
155
+ );
156
+ }
157
+
158
+ function providerJson(envelope: unknown, label: string): unknown {
159
+ const response = (envelope as FigmaProviderEnvelope | null)?.response;
160
+ if (!response) throw new Error(`Figma ${label} response was empty.`);
161
+ if (response.ok === false) {
162
+ const detail =
163
+ stringValue(response.text) ||
164
+ response.statusText ||
165
+ `HTTP ${response.status ?? "error"}`;
166
+ throw new Error(`Figma ${label} request failed: ${detail}`);
167
+ }
168
+ return response.json;
169
+ }
170
+
171
+ async function figmaGet(path: string, query?: Record<string, unknown>) {
172
+ return executeProviderApiRequest({
173
+ provider: "figma",
174
+ method: "GET",
175
+ path,
176
+ query,
177
+ maxBytes: 2 * 1024 * 1024,
178
+ });
179
+ }
180
+
181
+ export default defineAction({
182
+ description:
183
+ "List reusable assets from a Figma file for the Design asset panel. Returns components and component sets with thumbnails, insertable render URLs, and source provenance. Requires a saved FIGMA_ACCESS_TOKEN secret.",
184
+ schema: schemaInput,
185
+ http: { method: "GET" },
186
+ readOnly: true,
187
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
188
+ run: async (args) => {
189
+ const fileKey =
190
+ parseFigmaFileKey(args.fileKey) ?? parseFigmaFileKey(args.fileUrl);
191
+ if (!fileKey) {
192
+ throw new Error("Could not find a Figma file key in the provided URL.");
193
+ }
194
+
195
+ const limit = numberOrDefault(args.limit, 48);
196
+ const [componentsEnvelope, componentSetsEnvelope] = await Promise.all([
197
+ figmaGet(`/files/${fileKey}/components`),
198
+ figmaGet(`/files/${fileKey}/component_sets`),
199
+ ]);
200
+ const componentRecords = normalizeFigmaRecords(
201
+ providerJson(componentsEnvelope, "components"),
202
+ "component",
203
+ )
204
+ .filter((record) => matchesQuery(record, args.query))
205
+ .map((record) => ({ kind: "component" as const, record }));
206
+ const componentSetRecords = normalizeFigmaRecords(
207
+ providerJson(componentSetsEnvelope, "component sets"),
208
+ "component_set",
209
+ )
210
+ .filter((record) => matchesQuery(record, args.query))
211
+ .map((record) => ({ kind: "component_set" as const, record }));
212
+
213
+ const rawItems = [...componentRecords, ...componentSetRecords].slice(
214
+ 0,
215
+ limit,
216
+ );
217
+ const renderIds = rawItems
218
+ .map((item) => stringValue(item.record.node_id))
219
+ .filter((id): id is string => !!id);
220
+ let rendered: Record<string, string | null | undefined> = {};
221
+ if (renderIds.length > 0) {
222
+ const imagesEnvelope = await figmaGet(`/images/${fileKey}`, {
223
+ ids: renderIds.join(","),
224
+ format: args.renderFormat,
225
+ svg_include_id: true,
226
+ });
227
+ const images = providerJson(
228
+ imagesEnvelope,
229
+ "images",
230
+ ) as FigmaImageResponse;
231
+ rendered = images.images ?? {};
232
+ }
233
+
234
+ const assets = rawItems
235
+ .map(({ kind, record }) => {
236
+ const nodeId = stringValue(record.node_id);
237
+ const key = stringValue(record.key);
238
+ const name = stringValue(record.name) ?? "Untitled Figma asset";
239
+ const description = stringValue(record.description);
240
+ const thumbnailUrl = stringValue(record.thumbnail_url);
241
+ const renderUrl = nodeId ? stringValue(rendered[nodeId]) : null;
242
+ const containingFrame =
243
+ record.containing_frame && typeof record.containing_frame === "object"
244
+ ? {
245
+ name:
246
+ stringValue(
247
+ (record.containing_frame as { name?: unknown }).name,
248
+ ) ?? null,
249
+ nodeId:
250
+ stringValue(
251
+ (record.containing_frame as { nodeId?: unknown }).nodeId,
252
+ ) ??
253
+ stringValue(
254
+ (record.containing_frame as { node_id?: unknown }).node_id,
255
+ ),
256
+ }
257
+ : null;
258
+
259
+ return {
260
+ id: `figma:${fileKey}:${nodeId ?? key ?? name}`,
261
+ kind,
262
+ fileKey,
263
+ nodeId,
264
+ componentKey: key,
265
+ name,
266
+ description,
267
+ thumbnailUrl,
268
+ renderUrl,
269
+ insertUrl: renderUrl ?? thumbnailUrl,
270
+ sourceUrl: nodeId
271
+ ? figmaNodeUrl(fileKey, nodeId, args.fileUrl)
272
+ : null,
273
+ containingFrame,
274
+ createdAt: stringValue(record.created_at),
275
+ updatedAt: stringValue(record.updated_at),
276
+ };
277
+ })
278
+ .filter((asset) => asset.nodeId || asset.componentKey);
279
+
280
+ return {
281
+ source: "figma",
282
+ fileKey,
283
+ query: args.query ?? null,
284
+ renderFormat: args.renderFormat,
285
+ total: componentRecords.length + componentSetRecords.length,
286
+ returned: assets.length,
287
+ assets,
288
+ guidance:
289
+ "Use renderUrl/insertUrl with insert-asset and preserve fileKey, nodeId, componentKey, and sourceUrl for provenance. Components/component sets are reusable Figma library assets; styles and variables belong in design-system token sync.",
290
+ };
291
+ },
292
+ });
@@ -0,0 +1,238 @@
1
+ /**
2
+ * migrate-board-objects-to-file
3
+ *
4
+ * Lazy, idempotent migration that converts the legacy designs.data.boardObjects
5
+ * JSON blob into the new board file architecture.
6
+ *
7
+ * ## What it does
8
+ *
9
+ * 1. Guards on boardFileId already set — returns early if the migration has
10
+ * already run (idempotent).
11
+ * 2. Reads designs.data.boardObjects and converts each entry into an HTML
12
+ * fragment via boardObjectEntryToHtmlFragment, preserving negative left/top
13
+ * coordinates exactly (no clamping).
14
+ * 3. Inserts the fragments as direct <body> children of a new __board__.html
15
+ * design file (or merges into an existing one if somehow already present).
16
+ * 4. Stores the new file's id into designs.data.boardFileId in a transaction.
17
+ * 5. Nulls out designs.data.boardObjects so the old model no longer conflicts.
18
+ *
19
+ * ## Contract
20
+ *
21
+ * - Requires editor access on the design.
22
+ * - SCHEMA ADDITIVE ONLY — no columns are dropped or renamed; only designs.data
23
+ * JSON keys change.
24
+ * - Safe to call on designs with no board objects: creates the board file with
25
+ * an empty body and sets boardFileId.
26
+ *
27
+ * ## Trigger
28
+ *
29
+ * Called by DesignEditor on design open when designs.data.boardFileId is absent.
30
+ */
31
+
32
+ import { defineAction } from "@agent-native/core";
33
+ import { seedFromText } from "@agent-native/core/collab";
34
+ import { assertAccess } from "@agent-native/core/sharing";
35
+ import { and, eq } from "drizzle-orm";
36
+ import { nanoid } from "nanoid";
37
+ import { z } from "zod";
38
+
39
+ import { getDb, schema } from "../server/db/index.js";
40
+ import {
41
+ BOARD_FILENAME,
42
+ boardObjectEntryToHtmlFragment,
43
+ emptyBoardHtml,
44
+ } from "../shared/board-file.js";
45
+ import { parseBoardObjects } from "../shared/board-objects.js";
46
+
47
+ export default defineAction({
48
+ description:
49
+ "Migrate the legacy boardObjects blob (designs.data.boardObjects) into the " +
50
+ "new board file architecture. Creates a __board__.html design file, writes " +
51
+ "each board object as an absolute-positioned HTML element (preserving " +
52
+ "negative coordinates), stores boardFileId in designs.data, and nulls " +
53
+ "boardObjects. Idempotent — returns immediately if boardFileId is already " +
54
+ "set. Requires editor access on the design.",
55
+ schema: z.object({
56
+ designId: z
57
+ .string()
58
+ .describe("Design project ID to migrate board objects for."),
59
+ }),
60
+ run: async ({ designId }) => {
61
+ await assertAccess("design", designId, "editor");
62
+
63
+ const db = getDb();
64
+
65
+ // ── 1. Idempotency guard ──────────────────────────────────────────────
66
+ // Read the design data JSON to check whether boardFileId is already set.
67
+ // guard:allow-unscoped — assertAccess "editor" above scopes this read to
68
+ // the requesting user's accessible designs.
69
+ const [existing] = await db
70
+ .select({ data: schema.designs.data })
71
+ .from(schema.designs)
72
+ .where(eq(schema.designs.id, designId))
73
+ .limit(1);
74
+
75
+ if (!existing) {
76
+ throw new Error(`Design "${designId}" not found.`);
77
+ }
78
+
79
+ let parsed: Record<string, unknown>;
80
+ try {
81
+ const raw = JSON.parse(existing.data);
82
+ parsed =
83
+ raw && typeof raw === "object" && !Array.isArray(raw)
84
+ ? (raw as Record<string, unknown>)
85
+ : {};
86
+ } catch {
87
+ parsed = {};
88
+ }
89
+
90
+ if (
91
+ typeof parsed["boardFileId"] === "string" &&
92
+ parsed["boardFileId"].length > 0
93
+ ) {
94
+ return {
95
+ designId,
96
+ migrated: false,
97
+ boardFileId: parsed["boardFileId"] as string,
98
+ reason: "boardFileId already set — migration already complete.",
99
+ };
100
+ }
101
+
102
+ // ── 2. Parse legacy board objects ────────────────────────────────────
103
+ const boardObjects = parseBoardObjects(parsed["boardObjects"]);
104
+ const entries = Object.values(boardObjects);
105
+
106
+ // ── 3. Build the board HTML ───────────────────────────────────────────
107
+ // Start from the canonical empty-board template and inject each entry as
108
+ // a direct <body> child. Negative left/top are preserved — the migration
109
+ // intentionally does NOT clamp coords (appendCanvasPrimitiveToHtml clamps
110
+ // to x/y >= 0 for new screen primitives; the board surface has no such
111
+ // restriction).
112
+ let boardHtml = emptyBoardHtml();
113
+ if (entries.length > 0) {
114
+ const fragments = entries
115
+ .sort((a, b) => {
116
+ // Stable render order: lower z first, then creation order.
117
+ const az = a.geometry.z ?? 0;
118
+ const bz = b.geometry.z ?? 0;
119
+ if (az !== bz) return az - bz;
120
+ return a.createdAt < b.createdAt ? -1 : 1;
121
+ })
122
+ .map((entry) => boardObjectEntryToHtmlFragment(entry))
123
+ .join("\n");
124
+
125
+ // Insert fragments before </body>.
126
+ boardHtml = boardHtml.replace("</body>", `${fragments}\n</body>`);
127
+ }
128
+
129
+ const now = new Date().toISOString();
130
+
131
+ // ── 4. Upsert the board file and update designs.data atomically ───────
132
+ // Run in a transaction so concurrent opens cannot create two board files.
133
+ let boardFileId = "";
134
+
135
+ await db.transaction(async (tx) => {
136
+ // Re-read designs.data inside the transaction to guard against a
137
+ // concurrent migration that raced us.
138
+ const [fresh] = await tx
139
+ .select({ data: schema.designs.data })
140
+ .from(schema.designs)
141
+ .where(eq(schema.designs.id, designId))
142
+ .limit(1);
143
+
144
+ if (!fresh) {
145
+ throw new Error(`Design "${designId}" not found.`);
146
+ }
147
+
148
+ let freshParsed: Record<string, unknown>;
149
+ try {
150
+ const raw = JSON.parse(fresh.data);
151
+ freshParsed =
152
+ raw && typeof raw === "object" && !Array.isArray(raw)
153
+ ? (raw as Record<string, unknown>)
154
+ : {};
155
+ } catch {
156
+ freshParsed = {};
157
+ }
158
+
159
+ // Double-check inside the transaction.
160
+ if (
161
+ typeof freshParsed["boardFileId"] === "string" &&
162
+ freshParsed["boardFileId"].length > 0
163
+ ) {
164
+ boardFileId = freshParsed["boardFileId"] as string;
165
+ return; // Another concurrent call already finished — skip.
166
+ }
167
+
168
+ // Check if a __board__.html file was somehow already created (shouldn't
169
+ // happen in normal flow, but be defensive).
170
+ const [existingBoardFile] = await tx
171
+ .select({ id: schema.designFiles.id })
172
+ .from(schema.designFiles)
173
+ .where(
174
+ and(
175
+ eq(schema.designFiles.designId, designId),
176
+ eq(schema.designFiles.filename, BOARD_FILENAME),
177
+ ),
178
+ )
179
+ .limit(1);
180
+
181
+ if (existingBoardFile) {
182
+ // Board file exists but boardFileId was not set — link it and update
183
+ // its content.
184
+ boardFileId = existingBoardFile.id;
185
+ await tx
186
+ .update(schema.designFiles)
187
+ .set({ content: boardHtml, updatedAt: now })
188
+ .where(eq(schema.designFiles.id, boardFileId));
189
+ } else {
190
+ // Create the board file.
191
+ boardFileId = nanoid();
192
+ await tx.insert(schema.designFiles).values({
193
+ id: boardFileId,
194
+ designId,
195
+ filename: BOARD_FILENAME,
196
+ fileType: "html",
197
+ content: boardHtml,
198
+ createdAt: now,
199
+ updatedAt: now,
200
+ });
201
+ }
202
+
203
+ // Write boardFileId into designs.data; null out boardObjects.
204
+ const nextData: Record<string, unknown> = {
205
+ ...freshParsed,
206
+ boardFileId,
207
+ boardObjects: null,
208
+ };
209
+
210
+ await tx
211
+ .update(schema.designs)
212
+ .set({
213
+ data: JSON.stringify(nextData),
214
+ updatedAt: now,
215
+ })
216
+ .where(eq(schema.designs.id, designId));
217
+ });
218
+
219
+ // ── 5. Seed collab state for the new board file ───────────────────────
220
+ // (Best-effort: collab seeding after the file row is committed.)
221
+ try {
222
+ await seedFromText(boardFileId, boardHtml);
223
+ } catch {
224
+ // Non-fatal — the board file still renders from SQL content.
225
+ }
226
+
227
+ return {
228
+ designId,
229
+ migrated: true,
230
+ boardFileId,
231
+ migratedObjectCount: entries.length,
232
+ message:
233
+ entries.length > 0
234
+ ? `Migrated ${entries.length} board object(s) to board file ${boardFileId}.`
235
+ : `Created empty board file ${boardFileId} (no legacy board objects).`,
236
+ };
237
+ },
238
+ });
@@ -18,7 +18,8 @@
18
18
  * --view View name (list, editor, design-systems, present, templates, settings)
19
19
  * --designId Design ID (for editor/present views)
20
20
  * --editorView Editor mode for designs: single or overview
21
- * --inspectorTab Inspector tab for designs: design, tweaks, or extensions
21
+ * --inspectorTab Inspector tab for designs: design or tweaks (extensions opens Tools for compatibility)
22
+ * --leftPanel Left editor panel: file, agent, assets, tools, or tokens
22
23
  * --fileId Screen/file id to focus in the design editor
23
24
  * --filename Screen filename to focus in the design editor
24
25
  * --tool Design editor tool to activate
@@ -47,9 +48,17 @@ const designEditorToolSchema = z.enum([
47
48
  "scale",
48
49
  ]);
49
50
 
51
+ const designLeftPanelSchema = z.enum([
52
+ "file",
53
+ "agent",
54
+ "assets",
55
+ "tools",
56
+ "tokens",
57
+ ]);
58
+
50
59
  export default defineAction({
51
60
  description:
52
- "Navigate the UI to a specific view or path. Views: list, editor, design-systems, present, templates, settings. Use --designId with editor/present views and --designSystemId with design-systems. For designs, use editorView=overview to show the infinite screens canvas, or editorView=single with fileId/filename/screen to focus a screen. Use inspectorTab=extensions to focus the in-editor extension panel. Use tool to activate a design editor tool.",
61
+ "Navigate the UI to a specific view or path. Views: list, editor, design-systems, present, templates, settings. Use --designId with editor/present views and --designSystemId with design-systems. For designs, use editorView=overview to show the infinite screens canvas, or editorView=single with fileId/filename/screen to focus a screen. Use leftPanel=file|agent|assets|tools|tokens to focus the Figma-style left rail. Legacy inspectorTab=extensions opens Tools. Use tool to activate a design editor tool.",
53
62
  schema: z
54
63
  .object({
55
64
  view: z
@@ -83,6 +92,10 @@ export default defineAction({
83
92
  .enum(["design", "tweaks", "extensions"])
84
93
  .optional()
85
94
  .describe("Alias for inspectorTab"),
95
+ leftPanel: designLeftPanelSchema
96
+ .optional()
97
+ .describe("Design editor left rail panel to focus"),
98
+ panel: designLeftPanelSchema.optional().describe("Alias for leftPanel"),
86
99
  fileId: z.string().optional().describe("Design file/screen ID to focus"),
87
100
  screenId: z.string().optional().describe("Alias for fileId"),
88
101
  filename: z
@@ -146,6 +159,8 @@ export default defineAction({
146
159
  if (editorView) nav.editorView = editorView;
147
160
  const inspectorTab = args.inspectorTab ?? args.inspector;
148
161
  if (inspectorTab) nav.inspectorTab = inspectorTab;
162
+ const leftPanel = args.leftPanel ?? args.panel;
163
+ if (leftPanel) nav.leftPanel = leftPanel;
149
164
  if (args.fileId) nav.fileId = args.fileId;
150
165
  if (args.screenId) nav.screenId = args.screenId;
151
166
  if (args.filename) nav.filename = args.filename;
@@ -159,7 +174,7 @@ export default defineAction({
159
174
  args.designId ? ` (design: ${args.designId})` : ""
160
175
  }${editorView ? ` (${editorView} view)` : ""}${
161
176
  inspectorTab ? ` (${inspectorTab} inspector)` : ""
162
- }${
177
+ }${leftPanel ? ` (${leftPanel} panel)` : ""}${
163
178
  args.fileId || args.screenId || args.filename || args.screen
164
179
  ? ` (screen: ${args.fileId ?? args.screenId ?? args.filename ?? args.screen})`
165
180
  : ""
@@ -10,7 +10,7 @@ const ProviderSchema = z.enum(DESIGN_PROVIDER_API_IDS);
10
10
 
11
11
  export default defineAction({
12
12
  description:
13
- "List raw HTTP API capabilities for Design-connected providers. Use before provider-api-request when design-token import convenience actions are too narrow. Returns provider base URLs, auth style, credential key names, docs/spec URLs, placeholders, and examples; never returns secret values.",
13
+ "List raw HTTP API capabilities for Design-connected providers such as GitHub and Figma. Use before provider-api-request when design-token, repository, or Figma-library convenience actions are too narrow. Returns provider base URLs, auth style, credential key names, docs/spec URLs, placeholders, and examples; never returns secret values.",
14
14
  schema: z.object({
15
15
  provider: ProviderSchema.optional().describe(
16
16
  "Optional provider id to inspect. Omit to list every Design provider API escape hatch.",
@@ -22,7 +22,7 @@ export default defineAction({
22
22
  return {
23
23
  providers: await listProviderApiCatalog(provider),
24
24
  guidance:
25
- "Design actions like import-github are workflow shortcuts, not capability limits. When the GitHub API can answer the question with a better endpoint, query, body, pagination mode, metadata field, or API version, inspect docs/spec URLs and call provider-api-request directly. GitHub auth uses the saved GITHUB_TOKEN secret and never exposes the token value.",
25
+ "Design actions like import-github and list-figma-library-assets are workflow shortcuts, not capability limits. When GitHub or Figma can answer the question with a better endpoint, query, body, pagination mode, metadata field, or API version, inspect docs/spec URLs and call provider-api-request directly. Auth uses saved scoped secrets such as GITHUB_TOKEN or FIGMA_ACCESS_TOKEN and never exposes token values.",
26
26
  };
27
27
  },
28
28
  });
@@ -25,7 +25,7 @@ const WebContentSearchSchema = z.object({
25
25
 
26
26
  export default defineAction({
27
27
  description:
28
- "Inspect provider API docs/spec metadata, or fetch any public API documentation page, OpenAPI spec, changelog, or web page. Use this before provider-api-request when the exact GitHub endpoint, payload shape, pagination, or API version is uncertain.",
28
+ "Inspect provider API docs/spec metadata, or fetch any public API documentation page, OpenAPI spec, changelog, or web page. Use this before provider-api-request when the exact GitHub/Figma endpoint, payload shape, pagination, or API version is uncertain.",
29
29
  schema: z.object({
30
30
  provider: ProviderSchema.describe(
31
31
  "Provider whose API docs/spec to inspect.",
@@ -71,8 +71,8 @@ const PaginationSchema = z
71
71
  export default defineAction({
72
72
  description:
73
73
  "Make an arbitrary authenticated HTTP request to a Design-connected provider API. " +
74
- "Use this as the flexible escape hatch when a convenience action cannot express the needed GitHub endpoint, file tree query, issue/search query, request body, pagination mode, payload shape, or API version. " +
75
- "The request is constrained to the provider host, uses the saved GITHUB_TOKEN automatically, blocks private/internal URLs, and redacts secrets from responses. " +
74
+ "Use this as the flexible escape hatch when a convenience action cannot express the needed GitHub endpoint, Figma file/library endpoint, request body, pagination mode, payload shape, or API version. " +
75
+ "The request is constrained to the provider host, uses saved scoped credentials such as GITHUB_TOKEN or FIGMA_ACCESS_TOKEN automatically, blocks private/internal URLs, and redacts secrets from responses. " +
76
76
  "\n\nSTAGING MODE (preferred for large responses): Pass stageAs to write response items into a scratch dataset instead of returning the raw body. " +
77
77
  "Returns { dataset, rowCount, columns, sampleRows } so only a compact summary enters context. Use query-staged-dataset to aggregate, filter, and project the data without re-fetching. " +
78
78
  "\n\nPAGINATION: When stageAs is set, pass pagination config to fetch all pages server-side into the same dataset. For GitHub page pagination, use pageParam='page', pageSize from per_page, and itemsPath when the response object contains an items array.",
@@ -83,7 +83,7 @@ export default defineAction({
83
83
  .string()
84
84
  .min(1)
85
85
  .describe(
86
- "Provider API path such as /repos/{owner}/{repo}/contents/{path}, /search/code, /issues, or a full URL on an allowed provider host. Use placeholders from provider-api-catalog when provided.",
86
+ "Provider API path such as /repos/{owner}/{repo}/contents/{path}, /search/code, /files/{fileKey}/components, /images/{fileKey}, or a full URL on an allowed provider host. Use placeholders from provider-api-catalog when provided.",
87
87
  ),
88
88
  query: z
89
89
  .unknown()
@@ -0,0 +1,48 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
+ import { assertAccess } from "@agent-native/core/sharing";
4
+ import { and, eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+
7
+ import { getDb, schema } from "../server/db/index.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "Revoke a previously granted localhost write-consent for a design + connection. " +
12
+ "After revocation the agent can no longer write local files until the user re-grants consent. " +
13
+ "Requires editor access on the design.",
14
+ schema: z.object({
15
+ designId: z.string().describe("Design ID."),
16
+ connectionId: z
17
+ .string()
18
+ .describe("Localhost connection ID whose grant should be revoked."),
19
+ }),
20
+ run: async ({ designId, connectionId }) => {
21
+ await assertAccess("design", designId, "editor");
22
+
23
+ const ownerEmail = getRequestUserEmail();
24
+ if (!ownerEmail) throw new Error("no authenticated user");
25
+
26
+ const db = getDb();
27
+
28
+ const deleted = await db
29
+ .delete(schema.designLocalhostWriteGrants)
30
+ .where(
31
+ and(
32
+ eq(schema.designLocalhostWriteGrants.designId, designId),
33
+ eq(schema.designLocalhostWriteGrants.connectionId, connectionId),
34
+ eq(schema.designLocalhostWriteGrants.ownerEmail, ownerEmail),
35
+ ),
36
+ );
37
+
38
+ // `deleted` shape varies by dialect; check rows affected via the object.
39
+ const rowsAffected =
40
+ (deleted as unknown as { rowsAffected?: number })?.rowsAffected ?? 0;
41
+
42
+ return {
43
+ designId,
44
+ connectionId,
45
+ revoked: rowsAffected > 0,
46
+ };
47
+ },
48
+ });