@agent-native/core 0.114.10 → 0.114.12

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 (26) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +89 -1
  5. package/corpus/core/src/extensions/actions.ts +219 -0
  6. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +19 -0
  7. package/corpus/templates/clips/app/components/player/video-player.tsx +48 -0
  8. package/corpus/templates/clips/app/lib/fmp4.ts +26 -4
  9. package/corpus/templates/clips/app/lib/mse-video-loader.ts +13 -2
  10. package/corpus/templates/clips/changelog/2026-07-20-rewind-capture-status.md +6 -0
  11. package/corpus/templates/clips/desktop/src/app.tsx +81 -71
  12. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +103 -0
  13. package/corpus/templates/clips/desktop/src/styles.css +10 -0
  14. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  15. package/dist/client/extensions/EmbeddedExtension.js +65 -1
  16. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  17. package/dist/extensions/actions.d.ts.map +1 -1
  18. package/dist/extensions/actions.js +193 -1
  19. package/dist/extensions/actions.js.map +1 -1
  20. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  21. package/dist/resources/handlers.d.ts +1 -1
  22. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +19 -0
  23. package/package.json +1 -1
  24. package/src/client/extensions/EmbeddedExtension.tsx +89 -1
  25. package/src/extensions/actions.ts +219 -0
  26. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +19 -0
@@ -49,8 +49,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
49
49
  }>;
50
50
  /** DELETE /_agent-native/resources/:id — delete a resource */
