framer-dalton 0.0.25 → 0.0.27

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.
@@ -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.25 */
17
+ /* @framer/ai relay server v0.0.27 */
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) => {
@@ -160,7 +160,7 @@ __name(debug, "debug");
160
160
  // src/version.ts
161
161
  var VERSION = (
162
162
  // typeof is used to ensure this can be used just via tsx or node etc. without build
163
- "0.0.25"
163
+ "0.0.27"
164
164
  );
165
165
 
166
166
  // src/relay-client.ts
@@ -182,6 +182,35 @@ createTRPCClient({
182
182
  ]
183
183
  });
184
184
 
185
+ // src/agent-thread-context.ts
186
+ var MAX_AGENT_THREAD_ID_LENGTH = 256;
187
+ var MAX_AGENT_PROVIDER_LENGTH = 64;
188
+ var SAFE_VALUE_PATTERN = /^[A-Za-z0-9._:-]+$/;
189
+ function normalizeValue(value, maxLength) {
190
+ const trimmed = value?.trim();
191
+ if (!trimmed) return void 0;
192
+ if (trimmed.length > maxLength) return void 0;
193
+ if (!SAFE_VALUE_PATTERN.test(trimmed)) return void 0;
194
+ return trimmed;
195
+ }
196
+ __name(normalizeValue, "normalizeValue");
197
+ function normalizeAgentThreadContext(input) {
198
+ const agentThreadId = normalizeValue(
199
+ input.agentThreadId,
200
+ MAX_AGENT_THREAD_ID_LENGTH
201
+ );
202
+ if (!agentThreadId) return {};
203
+ const agentProvider = normalizeValue(
204
+ input.agentProvider,
205
+ MAX_AGENT_PROVIDER_LENGTH
206
+ );
207
+ return {
208
+ agentThreadId,
209
+ ...agentProvider ? { agentProvider } : {}
210
+ };
211
+ }
212
+ __name(normalizeAgentThreadContext, "normalizeAgentThreadContext");
213
+
185
214
  // src/connection-errors.ts
