@framer/agent 0.0.33 → 0.0.34

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
@@ -121,3 +121,17 @@ Usage will only consume your own agent's tokens.
121
121
  ### Why does this use so many tokens?
122
122
 
123
123
  Token consumption for Framer Agent can be relatively high. For other work like web development tasks, your agent already has a lot of knowledge baked into its model and so is able to get to work straight away. For Framer Agent, we need to first teach your AI how to interact with Framer projects and provide a lot of context. This requires a certain amount of token usage before being able to undertake the task at hand.
124
+
125
+ ### How do I get the latest releases if I have `min-release-age` set in my npm config?
126
+
127
+ If your npm config delays new packages with `min-release-age`, you can make generated Framer Agent skills override that delay. This updates your installed skills immediately:
128
+
129
+ ```bash
130
+ npx --min-release-age=0 @framer/agent@latest override-min-release-age enable
131
+ ```
132
+
133
+ To restore your default npm behavior in generated skills:
134
+
135
+ ```bash
136
+ npx @framer/agent@latest override-min-release-age disable
137
+ ```
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import fs7 from 'fs';
2
+ import fs8 from 'fs';
3
3
  import path7 from 'path';
4
4
  import { Command } from 'commander';
5
5
  import crypto, { randomUUID } from 'crypto';
@@ -7,12 +7,13 @@ import http from 'http';
7
7
  import { spawn, execFile, execFileSync } from 'child_process';
8
8
  import { z } from 'zod';
9
9
  import os2 from 'os';
10
+ import isWsl from 'is-wsl';
10
11
  import { ErrorCode, FramerAPIError } from 'framer-api';
11
12
  import net from 'net';
12
13
  import { fileURLToPath } from 'url';
13
14
  import { createTRPCClient, httpLink } from '@trpc/client';
14
15
 
15
- /* @framer/ai CLI v0.0.33 */
16
+ /* @framer/ai CLI v0.0.34 */
16
17
  var __defProp = Object.defineProperty;
17
18
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
18
19
 
@@ -34,11 +35,12 @@ Please upgrade: https://nodejs.org/`
34
35
  var MAX_AGENT_THREAD_ID_LENGTH = 256;
35
36
  var MAX_AGENT_PROVIDER_LENGTH = 64;
36
37
  var SAFE_VALUE_PATTERN = /^[A-Za-z0-9._:-]+$/;
