@ttsalpha/mirra 1.2.0 → 1.3.0

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
@@ -65,11 +65,13 @@ skips all of it. The command is written with the real path of the Node binary th
65
65
  because a version manager puts `node` behind a path that changes with every shell and a
66
66
  GUI client never sees it.
67
67
 
68
- Then, by hand:
68
+ It also registers the plugin with Figma Desktop by writing into Figma's own settings,
69
+ which only works while Figma is **closed** — that file is rewritten from memory on quit.
70
+ If Figma is running, or the format turns out to be one Mirra does not recognise, `init` says
71
+ so and falls back to the manual route: Figma Desktop → Plugins → Development → Import plugin
72
+ from manifest… → `~/.mirra/plugin/manifest.json`.
69
73
 
70
- 1. Figma Desktop → Plugins → Development → Import plugin from manifest… →
71
- `~/.mirra/plugin/manifest.json`
72
- 2. **Restart the clients**, or they will not see the tools
74
+ Either way, **restart the clients**, or they will not see the tools.
73
75
 
74
76
  And once per working session: open a file in Figma, run the Mirra plugin, leave its panel
75
77
  open. Figma has no background plugins, so closing the panel ends the connection.
package/dist/index.js CHANGED
@@ -32732,6 +32732,13 @@ function claudeCode(node2, entryPoint) {
32732
32732
  };
32733
32733
  }
32734
32734
 
