framer-dalton 0.0.2 → 0.0.5

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.
@@ -3,13 +3,17 @@ import os from 'os';
3
3
  import path from 'path';
4
4
  import 'child_process';
5
5
  import { fileURLToPath } from 'url';
6
+ import { createTRPCClient, httpLink } from '@trpc/client';
6
7
  import http from 'http';
8
+ import { createHTTPHandler } from '@trpc/server/adapters/standalone';
9
+ import { initTRPC, TRPCError } from '@trpc/server';
10
+ import { z } from 'zod';
7
11
  import crypto from 'crypto';
8
12
  import { createRequire } from 'module';
9
13
  import * as vm from 'vm';
10
14
  import { connect } from 'framer-api';
11
15
 
12
- /* @framer/ai relay server v0.0.2 */
16
+ /* @framer/ai relay server v0.0.5 */
13
17
  var __defProp = Object.defineProperty;
14
18
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
19
  function getLogPath() {
@@ -46,8 +50,15 @@ function log(message) {
46
50
  __name(log, "log");
47
51
  var __filename$1 = fileURLToPath(import.meta.url);
48
52
  path.dirname(__filename$1);
49
- var VERSION = "0.0.2" ;
50
- var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19987;
53
+ var VERSION = "0.0.5" ;
54
+ var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
55
+ createTRPCClient({
56
+ links: [
57
+ httpLink({
58
+ url: `http://127.0.0.1:${RELAY_PORT}`
59
+ })
60
+ ]
61
+ });
51
62
 
52
63
  // src/connection-errors.ts
53
64
  var CONNECTION_ERROR_PATTERNS = [
@@ -92,14 +103,14 @@ var ScopedFS = class {
92
103
  resolvePath(filePath) {
93
104
  const resolved = path.resolve(filePath);
94
105
  if (!this.isPathAllowed(resolved)) {
95
- const error2 = new Error(
106
+ const error = new Error(
96
107
  `EPERM: operation not permitted, access outside allowed directories: ${filePath}`
97
108
  );
98
- error2.code = "EPERM";
99
- error2.errno = -1;
100
- error2.syscall = "access";
101
- error2.path = filePath;
102
- throw error2;
109
+ error.code = "EPERM";
110
+ error.errno = -1;
111
+ error.syscall = "access";
112
+ error.path = filePath;
113
+ throw error;
103
114
  }
104
115
  return resolved;
105
116
  }
@@ -255,8 +266,8 @@ var ScopedFS = class {
255
266
  constants = fs2.constants;
256
267
  };
257
268
 
258
- // src/executor.ts
259
- var DEFAULT_TIMEOUT = 3e4;
269
+ // src/execute.ts
270
+ var EXECUTION_TIMEOUT = 10 * 60 * 1e3;
260
271
  var baseRequire = createRequire(import.meta.url);
261
272
  var ALLOWED_MODULES = /* @__PURE__ */ new Set([
262
273
  "path",
@@ -279,11 +290,11 @@ var ALLOWED_MODULES = /* @__PURE__ */ new Set([
279
290
  function createSandboxedRequire(scopedFs) {
280
291
  const sandboxedRequire = /* @__PURE__ */ __name(((id) => {
281
292
  if (!ALLOWED_MODULES.has(id)) {
282
- const error2 = new Error(
293
+ const error = new Error(
283
294
  `Module "${id}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
284
295
  );
285
- error2.name = "ModuleNotAllowedError";
286
- throw error2;
296
+ error.name = "ModuleNotAllowedError";
297
+ throw error;
287
298
  }
288
299
  if (id === "fs" || id === "node:fs") {
289
300
  return scopedFs;
@@ -302,11 +313,11 @@ function createSandboxedRequire(scopedFs) {
302
313
  __name(createSandboxedRequire, "createSandboxedRequire");
303
314
  async function sandboxedImport(scopedFs, specifier) {
304
315
  if (!ALLOWED_MODULES.has(specifier)) {
305
- const error2 = new Error(
316
+ const error = new Error(
306
317
  `Module "${specifier}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
307
318
  );
308
- error2.name = "ModuleNotAllowedError";
309
- throw error2;
319
+ error.name = "ModuleNotAllowedError";
320
+ throw error;
310
321
  }
311
322
  if (specifier === "fs" || specifier === "node:fs") {
312
323
  return scopedFs;
@@ -317,8 +328,8 @@ async function sandboxedImport(scopedFs, specifier) {
317
328
  return import(specifier);
318
329
  }
319
330
  __name(sandboxedImport, "sandboxedImport");
320
- async function execute(session, connection, code, options = {}) {
321
- const { timeout = DEFAULT_TIMEOUT, cwd } = options;
331
+ async function execute(session, framer, code, options = {}) {
332
+ const { cwd } = options;
322
333
  const output = [];
323
334
  const customConsole = {
324
335
  log: /* @__PURE__ */ __name((...args) => {
@@ -334,11 +345,11 @@ async function execute(session, connection, code, options = {}) {
334
345
  output.push(args.map((arg) => formatValue(arg)).join(" "));
335
346
  }, "info")
336
347
  };
337
- const scopedFs = cwd ? new ScopedFS([cwd, "/tmp"]) : new ScopedFS();
348
+ const scopedFs = cwd ? new ScopedFS([cwd, "/tmp", os.tmpdir()]) : new ScopedFS();
338
349
  const sandboxedRequire = createSandboxedRequire(scopedFs);
339
350
  const vmContextObj = {
340
351
  // Framer API
341
- framer: connection,
352
+ framer,
342
353
  state: session.state,
343
354
  // Console
344
355
  console: customConsole,
@@ -365,22 +376,24 @@ async function execute(session, connection, code, options = {}) {
365
376
  };
366
377
  const vmContext = vm.createContext(vmContextObj);
367
378
  const wrappedCode = `(async () => { ${code} })()`;
379
+ let timeoutId;
368
380
  try {
369
381
  const script = new vm.Script(wrappedCode, {
370
382
  filename: "framer-exec.js"
371
383
  });
372
384
  const resultPromise = script.runInContext(vmContext, {
373
385
  timeout: 5e3
374
- // Short timeout for sync part
375
386
  });
376
387
  const result = await Promise.race([
377
388
  resultPromise,
378
- new Promise(
379
- (_, reject) => setTimeout(
380
- () => reject(new Error(`Execution timed out after ${timeout}ms`)),
381
- timeout
382
- )
383
- )
389
+ new Promise((_, reject) => {
390
+ timeoutId = setTimeout(
391
+ () => reject(
392
+ new Error(`Execution timed out after ${EXECUTION_TIMEOUT}ms`)
393
+ ),
394
+ EXECUTION_TIMEOUT
395
+ );
396
+ })
384
397
  ]);
385
398
  if (result !== void 0) {
386
399
  output.push(formatValue(result));
@@ -392,9 +405,42 @@ async function execute(session, connection, code, options = {}) {
392
405
  output,
393
406
  error: errorMessage
394
407
  };
408
+ } finally {
409
+ if (timeoutId) clearTimeout(timeoutId);
395
410
  }
396
411
  }
397
412
  __name(execute, "execute");
413
+ async function executeWithReconnect(session, framer, code, options, reconnect) {
414
+ const result = await tryExecute(session, framer, code, options);
415
+ if (!result.error || !isConnectionError(result.error)) {
416
+ return result;
417
+ }
418
+ log(
419
+ `reconnect session=${session.id} project=${session.projectId} reason="${result.error}"`
420
+ );
421
+ const newFramer = await reconnect();
422
+ if (!newFramer) {
423
+ log(
424
+ `reconnect.failed session=${session.id} error="no connection returned"`
425
+ );
426
+ return {
427
+ output: [],
428
+ error: "Connection lost and failed to reconnect"
429
+ };
430
+ }
431
+ log(`reconnect.success session=${session.id}`);
432
+ return tryExecute(session, newFramer, code, options);
433
+ }
434
+ __name(executeWithReconnect, "executeWithReconnect");
435
+ async function tryExecute(session, framer, code, options) {
436
+ try {
437
+ return await execute(session, framer, code, options);
438
+ } catch (err) {
439
+ const error = err instanceof Error ? err.message : String(err);
440
+ return { output: [], error };
441
+ }
442
+ }
443
+ __name(tryExecute, "tryExecute");
398
444
  function formatValue(value) {
399
445
  if (value === null) return "null";
400
446
  if (value === void 0) return "undefined";
@@ -420,6 +466,7 @@ var ConnectionPool = class {
420
466
  __name(this, "ConnectionPool");
421
467
  }
422
468
  pool = /* @__PURE__ */ new Map();
469
+ reconnectPromises = /* @__PURE__ */ new Map();
423
470
  /**
424
471
  * Acquire a connection for a session.
425
472
  * If a connection already exists for the project, the session is added to it.
@@ -447,15 +494,26 @@ var ConnectionPool = class {
447
494
  }
448
495
  /**
449
496
  * Reconnect a project's connection (call after catching a connection error).
450
- * Uses the same connection object but swaps the underlying WebSocket.
497
+ * Concurrent callers for the same project share a single reconnect attempt.
451
498
  */
452
499
  async reconnect(projectId) {
500
+ const existingPromise = this.reconnectPromises.get(projectId);
501
+ if (existingPromise) return existingPromise;
502
+ const promise = this.doReconnect(projectId).finally(() => {
503
+ this.reconnectPromises.delete(projectId);
504
+ });
505
+ this.reconnectPromises.set(projectId, promise);
506
+ return promise;
507
+ }
508
+ async doReconnect(projectId) {
453
509
  const entry = this.pool.get(projectId);
454
- if (!entry) {
510
+ if (!entry) return null;
511
+ try {
512
+ await entry.connection.reconnect();
513
+ return entry.connection;
514
+ } catch {
455
515
  return null;
456
516
  }
457
- await entry.connection.reconnect();
458
- return entry.connection;
459
517
  }
460
518
  /**
461
519
  * Release a session from a connection.
@@ -515,7 +573,7 @@ var SessionManager = class {
515
573
  get(id) {
516
574
  return this.sessions.get(id);
517
575
  }
518
- getConnection(session) {
576
+ getFramer(session) {
519
577
  return connectionPool.getConnection(session.projectId);
520
578
  }
521
579
  async reconnect(session) {
@@ -537,144 +595,79 @@ var SessionManager = class {
537
595
  };
538
596
  var sessionManager = new SessionManager();
539
597
 
598
+ // src/router.ts
599
+ var t = initTRPC.create();
600
+ var appRouter = t.router({
601
+ version: t.procedure.query(() => {
602
+ return { version: VERSION };
603
+ }),
604
+ listSessions: t.procedure.query(() => {
605
+ return sessionManager.list();
606
+ }),
607
+ 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 };
611
+ }),
612
+ destroySession: t.procedure.input(z.object({ sessionId: z.string() })).mutation(async ({ input }) => {
613
+ await sessionManager.destroy(input.sessionId);
614
+ log(`session.destroy id=${input.sessionId}`);
615
+ }),
616
+ exec: t.procedure.input(
617
+ z.object({
618
+ sessionId: z.string(),
619
+ code: z.string(),
620
+ cwd: z.string().optional()
621
+ })
622
+ ).mutation(async ({ input }) => {
623
+ const { sessionId, code, cwd } = input;
624
+ const session = sessionManager.get(sessionId);
625
+ if (!session) {
626
+ throw new TRPCError({
627
+ code: "NOT_FOUND",
628
+ message: `Session ${sessionId} not found`
629
+ });
630
+ }
631
+ log(
632
+ `exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
633
+ );
634
+ const framer = sessionManager.getFramer(session);
635
+ if (!framer) {
636
+ return {
637
+ output: [],
638
+ error: "Failed to get connection for session"
639
+ };
640
+ }
641
+ const result = await executeWithReconnect(
642
+ session,
643
+ framer,
644
+ code,
645
+ { cwd },
646
+ () => sessionManager.reconnect(session)
647
+ );
648
+ if (result.error) {
649
+ log(`exec.error session=${sessionId} error="${result.error}"`);
650
+ }
651
+ return result;
652
+ }),
653
+ shutdown: t.procedure.mutation(() => {
654
+ log("shutdown requested");
655
+ setTimeout(() => {
656
+ process.exit(0);
657
+ }, 100);
658
+ })
659
+ });
660
+
540
661
  // src/relay-server.ts
541
- function json(res, data, status = 200) {
542
- res.writeHead(status, { "Content-Type": "application/json" });
543
- res.end(JSON.stringify(data));
544
- }
545
- __name(json, "json");
546
- function error(res, message, status = 400) {
547
- json(res, { error: message }, status);
548
- }
549
- __name(error, "error");
550
- async function readBody(req) {
551
- return new Promise((resolve, reject) => {
552
- let body = "";
553
- req.on("data", (chunk) => {
554
- body += chunk;
555
- });
556
- req.on("end", () => {
557
- try {
558
- resolve(body ? JSON.parse(body) : {});
559
- } catch {
560
- reject(new Error("Invalid JSON body"));
561
- }
562
- });
563
- req.on("error", reject);
564
- });
565
- }
566
- __name(readBody, "readBody");
662
+ var trpcHandler = createHTTPHandler({ router: appRouter });
567
663
  async function startRelayServer(port = RELAY_PORT) {
568
- const server = http.createServer(async (req, res) => {
569
- const url = req.url || "/";
570
- try {
571
- if (req.method === "GET" && url === "/version") {
572
- return json(res, { version: VERSION });
573
- }
574
- if (req.method === "GET" && url === "/cli/sessions") {
575
- const sessions = sessionManager.list();
576
- return json(res, { sessions });
577
- }
578
- if (req.method === "POST" && url === "/cli/session/new") {
579
- const body = await readBody(req);
580
- const projectId = body.projectId;
581
- const apiKey = body.apiKey;
582
- if (!projectId || !apiKey) {
583
- return error(res, "projectId and apiKey are required");
584
- }
585
- const id = await sessionManager.create(projectId, apiKey);
586
- log(`session.new id=${id} project=${projectId}`);
587
- return json(res, { id });
588
- }
589
- if (req.method === "POST" && url === "/cli/session/destroy") {
590
- const body = await readBody(req);
591
- const sessionId = body.sessionId ? String(body.sessionId) : "";
592
- if (!sessionId) {
593
- return error(res, "sessionId is required");
594
- }
595
- await sessionManager.destroy(sessionId);
596
- log(`session.destroy id=${sessionId}`);
597
- return json(res, { success: true });
598
- }
599
- if (req.method === "POST" && url === "/cli/exec") {
600
- const body = await readBody(req);
601
- const sessionId = String(body.sessionId);
602
- const code = body.code;
603
- const timeout = body.timeout || 3e4;
604
- const cwd = body.cwd;
605
- if (!sessionId) {
606
- return error(res, "sessionId is required");
607
- }
608
- if (!code) {
609
- return error(res, "code is required");
610
- }
611
- const session = sessionManager.get(sessionId);
612
- if (!session) {
613
- return error(res, `Session ${sessionId} not found`, 404);
614
- }
615
- log(
616
- `exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
617
- );
618
- const connection = sessionManager.getConnection(session);
619
- if (!connection) {
620
- return json(res, {
621
- output: [],
622
- error: "Failed to get connection for session"
623
- });
624
- }
625
- let result;
626
- try {
627
- result = await execute(session, connection, code, { timeout, cwd });
628
- } catch (execErr) {
629
- const errMsg = execErr instanceof Error ? execErr.message : String(execErr);
630
- result = { output: [], error: errMsg };
631
- }
632
- if (result.error && isConnectionError(result.error)) {
633
- log(
634
- `reconnect session=${sessionId} project=${session.projectId} reason="${result.error}"`
635
- );
636
- const newConnection = await sessionManager.reconnect(session);
637
- if (newConnection) {
638
- try {
639
- result = await execute(session, newConnection, code, {
640
- timeout,
641
- cwd
642
- });
643
- log(`reconnect.success session=${sessionId}`);
644
- } catch (retryErr) {
645
- const errMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
646
- log(`reconnect.failed session=${sessionId} error="${errMsg}"`);
647
- result = { output: [], error: errMsg };
648
- }
649
- } else {
650
- log(
651
- `reconnect.failed session=${sessionId} error="no connection returned"`
652
- );
653
- }
654
- }
655
- if (result.error) {
656
- log(`exec.error session=${sessionId} error="${result.error}"`);
657
- }
658
- return json(res, result);
659
- }
660
- if (req.method === "POST" && url === "/shutdown") {
661
- log("shutdown requested");
662
- json(res, { success: true });
663
- setTimeout(() => {
664
- process.exit(0);
665
- }, 100);
666
- return;
667
- }
668
- error(res, "Not found", 404);
669
- } catch (err) {
670
- const message = err instanceof Error ? err.message : String(err);
671
- log(`error: ${message}`);
672
- error(res, message, 500);
673
- }
664
+ const server = http.createServer((req, res) => {
665
+ trpcHandler(req, res);
674
666
  });
675
667
  return new Promise((resolve, reject) => {
676
668
  server.on("error", reject);
677
669
  server.listen(port, "127.0.0.1", () => {
670
+ log(`started v${VERSION} on port ${port}`);
678
671
  resolve(server);
679
672
  });
680
673
  });
@@ -0,0 +1,6 @@
1
+ ## All Skills
2
+
3
+ - `npx framer-dalton skill` — Server API docs (sessions, collections, canvas, styles, publishing)
4
+ - `npx framer-dalton skill code-components` — Code component authoring guide
5
+ - `npx framer-dalton skill property-controls` — Property control reference (all 20+ types)
6
+ - `npx framer-dalton skill component-examples` — Complete code component examples
@@ -0,0 +1,115 @@
1
+ # Framer Code Components
2
+
3
+ ## Best Practices
4
+
5
+ ### Component Structure
6
+
7
+ ```tsx
8
+ import { addPropertyControls, ControlType } from "framer";
9
+ import { motion } from "framer-motion"; // NOT from "framer"
10
+
11
+ interface MyComponentProps {
12
+ /* typed props */
13
+ }
14
+
15
+ /**
16
+ * @framerSupportedLayoutWidth any-prefer-fixed
17
+ * @framerSupportedLayoutHeight any-prefer-fixed
18
+ */
19
+ export default function MyComponent(props: MyComponentProps) {
20
+ // component
21
+ }
22
+
23
+ addPropertyControls(MyComponent, {
24
+ /* controls */
25
+ });
26
+ ```
27
+
28
+ ### Platform Constraints
29
+
30
+ These will cause errors if violated:
31
+
32
+ 1. **Single file, default export** - Use named `function` syntax (not arrow functions), no named exports
33
+ 2. **Imports** - Only `react`, `react-dom`, `framer`, `framer-motion`. Import `motion` from `"framer-motion"`, not `"framer"`
34
+ 3. **Position** - Use `position: relative` on the root element, never `fixed`
35
+ 4. **SSR** - Guard `window`/`document` access: `if (typeof window !== "undefined")`
36
+ 5. **Annotations** - Include `@framerSupportedLayoutWidth/Height` in a `/** */` block comment immediately above the component function
37
+ 6. **Types** - Provide a typed props interface (e.g. `MyComponentProps`). Avoid NodeJS types like `Timeout` — use `number` instead
38
+
39
+ ### Layout Annotations
40
+
41
+ | Content | Width | Height |
42
+ | ----------------- | ------------------ | ------------------ |
43
+ | No intrinsic size | `fixed` | `fixed` |
44
+ | Text/auto-sizing | `auto` | `auto` |
45
+ | Flexible | `any-prefer-fixed` | `any-prefer-fixed` |
46
+
47
+ Detect auto vs fixed sizing: check if `style.width` or `style.height` is `"100%"`.
48
+
49
+ ### Property Controls
50
+
51
+ To make components configurable in Framer's properties panel, add property controls:
52
+
53
+ - To make colors customizable, use `ControlType.Color`. Reuse the same prop for elements sharing a color.
54
+ - To make text styling customizable, use `ControlType.Font` with `controls: "extended"` and `defaultFontType: "sans-serif"`.
55
+ - For images, use `ControlType.ResponsiveImage`. Set defaults in the component body via destructuring (the control doesn't support `defaultValue`).
56
+ - Provide a `defaultValue` for every prop so components render correctly in the Framer canvas. Include at least one item in `ControlType.Array` controls.
57
+ - `ComponentName.defaultProps` is not supported in Framer — use `defaultValue` on the property control instead.
58
+ - Use `hidden` for conditional visibility: `hidden: (props) => !props.showFeature`
59
+ - Prefer sliders over steppers unless step values are large.
60
+ - Keep controls focused — make key elements configurable, hardcode the rest.
61
+ - See `npx framer-dalton skill property-controls` for the full property control reference.
62
+
63
+ ### Image Defaults (in component body)
64
+
65
+ ```tsx
66
+ const {
67
+ image = {
68
+ src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
69
+ alt: "Default",
70
+ },
71
+ } = props;
72
+ ```
73
+
74
+ ### Animation Performance
75
+
76
+ ```tsx
77
+ import { useIsStaticRenderer } from "framer";
78
+ import { useInView } from "framer-motion";
79
+
80
+ const isStatic = useIsStaticRenderer();
81
+ const ref = useRef(null);
82
+ const isInView = useInView(ref);
83
+
84
+ if (isStatic) return <StaticPreview />; // Show useful static state
85
+ // Pause animations when out of viewport
86
+ ```
87
+
88
+ - For very complex animations, consider WebGL instead of `framer-motion`.
89
+ - Static preview should include visual effects, not just text.
90
+ - Wrapping state updates in `startTransition()` prevents UI blocking and keeps interactions smooth.
91
+
92
+ ### Text
93
+
94
+ - For auto-sized components with text, apply `width: max-content` or `minWidth: max-content` to prevent text from collapsing.
95
+
96
+ ### Common Errors
97
+
98
+ - WebGL cross-origin: handle `SecurityError: Failed to execute 'texImage2D'` for cross-origin images.
99
+ - Inverted Y-axis: check if WebGL images render upside down and accommodate.
100
+
101
+ ### Accessibility
102
+
103
+ - `aria` roles on interactive elements
104
+ - Semantic HTML (`<nav>`, `<article>`, `<section>`)
105
+ - `alt=""` on decorative images
106
+ - 4.5:1 color contrast
107
+
108
+ ## Term Interpretation
109
+
110
+ - "responsive" → width/height 100%
111
+ - "modern" → 8px radius, 16px spacing, subtle shadows
112
+ - "minimal" → limited colors, whitespace
113
+ - "interactive" → hover/active states
114
+ - "accessible" → ARIA, semantic HTML
115
+ - "props"/"properties" → Framer property controls