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.
- package/README.md +1 -1
- package/dist/cli.js +271 -411
- package/dist/start-relay-server.js +161 -57
- package/docs/skills/framer-project.md +127 -87
- package/docs/skills/framer.md +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs4 from 'fs';
|
|
2
|
-
import
|
|
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.
|
|
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 ||
|
|
86
|
+
process.env.APPDATA || os5.homedir(),
|
|
82
87
|
"framer",
|
|
83
88
|
"relay.log"
|
|
84
89
|
);
|
|
85
90
|
}
|
|
86
|
-
return path.join(
|
|
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 ||
|
|
149
|
+
return path.join(process.env.APPDATA || os5.homedir(), "framer");
|
|
111
150
|
}
|
|
112
|
-
return path.join(
|
|
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.
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
855
|
-
|
|
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
|
-
|
|
864
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
39
|
+
Choose the highest-priority interface available for the task:
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
####
|
|
180
|
+
#### List collections and fields
|
|
177
181
|
|
|
178
182
|
```js
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
####
|
|
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
|
|
202
|
-
const
|
|
203
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
id: post.id,
|
|
211
|
-
slug: post.slug,
|
|
212
|
-
fieldData: { title: post.title, content: post.body },
|
|
213
|
-
}));
|
|
208
|
+
First page:
|
|
214
209
|
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
To search across all collections, read item nodes directly and filter by `$parentId`:
|
|
226
236
|
|
|
227
|
-
|
|
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
|
-
####
|
|
246
|
+
#### Create and edit items
|
|
230
247
|
|
|
231
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
####
|
|
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
|
|
264
|
-
const
|
|
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
|
-
|
|
267
|
-
|
|
309
|
+
await framer.agent.applyChanges(commands.join(" "));
|
|
310
|
+
```
|
|
268
311
|
|
|
269
|
-
|
|
270
|
-
await item.navigateTo();
|
|
312
|
+
#### Field Types
|
|
271
313
|
|
|
272
|
-
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
);
|
package/docs/skills/framer.md
CHANGED
|
@@ -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
|
---
|