framer-dalton 0.0.8 → 0.0.10
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 +362 -57
- package/dist/start-relay-server.js +187 -35
- package/docs/skills/framer-canvas-editing-project.md +16 -16
- package/docs/skills/framer.md +24 -58
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs2 from 'fs';
|
|
2
3
|
import path3 from 'path';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
|
-
import
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import { spawn, execFile } from 'child_process';
|
|
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.
|
|
13
|
+
/* @framer/ai CLI v0.0.10 */
|
|
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:
|
|
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:
|
|
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:
|
|
10947
|
-
description:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
14307
|
-
description:
|
|
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.
|
|
14913
|
+
var VERSION = "0.0.10" ;
|
|
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
|
|
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
|
-
|
|
14906
|
-
|
|
14907
|
-
|
|
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
|
|
15187
|
+
printError(`Failed to resolve credentials: ${formatError(err)}`);
|
|
14911
15188
|
process.exit(1);
|
|
14912
15189
|
}
|
|
14913
15190
|
}
|
|
@@ -14937,9 +15214,17 @@ async function ensureRelayForCli() {
|
|
|
14937
15214
|
}
|
|
14938
15215
|
}
|
|
14939
15216
|
__name(ensureRelayForCli, "ensureRelayForCli");
|
|
14940
|
-
program.option("-s, --session <id>", "Session ID (required for code execution)").option("-e, --eval <code>", "Code to execute (or pipe via stdin)").action(async (options) => {
|
|
14941
|
-
const { session: sessionId, eval: evalCode } = options;
|
|
15217
|
+
program.option("-s, --session <id>", "Session ID (required for code execution)").option("-e, --eval <code>", "Code to execute (or pipe via stdin)").option("-f, --file <path>", "File containing code to execute").action(async (options) => {
|
|
15218
|
+
const { session: sessionId, eval: evalCode, file: filePath } = options;
|
|
14942
15219
|
let code = evalCode;
|
|
15220
|
+
if (!code && filePath) {
|
|
15221
|
+
try {
|
|
15222
|
+
code = fs2.readFileSync(filePath, "utf-8");
|
|
15223
|
+
} catch (err) {
|
|
15224
|
+
printError(`Failed to read file: ${formatError(err)}`);
|
|
15225
|
+
process.exit(1);
|
|
15226
|
+
}
|
|
15227
|
+
}
|
|
14943
15228
|
if (!code && !process.stdin.isTTY) {
|
|
14944
15229
|
code = await readStdin();
|
|
14945
15230
|
}
|
|
@@ -14950,7 +15235,7 @@ program.option("-s, --session <id>", "Session ID (required for code execution)")
|
|
|
14950
15235
|
if (!sessionId) {
|
|
14951
15236
|
printError("Error: -s/--session is required.");
|
|
14952
15237
|
printError(
|
|
14953
|
-
"Run `framer session new <
|
|
15238
|
+
"Run `framer session new <projectUrlOrId>` first to get a session ID."
|
|
14954
15239
|
);
|
|
14955
15240
|
process.exit(1);
|
|
14956
15241
|
}
|
|
@@ -14974,9 +15259,11 @@ program.option("-s, --session <id>", "Session ID (required for code execution)")
|
|
|
14974
15259
|
}
|
|
14975
15260
|
});
|
|
14976
15261
|
var session = program.command("session").description("Manage sessions");
|
|
14977
|
-
session.command("new <projectUrlOrId>
|
|
14978
|
-
const credentials =
|
|
14979
|
-
|
|
15262
|
+
session.command("new <projectUrlOrId>").description("Create a new session and print the session ID").action(async (projectUrlOrId) => {
|
|
15263
|
+
const [credentials] = await Promise.all([
|
|
15264
|
+
resolveSessionCredentials(projectUrlOrId),
|
|
15265
|
+
ensureRelayForCli()
|
|
15266
|
+
]);
|
|
14980
15267
|
try {
|
|
14981
15268
|
const result = await client.createSession.mutate({
|
|
14982
15269
|
projectId: credentials.projectId,
|
|
@@ -14995,7 +15282,15 @@ session.command("new <projectUrlOrId> [apiKey]").description("Create a new sessi
|
|
|
14995
15282
|
});
|
|
14996
15283
|
print(sessionId);
|
|
14997
15284
|
} catch (err) {
|
|
14998
|
-
|
|
15285
|
+
const message = formatError(err);
|
|
15286
|
+
printError(`Failed to create session: ${message}`);
|
|
15287
|
+
if (isAuthError(message)) {
|
|
15288
|
+
clearApiKey(credentials.projectId);
|
|
15289
|
+
printError("The stored API key was invalid and has been removed.");
|
|
15290
|
+
printError(
|
|
15291
|
+
`Please try creating a session again. This will start a browser authentication flow`
|
|
15292
|
+
);
|
|
15293
|
+
}
|
|
14999
15294
|
process.exit(1);
|
|
15000
15295
|
}
|
|
15001
15296
|
});
|
|
@@ -15023,6 +15318,16 @@ project.command("list").description("List recently used projects").action(() =>
|
|
|
15023
15318
|
}))
|
|
15024
15319
|
);
|
|
15025
15320
|
});
|
|
15321
|
+
project.command("auth <projectUrlOrId> [apiKey]").description("Authorize and save a project").action(async (projectUrlOrId, apiKey) => {
|
|
15322
|
+
const projectId = extractProjectId(projectUrlOrId);
|
|
15323
|
+
if (apiKey) {
|
|
15324
|
+
saveProject({ projectId, apiKey });
|
|
15325
|
+
} else {
|
|
15326
|
+
const credentials = await resolveSessionCredentials(projectUrlOrId);
|
|
15327
|
+
saveProject(credentials);
|
|
15328
|
+
}
|
|
15329
|
+
print(`Project ${projectId} saved`);
|
|
15330
|
+
});
|
|
15026
15331
|
session.command("destroy <sessionId>").description("Destroy a session").action(async (sessionId) => {
|
|
15027
15332
|
await ensureRelayForCli();
|
|
15028
15333
|
try {
|
|
@@ -13,9 +13,50 @@ 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.
|
|
16
|
+
/* @framer/ai relay server v0.0.10 */
|
|
17
17
|
var __defProp = Object.defineProperty;
|
|
18
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
|
|
19
|
+
var __typeError = (msg) => {
|
|
20
|
+
throw TypeError(msg);
|
|
21
|
+
};
|
|
18
22
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
23
|
+
var __using = (stack, value, async) => {
|
|
24
|
+
if (value != null) {
|
|
25
|
+
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
26
|
+
var dispose, inner;
|
|
27
|
+
if (dispose === void 0) {
|
|
28
|
+
dispose = value[__knownSymbol("dispose")];
|
|
29
|
+
}
|
|
30
|
+
if (typeof dispose !== "function") __typeError("Object not disposable");
|
|
31
|
+
if (inner) dispose = function() {
|
|
32
|
+
try {
|
|
33
|
+
inner.call(this);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return Promise.reject(e);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
stack.push([async, dispose, value]);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
var __callDispose = (stack, error, hasError) => {
|
|
43
|
+
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
44
|
+
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
45
|
+
};
|
|
46
|
+
var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
|
|
47
|
+
var next = (it) => {
|
|
48
|
+
while (it = stack.pop()) {
|
|
49
|
+
try {
|
|
50
|
+
var result = it[1] && it[1].call(it[2]);
|
|
51
|
+
if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
fail(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (hasError) throw error;
|
|
57
|
+
};
|
|
58
|
+
return next();
|
|
59
|
+
};
|
|
19
60
|
function getLogPath() {
|
|
20
61
|
if (process.env.XDG_STATE_HOME) {
|
|
21
62
|
return path.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
|
|
@@ -50,7 +91,7 @@ function log(message) {
|
|
|
50
91
|
__name(log, "log");
|
|
51
92
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
52
93
|
path.dirname(__filename$1);
|
|
53
|
-
var VERSION = "0.0.
|
|
94
|
+
var VERSION = "0.0.10" ;
|
|
54
95
|
var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
|
|
55
96
|
createTRPCClient({
|
|
56
97
|
links: [
|
|
@@ -85,6 +126,11 @@ function isConnectionError(errorMessage) {
|
|
|
85
126
|
);
|
|
86
127
|
}
|
|
87
128
|
__name(isConnectionError, "isConnectionError");
|
|
129
|
+
var AUTH_ERROR_PATTERNS = ["does not have access", "UNAUTHORIZED"];
|
|
130
|
+
function isAuthError(errorMessage) {
|
|
131
|
+
return AUTH_ERROR_PATTERNS.some((p) => errorMessage.includes(p));
|
|
132
|
+
}
|
|
133
|
+
__name(isAuthError, "isAuthError");
|
|
88
134
|
var ScopedFS = class {
|
|
89
135
|
static {
|
|
90
136
|
__name(this, "ScopedFS");
|
|
@@ -481,7 +527,8 @@ var ConnectionPool = class {
|
|
|
481
527
|
const connection = await connect(projectId, apiKey);
|
|
482
528
|
this.pool.set(projectId, {
|
|
483
529
|
connection,
|
|
484
|
-
sessions: /* @__PURE__ */ new Set([session])
|
|
530
|
+
sessions: /* @__PURE__ */ new Set([session]),
|
|
531
|
+
connected: true
|
|
485
532
|
});
|
|
486
533
|
return connection;
|
|
487
534
|
}
|
|
@@ -510,11 +557,26 @@ var ConnectionPool = class {
|
|
|
510
557
|
if (!entry) return null;
|
|
511
558
|
try {
|
|
512
559
|
await entry.connection.reconnect();
|
|
560
|
+
entry.connected = true;
|
|
513
561
|
return entry.connection;
|
|
514
562
|
} catch {
|
|
515
563
|
return null;
|
|
516
564
|
}
|
|
517
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Disconnect a project's connection without removing sessions.
|
|
568
|
+
* The next exec will trigger a reconnect via executeWithReconnect.
|
|
569
|
+
*/
|
|
570
|
+
async disconnect(projectId) {
|
|
571
|
+
const entry = this.pool.get(projectId);
|
|
572
|
+
if (!entry || !entry.connected) return;
|
|
573
|
+
entry.connected = false;
|
|
574
|
+
await entry.connection.disconnect();
|
|
575
|
+
}
|
|
576
|
+
isConnected(projectId) {
|
|
577
|
+
const entry = this.pool.get(projectId);
|
|
578
|
+
return entry?.connected ?? false;
|
|
579
|
+
}
|
|
518
580
|
/**
|
|
519
581
|
* Release a session from a connection.
|
|
520
582
|
* If no sessions remain, the connection is disconnected and removed.
|
|
@@ -543,11 +605,14 @@ var ConnectionPool = class {
|
|
|
543
605
|
var connectionPool = new ConnectionPool();
|
|
544
606
|
|
|
545
607
|
// src/session-manager.ts
|
|
608
|
+
var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
|
|
609
|
+
var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
|
|
546
610
|
var SessionManager = class {
|
|
547
611
|
static {
|
|
548
612
|
__name(this, "SessionManager");
|
|
549
613
|
}
|
|
550
614
|
sessions = /* @__PURE__ */ new Map();
|
|
615
|
+
idleCheck = null;
|
|
551
616
|
async create(projectId, apiKey) {
|
|
552
617
|
let id = 1;
|
|
553
618
|
while (this.sessions.has(String(id))) {
|
|
@@ -557,9 +622,13 @@ var SessionManager = class {
|
|
|
557
622
|
id: String(id),
|
|
558
623
|
projectId,
|
|
559
624
|
apiKey,
|
|
560
|
-
state: {}
|
|
625
|
+
state: {},
|
|
626
|
+
lastActivityAt: 0,
|
|
627
|
+
inflight: 0
|
|
561
628
|
};
|
|
629
|
+
this.startIdleCheck();
|
|
562
630
|
await connectionPool.acquire(projectId, apiKey, session);
|
|
631
|
+
session.lastActivityAt = Date.now();
|
|
563
632
|
this.sessions.set(String(id), session);
|
|
564
633
|
return String(id);
|
|
565
634
|
}
|
|
@@ -579,6 +648,21 @@ var SessionManager = class {
|
|
|
579
648
|
async reconnect(session) {
|
|
580
649
|
return connectionPool.reconnect(session.projectId);
|
|
581
650
|
}
|
|
651
|
+
/** Marks session as actively executing. Dispose to release. */
|
|
652
|
+
exec(id) {
|
|
653
|
+
const session = this.sessions.get(id);
|
|
654
|
+
if (session) {
|
|
655
|
+
session.inflight++;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
[Symbol.dispose]: () => {
|
|
659
|
+
if (session) {
|
|
660
|
+
session.inflight--;
|
|
661
|
+
session.lastActivityAt = Date.now();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
582
666
|
async destroy(id) {
|
|
583
667
|
const session = this.sessions.get(id);
|
|
584
668
|
if (!session) {
|
|
@@ -586,12 +670,50 @@ var SessionManager = class {
|
|
|
586
670
|
}
|
|
587
671
|
await connectionPool.release(session.projectId, session);
|
|
588
672
|
this.sessions.delete(id);
|
|
673
|
+
if (this.sessions.size === 0) {
|
|
674
|
+
this.stopIdleCheck();
|
|
675
|
+
}
|
|
589
676
|
}
|
|
590
677
|
async destroyAll() {
|
|
591
678
|
for (const id of this.sessions.keys()) {
|
|
592
679
|
await this.destroy(id);
|
|
593
680
|
}
|
|
594
681
|
}
|
|
682
|
+
startIdleCheck() {
|
|
683
|
+
if (this.idleCheck) return;
|
|
684
|
+
this.idleCheck = setInterval(() => {
|
|
685
|
+
this.reapIdleSessions().catch((err) => {
|
|
686
|
+
log(`reap error: ${err instanceof Error ? err.message : err}`);
|
|
687
|
+
});
|
|
688
|
+
}, SESSION_IDLE_CHECK_INTERVAL_MS);
|
|
689
|
+
this.idleCheck.unref();
|
|
690
|
+
}
|
|
691
|
+
stopIdleCheck() {
|
|
692
|
+
if (this.idleCheck) {
|
|
693
|
+
clearInterval(this.idleCheck);
|
|
694
|
+
this.idleCheck = null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async reapIdleSessions() {
|
|
698
|
+
const now = Date.now();
|
|
699
|
+
const disconnects = [];
|
|
700
|
+
for (const session of this.sessions.values()) {
|
|
701
|
+
if (session.inflight > 0) continue;
|
|
702
|
+
if (now - session.lastActivityAt < SESSION_IDLE_TIMEOUT_MS) continue;
|
|
703
|
+
if (!connectionPool.isConnected(session.projectId)) continue;
|
|
704
|
+
const { projectId } = session;
|
|
705
|
+
log(`idle disconnect project=${projectId}`);
|
|
706
|
+
disconnects.push(connectionPool.disconnect(projectId));
|
|
707
|
+
}
|
|
708
|
+
const results = await Promise.allSettled(disconnects);
|
|
709
|
+
for (const result of results) {
|
|
710
|
+
if (result.status === "rejected") {
|
|
711
|
+
log(
|
|
712
|
+
`disconnect error: ${result.reason instanceof Error ? result.reason.message : result.reason}`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
595
717
|
};
|
|
596
718
|
var sessionManager = new SessionManager();
|
|
597
719
|
|
|
@@ -605,9 +727,17 @@ var appRouter = t.router({
|
|
|
605
727
|
return sessionManager.list();
|
|
606
728
|
}),
|
|
607
729
|
createSession: t.procedure.input(z.object({ projectId: z.string(), apiKey: z.string() })).mutation(async ({ input }) => {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
730
|
+
try {
|
|
731
|
+
const id = await sessionManager.create(input.projectId, input.apiKey);
|
|
732
|
+
log(`session.new id=${id} project=${input.projectId}`);
|
|
733
|
+
return { id };
|
|
734
|
+
} catch (err) {
|
|
735
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
736
|
+
throw new TRPCError({
|
|
737
|
+
code: isAuthError(message) ? "UNAUTHORIZED" : "INTERNAL_SERVER_ERROR",
|
|
738
|
+
message
|
|
739
|
+
});
|
|
740
|
+
}
|
|
611
741
|
}),
|
|
612
742
|
destroySession: t.procedure.input(z.object({ sessionId: z.string() })).mutation(async ({ input }) => {
|
|
613
743
|
await sessionManager.destroy(input.sessionId);
|
|
@@ -620,35 +750,43 @@ var appRouter = t.router({
|
|
|
620
750
|
cwd: z.string().optional()
|
|
621
751
|
})
|
|
622
752
|
).mutation(async ({ input }) => {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
753
|
+
var _stack = [];
|
|
754
|
+
try {
|
|
755
|
+
const { sessionId, code, cwd } = input;
|
|
756
|
+
const session = sessionManager.get(sessionId);
|
|
757
|
+
if (!session) {
|
|
758
|
+
throw new TRPCError({
|
|
759
|
+
code: "NOT_FOUND",
|
|
760
|
+
message: `Session ${sessionId} not found`
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
const _guard = __using(_stack, sessionManager.exec(sessionId));
|
|
764
|
+
log(
|
|
765
|
+
`exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
|
|
766
|
+
);
|
|
767
|
+
const framer = sessionManager.getFramer(session);
|
|
768
|
+
if (!framer) {
|
|
769
|
+
return {
|
|
770
|
+
output: [],
|
|
771
|
+
error: "Failed to get connection for session"
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const result = await executeWithReconnect(
|
|
775
|
+
session,
|
|
776
|
+
framer,
|
|
777
|
+
code,
|
|
778
|
+
{ cwd },
|
|
779
|
+
() => sessionManager.reconnect(session)
|
|
780
|
+
);
|
|
781
|
+
if (result.error) {
|
|
782
|
+
log(`exec.error session=${sessionId} error="${result.error}"`);
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
785
|
+
} catch (_) {
|
|
786
|
+
var _error = _, _hasError = true;
|
|
787
|
+
} finally {
|
|
788
|
+
__callDispose(_stack, _error, _hasError);
|
|
650
789
|
}
|
|
651
|
-
return result;
|
|
652
790
|
}),
|
|
653
791
|
shutdown: t.procedure.mutation(() => {
|
|
654
792
|
log("shutdown requested");
|
|
@@ -659,11 +797,25 @@ var appRouter = t.router({
|
|
|
659
797
|
});
|
|
660
798
|
|
|
661
799
|
// src/relay-server.ts
|
|
800
|
+
var IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
|
|
801
|
+
var IDLE_CHECK_INTERVAL_MS = 60 * 1e3;
|
|
662
802
|
var trpcHandler = createHTTPHandler({ router: appRouter });
|
|
663
803
|
async function startRelayServer(port = RELAY_PORT) {
|
|
804
|
+
let lastActivityAt = Date.now();
|
|
664
805
|
const server = http.createServer((req, res) => {
|
|
806
|
+
lastActivityAt = Date.now();
|
|
665
807
|
trpcHandler(req, res);
|
|
666
808
|
});
|
|
809
|
+
const idleCheck = setInterval(() => {
|
|
810
|
+
const idleMs = Date.now() - lastActivityAt;
|
|
811
|
+
if (idleMs >= IDLE_TIMEOUT_MS) {
|
|
812
|
+
log(`idle for ${Math.round(idleMs / 1e3)}s, shutting down`);
|
|
813
|
+
clearInterval(idleCheck);
|
|
814
|
+
server.close();
|
|
815
|
+
process.exit(0);
|
|
816
|
+
}
|
|
817
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
818
|
+
idleCheck.unref();
|
|
667
819
|
return new Promise((resolve, reject) => {
|
|
668
820
|
server.on("error", reject);
|
|
669
821
|
server.listen(port, "127.0.0.1", () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: {{SKILL_NAME}}
|
|
3
3
|
description: "Project-scoped Framer canvas editing skill for project {{PROJECT_ID}}. Very important: never load this skill without having already read the `framer` skill and without having already run `session new`, which will dynamically update this skill."
|
|
4
|
-
allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)"]
|
|
4
|
+
allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)", "Write(/tmp/framer-*)"]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Project Scope
|
|
@@ -27,26 +27,26 @@ allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)"]
|
|
|
27
27
|
## Workflow Loop
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
# 1) Read page structure first
|
|
31
|
-
framer
|
|
32
|
-
const { results } = await framer.readProjectForAgent(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
);
|
|
30
|
+
# 1) Read page structure first — write code to /tmp/framer-<sessionId>.js, then execute with -f
|
|
31
|
+
# /tmp/framer-<sessionId>.js:
|
|
32
|
+
# const { results } = await framer.readProjectForAgent(
|
|
33
|
+
# [{ type: "page", path: "/" }],
|
|
34
|
+
# { pagePath: "/" }
|
|
35
|
+
# );
|
|
36
|
+
# console.log(results);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
EOF
|
|
38
|
+
framer -s <sessionId> -f /tmp/framer-<sessionId>.js
|
|
39
39
|
|
|
40
40
|
# 2) Request additional targeted queries only if needed
|
|
41
41
|
|
|
42
42
|
# 3) Apply changes in a later call, once `dsl` has been prepared
|
|
43
|
-
framer
|
|
44
|
-
const dsl = `
|
|
45
|
-
...your canvas DSL...
|
|
46
|
-
`;
|
|
43
|
+
# /tmp/framer-<sessionId>.js:
|
|
44
|
+
# const dsl = `
|
|
45
|
+
# ...your canvas DSL...
|
|
46
|
+
# `;
|
|
47
|
+
# await framer.applyAgentChanges(dsl, { pagePath: "/" });
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
EOF
|
|
49
|
+
framer -s <sessionId> -f /tmp/framer-<sessionId>.js
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Live Agent System Prompt
|
|
@@ -61,6 +61,6 @@ This is the static canvas-editing prompt returned by `framer.getAgentSystemPromp
|
|
|
61
61
|
|
|
62
62
|
## Live Agent Context (/)
|
|
63
63
|
|
|
64
|
-
This is the dynamic project context returned by `framer.getAgentContext({ pagePath: "/" })`. It contains project-specific data for the current page, including available fonts, components
|
|
64
|
+
This is the dynamic project context returned by `framer.getAgentContext({ pagePath: "/" })`. It contains project-specific data for the current page, including available fonts, available components, design tokens, style presets, and icon sets.
|
|
65
65
|
|
|
66
66
|
{{AGENT_CONTEXT}}
|
package/docs/skills/framer.md
CHANGED
|
@@ -3,7 +3,7 @@ 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
5
|
**Mandatory precondition**: run `npx framer-dalton@latest setup` and let it complete **BEFORE** loading this skill.
|
|
6
|
-
allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)"]
|
|
6
|
+
allowed-tools: ["Bash(npx framer-dalton:*)", "Bash(npx framer-dalton@latest:*)", "Write(/tmp/framer-*)"]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
If you didn't run this command before loading the skill, run it now:
|
|
@@ -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>"
|
|
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)
|
|
@@ -60,12 +59,14 @@ npx framer-dalton docs Collection.getItems # What are the parameters and return
|
|
|
60
59
|
|
|
61
60
|
#### 3. Execute code
|
|
62
61
|
|
|
63
|
-
Only after checking docs:
|
|
62
|
+
Only after checking docs, write your code to a `framer-<sessionId>.js` file in the OS temp directory and execute with `-f`. Use the session ID in the filename to avoid collisions between parallel sessions:
|
|
64
63
|
|
|
65
64
|
```bash
|
|
66
|
-
npx framer-dalton -s 1 -
|
|
65
|
+
npx framer-dalton -s 1 -f /tmp/framer-1.js
|
|
67
66
|
```
|
|
68
67
|
|
|
68
|
+
On Windows, use the equivalent temp directory (e.g. `%TEMP%\framer-1.js`).
|
|
69
|
+
|
|
69
70
|
#### 4. Store results in `state`
|
|
70
71
|
|
|
71
72
|
Always save results you'll need again. Don't repeat API calls.
|
|
@@ -91,13 +92,23 @@ Always save results you'll need again. Don't repeat API calls.
|
|
|
91
92
|
|
|
92
93
|
**Always store results in `state` when you'll need them again.** API calls are slow - don't repeat them.
|
|
93
94
|
|
|
95
|
+
```js
|
|
96
|
+
// /tmp/framer-1.js
|
|
97
|
+
state.collections = await framer.getCollections();
|
|
98
|
+
```
|
|
99
|
+
|
|
94
100
|
```bash
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
npx framer-dalton -s 1 -f /tmp/framer-1.js
|
|
102
|
+
```
|
|
97
103
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
```js
|
|
105
|
+
// /tmp/framer-1.js — reuse from state
|
|
106
|
+
state.teamItems = await state.collections.find(c => c.name === 'Team').getItems();
|
|
107
|
+
console.log(state.teamItems.length);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx framer-dalton -s 1 -f /tmp/framer-1.js
|
|
101
112
|
```
|
|
102
113
|
|
|
103
114
|
Store anything you'll reference again.
|
|
@@ -113,7 +124,7 @@ Each session maintains a persistent connection to a Framer project. Use sessions
|
|
|
113
124
|
Get a new session ID:
|
|
114
125
|
|
|
115
126
|
```bash
|
|
116
|
-
npx framer-dalton session new "https://framer.com/projects/Website--abc123"
|
|
127
|
+
npx framer-dalton session new "https://framer.com/projects/Website--abc123"
|
|
117
128
|
# outputs: 1
|
|
118
129
|
```
|
|
119
130
|
|
|
@@ -133,55 +144,10 @@ After session creation, load the dynamically created project-scoped skill `frame
|
|
|
133
144
|
|
|
134
145
|
## Execute Code
|
|
135
146
|
|
|
136
|
-
|
|
137
|
-
npx framer-dalton -s <sessionId> -e "<code>"
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**Escaping:** For code containing `$` (e.g. `$control__` properties), HTML, or nested quotes, use a heredoc (see below). `-e "..."` works for everything else, including multiline.
|
|
141
|
-
|
|
142
|
-
**Examples:**
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
# Fetch collections and store in state (always store results you'll reuse)
|
|
146
|
-
npx framer-dalton -s 1 -e "state.collections = await framer.getCollections(); console.log(state.collections.map(c => c.name))"
|
|
147
|
-
|
|
148
|
-
# Use stored data in subsequent calls
|
|
149
|
-
npx framer-dalton -s 1 -e "state.team = state.collections.find(c => c.name === 'Team')"
|
|
150
|
-
npx framer-dalton -s 1 -e "state.teamItems = await state.team.getItems(); console.log(state.teamItems.length)"
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Multiline code with heredoc (recommended for complex strings):**
|
|
154
|
-
|
|
155
|
-
For code containing HTML, quotes, or special characters, use a heredoc to avoid escaping issues:
|
|
147
|
+
Write your code to `framer-<sessionId>.js` in the OS temp directory and execute with `-f`:
|
|
156
148
|
|
|
157
149
|
```bash
|
|
158
|
-
npx framer-dalton -s
|
|
159
|
-
const translations = {
|
|
160
|
-
"node-id": "<h2>Ship's Treasures</h2>",
|
|
161
|
-
"other-id": "<p>Text with "quotes" and <tags></p>"
|
|
162
|
-
};
|
|
163
|
-
await framer.setLocalizationData({ valuesBySource: translations });
|
|
164
|
-
EOF
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
The `<<'EOF'` syntax (with quotes around EOF) prevents shell interpolation.
|
|
168
|
-
|
|
169
|
-
**Alternative: pipe from file:**
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
cat script.js | npx framer-dalton -s 1
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
**Multiline inline code:**
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
npx framer-dalton -s 1 -e "
|
|
179
|
-
const collections = await framer.getCollections();
|
|
180
|
-
for (const c of collections) {
|
|
181
|
-
const items = await c.getItems();
|
|
182
|
-
console.log(c.name, items.length);
|
|
183
|
-
}
|
|
184
|
-
"
|
|
150
|
+
npx framer-dalton -s <sessionId> -f /tmp/framer-<sessionId>.js
|
|
185
151
|
```
|
|
186
152
|
|
|
187
153
|
## API Documentation
|