37
- var providerEnvVars = [
38
+ var threadIdEnvVars = [
38
39
  { provider: "codex", envVar: "CODEX_THREAD_ID" },
39
40
  { provider: "claude", envVar: "CLAUDE_CODE_SESSION_ID" },
40
41
  { provider: "claude", envVar: "CLAUDE_CODE_REMOTE_SESSION_ID" }
41
42
  ];
43
+ var providerPresenceEnvVars = [{ provider: "cursor", envVar: "CURSOR_AGENT" }];
42
44
  function normalizeValue(value, maxLength) {
43
45
  const trimmed = value?.trim();
44
46
  if (!trimmed) return void 0;
@@ -48,29 +50,38 @@ function normalizeValue(value, maxLength) {
48
50
  }
49
51
  __name(normalizeValue, "normalizeValue");
50
52
  function normalizeAgentThreadContext(input) {
53
+ const trimmedAgentThreadId = input.agentThreadId?.trim();
51
54
  const agentThreadId = normalizeValue(
52
55
  input.agentThreadId,
53
56
  MAX_AGENT_THREAD_ID_LENGTH
54
57
  );
55
- if (!agentThreadId) return {};
58
+ if (trimmedAgentThreadId && !agentThreadId) return {};
56
59
  const agentProvider = normalizeValue(
57
60
  input.agentProvider,
58
61
  MAX_AGENT_PROVIDER_LENGTH
59
62
  );
63
+ if (!agentThreadId && !agentProvider) return {};
60
64
  return {
61
- agentThreadId,
65
+ ...agentThreadId ? { agentThreadId } : {},
62
66
  ...agentProvider ? { agentProvider } : {}
63
67
  };
64
68
  }
65
69
  __name(normalizeAgentThreadContext, "normalizeAgentThreadContext");
66
70
  function resolveAgentThreadContext(env = process.env) {
67
- for (const { provider, envVar } of providerEnvVars) {
71
+ for (const { provider, envVar } of threadIdEnvVars) {
68
72
  const agentThreadContext = normalizeAgentThreadContext({
69
73
  agentThreadId: env[envVar],
70
74
  agentProvider: provider
71
75
  });
72
76
  if (agentThreadContext.agentThreadId) return agentThreadContext;
73
77
  }
78
+ for (const { provider, envVar } of providerPresenceEnvVars) {
79
+ if (!env[envVar]?.trim()) continue;
80
+ const agentThreadContext = normalizeAgentThreadContext({
81
+ agentProvider: provider
82
+ });
83
+ if (agentThreadContext.agentProvider) return agentThreadContext;
84
+ }
74
85
  return {};
75
86
  }
76
87
  __name(resolveAgentThreadContext, "resolveAgentThreadContext");
@@ -107,7 +118,7 @@ function getConfigDir() {
107
118
  __name(getConfigDir, "getConfigDir");
108
119
  function ensureConfigDir() {
109
120
  const configDir = getConfigDir();
110
- fs7.mkdirSync(configDir, { recursive: true, mode: 448 });
121
+ fs8.mkdirSync(configDir, { recursive: true, mode: 448 });
111
122
  }
112
123
  __name(ensureConfigDir, "ensureConfigDir");
113
124
 
@@ -135,7 +146,7 @@ var ProjectsConfigSchema = z.object({
135
146
  var LegacyCredentialsSchema = z.record(z.string(), z.string());
136
147
  function readJsonFile(filePath) {
137
148
  try {
138
- return JSON.parse(fs7.readFileSync(filePath, "utf-8"));
149
+ return JSON.parse(fs8.readFileSync(filePath, "utf-8"));
139
150
  } catch (_error) {
140
151
  return null;
141
152
  }
@@ -143,7 +154,7 @@ function readJsonFile(filePath) {
143
154
  __name(readJsonFile, "readJsonFile");
144
155
  function writeProjectsConfig(config) {
145
156
  ensureConfigDir();
146
- fs7.writeFileSync(
157
+ fs8.writeFileSync(
147
158
  getProjectsConfigPath(),
148
159
  JSON.stringify(config, null, " "),
149
160
  {
@@ -170,7 +181,7 @@ function migrateLegacyCredentials() {
170
181
  }
171
182
  const config = { version: 2, projects };
172
183
  writeProjectsConfig(config);
173
- fs7.rmSync(getLegacyCredentialsPath(), { force: true });
184
+ fs8.rmSync(getLegacyCredentialsPath(), { force: true });
174
185
  return config;
175
186
  }
176
187
  __name(migrateLegacyCredentials, "migrateLegacyCredentials");
@@ -183,7 +194,7 @@ function readProjectsConfig() {
183
194
  return result.data;
184
195
  }
185
196
  }
186
- if (fs7.existsSync(getLegacyCredentialsPath())) {
197
+ if (fs8.existsSync(getLegacyCredentialsPath())) {
187
198
  return migrateLegacyCredentials();
188
199
  }
189
200
  return { version: 2, projects: {} };
@@ -266,7 +277,7 @@ var SettingsWatcher = class {
266
277
  __name(this, "SettingsWatcher");
267
278
  }
268
279
  start(callback) {
269
- fs7.watchFile(
280
+ fs8.watchFile(
270
281
  this.settingsPath,
271
282
  { persistent: false, interval: fsPollIntervalMs },
272
283
  (curr, prev) => {
@@ -283,7 +294,7 @@ var SettingsWatcher = class {
283
294
  return this;
284
295
  }
285
296
  stop() {
286
- fs7.unwatchFile(this.settingsPath);
297
+ fs8.unwatchFile(this.settingsPath);
287
298
  return this;
288
299
  }
289
300
  };
@@ -292,11 +303,13 @@ var SettingsWatcher = class {
292
303
  var DEFAULT_MACHINE_ID = "unknown-machine-id-this-should-never-be-persisted";
293
304
  var DEFAULT_SETTINGS = {
294
305
  machineId: DEFAULT_MACHINE_ID,
306
+ npxMinReleaseAgeOverrideEnabled: false,
295
307
  telemetryEnabled: true,
296
308
  telemetryNoticeShown: false
297
309
  };
298
310
  var SettingsFileSchema = z.object({
299
311
  machineId: z.string().optional(),
312
+ npxMinReleaseAgeOverrideEnabled: z.boolean().optional(),
300
313
  telemetryEnabled: z.boolean().optional(),
301
314
  telemetryNoticeShown: z.boolean().optional()
302
315
  });
@@ -320,10 +333,11 @@ __name(getSettings, "getSettings");
320
333
  function readSettingsFile() {
321
334
  const settingsPath = getSettingsPath();
322
335
  try {
323
- const raw = JSON.parse(fs7.readFileSync(settingsPath, "utf-8"));
336
+ const raw = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
324
337
  const result = SettingsFileSchema.parse(raw);
325
338
  return {
326
339
  machineId: result.machineId ?? DEFAULT_SETTINGS.machineId,
340
+ npxMinReleaseAgeOverrideEnabled: result.npxMinReleaseAgeOverrideEnabled ?? DEFAULT_SETTINGS.npxMinReleaseAgeOverrideEnabled,
327
341
  telemetryEnabled: result.telemetryEnabled ?? DEFAULT_SETTINGS.telemetryEnabled,
328
342
  telemetryNoticeShown: result.telemetryNoticeShown ?? DEFAULT_SETTINGS.telemetryNoticeShown
329
343
  };
@@ -339,7 +353,7 @@ function setSettings(newSettings) {
339
353
  __name(setSettings, "setSettings");
340
354
  function writeSettingsFile(settings2) {
341
355
  ensureConfigDir();
342
- fs7.writeFileSync(getSettingsPath(), JSON.stringify(settings2, null, " "), {
356
+ fs8.writeFileSync(getSettingsPath(), JSON.stringify(settings2, null, " "), {
343
357
  mode: 384
344
358
  });
345
359
  }
@@ -353,6 +367,15 @@ function getMachineId() {
353
367
  return getSettings().machineId;
354
368
  }
355
369
  __name(getMachineId, "getMachineId");
370
+ function isNpxMinReleaseAgeOverrideEnabled() {
371
+ return getSettings().npxMinReleaseAgeOverrideEnabled;
372
+ }
373
+ __name(isNpxMinReleaseAgeOverrideEnabled, "isNpxMinReleaseAgeOverrideEnabled");
374
+ function setNpxMinReleaseAgeOverrideEnabled(enabled2) {
375
+ const settings2 = getSettings();
376
+ setSettings({ ...settings2, npxMinReleaseAgeOverrideEnabled: enabled2 });
377
+ }
378
+ __name(setNpxMinReleaseAgeOverrideEnabled, "setNpxMinReleaseAgeOverrideEnabled");
356
379
  function isTelemetryEnabled() {
357
380
  return getSettings().telemetryEnabled;
358
381
  }
@@ -375,7 +398,7 @@ __name(markTelemetryNoticeShown, "markTelemetryNoticeShown");
375
398
  // src/version.ts
376
399
  var VERSION = (
377
400
  // typeof is used to ensure this can be used just via tsx or node etc. without build
378
- "0.0.33"
401
+ "0.0.34"
379
402
  );
380
403
  var trackingEndpoint = "https://events.framer.com/track";
381
404
  var inProgressTrackings = /* @__PURE__ */ new Set();
@@ -405,8 +428,27 @@ function getMacOsVersion() {
405
428
  }
406
429
  }
407
430
  __name(getMacOsVersion, "getMacOsVersion");
431
+ function parseOsReleaseValue(content, key) {
432
+ const line = content.split("\n").find((candidate) => candidate.startsWith(`${key}=`));
433
+ const value = line?.slice(key.length + 1).trim();
434
+ if (!value) return void 0;
435
+ return value.replace(/^"|"$/g, "") || void 0;
436
+ }
437
+ __name(parseOsReleaseValue, "parseOsReleaseValue");
438
+ function getLinuxOsVersion() {
439
+ let version;
440
+ try {
441
+ const osRelease = fs8.readFileSync("/etc/os-release", "utf8");
442
+ version = parseOsReleaseValue(osRelease, "PRETTY_NAME") ?? "unknown";
443
+ } catch {
444
+ version = os2.release().trim() || "unknown";
445
+ }
446
+ return isWsl ? `${version} wsl` : version;
447
+ }
448
+ __name(getLinuxOsVersion, "getLinuxOsVersion");
408
449
  function getOsVersion(platform) {
409
450
  if (platform === "darwin") return getMacOsVersion();
451
+ if (platform === "linux") return getLinuxOsVersion();
410
452
  const version = os2.release().trim();
411
453
  return version || "unknown";
412
454
  }
@@ -654,6 +696,7 @@ var themes = {
654
696
  };
655
697
  function htmlPage(opts) {
656
698
  const t = themes[opts.theme];
699
+ const footer = opts.action ? `<div class="footer"><a href="${opts.action.href}">${opts.action.label}</a></div>` : "";
657
700
  return `<!DOCTYPE html>
658
701
  <html lang="en">
659
702
  <head>
@@ -670,26 +713,28 @@ body{font-family:"Inter",system-ui,-apple-system,sans-serif;font-feature-setting
670
713
  .header{display:flex;align-items:center;height:50px;border-bottom:1px solid ${t.separatorColor};color:${t.titleColor};font-size:12px;font-weight:600;line-height:1}
671
714
  .content{padding:10px 0}
672
715
  .text{color:${t.textColor};font-size:12px;font-weight:500;line-height:1.5;text-wrap:balance}
673
- .footer{padding:10px 0;border-top:1px solid ${t.separatorColor}}
674
- .footer button{display:block;width:100%;height:30px;border:none;border-radius:8px;background:${t.buttonBackground};color:${t.buttonText};font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
675
- .footer button:hover{background:${t.buttonBackgroundHover}}
716
+ .footer{padding:0 0 10px}
717
+ .footer a{display:block;width:100%;height:30px;border:none;border-radius:8px;background:${t.buttonBackground};color:${t.buttonText};font-size:12px;font-weight:600;line-height:30px;text-align:center;cursor:pointer;font-family:inherit;text-decoration:none}
718
+ .footer a:hover{background:${t.buttonBackgroundHover}}
676
719
  </style>
677
720
  </head>
678
721
  <body>
679
722
  <div class="modal">
680
723
  <div class="header">${opts.heading}</div>
681
724
  <div class="content"><span class="text">${opts.message}</span></div>
725
+ ${footer}
682
726
  </div>
683
727
  </body>
684
728
  </html>`;
685
729
  }
686
730
  __name(htmlPage, "htmlPage");
687
- function successHtml(theme, message) {
731
+ function successHtml(theme, message, action) {
688
732
  return htmlPage({
689
733
  title: "Framer \u2014 Agent Approved",
690
734
  heading: "Agent Approved",
691
735
  message: message ?? "Your external agent now has edit access to the project. You can close this tab and continue with the external agent.",
692
- theme
736
+ theme,
737
+ action
693
738
  });
694
739
  }
695
740
  __name(successHtml, "successHtml");
@@ -729,12 +774,18 @@ __name(isFramerHostname, "isFramerHostname");
729
774
  function resolveProjectOrigin(projectUrlOrId) {
730
775
  try {
731
776
  const url = new URL(projectUrlOrId);
732
- if (isFramerHostname(url.hostname.toLowerCase())) return url.origin;
777
+ if (url.protocol === "https:" && isFramerHostname(url.hostname.toLowerCase())) {
778
+ return url.origin;
779
+ }
733
780
  } catch {
734
781
  }
735
782
  return "https://framer.com";
736
783
  }
737
784
  __name(resolveProjectOrigin, "resolveProjectOrigin");
785
+ function projectUrl(projectOrigin, projectId) {
786
+ return new URL(`/projects/${encodeURIComponent(projectId)}`, projectOrigin).href;
787
+ }
788
+ __name(projectUrl, "projectUrl");
738
789
  function runBrowserAuthFlow(options) {
739
790
  const {
740
791
  deeplinkParams,
@@ -762,8 +813,13 @@ function runBrowserAuthFlow(options) {
762
813
  debug("auth", `request: ${url.pathname}`);
763
814
  if (url.pathname === "/done") {
764
815
  const theme2 = parseTheme(url.searchParams.get("theme"));
816
+ const projectId = pendingAuth?.projectId ?? pollProjectId;
817
+ const action = projectId ? {
818
+ label: "Open Project",
819
+ href: projectUrl(projectOrigin, projectId)
820
+ } : void 0;
765
821
  res.writeHead(200, { "Content-Type": "text/html" });
766
- res.end(successHtml(theme2, browserSuccessMessage), () => {
822
+ res.end(successHtml(theme2, browserSuccessMessage, action), () => {
767
823
  if (pendingAuth) {
768
824
  const auth = pendingAuth;
769
825
  pendingAuth = null;
@@ -1112,11 +1168,14 @@ var types = {
1112
1168
  name: "AgentBranchChange",
1113
1169
  description: "",
1114
1170
  kind: "interface",
1115
- references: ["AgentBranchCollectionItemChange"],
1171
+ references: [
1172
+ "AgentBranchCollectionItemChange",
1173
+ "AgentBranchChangeSettingsTab"
1174
+ ],
1116
1175
  members: [
1117
1176
  {
1118
1177
  name: "type",
1119
- type: '"page" | "collection" | "designPage" | "siteSettings" | "component" | "layoutTemplate"',
1178
+ type: '"page" | "collection" | "designPage" | "siteSettings" | "component" | "layoutTemplate" | "vectorSet" | "codeFile"',
1120
1179
  description: "",
1121
1180
  optional: false
1122
1181
  },
@@ -1146,12 +1205,19 @@ var types = {
1146
1205
  },
1147
1206
  {
1148
1207
  name: "settingsTab",
1149
- type: '"general" | "redirects"',
1208
+ type: "AgentBranchChangeSettingsTab",
1150
1209
  description: "",
1151
1210
  optional: true
1152
1211
  }
1153
1212
  ]
1154
1213
  },
1214
+ agentbranchchangesettingstab: {
1215
+ name: "AgentBranchChangeSettingsTab",
1216
+ description: "",
1217
+ kind: "alias",
1218
+ alias: '"general" | "redirects" | "customCode"',
1219
+ references: []
1220
+ },
1155
1221
  agentbranchcollectionitemchange: {
1156
1222
  name: "AgentBranchCollectionItemChange",
1157
1223
  description: "",
@@ -7558,7 +7624,7 @@ var types = {
7558
7624
  },
7559
7625
  {
7560
7626
  name: "applyAgentChanges",
7561
- type: "(dsl: string, options?: {\n pagePath?: string;\n }) => Promise<void>",
7627
+ type: "(dsl: string, options?: {\n pagePath?: string;\n }) => Promise<unknown>",
7562
7628
  description: "@alpha",
7563
7629
  optional: false
7564
7630
  },
@@ -14123,8 +14189,8 @@ var methodsByCategory = {
14123
14189
  {
14124
14190
  name: "applyChanges",
14125
14191
  category: "FramerAgentAPI",
14126
- signature: "applyChanges(dsl: string, options?: { pagePath?: string; }): Promise<void>",
14127
- description: 'Applies commands to the canvas to create, update, remove, move, or duplicate nodes.\n\nThe command syntax is documented in the string returned by {@link getSystemPrompt}.\nEach call is scoped to a single page.\n\n@param dsl - A string of commands separated by `;`. See {@link getSystemPrompt} for syntax.\n@param options.pagePath - Target page path (e.g. `"/about"`). Defaults to the active page.',
14192
+ signature: "applyChanges(dsl: string, options?: { pagePath?: string; }): Promise<unknown>",
14193
+ description: 'Applies commands to the canvas to create, update, remove, move, or duplicate nodes.\n\nThe command syntax is documented in the string returned by {@link getSystemPrompt}.\nEach call is scoped to a single page.\n\nCommands that fail (including syntax errors) are reported in the result\'s `errors`\nwithout blocking the remaining commands. Errors returned here are not repeated by\n{@link reviewChanges}, which reports the remaining project diagnostics.\n\nThe result schema is documented in the string returned by {@link getSystemPrompt}.\n\n@param dsl - A string of commands separated by `;`. See {@link getSystemPrompt} for syntax.\n@param options.pagePath - Target page path (e.g. `"/about"`). Defaults to the active page.\n@returns The per-command status and errors, and the canonical system ids of created nodes.',
14128
14194
  references: []
14129
14195
  },
14130
14196
  {
@@ -14334,7 +14400,7 @@ var methodsByCategory = {
14334
14400
  name: "reviewChanges",
14335
14401
  category: "FramerAgentAPI",
14336
14402
  signature: "reviewChanges(): Promise<unknown>",
14337
- description: "Reviews changes made by prior {@link applyChanges} calls in this session.\n\nConsumes accumulated diagnostics from the session's agent context.\n\n@returns The session's accumulated changes, errors, warnings, and deferred trait reports.",
14403
+ description: "Reviews changes made by prior {@link applyChanges} calls in this session.\n\nConsumes accumulated diagnostics from the session's agent context. Command errors\nalready returned by {@link applyChanges} are not repeated here.\n\n@returns The session's accumulated changes, errors, warnings, and deferred trait reports.",
14338
14404
  references: []
14339
14405
  },
14340
14406
  {
@@ -16576,8 +16642,8 @@ function createProjectBaseUrl(origin) {
16576
16642
  __name(createProjectBaseUrl, "createProjectBaseUrl");
16577
16643
  function inferHeadlessServerUrl(projectUrlOrId) {
16578
16644
  try {
16579
- const projectUrl = new URL(projectUrlOrId);
16580
- const hostname = projectUrl.hostname.toLowerCase();
16645
+ const projectUrl2 = new URL(projectUrlOrId);
16646
+ const hostname = projectUrl2.hostname.toLowerCase();
16581
16647
  if (hostname === "development.framer.com") {
16582
16648
  return DEV_HEADLESS_SERVER_URL;
16583
16649
  }
@@ -16585,7 +16651,7 @@ function inferHeadlessServerUrl(projectUrlOrId) {
16585
16651
  const headlessServerUrl = new URL(PROD_HEADLESS_SERVER_URL);
16586
16652
  headlessServerUrl.searchParams.set(
16587
16653
  "baseUrl",
16588
- createProjectBaseUrl(projectUrl.origin)
16654
+ createProjectBaseUrl(projectUrl2.origin)
16589
16655
  );
16590
16656
  return headlessServerUrl.toString();
16591
16657
  }
@@ -16593,7 +16659,7 @@ function inferHeadlessServerUrl(projectUrlOrId) {
16593
16659
  const headlessServerUrl = new URL(DEV_HEADLESS_SERVER_URL);
16594
16660
  headlessServerUrl.searchParams.set(
16595
16661
  "baseUrl",
16596
- createProjectBaseUrl(projectUrl.origin)
16662
+ createProjectBaseUrl(projectUrl2.origin)
16597
16663
  );
16598
16664
  return headlessServerUrl.toString();
16599
16665
  }
@@ -16609,7 +16675,7 @@ function getRelayTokenPath() {
16609
16675
  __name(getRelayTokenPath, "getRelayTokenPath");
16610
16676
  function readRelayToken() {
16611
16677
  try {
16612
- const token = fs7.readFileSync(getRelayTokenPath(), "utf-8").trim();
16678
+ const token = fs8.readFileSync(getRelayTokenPath(), "utf-8").trim();
16613
16679
  return token.length > 0 ? token : null;
16614
16680
  } catch {
16615
16681
  return null;
@@ -16783,17 +16849,17 @@ async function waitForClaudeCodeSkillDiscovery() {
16783
16849
  __name(waitForClaudeCodeSkillDiscovery, "waitForClaudeCodeSkillDiscovery");
16784
16850
  var FRAMER_TEMPORARY_DIR = path7.join(os2.tmpdir(), "framer");
16785
16851
  function ensureTemporaryDir() {
16786
- fs7.mkdirSync(FRAMER_TEMPORARY_DIR, { recursive: true });
16852
+ fs8.mkdirSync(FRAMER_TEMPORARY_DIR, { recursive: true });
16787
16853
  }
16788
16854
  __name(ensureTemporaryDir, "ensureTemporaryDir");
16789
16855
  function cleanupStaleSessionCodeFiles(activeSessionIds) {
16790
- if (!fs7.existsSync(FRAMER_TEMPORARY_DIR)) return;
16856
+ if (!fs8.existsSync(FRAMER_TEMPORARY_DIR)) return;
16791
16857
  const activeSessionIdsSet = new Set(activeSessionIds);
16792
- for (const entry of fs7.readdirSync(FRAMER_TEMPORARY_DIR)) {
16858
+ for (const entry of fs8.readdirSync(FRAMER_TEMPORARY_DIR)) {
16793
16859
  const [sessionId] = entry.split("-");
16794
16860
  if (activeSessionIdsSet.has(sessionId)) continue;
16795
16861
  const filePath = path7.join(FRAMER_TEMPORARY_DIR, entry);
16796
- fs7.rmSync(filePath, { recursive: true, force: true, maxRetries: 2 });
16862
+ fs8.rmSync(filePath, { recursive: true, force: true, maxRetries: 2 });
16797
16863
  }
16798
16864
  }
16799
16865
  __name(cleanupStaleSessionCodeFiles, "cleanupStaleSessionCodeFiles");
@@ -16804,12 +16870,16 @@ var CODE_COMPONENTS_SKILL_NAME = "framer-code-components";
16804
16870
  var __dirname2 = path7.dirname(fileURLToPath(import.meta.url));
16805
16871
  var skillsDocsDir = path7.join(__dirname2, "..", "docs", "skills");
16806
16872
  function readSkillDoc(name2) {
16807
- return fs7.readFileSync(path7.join(skillsDocsDir, name2), "utf-8").trimEnd();
16873
+ return fs8.readFileSync(path7.join(skillsDocsDir, name2), "utf-8").trimEnd();
16808
16874
  }
16809
16875
  __name(readSkillDoc, "readSkillDoc");
16810
16876
  function buildMetaSkill() {
16877
+ const command = getFramerAgentCommand();
16811
16878
  const template = readSkillDoc("framer.md");
16812
- return `${renderTemplate(template, { FRAMER_TEMPORARY_DIR })}
16879
+ return `${renderTemplate(template, {
16880
+ FRAMER_TEMPORARY_DIR,
16881
+ FRAMER_AGENT_COMMAND: command
16882
+ })}
16813
16883
  `;
16814
16884
  }
16815
16885
  __name(buildMetaSkill, "buildMetaSkill");
@@ -16828,49 +16898,58 @@ function renderTemplate(template, values) {
16828
16898
  __name(renderTemplate, "renderTemplate");
16829
16899
  var PROJECT_SKILL_PREFIX = "framer-project-";
16830
16900
  var LEGACY_CANVAS_EDITING_SKILL_PREFIX = "framer-canvas-editing-project-";
16901
+ var PACKAGE_NAME = "@framer/agent";
16902
+ var DEFAULT_FRAMER_AGENT_COMMAND = `npx ${PACKAGE_NAME}`;
16903
+ var RELEASE_AGE_OVERRIDE_FRAMER_AGENT_COMMAND = `npx --min-release-age=0 ${PACKAGE_NAME}`;
16904
+ function getFramerAgentCommand() {
16905
+ return isNpxMinReleaseAgeOverrideEnabled() ? RELEASE_AGE_OVERRIDE_FRAMER_AGENT_COMMAND : DEFAULT_FRAMER_AGENT_COMMAND;
16906
+ }
16907
+ __name(getFramerAgentCommand, "getFramerAgentCommand");
16831
16908
  function toSafeProjectId(projectId) {
16832
16909
  return projectId.replace(/[^a-zA-Z0-9_-]/g, "-");
16833
16910
  }
16834
16911
  __name(toSafeProjectId, "toSafeProjectId");
16835
16912
  function buildProjectSkill(projectId, agentContext, agentPrompt) {
16913
+ const command = getFramerAgentCommand();
16836
16914
  const skillName = `${PROJECT_SKILL_PREFIX}${toSafeProjectId(projectId)}`;
16837
16915
  const template = readSkillDoc("framer-project.md");
16838
16916
  const content = `${renderTemplate(template, {
16839
16917
  SKILL_NAME: skillName,
16840
16918
  PROJECT_ID: projectId,
16841
16919
  GENERATED_AT: (/* @__PURE__ */ new Date()).toISOString(),
16842
- AGENT_PROMPT: agentPrompt.trimEnd(),
16843
- AGENT_CONTEXT: agentContext.trimEnd(),
16844
- FRAMER_TEMPORARY_DIR
16920
+ AGENT_PROMPT: agentPrompt.trimEnd().replaceAll(DEFAULT_FRAMER_AGENT_COMMAND, command),
16921
+ AGENT_CONTEXT: agentContext.trimEnd().replaceAll(DEFAULT_FRAMER_AGENT_COMMAND, command),
16922
+ FRAMER_TEMPORARY_DIR,
16923
+ FRAMER_AGENT_COMMAND: command
16845
16924
  })}
16846
16925
  `;
16847
16926
  return { skillName, content };
16848
16927
  }
16849
16928
  __name(buildProjectSkill, "buildProjectSkill");
16850
16929
  function writeSkill(root, skillName, content) {
16851
- fs7.mkdirSync(root, { recursive: true });
16852
- const rootStat = fs7.statSync(root);
16930
+ fs8.mkdirSync(root, { recursive: true });
16931
+ const rootStat = fs8.statSync(root);
16853
16932
  if (!rootStat.isDirectory()) {
16854
16933
  throw new Error(`Skill root is not a directory: ${root}`);
16855
16934
  }
16856
16935
  const skillDir = path7.join(root, skillName);
16857
16936
  const filePath = path7.join(skillDir, "SKILL.md");
16858
- const current = fs7.lstatSync(skillDir, { throwIfNoEntry: false });
16937
+ const current = fs8.lstatSync(skillDir, { throwIfNoEntry: false });
16859
16938
  if (current && (current.isSymbolicLink() || !current.isDirectory())) {
16860
- fs7.rmSync(skillDir, { recursive: true, force: true });
16939
+ fs8.rmSync(skillDir, { recursive: true, force: true });
16861
16940
  }
16862
- fs7.mkdirSync(skillDir, { recursive: true });
16863
- fs7.writeFileSync(filePath, content, "utf-8");
16941
+ fs8.mkdirSync(skillDir, { recursive: true });
16942
+ fs8.writeFileSync(filePath, content, "utf-8");
16864
16943
  return filePath;
16865
16944
  }
16866
16945
  __name(writeSkill, "writeSkill");
16867
16946
  function cleanupLegacyCanvasEditingSkills(skillRoots = getDefaultSkillRoots()) {
16868
16947
  for (const root of skillRoots) {
16869
- if (!fs7.existsSync(root)) continue;
16870
- for (const entry of fs7.readdirSync(root)) {
16948
+ if (!fs8.existsSync(root)) continue;
16949
+ for (const entry of fs8.readdirSync(root)) {
16871
16950
  if (!entry.startsWith(LEGACY_CANVAS_EDITING_SKILL_PREFIX)) continue;
16872
16951
  const skillDir = path7.join(root, entry);
16873
- fs7.rmSync(skillDir, { recursive: true, force: true, maxRetries: 2 });
16952
+ fs8.rmSync(skillDir, { recursive: true, force: true, maxRetries: 2 });
16874
16953
  }
16875
16954
  }
16876
16955
  }
@@ -16888,14 +16967,14 @@ function cleanupStaleSkills(activeProjectIds, skillRoots = getDefaultSkillRoots(
16888
16967
  [...activeProjectIds].map(toSafeProjectId)
16889
16968
  );
16890
16969
  for (const root of skillRoots) {
16891
- if (!fs7.existsSync(root)) continue;
16892
- for (const entry of fs7.readdirSync(root)) {
16970
+ if (!fs8.existsSync(root)) continue;
16971
+ for (const entry of fs8.readdirSync(root)) {
16893
16972
  if (!entry.startsWith(PROJECT_SKILL_PREFIX)) continue;
16894
16973
  const sanitizedProjectId = entry.slice(PROJECT_SKILL_PREFIX.length);
16895
16974
  const isActive = sanitizedActiveProjectIds.has(sanitizedProjectId);
16896
16975
  if (isActive) continue;
16897
16976
  const skillDir = path7.join(root, entry);
16898
- fs7.rmSync(skillDir, { recursive: true, force: true, maxRetries: 2 });
16977
+ fs8.rmSync(skillDir, { recursive: true, force: true, maxRetries: 2 });
16899
16978
  }
16900
16979
  }
16901
16980
  }
@@ -16981,6 +17060,19 @@ function printTelemetryNotice() {
16981
17060
  markTelemetryNoticeShown();
16982
17061
  }
16983
17062
  __name(printTelemetryNotice, "printTelemetryNotice");
17063
+ async function installBaseSkills() {
17064
+ try {
17065
+ cleanupLegacyCanvasEditingSkills();
17066
+ } catch (error) {
17067
+ printWarning(
17068
+ `Failed to clean up legacy canvas editing skills: ${formatErrorForUser(error)}`
17069
+ );
17070
+ }
17071
+ const results = installSkills();
17072
+ await waitForClaudeCodeSkillDiscovery();
17073
+ return results;
17074
+ }
17075
+ __name(installBaseSkills, "installBaseSkills");
16984
17076
  async function getAgentSystemPrompt(sessionId) {
16985
17077
  debug("exec", "getAgentSystemPrompt: calling relay...");
16986
17078
  const result = await client.exec.mutate({
@@ -17137,7 +17229,7 @@ program.command("exec").description("Execute code in a session").requiredOption(
17137
17229
  let code = evalCode;
17138
17230
  if (!code && filePath) {
17139
17231
  try {
17140
- code = fs7.readFileSync(filePath, "utf-8");
17232
+ code = fs8.readFileSync(filePath, "utf-8");
17141
17233
  } catch (err) {
17142
17234
  printError(`Failed to read file: ${formatErrorForUser(err)}`);
17143
17235
  await waitForTrackingToFinish();
@@ -17401,15 +17493,7 @@ program.command("setup").description(
17401
17493
  "Install Framer skills into ~/.agents/skills and ~/.claude/skills"
17402
17494
  ).action(async () => {
17403
17495
  try {
17404
- try {
17405
- cleanupLegacyCanvasEditingSkills();
17406
- } catch (error) {
17407
- printWarning(
17408
- `Failed to clean up legacy canvas editing skills: ${formatErrorForUser(error)}`
17409
- );
17410
- }
17411
- const results = installSkills();
17412
- await waitForClaudeCodeSkillDiscovery();
17496
+ const results = await installBaseSkills();
17413
17497
  printSetupSummary(results);
17414
17498
  printTelemetryNotice();
17415
17499
  } catch (err) {
@@ -17441,4 +17525,48 @@ telemetry.command("disable").description("Disable telemetry").action(() => {
17441
17525
  telemetry.command("status").description("Show current telemetry status").action(() => {
17442
17526
  print(`Telemetry is ${isTelemetryEnabled() ? "enabled" : "disabled"}`);
17443
17527
  });
17528
+ var overrideMinReleaseAge = program.command("override-min-release-age").description("Configure generated skills to override npm min-release-age");
17529
+ overrideMinReleaseAge.command("enable").description("Override npm min-release-age for generated Framer npx commands").action(async () => {
17530
+ try {
17531
+ setNpxMinReleaseAgeOverrideEnabled(true);
17532
+ print(
17533
+ "Generated Framer skills will use `npx --min-release-age=0 @framer/agent`."
17534
+ );
17535
+ const results = await installBaseSkills();
17536
+ printSetupSummary(results);
17537
+ } catch (err) {
17538
+ printError(
17539
+ `Failed to enable min-release-age override: ${formatErrorForUser(err)}`
17540
+ );
17541
+ await waitForTrackingToFinish();
17542
+ process.exit(1);
17543
+ }
17544
+ });
17545
+ overrideMinReleaseAge.command("disable").description("Use the default npm min-release-age for generated npx commands").action(async () => {
17546
+ try {
17547
+ setNpxMinReleaseAgeOverrideEnabled(false);
17548
+ print(
17549
+ "Generated Framer skills will use the default npm min-release-age."
17550
+ );
17551
+ const results = await installBaseSkills();
17552
+ printSetupSummary(results);
17553
+ } catch (err) {
17554
+ printError(
17555
+ `Failed to disable min-release-age override: ${formatErrorForUser(err)}`
17556
+ );
17557
+ await waitForTrackingToFinish();
17558
+ process.exit(1);
17559
+ }
17560
+ });
17561
+ overrideMinReleaseAge.command("status").description("Show current npm min-release-age override status").action(() => {
17562
+ if (isNpxMinReleaseAgeOverrideEnabled()) {
17563
+ print(
17564
+ "npm min-release-age override is enabled for generated Framer skills."
17565
+ );
17566
+ return;
17567
+ }
17568
+ print(
17569
+ "npm min-release-age override is disabled for generated Framer skills."
17570
+ );
17571
+ });
17444
17572
  program.parse();
@@ -13,8 +13,9 @@ import { connect, FramerAPIError } from 'framer-api';
13
13
  import { z } from 'zod';
14
14
  import { createRequire } from 'module';
15
15
  import * as vm from 'vm';
16
+ import isWsl from 'is-wsl';
16
17
 
17
- /* @framer/ai relay server v0.0.33 */
18
+ /* @framer/ai relay server v0.0.34 */
18
19
  var __defProp = Object.defineProperty;
19
20
  var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name2);
20
21
  var __typeError = (msg) => {
@@ -199,7 +200,7 @@ __name(debug, "debug");
199
200
  // src/version.ts
200
201
  var VERSION = (
201
202
  // typeof is used to ensure this can be used just via tsx or node etc. without build
202
- "0.0.33"
203
+ "0.0.34"
203
204
  );
204
205
 
205
206
  // src/relay-client.ts
@@ -234,17 +235,19 @@ function normalizeValue(value, maxLength) {
234
235
  }
235
236
  __name(normalizeValue, "normalizeValue");
236
237
  function normalizeAgentThreadContext(input) {
238
+ const trimmedAgentThreadId = input.agentThreadId?.trim();
237
239
  const agentThreadId = normalizeValue(
238
240
  input.agentThreadId,
239
241
  MAX_AGENT_THREAD_ID_LENGTH
240
242
  );
241
- if (!agentThreadId) return {};
243
+ if (trimmedAgentThreadId && !agentThreadId) return {};
242
244
  const agentProvider = normalizeValue(
243
245
  input.agentProvider,
244
246
  MAX_AGENT_PROVIDER_LENGTH
245
247
  );
248
+ if (!agentThreadId && !agentProvider) return {};
246
249
  return {
247
- agentThreadId,
250
+ ...agentThreadId ? { agentThreadId } : {},
248
251
  ...agentProvider ? { agentProvider } : {}
249
252
  };
250
253
  }
@@ -842,11 +845,13 @@ var SettingsWatcher = class {
842
845
  var DEFAULT_MACHINE_ID = "unknown-machine-id-this-should-never-be-persisted";
843
846
  var DEFAULT_SETTINGS = {
844
847
  machineId: DEFAULT_MACHINE_ID,
848
+ npxMinReleaseAgeOverrideEnabled: false,
845
849
  telemetryEnabled: true,
846
850
  telemetryNoticeShown: false
847
851
  };
848
852
  var SettingsFileSchema = z.object({
849
853
  machineId: z.string().optional(),
854
+ npxMinReleaseAgeOverrideEnabled: z.boolean().optional(),
850
855
  telemetryEnabled: z.boolean().optional(),
851
856
  telemetryNoticeShown: z.boolean().optional()
852
857
  });
@@ -874,6 +879,7 @@ function readSettingsFile() {
874
879
  const result = SettingsFileSchema.parse(raw);
875
880
  return {
876
881
  machineId: result.machineId ?? DEFAULT_SETTINGS.machineId,
882
+ npxMinReleaseAgeOverrideEnabled: result.npxMinReleaseAgeOverrideEnabled ?? DEFAULT_SETTINGS.npxMinReleaseAgeOverrideEnabled,
877
883
  telemetryEnabled: result.telemetryEnabled ?? DEFAULT_SETTINGS.telemetryEnabled,
878
884
  telemetryNoticeShown: result.telemetryNoticeShown ?? DEFAULT_SETTINGS.telemetryNoticeShown
879
885
  };
@@ -935,8 +941,27 @@ function getMacOsVersion() {
935
941
  }
936
942
  }
937
943
  __name(getMacOsVersion, "getMacOsVersion");
944
+ function parseOsReleaseValue(content, key) {
945
+ const line = content.split("\n").find((candidate) => candidate.startsWith(`${key}=`));
946
+ const value = line?.slice(key.length + 1).trim();
947
+ if (!value) return void 0;
948
+ return value.replace(/^"|"$/g, "") || void 0;
949
+ }
950
+ __name(parseOsReleaseValue, "parseOsReleaseValue");
951
+ function getLinuxOsVersion() {
952
+ let version;
953
+ try {
954
+ const osRelease = fs4.readFileSync("/etc/os-release", "utf8");
955
+ version = parseOsReleaseValue(osRelease, "PRETTY_NAME") ?? "unknown";
956
+ } catch {
957
+ version = os5.release().trim() || "unknown";
958
+ }
959
+ return isWsl ? `${version} wsl` : version;
960
+ }
961
+ __name(getLinuxOsVersion, "getLinuxOsVersion");
938
962
  function getOsVersion(platform) {
939
963
  if (platform === "darwin") return getMacOsVersion();
964
+ if (platform === "linux") return getLinuxOsVersion();
940
965
  const version = os5.release().trim();
941
966
  return version || "unknown";
942
967
  }
@@ -1106,22 +1131,20 @@ var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
1106
1131
  var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
1107
1132
  var MISSING_SERVER_SESSION_ID = "missing-session-id-should-not-happen";
1108
1133
  var Connection = class _Connection {
1109
- constructor(projectId, userId, apiKey, headlessServerUrl, framer) {
1134
+ constructor(projectId, userId, headlessServerUrl, framer) {
1110
1135
  this.projectId = projectId;
1111
1136
  this.userId = userId;
1112
- this.apiKey = apiKey;
1113
1137
  this.headlessServerUrl = headlessServerUrl;
1114
1138
  this.framer = framer;
1115
1139
  }
1116
1140
  projectId;
1117
1141
  userId;
1118
- apiKey;
1119
1142
  headlessServerUrl;
1120
1143
  framer;
1121
1144
  static {
1122
1145
  __name(this, "Connection");
1123
1146
  }
1124
- sessions = [];
1147
+ session;
1125
1148
  status = "connected";
1126
1149
  pendingReconnect;
1127
1150
  activeBranchId;
@@ -1130,7 +1153,7 @@ var Connection = class _Connection {
1130
1153
  clientId: `dalton/${VERSION}`,
1131
1154
  serverUrl: headlessServerUrl
1132
1155
  });
1133
- return new _Connection(projectId, userId, apiKey, headlessServerUrl, framer);
1156
+ return new _Connection(projectId, userId, headlessServerUrl, framer);
1134
1157
  }
1135
1158
  isConnected() {
1136
1159
  return this.status === "connected";
@@ -1144,15 +1167,19 @@ var Connection = class _Connection {
1144
1167
  getServerSessionId() {
1145
1168
  return this.framer.sessionId ?? MISSING_SERVER_SESSION_ID;
1146
1169
  }
1147
- listSessionInfo() {
1148
- return this.sessions.map((session) => ({
1149
- id: session.id,
1170
+ getSessionInfo() {
1171
+ if (!this.session) return void 0;
1172
+ return {
1173
+ id: this.session.id,
1150
1174
  projectId: this.projectId,
1151
- stateKeys: Object.keys(session.state),
1175
+ stateKeys: Object.keys(this.session.state),
1152
1176
  headlessServerUrl: this.headlessServerUrl
1153
- }));
1177
+ };
1154
1178
  }
1155
1179
  createSession(id, agentThreadContext) {
1180
+ if (this.session) {
1181
+ throw new Error("Connection already has a session");
1182
+ }
1156
1183
  const session = {
1157
1184
  id,
1158
1185
  state: {},
@@ -1161,28 +1188,17 @@ var Connection = class _Connection {
1161
1188
  inflight: 0,
1162
1189
  connection: this
1163
1190
  };
1164
- this.sessions.push(session);
1191
+ this.session = session;
1165
1192
  return session;
1166
1193
  }
1167
1194
  findSession(id) {
1168
- return this.sessions.find((session) => session.id === id);
1169
- }
1170
- removeSession(id) {
1171
- const index = this.sessions.findIndex((session) => session.id === id);
1172
- if (index >= 0) {
1173
- this.sessions.splice(index, 1);
1174
- }
1195
+ return this.session?.id === id ? this.session : void 0;
1175
1196
  }
1176
- hasSessions() {
1177
- return this.sessions.length > 0;
1178
- }
1179
- clearSessions() {
1180
- this.sessions.length = 0;
1197
+ clearSession() {
1198
+ this.session = void 0;
1181
1199
  }
1182
1200
  isIdle(now, timeoutMs) {
1183
- return this.sessions.every(
1184
- (session) => session.inflight === 0 && now - session.lastActivityAt >= timeoutMs
1185
- );
1201
+ return this.session !== void 0 && this.session.inflight === 0 && now - this.session.lastActivityAt >= timeoutMs;
1186
1202
  }
1187
1203
  async reconnect(expectedInitialBranchId = this.activeBranchId) {
1188
1204
  if (this.status === "connected") {
@@ -1228,7 +1244,7 @@ var SessionManager = class {
1228
1244
  connections = [];
1229
1245
  idleCheck = null;
1230
1246
  async create(projectId, userId, apiKey, headlessServerUrl, agentThreadContext = {}) {
1231
- const connection = await this.findOrCreateConnection(
1247
+ const connection = await Connection.open(
1232
1248
  projectId,
1233
1249
  userId,
1234
1250
  apiKey,
@@ -1238,13 +1254,23 @@ var SessionManager = class {
1238
1254
  this.getNextSessionId(),
1239
1255
  agentThreadContext
1240
1256
  );
1257
+ trackConnectionAcquire({
1258
+ projectId,
1259
+ userId: connection.userId,
1260
+ sessionId: connection.getServerSessionId(),
1261
+ isReuse: false
1262
+ });
1263
+ this.connections.push(connection);
1241
1264
  this.startIdleCheck();
1242
1265
  return session;
1243
1266
  }
1244
1267
  list() {
1245
- return this.connections.flatMap(
1246
- (connection) => connection.listSessionInfo()
1247
- );
1268
+ const sessions = [];
1269
+ for (const connection of this.connections) {
1270
+ const sessionInfo = connection.getSessionInfo();
1271
+ if (sessionInfo) sessions.push(sessionInfo);
1272
+ }
1273
+ return sessions;
1248
1274
  }
1249
1275
  get(id) {
1250
1276
  return this.findSession(id);
@@ -1305,30 +1331,29 @@ var SessionManager = class {
1305
1331
  if (!session) {
1306
1332
  return;
1307
1333
  }
1334
+ const connection = session.connection;
1335
+ await connection.disconnect();
1336
+ connection.clearSession();
1337
+ const index = this.connections.indexOf(connection);
1338
+ if (index >= 0) {
1339
+ this.connections.splice(index, 1);
1340
+ }
1341
+ if (this.connections.length === 0) {
1342
+ this.stopIdleCheck();
1343
+ }
1308
1344
  trackSessionDestroy({
1309
1345
  projectId: session.connection.projectId,
1310
1346
  userId: session.connection.userId,
1311
1347
  sessionId: session.connection.getServerSessionId(),
1312
1348
  ...session.agentThreadContext
1313
1349
  });
1314
- session.connection.removeSession(id);
1315
- if (!session.connection.hasSessions()) {
1316
- await session.connection.disconnect();
1317
- const index = this.connections.indexOf(session.connection);
1318
- if (index >= 0) {
1319
- this.connections.splice(index, 1);
1320
- }
1321
- }
1322
- if (this.connections.length === 0) {
1323
- this.stopIdleCheck();
1324
- }
1325
1350
  }
1326
1351
  async destroyAll() {
1327
1352
  const connections = [...this.connections];
1328
1353
  this.connections = [];
1329
1354
  this.stopIdleCheck();
1330
1355
  for (const connection of connections) {
1331
- connection.clearSessions();
1356
+ connection.clearSession();
1332
1357
  await connection.disconnect();
1333
1358
  }
1334
1359
  }
@@ -1385,42 +1410,6 @@ var SessionManager = class {
1385
1410
  }
1386
1411
  }
1387
1412
  }
1388
- findConnection(projectId, apiKey, headlessServerUrl) {
1389
- return this.connections.find(
1390
- (connection) => connection.projectId === projectId && connection.apiKey === apiKey && connection.headlessServerUrl === headlessServerUrl
1391
- );
1392
- }
1393
- async findOrCreateConnection(projectId, userId, apiKey, headlessServerUrl) {
1394
- const existingConnection = this.findConnection(
1395
- projectId,
1396
- apiKey,
1397
- headlessServerUrl
1398
- );
1399
- if (existingConnection) {
1400
- await existingConnection.reconnect();
1401
- trackConnectionAcquire({
1402
- projectId,
1403
- userId: existingConnection.userId,
1404
- sessionId: existingConnection.getServerSessionId(),
1405
- isReuse: true
1406
- });
1407
- return existingConnection;
1408
- }
1409
- const connection = await Connection.open(
1410
- projectId,
1411
- userId,
1412
- apiKey,
1413
- headlessServerUrl
1414
- );
1415
- trackConnectionAcquire({
1416
- projectId,
1417
- userId: connection.userId,
1418
- sessionId: connection.getServerSessionId(),
1419
- isReuse: false
1420
- });
1421
- this.connections.push(connection);
1422
- return connection;
1423
- }
1424
1413
  findSession(id) {
1425
1414
  for (const connection of this.connections) {
1426
1415
  const session = connection.findSession(id);
@@ -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/agent:*)", "Bash(npx @framer/agent@latest:*)", "Read({{FRAMER_TEMPORARY_DIR}}/*)", "Write({{FRAMER_TEMPORARY_DIR}}/*)"]
4
+ allowed-tools: ["Bash({{FRAMER_AGENT_COMMAND}}:*)", "Bash({{FRAMER_AGENT_COMMAND}}@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/agent@latest docs` before writing any code.** Do not guess method names or signatures.
18
+ **You MUST run `{{FRAMER_AGENT_COMMAND}}@latest docs` before writing any code.** Do not guess method names or signatures.
19
19
 
20
20
  ```bash
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?
21
+ {{FRAMER_AGENT_COMMAND}}@latest docs Collection # What methods exist?
22
+ {{FRAMER_AGENT_COMMAND}}@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/agent@latest docs Collection.getItems # What are the parameters and
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/agent@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-read-collections.js
30
+ {{FRAMER_AGENT_COMMAND}}@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-read-collections.js
31
31
  ```
32
32
 
33
33
  ### 3. Store results in `state`
@@ -38,10 +38,10 @@ Always save results you'll need again. Don't repeat API calls.
38
38
 
39
39
  Prefer the agent-specific methods `framer.agent.*` over regular plugin API methods (`framer.*`).
40
40
 
41
- For `framer.agent.readProject` and `framer.agent.applyChanges` calls, use the `npx @framer/agent@latest read-project` and `npx @framer/agent@latest apply-changes` subcommands when possible. It's still ok to call those methods in exec scripts if you require something more complex than a plain method call.
41
+ For `framer.agent.readProject` and `framer.agent.applyChanges` calls, use the `{{FRAMER_AGENT_COMMAND}}@latest read-project` and `{{FRAMER_AGENT_COMMAND}}@latest apply-changes` subcommands when possible. It's still ok to call those methods in exec scripts if you require something more complex than a plain method call.
42
42
 
43
43
  - Use `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.
44
- - Do not use `npx @framer/agent@latest read-project` or `framer.agent.readProject` for node tree reads unless you have just checked current docs and confirmed the exact query type. Query shapes such as `{ type: "node-by-id" }` are not valid for the current local API.
44
+ - Do not use `{{FRAMER_AGENT_COMMAND}}@latest read-project` or `framer.agent.readProject` for node tree reads unless you have just checked current docs and confirmed the exact query type. Query shapes such as `{ type: "node-by-id" }` are not valid for the current local API.
45
45
  - 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.
46
46
  - 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.
47
47
  - Use `framer.agent.publish` for publishing. Do not use `publish`, `getDeployments`, or `deploy` for normal agent publishing flows.
@@ -87,7 +87,7 @@ state.collections = await framer.getCollections();
87
87
  ```
88
88
 
89
89
  ```bash
90
- npx @framer/agent@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
90
+ {{FRAMER_AGENT_COMMAND}}@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
91
91
  ```
92
92
 
93
93
  ```js
@@ -97,7 +97,7 @@ console.log(state.teamItems.length);
97
97
  ```
98
98
 
99
99
  ```bash
100
- npx @framer/agent@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-team-items.js
100
+ {{FRAMER_AGENT_COMMAND}}@latest exec -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-team-items.js
101
101
  ```
102
102
 
103
103
  Store anything you'll reference again.
@@ -141,7 +141,7 @@ Prompting may take a while to complete, so set the command timeout to 10 minutes
141
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`:
142
142
 
143
143
  ```bash
144
- npx @framer/agent@latest exec -s <sessionId> -f {{FRAMER_TEMPORARY_DIR}}/<sessionId>-<short-summary>.js
144
+ {{FRAMER_AGENT_COMMAND}}@latest exec -s <sessionId> -f {{FRAMER_TEMPORARY_DIR}}/<sessionId>-<short-summary>.js
145
145
  ```
146
146
 
147
147
  For short snippets, `exec` also accepts `-e <code>` or code piped on stdin.
@@ -154,24 +154,24 @@ In Windows PowerShell, if an argument contains nested quotes, use a single-quote
154
154
  $value = @'
155
155
  [{"key":"value","filter":["text","$rect"]}]
156
156
  '@
157
- npx @framer/agent@latest <command> --option $value
157
+ {{FRAMER_AGENT_COMMAND}}@latest <command> --option $value
158
158
  ```
159
159
 
160
160
  ## API Documentation
161
161
 
162
162
  ```bash
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
163
+ {{FRAMER_AGENT_COMMAND}}@latest docs # List all available methods
164
+ {{FRAMER_AGENT_COMMAND}}@latest docs framer.getCollections # Show top level method
165
+ {{FRAMER_AGENT_COMMAND}}@latest docs Collection # Show class with all method signatures
166
+ {{FRAMER_AGENT_COMMAND}}@latest docs Collection.addItems # Show method + recursively expand all referenced types
167
+ {{FRAMER_AGENT_COMMAND}}@latest docs ScreenshotOptions # Show type + recursively expand all referenced types
168
168
  ```
169
169
 
170
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.
171
171
 
172
172
  ## API Examples
173
173
 
174
- **STOP: These are patterns only. Before using any method below, run `npx @framer/agent@latest docs <ClassName>` to verify the current signature.**
174
+ **STOP: These are patterns only. Before using any method below, run `{{FRAMER_AGENT_COMMAND}}@latest docs <ClassName>` to verify the current signature.**
175
175
 
176
176
  ### Working with Collections (CMS)
177
177
 
@@ -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/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}}/*)"]
5
+ **Mandatory precondition**: run `{{FRAMER_AGENT_COMMAND}}@latest setup` and let it complete **BEFORE** loading this skill.
6
+ allowed-tools: ["Bash({{FRAMER_AGENT_COMMAND}}:*)", "Bash({{FRAMER_AGENT_COMMAND}}@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/agent@latest setup
11
+ {{FRAMER_AGENT_COMMAND}}@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/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.
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 `{{FRAMER_AGENT_COMMAND}}@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/agent@latest project list
39
+ {{FRAMER_AGENT_COMMAND}}@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/agent@latest session new "<url or id>"
49
+ {{FRAMER_AGENT_COMMAND}}@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,21 +54,21 @@ 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/agent@latest project new
58
- npx @framer/agent@latest session new "<returned project id>"
57
+ {{FRAMER_AGENT_COMMAND}}@latest project new
58
+ {{FRAMER_AGENT_COMMAND}}@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/agent@latest project remix "<url, project id, or remix link>"
65
- npx @framer/agent@latest session new "<returned project id>"
64
+ {{FRAMER_AGENT_COMMAND}}@latest project remix "<url, project id, or remix link>"
65
+ {{FRAMER_AGENT_COMMAND}}@latest session new "<returned project id>"
66
66
  ```
67
67
 
68
68
  List active sessions:
69
69
 
70
70
  ```bash
71
- npx @framer/agent@latest session list
71
+ {{FRAMER_AGENT_COMMAND}}@latest session list
72
72
  ```
73
73
 
74
74
  ## Project-scoped skill
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@framer/agent",
3
- "version": "0.0.33",
3
+ "version": "0.0.34",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "main": "./dist/cli.js",
@@ -24,15 +24,16 @@
24
24
  "@trpc/client": "^11.17.0",
25
25
  "@trpc/server": "^11.17.0",
26
26
  "commander": "^12.1.0",
27
- "framer-api": "^0.1.16",
27
+ "framer-api": "^0.1.17",
28
+ "is-wsl": "^3.1.1",
28
29
  "zod": "^4.4.3"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@biomejs/biome": "^2.4.13",
32
- "@framerjs/framer-events": "0.0.185",
33
+ "@framerjs/framer-events": "0.0.187",
33
34
  "@types/node": "24.13.0",
34
35
  "tsup": "^8.0.2",
35
- "tsx": "^4.22.3",
36
+ "tsx": "^4.22.4",
36
37
  "typescript": "^5.9.2",
37
38
  "vitest": "^4.1.7"
38
39
  },