51
51
  export declare function handleDeleteResource(event: any): Promise<{
52
- ok?: undefined;
53
52
  error: string;
53
+ ok?: undefined;
54
54
  } | {
55
55
  error?: undefined;
56
56
  ok: boolean;
@@ -153,6 +153,25 @@ persistence** — it handles everything automatically. Only use
153
153
  `dbQuery`/`dbExec` when querying the app's existing tables. See
154
154
  `references/api.md` for the full `get`/`remove`/scope reference.
155
155
 
156
+ ### Agent-side extension data access
157
+
158
+ The agent can read and write `extensionData` directly using two dedicated
159
+ actions — no need to go through the iframe bridge or raw SQL:
160
+
161
+ | Action | Purpose |
162
+ | --------------------- | ------------------------------------------------- |
163
+ | `extension-data-set` | Upsert an item in an extension's data store |
164
+ | `extension-data-get` | Read items from an extension's data store |
165
+
166
+ Use `extension-data-set` when the agent needs to seed, refresh, or update
167
+ data that an extension reads at render time via `extensionData.get()`. This
168
+ is the correct path for agent-driven dashboard refreshes — the agent
169
+ fetches fresh data from providers, then writes the merged result with
170
+ `extension-data-set`, and the extension picks it up on next load.
171
+
172
+ Use `extension-data-get` to inspect what data an extension currently stores,
173
+ or to verify a write succeeded.
174
+
156
175
  ## What extensions are
157
176
 
158
177
  Extensions are mini Alpine.js apps that run inside sandboxed iframes. They
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.114.10",
3
+ "version": "0.114.12",
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": {
@@ -177,6 +177,7 @@ export function EmbeddedExtension({
177
177
  // originates inside the same sandboxed realm as user code) and must be
178
178
  // ignored so a viewer cannot self-escalate to owner.
179
179
  const bindingLatchedRef = useRef(false);
180
+ const extensionRef = useRef(null as null | Extension);
180
181
 
181
182
  useEffect(() => {
182
183
  setIsDark(document.documentElement.classList.contains("dark"));
@@ -216,6 +217,7 @@ export function EmbeddedExtension({
216
217
  retry: shouldRetryExtensionLoad,
217
218
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),
218
219
  });
220
+ extensionRef.current = extension ?? null;
219
221
 
220
222
  // Notify the host once when the extension can't be loaded for this viewer so
221
223
  // it can show a fallback instead of a blank panel.
@@ -338,6 +340,92 @@ export function EmbeddedExtension({
338
340
  return;
339
341
  }
340
342
 
343
+ if (message.type === "agent-native-extension-error-fix") {
344
+ const name = extensionRef.current?.name ?? extensionId;
345
+ const errors: string[] = Array.isArray((message as any).errors)
346
+ ? (message as any).errors
347
+ : [];
348
+ const errorDetails: Array<{ message: string; stack: string }> =
349
+ Array.isArray((message as any).errorDetails)
350
+ ? (message as any).errorDetails
351
+ : [];
352
+ const consoleLogs: Array<{ level: string; message: string }> =
353
+ Array.isArray((message as any).consoleLogs)
354
+ ? (message as any).consoleLogs
355
+ : [];
356
+ const networkLogs: Array<{
357
+ path: string;
358
+ method: string;
359
+ ok?: boolean;
360
+ status?: number;
361
+ error?: string;
362
+ }> = Array.isArray((message as any).networkLogs)
363
+ ? (message as any).networkLogs
364
+ : [];
365
+
366
+ const detailedTrace = errorDetails
367
+ .filter((e) => e && typeof e === "object")
368
+ .map((e) => {
369
+ const msg =
370
+ typeof e.message === "string" ? e.message : String(e.message);
371
+ return typeof e.stack === "string" ? `${msg}\n${e.stack}` : msg;
372
+ })
373
+ .join("\n\n");
374
+
375
+ // Force a fresh read from the server, same as ExtensionViewer's fix
376
+ // flow — the query cache may hold the agent's previous (broken) turn.
377
+ let freshContent: string | undefined;
378
+ try {
379
+ const res = await fetch(
380
+ agentNativePath(`/_agent-native/extensions/${extensionId}`),
381
+ { cache: "no-store" },
382
+ );
383
+ if (res.ok) {
384
+ const fresh = (await res.json()) as Extension;
385
+ freshContent =
386
+ typeof fresh?.content === "string" ? fresh.content : undefined;
387
+ }
388
+ } catch {
389
+ // Fall through with no snapshot — agent can still re-read via get-extension.
390
+ }
391
+
392
+ const contextParts = [
393
+ `The user is viewing a dashboard panel embedding extension "${name}" (id: ${extensionId}, slot: ${slotId}) and there are runtime errors that need fixing.`,
394
+ `\nFull error details:\n${detailedTrace}`,
395
+ ];
396
+
397
+ if (consoleLogs.length > 0) {
398
+ const consoleStr = consoleLogs
399
+ .map((l) => `[${l.level}] ${l.message}`)
400
+ .join("\n");
401
+ contextParts.push(`\nRecent console output:\n${consoleStr}`);
402
+ }
403
+
404
+ if (networkLogs.length > 0) {
405
+ const netStr = networkLogs
406
+ .map(
407
+ (l) =>
408
+ `${l.method} ${l.path} → ${l.ok ? l.status : "FAILED: " + (l.error || l.status)}`,
409
+ )
410
+ .join("\n");
411
+ contextParts.push(`\nRecent network requests:\n${netStr}`);
412
+ }
413
+
414
+ if (freshContent) {
415
+ contextParts.push(
416
+ `\nCurrent extension content (just re-read from the database — this is the authoritative source, not anything you may have written in a previous turn):\n\`\`\`html\n${freshContent}\n\`\`\``,
417
+ );
418
+ }
419
+
420
+ sendToAgentChat({
421
+ message: `Fix runtime errors in extension "${name}" (id: ${extensionId}), embedded as a dashboard panel. The content snapshot below was just re-read from the database — treat it as authoritative and ignore any prior version you may have generated in this chat. If in doubt, call get-extension first.\n\nErrors:\n${errors.join("\n")}`,
422
+ context: contextParts.join("\n"),
423
+ submit: true,
424
+ openSidebar: true,
425
+ });
426
+ return;
427
+ }
428
+
341
429
  if (message.type !== "agent-native-extension-request") return;
342
430
 
343
431
  const requestId = String(message.requestId ?? "");
@@ -410,7 +498,7 @@ export function EmbeddedExtension({
410
498
 
411
499
  window.addEventListener("message", handleMessage);
412
500
  return () => window.removeEventListener("message", handleMessage);
413
- }, [extensionId]);
501
+ }, [extensionId, slotId]);
414
502
 
415
503
  if (!extension) {
416
504
  if (!isLoading && !isFetching) return null;
@@ -1,8 +1,11 @@
1
+ import { randomUUID } from "node:crypto";
2
+
1
3
  import { ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER } from "../action-ui.js";
2
4
  import { AgentActionStopError, type ActionRunContext } from "../action.js";
3
5
  import type { ActionEntry } from "../agent/production-agent.js";
4
6
  import type { AgentChatAttachment } from "../agent/types.js";
5
7
  import { writeAppState } from "../application-state/script-helpers.js";
8
+ import { getDbExec, isPostgres } from "../db/client.js";
6
9
  import { readResource } from "../resources/script-helpers.js";
7
10
  import {
8
11
  getRequestOrgId,
@@ -36,6 +39,7 @@ import {
36
39
  import {
37
40
  createExtension,
38
41
  deleteExtension,
42
+ ensureExtensionsTables,
39
43
  findRecentDuplicateExtension,
40
44
  getHiddenExtensionIdsForCurrentUser,
41
45
  getExtension,
@@ -45,6 +49,7 @@ import {
45
49
  hideExtension,
46
50
  listExtensionHistory,
47
51
  listExtensions,
52
+ notifyExtensionChangeForResource,
48
53
  restoreExtensionHistoryVersion,
49
54
  unhideExtension,
50
55
  updateExtension,
@@ -818,6 +823,220 @@ export function createExtensionActionEntries(): Record<string, ActionEntry> {
818
823
  },
819
824
  },
820
825
 
826
+ "extension-data-set": {
827
+ tool: {
828
+ description:
829
+ "Write a value to an extension's extensionData store (the tool_data table) from the agent side. Use this when the agent needs to update or seed data that an extension reads via extensionData.get() at render time. Requires editor access to the extension.",
830
+ parameters: {
831
+ type: "object",
832
+ properties: {
833
+ extensionId: {
834
+ type: "string",
835
+ description: "Extension id whose data store to write to.",
836
+ },
837
+ collection: {
838
+ type: "string",
839
+ description: "Collection name within the extension's data store.",
840
+ },
841
+ itemId: {
842
+ type: "string",
843
+ description:
844
+ "Item id within the collection. Upserts if it already exists.",
845
+ },
846
+ data: {
847
+ description:
848
+ "The data value to store. Objects are JSON-serialized automatically.",
849
+ },
850
+ scope: {
851
+ type: "string",
852
+ enum: ["user", "org"],
853
+ description:
854
+ "Storage scope. 'user' (default) is private to the current user; 'org' is shared across the organization.",
855
+ },
856
+ },
857
+ required: ["extensionId", "collection", "itemId", "data"],
858
+ },
859
+ },
860
+ run: async (args) => {
861
+ const extensionId = String(args?.extensionId ?? "").trim();
862
+ const collection = String(args?.collection ?? "").trim();
863
+ const itemId = String(args?.itemId ?? "").trim();
864
+ if (!extensionId || !collection || !itemId)
865
+ return "Error: extensionId, collection, and itemId are required.";
866
+ if (args?.data === undefined) return "Error: data is required.";
867
+
868
+ await ensureExtensionsTables();
869
+ const access = await resolveAccess("extension", extensionId);
870
+ if (!access || access.role === "viewer")
871
+ return `Error: editor access required for extension ${extensionId}.`;
872
+
873
+ const userEmail = getRequestUserEmail()?.toLowerCase() ?? "";
874
+ const scope = args?.scope === "org" ? "org" : "user";
875
+ const orgId = getRequestOrgId();
876
+ if (scope === "org" && !orgId)
877
+ return "Error: org context required for scope=org.";
878
+
879
+ const data =
880
+ typeof args.data === "string" ? args.data : JSON.stringify(args.data);
881
+ const MAX_BYTES = 1024 * 1024;
882
+ if (Buffer.byteLength(data, "utf8") > MAX_BYTES)
883
+ return `Error: data exceeds ${MAX_BYTES} byte limit. Store large payloads in file storage instead.`;
884
+
885
+ const now = new Date().toISOString();
886
+ const scopeKey = scope === "org" ? `org:${orgId}` : userEmail;
887
+ const client = getDbExec();
888
+ const pg = isPostgres();
889
+ const conflictClause = pg
890
+ ? `ON CONFLICT (tool_id, collection, scope_key, item_id)
891
+ DO UPDATE SET data = EXCLUDED.data, updated_at = EXCLUDED.updated_at`
892
+ : `ON CONFLICT (tool_id, collection, scope_key, item_id)
893
+ DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`;
894
+
895
+ await client.execute({
896
+ sql: `UPDATE tools SET updated_at = ? WHERE id = ?`,
897
+ args: [now, extensionId],
898
+ });
899
+ await client.execute({
900
+ sql: `INSERT INTO tool_data (id, tool_id, collection, item_id, data, owner_email, scope, org_id, scope_key, created_at, updated_at)
901
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
902
+ ${conflictClause}`,
903
+ args: [
904
+ randomUUID(),
905
+ extensionId,
906
+ collection,
907
+ itemId,
908
+ data,
909
+ userEmail,
910
+ scope,
911
+ scope === "org" ? orgId! : null,
912
+ scopeKey,
913
+ now,
914
+ now,
915
+ ],
916
+ });
917
+ await notifyExtensionChangeForResource(extensionId);
918
+
919
+ return {
920
+ ok: true,
921
+ id: itemId,
922
+ extensionId,
923
+ collection,
924
+ scope,
925
+ updatedAt: now,
926
+ };
927
+ },
928
+ },
929
+
930
+ "extension-data-get": {
931
+ tool: {
932
+ description:
933
+ "Read items from an extension's extensionData store (the tool_data table) from the agent side. Use this to inspect what data an extension currently has stored, or to verify a write succeeded. Requires viewer access to the extension.",
934
+ parameters: {
935
+ type: "object",
936
+ properties: {
937
+ extensionId: {
938
+ type: "string",
939
+ description: "Extension id whose data store to read from.",
940
+ },
941
+ collection: {
942
+ type: "string",
943
+ description: "Collection name within the extension's data store.",
944
+ },
945
+ itemId: {
946
+ type: "string",
947
+ description:
948
+ "Optional specific item id. If omitted, lists all items in the collection.",
949
+ },
950
+ scope: {
951
+ type: "string",
952
+ enum: ["user", "org", "all"],
953
+ description:
954
+ "Storage scope to read. 'user' (default) reads the current user's items; 'org' reads organization-shared items; 'all' reads both.",
955
+ },
956
+ limit: {
957
+ type: "number",
958
+ description:
959
+ "Maximum items to return when listing (default 100, max 1000).",
960
+ },
961
+ },
962
+ required: ["extensionId", "collection"],
963
+ },
964
+ },
965
+ run: async (args) => {
966
+ const extensionId = String(args?.extensionId ?? "").trim();
967
+ const collection = String(args?.collection ?? "").trim();
968
+ if (!extensionId || !collection)
969
+ return "Error: extensionId and collection are required.";
970
+
971
+ await ensureExtensionsTables();
972
+ const access = await resolveAccess("extension", extensionId);
973
+ if (!access) return `Error: no access to extension ${extensionId}.`;
974
+
975
+ const userEmail = getRequestUserEmail()?.toLowerCase() ?? "";
976
+ const scope = args?.scope ?? "user";
977
+ const orgId = getRequestOrgId();
978
+ if ((scope === "org" || scope === "all") && !orgId)
979
+ return "Error: org context required for scope=org or scope=all.";
980
+ const itemId = args?.itemId ? String(args.itemId).trim() : null;
981
+ const limit = Math.min(Math.max(1, Number(args?.limit) || 100), 1000);
982
+ const client = getDbExec();
983
+
984
+ if (itemId) {
985
+ const scopeClause =
986
+ scope === "org"
987
+ ? `AND scope = 'org' AND org_id = ?`
988
+ : scope === "all"
989
+ ? `AND ((scope = 'user' AND lower(owner_email) = ?) OR (scope = 'org' AND org_id = ?))`
990
+ : `AND scope = 'user' AND lower(owner_email) = ?`;
991
+ const scopeArgs =
992
+ scope === "org"
993
+ ? [orgId ?? ""]
994
+ : scope === "all"
995
+ ? [userEmail, orgId ?? ""]
996
+ : [userEmail];
997
+
998
+ const result = await client.execute({
999
+ sql: `SELECT COALESCE(item_id, id) AS id, data, scope, owner_email, updated_at
1000
+ FROM tool_data
1001
+ WHERE tool_id = ? AND collection = ? AND COALESCE(item_id, id) = ? ${scopeClause}`,
1002
+ args: [extensionId, collection, itemId, ...scopeArgs],
1003
+ });
1004
+ const row = result.rows?.[0];
1005
+ if (!row) return { found: false, extensionId, collection, itemId };
1006
+ return { found: true, ...row };
1007
+ }
1008
+
1009
+ const scopeClause =
1010
+ scope === "org"
1011
+ ? `AND scope = 'org' AND org_id = ?`
1012
+ : scope === "all"
1013
+ ? `AND ((scope = 'user' AND lower(owner_email) = ?) OR (scope = 'org' AND org_id = ?))`
1014
+ : `AND scope = 'user' AND lower(owner_email) = ?`;
1015
+ const scopeArgs =
1016
+ scope === "org"
1017
+ ? [orgId ?? ""]
1018
+ : scope === "all"
1019
+ ? [userEmail, orgId ?? ""]
1020
+ : [userEmail];
1021
+
1022
+ const result = await client.execute({
1023
+ sql: `SELECT COALESCE(item_id, id) AS id, data, scope, owner_email, updated_at
1024
+ FROM tool_data
1025
+ WHERE tool_id = ? AND collection = ? ${scopeClause}
1026
+ ORDER BY updated_at DESC
1027
+ LIMIT ?`,
1028
+ args: [extensionId, collection, ...scopeArgs, limit],
1029
+ });
1030
+ return {
1031
+ extensionId,
1032
+ collection,
1033
+ scope,
1034
+ count: result.rows?.length ?? 0,
1035
+ items: result.rows ?? [],
1036
+ };
1037
+ },
1038
+ },
1039
+
821
1040
  "delete-extension": {
822
1041
  tool: {
823
1042
  description:
@@ -153,6 +153,25 @@ persistence** — it handles everything automatically. Only use
153
153
  `dbQuery`/`dbExec` when querying the app's existing tables. See
154
154
  `references/api.md` for the full `get`/`remove`/scope reference.
155
155
 
156
+ ### Agent-side extension data access
157
+
158
+ The agent can read and write `extensionData` directly using two dedicated
159
+ actions — no need to go through the iframe bridge or raw SQL:
160
+
161
+ | Action | Purpose |
162
+ | --------------------- | ------------------------------------------------- |
163
+ | `extension-data-set` | Upsert an item in an extension's data store |
164
+ | `extension-data-get` | Read items from an extension's data store |
165
+
166
+ Use `extension-data-set` when the agent needs to seed, refresh, or update
167
+ data that an extension reads at render time via `extensionData.get()`. This
168
+ is the correct path for agent-driven dashboard refreshes — the agent
169
+ fetches fresh data from providers, then writes the merged result with
170
+ `extension-data-set`, and the extension picks it up on next load.
171
+
172
+ Use `extension-data-get` to inspect what data an extension currently stores,
173
+ or to verify a write succeeded.
174
+
156
175
  ## What extensions are
157
176
 
158
177
  Extensions are mini Alpine.js apps that run inside sandboxed iframes. They