framer-dalton 0.0.25 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ 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.25 */
15
+ /* @framer/ai CLI v0.0.26 */
16
16
  var __defProp = Object.defineProperty;
17
17
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
18
18
 
@@ -29,6 +29,51 @@ Please upgrade: https://nodejs.org/`
29
29
  );
30
30
  process.exit(1);
31
31
  }
32
+
33
+ // src/agent-thread-context.ts
34
+ var MAX_AGENT_THREAD_ID_LENGTH = 256;
35
+ var MAX_AGENT_PROVIDER_LENGTH = 64;
36
+ var SAFE_VALUE_PATTERN = /^[A-Za-z0-9._:-]+$/;
37
+ var providerEnvVars = [
38
+ { provider: "codex", envVar: "CODEX_THREAD_ID" },
39
+ { provider: "claude", envVar: "CLAUDE_CODE_SESSION_ID" },
40
+ { provider: "claude", envVar: "CLAUDE_CODE_REMOTE_SESSION_ID" }
41
+ ];
42
+ function normalizeValue(value, maxLength) {
43
+ const trimmed = value?.trim();
44
+ if (!trimmed) return void 0;
45
+ if (trimmed.length > maxLength) return void 0;
46
+ if (!SAFE_VALUE_PATTERN.test(trimmed)) return void 0;
47
+ return trimmed;
48
+ }
49
+ __name(normalizeValue, "normalizeValue");
50
+ function normalizeAgentThreadContext(input) {
51
+ const agentThreadId = normalizeValue(
52
+ input.agentThreadId,
53
+ MAX_AGENT_THREAD_ID_LENGTH
54
+ );
55
+ if (!agentThreadId) return {};
56
+ const agentProvider = normalizeValue(
57
+ input.agentProvider,
58
+ MAX_AGENT_PROVIDER_LENGTH
59
+ );
60
+ return {
61
+ agentThreadId,
62
+ ...agentProvider ? { agentProvider } : {}
63
+ };
64
+ }
65
+ __name(normalizeAgentThreadContext, "normalizeAgentThreadContext");
66
+ function resolveAgentThreadContext(env = process.env) {
67
+ for (const { provider, envVar } of providerEnvVars) {
68
+ const agentThreadContext = normalizeAgentThreadContext({
69
+ agentThreadId: env[envVar],
70
+ agentProvider: provider
71
+ });
72
+ if (agentThreadContext.agentThreadId) return agentThreadContext;
73
+ }
74
+ return {};
75
+ }
76
+ __name(resolveAgentThreadContext, "resolveAgentThreadContext");
32
77
  function openUrl(url) {
33
78
  const platform = process.platform;
34
79
  let cmd;
@@ -330,7 +375,7 @@ __name(markTelemetryNoticeShown, "markTelemetryNoticeShown");
330
375
  // src/version.ts
331
376
  var VERSION = (
332
377
  // typeof is used to ensure this can be used just via tsx or node etc. without build
333
- "0.0.25"
378
+ "0.0.26"
334
379
  );
335
380
  var trackingEndpoint = "https://events.framer.com/track";
336
381
  var inProgressTrackings = /* @__PURE__ */ new Set();
@@ -420,6 +465,45 @@ function trackError(payload) {
420
465
  });
421
466
  }
422
467
  __name(trackError, "trackError");
