framer-dalton 0.0.5 → 0.0.6

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/README.md CHANGED
@@ -1,34 +1,78 @@
1
1
  # Framer Agent CLI
2
2
 
3
- CLI and Agent skill for interacting with Framer projects via the Framer Server API.
3
+ CLI and agent skills for interacting with Framer projects via the Framer Server API.
4
4
 
5
5
  ## Installation
6
6
 
7
+ Install or refresh the globally available skills:
8
+
7
9
  ```bash
8
- npx skills add framer/dalton
10
+ npx framer-dalton@latest setup
9
11
  ```
10
12
 
11
- Then, simply open your preferred agent, ask to interact with your Framer project and the agent will handle the rest.
13
+ That installs skills into `~/.agents/skills` and `~/.claude/skills`.
14
+
15
+ ## How It Works
12
16
 
13
- ## CLI Usage
17
+ The CLI and the installed skills are meant to work together. There are three skills:
14
18
 
15
- See [skills/framer/SKILL.md](skills/framer/SKILL.md) for full CLI usage and API documentation.
19
+ - `framer` - the base skill that explains how to use the CLI and the API in general.
20
+ - `framer-code-components` - explains specific prompts for how to write code components and provides examples.
21
+ - `framer-canvas-editing-project-<project id>` - a dynamically-created skill that explains how to canvas edit and includes project context.
22
+
23
+ 1. Running `npx framer-dalton@latest setup` will install the base `framer` and `framer-code-component` skills.
24
+ 2. The frontmatter of `framer-code-components` tells agents to always load `framer` first.
25
+ 3. The frontmatter of `framer` tells agents to run `npx framer-dalton@latest setup` BEFORE loading the skill. This command will auto-update the cli and update the skill files.
26
+ 4. Creating a new session for a project will create a `framer-canvas-editing-project-<project id>` file. This file contains the latest agent system prompt from `framer.getAgentSystemPrompt` and the latest project context from `framer.getAgentContext`. Agents are told not to load this skill until after creating a session.
16
27
 
17
28
  ## Local Development
18
29
 
19
- Build and symlink to PATH + install skill locally:
30
+ ### Running a development build against production headless api server
31
+
32
+ 1. Run `make install-dev` in this repo.
33
+
34
+ 2. Run your agent in this directory and specifically load the `framer-dev` skill:
20
35
 
21
36
  ```bash
22
- make install-dev
37
+ # Claude:
38
+ > /framer-dev
39
+
40
+ # Codex:
41
+ > $framer-dev
23
42
  ```
24
43
 
25
- If you want to hit a local FramerHeadlessAPI instance, when interacting with your agent, tell it something like this:
44
+ ### Running with the local headless plugin server
45
+
46
+ Use this when you change something in the Server API implementation.
47
+
48
+ 1. Start FramerStudio with `make dev`.
49
+ 2. Start FramerHeadlessAPI against the local tunnel with `make dev-tunnel`.
50
+ 3. Run `make install-dev` in this repo.
51
+ 4. Run your agent in this directory and specifically load the `framer-dev-local` skill:
26
52
 
53
+ ```bash
54
+ # Claude:
55
+ > /framer-dev-local
56
+
57
+ # Codex:
58
+ > $framer-dev-local
27
59
  ```
28
- Use the framer skill, but use `FRAMER_HEADLESS_SERVER_URL=ws://localhost:8080/channel/headless-plugin framer` (never with @latest) for all commands instead of `npx framer-dalton`
60
+
61
+ ### Using a local `framer-api` package
62
+
63
+ Use this when you change the server API interface itself, such as adding or changing API methods.
64
+
65
+ 1. Publish or otherwise make your updated `framer-api` package available.
66
+ 2. Install it locally with `npm install ...`.
67
+ 3. Regenerate the type data:
68
+
69
+ ```bash
70
+ make generate-types
29
71
  ```
30
72
 
31
- Remove local install:
73
+ ### Removing local builds and returning to production
74
+
75
+ Later, to return to the procution skills:
32
76
 