32735
+ // src/figma-settings.ts
32736
+ import { execFile } from "node:child_process";
32737
+ import { readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
32738
+ import { homedir as homedir3 } from "node:os";
32739
+ import { dirname as dirname2, join as join3, sep as sep2 } from "node:path";
32740
+ import { promisify } from "node:util";
32741
+
32735
32742
  // src/install.ts
32736
32743
  import { spawn } from "node:child_process";
32737
32744
  import { access as access2, cp, mkdir, readFile as readFile2, rm } from "node:fs/promises";
@@ -32784,6 +32791,106 @@ function reveal(path) {
32784
32791
  spawn("open", ["-R", path], { stdio: "ignore", detached: true }).unref();
32785
32792
  }
32786
32793
 
32794
+ // src/figma-settings.ts
32795
+ var FIGMA_SETTINGS = settingsPath();
32796
+ function settingsPath() {
32797
+ if (process.platform === "darwin") {
32798
+ return join3(homedir3(), "Library/Application Support/Figma/settings.json");
32799
+ }
32800
+ if (process.platform === "win32") {
32801
+ const local = process.env["LOCALAPPDATA"] ?? join3(homedir3(), "AppData/Local");
32802
+ return join3(local, "Figma/settings.json");
32803
+ }
32804
+ return join3(homedir3(), ".config/Figma/settings.json");
32805
+ }
32806
+ var PLUGIN_ID = "mirra-figma-bridge";
32807
+ async function registerPluginWithFigma(pluginDir) {
32808
+ if (await isFigmaRunning()) {
32809
+ return { status: "skipped", reason: "Figma Desktop is running; quit it first" };
32810
+ }
32811
+ let raw;
32812
+ try {
32813
+ raw = await readFile3(FIGMA_SETTINGS, "utf8");
32814
+ } catch {
32815
+ return { status: "skipped", reason: `no settings file at ${FIGMA_SETTINGS}` };
32816
+ }
32817
+ try {
32818
+ const settings = JSON.parse(raw);
32819
+ const entries = settings["localFileExtensions"];
32820
+ if (!Array.isArray(entries)) {
32821
+ return { status: "skipped", reason: "unfamiliar settings format" };
32822
+ }
32823
+ const next = rebuild(entries, pluginDir);
32824
+ if (!next) return { status: "already" };
32825
+ settings["localFileExtensions"] = next;
32826
+ const temporary = join3(dirname2(FIGMA_SETTINGS), `.mirra-${process.pid}.json`);
32827
+ await writeFile2(temporary, JSON.stringify(settings, null, 2));
32828
+ await rename2(temporary, FIGMA_SETTINGS);
32829
+ return { status: "registered" };
32830
+ } catch (error62) {
32831
+ return { status: "skipped", reason: error62 instanceof Error ? error62.message : String(error62) };
32832
+ }
32833
+ }
32834
+ function rebuild(entries, pluginDir) {
32835
+ const mine = entries.filter((entry) => isMine(entry, entries));
32836
+ if (isCleanTrio(mine, pluginDir)) return null;
32837
+ const others = entries.filter((entry) => !mine.includes(entry));
32838
+ return [...others, ...entriesFor(pluginDir, nextId(others))];
32839
+ }
32840
+ function isCleanTrio(mine, pluginDir) {
32841
+ const manifest = mine.find((entry) => entry.fileMetadata.type === "manifest");
32842
+ if (mine.length !== 3 || !manifest || manifest.fileMetadata.type !== "manifest") return false;
32843
+ if (manifest.manifestPath !== join3(pluginDir, "manifest.json")) return false;
32844
+ const { codeFileId, uiFileIds } = manifest.fileMetadata;
32845
+ const paths = new Map(mine.map((entry) => [entry.id, entry.manifestPath]));
32846
+ return paths.get(codeFileId) === join3(pluginDir, "main.js") && uiFileIds.length === 1 && paths.get(uiFileIds[0]) === join3(pluginDir, "ui.html");
32847
+ }
32848
+ function isMine(entry, all) {
32849
+ if (entry.lastKnownPluginId === PLUGIN_ID) return true;
32850
+ if (entry.manifestPath.startsWith(`${MIRRA_HOME}${sep2}`)) return true;
32851
+ const metadata = entry.fileMetadata;
32852
+ return metadata.type !== "manifest" && !all.some((e) => e.id === metadata.manifestFileId);
32853
+ }
32854
+ async function isFigmaRunning() {
32855
+ const run = promisify(execFile);
32856
+ try {
32857
+ if (process.platform === "win32") {
32858
+ const { stdout } = await run("tasklist", ["/fi", "imagename eq Figma.exe", "/nh"]);
32859
+ return stdout.includes("Figma.exe");
32860
+ }
32861
+ await run("pgrep", ["-x", "Figma"]);
32862
+ return true;
32863
+ } catch {
32864
+ return false;
32865
+ }
32866
+ }
32867
+ function nextId(entries) {
32868
+ return entries.reduce((highest, entry) => Math.max(highest, entry.id), 0) + 1;
32869
+ }
32870
+ function entriesFor(pluginDir, first) {
32871
+ const [manifestId, codeId, uiId] = [first, first + 1, first + 2];
32872
+ return [
32873
+ {
32874
+ id: manifestId,
32875
+ manifestPath: join3(pluginDir, "manifest.json"),
32876
+ lastKnownName: "Mirra",
32877
+ lastKnownPluginId: PLUGIN_ID,
32878
+ cachedContainsWidget: false,
32879
+ fileMetadata: { type: "manifest", codeFileId: codeId, uiFileIds: [uiId] }
32880
+ },
32881
+ {
32882
+ id: codeId,
32883
+ manifestPath: join3(pluginDir, "main.js"),
32884
+ fileMetadata: { type: "code", manifestFileId: manifestId }
32885
+ },
32886
+ {
32887
+ id: uiId,
32888
+ manifestPath: join3(pluginDir, "ui.html"),
32889
+ fileMetadata: { type: "ui", manifestFileId: manifestId }
32890
+ }
32891
+ ];
32892
+ }
32893
+
32787
32894
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v3/helpers/util.js
32788
32895
  var util;
32789
32896
  (function(util2) {
@@ -40821,7 +40928,20 @@ var EMPTY_COMPLETION_RESULT = {
40821
40928
  };
40822
40929
 
40823
40930
  // src/version.ts
40824
- var SERVER_VERSION = "1.2.0";
40931
+ var SERVER_VERSION = "1.3.0";
40932
+
40933
+ // src/tools/annotations.ts
40934
+ var READS = { readOnlyHint: true, openWorldHint: false };
40935
+ var ADDS = {
40936
+ readOnlyHint: false,
40937
+ destructiveHint: false,
40938
+ openWorldHint: false
40939
+ };
40940
+ var CHANGES = {
40941
+ readOnlyHint: false,
40942
+ destructiveHint: true,
40943
+ openWorldHint: false
40944
+ };
40825
40945
 
40826
40946
  // src/errors.ts
40827
40947
  var MirraError = class extends Error {
@@ -40853,7 +40973,12 @@ value keyed by mode name sets one value per mode. Both halves are optional.`;
40853
40973
  function registerDefine(server, bridge) {
40854
40974
  server.registerTool(
40855
40975
  "mirra_define_tokens",
40856
- { title: "Define Figma variables", description: DESCRIPTION, inputSchema: DefineParams.shape },
40976
+ {
40977
+ annotations: CHANGES,
40978
+ title: "Define Figma variables",
40979
+ description: DESCRIPTION,
40980
+ inputSchema: DefineParams.shape
40981
+ },
40857
40982
  async (params) => {
40858
40983
  try {
40859
40984
  const result = DefineResult.parse(await bridge.request("define", params));
@@ -40879,7 +41004,12 @@ The body is an async function receiving \`figma\`; use it only for what the type
40879
41004
  function registerExec(server, bridge) {
40880
41005
  server.registerTool(
40881
41006
  "mirra_exec",
40882
- { title: "Run Plugin API code", description: DESCRIPTION2, inputSchema: ExecParams.shape },
41007
+ {
41008
+ annotations: CHANGES,
41009
+ title: "Run Plugin API code",
41010
+ description: DESCRIPTION2,
41011
+ inputSchema: ExecParams.shape
41012
+ },
40883
41013
  async (params) => {
40884
41014
  try {
40885
41015
  const result = ExecResult.parse(await bridge.request("exec", params));
@@ -40892,11 +41022,11 @@ function registerExec(server, bridge) {
40892
41022
  }
40893
41023
 
40894
41024
  // src/tools/export.ts
40895
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
41025
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
40896
41026
  import { resolve as resolve3 } from "node:path";
40897
41027
 
40898
41028
  // src/tools/paths.ts
40899
- import { resolve as resolve2, sep as sep2 } from "node:path";
41029
+ import { resolve as resolve2, sep as sep3 } from "node:path";
40900
41030
  function rootDirectory() {
40901
41031
  const configured = process.env["MIRRA_ROOT"];
40902
41032
  return configured ? resolve2(configured) : process.cwd();
@@ -40905,7 +41035,7 @@ function resolveInRoot(path) {
40905
41035
  const root = rootDirectory();
40906
41036
  const full = resolve2(root, path);
40907
41037
  if (!process.env["MIRRA_ROOT"]) return full;
40908
- if (full !== root && !full.startsWith(root + sep2)) {
41038
+ if (full !== root && !full.startsWith(root + sep3)) {
40909
41039
  throw new MirraError(
40910
41040
  `${path} is outside MIRRA_ROOT`,
40911
41041
  "outside_root",
@@ -40922,6 +41052,7 @@ function registerExport(server, bridge) {
40922
41052
  server.registerTool(
40923
41053
  "mirra_export_assets",
40924
41054
  {
41055
+ annotations: CHANGES,
40925
41056
  title: "Export Figma layers to files",
40926
41057
  description: DESCRIPTION3,
40927
41058
  inputSchema: ExportParams.shape
@@ -40934,7 +41065,7 @@ function registerExport(server, bridge) {
40934
41065
  const written = [];
40935
41066
  for (const asset2 of result.assets) {
40936
41067
  const file2 = resolve3(directory, `${slug(asset2.name)}.${asset2.extension}`);
40937
- await writeFile2(file2, Buffer.from(asset2.data, "base64"));
41068
+ await writeFile3(file2, Buffer.from(asset2.data, "base64"));
40938
41069
  written.push(file2);
40939
41070
  }
40940
41071
  return text([`wrote ${written.length} file(s):`, ...written]);
@@ -40957,7 +41088,12 @@ tool works on the current page, so start here when the layer you want is elsewhe
40957
41088
  function registerFind(server, bridge) {
40958
41089
  server.registerTool(
40959
41090
  "mirra_find",
40960
- { title: "Find Figma layers", description: FIND, inputSchema: FindParams.shape },
41091
+ {
41092
+ annotations: READS,
41093
+ title: "Find Figma layers",
41094
+ description: FIND,
41095
+ inputSchema: FindParams.shape
41096
+ },
40961
41097
  async (params) => {
40962
41098
  try {
40963
41099
  const result = FindResult.parse(await bridge.request("find", params));
@@ -40975,7 +41111,13 @@ function registerFind(server, bridge) {
40975
41111
  );
40976
41112
  server.registerTool(
40977
41113
  "mirra_pages",
40978
- { title: "List or switch Figma pages", description: PAGES, inputSchema: PagesParams.shape },
41114
+ {
41115
+ // Not read-only: it can add a page and change which one is current.
41116
+ annotations: ADDS,
41117
+ title: "List, add or switch Figma pages",
41118
+ description: PAGES,
41119
+ inputSchema: PagesParams.shape
41120
+ },
40979
41121
  async (params) => {
40980
41122
  try {
40981
41123
  const result = PagesResult.parse(await bridge.request("pages", params));
@@ -40990,7 +41132,7 @@ function registerFind(server, bridge) {
40990
41132
  }
40991
41133
 
40992
41134
  // src/tools/assets.ts
40993
- import { readFile as readFile3 } from "node:fs/promises";
41135
+ import { readFile as readFile4 } from "node:fs/promises";
40994
41136
  import { extname } from "node:path";
40995
41137
  var MIME = /* @__PURE__ */ new Map([
40996
41138
  [".png", "image/png"],
@@ -41020,13 +41162,13 @@ function isPath(src) {
41020
41162
  return true;
41021
41163
  }
41022
41164
  async function readSvg(path) {
41023
- return readFile3(resolveInRoot(path), "utf8");
41165
+ return readFile4(resolveInRoot(path), "utf8");
41024
41166
  }
41025
41167
  async function readImage(path) {
41026
41168
  const file2 = resolveInRoot(path);
41027
41169
  const type = MIME.get(extname(file2).toLowerCase());
41028
41170
  if (!type) throw new Error(`Unsupported image type: ${extname(file2) || file2}`);
41029
- return `data:${type};base64,${(await readFile3(file2)).toString("base64")}`;
41171
+ return `data:${type};base64,${(await readFile4(file2)).toString("base64")}`;
41030
41172
  }
41031
41173
 
41032
41174
  // src/tools/patch.ts
@@ -41038,6 +41180,7 @@ function registerPatch(server, bridge) {
41038
41180
  server.registerTool(
41039
41181
  "mirra_patch",
41040
41182
  {
41183
+ annotations: CHANGES,
41041
41184
  title: "Patch existing Figma layers",
41042
41185
  description: DESCRIPTION4,
41043
41186
  inputSchema: PatchParams.shape
@@ -41062,6 +41205,7 @@ function registerPing(server, bridge) {
41062
41205
  server.registerTool(
41063
41206
  "mirra_ping",
41064
41207
  {
41208
+ annotations: READS,
41065
41209
  title: "Ping the Figma plugin",
41066
41210
  description: "Check that the Mirra plugin is connected, and report the open file, current page and selection size."
41067
41211
  },
@@ -41097,6 +41241,7 @@ function registerRead(server, bridge) {
41097
41241
  server.registerTool(
41098
41242
  "mirra_read",
41099
41243
  {
41244
+ annotations: READS,
41100
41245
  title: "Read Figma layers as a UI tree",
41101
41246
  description: DESCRIPTION5,
41102
41247
  inputSchema: ReadParams.shape
@@ -41126,6 +41271,7 @@ function registerRender(server, bridge) {
41126
41271
  server.registerTool(
41127
41272
  "mirra_render",
41128
41273
  {
41274
+ annotations: ADDS,
41129
41275
  title: "Render a UI tree into Figma",
41130
41276
  description: DESCRIPTION6,
41131
41277
  inputSchema: RenderParams.shape
@@ -41155,6 +41301,7 @@ function registerScreenshot(server, bridge) {
41155
41301
  server.registerTool(
41156
41302
  "mirra_screenshot",
41157
41303
  {
41304
+ annotations: READS,
41158
41305
  title: "Screenshot a Figma layer",
41159
41306
  description: DESCRIPTION7,
41160
41307
  inputSchema: ScreenshotParams.shape
@@ -41166,7 +41313,7 @@ function registerScreenshot(server, bridge) {
41166
41313
  content: [
41167
41314
  {
41168
41315
  type: "text",
41169
- text: `${shot.name} (${shot.width}x${shot.height}, ${shot.id})`
41316
+ text: `${shot.name} (${shot.width}x${shot.height}, ${shot.id})` + (shot.scale === 1 ? "" : ` at ${shot.scale.toFixed(2)}x`)
41170
41317
  },
41171
41318
  { type: "image", data: shot.png, mimeType: "image/png" }
41172
41319
  ]
@@ -41185,7 +41332,7 @@ bound to the design system instead of to a hardcoded value.`;
41185
41332
  function registerTokens(server, bridge) {
41186
41333
  server.registerTool(
41187
41334
  "mirra_tokens",
41188
- { title: "List design tokens", description: DESCRIPTION8 },
41335
+ { annotations: READS, title: "List design tokens", description: DESCRIPTION8 },
41189
41336
  async () => {
41190
41337
  try {
41191
41338
  const result = TokensResult.parse(await bridge.request("tokens"));
@@ -41422,7 +41569,7 @@ var USAGE = `mirra <command>
41422
41569
  doctor Bind the plugin port and wait for the Figma plugin to connect
41423
41570
 
41424
41571
  Flags:
41425
- --no-reveal init: do not open the folder in Finder
41572
+ --no-reveal init: do not open the folder in Finder on a manual import
41426
41573
  --no-config init: do not touch any client's configuration
41427
41574
 
41428
41575
  Environment:
@@ -41478,14 +41625,26 @@ async function init() {
41478
41625
  }
41479
41626
  lines.push("", "A client that was already running has to be restarted to see the tools.", "");
41480
41627
  }
41481
- lines.push(
41482
- "Then, in Figma Desktop:",
41483
- " 1. Plugins -> Development -> Import plugin from manifest...",
41484
- ` and choose ${manifest}`,
41485
- " 2. Open a file, run Mirra, and leave the panel open",
41486
- " 3. Ask the agent for mirra_ping; the panel badge should turn green"
41487
- );
41488
- if (!process.argv.includes("--no-reveal")) reveal(manifest);
41628
+ const figma = await registerPluginWithFigma(PLUGIN_HOME);
41629
+ const manual = figma.status === "skipped";
41630
+ if (manual) {
41631
+ lines.push(
41632
+ `Could not register the plugin with Figma Desktop: ${figma.reason}`,
41633
+ "Then, in Figma Desktop:",
41634
+ " 1. Plugins -> Development -> Import plugin from manifest...",
41635
+ ` and choose ${manifest}`,
41636
+ " 2. Open a file, run Mirra, and leave the panel open",
41637
+ " 3. Ask the agent for mirra_ping; the panel badge should turn green"
41638
+ );
41639
+ } else {
41640
+ lines.push(
41641
+ figma.status === "registered" ? "Registered the plugin with Figma Desktop." : "Figma Desktop already knows the plugin.",
41642
+ "Then, in Figma Desktop:",
41643
+ " 1. Open a file, run Plugins -> Development -> Mirra, and leave the panel open",
41644
+ " 2. Ask the agent for mirra_ping; the panel badge should turn green"
41645
+ );
41646
+ }
41647
+ if (manual && !process.argv.includes("--no-reveal")) reveal(manifest);
41489
41648
  console.log(lines.join("\n"));
41490
41649
  }
41491
41650
  async function doctor() {
@@ -950,7 +950,7 @@
950
950
  }
951
951
 
952
952
  // src/version.ts
953
- var PLUGIN_VERSION = "1.2.0";
953
+ var PLUGIN_VERSION = "1.3.0";
954
954
 
955
955
  // src/ops/ping.ts
956
956
  function ping() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsalpha/mirra",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Two-way MCP server for Figma: read a design and render one back through the Figma Plugin API",
5
5
  "keywords": [
6
6
  "figma",