framer-dalton 0.0.9 → 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 CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import fs2 from 'fs';
2
3
  import path3 from 'path';
3
4
  import { Command } from 'commander';
4
5
  import crypto from 'crypto';
5
6
  import http from 'http';
6
7
  import { spawn, execFile } from 'child_process';
7
- import fs2 from 'fs';
8
8
  import os from 'os';
9
9
  import { z } from 'zod';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { createTRPCClient, httpLink } from '@trpc/client';
12
12
 
13
- /* @framer/ai CLI v0.0.9 */
13
+ /* @framer/ai CLI v0.0.10 */
14
14
  var __defProp = Object.defineProperty;
15
15
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
16
16
  function openUrl(url) {
@@ -14910,7 +14910,7 @@ ${typeDef}`);
14910
14910
  __name(renderDocs, "renderDocs");
14911
14911
  var __filename$1 = fileURLToPath(import.meta.url);
14912
14912
  var __dirname$1 = path3.dirname(__filename$1);
14913
- var VERSION = "0.0.9" ;
14913
+ var VERSION = "0.0.10" ;
14914
14914
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
14915
14915
  var client = createTRPCClient({
14916
14916
  links: [
@@ -15214,9 +15214,17 @@ async function ensureRelayForCli() {
15214
15214
  }
15215
15215
  }
15216
15216
  __name(ensureRelayForCli, "ensureRelayForCli");
15217
- 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) => {
15218
- 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;
15219
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
+ }
15220
15228
  if (!code && !process.stdin.isTTY) {
15221
15229
  code = await readStdin();
15222
15230
  }
@@ -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.9 */
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.9" ;
94
+ var VERSION = "0.0.10" ;
54
95
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
55
96
  createTRPCClient({
56
97
  links: [
@@ -486,7 +527,8 @@ var ConnectionPool = class {
486
527
  const connection = await connect(projectId, apiKey);
487
528
  this.pool.set(projectId, {
488
529
  connection,
489
- sessions: /* @__PURE__ */ new Set([session])
530
+ sessions: /* @__PURE__ */ new Set([session]),
531
+ connected: true
490
532
  });
491
533
  return connection;
492
534
  }
@@ -515,11 +557,26 @@ var ConnectionPool = class {
515
557
  if (!entry) return null;
516
558
  try {
517
559
  await entry.connection.reconnect();
560
+ entry.connected = true;
518
561
  return entry.connection;
519
562
  } catch {
520
563
  return null;
521
564
  }
522
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
+ }
523
580
  /**
524
581
  * Release a session from a connection.
525
582
  * If no sessions remain, the connection is disconnected and removed.
@@ -548,11 +605,14 @@ var ConnectionPool = class {
548
605
  var connectionPool = new ConnectionPool();
549
606
 
550
607
  // src/session-manager.ts
608
+ var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
609
+ var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
551
610
  var SessionManager = class {
552
611
  static {
553
612
  __name(this, "SessionManager");
554
613
  }
555
614
  sessions = /* @__PURE__ */ new Map();
615
+ idleCheck = null;
556
616
  async create(projectId, apiKey) {
557
617
  let id = 1;
558
618
  while (this.sessions.has(String(id))) {
@@ -562,9 +622,13 @@ var SessionManager = class {
562
622
  id: String(id),
563
623
  projectId,
564
624
  apiKey,
565
- state: {}
625
+ state: {},
626
+ lastActivityAt: 0,
627
+ inflight: 0
566
628
  };
629
+ this.startIdleCheck();
567
630
  await connectionPool.acquire(projectId, apiKey, session);
631
+ session.lastActivityAt = Date.now();
568
632
  this.sessions.set(String(id), session);
569
633
  return String(id);
570
634
  }
@@ -584,6 +648,21 @@ var SessionManager = class {
584
648
  async reconnect(session) {
585
649
  return connectionPool.reconnect(session.projectId);
586
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
+ }
587
666
  async destroy(id) {
588
667
  const session = this.sessions.get(id);
589
668
  if (!session) {
@@ -591,12 +670,50 @@ var SessionManager = class {
591
670
  }
592
671
  await connectionPool.release(session.projectId, session);
593
672
  this.sessions.delete(id);
673
+ if (this.sessions.size === 0) {
674
+ this.stopIdleCheck();
675
+ }
594
676
  }
595
677
  async destroyAll() {
596
678
  for (const id of this.sessions.keys()) {
597
679
  await this.destroy(id);
598
680
  }
599
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
+ }
600
717
  };
601
718
  var sessionManager = new SessionManager();
602
719
 
@@ -633,35 +750,43 @@ var appRouter = t.router({
633
750
  cwd: z.string().optional()
634
751
  })
635
752
  ).mutation(async ({ input }) => {
636
- const { sessionId, code, cwd } = input;
637
- const session = sessionManager.get(sessionId);
638
- if (!session) {
639
- throw new TRPCError({
640
- code: "NOT_FOUND",
641
- message: `Session ${sessionId} not found`
642
- });
643
- }
644
- log(
645
- `exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
646
- );
647
- const framer = sessionManager.getFramer(session);
648
- if (!framer) {
649
- return {
650
- output: [],
651
- error: "Failed to get connection for session"
652
- };
653
- }
654
- const result = await executeWithReconnect(
655
- session,
656
- framer,
657
- code,
658
- { cwd },
659
- () => sessionManager.reconnect(session)
660
- );
661
- if (result.error) {
662
- log(`exec.error session=${sessionId} error="${result.error}"`);
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);
663
789
  }
