framer-dalton 0.0.26 → 0.0.28

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.
@@ -1,7 +1,7 @@
1
1
  import fs4 from 'fs';
2
- import os from 'os';
2
+ import os5 from 'os';
3
3
  import path from 'path';
4
- import 'child_process';
4
+ import { execFileSync } from 'child_process';
5
5
  import 'net';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { createTRPCClient, httpLink } from '@trpc/client';
@@ -14,7 +14,7 @@ import { z } from 'zod';
14
14
  import { createRequire } from 'module';
15
15
  import * as vm from 'vm';
16
16
 
17
- /* @framer/ai relay server v0.0.26 */
17
+ /* @framer/ai relay server v0.0.28 */
18
18
  var __defProp = Object.defineProperty;
19
19
  var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name2);
20
20
  var __typeError = (msg) => {
@@ -72,22 +72,28 @@ Please upgrade: https://nodejs.org/`
72
72
  );
73
73
  process.exit(1);
74
74
  }
75
+ var DEFAULT_MAX_LOG_FILE_BYTES = 1024 * 1024;
76
+ var DEFAULT_RETAINED_LOG_FILE_BYTES = 512 * 1024;
77
+ var LOG_TRIM_CHECK_INTERVAL = 10;
78
+ var maxLogFileBytes = DEFAULT_MAX_LOG_FILE_BYTES;
79
+ var retainedLogFileBytes = DEFAULT_RETAINED_LOG_FILE_BYTES;
75
80
  function getLogPath() {
76
81
  if (process.env.XDG_STATE_HOME) {
77
82
  return path.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
78
83
  }
79
84
  if (process.platform === "win32") {
80
85
  return path.join(
81
- process.env.APPDATA || os.homedir(),
86
+ process.env.APPDATA || os5.homedir(),
82
87
  "framer",
83
88
  "relay.log"
84
89
  );
85
90
  }
86
- return path.join(os.homedir(), ".local", "state", "framer", "relay.log");
91
+ return path.join(os5.homedir(), ".local", "state", "framer", "relay.log");
87
92
  }
88
93
  __name(getLogPath, "getLogPath");
89
94
  var logPath = getLogPath();
90
95
  var initialized = false;
96
+ var logEntriesSinceTrimCheck = LOG_TRIM_CHECK_INTERVAL - 1;
91
97
  function ensureLogDir() {
92
98
  if (initialized) return;
93
99
  const dir = path.dirname(logPath);
@@ -95,11 +101,44 @@ function ensureLogDir() {
95
101
  initialized = true;
96
102
  }
97
103
  __name(ensureLogDir, "ensureLogDir");
104
+ function trimLogFileIfNeeded() {
105
+ logEntriesSinceTrimCheck += 1;
106
+ if (logEntriesSinceTrimCheck < LOG_TRIM_CHECK_INTERVAL) return;
107
+ logEntriesSinceTrimCheck = 0;
108
+ let size;
109
+ try {
110
+ size = fs4.statSync(logPath).size;
111
+ } catch (err) {
112
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
113
+ return;
114
+ }
115
+ throw err;
116
+ }
117
+ if (size <= maxLogFileBytes) return;
118
+ const retainedBytes = Math.min(retainedLogFileBytes, size);
119
+ const buffer = Buffer.alloc(retainedBytes);
120
+ const file = fs4.openSync(logPath, "r");
121
+ let bytesRead = 0;
122
+ try {
123
+ bytesRead = fs4.readSync(
124
+ file,
125
+ buffer,
126
+ 0,
127
+ retainedBytes,
128
+ size - retainedBytes
129
+ );
130
+ } finally {
131
+ fs4.closeSync(file);
132
+ }
133
+ fs4.writeFileSync(logPath, buffer.subarray(0, bytesRead));
134
+ }
135
+ __name(trimLogFileIfNeeded, "trimLogFileIfNeeded");
98
136
  function log(message) {
99
137
  ensureLogDir();
100
138
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
101
139
  fs4.appendFileSync(logPath, `${timestamp} ${message}
102
140
  `);
141
+ trimLogFileIfNeeded();
103
142
  }