186
215
  var CONNECTION_ERROR_PATTERNS = [
187
216
  "Connection closed",
@@ -198,8 +227,15 @@ var CONNECTION_ERROR_PATTERNS = [
198
227
  // Our own check in executor.ts
199
228
  "Execution timed out after",
200
229
  // Likely dead connection
201
- "Session expired"
230
+ "Session expired",
202
231
  // Server-side session expiry
232
+ // Typed page-death (PROJECT_CLOSED) for the string fallback; isRetryableError is the primary path.
233
+ "Project session crashed",
234
+ // PageCrashedError (renderer crash / OOM)
235
+ "Project was closed",
236
+ // StaleSessionError (page closed/reaped)
237
+ "Session was recycled"
238
+ // SessionRecycledError (destroy->respawn reconnect race)
203
239
  ];
204
240
  function isConnectionError(errorMessage) {
205
241
  return CONNECTION_ERROR_PATTERNS.some(
@@ -207,6 +243,18 @@ function isConnectionError(errorMessage) {
207
243
  );
208
244
  }
209
245
  __name(isConnectionError, "isConnectionError");
246
+ function isRetryableError(err) {
247
+ if (err && typeof err === "object") {
248
+ const e = err;
249
+ if (e.retryable === true) return true;
250
+ if (e.code === "PROJECT_CLOSED") return true;
251
+ if (typeof e.message === "string" && isConnectionError(e.message))
252
+ return true;
253
+ return false;
254
+ }
255
+ return typeof err === "string" && isConnectionError(err);
256
+ }
257
+ __name(isRetryableError, "isRetryableError");
210
258
  var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
211
259
  function isAuthError(errorMessage) {
212
260
  return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
@@ -541,20 +589,37 @@ async function execute(session, code, options = {}) {
541
589
  return { output };
542
590
  } catch (err) {
543
591
  const errorMessage = err instanceof Error ? err.message : String(err);
544
- if (isConnectionError(errorMessage)) {
592
+ const retryable = isRetryableError(err);
593
+ if (retryable || isConnectionError(errorMessage)) {
545
594
  session.connection.markDisconnected();
546
595
  }
547
596
  const errorRef = getErrorRef(err);
548
597
  return {
549
598
  output,
550
599
  error: errorMessage,
551
- ...errorRef ? { errorRef } : {}
600
+ ...errorRef ? { errorRef } : {},
601
+ ...retryable ? { retryable: true } : {}
552
602
  };
553
603
  } finally {
554
604
  if (timeoutId) clearTimeout(timeoutId);
555
605
  }
556
606
  }
557
607
  __name(execute, "execute");
608
+ function isReconnectable(err) {
609
+ return isRetryableError(err) || isConnectionError(err instanceof Error ? err.message : String(err));
610
+ }
611
+ __name(isReconnectable, "isReconnectable");
612
+ function connectionFailureResult(prefix, err) {
613
+ const inner = err instanceof Error ? err.message : String(err);
614
+ const errorRef = getErrorRef(err);
615
+ return {
616
+ output: [],
617
+ error: `${prefix}: ${inner}`,
618
+ ...errorRef ? { errorRef } : {},
619
+ ...isRetryableError(err) ? { retryable: true } : {}
620
+ };
621
+ }
622
+ __name(connectionFailureResult, "connectionFailureResult");
558
623
  async function executeWithReconnect(session, code, options, execId) {
559
624
  if (!session.connection.isConnected()) {
560
625
  log(
@@ -563,17 +628,27 @@ async function executeWithReconnect(session, code, options, execId) {
563
628
  try {
564
629
  await session.connection.reconnect();
565
630
  } catch (err) {
566
- const inner = err instanceof Error ? err.message : String(err);
567
- const errorRef = getErrorRef(err);
568
- return {
569
- output: [],
570
- error: `Failed to get connection for session: ${inner}`,
571
- ...errorRef ? { errorRef } : {}
572
- };
631
+ if (!isReconnectable(err)) {
632
+ return connectionFailureResult(
633
+ "Failed to get connection for session",
634
+ err
635
+ );
636
+ }
637
+ log(
638
+ `exec.reconnect.retry exec=${execId} session=${session.id} reason="${err instanceof Error ? err.message : String(err)}"`
639
+ );
640
+ try {
641
+ await session.connection.reconnect();
642
+ } catch (err2) {
643
+ return connectionFailureResult(
644
+ "Failed to get connection for session",
645
+ err2
646
+ );
647
+ }
573
648
  }
574
649
  }
575
650
  const result = await execute(session, code, options);
576
- if (!result.error || !isConnectionError(result.error)) {
651
+ if (!result.error || !(result.retryable || isConnectionError(result.error))) {
577
652
  return result;
578
653
  }
579
654
  log(
@@ -582,16 +657,13 @@ async function executeWithReconnect(session, code, options, execId) {
582
657
  try {
583
658
  await session.connection.reconnect();
584
659
  } catch (err) {
585
- const inner = err instanceof Error ? err.message : String(err);
586
- const errorRef = getErrorRef(err);
587
660
  log(
588
- `reconnect.failed exec=${execId} session=${session.id} req=${session.connection.framer.requestId} error="${inner}"`
661
+ `reconnect.failed exec=${execId} session=${session.id} req=${session.connection.framer.requestId} error="${err instanceof Error ? err.message : String(err)}"`
662
+ );
663
+ return connectionFailureResult(
664
+ "Connection lost and failed to reconnect",
665
+ err
589
666
  );
590
- return {
591
- output: [],
592
- error: `Connection lost and failed to reconnect: ${inner}`,
593
- ...errorRef ? { errorRef } : {}
594
- };
595
667
  }
596
668
  log(
597
669
  `reconnect.success exec=${execId} session=${session.id} req=${session.connection.framer.requestId}`
@@ -897,10 +969,11 @@ var Connection = class _Connection {
897
969
  headlessServerUrl: this.headlessServerUrl
898
970
  }));
899
971
  }
900
- createSession(id) {
972
+ createSession(id, agentThreadContext) {
901
973
  const session = {
902
974
  id,
903
975
  state: {},
976
+ agentThreadContext,
904
977
  lastActivityAt: Date.now(),
905
978
  inflight: 0,
906
979
  connection: this
@@ -971,14 +1044,17 @@ var SessionManager = class {
971
1044
  }
972
1045
  connections = [];
973
1046
  idleCheck = null;
974
- async create(projectId, userId, apiKey, headlessServerUrl) {
1047
+ async create(projectId, userId, apiKey, headlessServerUrl, agentThreadContext = {}) {
975
1048
  const connection = await this.findOrCreateConnection(
976
1049
  projectId,
977
1050
  userId,
978
1051
  apiKey,
979
1052
  headlessServerUrl
980
1053
  );
981
- const session = connection.createSession(this.getNextSessionId());
1054
+ const session = connection.createSession(
1055
+ this.getNextSessionId(),
1056
+ agentThreadContext
1057
+ );
982
1058
  this.startIdleCheck();
983
1059
  return session;
984
1060
  }
@@ -1008,6 +1084,7 @@ var SessionManager = class {
1008
1084
  projectId: session.connection.projectId,
1009
1085
  userId: session.connection.userId,
1010
1086
  sessionId: session.connection.getServerSessionId(),
1087
+ ...session.agentThreadContext,
1011
1088
  durationMs,
1012
1089
  hasError: result.error !== void 0,
1013
1090
  ...result.error ? { errorMessage: result.error } : {}
@@ -1048,7 +1125,8 @@ var SessionManager = class {
1048
1125
  trackSessionDestroy({
1049
1126
  projectId: session.connection.projectId,
1050
1127
  userId: session.connection.userId,
1051
- sessionId: session.connection.getServerSessionId()
1128
+ sessionId: session.connection.getServerSessionId(),
1129
+ ...session.agentThreadContext
1052
1130
  });
1053
1131
  session.connection.removeSession(id);
1054
1132
  if (!session.connection.hasSessions()) {
@@ -1202,16 +1280,20 @@ var appRouter = t.router({
1202
1280
  projectId: z.string(),
1203
1281
  apiKey: z.string(),
1204
1282
  userId: z.string().optional(),
1205
- headlessServerUrl: z.string()
1283
+ headlessServerUrl: z.string(),
1284
+ agentThreadId: z.string().optional(),
1285
+ agentProvider: z.string().optional()
1206
1286
  })
1207
1287
  ).mutation(async ({ input }) => {
1208
1288
  const start = performance.now();
1289
+ const agentThreadContext = normalizeAgentThreadContext(input);
1209
1290
  try {
1210
1291
  const session = await sessionManager.create(
1211
1292
  input.projectId,
1212
1293
  input.userId,
1213
1294
  input.apiKey,
1214
- input.headlessServerUrl
1295
+ input.headlessServerUrl,
1296
+ agentThreadContext
1215
1297
  );
1216
1298
  log(
1217
1299
  `session.new id=${session.id} project=${input.projectId} headlessServerUrl=${input.headlessServerUrl}`
@@ -1220,6 +1302,7 @@ var appRouter = t.router({
1220
1302
  projectId: input.projectId,
1221
1303
  userId: session.connection.userId,
1222
1304
  sessionId: session.connection.getServerSessionId(),
1305
+ ...session.agentThreadContext,
1223
1306
  durationMs: Math.round(performance.now() - start)
1224
1307
  });
1225
1308
  return { id: session.id };
@@ -1229,6 +1312,7 @@ var appRouter = t.router({
1229
1312
  errorType: "session_create_error",
1230
1313
  projectId: input.projectId,
1231
1314
  userId: input.userId,
1315
+ ...agentThreadContext,
1232
1316
  errorMessage: message
1233
1317
  });
1234
1318
  throw new TRPCError({
@@ -36,15 +36,20 @@ Always save results you'll need again. Don't repeat API calls.
36
36
 
37
37
  ## Method Selection
38
38
 
39
- Use agent-specific methods whenever a plugin API method and an agent method overlap:
39
+ Choose the highest-priority interface available for the task:
40
40
 
41
- - Use `readProjectForAgent`, `getNodeForAgent`, `getNodesForAgent`, `getNodesOfTypesForAgent`, `getScopeNodeForAgent`, `getGroundNodeForAgent`, `getParentNodeForAgent`, `getAncestorsForAgent`, `serializeForAgent`, `serializeNodesForAgent`, and `paginateForAgent` for project tree reads.
42
- - Use `applyAgentChanges` for page and layout edits. Do not use low-level node APIs like `createNode`, `setAttributes`, or `setRect` for design/layout work.
43
- - Use `publishForAgent` for publishing. Do not use `publish`, `getDeployments`, or `deploy` for normal agent publishing flows.
41
+ 1. Subcommands, such as `framer-dalton read-project` and `framer-dalton apply-changes`
42
+ 2. Agent-specific methods, such as `framer.agent.readProject`, `framer.agent.applyChanges`, and `framer.agent.publish`
43
+ 3. Generic plugin API methods, such as top-level `framer.*` methods
44
44
 
45
- Use the plugin API for capabilities that do not have an agent-specific counterpart, such as code file management, localization, and redirects.
45
+ - Use `framer.agent.readProject`, `framer.agent.getNode`, `framer.agent.getNodes`, `framer.agent.getNodesOfTypes`, `framer.agent.getScopeNode`, `framer.agent.getGroundNode`, `framer.agent.getParentNode`, `framer.agent.getAncestors`, `framer.agent.serialize`, `framer.agent.serializeNodes`, and `framer.agent.paginate` for project tree reads.
46
+ - Use `framer.agent.readComponentControls`, `framer.agent.readIconSetControls`, `framer.agent.readIcons`, `framer.agent.readLayoutTemplateControls`, and `framer.agent.readShaderControls` for reading the controls of components, icon sets, icons, layout templates, and shaders.
47
+ - Use `framer.agent.applyChanges` for page and layout edits. Do not use low-level node APIs like `createNode`, `setAttributes`, or `setRect` for design/layout work.
48
+ - Use `framer.agent.publish` for publishing. Do not use `publish`, `getDeployments`, or `deploy` for normal agent publishing flows.
46
49
 
47
- During normal task execution, do not call `framer.getAgentSystemPrompt()` or `framer.getAgentContext()`. This skill already includes their output.
50
+ 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
+
52
+ During normal task execution, do not call `framer.agent.getSystemPrompt()` or `framer.agent.getContext()`. This skill already includes their output.
48
53
 
49
54
  ## Core Usage Principles
50
55
 
@@ -99,15 +104,15 @@ Store anything you'll reference again.
99
104
 
100
105
  Use this approach only if the user asks for it. Do not mention this approach to the user otherwise.
101
106
 
102
- This approach is called “Prompt the Framer agent”. With this approach, use `framer.startAgentConversation()` to start a stateful design subagent. Keep the `conversationId` it returns in `state`, and call `framer.continueAgentConversation()` with it to continue the same design task.
107
+ This approach is called “Prompt the Framer agent”. With this approach, use `framer.agent.startConversation()` to start a stateful design subagent. Keep the `conversationId` it returns in `state`, and call `framer.agent.continueConversation()` with it to continue the same design task.
103
108
 
104
- Do not call `framer.getAgentSystemPrompt()` or `framer.getAgentContext()` or `framer.applyAgentChanges()` with this approach.
109
+ Do not call `framer.agent.getSystemPrompt()` or `framer.agent.getContext()` or `framer.agent.applyChanges()` with this approach.
105
110
 
106
111
  Example:
107
112
 
108
113
  ```js
109
114
  state.agent ??= {};
110
- const first = await framer.startAgentConversation(
115
+ const first = await framer.agent.startConversation(
111
116
  "Build me a landing page based on the attached screenshot",
112
117
  {
113
118
  pagePath: "/",
@@ -118,7 +123,7 @@ const first = await framer.startAgentConversation(
118
123
  state.agent.conversationId = first.conversationId;
119
124
  console.log(first.responseMessages);
120
125
 
121
- const second = await framer.continueAgentConversation("Now make it pink", {
126
+ const second = await framer.agent.continueConversation("Now make it pink", {
122
127
  conversationId: state.agent.conversationId,
123
128
  selectionNodeIds: ["someNodeId"],
124
129
  // imageUrls: [...],
@@ -170,7 +175,7 @@ npx framer-dalton docs ScreenshotOptions # Show type + recursively expa
170
175
 
171
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.
172
177
 
173
- **Exception — CMS Collection Lists.** The in-canvas construct that *renders* a collection as a repeating list of cards (the "Collection List" / repeater) is an agent/page concern. Build and edit those through `readProjectForAgent` and `applyAgentChanges`, not the plugin API. Use the plugin Collection API to read available collections and field schema first, then drive the list through the project-scoped skill.
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.
174
179
 
175
180
  #### Reading Collections
176
181
 
@@ -182,10 +187,11 @@ console.log(collections.map((c) => ({ name: c.name, id: c.id })));
182
187
  // Get a specific collection by ID
183
188
  const collection = await framer.getCollection("collection-id");
184
189
 
185
- // Get fields (columns) - returns array of { id, type, name }
190
+ // Get fields (columns)
186
191
  const fields = await collection.getFields();
187
- console.log(fields);
188
- // [{ id: "BnNuS2i3o", type: "string", name: "Title" }, ...]
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" }, ...]
189
195
 
190
196
  // Get items (rows)
191
197
  const items = await collection.getItems();
@@ -244,6 +250,17 @@ const ids = items.map((i) => i.id).reverse();
244
250
  await collection.setItemOrder(ids);
245
251
  ```
246
252
 
253
+ #### Writing enum fields
254
+
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:
256
+
257
+ ```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
+ ]);
262
+ ```
263
+
247
264
  #### Working with CollectionItem
248
265
 
249
266
  ```js
@@ -262,14 +279,14 @@ await item.remove();
262
279
 
263
280
  ### Working with Images
264
281
 
265
- Canvas editing accepts image URLs directly when setting an image fill, so the typical task is to obtain a URL and pass it through to the project-scoped skill.
282
+ Canvas editing accepts image URLs directly when setting an image fill, so the typical task is to obtain a URL and use it directly with `framer.agent.applyChanges`.
266
283
 
267
284
  There are three sources you'll typically pull URLs from:
268
285
 
269
- **Stock photography.** Use `framer.queryImagesForAgent` to source candidates and stash the URL you want:
286
+ **Stock photography.** Use `framer.agent.queryImages` to source candidates and stash the URL you want:
270
287
 
271
288
  ```js
272
- const { results } = await framer.queryImagesForAgent({
289
+ const { results } = await framer.agent.queryImages({
273
290
  source: "unsplash",
274
291
  query: "snow-capped mountains",
275
292
  count: 4,
@@ -290,7 +307,7 @@ state.heroUrl = (await framer.uploadImage({
290
307
  **An image already on the canvas.** Read the node and reuse its existing image URL:
291
308
 
292
309
  ```js
293
- const node = await framer.getNodeForAgent({ id: "<image-node-id>" });
310
+ const node = await framer.agent.getNode({ id: "<image-node-id>" });
294
311
  state.heroUrl = node.attributes.fill;
295
312
  ```
296
313
 
@@ -330,7 +347,7 @@ console.log(state.diagnostics);
330
347
  state.component = state.codeFile.exports.find(
331
348
  (exportItem) => exportItem.type === "component" && exportItem.isDefaultExport,
332
349
  );
333
- await framer.applyAgentChanges(
350
+ await framer.agent.applyChanges(
334
351
  `+ComponentInstanceNode badge parent="wrapper" component="${state.component.componentId}"; SET badge width=120 height=48;`,
335
352
  { pagePath: "/" },
336
353
  );
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: framer
3
3
  description: >
4
- Use when the user wants to design, edit, or publish a website or web page — creating layouts, editing sections, updating text or images, managing CMS collections and content, syncing external data, creating or modifying code components, managing color and text styles, handling localization, or publishing deployments. Trigger when the user mentions Framer, references their website or web pages, asks to edit designs, update site content, or work with any Framer project — even if they don't explicitly say 'Framer'.
4
+ Use when the user wants to design, edit, or publish a website or web page — creating layouts, editing sections, updating text or images, managing CMS collections and content, syncing external data, creating or modifying code components, managing color and text styles, handling localization, or publishing deployments. Trigger when the user mentions Framer, references their website or web pages, asks to edit designs, update site content, or work with any Framer project — even if they don't explicitly say 'Framer'.
5
5
  **Mandatory precondition**: run `npx framer-dalton@latest setup` and let it complete **BEFORE** loading this skill.
6
6
  allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
7
7
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-dalton",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "main": "./dist/cli.js",
@@ -16,6 +16,7 @@
16
16
  "typecheck": "tsc --noEmit",
17
17
  "clean": "rm -rf dist",
18
18
  "check": "biome check .",
19
+ "check:fix": "biome check --write .",
19
20
  "format": "biome format --write .",
20
21
  "generate-types": "tsx scripts/generate-types.ts"
21
22
  },
@@ -23,17 +24,17 @@
23
24
  "@trpc/client": "^11.17.0",
24
25
  "@trpc/server": "^11.17.0",
25
26
  "commander": "^12.1.0",
26
- "framer-api": "0.1.11",
27
- "zod": "^4.3.6"
27
+ "framer-api": "0.1.14",
28
+ "zod": "^4.4.3"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@biomejs/biome": "^2.4.13",
31
- "@framerjs/framer-events": "0.0.175",
32
- "@types/node": "24.10.9",
32
+ "@framerjs/framer-events": "0.0.183",
33
+ "@types/node": "24.12.4",
33
34
  "tsup": "^8.0.2",
34
- "tsx": "^4.19.0",
35
+ "tsx": "^4.22.3",
35
36
  "typescript": "^5.9.2",
36
- "vitest": "^4.0.18"
37
+ "vitest": "^4.1.7"
37
38
  },
38
39
  "packageManager": "yarn@4.13.0",
39
40
  "engines": {