framer-dalton 0.0.27 → 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.
package/dist/cli.js CHANGED
@@ -4,15 +4,15 @@ import path7 from 'path';
4
4
  import { Command } from 'commander';
5
5
  import crypto, { randomUUID } from 'crypto';
6
6
  import http from 'http';
7
- import { spawn, execFile } from 'child_process';
7
+ import { spawn, execFile, execFileSync } from 'child_process';
8
8
  import { z } from 'zod';
9
- import os from 'os';
9
+ import os2 from 'os';
10
10
  import { ErrorCode, FramerAPIError } from 'framer-api';
11
11
  import net from 'net';
12
12
  import { fileURLToPath } from 'url';
13
13
  import { createTRPCClient, httpLink } from '@trpc/client';
14
14
 
15
- /* @framer/ai CLI v0.0.27 */
15
+ /* @framer/ai CLI v0.0.28 */
16
16
  var __defProp = Object.defineProperty;
17
17
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
18
18
 
@@ -100,9 +100,9 @@ function getConfigDir() {
100
100
  return path7.join(process.env.XDG_CONFIG_HOME, "framer");
101
101
  }
102
102
  if (process.platform === "win32") {
103
- return path7.join(process.env.APPDATA || os.homedir(), "framer");
103
+ return path7.join(process.env.APPDATA || os2.homedir(), "framer");
104
104
  }
105
- return path7.join(os.homedir(), ".config", "framer");
105
+ return path7.join(os2.homedir(), ".config", "framer");
106
106
  }
107
107
  __name(getConfigDir, "getConfigDir");
108
108
  function ensureConfigDir() {
@@ -375,15 +375,59 @@ __name(markTelemetryNoticeShown, "markTelemetryNoticeShown");
375
375
  // src/version.ts
376
376
  var VERSION = (
377
377
  // typeof is used to ensure this can be used just via tsx or node etc. without build
378
- "0.0.27"
378
+ "0.0.28"
379
379
  );
380
380
  var trackingEndpoint = "https://events.framer.com/track";
381
381
  var inProgressTrackings = /* @__PURE__ */ new Set();
382
- function sharedFields() {
382
+ var cachedSharedFields;
383
+ function getOsName(platform) {
384
+ switch (platform) {
385
+ case "darwin":
386
+ return "macos";
387
+ case "win32":
388
+ return "windows";
389
+ case "linux":
390
+ return "linux";
391
+ default:
392
+ return "unknown";
393
+ }
394
+ }
395
+ __name(getOsName, "getOsName");
396
+ function getMacOsVersion() {
397
+ try {
398
+ const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
399
+ encoding: "utf8",
400
+ timeout: 1e3
401
+ }).trim();
402
+ return version || "unknown";
403
+ } catch {
404
+ return "unknown";
405
+ }
406
+ }
407
+ __name(getMacOsVersion, "getMacOsVersion");
408
+ function getOsVersion(platform) {
409
+ if (platform === "darwin") return getMacOsVersion();
410
+ const version = os2.release().trim();
411
+ return version || "unknown";
412
+ }
413
+ __name(getOsVersion, "getOsVersion");
414
+ function getOsMetadata() {
415
+ const platform = os2.platform();
416
+ const arch = os2.arch();
383
417
  return {
418
+ arch: arch || "unknown",
419
+ osName: getOsName(platform),
420
+ osVersion: getOsVersion(platform)
421
+ };
422
+ }
423
+ __name(getOsMetadata, "getOsMetadata");
424
+ function sharedFields() {
425
+ cachedSharedFields ??= {
384
426
  machineId: getMachineId(),
385
- cliVersion: VERSION
427
+ cliVersion: VERSION,
428
+ ...getOsMetadata()
386
429
  };
430
+ return cachedSharedFields;
387
431
  }
388
432
  __name(sharedFields, "sharedFields");
389
433
  function wrapEvent(event) {
@@ -396,16 +440,19 @@ function wrapEvent(event) {
396
440
  };
397
441
  }
398
442
  __name(wrapEvent, "wrapEvent");
