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 +72 -38
- package/dist/start-relay-server.js +205 -40
- package/docs/skills/framer-canvas-editing-project.md +16 -16
- package/docs/skills/framer.md +20 -55
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
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.
|
|
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
|
|
39
|
+
return path4.join(process.env.XDG_CONFIG_HOME, "framer");
|
|
40
40
|
}
|
|
41
41
|
if (process.platform === "win32") {
|
|
42
|
-
return
|
|
42
|
+
return path4.join(process.env.APPDATA || os.homedir(), "framer");
|
|
43
43
|
}
|
|
44
|
-
return
|
|
44
|
+
return path4.join(os.homedir(), ".config", "framer");
|
|
45
45
|
}
|
|
46
46
|
__name(getConfigDir, "getConfigDir");
|
|
47
47
|
function getProjectsConfigPath() {
|
|
48
|
-
return
|
|
48
|
+
return path4.join(getConfigDir(), "projects.json");
|
|
49
49
|
}
|
|
50
50
|
__name(getProjectsConfigPath, "getProjectsConfigPath");
|
|
51
51
|
function getLegacyCredentialsPath() {
|
|
52
|
-
return
|
|
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 (!
|
|
68
|
+
if (!fs3.existsSync(filePath)) {
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
try {
|
|
72
|
-
return JSON.parse(
|
|
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 (!
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
14913
|
-
var VERSION = "0.0.
|
|
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 ?
|
|
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 =
|
|
15000
|
-
var skillsDocsDir =
|
|
15019
|
+
var __dirname2 = path4.dirname(fileURLToPath(import.meta.url));
|
|
15020
|
+
var skillsDocsDir = path4.join(__dirname2, "..", "docs", "skills");
|
|
15001
15021
|
function readSkillDoc(name) {
|
|
15002
|
-
return
|
|
15022
|
+
return fs3.readFileSync(path4.join(skillsDocsDir, name), "utf-8").trimEnd();
|
|
15003
15023
|
}
|
|
15004
15024
|
__name(readSkillDoc, "readSkillDoc");
|
|
15005
15025
|
function buildMetaSkill() {
|
|
15006
|
-
|
|
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
|
-
|
|
15044
|
-
const rootStat =
|
|
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 =
|
|
15049
|
-
const filePath =
|
|
15050
|
-
if (
|
|
15051
|
-
const current =
|
|
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
|
-
|
|
15075
|
+
fs3.rmSync(skillDir, { recursive: true, force: true });
|
|
15054
15076
|
}
|
|
15055
15077
|
}
|
|
15056
|
-
|
|
15057
|
-
|
|
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
|
-
|
|
15065
|
-
|
|
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 =
|
|
15115
|
-
const root =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
code,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
32
|
-
const { results } = await framer.readProjectForAgent(
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({{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 -
|
|
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
|
-
|
|
95
|
-
|
|
99
|
+
npx framer-dalton -s 1 -f {{FRAMER_TEMPORARY_DIR}}/1-get-collections.js
|
|
100
|
+
```
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|