468
+
469
+ // src/connection-errors.ts
470
+ var RELAY_CONNECTION_LOST_PATTERNS = [
471
+ "fetch failed",
472
+ // undici top-level: relay gone / socket closed mid-request
473
+ "other side closed",
474
+ // undici SocketError: relay exited mid-request
475
+ "UND_ERR_SOCKET",
476
+ // undici socket error code
477
+ "ECONNREFUSED",
478
+ // relay not listening
479
+ "ECONNRESET",
480
+ // connection reset mid-request
481
+ "socket hang up"
482
+ ];
483
+ function isRelayConnectionLost(err) {
484
+ const seen = /* @__PURE__ */ new Set();
485
+ const stack = [err];
486
+ while (stack.length > 0) {
487
+ const node = stack.pop();
488
+ if (node == null || seen.has(node)) continue;
489
+ seen.add(node);
490
+ const e = node;
491
+ const text = `${typeof e.message === "string" ? e.message : ""} ${typeof e.code === "string" ? e.code : ""}`;
492
+ if (RELAY_CONNECTION_LOST_PATTERNS.some((pattern) => text.includes(pattern)))
493
+ return true;
494
+ if (e.cause != null) stack.push(e.cause);
495
+ if (Array.isArray(e.errors)) stack.push(...e.errors);
496
+ }
497
+ return false;
498
+ }
499
+ __name(isRelayConnectionLost, "isRelayConnectionLost");
500
+ var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
501
+ function isAuthError(errorMessage) {
502
+ return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
503
+ }
504
+ __name(isAuthError, "isAuthError");
505
+
506
+ // src/utils.ts
423
507
  function rehydrateFramerAPIError(error) {
424
508
  if (!(error instanceof Error)) return null;
425
509
  const framerApi = error.data?.framerApi;
@@ -448,10 +532,12 @@ function formatError(error) {
448
532
  return String(error);
449
533
  }
450
534
  __name(formatError, "formatError");
451
- function formatErrorForUser(error) {
535
+ function formatErrorForUser(error, opts) {
452
536
  if (!(error instanceof Error)) return String(error);
453
537
  const rehydrated = rehydrateFramerAPIError(error);
454
538
  if (rehydrated) return rehydrated.toString();
539
+ if (!opts?.fromExecResult && isRelayConnectionLost(error))
540
+ return "Connection to the Framer server was lost. Run `framer session new` to reconnect.";
455
541
  const ref = getErrorRef(error);
456
542
  return ref ? `${error.message} [ref: ${ref}]` : error.message;
457
543
  }
@@ -819,13 +905,6 @@ async function acquireAuthWithRemixProject(sourceProjectUrlOrId) {
819
905
  }
820
906
  __name(acquireAuthWithRemixProject, "acquireAuthWithRemixProject");
821
907
 
822
- // src/connection-errors.ts
823
- var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
824
- function isAuthError(errorMessage) {
825
- return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
826
- }
827
- __name(isAuthError, "isAuthError");
828
-
829
908
  // src/closest-match.ts
830
909
  function levenshteinDistance(source, target) {
831
910
  const sourceLength = source.length;
@@ -7210,12 +7289,48 @@ var types = {
7210
7289
  description: "@alpha",
7211
7290
  optional: false
7212
7291
  },
7292
+ {
7293
+ name: "readComponentControlsForAgent",
7294
+ type: "(input: {\n componentIds: readonly string[];\n }, options?: {\n pagePath?: string;\n }) => Promise<unknown>",
7295
+ description: "@alpha",
7296
+ optional: false
7297
+ },
7298
+ {
7299
+ name: "readIconSetControlsForAgent",
7300
+ type: "(input: {\n iconSetNames: readonly string[];\n }, options?: {\n pagePath?: string;\n }) => Promise<unknown>",
7301
+ description: "@alpha",
7302
+ optional: false
7303
+ },
7304
+ {
7305
+ name: "readIconsForAgent",
7306
+ type: "(input: {\n iconSetName: string;\n }, options?: {\n pagePath?: string;\n }) => Promise<string[]>",
7307
+ description: "@alpha",
7308
+ optional: false
7309
+ },
7310
+ {
7311
+ name: "readLayoutTemplateControlsForAgent",
7312
+ type: "(input: {\n layoutTemplateIds: readonly string[];\n }, options?: {\n pagePath?: string;\n }) => Promise<unknown>",
7313
+ description: "@alpha",
7314
+ optional: false
7315
+ },
7316
+ {
7317
+ name: "readShaderControlsForAgent",
7318
+ type: "(input: {\n shaderNames: readonly string[];\n }, options?: {\n pagePath?: string;\n }) => Promise<unknown>",
7319
+ description: "@alpha",
7320
+ optional: false
7321
+ },
7213
7322
  {
7214
7323
  name: "applyAgentChanges",
7215
7324
  type: "(dsl: string, options?: {\n pagePath?: string;\n }) => Promise<void>",
7216
7325
  description: "@alpha",
7217
7326
  optional: false
7218
7327
  },
7328
+ {
7329
+ name: "ping",
7330
+ type: "() => Promise<void>",
7331
+ description: "@alpha Liveness round-trip; resolves when Vekter's plugin handler answers.",
7332
+ optional: false
7333
+ },
7219
7334
  {
7220
7335
  name: "publishForAgent",
7221
7336
  type: "(input?: Record<string, unknown>) => Promise<unknown>",
@@ -7825,6 +7940,18 @@ var types = {
7825
7940
  type: "string",
7826
7941
  description: "JSONL string with one training row per inner-agent step, present when `captureTrainingData` was set.",
7827
7942
  optional: true
7943
+ },
7944
+ {
7945
+ name: "fixtureFilename",
7946
+ type: "string",
7947
+ description: "Document fixture filename, always present after a completed run.",
7948
+ optional: false
7949
+ },
7950
+ {
7951
+ name: "fixtureContent",
7952
+ type: "string",
7953
+ description: "Document fixture JSON content (tree + code files).",
7954
+ optional: false
7828
7955
  }
7829
7956
  ]
7830
7957
  },
@@ -13224,6 +13351,13 @@ var methodsByCategory = {
13224
13351
  description: 'Paginate a large array of values across multiple calls. The cursor is\nopaque and only valid within the same headless session and page.\n\n- First page: pass `{ items }`.\n- Continuation: pass `{ keyName, cursor }` using values returned by a previous call.\n\n@param input - `{ items }` for a fresh array, or `{ keyName, cursor }` for continuation.\n@param options.pagePath - Target page path (e.g. `"/about"`). Defaults to the active page.\n@returns The current page, including `keyName`, `cursor`, `results`, `totalResults`, and (if more pages remain) `nextCursor`.',
13225
13352
  references: []
13226
13353
  },
13354
+ {
13355
+ name: "[$framerApiOnly.ping]",
13356
+ category: "framer",
13357
+ signature: "[$framerApiOnly.ping](): Promise<void>",
13358
+ description: "Liveness round-trip: resolves once Vekter's plugin handler answers. Used by the headless\n backend to verify the transport actually routes, not just that the page is alive.",
13359
+ references: []
13360
+ },
13227
13361
  {
13228
13362
  name: "[$framerApiOnly.publishForAgent]",
13229
13363
  category: "framer",
@@ -13238,6 +13372,34 @@ var methodsByCategory = {
13238
13372
  description: 'Searches for stock images to use on the canvas (e.g. `"FrameNode"` `fill` values). Returns\ncandidate images with preview thumbnails and a `url` field for each. The returned URLs are\nregistered as trusted for this session so they can be applied directly via `fill="<url>"`\nin a subsequent {@link applyAgentChanges} DSL command. Without calling this method first,\nexternal image URLs are rejected at apply time.\n\nThe input schema is documented in the string returned by {@link getAgentSystemPrompt}.\n\n@param input - Search input object (source / query / count / orientation).\n@returns An object describing the candidates or an error.',
13239
13373
  references: []
13240
13374
  },
13375
+ {
13376
+ name: "[$framerApiOnly.readComponentControlsForAgent]",
13377
+ category: "framer",
13378
+ signature: "[$framerApiOnly.readComponentControlsForAgent](input: { componentIds: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
13379
+ description: "@alpha",
13380
+ references: []
13381
+ },
13382
+ {
13383
+ name: "[$framerApiOnly.readIconSetControlsForAgent]",
13384
+ category: "framer",
13385
+ signature: "[$framerApiOnly.readIconSetControlsForAgent](input: { iconSetNames: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
13386
+ description: "@alpha",
13387
+ references: []
13388
+ },
13389
+ {
13390
+ name: "[$framerApiOnly.readIconsForAgent]",
13391
+ category: "framer",
13392
+ signature: "[$framerApiOnly.readIconsForAgent](input: { iconSetName: string; }, options?: { pagePath?: string; }): Promise<string[]>",
13393
+ description: "@alpha",
13394
+ references: []
13395
+ },
13396
+ {
13397
+ name: "[$framerApiOnly.readLayoutTemplateControlsForAgent]",
13398
+ category: "framer",
13399
+ signature: "[$framerApiOnly.readLayoutTemplateControlsForAgent](input: { layoutTemplateIds: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
13400
+ description: "@alpha",
13401
+ references: []
13402
+ },
13241
13403
  {
13242
13404
  name: "[$framerApiOnly.readProjectForAgent]",
13243
13405
  category: "framer",
@@ -13245,6 +13407,13 @@ var methodsByCategory = {
13245
13407
  description: 'Reads project state by executing an array of queries against the project.\n\nReturns one result per query. Available query types and their parameters\nare documented in the string returned by {@link getAgentSystemPrompt}.\n\n@param queries - Array of query objects. See {@link getAgentSystemPrompt} for available types.\n@param options.pagePath - Target page path (e.g. `"/about"`). Defaults to the active page.\n@returns An object with a `results` array, one entry per query.',
13246
13408
  references: []
13247
13409
  },
13410
+ {
13411
+ name: "[$framerApiOnly.readShaderControlsForAgent]",
13412
+ category: "framer",
13413
+ signature: "[$framerApiOnly.readShaderControlsForAgent](input: { shaderNames: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
13414
+ description: "@alpha",
13415
+ references: []
13416
+ },
13248
13417
  {
13249
13418
  name: "[$framerApiOnly.reviewChangesForAgent]",
13250
13419
  category: "framer",
@@ -13804,6 +13973,13 @@ var methodsByCategory = {
13804
13973
  description: 'Paginate a large array of values across multiple calls. The cursor is\nopaque and only valid within the same headless session and page.\n\n- First page: pass `{ items }`.\n- Continuation: pass `{ keyName, cursor }` using values returned by a previous call.\n\n@param input - `{ items }` for a fresh array, or `{ keyName, cursor }` for continuation.\n@param options.pagePath - Target page path (e.g. `"/about"`). Defaults to the active page.\n@returns The current page, including `keyName`, `cursor`, `results`, `totalResults`, and (if more pages remain) `nextCursor`.',
13805
13974
  references: []
13806
13975
  },
13976
+ {
13977
+ name: "ping",
13978
+ category: "framer",
13979
+ signature: "ping(): Promise<void>",
13980
+ description: "Liveness round-trip: resolves once Vekter's plugin handler answers. Used by the headless\n backend to verify the transport actually routes, not just that the page is alive.",
13981
+ references: []
13982
+ },
13807
13983
  {
13808
13984
  name: "publish",
13809
13985
  category: "framer",
@@ -13825,6 +14001,34 @@ var methodsByCategory = {
13825
14001
  description: 'Searches for stock images to use on the canvas (e.g. `"FrameNode"` `fill` values). Returns\ncandidate images with preview thumbnails and a `url` field for each. The returned URLs are\nregistered as trusted for this session so they can be applied directly via `fill="<url>"`\nin a subsequent {@link applyAgentChanges} DSL command. Without calling this method first,\nexternal image URLs are rejected at apply time.\n\nThe input schema is documented in the string returned by {@link getAgentSystemPrompt}.\n\n@param input - Search input object (source / query / count / orientation).\n@returns An object describing the candidates or an error.',
13826
14002
  references: []
13827
14003
  },
14004
+ {
14005
+ name: "readComponentControlsForAgent",
14006
+ category: "framer",
14007
+ signature: "readComponentControlsForAgent(input: { componentIds: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
14008
+ description: "@alpha",
14009
+ references: []
14010
+ },
14011
+ {
14012
+ name: "readIconSetControlsForAgent",
14013
+ category: "framer",
14014
+ signature: "readIconSetControlsForAgent(input: { iconSetNames: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
14015
+ description: "@alpha",
14016
+ references: []
14017
+ },
14018
+ {
14019
+ name: "readIconsForAgent",
14020
+ category: "framer",
14021
+ signature: "readIconsForAgent(input: { iconSetName: string; }, options?: { pagePath?: string; }): Promise<string[]>",
14022
+ description: "@alpha",
14023
+ references: []
14024
+ },
14025
+ {
14026
+ name: "readLayoutTemplateControlsForAgent",
14027
+ category: "framer",
14028
+ signature: "readLayoutTemplateControlsForAgent(input: { layoutTemplateIds: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
14029
+ description: "@alpha",
14030
+ references: []
14031
+ },
13828
14032
  {
13829
14033
  name: "readProjectForAgent",
13830
14034
  category: "framer",
@@ -13832,6 +14036,13 @@ var methodsByCategory = {
13832
14036
  description: 'Reads project state by executing an array of queries against the project.\n\nReturns one result per query. Available query types and their parameters\nare documented in the string returned by {@link getAgentSystemPrompt}.\n\n@param queries - Array of query objects. See {@link getAgentSystemPrompt} for available types.\n@param options.pagePath - Target page path (e.g. `"/about"`). Defaults to the active page.\n@returns An object with a `results` array, one entry per query.',
13833
14037
  references: []
13834
14038
  },
14039
+ {
14040
+ name: "readShaderControlsForAgent",
14041
+ category: "framer",
14042
+ signature: "readShaderControlsForAgent(input: { shaderNames: readonly string[]; }, options?: { pagePath?: string; }): Promise<unknown>",
14043
+ description: "@alpha",
14044
+ references: []
14045
+ },
13835
14046
  {
13836
14047
  name: "rejectAllPending",
13837
14048
  category: "framer",
@@ -14024,6 +14235,22 @@ var methodsByCategory = {
14024
14235
  references: ["NamedImageAssetInput", "ImageAsset"]
14025
14236
  }
14026
14237
  ],
14238
+ framerapierror: [
14239
+ {
14240
+ name: "recentMethods",
14241
+ category: "FramerAPIError",
14242
+ signature: "recentMethods: string[]",
14243
+ description: "@see FramerAPIErrorOptions.recentMethods",
14244
+ references: []
14245
+ },
14246
+ {
14247
+ name: "retryable",
14248
+ category: "FramerAPIError",
14249
+ signature: "retryable: boolean",
14250
+ description: "@see FramerAPIErrorOptions.retryable",
14251
+ references: []
14252
+ }
14253
+ ],
14027
14254
  imageasset: [
14028
14255
  {
14029
14256
  name: "altText",
@@ -16729,7 +16956,7 @@ async function execAndPrint(sessionId, code) {
16729
16956
  }
16730
16957
  if (result.error) {
16731
16958
  printError(
16732
- `Error: ${formatErrorForUser(errorWithRef(result.error, result.errorRef))}`
16959
+ `Error: ${formatErrorForUser(errorWithRef(result.error, result.errorRef), { fromExecResult: true })}`
16733
16960
  );
16734
16961
  await waitForTrackingToFinish();
16735
16962
  process.exit(1);
@@ -16807,6 +17034,7 @@ session.command("new <projectUrlOrId>").description("Create a new session and pr
16807
17034
  ).action(
16808
17035
  async (projectUrlOrId, options) => {
16809
17036
  const projectId = extractProjectId(projectUrlOrId);
17037
+ const agentThreadContext = resolveAgentThreadContext();
16810
17038
  let sessionId;
16811
17039
  let userId;
16812
17040
  try {
@@ -16831,7 +17059,8 @@ session.command("new <projectUrlOrId>").description("Create a new session and pr
16831
17059
  projectId,
16832
17060
  apiKey: credentials.apiKey,
16833
17061
  userId: credentials.userId,
16834
- headlessServerUrl
17062
+ headlessServerUrl,
17063
+ ...agentThreadContext
16835
17064
  });
16836
17065
  sessionId = result.id;
16837
17066
  debug("session.new", `session created id=${sessionId}`);
@@ -16885,6 +17114,7 @@ session.command("new <projectUrlOrId>").description("Create a new session and pr
16885
17114
  errorType: "session_new_error",
16886
17115
  projectId,
16887
17116
  userId,
17117
+ ...agentThreadContext,
16888
17118
  errorMessage: message
16889
17119
  });
16890
17120
  printError(`Failed to create session: ${formatErrorForUser(err)}`);
@@ -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.26 */
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.26"
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({
@@ -39,6 +39,7 @@ Always save results you'll need again. Don't repeat API calls.
39
39
  Use agent-specific methods whenever a plugin API method and an agent method overlap:
40
40
 
41
41
  - Use `readProjectForAgent`, `getNodeForAgent`, `getNodesForAgent`, `getNodesOfTypesForAgent`, `getScopeNodeForAgent`, `getGroundNodeForAgent`, `getParentNodeForAgent`, `getAncestorsForAgent`, `serializeForAgent`, `serializeNodesForAgent`, and `paginateForAgent` for project tree reads.
42
+ - Use `readComponentControlsForAgent`, `readIconSetControlsForAgent`, `readIconsForAgent`, `readLayoutTemplateControlsForAgent`, and `readShaderControlsForAgent` for reading the controls of components, icon sets, icons, layout templates, and shaders.
42
43
  - Use `applyAgentChanges` for page and layout edits. Do not use low-level node APIs like `createNode`, `setAttributes`, or `setRect` for design/layout work.
43
44
  - Use `publishForAgent` for publishing. Do not use `publish`, `getDeployments`, or `deploy` for normal agent publishing flows.
44
45
 
@@ -170,7 +171,7 @@ npx framer-dalton docs ScreenshotOptions # Show type + recursively expa
170
171
 
171
172
  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
173
 
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.
174
+ **Exception — CMS Collection Lists.** The in-canvas construct that *renders* a collection as a repeating list of cards (the "Collection List" / repeater) is an agent/page concern. Build and edit those through `readProjectForAgent` and `applyAgentChanges`, not the plugin API. Use the plugin Collection API to read available collections and field schema first, then use `applyAgentChanges` to add the list.
174
175
 
175
176
  #### Reading Collections
176
177
 
@@ -182,10 +183,11 @@ console.log(collections.map((c) => ({ name: c.name, id: c.id })));
182
183
  // Get a specific collection by ID
183
184
  const collection = await framer.getCollection("collection-id");
184
185
 
185
- // Get fields (columns) - returns array of { id, type, name }
186
+ // Get fields (columns)
186
187
  const fields = await collection.getFields();
187
- console.log(fields);
188
- // [{ id: "BnNuS2i3o", type: "string", name: "Title" }, ...]
188
+ // Note that a plain console.log will miss getter-backed properties
189
+ console.log(fields.map((f) => ({ id: f.id, name: f.name, type: f.type })));
190
+ // [{ id: "BnNuS2i3o", name: "Title", type: "string" }, ...]
189
191
 
190
192
  // Get items (rows)
191
193
  const items = await collection.getItems();
@@ -244,6 +246,17 @@ const ids = items.map((i) => i.id).reverse();
244
246
  await collection.setItemOrder(ids);
245
247
  ```
246
248
 
249
+ #### Writing enum fields
250
+
251
+ 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:
252
+
253
+ ```js
254
+ const caseId = field.cases.find((c) => c.name === "New").id;
255
+ await collection.addItems([
256
+ { slug: "hello-world", fieldData: { [field.id]: { type: "enum", value: caseId } } },
257
+ ]);
258
+ ```
259
+
247
260
  #### Working with CollectionItem
248
261
 
249
262
  ```js
@@ -262,7 +275,7 @@ await item.remove();
262
275
 
263
276
  ### Working with Images
264
277
 
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.
278
+ Canvas editing accepts image URLs directly when setting an image fill, so the typical task is to obtain a URL and use it directly with `applyAgentChanges`.
266
279
 
267
280
  There are three sources you'll typically pull URLs from:
268
281
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-dalton",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
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-beta",
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": {