framer-dalton 0.0.8 → 0.0.9

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
@@ -1,16 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
  import path3 from 'path';
3
3
  import { Command } from 'commander';
4
+ import crypto from 'crypto';
5
+ import http from 'http';
6
+ import { spawn, execFile } from 'child_process';
4
7
  import fs2 from 'fs';
5
8
  import os from 'os';
6
9
  import { z } from 'zod';
7
- import { spawn } from 'child_process';
8
10
  import { fileURLToPath } from 'url';
9
11
  import { createTRPCClient, httpLink } from '@trpc/client';
10
12
 
11
- /* @framer/ai CLI v0.0.8 */
13
+ /* @framer/ai CLI v0.0.9 */
12
14
  var __defProp = Object.defineProperty;
13
15
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
16
+ function openUrl(url) {
17
+ const platform = process.platform;
18
+ let cmd;
19
+ let args;
20
+ if (platform === "darwin") {
21
+ cmd = "open";
22
+ args = [url];
23
+ } else if (platform === "win32") {
24
+ cmd = "cmd";
25
+ args = ["/c", "start", "", url];
26
+ } else {
27
+ cmd = "xdg-open";
28
+ args = [url];
29
+ }
30
+ return new Promise((resolve) => {
31
+ const child = execFile(cmd, args);
32
+ child.on("error", () => resolve(false));
33
+ child.on("exit", (code) => resolve(code === 0));
34
+ });
35
+ }
36
+ __name(openUrl, "openUrl");
14
37
  function getConfigDir() {
15
38
  if (process.env.XDG_CONFIG_HOME) {
16
39
  return path3.join(process.env.XDG_CONFIG_HOME, "framer");
@@ -149,6 +172,245 @@ function saveProject(project2) {
149
172
  writeProjectsConfig(config);
150
173
  }
151
174
  __name(saveProject, "saveProject");
175
+ function clearApiKey(projectId) {
176
+ const config = readProjectsConfig();
177
+ if (!(projectId in config.projects)) return false;
178
+ delete config.projects[projectId];
179
+ writeProjectsConfig(config);
180
+ return true;
181
+ }
182
+ __name(clearApiKey, "clearApiKey");
183
+
184
+ // src/utils.ts
185
+ function formatError(error) {
186
+ if (error instanceof Error) {
187
+ return error.message;
188
+ }
189
+ return String(error);
190
+ }
191
+ __name(formatError, "formatError");
192
+ function printJson(value) {
193
+ console.log(JSON.stringify(value, null, 2));
194
+ }
195
+ __name(printJson, "printJson");
196
+ function print(message) {
197
+ console.log(message);
198
+ }
199
+ __name(print, "print");
200
+ function printError(message) {
201
+ console.error(message);
202
+ }
203
+ __name(printError, "printError");
204
+
205
+ // src/auth-callback.ts
206
+ var TIMEOUT_MS = 3e5;
207
+ var themes = {
208
+ dark: {
209
+ pageBackground: "rgb(17, 17, 17)",
210
+ modalBackground: "rgb(17, 17, 17)",
211
+ modalBorder: "rgba(255, 255, 255, 0.07)",
212
+ titleColor: "#fff",
213
+ textColor: "rgb(102, 102, 102)",
214
+ separatorColor: "rgba(255, 255, 255, 0.07)",
215
+ buttonBackground: "#333",
216
+ buttonBackgroundHover: "#3a3a3a",
217
+ buttonText: "#fff",
218
+ shadow: "0px 10px 30px 0px rgba(0, 0, 0, 0.15)"
219
+ },
220
+ light: {
221
+ pageBackground: "#eee",
222
+ modalBackground: "#fff",
223
+ modalBorder: "rgba(0, 0, 0, 0.07)",
224
+ titleColor: "#333",
225
+ textColor: "rgb(102, 102, 102)",
226
+ separatorColor: "rgba(0, 0, 0, 0.07)",
227
+ buttonBackground: "rgba(0, 0, 0, 0.05)",
228
+ buttonBackgroundHover: "rgba(0, 0, 0, 0.08)",
229
+ buttonText: "#333",
230
+ shadow: "0px 10px 30px 0px rgba(0, 0, 0, 0.15)"
231
+ }
232
+ };
233
+ function htmlPage(opts) {
234
+ const t = themes[opts.theme];
235
+ return `<!DOCTYPE html>
236
+ <html lang="en">
237
+ <head>
238
+ <meta charset="utf-8">
239
+ <meta name="viewport" content="width=device-width,initial-scale=1">
240
+ <title>${opts.title}</title>
241
+ <link rel="preconnect" href="https://fonts.googleapis.com">
242
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
243
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@500;600&display=swap">
244
+ <style>
245
+ *{margin:0;padding:0;box-sizing:border-box}
246
+ body{font-family:"Inter",system-ui,-apple-system,sans-serif;font-feature-settings:"cv01" 1,"cv05" 1,"cv09" 1,"cv11" 1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;display:flex;justify-content:center;align-items:center;min-height:100vh;background:${t.pageBackground}}
247
+ .modal{width:260px;padding:0 10px;border-radius:18px;background:${t.modalBackground};border:1px solid ${t.modalBorder};box-shadow:${t.shadow}}
248
+ .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}
249
+ .content{padding:10px 0}
250
+ .text{color:${t.textColor};font-size:12px;font-weight:500;line-height:1.5;text-wrap:balance}
251
+ .footer{padding:10px 0;border-top:1px solid ${t.separatorColor}}
252
+ .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}
253
+ .footer button:hover{background:${t.buttonBackgroundHover}}
254
+ </style>
255
+ </head>
256
+ <body>
257
+ <div class="modal">
258
+ <div class="header">${opts.heading}</div>
259
+ <div class="content"><span class="text">${opts.message}</span></div>
260
+ </div>
261
+ </body>
262
+ </html>`;
263
+ }
264
+ __name(htmlPage, "htmlPage");
265
+ function successHtml(theme) {
266
+ return htmlPage({
267
+ title: "Framer \u2014 Agent Approved",
268
+ heading: "Agent Approved",
269
+ message: "Your local agent now has edit access to the project. You can close this tab and continue with the local agent.",
270
+ theme
271
+ });
272
+ }
273
+ __name(successHtml, "successHtml");
274
+ function deniedHtml(theme) {
275
+ return htmlPage({
276
+ title: "Framer \u2014 Authorization Denied",
277
+ heading: "Authorization Denied",
278
+ message: "Agent access was denied. You can close this tab.",
279
+ theme
280
+ });
281
+ }
282
+ __name(deniedHtml, "deniedHtml");
283
+ function errorHtml(theme) {
284
+ return htmlPage({
285
+ title: "Framer \u2014 Authorization Failed",
286
+ heading: "Authorization Failed",
287
+ message: "Missing or invalid parameters. Please try again.",
288
+ theme
289
+ });
290
+ }
291
+ __name(errorHtml, "errorHtml");
292
+ function parseTheme(value) {
293
+ return value === "light" ? "light" : "dark";
294
+ }
295
+ __name(parseTheme, "parseTheme");
296
+ async function acquireKeyFromBrowser(projectId) {
297
+ const state = crypto.randomBytes(16).toString("hex");
298
+ return new Promise((resolve, reject) => {
299
+ let pendingResult = null;
300
+ const server = http.createServer((req, res) => {
301
+ const url = new URL(req.url ?? "/", `http://127.0.0.1`);
302
+ if (url.pathname === "/done") {
303
+ const theme2 = parseTheme(url.searchParams.get("theme"));
304
+ res.writeHead(200, { "Content-Type": "text/html" });
305
+ res.end(successHtml(theme2));
306
+ if (pendingResult) {
307
+ const { apiKey: apiKey2 } = pendingResult;
308
+ pendingResult = null;
309
+ cleanup();
310
+ print("\u2713 Agent authorized");
311
+ resolve(apiKey2);
312
+ }
313
+ return;
314
+ }
315
+ if (url.pathname === "/error") {
316
+ const theme2 = parseTheme(url.searchParams.get("theme"));
317
+ res.writeHead(200, { "Content-Type": "text/html" });
318
+ res.end(errorHtml(theme2));
319
+ return;
320
+ }
321
+ if (url.pathname !== "/callback") {
322
+ res.writeHead(404);
323
+ res.end();
324
+ return;
325
+ }
326
+ const theme = parseTheme(url.searchParams.get("theme"));
327
+ const error = url.searchParams.get("error");
328
+ if (error === "denied") {
329
+ res.writeHead(200, { "Content-Type": "text/html" });
330
+ res.end(deniedHtml(theme));
331
+ cleanup();
332
+ reject(new Error("Authorization denied by user."));
333
+ return;
334
+ }
335
+ const apiKey = url.searchParams.get("apiKey");
336
+ const returnedState = url.searchParams.get("state");
337
+ if (!apiKey || returnedState !== state) {
338
+ res.writeHead(302, { Location: `/error?theme=${theme}` });
339
+ res.end();
340
+ return;
341
+ }
342
+ pendingResult = { apiKey, theme };
343
+ res.writeHead(302, { Location: `/done?theme=${theme}` });
344
+ res.end();
345
+ });
346
+ const POLL_INTERVAL_MS = 1e3;
347
+ const pollTimer = setInterval(() => {
348
+ const apiKey = getApiKey(projectId);
349
+ if (apiKey) {
350
+ cleanup();
351
+ print("\u2713 API key detected from config");
352
+ resolve(apiKey);
353
+ }
354
+ }, POLL_INTERVAL_MS);
355
+ const HINT_MS = 55e3;
356
+ const hintTimer = setTimeout(() => {
357
+ const settingsUrl = `https://framer.com/projects/${projectId}?view=settings%3Aproject`;
358
+ print("");
359
+ printError("Taking a while? You can generate an API key manually:");
360
+ printError(` 1. Open Site Settings \u2192 General: ${settingsUrl}`);
361
+ printError(" 2. Scroll to API Keys and create a new key");
362
+ printError(
363
+ ` 3. In another terminal, run: framer project auth ${projectId} <your-api-key>`
364
+ );
365
+ print("");
366
+ print("Waiting for authorization in browser...");
367
+ }, HINT_MS);
368
+ const timer = setTimeout(() => {
369
+ cleanup();
370
+ reject(
371
+ new Error(
372
+ "Browser authorization timed out. Use `framer project auth <projectUrlOrId> <apiKey>` instead."
373
+ )
374
+ );
375
+ }, TIMEOUT_MS);
376
+ function cleanup() {
377
+ clearTimeout(timer);
378
+ clearTimeout(hintTimer);
379
+ clearInterval(pollTimer);
380
+ server.close();
381
+ }
382
+ __name(cleanup, "cleanup");
383
+ server.listen(0, "127.0.0.1", async () => {
384
+ const addr = server.address();
385
+ if (!addr || typeof addr === "string") {
386
+ cleanup();
387
+ reject(new Error("Failed to start callback server."));
388
+ return;
389
+ }
390
+ const callbackUrl = `http://127.0.0.1:${addr.port}/callback`;
391
+ const deeplink = new URL("https://framer.com/projects/server-api/auth");
392
+ deeplink.searchParams.set("callback", callbackUrl);
393
+ deeplink.searchParams.set("state", state);
394
+ deeplink.searchParams.set("projectId", projectId);
395
+ const deeplinkStr = deeplink.toString();
396
+ const opened = await openUrl(deeplinkStr);
397
+ if (opened) {
398
+ print("Waiting for authorization in browser...");
399
+ } else {
400
+ printError("Could not open browser. Open this URL manually:");
401
+ printError(deeplinkStr);
402
+ }
403
+ });
404
+ });
405
+ }
406
+ __name(acquireKeyFromBrowser, "acquireKeyFromBrowser");
407
+
408
+ // src/connection-errors.ts
409
+ var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
410
+ function isAuthError(errorMessage) {
411
+ return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
412
+ }
413
+ __name(isAuthError, "isAuthError");
152
414
 
153
415
  // src/types-data.ts
154
416
  var types = {
@@ -2320,6 +2582,20 @@ var types = {
2320
2582
  }
2321
2583
  ]
2322
2584
  },
2585
+ designpagecloneoptions: {
2586
+ name: "DesignPageCloneOptions",
2587
+ description: "",
2588
+ kind: "interface",
2589
+ references: [],
2590
+ members: [
2591
+ {
2592
+ name: "name",
2593
+ type: "string",
2594
+ description: "",
2595
+ optional: true
2596
+ }
2597
+ ]
2598
+ },
2323
2599
  diagnosticbase: {
2324
2600
  name: "DiagnosticBase",
2325
2601
  description: "",
@@ -4495,7 +4771,7 @@ var types = {
4495
4771
  name: "LocationControl",
4496
4772
  description: "",
4497
4773
  kind: "interface",
4498
- references: ["ControlBase", "Location"],
4774
+ references: ["ControlBase", "Location", "UnsupportedVariable"],
4499
4775
  members: [
4500
4776
  {
4501
4777
  name: "type",
@@ -4505,7 +4781,7 @@ var types = {
4505
4781
  },
4506
4782
  {
4507
4783
  name: "value",
4508
- type: "Location | undefined",
4784
+ type: "Location | UnsupportedVariable | undefined",
4509
4785
  description: "",
4510
4786
  optional: true
4511
4787
  }
@@ -5549,6 +5825,18 @@ var types = {
5549
5825
  description: "",
5550
5826
  optional: false
5551
5827
  },
5828
+ {
5829
+ name: "cloneWebPage",
5830
+ type: "(nodeId: NodeId, options?: WebPageCloneOptions) => Promise<SomeNodeData | null>",
5831
+ description: "",
5832
+ optional: false
5833
+ },
5834
+ {
5835
+ name: "cloneDesignPage",
5836
+ type: "(nodeId: NodeId, options?: DesignPageCloneOptions) => Promise<SomeNodeData | null>",
5837
+ description: "",
5838
+ optional: false
5839
+ },
5552
5840
  {
5553
5841
  name: "getNode",
5554
5842
  type: "(nodeId: NodeId) => Promise<SomeNodeData | null>",
@@ -8300,6 +8588,20 @@ var types = {
8300
8588
  }
8301
8589
  ]
8302
8590
  },
8591
+ webpagecloneoptions: {
8592
+ name: "WebPageCloneOptions",
8593
+ description: "",
8594
+ kind: "interface",
8595
+ references: [],
8596
+ members: [
8597
+ {
8598
+ name: "path",
8599
+ type: "string",
8600
+ description: "",
8601
+ optional: true
8602
+ }
8603
+ ]
8604
+ },
8303
8605
  widthconstraint: {
8304
8606
  name: "WidthConstraint",
8305
8607
  description: "",
@@ -10425,7 +10727,7 @@ var methodsByCategory = {
10425
10727
  {
10426
10728
  name: "clone",
10427
10729
  category: "ComponentInstanceNode",
10428
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
10730
+ signature: "clone(): Promise<typeof this | null>",
10429
10731
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
10430
10732
  references: []
10431
10733
  },
@@ -10704,7 +11006,7 @@ var methodsByCategory = {
10704
11006
  {
10705
11007
  name: "clone",
10706
11008
  category: "ComponentNode",
10707
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
11009
+ signature: "clone(): Promise<typeof this | null>",
10708
11010
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
10709
11011
  references: []
10710
11012
  },
@@ -10943,9 +11245,9 @@ var methodsByCategory = {
10943
11245
  {
10944
11246
  name: "clone",
10945
11247
  category: "DesignPageNode",
10946
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
10947
- description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
10948
- references: []
11248
+ signature: "clone(options?: DesignPageCloneOptions): Promise<this>",
11249
+ description: "Clone the DesignPageNode into a new one with the same content\nIf the given name already exists, the cloned page will be created with a unique name.",
11250
+ references: ["DesignPageCloneOptions"]
10949
11251
  },
10950
11252
  {
10951
11253
  name: "getChildren",
@@ -11457,7 +11759,7 @@ var methodsByCategory = {
11457
11759
  {
11458
11760
  name: "clone",
11459
11761
  category: "FrameNode",
11460
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
11762
+ signature: "clone(): Promise<typeof this | null>",
11461
11763
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
11462
11764
  references: []
11463
11765
  },
@@ -13046,7 +13348,7 @@ var methodsByCategory = {
13046
13348
  {
13047
13349
  name: "clone",
13048
13350
  category: "SVGNode",
13049
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
13351
+ signature: "clone(): Promise<typeof this | null>",
13050
13352
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
13051
13353
  references: []
13052
13354
  },
@@ -13265,7 +13567,7 @@ var methodsByCategory = {
13265
13567
  {
13266
13568
  name: "clone",
13267
13569
  category: "TextNode",
13268
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
13570
+ signature: "clone(): Promise<typeof this | null>",
13269
13571
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
13270
13572
  references: []
13271
13573
  },
@@ -13813,7 +14115,7 @@ var methodsByCategory = {
13813
14115
  {
13814
14116
  name: "clone",
13815
14117
  category: "UnknownNode",
13816
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
14118
+ signature: "clone(): Promise<typeof this | null>",
13817
14119
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
13818
14120
  references: []
13819
14121
  },
@@ -14012,7 +14314,7 @@ var methodsByCategory = {
14012
14314
  {
14013
14315
  name: "clone",
14014
14316
  category: "VectorSetItemNode",
14015
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
14317
+ signature: "clone(): Promise<typeof this | null>",
14016
14318
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
14017
14319
  references: []
14018
14320
  },
@@ -14182,7 +14484,7 @@ var methodsByCategory = {
14182
14484
  {
14183
14485
  name: "clone",
14184
14486
  category: "VectorSetNode",
14185
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
14487
+ signature: "clone(): Promise<typeof this | null>",
14186
14488
  description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
14187
14489
  references: []
14188
14490
  },
@@ -14303,9 +14605,9 @@ var methodsByCategory = {
14303
14605
  {
14304
14606
  name: "clone",
14305
14607
  category: "WebPageNode",
14306
- signature: 'clone(): Promise<(typeof this)[ClassKey] extends "UnknownNode" ? never : typeof this | null>',
14307
- description: 'Clone this node, creating a duplicate in the canvas tree.\n\n@returns The cloned node, or `null` if the clone failed.\n@throws If the node is an `UnknownNode`.\n\nUse `"Node.clone"` to check if this method is allowed.',
14308
- references: []
14608
+ signature: "clone(options?: WebPageCloneOptions): Promise<this>",
14609
+ description: "Clone the WebPageNode into a new one with the same content and settings, as a draft\nIf the given path already exists, the cloned page will be created with a unique path.",
14610
+ references: ["WebPageCloneOptions"]
14309
14611
  },
14310
14612
  {
14311
14613
  name: "collectionId",
@@ -14608,7 +14910,7 @@ ${typeDef}`);
14608
14910
  __name(renderDocs, "renderDocs");
14609
14911
  var __filename$1 = fileURLToPath(import.meta.url);
14610
14912
  var __dirname$1 = path3.dirname(__filename$1);
14611
- var VERSION = "0.0.8" ;
14913
+ var VERSION = "0.0.9" ;
14612
14914
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
14613
14915
  var client = createTRPCClient({
14614
14916
  links: [
@@ -14793,27 +15095,6 @@ function installSkills(options = { type: "base" }) {
14793
15095
  }
14794
15096
  __name(installSkills, "installSkills");
14795
15097
 
14796
- // src/utils.ts
14797
- function formatError(error) {
14798
- if (error instanceof Error) {
14799
- return error.message;
14800
- }
14801
- return String(error);
14802
- }
14803
- __name(formatError, "formatError");
14804
- function printJson(value) {
14805
- console.log(JSON.stringify(value, null, 2));
14806
- }
14807
- __name(printJson, "printJson");
14808
- function print(message) {
14809
- console.log(message);
14810
- }
14811
- __name(print, "print");
14812
- function printError(message) {
14813
- console.error(message);
14814
- }
14815
- __name(printError, "printError");
14816
-
14817
15098
  // src/cli.ts
14818
15099
  var program = new Command();
14819
15100
  program.name("framer").version(VERSION).description("Framer Server API CLI");
@@ -14892,22 +15173,18 @@ async function refreshSkillsFromSession(sessionId, projectId) {
14892
15173
  }
14893
15174
  }
14894
15175
  __name(refreshSkillsFromSession, "refreshSkillsFromSession");
14895
- function resolveSessionCredentials(projectUrlOrId, apiKey) {
15176
+ async function resolveSessionCredentials(projectUrlOrId) {
14896
15177
  try {
14897
15178
  const projectId = extractProjectId(projectUrlOrId);
14898
- if (apiKey) {
14899
- return { projectId, apiKey };
14900
- }
14901
15179
  const cachedApiKey = getApiKey(projectId);
14902
15180
  if (cachedApiKey) {
14903
15181
  return { projectId, apiKey: cachedApiKey };
14904
15182
  }
14905
- printError("No API key provided and none cached for this project.");
14906
- printError("");
14907
- printError("Usage: framer session new <projectUrl> <apiKey>");
14908
- process.exit(1);
15183
+ const newApiKey = await acquireKeyFromBrowser(projectId);
15184
+ saveProject({ projectId, apiKey: newApiKey });
15185
+ return { projectId, apiKey: newApiKey };
14909
15186
  } catch (err) {
14910
- printError(`Failed to create session: ${formatError(err)}`);
15187
+ printError(`Failed to resolve credentials: ${formatError(err)}`);
14911
15188
  process.exit(1);
14912
15189
  }
14913
15190
  }
@@ -14950,7 +15227,7 @@ program.option("-s, --session <id>", "Session ID (required for code execution)")
14950
15227
  if (!sessionId) {
14951
15228
  printError("Error: -s/--session is required.");
14952
15229
  printError(
14953
- "Run `framer session new <projectUrl> <apiKey>` first to get a session ID."
15230
+ "Run `framer session new <projectUrlOrId>` first to get a session ID."
14954
15231
  );
14955
15232
  process.exit(1);
14956
15233
  }
@@ -14974,9 +15251,11 @@ program.option("-s, --session <id>", "Session ID (required for code execution)")
14974
15251
  }
14975
15252
  });
14976
15253
  var session = program.command("session").description("Manage sessions");
14977
- session.command("new <projectUrlOrId> [apiKey]").description("Create a new session and print the session ID").action(async (projectUrlOrId, apiKey) => {
14978
- const credentials = resolveSessionCredentials(projectUrlOrId, apiKey);
14979
- await ensureRelayForCli();
15254
+ session.command("new <projectUrlOrId>").description("Create a new session and print the session ID").action(async (projectUrlOrId) => {
15255
+ const [credentials] = await Promise.all([
15256
+ resolveSessionCredentials(projectUrlOrId),
15257
+ ensureRelayForCli()
15258
+ ]);
14980
15259
  try {
14981
15260
  const result = await client.createSession.mutate({
14982
15261
  projectId: credentials.projectId,
@@ -14995,7 +15274,15 @@ session.command("new <projectUrlOrId> [apiKey]").description("Create a new sessi
14995
15274
  });
14996
15275
  print(sessionId);
14997
15276
  } catch (err) {
14998
- printError(`Failed to create session: ${formatError(err)}`);
15277
+ const message = formatError(err);
15278
+ printError(`Failed to create session: ${message}`);
15279
+ if (isAuthError(message)) {
15280
+ clearApiKey(credentials.projectId);
15281
+ printError("The stored API key was invalid and has been removed.");
15282
+ printError(
15283
+ `Please try creating a session again. This will start a browser authentication flow`
15284
+ );
15285
+ }
14999
15286
  process.exit(1);
15000
15287
  }
15001
15288
  });
@@ -15023,6 +15310,16 @@ project.command("list").description("List recently used projects").action(() =>
15023
15310
  }))
15024
15311
  );
15025
15312
  });
15313
+ project.command("auth <projectUrlOrId> [apiKey]").description("Authorize and save a project").action(async (projectUrlOrId, apiKey) => {
15314
+ const projectId = extractProjectId(projectUrlOrId);
15315
+ if (apiKey) {
15316
+ saveProject({ projectId, apiKey });
15317
+ } else {
15318
+ const credentials = await resolveSessionCredentials(projectUrlOrId);
15319
+ saveProject(credentials);
15320
+ }
15321
+ print(`Project ${projectId} saved`);
15322
+ });
15026
15323
  session.command("destroy <sessionId>").description("Destroy a session").action(async (sessionId) => {
15027
15324
  await ensureRelayForCli();
15028
15325
  try {
@@ -13,7 +13,7 @@ import { createRequire } from 'module';
13
13
  import * as vm from 'vm';
14
14
  import { connect } from 'framer-api';
15
15
 
16
- /* @framer/ai relay server v0.0.8 */
16
+ /* @framer/ai relay server v0.0.9 */
17
17
  var __defProp = Object.defineProperty;
18
18
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
19
19
  function getLogPath() {
@@ -50,7 +50,7 @@ function log(message) {
50
50
  __name(log, "log");
51
51
  var __filename$1 = fileURLToPath(import.meta.url);
52
52
  path.dirname(__filename$1);
53
- var VERSION = "0.0.8" ;
53
+ var VERSION = "0.0.9" ;
54
54
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
55
55
  createTRPCClient({
56
56
  links: [
@@ -85,6 +85,11 @@ function isConnectionError(errorMessage) {
85
85
  );
86
86
  }
87
87
  __name(isConnectionError, "isConnectionError");
88
+ var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
89
+ function isAuthError(errorMessage) {
90
+ return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
91
+ }
92
+ __name(isAuthError, "isAuthError");
88
93
  var ScopedFS = class {
89
94
  static {
90
95
  __name(this, "ScopedFS");
@@ -605,9 +610,17 @@ var appRouter = t.router({
605
610
  return sessionManager.list();
606
611
  }),
607
612
  createSession: t.procedure.input(z.object({ projectId: z.string(), apiKey: z.string() })).mutation(async ({ input }) => {
608
- const id = await sessionManager.create(input.projectId, input.apiKey);
609
- log(`session.new id=${id} project=${input.projectId}`);
610
- return { id };
613
+ try {
614
+ const id = await sessionManager.create(input.projectId, input.apiKey);
615
+ log(`session.new id=${id} project=${input.projectId}`);
616
+ return { id };
617
+ } catch (err) {
618
+ const message = err instanceof Error ? err.message : String(err);
619
+ throw new TRPCError({
620
+ code: isAuthError(message) ? "UNAUTHORIZED" : "INTERNAL_SERVER_ERROR",
621
+ message
622
+ });
623
+ }
611
624
  }),
612
625
  destroySession: t.procedure.input(z.object({ sessionId: z.string() })).mutation(async ({ input }) => {
613
626
  await sessionManager.destroy(input.sessionId);
@@ -45,8 +45,7 @@ Use that list to infer the likely project from the names and recency. If the rig
45
45
  Create a session:
46
46
 
47
47
  ```bash
48
- npx framer-dalton session new "<url or id>" # Uses cached API key
49
- npx framer-dalton session new "<url or id>" "<apiKey>" # First time: needs API key (Project Settings > General > API Keys)
48
+ npx framer-dalton session new "<url or id>"
50
49
  ```
51
50
 
52
51
  #### 2. Look up the API (before EVERY code execution)
@@ -113,7 +112,7 @@ Each session maintains a persistent connection to a Framer project. Use sessions
113
112
  Get a new session ID:
114
113
 
115
114
  ```bash
116
- npx framer-dalton session new "https://framer.com/projects/Website--abc123" "framer_api_xxx"
115
+ npx framer-dalton session new "https://framer.com/projects/Website--abc123"
117
116
  # outputs: 1
118
117
  ```
119
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-dalton",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "framer-dalton": "./dist/cli.js"