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 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-dalton@latest setup
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-dalton@latest setup` will install the base `framer` and `framer-code-components` skills.
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-dalton@latest setup` BEFORE loading the skill. This command will auto-update the cli and update the skill files.
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 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.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-dalton";
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 || 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.29"
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
 
@@ -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(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")
@@ -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-dalton";
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 os from 'os';
3
- import path from 'path';
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.27 */
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-dalton";
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 path.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
82
+ return path5.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
78
83
  }
79
84
  if (process.platform === "win32") {
80
- return path.join(
81
- process.env.APPDATA || os.homedir(),
85
+ return path5.join(
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 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 = path.dirname(logPath);
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 path.join(process.env.XDG_CONFIG_HOME, "framer");
146
+ return path5.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 path5.join(process.env.APPDATA || os5.homedir(), "framer");
111
150
  }
112
- return path.join(os.homedir(), ".config", "framer");
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 path.join(getConfigDir(), "relay-token");
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.27"
202
+ "0.0.29"
164
203
  );
165
204
 
166
205
  // src/relay-client.ts
167
206
  var __filename$1 = fileURLToPath(import.meta.url);
168
- path.dirname(__filename$1);
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
- allowedDirs;
306
+ allowedRoots;
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
- this.allowedDirs = [...new Set(dirs.map((d) => path.resolve(d)))];
310
+ this.allowedRoots = [
311
+ ...new Set(dirs.map((dir) => this.canonicalizePath(dir)))
312
+ ];
272
313
  }
273
- isPathAllowed(resolved) {
274
- return this.allowedDirs.some((dir) => {
275
- return resolved === dir || resolved.startsWith(dir + path.sep);
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 resolved = path.resolve(filePath);
280
- if (!this.isPathAllowed(resolved)) {
281
- const error = new Error(
282
- `EPERM: operation not permitted, access outside allowed directories: ${filePath}`
283
- );
284
- error.code = "EPERM";
285
- error.errno = -1;
286
- error.syscall = "access";
287
- error.path = filePath;
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
- return resolved;
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", os.tmpdir()]) : new ScopedFS();
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 path.join(getConfigDir(), "settings.json");
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
- function sharedFields() {
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(event) {
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: JSON.stringify([wrapEvent(event)]),
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
- event: "local_agents_relay_start",
855
- ...sharedFields()
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
- event: "local_agents_relay_shutdown",
864
- ...sharedFields()
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
- event: "local_agents_session_create",
871
- ...sharedFields(),
872
- ...payload
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
- event: "local_agents_session_destroy",
879
- ...sharedFields(),
880
- ...payload
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
- event: "local_agents_exec",
887
- ...sharedFields(),
888
- ...payload
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
- event: "local_agents_connection_acquire",
895
- ...sharedFields(),
896
- ...payload
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
- event: "local_agents_connection_reconnect",
903
- ...sharedFields(),
904
- ...payload
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
- event: "local_agents_connection_idle_disconnect",
911
- ...sharedFields(),
912
- ...payload
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
- event: "local_agents_error",
919
- ...sharedFields(),
920
- ...payload
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-dalton:*)", "Bash(npx framer-dalton@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
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-dalton docs` before writing any code.** Do not guess method names or signatures.
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-dalton docs Collection # What methods exist?
22
- npx framer-dalton docs Collection.getItems # What are the parameters and return type?
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-dalton exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-read-collections.js
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-dalton read-project` and `framer-dalton apply-changes`
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-dalton exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
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-dalton exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-team-items.js
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-dalton exec -s <sessionId> -f {{FRAMER_TEMPORARY_DIR}}/<sessionId>-<short-summary>.js
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-dalton <command> --option $value
157
+ npx @framer/agent@latest <command> --option $value
156
158
  ```
157
159
 
158
160
  ## API Documentation
159
161
 
160
162
  ```bash
161
- npx framer-dalton docs # List all available methods
162
- npx framer-dalton docs framer.getCollections # Show top level method
163
- npx framer-dalton docs Collection # Show class with all method signatures
164
- npx framer-dalton docs Collection.addItems # Show method + recursively expand all referenced types
165
- npx framer-dalton docs ScreenshotOptions # Show type + recursively expand all referenced types
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-dalton docs <ClassName>` to verify the current signature.**
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 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
 
@@ -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-dalton@latest setup` and let it complete **BEFORE** loading this skill.
6
- allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
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-dalton@latest setup
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-dalton` commands with elevated permissions from the get-go. Do not try to execute them with the default sandboxed permissions; they will hang or fail.
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-dalton project list
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-dalton session new "<url or id>"
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-dalton project new
58
- npx framer-dalton session new "<returned project id>"
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-dalton project remix "<url, project id, or remix link>"
65
- npx framer-dalton session new "<returned project id>"
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-dalton session list
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.27",
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.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",