33
77
  ```bash
34
78
  make uninstall-dev
package/dist/cli.js CHANGED
@@ -1,34 +1,34 @@
1
1
  #!/usr/bin/env node
2
+ import path3 from 'path';
2
3
  import { Command } from 'commander';
3
- import fs, { readFileSync } from 'fs';
4
+ import fs2 from 'fs';
4
5
  import os from 'os';
5
- import path, { dirname, join } from 'path';
6
6
  import { spawn } from 'child_process';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { createTRPCClient, httpLink } from '@trpc/client';
9
9
 
10
- /* @framer/ai CLI v0.0.5 */
10
+ /* @framer/ai CLI v0.0.6 */
11
11
  var __defProp = Object.defineProperty;
12
12
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
13
13
  function getConfigDir() {
14
14
  if (process.env.XDG_CONFIG_HOME) {
15
- return path.join(process.env.XDG_CONFIG_HOME, "framer");
15
+ return path3.join(process.env.XDG_CONFIG_HOME, "framer");
16
16
  }
17
17
  if (process.platform === "win32") {
18
- return path.join(process.env.APPDATA || os.homedir(), "framer");
18
+ return path3.join(process.env.APPDATA || os.homedir(), "framer");
19
19
  }
20
- return path.join(os.homedir(), ".config", "framer");
20
+ return path3.join(os.homedir(), ".config", "framer");
21
21
  }
22
22
  __name(getConfigDir, "getConfigDir");
23
23
  function getCredentialsPath() {
24
- return path.join(getConfigDir(), "credentials.json");
24
+ return path3.join(getConfigDir(), "credentials.json");
25
25
  }
26
26
  __name(getCredentialsPath, "getCredentialsPath");
27
27
  function readCredentials() {
28
28
  const credPath = getCredentialsPath();
29
29
  try {
30
- if (fs.existsSync(credPath)) {
31
- return JSON.parse(fs.readFileSync(credPath, "utf-8"));
30
+ if (fs2.existsSync(credPath)) {
31
+ return JSON.parse(fs2.readFileSync(credPath, "utf-8"));
32
32
  }
33
33
  } catch {
34
34
  }
@@ -38,10 +38,10 @@ __name(readCredentials, "readCredentials");
38
38
  function writeCredentials(creds) {
39
39
  const credPath = getCredentialsPath();
40
40
  const configDir = getConfigDir();
41
- if (!fs.existsSync(configDir)) {
42
- fs.mkdirSync(configDir, { recursive: true, mode: 448 });
41
+ if (!fs2.existsSync(configDir)) {
42
+ fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
43
43
  }
44
- fs.writeFileSync(credPath, JSON.stringify(creds, null, " "), {
44
+ fs2.writeFileSync(credPath, JSON.stringify(creds, null, " "), {
45
45
  mode: 384
46
46
  });
47
47
  }
@@ -14460,6 +14460,7 @@ __name(expandReferences, "expandReferences");
14460
14460
  function renderDocs(queries) {
14461
14461
  const lines = [];
14462
14462
  const errors = [];
14463
+ const framerClass = getClass("framer");
14463
14464
  if (queries.length === 0) {
14464
14465
  queries = ["framer"];
14465
14466
  }
@@ -14482,6 +14483,20 @@ function renderDocs(queries) {
14482
14483
  ${typeDef}`);
14483
14484
  }