399
- function postEvent(event) {
443
+ function postEvent(createEvent) {
400
444
  if (!isTelemetryEnabled()) return false;
401
445
  if (process.env.NODE_ENV === "test" && true) return false;
446
+ const event = createEvent();
447
+ const body = JSON.stringify([wrapEvent(event)]);
448
+ debug("tracking", `sending ${event.event} to ${trackingEndpoint}: ${body}`);
402
449
  const promise = fetch(trackingEndpoint, {
403
450
  method: "POST",
404
451
  headers: {
405
452
  "Content-Type": "application/json",
406
453
  "User-Agent": `framer-dalton/${VERSION}`
407
454
  },
408
- body: JSON.stringify([wrapEvent(event)]),
455
+ body,
409
456
  signal: AbortSignal.timeout(
410
457
  5e3
411
458
  /* 5 seconds */
@@ -434,35 +481,43 @@ function waitForTrackingToFinish() {
434
481
  }
435
482
  __name(waitForTrackingToFinish, "waitForTrackingToFinish");
436
483
  function trackAuthStart(payload) {
437
- postEvent({
438
- event: "local_agents_auth_start",
439
- ...sharedFields(),
440
- ...payload
441
- });
484
+ postEvent(
485
+ () => ({
486
+ event: "local_agents_auth_start",
487
+ ...sharedFields(),
488
+ ...payload
489
+ })
490
+ );
442
491
  }
443
492
  __name(trackAuthStart, "trackAuthStart");
444
493
  function trackAuthSuccess(payload) {
445
- postEvent({
446
- event: "local_agents_auth_success",
447
- ...sharedFields(),
448
- ...payload
449
- });
494
+ postEvent(
495
+ () => ({
496
+ event: "local_agents_auth_success",
497
+ ...sharedFields(),
498
+ ...payload
499
+ })
500
+ );
450
501
  }
451
502
  __name(trackAuthSuccess, "trackAuthSuccess");
452
503
  function trackSkillsInstall(payload) {
453
- postEvent({
454
- event: "local_agents_skills_install",
455
- ...sharedFields(),
456
- ...payload
457
- });
504
+ postEvent(
505
+ () => ({
506
+ event: "local_agents_skills_install",
507
+ ...sharedFields(),
508
+ ...payload
509
+ })
510
+ );
458
511
  }
459
512
  __name(trackSkillsInstall, "trackSkillsInstall");
460
513
  function trackError(payload) {
461
- postEvent({
462
- event: "local_agents_error",
463
- ...sharedFields(),
464
- ...payload
465
- });
514
+ postEvent(
515
+ () => ({
516
+ event: "local_agents_error",
517
+ ...sharedFields(),
518
+ ...payload
519
+ })
520
+ );
466
521
  }
467
522
  __name(trackError, "trackError");
468
523
 
@@ -16430,7 +16485,7 @@ async function waitForClaudeCodeSkillDiscovery() {
16430
16485
  );
16431
16486
  }
16432
16487
  __name(waitForClaudeCodeSkillDiscovery, "waitForClaudeCodeSkillDiscovery");
16433
- var FRAMER_TEMPORARY_DIR = path7.join(os.tmpdir(), "framer");
16488
+ var FRAMER_TEMPORARY_DIR = path7.join(os2.tmpdir(), "framer");
16434
16489
  function ensureTemporaryDir() {
16435
16490
  fs7.mkdirSync(FRAMER_TEMPORARY_DIR, { recursive: true });
16436
16491
  }
@@ -16525,7 +16580,7 @@ function cleanupLegacyCanvasEditingSkills(skillRoots = getDefaultSkillRoots()) {
16525
16580
  }
16526
16581
  __name(cleanupLegacyCanvasEditingSkills, "cleanupLegacyCanvasEditingSkills");
16527
16582
  function getDefaultSkillRoots() {
16528
- const home = os.homedir();
16583
+ const home = os2.homedir();
16529
16584
  return [
16530
16585
  path7.join(home, ".agents", "skills"),
16531
16586
  path7.join(home, ".claude", "skills")
@@ -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.27 */
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.27"
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
 
@@ -46,6 +46,8 @@ Choose the highest-priority interface available for the task:
46
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
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
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.
49
51
 
50
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.
51
53
 
@@ -173,109 +175,143 @@ npx framer-dalton docs ScreenshotOptions # Show type + recursively expa
173
175
 
174
176
  ### Working with Collections (CMS)
175
177
 
176
- 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.
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.
177
179
 
178
- **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 `framer.agent.readProject` and `framer.agent.applyChanges`, not the plugin API. Use the plugin Collection API to read available collections and field schema first, then use `framer.agent.applyChanges` to add the list.
179
-
180
- #### Reading Collections
180
+ #### List collections and fields
181
181
 
182
182
  ```js
183
- // Get all collections
184
- const collections = await framer.getCollections();
185
- console.log(collections.map((c) => ({ name: c.name, id: c.id })));
186
-
187
- // Get a specific collection by ID
188
- const collection = await framer.getCollection("collection-id");
189
-
190
- // Get fields (columns)
191
- const fields = await collection.getFields();
192
- // Note that a plain console.log will miss getter-backed properties
193
- console.log(fields.map((f) => ({ id: f.id, name: f.name, type: f.type })));
194
- // [{ id: "BnNuS2i3o", name: "Title", type: "string" }, ...]
195
-
196
- // Get items (rows)
197
- const items = await collection.getItems();
198
- console.log(items);
199
- // [{ 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
+ })));
200
190
  ```
201
191
 
202
- #### 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.
203
195
 
204
196
  ```js
205
- const collection = await framer.getCollection("collection-id");
206
- const existing = await collection.getItems();
207
- 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
+ ```
208
205
 
209
- const externalData = await fetch("https://api.example.com/posts").then((r) =>
210
- r.json(),
211
- );
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.
212
207
 
213
- const items = externalData.map((post) => ({
214
- id: post.id,
215
- slug: post.slug,
216
- fieldData: { title: post.title, content: post.body },
217
- }));
208
+ First page:
218
209
 
219
- 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
+ ```
220
220
 
221
- // Remove items no longer in external source
222
- const newIds = new Set(items.map((i) => i.id));
223
- const toRemove = [...existingIds].filter((id) => !newIds.has(id));
224
- if (toRemove.length) await collection.removeItems(toRemove);
221
+ Next page, if nextCursor is set:
225
222
 
226
- 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
+ });
227
233
  ```
228
234
 
229
- #### Field Types
235
+ To search across all collections, read item nodes directly and filter by `$parentId`:
230
236
 
231
- `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
+ ```
232
245
 
233
- #### Updating Collection Items
246
+ #### Create and edit items
234
247
 
235
- ```js
236
- // Add or update items (if id matches existing item, it updates)
237
- await collection.addItems([
238
- {
239
- id: "new-item-1",
240
- slug: "hello-world",
241
- fieldData: { titleFieldId: "Hello World" },
242
- },
243
- ]);
248
+ `+CollectionItemNode` adds a row; `SET … $control__<fieldId>` sets cells (a `SET` on an existing item id updates it; `DEL <itemId>` removes it).
244
249
 
245
- // Remove items
246
- 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
+ });
247
264
 
248
- // Reorder items
249
- const ids = items.map((i) => i.id).reverse();
250
- await collection.setItemOrder(ids);
265
+ await framer.agent.applyChanges(commands.join(" "));
251
266
  ```
252
267
 
253
268
  #### Writing enum fields
254
269
 
255
- 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:
256
271
 
257
272
  ```js
258
- const caseId = field.cases.find((c) => c.name === "New").id;
259
- await collection.addItems([
260
- { slug: "hello-world", fieldData: { [field.id]: { type: "enum", value: caseId } } },
261
- ]);
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
+ );
262
282
  ```
263
283
 
264
- #### 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.
265
287
 
266
288
  ```js
267
- const items = await collection.getItems();
268
- 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};`));
269
308
 
270
- // Update item attributes
271
- await item.setAttributes({ slug: "new-slug" });
309
+ await framer.agent.applyChanges(commands.join(" "));
310
+ ```
272
311
 
273
- // Navigate to item in Framer UI
274
- await item.navigateTo();
312
+ #### Field Types
275
313
 
276
- // Remove item
277
- await item.remove();
278
- ```
314
+ `boolean`, `color`, `number`, `string`, `formattedText` (HTML), `image`, `file`, `link`, `date`, `enum`, `collectionReference`, `multiCollectionReference`, `array` (galleries)
279
315
 
280
316
  ### Working with Images
281
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-dalton",
3
- "version": "0.0.27",
3
+ "version": "0.0.28",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "main": "./dist/cli.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@biomejs/biome": "^2.4.13",
32
- "@framerjs/framer-events": "0.0.183",
32
+ "@framerjs/framer-events": "0.0.185",
33
33
  "@types/node": "24.12.4",
34
34
  "tsup": "^8.0.2",
35
35
  "tsx": "^4.22.3",