664
- return result;
665
790
  }),
666
791
  shutdown: t.procedure.mutation(() => {
667
792
  log("shutdown requested");
@@ -672,11 +797,25 @@ var appRouter = t.router({
672
797
  });
673
798
 
674
799
  // src/relay-server.ts
800
+ var IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
801
+ var IDLE_CHECK_INTERVAL_MS = 60 * 1e3;
675
802
  var trpcHandler = createHTTPHandler({ router: appRouter });
676
803
  async function startRelayServer(port = RELAY_PORT) {
804
+ let lastActivityAt = Date.now();
677
805
  const server = http.createServer((req, res) => {
806
+ lastActivityAt = Date.now();
678
807
  trpcHandler(req, res);
679
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();
680
819
  return new Promise((resolve, reject) => {
681
820
  server.on("error", reject);
682
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 -s <sessionId> <<'EOF'
32
- const { results } = await framer.readProjectForAgent(
33
- [{ type: "page", path: "/" }],
34
- { pagePath: "/" }
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
- console.log(results);
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 -s <sessionId> <<'EOF'
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
- await framer.applyAgentChanges(dsl, { pagePath: "/" });
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 and their controls, design tokens, style presets, and icon sets.
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}}
@@ -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:
@@ -59,12 +59,14 @@ npx framer-dalton docs Collection.getItems # What are the parameters and return
59
59
 
60
60
  #### 3. Execute code
61
61
 
62
- 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:
63
63
 
64
64
  ```bash
65
- npx framer-dalton -s 1 -e "state.collections = await framer.getCollections(); console.log(state.collections.length)"
65
+ npx framer-dalton -s 1 -f /tmp/framer-1.js
66
66
  ```
67
67
 
68
+ On Windows, use the equivalent temp directory (e.g. `%TEMP%\framer-1.js`).
69
+
68
70
  #### 4. Store results in `state`
69
71
 
70
72
  Always save results you'll need again. Don't repeat API calls.
@@ -90,13 +92,23 @@ Always save results you'll need again. Don't repeat API calls.
90
92
 
91
93
  **Always store results in `state` when you'll need them again.** API calls are slow - don't repeat them.
92
94
 
95
+ ```js
96
+ // /tmp/framer-1.js
97
+ state.collections = await framer.getCollections();
98
+ ```
99
+
93
100
  ```bash
94
- # First call: fetch and store
95
- npx framer-dalton -s 1 -e "state.collections = await framer.getCollections()"
101
+ npx framer-dalton -s 1 -f /tmp/framer-1.js
102
+ ```
96
103
 
97
- # Later calls: reuse from state
98
- npx framer-dalton -s 1 -e "const team = state.collections.find(c => c.name === 'Team')"
99
- npx framer-dalton -s 1 -e "state.teamItems = await state.collections.find(c => c.name === 'Team').getItems()"
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
100
112
  ```
101
113
 
102
114
  Store anything you'll reference again.
@@ -132,55 +144,10 @@ After session creation, load the dynamically created project-scoped skill `frame
132
144
 
133
145
  ## Execute Code
134
146
 
135
- ```bash
136
- npx framer-dalton -s <sessionId> -e "<code>"
137
- ```
138
-
139
- **Escaping:** For code containing `$` (e.g. `$control__` properties), HTML, or nested quotes, use a heredoc (see below). `-e "..."` works for everything else, including multiline.
140
-
141
- **Examples:**
142
-
143
- ```bash
144
- # Fetch collections and store in state (always store results you'll reuse)
145
- npx framer-dalton -s 1 -e "state.collections = await framer.getCollections(); console.log(state.collections.map(c => c.name))"
146
-
147
- # Use stored data in subsequent calls
148
- npx framer-dalton -s 1 -e "state.team = state.collections.find(c => c.name === 'Team')"
149
- npx framer-dalton -s 1 -e "state.teamItems = await state.team.getItems(); console.log(state.teamItems.length)"
150
- ```
151
-
152
- **Multiline code with heredoc (recommended for complex strings):**
153
-
154
- 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`:
155
148
 
156
149
  ```bash
157
- npx framer-dalton -s 1 <<'EOF'
158
- const translations = {
159
- "node-id": "<h2>Ship's Treasures</h2>",
160
- "other-id": "<p>Text with "quotes" and <tags></p>"
161
- };
162
- await framer.setLocalizationData({ valuesBySource: translations });
163
- EOF
164
- ```
165
-
166
- The `<<'EOF'` syntax (with quotes around EOF) prevents shell interpolation.
167
-
168
- **Alternative: pipe from file:**
169
-
170
- ```bash
171
- cat script.js | npx framer-dalton -s 1
172
- ```
173
-
174
- **Multiline inline code:**
175
-
176
- ```bash
177
- npx framer-dalton -s 1 -e "
178
- const collections = await framer.getCollections();
179
- for (const c of collections) {
180
- const items = await c.getItems();
181
- console.log(c.name, items.length);
182
- }
183
- "
150
+ npx framer-dalton -s <sessionId> -f /tmp/framer-<sessionId>.js
184
151
  ```
185
152
 
186
153
  ## API Documentation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-dalton",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "framer-dalton": "./dist/cli.js"