104
143
  __name(log, "log");
105
144
  function getConfigDir() {
@@ -107,9 +146,9 @@ function getConfigDir() {
107
146
  return path.join(process.env.XDG_CONFIG_HOME, "framer");
108
147
  }
109
148
  if (process.platform === "win32") {
110
- return path.join(process.env.APPDATA || os.homedir(), "framer");
149
+ return path.join(process.env.APPDATA || os5.homedir(), "framer");
111
150
  }
112
- return path.join(os.homedir(), ".config", "framer");
151
+ return path.join(os5.homedir(), ".config", "framer");
113
152
  }
114
153
  __name(getConfigDir, "getConfigDir");
115
154
  function ensureConfigDir() {
@@ -160,7 +199,7 @@ __name(debug, "debug");
160
199
  // src/version.ts
161
200
  var VERSION = (
162
201
  // typeof is used to ensure this can be used just via tsx or node etc. without build
163
- "0.0.26"
202
+ "0.0.28"
164
203
  );
165
204
 
166
205
  // src/relay-client.ts
@@ -266,7 +305,7 @@ var ScopedFS = class {
266
305
  }
267
306
  allowedDirs;
268
307
  constructor(allowedDirs) {
269
- const defaultDirs = [process.cwd(), "/tmp", os.tmpdir()];
308
+ const defaultDirs = [process.cwd(), "/tmp", os5.tmpdir()];
270
309
  const dirs = allowedDirs ?? defaultDirs;
271
310
  this.allowedDirs = [...new Set(dirs.map((d) => path.resolve(d)))];
272
311
  }
@@ -533,7 +572,7 @@ async function execute(session, code, options = {}) {
533
572
  output.push(args.map((arg) => formatValue(arg)).join(" "));
534
573
  }, "info")
535
574
  };
536
- const scopedFs = cwd ? new ScopedFS([cwd, "/tmp", os.tmpdir()]) : new ScopedFS();
575
+ const scopedFs = cwd ? new ScopedFS([cwd, "/tmp", os5.tmpdir()]) : new ScopedFS();
537
576
  const sandboxedRequire = createSandboxedRequire(scopedFs);
