framer-dalton 0.0.27 → 0.0.29
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 +3 -3
- package/dist/cli.js +94 -39
- package/dist/start-relay-server.js +229 -81
- package/docs/skills/framer-project.md +123 -87
- package/docs/skills/framer.md +11 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ CLI and agent skills for interacting with Framer projects via the Framer Server
|
|
|
7
7
|
Install or refresh the globally available skills:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npx framer
|
|
10
|
+
npx @framer/agent@latest setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
That installs skills into `~/.agents/skills` and `~/.claude/skills`.
|
|
@@ -20,9 +20,9 @@ The CLI and the installed skills are meant to work together. There are three ski
|
|
|
20
20
|
- `framer-code-components` - explains specific prompts for how to write code components and provides examples.
|
|
21
21
|
- `framer-project-<project id>` - a dynamically-created skill for interacting with a specific project.
|
|
22
22
|
|
|
23
|
-
1. Running `npx framer
|
|
23
|
+
1. Running `npx @framer/agent@latest setup` will install the base `framer` and `framer-code-components` skills.
|
|
24
24
|
2. The frontmatter of `framer-code-components` tells agents to always load `framer` and the generated project skill first.
|
|
25
|
-
3. The frontmatter of `framer` tells agents to run `npx framer
|
|
25
|
+
3. The frontmatter of `framer` tells agents to run `npx @framer/agent@latest setup` BEFORE loading the skill. This command will auto-update the cli and update the skill files.
|
|
26
26
|
4. Creating a new session for a project will create a `framer-project-<project id>` file. This file contains the latest agent system prompt from `framer.agent.getSystemPrompt` and the latest project context from `framer.agent.getContext`. Agents must load this project-specific skill immediately after creating a session before doing any connected-project work.
|
|
27
27
|
|
|
28
28
|
## Local Development
|
package/dist/cli.js
CHANGED
|
@@ -4,20 +4,20 @@ 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
|
|
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.
|
|
15
|
+
/* @framer/ai CLI v0.0.29 */
|
|
16
16
|
var __defProp = Object.defineProperty;
|
|
17
17
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
18
18
|
|
|
19
19
|
// package.json
|
|
20
|
-
var name = "framer
|
|
20
|
+
var name = "@framer/agent";
|
|
21
21
|
|
|
22
22
|
// src/check-node-version.ts
|
|
23
23
|
var MINIMUM_NODE_VERSION = 22;
|
|
@@ -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 ||
|
|
103
|
+
return path7.join(process.env.APPDATA || os2.homedir(), "framer");
|
|
104
104
|
}
|
|
105
|
-
return path7.join(
|
|
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.
|
|
378
|
+
"0.0.29"
|
|
379
379
|
);
|
|
380
380
|
var trackingEndpoint = "https://events.framer.com/track";
|
|
381
381
|
var inProgressTrackings = /* @__PURE__ */ new Set();
|
|
382
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
514
|
+
postEvent(
|
|
515
|
+
() => ({
|
|
516
|
+
event: "local_agents_error",
|
|
517
|
+
...sharedFields(),
|
|
518
|
+
...payload
|
|
519
|
+
})
|
|
520
|
+
);
|
|
466
521
|
}
|
|
467
522
|
__name(trackError, "trackError");
|
|
468
523
|
|
|
@@ -537,7 +592,7 @@ function formatErrorForUser(error, opts) {
|
|
|
537
592
|
const rehydrated = rehydrateFramerAPIError(error);
|
|
538
593
|
if (rehydrated) return rehydrated.toString();
|
|
539
594
|
if (!opts?.fromExecResult && isRelayConnectionLost(error))
|
|
540
|
-
return "Connection to the Framer server was lost. Run `framer session new` to reconnect.";
|
|
595
|
+
return "Connection to the Framer server was lost. Run `npx @framer/agent@latest session new` to reconnect.";
|
|
541
596
|
const ref = getErrorRef(error);
|
|
542
597
|
return ref ? `${error.message} [ref: ${ref}]` : error.message;
|
|
543
598
|
}
|
|
@@ -800,7 +855,7 @@ function runBrowserAuthFlow(options) {
|
|
|
800
855
|
printError(` 1. Open Site Settings \u2192 General: ${settingsUrl}`);
|
|
801
856
|
printError(" 2. Scroll to API Keys and create a new key");
|
|
802
857
|
printError(
|
|
803
|
-
` 3. In another terminal, run: framer project auth ${pollProjectId} <your-api-key>`
|
|
858
|
+
` 3. In another terminal, run: npx @framer/agent@latest project auth ${pollProjectId} <your-api-key>`
|
|
804
859
|
);
|
|
805
860
|
print("");
|
|
806
861
|
print("Waiting for authorization in browser...");
|
|
@@ -814,7 +869,7 @@ function runBrowserAuthFlow(options) {
|
|
|
814
869
|
});
|
|
815
870
|
reject(
|
|
816
871
|
new Error(
|
|
817
|
-
pollProjectId ? "Browser authorization timed out. Use `framer project auth <projectUrlOrId> <apiKey>` instead." : "Browser authorization timed out."
|
|
872
|
+
pollProjectId ? "Browser authorization timed out. Use `npx @framer/agent@latest project auth <projectUrlOrId> <apiKey>` instead." : "Browser authorization timed out."
|
|
818
873
|
)
|
|
819
874
|
);
|
|
820
875
|
});
|
|
@@ -16142,7 +16197,7 @@ function renderDocs(queries) {
|
|
|
16142
16197
|
const suggestion = findClosestMatch(query, methodNames);
|
|
16143
16198
|
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
16144
16199
|
errors.push(
|
|
16145
|
-
`Method '${query}' not found.${hint} Use 'framer docs' to list all.`
|
|
16200
|
+
`Method '${query}' not found.${hint} Use 'npx @framer/agent@latest docs' to list all.`
|
|
16146
16201
|
);
|
|
16147
16202
|
return { lines, errors };
|
|
16148
16203
|
}
|
|
@@ -16191,7 +16246,7 @@ ${typeDef}`);
|
|
|
16191
16246
|
const suggestion = findClosestMatch(query, bareNames);
|
|
16192
16247
|
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
16193
16248
|
errors.push(
|
|
16194
|
-
`'${query}' not found.${hint} Use 'framer docs' to list all.`
|
|
16249
|
+
`'${query}' not found.${hint} Use 'npx @framer/agent@latest docs' to list all.`
|
|
16195
16250
|
);
|
|
16196
16251
|
return { lines, errors };
|
|
16197
16252
|
}
|
|
@@ -16430,7 +16485,7 @@ async function waitForClaudeCodeSkillDiscovery() {
|
|
|
16430
16485
|
);
|
|
16431
16486
|
}
|
|
16432
16487
|
__name(waitForClaudeCodeSkillDiscovery, "waitForClaudeCodeSkillDiscovery");
|
|
16433
|
-
var FRAMER_TEMPORARY_DIR = path7.join(
|
|
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 =
|
|
16583
|
+
const home = os2.homedir();
|
|
16529
16584
|
return [
|
|
16530
16585
|
path7.join(home, ".agents", "skills"),
|
|
16531
16586
|
path7.join(home, ".claude", "skills")
|
|
@@ -16584,7 +16639,7 @@ function installSkills(options = { type: "base" }) {
|
|
|
16584
16639
|
__name(installSkills, "installSkills");
|
|
16585
16640
|
|
|
16586
16641
|
// src/cli.ts
|
|
16587
|
-
var PROGRAM_NAME = "framer
|
|
16642
|
+
var PROGRAM_NAME = "@framer/agent";
|
|
16588
16643
|
var program = new Command();
|
|
16589
16644
|
program.name(PROGRAM_NAME).version(VERSION).description("Framer Server API CLI").option("--debug", "Enable debug logging").hook("preAction", (thisCommand) => {
|
|
16590
16645
|
const opts = thisCommand.opts();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs4 from 'fs';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import 'child_process';
|
|
2
|
+
import os5 from 'os';
|
|
3
|
+
import path5 from 'path';
|
|
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.29 */
|
|
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) => {
|
|
@@ -60,7 +60,7 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// package.json
|
|
63
|
-
var name = "framer
|
|
63
|
+
var name = "@framer/agent";
|
|
64
64
|
|
|
65
65
|
// src/check-node-version.ts
|
|
66
66
|
var MINIMUM_NODE_VERSION = 22;
|
|
@@ -72,44 +72,83 @@ 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
|
-
return
|
|
82
|
+
return path5.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
|
|
78
83
|
}
|
|
79
84
|
if (process.platform === "win32") {
|
|
80
|
-
return
|
|
81
|
-
process.env.APPDATA ||
|
|
85
|
+
return path5.join(
|
|
86
|
+
process.env.APPDATA || os5.homedir(),
|
|
82
87
|
"framer",
|
|
83
88
|
"relay.log"
|
|
84
89
|
);
|
|
85
90
|
}
|
|
86
|
-
return
|
|
91
|
+
return path5.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
|
-
const dir =
|
|
99
|
+
const dir = path5.dirname(logPath);
|
|
94
100
|
fs4.mkdirSync(dir, { recursive: true });
|
|
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() {
|
|
106
145
|
if (process.env.XDG_CONFIG_HOME) {
|
|
107
|
-
return
|
|
146
|
+
return path5.join(process.env.XDG_CONFIG_HOME, "framer");
|
|
108
147
|
}
|
|
109
148
|
if (process.platform === "win32") {
|
|
110
|
-
return
|
|
149
|
+
return path5.join(process.env.APPDATA || os5.homedir(), "framer");
|
|
111
150
|
}
|
|
112
|
-
return
|
|
151
|
+
return path5.join(os5.homedir(), ".config", "framer");
|
|
113
152
|
}
|
|
114
153
|
__name(getConfigDir, "getConfigDir");
|
|
115
154
|
function ensureConfigDir() {
|
|
@@ -120,7 +159,7 @@ __name(ensureConfigDir, "ensureConfigDir");
|
|
|
120
159
|
|
|
121
160
|
// src/config/relay-token.ts
|
|
122
161
|
function getRelayTokenPath() {
|
|
123
|
-
return
|
|
162
|
+
return path5.join(getConfigDir(), "relay-token");
|
|
124
163
|
}
|
|
125
164
|
__name(getRelayTokenPath, "getRelayTokenPath");
|
|
126
165
|
function generateRelayToken() {
|
|
@@ -160,12 +199,12 @@ __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.29"
|
|
164
203
|
);
|
|
165
204
|
|
|
166
205
|
// src/relay-client.ts
|
|
167
206
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
168
|
-
|
|
207
|
+
path5.dirname(__filename$1);
|
|
169
208
|
var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
|
|
170
209
|
createTRPCClient({
|
|
171
210
|
links: [
|
|
@@ -264,30 +303,74 @@ var ScopedFS = class {
|
|
|
264
303
|
static {
|
|
265
304
|
__name(this, "ScopedFS");
|
|
266
305
|
}
|
|
267
|
-
|
|
306
|
+
allowedRoots;
|
|
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
|
-
this.
|
|
310
|
+
this.allowedRoots = [
|
|
311
|
+
...new Set(dirs.map((dir) => this.canonicalizePath(dir)))
|
|
312
|
+
];
|
|
272
313
|
}
|
|
273
|
-
isPathAllowed(
|
|
274
|
-
return this.
|
|
275
|
-
return
|
|
314
|
+
isPathAllowed(candidatePath) {
|
|
315
|
+
return this.allowedRoots.some((allowedRoot) => {
|
|
316
|
+
return candidatePath === allowedRoot || candidatePath.startsWith(allowedRoot + path5.sep);
|
|
276
317
|
});
|
|
277
318
|
}
|
|
278
319
|
resolvePath(filePath) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
320
|
+
const resolvedPath = path5.resolve(filePath);
|
|
321
|
+
const canonicalPath = this.canonicalizePath(resolvedPath);
|
|
322
|
+
if (!this.isPathAllowed(canonicalPath)) {
|
|
323
|
+
this.throwOutsideAllowedRoots(filePath);
|
|
324
|
+
}
|
|
325
|
+
return resolvedPath;
|
|
326
|
+
}
|
|
327
|
+
canonicalizePath(inputPath) {
|
|
328
|
+
const resolvedPath = path5.resolve(inputPath);
|
|
329
|
+
const missingSegments = [];
|
|
330
|
+
let ancestorPath = resolvedPath;
|
|
331
|
+
while (true) {
|
|
332
|
+
const realPath = this.resolveRealPath(ancestorPath);
|
|
333
|
+
if (realPath !== null) {
|
|
334
|
+
return path5.resolve(realPath, ...missingSegments);
|
|
335
|
+
}
|
|
336
|
+
if (this.isSymbolicLink(ancestorPath)) {
|
|
337
|
+
this.throwOutsideAllowedRoots(inputPath);
|
|
338
|
+
}
|
|
339
|
+
const parentPath = path5.dirname(ancestorPath);
|
|
340
|
+
if (parentPath === ancestorPath) {
|
|
341
|
+
return resolvedPath;
|
|
342
|
+
}
|
|
343
|
+
missingSegments.unshift(path5.basename(ancestorPath));
|
|
344
|
+
ancestorPath = parentPath;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
resolveRealPath(targetPath) {
|
|
348
|
+
try {
|
|
349
|
+
return path5.resolve(fs4.realpathSync.native(targetPath));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const errnoError = error;
|
|
352
|
+
if (errnoError.code === "ENOENT") {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
288
355
|
throw error;
|
|
289
356
|
}
|
|
290
|
-
|
|
357
|
+
}
|
|
358
|
+
isSymbolicLink(targetPath) {
|
|
359
|
+
try {
|
|
360
|
+
return fs4.lstatSync(targetPath).isSymbolicLink();
|
|
361
|
+
} catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
throwOutsideAllowedRoots(filePath) {
|
|
366
|
+
const error = new Error(
|
|
367
|
+
`EPERM: operation not permitted, access outside allowed directories: ${filePath}`
|
|
368
|
+
);
|
|
369
|
+
error.code = "EPERM";
|
|
370
|
+
error.errno = -1;
|
|
371
|
+
error.syscall = "access";
|
|
372
|
+
error.path = filePath;
|
|
373
|
+
throw error;
|
|
291
374
|
}
|
|
292
375
|
// Sync methods
|
|
293
376
|
readFileSync = /* @__PURE__ */ __name((filePath, options) => {
|
|
@@ -533,7 +616,7 @@ async function execute(session, code, options = {}) {
|
|
|
533
616
|
output.push(args.map((arg) => formatValue(arg)).join(" "));
|
|
534
617
|
}, "info")
|
|
535
618
|
};
|
|
536
|
-
const scopedFs = cwd ? new ScopedFS([cwd, "/tmp",
|
|
619
|
+
const scopedFs = cwd ? new ScopedFS([cwd, "/tmp", os5.tmpdir()]) : new ScopedFS();
|
|
537
620
|
const sandboxedRequire = createSandboxedRequire(scopedFs);
|
|
538
621
|
const vmContextObj = {
|
|
539
622
|
// Framer API
|
|
@@ -737,7 +820,7 @@ var SettingsFileSchema = z.object({
|
|
|
737
820
|
telemetryNoticeShown: z.boolean().optional()
|
|
738
821
|
});
|
|
739
822
|
function getSettingsPath() {
|
|
740
|
-
return
|
|
823
|
+
return path5.join(getConfigDir(), "settings.json");
|
|
741
824
|
}
|
|
742
825
|
__name(getSettingsPath, "getSettingsPath");
|
|
743
826
|
var settings;
|
|
@@ -795,11 +878,55 @@ function isTelemetryEnabled() {
|
|
|
795
878
|
__name(isTelemetryEnabled, "isTelemetryEnabled");
|
|
796
879
|
var trackingEndpoint = "https://events.framer.com/track";
|
|
797
880
|
var inProgressTrackings = /* @__PURE__ */ new Set();
|
|
798
|
-
|
|
881
|
+
var cachedSharedFields;
|
|
882
|
+
function getOsName(platform) {
|
|
883
|
+
switch (platform) {
|
|
884
|
+
case "darwin":
|
|
885
|
+
return "macos";
|
|
886
|
+
case "win32":
|
|
887
|
+
return "windows";
|
|
888
|
+
case "linux":
|
|
889
|
+
return "linux";
|
|
890
|
+
default:
|
|
891
|
+
return "unknown";
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
__name(getOsName, "getOsName");
|
|
895
|
+
function getMacOsVersion() {
|
|
896
|
+
try {
|
|
897
|
+
const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
|
|
898
|
+
encoding: "utf8",
|
|
899
|
+
timeout: 1e3
|
|
900
|
+
}).trim();
|
|
901
|
+
return version || "unknown";
|
|
902
|
+
} catch {
|
|
903
|
+
return "unknown";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
__name(getMacOsVersion, "getMacOsVersion");
|
|
907
|
+
function getOsVersion(platform) {
|
|
908
|
+
if (platform === "darwin") return getMacOsVersion();
|
|
909
|
+
const version = os5.release().trim();
|
|
910
|
+
return version || "unknown";
|
|
911
|
+
}
|
|
912
|
+
__name(getOsVersion, "getOsVersion");
|
|
913
|
+
function getOsMetadata() {
|
|
914
|
+
const platform = os5.platform();
|
|
915
|
+
const arch = os5.arch();
|
|
799
916
|
return {
|
|
917
|
+
arch: arch || "unknown",
|
|
918
|
+
osName: getOsName(platform),
|
|
919
|
+
osVersion: getOsVersion(platform)
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
__name(getOsMetadata, "getOsMetadata");
|
|
923
|
+
function sharedFields() {
|
|
924
|
+
cachedSharedFields ??= {
|
|
800
925
|
machineId: getMachineId(),
|
|
801
|
-
cliVersion: VERSION
|
|
926
|
+
cliVersion: VERSION,
|
|
927
|
+
...getOsMetadata()
|
|
802
928
|
};
|
|
929
|
+
return cachedSharedFields;
|
|
803
930
|
}
|
|
804
931
|
__name(sharedFields, "sharedFields");
|
|
805
932
|
function wrapEvent(event) {
|
|
@@ -812,16 +939,19 @@ function wrapEvent(event) {
|
|
|
812
939
|
};
|
|
813
940
|
}
|
|
814
941
|
__name(wrapEvent, "wrapEvent");
|
|
815
|
-
function postEvent(
|
|
942
|
+
function postEvent(createEvent) {
|
|
816
943
|
if (!isTelemetryEnabled()) return false;
|
|
817
944
|
if (process.env.NODE_ENV === "test" && true) return false;
|
|
945
|
+
const event = createEvent();
|
|
946
|
+
const body = JSON.stringify([wrapEvent(event)]);
|
|
947
|
+
debug("tracking", `sending ${event.event} to ${trackingEndpoint}: ${body}`);
|
|
818
948
|
const promise = fetch(trackingEndpoint, {
|
|
819
949
|
method: "POST",
|
|
820
950
|
headers: {
|
|
821
951
|
"Content-Type": "application/json",
|
|
822
952
|
"User-Agent": `framer-dalton/${VERSION}`
|
|
823
953
|
},
|
|
824
|
-
body
|
|
954
|
+
body,
|
|
825
955
|
signal: AbortSignal.timeout(
|
|
826
956
|
5e3
|
|
827
957
|
/* 5 seconds */
|
|
@@ -850,75 +980,93 @@ function waitForTrackingToFinish() {
|
|
|
850
980
|
}
|
|
851
981
|
__name(waitForTrackingToFinish, "waitForTrackingToFinish");
|
|
852
982
|
function trackRelayStart() {
|
|
853
|
-
postEvent(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
983
|
+
postEvent(
|
|
984
|
+
() => ({
|
|
985
|
+
event: "local_agents_relay_start",
|
|
986
|
+
...sharedFields()
|
|
987
|
+
})
|
|
988
|
+
);
|
|
857
989
|
}
|
|
858
990
|
__name(trackRelayStart, "trackRelayStart");
|
|
859
991
|
var relayShutdownTracked = false;
|
|
860
992
|
function trackRelayShutdown() {
|
|
861
993
|
if (relayShutdownTracked) return;
|
|
862
|
-
relayShutdownTracked = postEvent(
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
994
|
+
relayShutdownTracked = postEvent(
|
|
995
|
+
() => ({
|
|
996
|
+
event: "local_agents_relay_shutdown",
|
|
997
|
+
...sharedFields()
|
|
998
|
+
})
|
|
999
|
+
);
|
|
866
1000
|
}
|
|
867
1001
|
__name(trackRelayShutdown, "trackRelayShutdown");
|
|
868
1002
|
function trackSessionCreate(payload) {
|
|
869
|
-
postEvent(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1003
|
+
postEvent(
|
|
1004
|
+
() => ({
|
|
1005
|
+
event: "local_agents_session_create",
|
|
1006
|
+
...sharedFields(),
|
|
1007
|
+
...payload
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
874
1010
|
}
|
|
875
1011
|
__name(trackSessionCreate, "trackSessionCreate");
|
|
876
1012
|
function trackSessionDestroy(payload) {
|
|
877
|
-
postEvent(
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1013
|
+
postEvent(
|
|
1014
|
+
() => ({
|
|
1015
|
+
event: "local_agents_session_destroy",
|
|
1016
|
+
...sharedFields(),
|
|
1017
|
+
...payload
|
|
1018
|
+
})
|
|
1019
|
+
);
|
|
882
1020
|
}
|
|
883
1021
|
__name(trackSessionDestroy, "trackSessionDestroy");
|
|
884
1022
|
function trackExec(payload) {
|
|
885
|
-
postEvent(
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1023
|
+
postEvent(
|
|
1024
|
+
() => ({
|
|
1025
|
+
event: "local_agents_exec",
|
|
1026
|
+
...sharedFields(),
|
|
1027
|
+
...payload
|
|
1028
|
+
})
|
|
1029
|
+
);
|
|
890
1030
|
}
|
|
891
1031
|
__name(trackExec, "trackExec");
|
|
892
1032
|
function trackConnectionAcquire(payload) {
|
|
893
|
-
postEvent(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1033
|
+
postEvent(
|
|
1034
|
+
() => ({
|
|
1035
|
+
event: "local_agents_connection_acquire",
|
|
1036
|
+
...sharedFields(),
|
|
1037
|
+
...payload
|
|
1038
|
+
})
|
|
1039
|
+
);
|
|
898
1040
|
}
|
|
899
1041
|
__name(trackConnectionAcquire, "trackConnectionAcquire");
|
|
900
1042
|
function trackConnectionReconnect(payload) {
|
|
901
|
-
postEvent(
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1043
|
+
postEvent(
|
|
1044
|
+
() => ({
|
|
1045
|
+
event: "local_agents_connection_reconnect",
|
|
1046
|
+
...sharedFields(),
|
|
1047
|
+
...payload
|
|
1048
|
+
})
|
|
1049
|
+
);
|
|
906
1050
|
}
|
|
907
1051
|
__name(trackConnectionReconnect, "trackConnectionReconnect");
|
|
908
1052
|
function trackConnectionIdleDisconnect(payload) {
|
|
909
|
-
postEvent(
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1053
|
+
postEvent(
|
|
1054
|
+
() => ({
|
|
1055
|
+
event: "local_agents_connection_idle_disconnect",
|
|
1056
|
+
...sharedFields(),
|
|
1057
|
+
...payload
|
|
1058
|
+
})
|
|
1059
|
+
);
|
|
914
1060
|
}
|
|
915
1061
|
__name(trackConnectionIdleDisconnect, "trackConnectionIdleDisconnect");
|
|
916
1062
|
function trackError(payload) {
|
|
917
|
-
postEvent(
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1063
|
+
postEvent(
|
|
1064
|
+
() => ({
|
|
1065
|
+
event: "local_agents_error",
|
|
1066
|
+
...sharedFields(),
|
|
1067
|
+
...payload
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
922
1070
|
}
|
|
923
1071
|
__name(trackError, "trackError");
|
|
924
1072
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: {{SKILL_NAME}}
|
|
3
3
|
description: "Project-scoped Framer skill for project {{PROJECT_ID}}. Covers project reads, edits, change review, publishing, image sourcing, component operations, and canvas editing. Very important: never load this skill without having already read the `framer` skill and without having already run `session new`, which will dynamically update this skill."
|
|
4
|
-
allowed-tools: ["Bash(npx framer
|
|
4
|
+
allowed-tools: ["Bash(npx @framer/agent:*)", "Bash(npx @framer/agent@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Project Scope
|
|
@@ -15,11 +15,11 @@ Every connected-project task follows these steps:
|
|
|
15
15
|
|
|
16
16
|
### 1. Look up the API (before EVERY new use of an API method)
|
|
17
17
|
|
|
18
|
-
**You MUST run `npx framer
|
|
18
|
+
**You MUST run `npx @framer/agent@latest docs` before writing any code.** Do not guess method names or signatures.
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx framer
|
|
22
|
-
npx framer
|
|
21
|
+
npx @framer/agent@latest docs Collection # What methods exist?
|
|
22
|
+
npx @framer/agent@latest docs Collection.getItems # What are the parameters and return type?
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### 2. Execute code
|
|
@@ -27,7 +27,7 @@ npx framer-dalton docs Collection.getItems # What are the parameters and return
|
|
|
27
27
|
Only after checking docs, use your write tool to write your code to a unique file under `{{FRAMER_TEMPORARY_DIR}}/`, then execute it with `-f`. Do not create code files with shell heredocs or `cat`. Name each file `<sessionId>-<short-summary>.js` where `<short-summary>` is a brief kebab-case description (e.g., `1-read-collections.js`, `1-add-team-member.js`).
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npx framer
|
|
30
|
+
npx @framer/agent@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-read-collections.js
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### 3. Store results in `state`
|
|
@@ -38,7 +38,7 @@ Always save results you'll need again. Don't repeat API calls.
|
|
|
38
38
|
|
|
39
39
|
Choose the highest-priority interface available for the task:
|
|
40
40
|
|
|
41
|
-
1. Subcommands, such as `framer
|
|
41
|
+
1. Subcommands, such as `npx @framer/agent@latest read-project` and `npx @framer/agent@latest apply-changes`
|
|
42
42
|
2. Agent-specific methods, such as `framer.agent.readProject`, `framer.agent.applyChanges`, and `framer.agent.publish`
|
|
43
43
|
3. Generic plugin API methods, such as top-level `framer.*` methods
|
|
44
44
|
|
|
@@ -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
|
|
|
@@ -85,7 +87,7 @@ state.collections = await framer.getCollections();
|
|
|
85
87
|
```
|
|
86
88
|
|
|
87
89
|
```bash
|
|
88
|
-
npx framer
|
|
90
|
+
npx @framer/agent@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
```js
|
|
@@ -95,7 +97,7 @@ console.log(state.teamItems.length);
|
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
```bash
|
|
98
|
-
npx framer
|
|
100
|
+
npx @framer/agent@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-team-items.js
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
Store anything you'll reference again.
|
|
@@ -139,7 +141,7 @@ Prompting may take a while to complete, so set the command timeout to 10 minutes
|
|
|
139
141
|
Prefer using your write tool to write code to a unique file under `{{FRAMER_TEMPORARY_DIR}}/` and executing it with `-f`. Do not create code files with shell heredocs or `cat`:
|
|
140
142
|
|
|
141
143
|
```bash
|
|
142
|
-
npx framer
|
|
144
|
+
npx @framer/agent@latest exec -s <sessionId> -f {{FRAMER_TEMPORARY_DIR}}/<sessionId>-<short-summary>.js
|
|
143
145
|
```
|
|
144
146
|
|
|
145
147
|
For short snippets, `exec` also accepts `-e <code>` or code piped on stdin.
|
|
@@ -152,130 +154,164 @@ In Windows PowerShell, if an argument contains nested quotes, use a single-quote
|
|
|
152
154
|
$value = @'
|
|
153
155
|
[{"key":"value","filter":["text","$rect"]}]
|
|
154
156
|
'@
|
|
155
|
-
npx framer
|
|
157
|
+
npx @framer/agent@latest <command> --option $value
|
|
156
158
|
```
|
|
157
159
|
|
|
158
160
|
## API Documentation
|
|
159
161
|
|
|
160
162
|
```bash
|
|
161
|
-
npx framer
|
|
162
|
-
npx framer
|
|
163
|
-
npx framer
|
|
164
|
-
npx framer
|
|
165
|
-
npx framer
|
|
163
|
+
npx @framer/agent@latest docs # List all available methods
|
|
164
|
+
npx @framer/agent@latest docs framer.getCollections # Show top level method
|
|
165
|
+
npx @framer/agent@latest docs Collection # Show class with all method signatures
|
|
166
|
+
npx @framer/agent@latest docs Collection.addItems # Show method + recursively expand all referenced types
|
|
167
|
+
npx @framer/agent@latest docs ScreenshotOptions # Show type + recursively expand all referenced types
|
|
166
168
|
```
|
|
167
169
|
|
|
168
170
|
`docs` with no arguments lists available methods. Looking up a class shows its full definition without expanding referenced types. Looking up a specific method or type automatically expands all referenced types recursively.
|
|
169
171
|
|
|
170
172
|
## API Examples
|
|
171
173
|
|
|
172
|
-
**STOP: These are patterns only. Before using any method below, run `npx framer
|
|
174
|
+
**STOP: These are patterns only. Before using any method below, run `npx @framer/agent@latest docs <ClassName>` to verify the current signature.**
|
|
173
175
|
|
|
174
176
|
### Working with Collections (CMS)
|
|
175
177
|
|
|
176
|
-
Collections are
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
#### Reading Collections
|
|
180
|
+
#### List collections and fields
|
|
181
181
|
|
|
182
182
|
```js
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
####
|
|
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
|
|
206
|
-
const
|
|
207
|
-
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
|
+
```
|
|
208
205
|
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
id: post.id,
|
|
215
|
-
slug: post.slug,
|
|
216
|
-
fieldData: { title: post.title, content: post.body },
|
|
217
|
-
}));
|
|
208
|
+
First page:
|
|
218
209
|
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
To search across all collections, read item nodes directly and filter by `$parentId`:
|
|
230
236
|
|
|
231
|
-
|
|
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
|
-
####
|
|
246
|
+
#### Create and edit items
|
|
234
247
|
|
|
235
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
####
|
|
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
|
|
268
|
-
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};`));
|
|
269
308
|
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
await framer.agent.applyChanges(commands.join(" "));
|
|
310
|
+
```
|
|
272
311
|
|
|
273
|
-
|
|
274
|
-
await item.navigateTo();
|
|
312
|
+
#### Field Types
|
|
275
313
|
|
|
276
|
-
|
|
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/docs/skills/framer.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
name: framer
|
|
3
3
|
description: >
|
|
4
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
|
-
**Mandatory precondition**: run `npx framer
|
|
6
|
-
allowed-tools: ["Bash(npx framer
|
|
5
|
+
**Mandatory precondition**: run `npx @framer/agent@latest setup` and let it complete **BEFORE** loading this skill.
|
|
6
|
+
allowed-tools: ["Bash(npx @framer/agent:*)", "Bash(npx @framer/agent@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
If you didn't run this command before loading the skill, run it now:
|
|
10
10
|
```bash
|
|
11
|
-
npx framer
|
|
11
|
+
npx @framer/agent@latest setup
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Capabilities
|
|
@@ -29,14 +29,14 @@ What you can do with the Framer CLI:
|
|
|
29
29
|
|
|
30
30
|
### Permissions
|
|
31
31
|
|
|
32
|
-
The Framer CLI requires network access (to download packages and connect to Framer servers) and file-system access outside the project directory (to install skills into `~/.agents/skills/` and `~/.claude/skills/`). In sandboxed environments, you must execute `npx framer
|
|
32
|
+
The Framer CLI requires network access (to download packages and connect to Framer servers) and file-system access outside the project directory (to install skills into `~/.agents/skills/` and `~/.claude/skills/`). In sandboxed environments, you must execute `npx @framer/agent@latest` commands with elevated permissions from the get-go. Do not try to execute them with the default sandboxed permissions; they will hang or fail.
|
|
33
33
|
|
|
34
34
|
### Session Management
|
|
35
35
|
|
|
36
36
|
If the user refers to a project by name or description rather than giving a URL, first inspect the recent projects:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
npx framer
|
|
39
|
+
npx @framer/agent@latest project list
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Use that list to infer the likely project from the names and recency. If the right project is already known, use its project ID with `session new`. If there are multiple possible matches, clarify with the user. Only ask the user for a Project URL if there is no clear match. Avoid talking about the technical contents of returned data like IDs.
|
|
@@ -46,7 +46,7 @@ Each session maintains a persistent connection to a Framer project. Use sessions
|
|
|
46
46
|
Create a session against an existing project:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
npx framer
|
|
49
|
+
npx @framer/agent@latest session new "<url or id>"
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
This prints the session ID. You must always use that session ID with `-s <id>` for all subsequent commands. Using the same session preserves your `state` between calls.
|
|
@@ -54,15 +54,15 @@ This prints the session ID. You must always use that session ID with `-s <id>` f
|
|
|
54
54
|
To create a brand new empty project and connect to it:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
npx framer
|
|
58
|
-
npx framer
|
|
57
|
+
npx @framer/agent@latest project new
|
|
58
|
+
npx @framer/agent@latest session new "<returned project id>"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
To remix (duplicate) an existing project and connect to the copy:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
npx framer
|
|
65
|
-
npx framer
|
|
64
|
+
npx @framer/agent@latest project remix "<url, project id, or remix link>"
|
|
65
|
+
npx @framer/agent@latest session new "<returned project id>"
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Note that during beta, you cannot connect to non-beta projects. If creating a session errors with a message about this, you need to move your project to beta.
|
|
@@ -70,7 +70,7 @@ Note that during beta, you cannot connect to non-beta projects. If creating a se
|
|
|
70
70
|
List active sessions:
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
npx framer
|
|
73
|
+
npx @framer/agent@latest session list
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
## Project-scoped skill
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-dalton",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
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.
|
|
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",
|