14484
14485
  } else {
14486
+ if (framerClass) {
14487
+ const method = framerClass.methods.find((m) => m.name === query);
14488
+ if (method) {
14489
+ if (method.description)
14490
+ lines.push(formatDocComment(method.description));
14491
+ lines.push(`${method.category}.${method.signature}`);
14492
+ const seen = /* @__PURE__ */ new Set();
14493
+ for (const typeDef of expandReferences(method.references, seen)) {
14494
+ lines.push(`
14495
+ ${typeDef}`);
14496
+ }
14497
+ continue;
14498
+ }
14499
+ }
14485
14500
  const classData = getClass(query);
14486
14501
  if (classData) {
14487
14502
  lines.push(
@@ -14511,8 +14526,8 @@ ${typeDef}`);
14511
14526
  }
14512
14527
  __name(renderDocs, "renderDocs");
14513
14528
  var __filename$1 = fileURLToPath(import.meta.url);
14514
- var __dirname$1 = path.dirname(__filename$1);
14515
- var VERSION = "0.0.5" ;
14529
+ var __dirname$1 = path3.dirname(__filename$1);
14530
+ var VERSION = "0.0.6" ;
14516
14531
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
14517
14532
  var client = createTRPCClient({
14518
14533
  links: [
@@ -14548,7 +14563,7 @@ function compareVersions(v1, v2) {
14548
14563
  return 0;
14549
14564
  }
14550
14565
  __name(compareVersions, "compareVersions");
14551
- async function ensureRelayServer(options = {}) {
14566
+ async function ensureRelayServerRunning(options = {}) {
14552
14567
  const { logger, restartOnVersionMismatch = true } = options;
14553
14568
  const serverVersion = await getRelayServerVersion();
14554
14569
  if (serverVersion === VERSION) {
@@ -14574,7 +14589,7 @@ async function ensureRelayServer(options = {}) {
14574
14589
  logger?.log("Relay server not running, starting it...");
14575
14590
  }
14576
14591
  const isRunningFromSource = __filename$1.endsWith(".ts");
14577
- const scriptPath = isRunningFromSource ? path.resolve(__dirname$1, "./start-relay-server.ts") : path.resolve(__dirname$1, "./start-relay-server.js");
14592
+ const scriptPath = isRunningFromSource ? path3.resolve(__dirname$1, "./start-relay-server.ts") : path3.resolve(__dirname$1, "./start-relay-server.js");
14578
14593
  const serverProcess = spawn(
14579
14594
  isRunningFromSource ? "tsx" : process.execPath,
14580
14595
  [scriptPath],
@@ -14595,22 +14610,107 @@ async function ensureRelayServer(options = {}) {
14595
14610
  }
14596
14611
  throw new Error("Failed to start relay server after 5 seconds");
14597
14612
  }
14598
- __name(ensureRelayServer, "ensureRelayServer");
14599
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
14600
- var docsDir = join(__dirname2, "..", "docs");
14601
- function readDoc(name) {
14602
- return readFileSync(join(docsDir, `${name}.md`), "utf-8").trimEnd();
14613
+ __name(ensureRelayServerRunning, "ensureRelayServerRunning");
14614
+ var META_SKILL_NAME = "framer";
14615
+ var CODE_COMPONENTS_SKILL_NAME = "framer-code-components";
14616
+ var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
14617
+ var skillsDocsDir = path3.join(__dirname2, "..", "docs", "skills");
14618
+ function readSkillDoc(name) {
14619
+ return fs2.readFileSync(path3.join(skillsDocsDir, name), "utf-8").trimEnd();
14620
+ }
14621
+ __name(readSkillDoc, "readSkillDoc");
14622
+ function buildMetaSkill() {
14623
+ return `${readSkillDoc("framer.md")}
14624
+ `;
14625
+ }
14626
+ __name(buildMetaSkill, "buildMetaSkill");
14627
+ function buildCodeComponentsSkill() {
14628
+ return `${readSkillDoc("framer-code-components.md")}
14629
+ `;
14630
+ }
14631
+ __name(buildCodeComponentsSkill, "buildCodeComponentsSkill");
14632
+ function renderTemplate(template, values) {
14633
+ let rendered = template;
14634
+ for (const [key, value] of Object.entries(values)) {
14635
+ rendered = rendered.split(`{{${key}}}`).join(value);
14636
+ }
14637
+ return rendered;
14638
+ }
14639
+ __name(renderTemplate, "renderTemplate");
14640
+ function toProjectSkillName(projectId) {
14641
+ const safeProjectId = projectId.replace(/[^a-zA-Z0-9_-]/g, "-");
14642
+ return `framer-canvas-editing-project-${safeProjectId}`;
14643
+ }
14644
+ __name(toProjectSkillName, "toProjectSkillName");
14645
+ function buildProjectCanvasSkill(projectId, agentContext, canvasPrompt) {
14646
+ const skillName = toProjectSkillName(projectId);
14647
+ const template = readSkillDoc("framer-canvas-editing-project.md");
14648
+ const content = `${renderTemplate(template, {
14649
+ SKILL_NAME: skillName,
14650
+ PROJECT_ID: projectId,
14651
+ GENERATED_AT: (/* @__PURE__ */ new Date()).toISOString(),
14652
+ CANVAS_PROMPT: canvasPrompt.trimEnd(),
14653
+ AGENT_CONTEXT: agentContext.trimEnd()
14654
+ })}
14655
+ `;
14656
+ return { skillName, content };
14603
14657
  }
14604
- __name(readDoc, "readDoc");
14605
- var skillTopics = [
14606
- "code-components",
14607
- "property-controls",
14608
- "component-examples"
14609
- ];
14610
- function isSkillTopic(topic) {
14611
- return skillTopics.some((name) => name === topic);
14658
+ __name(buildProjectCanvasSkill, "buildProjectCanvasSkill");
14659
+ function writeSkill(root, skillName, content) {
14660
+ fs2.mkdirSync(root, { recursive: true });
14661
+ const rootStat = fs2.statSync(root);
14662
+ if (!rootStat.isDirectory()) {
14663
+ throw new Error(`Skill root is not a directory: ${root}`);
14664
+ }
14665
+ const skillDir = path3.join(root, skillName);
14666
+ const filePath = path3.join(skillDir, "SKILL.md");
14667
+ if (fs2.existsSync(skillDir)) {
14668
+ const current = fs2.lstatSync(skillDir);
14669
+ if (current.isSymbolicLink() || !current.isDirectory()) {
14670
+ fs2.rmSync(skillDir, { recursive: true, force: true });
14671
+ }
14672
+ }
14673
+ fs2.mkdirSync(skillDir, { recursive: true });
14674
+ fs2.writeFileSync(filePath, content, "utf-8");
14675
+ return filePath;
14676
+ }
14677
+ __name(writeSkill, "writeSkill");
14678
+ function getDefaultSkillRoots() {
14679
+ const home = os.homedir();
14680
+ return [
14681
+ path3.join(home, ".agents", "skills"),
14682
+ path3.join(home, ".claude", "skills")
14683
+ ];
14684
+ }
14685
+ __name(getDefaultSkillRoots, "getDefaultSkillRoots");
14686
+ function installSkills(options = { type: "base" }) {
14687
+ const skillRoots = options.skillRoots ?? getDefaultSkillRoots();
14688
+ const skills = [
14689
+ { name: META_SKILL_NAME, content: buildMetaSkill() },
14690
+ {
14691
+ name: CODE_COMPONENTS_SKILL_NAME,
14692
+ content: buildCodeComponentsSkill()
14693
+ }
14694
+ ];
14695
+ if (options.type === "project") {
14696
+ const projectSkill = buildProjectCanvasSkill(
14697
+ options.projectId,
14698
+ options.agentContext,
14699
+ options.canvasPrompt
14700
+ );
14701
+ skills.push({
14702
+ name: projectSkill.skillName,
14703
+ content: projectSkill.content
14704
+ });
14705
+ }
14706
+ return skills.map((skill) => ({
14707
+ skillName: skill.name,
14708
+ paths: skillRoots.map(
14709
+ (root) => writeSkill(root, skill.name, skill.content)
14710
+ )
14711
+ }));
14612
14712
  }
14613
- __name(isSkillTopic, "isSkillTopic");
14713
+ __name(installSkills, "installSkills");
14614
14714
 
14615
14715
  // src/utils.ts
14616
14716
  function formatError(error) {
@@ -14644,6 +14744,116 @@ async function readStdin() {
14644
14744
  return Buffer.concat(chunks).toString("utf-8");
14645
14745
  }
14646
14746
  __name(readStdin, "readStdin");
14747
+ function printSetupSummary(results) {
14748
+ const skillCount = results.length;
14749
+ const installLocations = /* @__PURE__ */ new Set();
14750
+ for (const result of results) {
14751
+ for (const filePath of result.paths) {
14752
+ const skillDir = path3.dirname(filePath);
14753
+ const root = path3.dirname(skillDir);
14754
+ installLocations.add(root);
14755
+ }
14756
+ }
14757
+ print(
14758
+ `Installed ${skillCount} skills to ${Array.from(installLocations).sort().join(", ")}`
14759
+ );
14760
+ }
14761
+ __name(printSetupSummary, "printSetupSummary");
14762
+ async function getAgentSystemPrompt(sessionId) {
14763
+ const result = await client.exec.mutate({
14764
+ sessionId,
14765
+ code: "return await framer.getAgentSystemPrompt();",
14766
+ cwd: process.cwd()
14767
+ });
14768
+ if (result.error) {
14769
+ throw new Error(result.error);
14770
+ }
14771
+ const prompt = result.output.at(-1);
14772
+ if (typeof prompt !== "string") {
14773
+ throw new Error("Did not receive agent prompt output.");
14774
+ }
14775
+ return prompt;
14776
+ }
14777
+ __name(getAgentSystemPrompt, "getAgentSystemPrompt");
14778
+ async function getAgentContext(sessionId) {
14779
+ const result = await client.exec.mutate({
14780
+ sessionId,
14781
+ code: "return await framer.getAgentContext({ pagePath: '/' });",
14782
+ cwd: process.cwd()
14783
+ });
14784
+ if (result.error) {
14785
+ throw new Error(result.error);
14786
+ }
14787
+ const context = result.output.at(-1);
14788
+ if (typeof context !== "string") {
14789
+ throw new Error("Did not receive agent context output.");
14790
+ }
14791
+ return context;
14792
+ }
14793
+ __name(getAgentContext, "getAgentContext");
14794
+ async function refreshSkillsFromSession(sessionId, projectId) {
14795
+ try {
14796
+ const [canvasPrompt, agentContext] = await Promise.all([
14797
+ getAgentSystemPrompt(sessionId),
14798
+ getAgentContext(sessionId)
14799
+ ]);
14800
+ installSkills({
14801
+ type: "project",
14802
+ canvasPrompt,
14803
+ projectId,
14804
+ agentContext
14805
+ });
14806
+ } catch (err) {
14807
+ printError(
14808
+ `Failed to refresh skills for session ${sessionId}: ${formatError(err)}`
14809
+ );
14810
+ process.exit(1);
14811
+ }
14812
+ }
14813
+ __name(refreshSkillsFromSession, "refreshSkillsFromSession");
14814
+ function resolveSessionCredentials(projectUrl, apiKeyArg) {
14815
+ try {
14816
+ const projectId = extractProjectId(projectUrl);
14817
+ if (!apiKeyArg) {
14818
+ const cachedApiKey = getApiKey(projectId);
14819
+ if (cachedApiKey) {
14820
+ return { projectId, apiKey: cachedApiKey };
14821
+ }
14822
+ printError("No API key provided and none cached for this project.");
14823
+ printError("");
14824
+ printError("Usage: framer session new <projectUrl> <apiKey>");
14825
+ process.exit(1);
14826
+ }
14827
+ saveApiKey(projectId, apiKeyArg);
14828
+ return { projectId, apiKey: apiKeyArg };
14829
+ } catch (err) {
14830
+ printError(`Failed to create session: ${formatError(err)}`);
14831
+ process.exit(1);
14832
+ }
14833
+ }
14834
+ __name(resolveSessionCredentials, "resolveSessionCredentials");
14835
+ async function createSession(projectId, apiKey) {
14836
+ try {
14837
+ const result = await client.createSession.mutate({
14838
+ projectId,
14839
+ apiKey
14840
+ });
14841
+ return result.id;
14842
+ } catch (err) {
14843
+ printError(`Failed to create session: ${formatError(err)}`);
14844
+ process.exit(1);
14845
+ }
14846
+ }
14847
+ __name(createSession, "createSession");
14848
+ async function ensureRelayForCli() {
14849
+ try {
14850
+ await ensureRelayServerRunning({ logger: { log: print } });
14851
+ } catch (err) {
14852
+ printError(`Failed to check relay status: ${formatError(err)}`);
14853
+ process.exit(1);
14854
+ }
14855
+ }
14856
+ __name(ensureRelayForCli, "ensureRelayForCli");
14647
14857
  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) => {
14648
14858
  const { session: sessionId, eval: evalCode } = options;
14649
14859
  let code = evalCode;
@@ -14661,8 +14871,8 @@ program.option("-s, --session <id>", "Session ID (required for code execution)")
14661
14871
  );
14662
14872
  process.exit(1);
14663
14873
  }
14874
+ await ensureRelayForCli();
14664
14875
  try {
14665
- await ensureRelayServer({ logger: { log: print } });
14666
14876
  const result = await client.exec.mutate({
14667
14877
  sessionId: String(sessionId),
14668
14878
  code,
@@ -14682,36 +14892,18 @@ program.option("-s, --session <id>", "Session ID (required for code execution)")
14682
14892
  });
14683
14893
  var session = program.command("session").description("Manage sessions");
14684
14894
  session.command("new <projectUrl> [apiKey]").description("Create a new session and print the session ID").action(async (projectUrlArg, apiKeyArg) => {
14685
- try {
14686
- const projectId = extractProjectId(projectUrlArg);
14687
- let apiKey = apiKeyArg;
14688
- if (!apiKey) {
14689
- const cached = getApiKey(projectId);
14690
- if (cached) {
14691
- apiKey = cached;
14692
- } else {
14693
- printError("No API key provided and none cached for this project.");
14694
- printError("");
14695
- printError("Usage: framer session new <projectUrl> <apiKey>");
14696
- process.exit(1);
14697
- }
14698
- } else {
14699
- saveApiKey(projectId, apiKey);
14700
- }
14701
- await ensureRelayServer({ logger: { log: print } });
14702
- const result = await client.createSession.mutate({
14703
- projectId,
14704
- apiKey
14705
- });
14706
- print(result.id);
14707
- } catch (err) {
14708
- printError(`Failed to create session: ${formatError(err)}`);
14709
- process.exit(1);
14710
- }
14895
+ const { projectId, apiKey } = resolveSessionCredentials(
14896
+ projectUrlArg,
14897
+ apiKeyArg
14898
+ );
14899
+ await ensureRelayForCli();
14900
+ const sessionId = await createSession(projectId, apiKey);
14901
+ await refreshSkillsFromSession(sessionId, projectId);
14902
+ print(sessionId);
14711
14903
  });
14712
14904
  session.command("list").description("List all active sessions").action(async () => {
14905
+ await ensureRelayForCli();
14713
14906
  try {
14714
- await ensureRelayServer({ logger: { log: print } });
14715
14907
  const sessions = await client.listSessions.query();
14716
14908
  if (sessions.length === 0) {
14717
14909
  print("No active sessions");
@@ -14724,8 +14916,8 @@ session.command("list").description("List all active sessions").action(async ()
14724
14916
  }
14725
14917
  });
14726
14918
  session.command("destroy <sessionId>").description("Destroy a session").action(async (sessionId) => {
14919
+ await ensureRelayForCli();
14727
14920
  try {
14728
- await ensureRelayServer({ logger: { log: print } });
14729
14921
  await client.destroySession.mutate({ sessionId });
14730
14922
  print(`Session ${sessionId} destroyed`);
14731
14923
  } catch (err) {
@@ -14749,24 +14941,18 @@ relay.command("restart").description("Restart the relay server").action(async ()
14749
14941
  } catch {
14750
14942
  }
14751
14943
  await new Promise((resolve) => setTimeout(resolve, 500));
14752
- await ensureRelayServer({ logger: { log: print } });
14944
+ await ensureRelayForCli();
14753
14945
  });
14754
- program.command("skill [topic]").description("Output skill documentation").action((topic) => {
14755
- if (!topic) {
14756
- print(readDoc("server-api"));
14757
- print("");
14758
- print(readDoc("all-skills"));
14759
- return;
14760
- }
14761
- if (!isSkillTopic(topic)) {
14762
- const available = ["(no argument)", ...skillTopics];
14763
- printError(`Unknown skill topic: ${topic}`);
14764
- printError(`Available topics: ${available.join(", ")}`);
14946
+ program.command("setup").description(
14947
+ "Install Framer skills into ~/.agents/skills and ~/.claude/skills"
14948
+ ).action(() => {
14949
+ try {
14950
+ const results = installSkills();
14951
+ printSetupSummary(results);
14952
+ } catch (err) {
14953
+ printError(`Setup failed: ${formatError(err)}`);
14765
14954
  process.exit(1);
14766
14955
  }
14767
- print(readDoc(topic));
14768
- print("");
14769
- print(readDoc("all-skills"));
14770
14956
  });
14771
14957
  program.command("docs [queries...]").description(
14772
14958
  "Look up API documentation. Use: docs, docs ClassName, docs Class.method, or docs TypeName"
@@ -13,7 +13,7 @@ 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.5 */
16
+ /* @framer/ai relay server v0.0.6 */
17
17
  var __defProp = Object.defineProperty;
18
18
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
19
19
  function getLogPath() {
@@ -50,7 +50,7 @@ function log(message) {
50
50
  __name(log, "log");
51
51
  var __filename$1 = fileURLToPath(import.meta.url);
52
52
  path.dirname(__filename$1);
53
- var VERSION = "0.0.5" ;
53
+ var VERSION = "0.0.6" ;
54
54
  var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
55
55
  createTRPCClient({
56
56
  links: [
@@ -688,7 +688,6 @@ process.on("unhandledRejection", (reason) => {
688
688
  });
689
689
  async function main() {
690
690
  const server = await startRelayServer(RELAY_PORT);
691
- log(`started v${VERSION} on port ${RELAY_PORT}`);
692
691
  console.log(`Framer relay server v${VERSION} running on port ${RELAY_PORT}`);
693
692
  process.on("SIGINT", () => {
694
693
  log("shutdown SIGINT");
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: {{SKILL_NAME}}
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
+ ---
5
+
6
+ ## Project Scope
7
+
8
+ - Project ID: {{PROJECT_ID}}
9
+ - Generated At: {{GENERATED_AT}}
10
+
11
+ ## Rules
12
+
13
+ - For design/layout work, do not use low-level node APIs (`createNode`, `setAttributes`, `setRect`, etc.). Use the canvas editing flow (`readProjectForAgent` + `applyAgentChanges`) with the embedded prompt and project context in this skill.
14
+ - Canvas editing agent methods are employee-only.
15
+ - Canvas editing methods are not globals. Call `framer.readProjectForAgent` and `framer.applyAgentChanges` as `framer.*`.
16
+ - During normal task execution, do not call `framer.getAgentSystemPrompt()` or `framer.getAgentContext()`. This skill already includes `getAgentContext({ pagePath: "/" })`.
17
+ - `framer.readProjectForAgent(queries, { pagePath })` reads project state. Start with page structure only, then request additional targeted queries only if needed. Query types are documented in the embedded prompt below. Never guess names for examples, icon sets, or fonts; look them up first.
18
+ - `framer.applyAgentChanges(dsl, { pagePath })` applies canvas DSL changes. After applying changes, re-read project state to inspect results and iterate with `readProjectForAgent` + `applyAgentChanges` until accurate.
19
+ - Request examples only when needed, and only after inspecting page structure first. Keep example queries targeted and minimal.
20
+ - Use screenshots only when visual verification is necessary and cannot be confirmed from state reads. Do not use them for initial inspection or between every small change.
21
+ - When setting text content, use raw Unicode characters directly (for example `->`, not `\u2192`).
22
+ - Regenerate the embedded prompt and context by running `session new` again for the same project.
23
+ - Node IDs assigned during commands are temporary and can only be referenced within the same `framer.applyAgentChanges` call. For later calls, re-read the project to get real IDs.
24
+ - For rich text updates, default to `SET <RichTextNode-id> text="..."` for normal text replacement. Use `SET <TextRun-id> text="..."` only for intentional targeted run-level edits in multi-run content.
25
+ - Child `TextRun` IDs are stable within one `applyAgentChanges` batch, but may change across batches or turns after re-hydration. Re-read before dependent follow-up commands in a new batch.
26
+ - In rich text, block-level properties (for example line height or alignment) belong on block or parent targets, not `TextRun` targets.
27
+ - `IconNode` `$control__icon` is immutable after creation. To change it, delete and recreate the node with the desired icon value.
28
+ - `var(--token-<id>)` requires stable token IDs. Temporary IDs are only reliable within the same `applyAgentChanges` batch. If a token was created in a previous batch, re-read state to get its current real ID before referencing it, or create the token and dependents in one batch.
29
+
30
+ ## Workflow Loop
31
+
32
+ ```bash
33
+ # 1) Read page structure first
34
+ framer -s <sessionId> <<'EOF'
35
+ const { results } = await framer.readProjectForAgent(
36
+ [{ type: "page", path: "/" }],
37
+ { pagePath: "/" }
38
+ );
39
+
40
+ console.log(results);
41
+ EOF
42
+
43
+ # 2) Request additional targeted queries only if needed
44
+
45
+ # 3) Apply changes in a later call, once `dsl` has been prepared
46
+ framer -s <sessionId> <<'EOF'
47
+ const dsl = `
48
+ ...your canvas DSL...
49
+ `;
50
+
51
+ await framer.applyAgentChanges(dsl, { pagePath: "/" });
52
+ EOF
53
+ ```
54
+
55
+ ## Live Agent System Prompt
56
+
57
+ This is the static canvas-editing prompt returned by `framer.getAgentSystemPrompt()`. It is the sole documentation for:
58
+
59
+ - command syntax for `applyAgentChanges`
60
+ - query types and parameters for `readProjectForAgent`
61
+ - design rules and examples used by the canvas editing flow
62
+
63
+ {{CANVAS_PROMPT}}
64
+
65
+ ## Live Agent Context (/)
66
+
67
+ 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.
68
+
69
+ {{AGENT_CONTEXT}}