538
577
  const vmContextObj = {
539
578
  // Framer API
@@ -795,11 +834,55 @@ function isTelemetryEnabled() {
795
834
  __name(isTelemetryEnabled, "isTelemetryEnabled");
796
835
  var trackingEndpoint = "https://events.framer.com/track";
797
836
  var inProgressTrackings = /* @__PURE__ */ new Set();
798
- function sharedFields() {
837
+ var cachedSharedFields;
838
+ function getOsName(platform) {
839
+ switch (platform) {
840
+ case "darwin":
841
+ return "macos";
842
+ case "win32":
843
+ return "windows";
844
+ case "linux":
845
+ return "linux";
846
+ default:
847
+ return "unknown";
848
+ }
849
+ }
850
+ __name(getOsName, "getOsName");
851
+ function getMacOsVersion() {
852
+ try {
853
+ const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
854
+ encoding: "utf8",
855
+ timeout: 1e3
856
+ }).trim();
857
+ return version || "unknown";
858
+ } catch {
859
+ return "unknown";
860
+ }
861
+ }
862
+ __name(getMacOsVersion, "getMacOsVersion");
863
+ function getOsVersion(platform) {
864
+ if (platform === "darwin") return getMacOsVersion();
865
+ const version = os5.release().trim();
866
+ return version || "unknown";
867
+ }
868
+ __name(getOsVersion, "getOsVersion");
869
+ function getOsMetadata() {
870
+ const platform = os5.platform();
871
+ const arch = os5.arch();
799
872
  return {
873
+ arch: arch || "unknown",
874
+ osName: getOsName(platform),
875
+ osVersion: getOsVersion(platform)
876
+ };
877
+ }
878
+ __name(getOsMetadata, "getOsMetadata");
879
+ function sharedFields() {
880
+ cachedSharedFields ??= {
800
881
  machineId: getMachineId(),
801
- cliVersion: VERSION
882
+ cliVersion: VERSION,
883
+ ...getOsMetadata()
802
884
  };
885
+ return cachedSharedFields;
803
886
  }
804
887
  __name(sharedFields, "sharedFields");
805
888
  function wrapEvent(event) {
@@ -812,16 +895,19 @@ function wrapEvent(event) {
812
895
  };
813
896
  }
814
897
  __name(wrapEvent, "wrapEvent");
815
- function postEvent(event) {
898
+ function postEvent(createEvent) {
816
899
  if (!isTelemetryEnabled()) return false;
817
900
  if (process.env.NODE_ENV === "test" && true) return false;
901
+ const event = createEvent();
902
+ const body = JSON.stringify([wrapEvent(event)]);
903
+ debug("tracking", `sending ${event.event} to ${trackingEndpoint}: ${body}`);
818
904
  const promise = fetch(trackingEndpoint, {
819
905
  method: "POST",
820
906
  headers: {
821
907
  "Content-Type": "application/json",
822
908
  "User-Agent": `framer-dalton/${VERSION}`
823
909
  },
824
- body: JSON.stringify([wrapEvent(event)]),
910
+ body,
825
911
  signal: AbortSignal.timeout(
826
912
  5e3
827
913
  /* 5 seconds */
@@ -850,75 +936,93 @@ function waitForTrackingToFinish() {
850
936
  }
851
937
  __name(waitForTrackingToFinish, "waitForTrackingToFinish");
852
938
  function trackRelayStart() {
853
- postEvent({
854
- event: "local_agents_relay_start",
855
- ...sharedFields()
856
- });
939
+ postEvent(
940
+ () => ({
941
+ event: "local_agents_relay_start",
942
+ ...sharedFields()
943
+ })
944
+ );
857
945
  }
858
946
  __name(trackRelayStart, "trackRelayStart");
859
947
  var relayShutdownTracked = false;
860
948
  function trackRelayShutdown() {
861
949
  if (relayShutdownTracked) return;
862
- relayShutdownTracked = postEvent({
863
- event: "local_agents_relay_shutdown",
864
- ...sharedFields()
865
- });
950
+ relayShutdownTracked = postEvent(
951
+ () => ({
952
+ event: "local_agents_relay_shutdown",
953
+ ...sharedFields()
954
+ })
955
+ );
866
956
  }
867
957
  __name(trackRelayShutdown, "trackRelayShutdown");
868
958
  function trackSessionCreate(payload) {
869
- postEvent({
870
- event: "local_agents_session_create",
871
- ...sharedFields(),
872
- ...payload
873
- });
959
+ postEvent(
960
+ () => ({
961
+ event: "local_agents_session_create",
962
+ ...sharedFields(),
963
+ ...payload
964
+ })
965
+ );
874
966
  }
875
967
  __name(trackSessionCreate, "trackSessionCreate");
876
968
  function trackSessionDestroy(payload) {
877
- postEvent({
878
- event: "local_agents_session_destroy",
879
- ...sharedFields(),
880
- ...payload
881
- });
969
+ postEvent(
970
+ () => ({
971
+ event: "local_agents_session_destroy",
972
+ ...sharedFields(),
973
+ ...payload
974
+ })
975
+ );
882
976
  }
883
977
  __name(trackSessionDestroy, "trackSessionDestroy");
884
978
  function trackExec(payload) {
885
- postEvent({
886
- event: "local_agents_exec",
887
- ...sharedFields(),
888
- ...payload
889
- });
979
+ postEvent(
980
+ () => ({
981
+ event: "local_agents_exec",
982
+ ...sharedFields(),
983
+ ...payload
984
+ })
985
+ );
890
986
  }
891
987
  __name(trackExec, "trackExec");
892
988
  function trackConnectionAcquire(payload) {
893
- postEvent({
894
- event: "local_agents_connection_acquire",
895
- ...sharedFields(),
896
- ...payload
897
- });
989
+ postEvent(
990
+ () => ({
991
+ event: "local_agents_connection_acquire",
992
+ ...sharedFields(),
993
+ ...payload
994
+ })
995
+ );
898
996
  }
899
997
  __name(trackConnectionAcquire, "trackConnectionAcquire");
900
998
  function trackConnectionReconnect(payload) {
901
- postEvent({
902
- event: "local_agents_connection_reconnect",
903
- ...sharedFields(),
904
- ...payload
905
- });
999
+ postEvent(
1000
+ () => ({
1001
+ event: "local_agents_connection_reconnect",
1002
+ ...sharedFields(),
1003
+ ...payload
1004
+ })
1005
+ );
906
1006
  }
907
1007
  __name(trackConnectionReconnect, "trackConnectionReconnect");
908
1008
  function trackConnectionIdleDisconnect(payload) {
909
- postEvent({
910
- event: "local_agents_connection_idle_disconnect",
911
- ...sharedFields(),
912
- ...payload
913
- });
1009
+ postEvent(
1010
+ () => ({
1011
+ event: "local_agents_connection_idle_disconnect",
1012
+ ...sharedFields(),
1013
+ ...payload
1014
+ })
1015
+ );
914
1016
  }
915
1017
  __name(trackConnectionIdleDisconnect, "trackConnectionIdleDisconnect");
916
1018
  function trackError(payload) {
917
- postEvent({
918
- event: "local_agents_error",
919
- ...sharedFields(),
920
- ...payload
921
- });
1019
+ postEvent(
1020
+ () => ({
1021
+ event: "local_agents_error",
1022
+ ...sharedFields(),
1023
+ ...payload
1024
+ })
1025
+ );
922
1026
  }
923
1027
  __name(trackError, "trackError");
924
1028
 
@@ -36,16 +36,22 @@ Always save results you'll need again. Don't repeat API calls.
36
36
 
37
37
  ## Method Selection
38
38
 
39
- Use agent-specific methods whenever a plugin API method and an agent method overlap:
39
+ Choose the highest-priority interface available for the task:
40
40
 
41
- - Use `readProjectForAgent`, `getNodeForAgent`, `getNodesForAgent`, `getNodesOfTypesForAgent`, `getScopeNodeForAgent`, `getGroundNodeForAgent`, `getParentNodeForAgent`, `getAncestorsForAgent`, `serializeForAgent`, `serializeNodesForAgent`, and `paginateForAgent` for project tree reads.
42
- - Use `readComponentControlsForAgent`, `readIconSetControlsForAgent`, `readIconsForAgent`, `readLayoutTemplateControlsForAgent`, and `readShaderControlsForAgent` for reading the controls of components, icon sets, icons, layout templates, and shaders.
43
- - Use `applyAgentChanges` for page and layout edits. Do not use low-level node APIs like `createNode`, `setAttributes`, or `setRect` for design/layout work.
44
- - Use `publishForAgent` for publishing. Do not use `publish`, `getDeployments`, or `deploy` for normal agent publishing flows.
41
+ 1. Subcommands, such as `framer-dalton read-project` and `framer-dalton apply-changes`
42
+ 2. Agent-specific methods, such as `framer.agent.readProject`, `framer.agent.applyChanges`, and `framer.agent.publish`
43
+ 3. Generic plugin API methods, such as top-level `framer.*` methods
45
44
 
46
- Use the plugin API for capabilities that do not have an agent-specific counterpart, such as code file management, localization, and redirects.
45
+ - Use `framer.agent.readProject`, `framer.agent.getNode`, `framer.agent.getNodes`, `framer.agent.getNodesOfTypes`, `framer.agent.getScopeNode`, `framer.agent.getGroundNode`, `framer.agent.getParentNode`, `framer.agent.getAncestors`, `framer.agent.serialize`, `framer.agent.serializeNodes`, and `framer.agent.paginate` for project tree reads.
46
+ - Use `framer.agent.readComponentControls`, `framer.agent.readIconSetControls`, `framer.agent.readIcons`, `framer.agent.readLayoutTemplateControls`, and `framer.agent.readShaderControls` for reading the controls of components, icon sets, icons, layout templates, and shaders.
47
+ - Use `framer.agent.applyChanges` for page and layout edits. Do not use low-level node APIs like `createNode`, `setAttributes`, or `setRect` for design/layout work.
48
+ - Use `framer.agent.publish` for publishing. Do not use `publish`, `getDeployments`, or `deploy` for normal agent publishing flows.
49
+ - Prefer `framer.agent.applyChanges` and project tree read methods for CMS work where possible. Fall back to the collection APIs only for functionality otherwise not supported. Note that if you add collections or fields via collection APIs, some things may not work as expected when then using those collections or fields on the canvas via `framer.agent.applyChanges`.
50
+ - Create styles, design tokens, components, and variables via `framer.agent.applyChanges`. Using plugin API methods can cause issues when trying to use newly created values later in `framer.agent.applyChanges` calls.
47
51
 
48
- During normal task execution, do not call `framer.getAgentSystemPrompt()` or `framer.getAgentContext()`. This skill already includes their output.
52
+ Use generic plugin API methods only for capabilities that do not have a subcommand or agent-specific counterpart, such as code file management, localization, and redirects.
53
+
54
+ During normal task execution, do not call `framer.agent.getSystemPrompt()` or `framer.agent.getContext()`. This skill already includes their output.
49
55
 
50
56
  ## Core Usage Principles
51
57
 
@@ -100,15 +106,15 @@ Store anything you'll reference again.
100
106
 
101
107
  Use this approach only if the user asks for it. Do not mention this approach to the user otherwise.
102
108
 
103
- This approach is called “Prompt the Framer agent”. With this approach, use `framer.startAgentConversation()` to start a stateful design subagent. Keep the `conversationId` it returns in `state`, and call `framer.continueAgentConversation()` with it to continue the same design task.
109
+ This approach is called “Prompt the Framer agent”. With this approach, use `framer.agent.startConversation()` to start a stateful design subagent. Keep the `conversationId` it returns in `state`, and call `framer.agent.continueConversation()` with it to continue the same design task.
104
110
 
105
- Do not call `framer.getAgentSystemPrompt()` or `framer.getAgentContext()` or `framer.applyAgentChanges()` with this approach.
111
+ Do not call `framer.agent.getSystemPrompt()` or `framer.agent.getContext()` or `framer.agent.applyChanges()` with this approach.
106
112
 
107
113
  Example:
108
114
 
109
115
  ```js
110
116
  state.agent ??= {};
111
- const first = await framer.startAgentConversation(
117
+ const first = await framer.agent.startConversation(
112
118
  "Build me a landing page based on the attached screenshot",
113
119
  {
114
120
  pagePath: "/",
@@ -119,7 +125,7 @@ const first = await framer.startAgentConversation(
119
125
  state.agent.conversationId = first.conversationId;
120
126
  console.log(first.responseMessages);
121
127
 
122
- const second = await framer.continueAgentConversation("Now make it pink", {
128
+ const second = await framer.agent.continueConversation("Now make it pink", {
123
129
  conversationId: state.agent.conversationId,
124
130
  selectionNodeIds: ["someNodeId"],
125
131
  // imageUrls: [...],
@@ -169,120 +175,154 @@ npx framer-dalton docs ScreenshotOptions # Show type + recursively expa
169
175
 
170
176
  ### Working with Collections (CMS)
171
177
 
172
- Collections are Framer's CMS. Each collection has fields (columns) and items (rows). Use the plugin Collection API (the methods below) for collection schema and item CRUD.
173
-
174
- **Exception — CMS Collection Lists.** The in-canvas construct that *renders* a collection as a repeating list of cards (the "Collection List" / repeater) is an agent/page concern. Build and edit those through `readProjectForAgent` and `applyAgentChanges`, not the plugin API. Use the plugin Collection API to read available collections and field schema first, then use `applyAgentChanges` to add the list.
178
+ Collections and items are nodes: read them with the agent read methods, create and edit them with `framer.agent.applyChanges`. A collection's fields are its `variables`; an item's cells are `$control__<fieldId>` attributes.
175
179
 
176
- #### Reading Collections
180
+ #### List collections and fields
177
181
 
178
182
  ```js
179
- // Get all collections
180
- const collections = await framer.getCollections();
181
- console.log(collections.map((c) => ({ name: c.name, id: c.id })));
182
-
183
- // Get a specific collection by ID
184
- const collection = await framer.getCollection("collection-id");
185
-
186
- // Get fields (columns)
187
- const fields = await collection.getFields();
188
- // Note that a plain console.log will miss getter-backed properties
189
- console.log(fields.map((f) => ({ id: f.id, name: f.name, type: f.type })));
190
- // [{ id: "BnNuS2i3o", name: "Title", type: "string" }, ...]
191
-
192
- // Get items (rows)
193
- const items = await collection.getItems();
194
- console.log(items);
195
- // [{ id: "XTM8FSHGs", slug: "post-1", draft: false, fieldData: {...} }, ...]
183
+ const collections = await framer.agent.getNodesOfTypes({ types: ["CollectionNode"] });
184
+ console.log(collections.map((c) => ({
185
+ id: c.id,
186
+ name: c.name,
187
+ itemCount: c.$itemCount,
188
+ fields: c.variables.map((v) => ({ id: v.id, name: v.name, type: v.type })),
189
+ })));
196
190
  ```
197
191
 
198
- #### Sync external data to collection
192
+ #### Read items
193
+
194
+ If you know the collection id, serialize the collection with `depth: 1` to read its direct item children.
199
195
 
200
196
  ```js
201
- const collection = await framer.getCollection("collection-id");
202
- const existing = await collection.getItems();
203
- const existingIds = new Set(existing.map((i) => i.id));
197
+ const collectionId = "collection-id";
198
+ const collection = await framer.agent.serialize({ id: collectionId, depth: 1 });
199
+ const items = collection.children ?? [];
200
+ console.log({
201
+ itemCount: collection.$itemCount,
202
+ items: items.map((item) => ({ id: item.id, ...item.attributes })),
203
+ });
204
+ ```
204
205
 
205
- const externalData = await fetch("https://api.example.com/posts").then((r) =>
206
- r.json(),
207
- );
206
+ For huge collections, paginate the serialized children and process one page per call. Store the page in `state` only when you need to continue in a later exec call. Stop when the logged `nextCursor` is missing.
208
207
 
209
- const items = externalData.map((post) => ({
210
- id: post.id,
211
- slug: post.slug,
212
- fieldData: { title: post.title, content: post.body },
213
- }));
208
+ First page:
214
209
 
215
- await collection.addItems(items);
210
+ ```js
211
+ const collectionId = "collection-id";
212
+ const collection = await framer.agent.serialize({ id: collectionId, depth: 1 });
213
+ state.page = await framer.agent.paginate({ items: collection.children ?? [] });
214
+ console.log({
215
+ totalResults: state.page.totalResults,
216
+ nextCursor: state.page.nextCursor,
217
+ items: state.page.results.map((item) => ({ id: item.id, ...item.attributes })),
218
+ });
219
+ ```
216
220
 
217
- // Remove items no longer in external source
218
- const newIds = new Set(items.map((i) => i.id));
219
- const toRemove = [...existingIds].filter((id) => !newIds.has(id));
220
- if (toRemove.length) await collection.removeItems(toRemove);
221
+ Next page, if nextCursor is set:
221
222
 
222
- await collection.setPluginData("lastSync", new Date().toISOString());
223
+ ```js
224
+ state.page = await framer.agent.paginate({
225
+ keyName: state.page.keyName,
226
+ cursor: state.page.nextCursor,
227
+ });
228
+ console.log({
229
+ totalResults: state.page.totalResults,
230
+ nextCursor: state.page.nextCursor,
231
+ items: state.page.results.map((item) => ({ id: item.id, ...item.attributes })),
232
+ });
223
233
  ```
224
234
 
225
- #### Field Types
235
+ To search across all collections, read item nodes directly and filter by `$parentId`:
226
236
 
227
- `boolean`, `color`, `number`, `string`, `formattedText` (HTML), `image`, `file`, `link`, `date`, `enum`, `collectionReference`, `multiCollectionReference`, `array` (galleries)
237
+ ```js
238
+ const collectionId = "collection-id";
239
+ const items = await framer.agent.getNodesOfTypes({ types: ["CollectionItemNode"] });
240
+ const collectionItems = items
241
+ .filter((item) => item.$parentId === collectionId)
242
+ .map((item) => ({ id: item.id, ...item.attributes }));
243
+ console.log(collectionItems);
244
+ ```
228
245
 
229
- #### Updating Collection Items
246
+ #### Create and edit items
230
247
 
231
- ```js
232
- // Add or update items (if id matches existing item, it updates)
233
- await collection.addItems([
234
- {
235
- id: "new-item-1",
236
- slug: "hello-world",
237
- fieldData: { titleFieldId: "Hello World" },
238
- },
239
- ]);
248
+ `+CollectionItemNode` adds a row; `SET … $control__<fieldId>` sets cells (a `SET` on an existing item id updates it; `DEL <itemId>` removes it).
240
249
 
241
- // Remove items
242
- await collection.removeItems(["item-id-1", "item-id-2"]);
250
+ ```js
251
+ const { readFileSync } = require("fs");
252
+
253
+ const collectionId = "collection-id";
254
+ const columnToFieldId = { title: "title-field-id", body: "body-field-id" };
255
+ const rows = JSON.parse(readFileSync("/abs/path/to/import.json", "utf8"));
256
+
257
+ const commands = rows.flatMap((row, i) => {
258
+ const itemId = `item-${i}`;
259
+ const sets = Object.entries(columnToFieldId)
260
+ .filter(([col]) => row[col] != null)
261
+ .map(([col, fieldId]) => `$control__${fieldId}="${String(row[col]).replace(/"/g, '\\"')}"`);
262
+ return [`+CollectionItemNode ${itemId} parent="${collectionId}";`, `SET ${itemId} ${sets.join(" ")};`];
263
+ });
243
264
 
244
- // Reorder items
245
- const ids = items.map((i) => i.id).reverse();
246
- await collection.setItemOrder(ids);
265
+ await framer.agent.applyChanges(commands.join(" "));
247
266
  ```
248
267
 
249
268
  #### Writing enum fields
250
269
 
251
- Enum fields are **write-by-case-id, read-by-name**: a write must pass the case's `id`, but `getItems()` reads the value back as the case name. Writing `{ type: "enum", value: "New" }` (the name) is rejected — look up the id from the field definition first:
270
+ With agent methods, enum fields are read and written by case name. Look up the field on the collection's `variables`, verify the case exists, then set the field's `$control__...` key:
252
271
 
253
272
  ```js
254
- const caseId = field.cases.find((c) => c.name === "New").id;
255
- await collection.addItems([
256
- { slug: "hello-world", fieldData: { [field.id]: { type: "enum", value: caseId } } },
257
- ]);
273
+ const collection = (await framer.agent.getNodesOfTypes({ types: ["CollectionNode"] }))
274
+ .find((c) => c.name === "Posts");
275
+ const statusField = collection.variables.find((v) => v.name === "Status");
276
+ const status = statusField.cases.find((name) => name === "New");
277
+
278
+ await framer.agent.applyChanges(
279
+ `+CollectionItemNode newPost parent="${collection.id}";
280
+ SET newPost $control__slug="hello-world" ${statusField.key}="${status}";`,
281
+ );
258
282
  ```
259
283
 
260
- #### Working with CollectionItem
284
+ #### Sync external data
285
+
286
+ Upsert by a stable key (e.g. slug): `SET` existing rows, add new ones, `DEL` rows no longer in the source.
261
287
 
262
288
  ```js
263
- const items = await collection.getItems();
264
- const item = items[0];
289
+ const collectionId = "collection-id";
290
+ const fieldBySource = { title: "title-field-id", body: "body-field-id" };
291
+ const source = await fetch("https://api.example.com/posts").then((r) => r.json());
292
+
293
+ const existing = (await framer.agent.getNodesOfTypes({ types: ["CollectionItemNode"] }))
294
+ .filter((item) => item.$parentId === collectionId);
295
+ const idBySlug = new Map(existing.map((item) => [item.attributes.$control__slug, item.id]));
296
+
297
+ const seen = new Set();
298
+ const commands = source.flatMap((row, i) => {
299
+ const itemId = idBySlug.get(row.slug) ?? `new-${i}`;
300
+ seen.add(itemId);
301
+ const sets = Object.entries(fieldBySource)
302
+ .filter(([k]) => row[k] != null)
303
+ .map(([k, fieldId]) => `$control__${fieldId}="${String(row[k]).replace(/"/g, '\\"')}"`);
304
+ const add = idBySlug.has(row.slug) ? [] : [`+CollectionItemNode ${itemId} parent="${collectionId}";`];
305
+ return [...add, `SET ${itemId} ${sets.join(" ")};`];
306
+ });
307
+ existing.filter((item) => !seen.has(item.id)).forEach((item) => commands.push(`DEL ${item.id};`));
265
308
 
266
- // Update item attributes
267
- await item.setAttributes({ slug: "new-slug" });
309
+ await framer.agent.applyChanges(commands.join(" "));
310
+ ```
268
311
 
269
- // Navigate to item in Framer UI
270
- await item.navigateTo();
312
+ #### Field Types
271
313
 
272
- // Remove item
273
- await item.remove();
274
- ```
314
+ `boolean`, `color`, `number`, `string`, `formattedText` (HTML), `image`, `file`, `link`, `date`, `enum`, `collectionReference`, `multiCollectionReference`, `array` (galleries)
275
315
 
276
316
  ### Working with Images
277
317
 
278
- Canvas editing accepts image URLs directly when setting an image fill, so the typical task is to obtain a URL and use it directly with `applyAgentChanges`.
318
+ Canvas editing accepts image URLs directly when setting an image fill, so the typical task is to obtain a URL and use it directly with `framer.agent.applyChanges`.
279
319
 
280
320
  There are three sources you'll typically pull URLs from:
281
321
 
282
- **Stock photography.** Use `framer.queryImagesForAgent` to source candidates and stash the URL you want:
322
+ **Stock photography.** Use `framer.agent.queryImages` to source candidates and stash the URL you want:
283
323
 
284
324
  ```js
285
- const { results } = await framer.queryImagesForAgent({
325
+ const { results } = await framer.agent.queryImages({
286
326
  source: "unsplash",
287
327
  query: "snow-capped mountains",
288
328
  count: 4,
@@ -303,7 +343,7 @@ state.heroUrl = (await framer.uploadImage({
303
343
  **An image already on the canvas.** Read the node and reuse its existing image URL:
304
344
 
305
345
  ```js
306
- const node = await framer.getNodeForAgent({ id: "<image-node-id>" });
346
+ const node = await framer.agent.getNode({ id: "<image-node-id>" });
307
347
  state.heroUrl = node.attributes.fill;
308
348
  ```
309
349
 
@@ -343,7 +383,7 @@ console.log(state.diagnostics);
343
383
  state.component = state.codeFile.exports.find(
344
384
  (exportItem) => exportItem.type === "component" && exportItem.isDefaultExport,
345
385
  );
346
- await framer.applyAgentChanges(
386
+ await framer.agent.applyChanges(
347
387
  `+ComponentInstanceNode badge parent="wrapper" component="${state.component.componentId}"; SET badge width=120 height=48;`,
348
388
  { pagePath: "/" },
349
389
  );
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: framer
3
3
  description: >
4
- Use when the user wants to design, edit, or publish a website or web page — creating layouts, editing sections, updating text or images, managing CMS collections and content, syncing external data, creating or modifying code components, managing color and text styles, handling localization, or publishing deployments. Trigger when the user mentions Framer, references their website or web pages, asks to edit designs, update site content, or work with any Framer project — even if they don't explicitly say 'Framer'.
4
+ Use when the user wants to design, edit, or publish a website or web page — creating layouts, editing sections, updating text or images, managing CMS collections and content, syncing external data, creating or modifying code components, managing color and text styles, handling localization, or publishing deployments. Trigger when the user mentions Framer, references their website or web pages, asks to edit designs, update site content, or work with any Framer project — even if they don't explicitly say 'Framer'.
5
5
  **Mandatory precondition**: run `npx framer-dalton@latest setup` and let it complete **BEFORE** loading this skill.
6
6
  allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
7
7
  ---