framer-dalton 0.0.9 → 0.0.11

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 path3 from 'path';
2
+ import fs3 from 'fs';
3
+ import path4 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.11 */
14
14
  var __defProp = Object.defineProperty;
15
15
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
16
16
  function openUrl(url) {
@@ -36,20 +36,20 @@ function openUrl(url) {
36
36
  __name(openUrl, "openUrl");
37
37
  function getConfigDir() {
38
38
  if (process.env.XDG_CONFIG_HOME) {
39
- return path3.join(process.env.XDG_CONFIG_HOME, "framer");
39
+ return path4.join(process.env.XDG_CONFIG_HOME, "framer");
40
40
  }
41
41
  if (process.platform === "win32") {
42
- return path3.join(process.env.APPDATA || os.homedir(), "framer");
42
+ return path4.join(process.env.APPDATA || os.homedir(), "framer");
43
43
  }
44
- return path3.join(os.homedir(), ".config", "framer");
44
+ return path4.join(os.homedir(), ".config", "framer");
45
45
  }
46
46
  __name(getConfigDir, "getConfigDir");
47
47
  function getProjectsConfigPath() {
48
- return path3.join(getConfigDir(), "projects.json");
48
+ return path4.join(getConfigDir(), "projects.json");
49
49
  }
50
50
  __name(getProjectsConfigPath, "getProjectsConfigPath");
51
51
  function getLegacyCredentialsPath() {
52
- return path3.join(getConfigDir(), "credentials.json");
52
+ return path4.join(getConfigDir(), "credentials.json");
53
53
  }
54
54
  __name(getLegacyCredentialsPath, "getLegacyCredentialsPath");
55
55
  var ProjectsConfigSchema = z.object({
@@ -65,11 +65,11 @@ var ProjectsConfigSchema = z.object({
65
65
  });
66
66
  var LegacyCredentialsSchema = z.record(z.string(), z.string());
67
67
  function readJsonFile(filePath) {
68
- if (!fs2.existsSync(filePath)) {
68
+ if (!fs3.existsSync(filePath)) {
69
69
  return null;
70
70
  }
71
71
  try {
72
- return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
72
+ return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
73
73
  } catch (_error) {
74
74
  return null;
75
75
  }
@@ -77,14 +77,14 @@ function readJsonFile(filePath) {
77
77
  __name(readJsonFile, "readJsonFile");
78
78
  function ensureConfigDir() {
79
79
  const configDir = getConfigDir();
80
- if (!fs2.existsSync(configDir)) {
81
- fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
80
+ if (!fs3.existsSync(configDir)) {
81
+ fs3.mkdirSync(configDir, { recursive: true, mode: 448 });
82
82
  }
83
83
  }
84
84
  __name(ensureConfigDir, "ensureConfigDir");
85
85
  function writeProjectsConfig(config) {
86
86
  ensureConfigDir();
87
- fs2.writeFileSync(
87
+ fs3.writeFileSync(
88
88
  getProjectsConfigPath(),
89
89
  JSON.stringify(config, null, " "),
90
90
  {
@@ -111,7 +111,7 @@ function migrateLegacyCredentials() {
111
111
  }
112
112
  const config = { version: 2, projects };
113
113
  writeProjectsConfig(config);
114
- fs2.rmSync(getLegacyCredentialsPath(), { force: true });
114
+ fs3.rmSync(getLegacyCredentialsPath(), { force: true });
115
115
  return config;
116
116
  }
117
117
  __name(migrateLegacyCredentials, "migrateLegacyCredentials");
@@ -124,7 +124,7 @@ function readProjectsConfig() {
124
124
  return result.data;
125
125
  }
126
126
  }
127
- if (fs2.existsSync(getLegacyCredentialsPath())) {
127
+ if (fs3.existsSync(getLegacyCredentialsPath())) {
128
128
  return migrateLegacyCredentials();
129
129
  }
130
130
  return { version: 2, projects: {} };
@@ -14909,8 +14909,8 @@ ${typeDef}`);
14909
14909
  }
14910
14910
  __name(renderDocs, "renderDocs");
14911
14911
  var __filename$1 = fileURLToPath(import.meta.url);
14912
- var __dirname$1 = path3.dirname(__filename$1);
14913
- var VERSION = "0.0.9" ;
14912
+ var __dirname$1 = path4.dirname(__filename$1);
14913
+ var VERSION = "0.0.11" ;
14914
14914
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
14915
14915
  var client = createTRPCClient({
14916
14916
  links: [
@@ -14972,7 +14972,7 @@ async function ensureRelayServerRunning(options = {}) {
14972
14972
  logger?.log("Relay server not running, starting it...");
14973
14973
  }
14974
14974
  const isRunningFromSource = __filename$1.endsWith(".ts");
14975
- const scriptPath = isRunningFromSource ? path3.resolve(__dirname$1, "./start-relay-server.ts") : path3.resolve(__dirname$1, "./start-relay-server.js");
14975
+ const scriptPath = isRunningFromSource ? path4.resolve(__dirname$1, "./start-relay-server.ts") : path4.resolve(__dirname$1, "./start-relay-server.js");
14976
14976
  const serverProcess = spawn(
14977
14977
  isRunningFromSource ? "tsx" : process.execPath,
14978
14978
  [scriptPath],
@@ -14994,16 +14994,37 @@ async function ensureRelayServerRunning(options = {}) {
14994
14994
  throw new Error("Failed to start relay server after 5 seconds");
14995
14995
  }
14996
14996
  __name(ensureRelayServerRunning, "ensureRelayServerRunning");
14997
+ var FRAMER_TEMPORARY_DIR = path4.join(os.tmpdir(), "framer");
14998
+ function ensureTemporaryDir() {
14999
+ fs3.mkdirSync(FRAMER_TEMPORARY_DIR, { recursive: true });
15000
+ }
15001
+ __name(ensureTemporaryDir, "ensureTemporaryDir");
15002
+ function isTemporaryFile(filePath) {
15003
+ const absolutePath = path4.resolve(filePath);
15004
+ const isInTemporaryDir = absolutePath.startsWith(
15005
+ FRAMER_TEMPORARY_DIR + path4.sep
15006
+ );
15007
+ const isFile = fs3.statSync(absolutePath, { throwIfNoEntry: false })?.isFile() ?? false;
15008
+ return isInTemporaryDir && isFile;
15009
+ }
15010
+ __name(isTemporaryFile, "isTemporaryFile");
15011
+ function removeTemporaryFile(filePath) {
15012
+ fs3.unlinkSync(filePath);
15013
+ }
15014
+ __name(removeTemporaryFile, "removeTemporaryFile");
15015
+
15016
+ // src/skills.ts
14997
15017
  var META_SKILL_NAME = "framer";
14998
15018
  var CODE_COMPONENTS_SKILL_NAME = "framer-code-components";
14999
- var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
15000
- var skillsDocsDir = path3.join(__dirname2, "..", "docs", "skills");
15019
+ var __dirname2 = path4.dirname(fileURLToPath(import.meta.url));
15020
+ var skillsDocsDir = path4.join(__dirname2, "..", "docs", "skills");
15001
15021
  function readSkillDoc(name) {
15002
- return fs2.readFileSync(path3.join(skillsDocsDir, name), "utf-8").trimEnd();
15022
+ return fs3.readFileSync(path4.join(skillsDocsDir, name), "utf-8").trimEnd();
15003
15023
  }
15004
15024
  __name(readSkillDoc, "readSkillDoc");
15005
15025
  function buildMetaSkill() {
15006
- return `${readSkillDoc("framer.md")}
15026
+ const template = readSkillDoc("framer.md");
15027
+ return `${renderTemplate(template, { FRAMER_TEMPORARY_DIR })}
15007
15028
  `;
15008
15029
  }
15009
15030
  __name(buildMetaSkill, "buildMetaSkill");
@@ -15033,36 +15054,37 @@ function buildProjectCanvasSkill(projectId, agentContext, canvasPrompt) {
15033
15054
  PROJECT_ID: projectId,
15034
15055
  GENERATED_AT: (/* @__PURE__ */ new Date()).toISOString(),
15035
15056
  CANVAS_PROMPT: canvasPrompt.trimEnd(),
15036
- AGENT_CONTEXT: agentContext.trimEnd()
15057
+ AGENT_CONTEXT: agentContext.trimEnd(),
15058
+ FRAMER_TEMPORARY_DIR
15037
15059
  })}
15038
15060
  `;
15039
15061
  return { skillName, content };
15040
15062
  }
15041
15063
  __name(buildProjectCanvasSkill, "buildProjectCanvasSkill");
15042
15064
  function writeSkill(root, skillName, content) {
15043
- fs2.mkdirSync(root, { recursive: true });
15044
- const rootStat = fs2.statSync(root);
15065
+ fs3.mkdirSync(root, { recursive: true });
15066
+ const rootStat = fs3.statSync(root);
15045
15067
  if (!rootStat.isDirectory()) {
15046
15068
  throw new Error(`Skill root is not a directory: ${root}`);
15047
15069
  }
15048
- const skillDir = path3.join(root, skillName);
15049
- const filePath = path3.join(skillDir, "SKILL.md");
15050
- if (fs2.existsSync(skillDir)) {
15051
- const current = fs2.lstatSync(skillDir);
15070
+ const skillDir = path4.join(root, skillName);
15071
+ const filePath = path4.join(skillDir, "SKILL.md");
15072
+ if (fs3.existsSync(skillDir)) {
15073
+ const current = fs3.lstatSync(skillDir);
15052
15074
  if (current.isSymbolicLink() || !current.isDirectory()) {
15053
- fs2.rmSync(skillDir, { recursive: true, force: true });
15075
+ fs3.rmSync(skillDir, { recursive: true, force: true });
15054
15076
  }
15055
15077
  }
15056
- fs2.mkdirSync(skillDir, { recursive: true });
15057
- fs2.writeFileSync(filePath, content, "utf-8");
15078
+ fs3.mkdirSync(skillDir, { recursive: true });
15079
+ fs3.writeFileSync(filePath, content, "utf-8");
15058
15080
  return filePath;
15059
15081
  }
15060
15082
  __name(writeSkill, "writeSkill");
15061
15083
  function getDefaultSkillRoots() {
15062
15084
  const home = os.homedir();
15063
15085
  return [
15064
- path3.join(home, ".agents", "skills"),
15065
- path3.join(home, ".claude", "skills")
15086
+ path4.join(home, ".agents", "skills"),
15087
+ path4.join(home, ".claude", "skills")
15066
15088
  ];
15067
15089
  }
15068
15090
  __name(getDefaultSkillRoots, "getDefaultSkillRoots");
@@ -15111,8 +15133,8 @@ function printSetupSummary(results) {
15111
15133
  const installLocations = /* @__PURE__ */ new Set();
15112
15134
  for (const result of results) {
15113
15135
  for (const filePath of result.paths) {
15114
- const skillDir = path3.dirname(filePath);
15115
- const root = path3.dirname(skillDir);
15136
+ const skillDir = path4.dirname(filePath);
15137
+ const root = path4.dirname(skillDir);
15116
15138
  installLocations.add(root);
15117
15139
  }
15118
15140
  }
@@ -15214,9 +15236,21 @@ async function ensureRelayForCli() {
15214
15236
  }
15215
15237
  }
15216
15238
  __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;
15239
+ 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) => {
15240
+ const { session: sessionId, eval: evalCode, file: filePath } = options;
15241
+ ensureTemporaryDir();
15219
15242
  let code = evalCode;
15243
+ if (!code && filePath) {
15244
+ try {
15245
+ code = fs3.readFileSync(filePath, "utf-8");
15246
+ } catch (err) {
15247
+ printError(`Failed to read file: ${formatError(err)}`);
15248
+ process.exit(1);
15249
+ }
15250
+ if (isTemporaryFile(filePath)) {
15251
+ removeTemporaryFile(filePath);
15252
+ }
15253
+ }
15220
15254
  if (!code && !process.stdin.isTTY) {
15221
15255
  code = await readStdin();
15222
15256
  }
@@ -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.11 */
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.11" ;
54
95
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
55
96
  createTRPCClient({
56
97
  links: [
@@ -415,25 +456,24 @@ async function execute(session, framer, code, options = {}) {
415
456
  }
416
457
  }
417
458
  __name(execute, "execute");
418
- async function executeWithReconnect(session, framer, code, options, reconnect) {
459
+ async function executeWithReconnect(session, framer, code, options, reconnect, execId) {
419
460
  const result = await tryExecute(session, framer, code, options);
420
461
  if (!result.error || !isConnectionError(result.error)) {
421
462
  return result;
422
463
  }
423
- log(
424
- `reconnect session=${session.id} project=${session.projectId} reason="${result.error}"`
425
- );
464
+ const reqId = framer.requestId;
465
+ const tag = `exec=${execId} session=${session.id}${reqId ? ` req=${reqId}` : ""}`;
466
+ log(`reconnect ${tag} project=${session.projectId} reason="${result.error}"`);
426
467
  const newFramer = await reconnect();
427
468
  if (!newFramer) {
428
- log(
429
- `reconnect.failed session=${session.id} error="no connection returned"`
430
- );
469
+ log(`reconnect.failed ${tag} error="no connection returned"`);
431
470
  return {
432
471
  output: [],
433
472
  error: "Connection lost and failed to reconnect"
434
473
  };
435
474
  }
436
- log(`reconnect.success session=${session.id}`);
475
+ const newReqId = newFramer.requestId;
476
+ log(`reconnect.success ${tag}${newReqId ? ` new_req=${newReqId}` : ""}`);
437
477
  return tryExecute(session, newFramer, code, options);
438
478
  }
439
479
  __name(executeWithReconnect, "executeWithReconnect");
@@ -481,12 +521,17 @@ var ConnectionPool = class {
481
521
  const entry = this.pool.get(projectId);
482
522
  if (entry) {
483
523
  entry.sessions.add(session);
524
+ if (!entry.connected) {
525
+ await entry.connection.reconnect();
526
+ entry.connected = true;
527
+ }
484
528
  return entry.connection;
485
529
  }
486
530
  const connection = await connect(projectId, apiKey);
487
531
  this.pool.set(projectId, {
488
532
  connection,
489
- sessions: /* @__PURE__ */ new Set([session])
533
+ sessions: /* @__PURE__ */ new Set([session]),
534
+ connected: true
490
535
  });
491
536
  return connection;
492
537
  }
@@ -515,11 +560,26 @@ var ConnectionPool = class {
515
560
  if (!entry) return null;
516
561
  try {
517
562
  await entry.connection.reconnect();
563
+ entry.connected = true;
518
564
  return entry.connection;
519
565
  } catch {
520
566
  return null;
521
567
  }
522
568
  }
569
+ /**
570
+ * Disconnect a project's connection without removing sessions.
571
+ * The next exec will trigger a reconnect via executeWithReconnect.
572
+ */
573
+ async disconnect(projectId) {
574
+ const entry = this.pool.get(projectId);
575
+ if (!entry || !entry.connected) return;
576
+ entry.connected = false;
577
+ await entry.connection.disconnect();
578
+ }
579
+ isConnected(projectId) {
580
+ const entry = this.pool.get(projectId);
581
+ return entry?.connected ?? false;
582
+ }
523
583
  /**
524
584
  * Release a session from a connection.
525
585
  * If no sessions remain, the connection is disconnected and removed.
@@ -548,11 +608,14 @@ var ConnectionPool = class {
548
608
  var connectionPool = new ConnectionPool();
549
609
 
550
610
  // src/session-manager.ts
611
+ var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
612
+ var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
551
613
  var SessionManager = class {
552
614
  static {
553
615
  __name(this, "SessionManager");
554
616
  }
555
617
  sessions = /* @__PURE__ */ new Map();
618
+ idleCheck = null;
556
619
  async create(projectId, apiKey) {
557
620
  let id = 1;
558
621
  while (this.sessions.has(String(id))) {
@@ -562,9 +625,13 @@ var SessionManager = class {
562
625
  id: String(id),
563
626
  projectId,
564
627
  apiKey,
565
- state: {}
628
+ state: {},
629
+ lastActivityAt: 0,
630
+ inflight: 0
566
631
  };
632
+ this.startIdleCheck();
567
633
  await connectionPool.acquire(projectId, apiKey, session);
634
+ session.lastActivityAt = Date.now();
568
635
  this.sessions.set(String(id), session);
569
636
  return String(id);
570
637
  }
@@ -581,9 +648,27 @@ var SessionManager = class {
581
648
  getFramer(session) {
582
649
  return connectionPool.getConnection(session.projectId);
583
650
  }
651
+ isConnected(session) {
652
+ return connectionPool.isConnected(session.projectId);
653
+ }
584
654
  async reconnect(session) {
585
655
  return connectionPool.reconnect(session.projectId);
586
656
  }
657
+ /** Marks session as actively executing. Dispose to release. */
658
+ exec(id) {
659
+ const session = this.sessions.get(id);
660
+ if (session) {
661
+ session.inflight++;
662
+ }
663
+ return {
664
+ [Symbol.dispose]: () => {
665
+ if (session) {
666
+ session.inflight--;
667
+ session.lastActivityAt = Date.now();
668
+ }
669
+ }
670
+ };
671
+ }
587
672
  async destroy(id) {
588
673
  const session = this.sessions.get(id);
589
674
  if (!session) {
@@ -591,17 +676,66 @@ var SessionManager = class {
591
676
  }
592
677
  await connectionPool.release(session.projectId, session);
593
678
  this.sessions.delete(id);
679
+ if (this.sessions.size === 0) {
680
+ this.stopIdleCheck();
681
+ }
594
682
  }
595
683
  async destroyAll() {
596
684
  for (const id of this.sessions.keys()) {
597
685
  await this.destroy(id);
598
686
  }
599
687
  }
688
+ startIdleCheck() {
689
+ if (this.idleCheck) return;
690
+ this.idleCheck = setInterval(() => {
691
+ this.reapIdleSessions().catch((err) => {
692
+ log(`reap error: ${err instanceof Error ? err.message : err}`);
693
+ });
694
+ }, SESSION_IDLE_CHECK_INTERVAL_MS);
695
+ this.idleCheck.unref();
696
+ }
697
+ stopIdleCheck() {
698
+ if (this.idleCheck) {
699
+ clearInterval(this.idleCheck);
700
+ this.idleCheck = null;
701
+ }
702
+ }
703
+ async reapIdleSessions() {
704
+ const now = Date.now();
705
+ const projectSessions = /* @__PURE__ */ new Map();
706
+ for (const session of this.sessions.values()) {
707
+ const existing = projectSessions.get(session.projectId);
708
+ if (existing) existing.push(session);
709
+ else projectSessions.set(session.projectId, [session]);
710
+ }
711
+ const disconnects = [];
712
+ for (const [projectId, sessions] of projectSessions) {
713
+ if (!connectionPool.isConnected(projectId)) continue;
714
+ const allIdle = sessions.every(
715
+ (s) => s.inflight === 0 && now - s.lastActivityAt >= SESSION_IDLE_TIMEOUT_MS
716
+ );
717
+ if (!allIdle) continue;
718
+ const reqId = connectionPool.getConnection(projectId)?.requestId;
719
+ log(
720
+ `idle disconnect project=${projectId}${reqId ? ` req=${reqId}` : ""}`
721
+ );
722
+ disconnects.push(connectionPool.disconnect(projectId));
723
+ }
724
+ const results = await Promise.allSettled(disconnects);
725
+ for (const result of results) {
726
+ if (result.status === "rejected") {
727
+ log(
728
+ `disconnect error: ${result.reason instanceof Error ? result.reason.message : result.reason}`
729
+ );
730
+ }
731
+ }
732
+ }
600
733
  };
601
734
  var sessionManager = new SessionManager();
602
735
 
603
736
  // src/router.ts
604
737
  var t = initTRPC.create();
738
+ var nextExecId = 0;
605
739
  var appRouter = t.router({
606
740
  version: t.procedure.query(() => {
607
741
  return { version: VERSION };
@@ -633,35 +767,52 @@ var appRouter = t.router({
633
767
  cwd: z.string().optional()
634
768
  })
635
769
  ).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}"`);
770
+ var _stack = [];
771
+ try {
772
+ const { sessionId, code, cwd } = input;
773
+ const session = sessionManager.get(sessionId);
774
+ if (!session) {
775
+ throw new TRPCError({
776
+ code: "NOT_FOUND",
777
+ message: `Session ${sessionId} not found`
778
+ });
779
+ }
780
+ const _guard = __using(_stack, sessionManager.exec(sessionId));
781
+ const execId = nextExecId++;
782
+ let framer = sessionManager.getFramer(session);
783
+ if (framer && !sessionManager.isConnected(session)) {
784
+ log(
785
+ `exec.reconnect exec=${execId} session=${sessionId} reason="idle disconnected"`
786
+ );
787
+ framer = await sessionManager.reconnect(session);
788
+ }
789
+ const reqId = framer?.requestId;
790
+ const tag = `exec=${execId} session=${sessionId}${reqId ? ` req=${reqId}` : ""}`;
791
+ log(`exec ${tag} code=${JSON.stringify(code).slice(0, 100)}`);
792
+ if (!framer) {
793
+ log(`exec.error ${tag} error="no connection"`);
794
+ return {
795
+ output: [],
796
+ error: "Failed to get connection for session"
797
+ };
798
+ }
799
+ const result = await executeWithReconnect(
800
+ session,
801
+ framer,
802
+ code,
803
+ { cwd },
804
+ () => sessionManager.reconnect(session),
805
+ execId
806
+ );
807
+ if (result.error) {
808
+ log(`exec.error ${tag} error="${result.error}"`);
809
+ }
810
+ return result;
811
+ } catch (_) {
812
+ var _error = _, _hasError = true;
813
+ } finally {
814
+ __callDispose(_stack, _error, _hasError);
663
815
  }
664
- return result;
665
816
  }),
666
817
  shutdown: t.procedure.mutation(() => {
667
818
  log("shutdown requested");
@@ -672,11 +823,25 @@ var appRouter = t.router({
672
823
  });
673
824
 
674
825
  // src/relay-server.ts
826
+ var IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
827
+ var IDLE_CHECK_INTERVAL_MS = 60 * 1e3;
675
828
  var trpcHandler = createHTTPHandler({ router: appRouter });
676
829
  async function startRelayServer(port = RELAY_PORT) {
830
+ let lastActivityAt = Date.now();
677
831
  const server = http.createServer((req, res) => {
832
+ lastActivityAt = Date.now();
678
833
  trpcHandler(req, res);
679
834
  });
835
+ const idleCheck = setInterval(() => {
836
+ const idleMs = Date.now() - lastActivityAt;
837
+ if (idleMs >= IDLE_TIMEOUT_MS) {
838
+ log(`idle for ${Math.round(idleMs / 1e3)}s, shutting down`);
839
+ clearInterval(idleCheck);
840
+ server.close();
841
+ process.exit(0);
842
+ }
843
+ }, IDLE_CHECK_INTERVAL_MS);
844
+ idleCheck.unref();
680
845
  return new Promise((resolve, reject) => {
681
846
  server.on("error", reject);
682
847
  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({{FRAMER_TEMPORARY_DIR}}/*)"]
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 a unique file, then execute with -f
31
+ # {{FRAMER_TEMPORARY_DIR}}/<sessionId>-read-page.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 {{FRAMER_TEMPORARY_DIR}}/<sessionId>-read-page.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
+ # {{FRAMER_TEMPORARY_DIR}}/<sessionId>-apply-changes.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 {{FRAMER_TEMPORARY_DIR}}/<sessionId>-apply-changes.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({{FRAMER_TEMPORARY_DIR}}/*)"]
7
7
  ---
8
8
 
9
9
  If you didn't run this command before loading the skill, run it now:
@@ -59,10 +59,10 @@ 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 unique file under `{{FRAMER_TEMPORARY_DIR}}/` and execute with `-f`. Name each file `<sessionId>-<short-summary>.js` where `<short-summary>` is a brief kebab-case description (e.g., `1-read-collections.js`, `1-add-team-member.js`). Files are automatically deleted after execution.
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 {{FRAMER_TEMPORARY_DIR}}/1-read-collections.js
66
66
  ```
67
67
 
68
68
  #### 4. Store results in `state`
@@ -90,13 +90,23 @@ Always save results you'll need again. Don't repeat API calls.
90
90
 
91
91
  **Always store results in `state` when you'll need them again.** API calls are slow - don't repeat them.
92
92
 
93
+ ```js
94
+ // {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
95
+ state.collections = await framer.getCollections();
96
+ ```
97
+
93
98
  ```bash
94
- # First call: fetch and store
95
- npx framer-dalton -s 1 -e "state.collections = await framer.getCollections()"
99
+ npx framer-dalton -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
100
+ ```
96
101
 
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()"
102
+ ```js
103
+ // {{FRAMER_TEMPORARY_DIR}}/1-get-team-items.js reuse from state
104
+ state.teamItems = await state.collections.find(c => c.name === 'Team').getItems();
105
+ console.log(state.teamItems.length);
106
+ ```
107
+
108
+ ```bash
109
+ npx framer-dalton -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-team-items.js
100
110
  ```
101
111
 
102
112
  Store anything you'll reference again.
@@ -132,55 +142,10 @@ After session creation, load the dynamically created project-scoped skill `frame
132
142
 
133
143
  ## Execute Code
134
144
 
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:**
145
+ Write your code to a unique file under `{{FRAMER_TEMPORARY_DIR}}/` and execute with `-f`:
142
146
 
143
147
  ```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:
155
-
156
- ```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
- "
148
+ npx framer-dalton -s <sessionId> -f {{FRAMER_TEMPORARY_DIR}}/<sessionId>-<short-summary>.js
184
149
  ```
185
150
 
186
151
  ## 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.11",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "framer-dalton": "./dist/cli.js"