faux-studio 0.4.7 → 0.5.1

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.
Files changed (3) hide show
  1. package/README.md +29 -6
  2. package/dist/index.js +383 -105
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -14,10 +14,17 @@ On first run, a browser window opens for Figma OAuth. After authentication, the
14
14
 
15
15
  ### Claude Code
16
16
 
17
+ ```bash
18
+ claude mcp add --scope user faux-studio -- npx faux-studio@latest
19
+ ```
20
+
21
+ Or manually add to your MCP settings:
22
+
17
23
  ```json
18
24
  {
19
25
  "mcpServers": {
20
26
  "faux-studio": {
27
+ "type": "stdio",
21
28
  "command": "npx",
22
29
  "args": ["-y", "faux-studio"]
23
30
  }
@@ -25,21 +32,38 @@ On first run, a browser window opens for Figma OAuth. After authentication, the
25
32
  }
26
33
  ```
27
34
 
28
- ### Cursor / Windsurf
35
+ ### OpenCode
29
36
 
30
- Add to your MCP settings:
37
+ manually add to your MCP settings (~/.config/opencode/opencode.json):
31
38
 
32
39
  ```json
33
40
  {
34
- "mcpServers": {
41
+ "mcp": {
35
42
  "faux-studio": {
36
- "command": "npx",
37
- "args": ["-y", "faux-studio"]
43
+ "type": "local",
44
+ "command": [
45
+ "npx",
46
+ "faux-studio@latest"
47
+ ]
38
48
  }
39
49
  }
40
50
  }
41
51
  ```
42
52
 
53
+ ### Codex
54
+
55
+ ```bash
56
+ codex mcp add faux-studio -- npx -y faux-studio@latest
57
+ ```
58
+
59
+ Or manually add to your MCP settings (~/.codex/config.toml):
60
+
61
+ ```yml
62
+ [mcp_servers.faux-studio]
63
+ command = "npx"
64
+ args = ["-y", "faux-studio@latest"]
65
+ ```
66
+
43
67
  ## How It Works
44
68
 
45
69
  faux-studio runs locally and bridges your AI client to Figma Desktop:
@@ -58,7 +82,6 @@ faux-studio runs locally and bridges your AI client to Figma Desktop:
58
82
 
59
83
  | Variable | Description |
60
84
  |----------|-------------|
61
- | `FAUX_API_KEY` | Skip OAuth — use an API key instead |
62
85
  | `FAUX_TRANSPORT` | Force transport: `cdp` or `plugin` |
63
86
 
64
87
  ## Links
package/dist/index.js CHANGED
@@ -10563,17 +10563,72 @@ var SecurityCliKeychainStore = class {
10563
10563
  }
10564
10564
  }
10565
10565
  };
10566
+ var KeyringCredentialStore = class {
10567
+ name = "keychain";
10568
+ entry;
10569
+ cached = void 0;
10570
+ constructor(entry) {
10571
+ this.entry = entry;
10572
+ }
10573
+ async load() {
10574
+ if (this.cached !== void 0) return this.cached;
10575
+ try {
10576
+ const raw = await this.entry.getPassword();
10577
+ if (!raw) {
10578
+ this.cached = null;
10579
+ return null;
10580
+ }
10581
+ const creds = JSON.parse(raw);
10582
+ if (!creds.jwt || !creds.refreshToken || !creds.user) {
10583
+ this.cached = null;
10584
+ return null;
10585
+ }
10586
+ this.cached = creds;
10587
+ return creds;
10588
+ } catch {
10589
+ this.cached = null;
10590
+ return null;
10591
+ }
10592
+ }
10593
+ async save(creds) {
10594
+ try {
10595
+ await this.entry.setPassword(JSON.stringify(creds));
10596
+ this.cached = creds;
10597
+ } catch (err) {
10598
+ warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
10599
+ }
10600
+ }
10601
+ async clear() {
10602
+ try {
10603
+ await this.entry.deletePassword();
10604
+ this.cached = null;
10605
+ } catch {
10606
+ }
10607
+ }
10608
+ };
10566
10609
  async function tryCreateKeychainStore() {
10567
- if (process.platform !== "darwin") {
10568
- log("[keychain] not macOS \u2014 skipping keychain");
10569
- return null;
10610
+ if (process.platform === "darwin") {
10611
+ try {
10612
+ log("[keychain] probing macOS security CLI...");
10613
+ const t0 = Date.now();
10614
+ await securityExec("help");
10615
+ log(`[keychain] security CLI available (${Date.now() - t0}ms)`);
10616
+ return new SecurityCliKeychainStore();
10617
+ } catch (err) {
10618
+ warn(
10619
+ `OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
10620
+ );
10621
+ return null;
10622
+ }
10570
10623
  }
10571
10624
  try {
10572
- log("[keychain] probing macOS security CLI...");
10625
+ log("[keychain] importing @napi-rs/keyring...");
10573
10626
  const t0 = Date.now();
10574
- await securityExec("help");
10575
- log(`[keychain] security CLI available (${Date.now() - t0}ms)`);
10576
- return new SecurityCliKeychainStore();
10627
+ const { AsyncEntry } = await import("@napi-rs/keyring");
10628
+ const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
10629
+ await entry.getPassword();
10630
+ log(`[keychain] keyring available (${Date.now() - t0}ms)`);
10631
+ return new KeyringCredentialStore(entry);
10577
10632
  } catch (err) {
10578
10633
  warn(
10579
10634
  `OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
@@ -10602,7 +10657,7 @@ async function initStore() {
10602
10657
  const fileCreds = await fileStore.load();
10603
10658
  if (fileCreds) {
10604
10659
  await keychainStore.save(fileCreds);
10605
- keychainStore["cached"] = void 0;
10660
+ keychainStore.cached = void 0;
10606
10661
  const verified = await keychainStore.load();
10607
10662
  if (verified) {
10608
10663
  await fileStore.clear();
@@ -10626,24 +10681,33 @@ function isExpiringSoon(creds) {
10626
10681
  const expiresAt = new Date(creds.expiresAt).getTime();
10627
10682
  return Date.now() > expiresAt - REFRESH_BUFFER_MS;
10628
10683
  }
10684
+ var inflightRefresh = null;
10629
10685
  async function refreshJwt(creds) {
10630
- const res = await fetch(`${AUTH_BASE}/auth/refresh`, {
10631
- method: "POST",
10632
- headers: { "Content-Type": "application/json" },
10633
- body: JSON.stringify({ refreshToken: creds.refreshToken })
10634
- });
10635
- if (!res.ok) {
10636
- const text = await res.text().catch(() => "");
10637
- throw new Error(`Token refresh failed (HTTP ${res.status})${text ? ": " + text : ""}`);
10686
+ if (inflightRefresh) {
10687
+ return inflightRefresh;
10638
10688
  }
10639
- const data = await res.json();
10640
- return {
10641
- jwt: data.jwt,
10642
- refreshToken: data.refreshToken,
10643
- expiresAt: new Date(Date.now() + data.expiresIn * 1e3).toISOString(),
10644
- user: creds.user
10645
- // Refresh endpoint doesn't return user info — keep existing
10689
+ const doRefresh = async () => {
10690
+ const res = await fetch(`${AUTH_BASE}/auth/refresh`, {
10691
+ method: "POST",
10692
+ headers: { "Content-Type": "application/json" },
10693
+ body: JSON.stringify({ refreshToken: creds.refreshToken })
10694
+ });
10695
+ if (!res.ok) {
10696
+ const text = await res.text().catch(() => "");
10697
+ throw new Error(`Token refresh failed (HTTP ${res.status})${text ? ": " + text : ""}`);
10698
+ }
10699
+ const data = await res.json();
10700
+ return {
10701
+ jwt: data.jwt,
10702
+ refreshToken: data.refreshToken,
10703
+ expiresAt: new Date(Date.now() + data.expiresIn * 1e3).toISOString(),
10704
+ user: creds.user
10705
+ };
10646
10706
  };
10707
+ inflightRefresh = doRefresh().finally(() => {
10708
+ inflightRefresh = null;
10709
+ });
10710
+ return inflightRefresh;
10647
10711
  }
10648
10712
  function openBrowser(url2) {
10649
10713
  let child;
@@ -10919,6 +10983,124 @@ async function generateScript(jwt2, toolName, params) {
10919
10983
  return { script: data.script, signature: data.signature };
10920
10984
  }
10921
10985
 
10986
+ // src/icons.ts
10987
+ function parseIconName(iconName) {
10988
+ if (iconName.includes(":")) {
10989
+ const [prefix, name] = iconName.split(":");
10990
+ return { prefix, name };
10991
+ }
10992
+ const parts = iconName.split("-");
10993
+ if (parts.length > 1) {
10994
+ return { prefix: parts[0], name: parts.slice(1).join("-") };
10995
+ }
10996
+ return { prefix: "lucide", name: iconName };
10997
+ }
10998
+ async function fetchIconSvg(iconName, size = 24) {
10999
+ const { prefix, name } = parseIconName(iconName);
11000
+ const url2 = `https://api.iconify.design/${prefix}/${name}.svg?width=${size}&height=${size}`;
11001
+ const response = await fetch(url2);
11002
+ if (!response.ok) {
11003
+ if (response.status === 404) throw new Error(`Icon not found: ${iconName}`);
11004
+ throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
11005
+ }
11006
+ return await response.text();
11007
+ }
11008
+ function detectIconType(svgContent) {
11009
+ if (svgContent.includes('stroke="currentColor"') || svgContent.includes("stroke='currentColor'")) {
11010
+ return "stroke";
11011
+ }
11012
+ if (svgContent.includes('opacity="0.') || svgContent.includes("opacity='0.")) {
11013
+ return "duotone";
11014
+ }
11015
+ return "fill";
11016
+ }
11017
+ async function searchSingleQuery(query, limit, prefix, category) {
11018
+ let url2 = `https://api.iconify.design/search?query=${encodeURIComponent(query)}&limit=${limit}`;
11019
+ if (prefix) url2 += `&prefix=${encodeURIComponent(prefix)}`;
11020
+ if (category) url2 += `&category=${encodeURIComponent(category)}`;
11021
+ const response = await fetch(url2);
11022
+ if (!response.ok) {
11023
+ if (response.status === 404) throw new Error("Icon search is currently disabled");
11024
+ throw new Error(`Search failed: HTTP ${response.status}`);
11025
+ }
11026
+ const data = await response.json();
11027
+ const icons = data.icons || [];
11028
+ return { query, total: icons.length, icons };
11029
+ }
11030
+ async function searchIcons(params) {
11031
+ const { limit = 3, prefix, category } = params;
11032
+ const effectiveLimit = Math.min(limit, 100);
11033
+ if (params.queries && params.queries.length > 0) {
11034
+ const results = await Promise.all(
11035
+ params.queries.map((q) => searchSingleQuery(q.trim(), effectiveLimit, prefix, category))
11036
+ );
11037
+ return { results, totalQueries: results.length };
11038
+ }
11039
+ const query = params.query;
11040
+ if (!query || query.trim().length === 0) {
11041
+ throw new Error("Search query is required (provide `query` or `queries`)");
11042
+ }
11043
+ return searchSingleQuery(query.trim(), effectiveLimit, prefix, category);
11044
+ }
11045
+ async function resolveIconsInNode(node) {
11046
+ if (node.$icon && typeof node.$icon === "string") {
11047
+ const searchTerm = node.$icon;
11048
+ const size = typeof node.size === "number" ? node.size : 24;
11049
+ try {
11050
+ const searchResult = await searchIcons({ query: searchTerm, limit: 1 });
11051
+ if (searchResult.icons.length === 0) {
11052
+ warn(`No icons found for: ${searchTerm}`);
11053
+ const { $icon: $icon2, ...rest2 } = node;
11054
+ return { ...rest2, $iconError: `No icons found for: ${searchTerm}` };
11055
+ }
11056
+ const iconName = searchResult.icons[0];
11057
+ const svgContent = await fetchIconSvg(iconName, size);
11058
+ const iconType = detectIconType(svgContent);
11059
+ const { $icon, ...rest } = node;
11060
+ return {
11061
+ ...rest,
11062
+ $iconResolved: { svgContent, iconType, iconName, searchTerm }
11063
+ };
11064
+ } catch (error3) {
11065
+ error(`Failed to resolve icon "${searchTerm}": ${error3 instanceof Error ? error3.message : String(error3)}`);
11066
+ const { $icon, ...rest } = node;
11067
+ return { ...rest, $iconError: `Failed to load icon: ${error3 instanceof Error ? error3.message : "Unknown error"}` };
11068
+ }
11069
+ }
11070
+ let result = { ...node };
11071
+ if (node.children && Array.isArray(node.children)) {
11072
+ result.children = await Promise.all(
11073
+ node.children.map(async (child) => {
11074
+ if (child && typeof child === "object") {
11075
+ return resolveIconsInNode(child);
11076
+ }
11077
+ return child;
11078
+ })
11079
+ );
11080
+ }
11081
+ if (node.template && typeof node.template === "object") {
11082
+ result.template = await resolveIconsInNode(node.template);
11083
+ }
11084
+ return result;
11085
+ }
11086
+ var ICON_TOOLS = /* @__PURE__ */ new Set(["create_from_schema", "modify_via_schema"]);
11087
+ async function preprocessIconsInParams(toolName, params) {
11088
+ if (!ICON_TOOLS.has(toolName)) return params;
11089
+ const result = { ...params };
11090
+ if (result.schema && typeof result.schema === "object") {
11091
+ result.schema = await resolveIconsInNode(result.schema);
11092
+ }
11093
+ if (result.modifications && Array.isArray(result.modifications)) {
11094
+ result.modifications = await Promise.all(
11095
+ result.modifications.map(async (mod) => ({
11096
+ ...mod,
11097
+ schema: await resolveIconsInNode(mod.schema)
11098
+ }))
11099
+ );
11100
+ }
11101
+ return result;
11102
+ }
11103
+
10922
11104
  // node_modules/ws/wrapper.mjs
10923
11105
  var import_stream = __toESM(require_stream(), 1);
10924
11106
  var import_receiver = __toESM(require_receiver(), 1);
@@ -11396,7 +11578,7 @@ var PORT_RANGE = 8;
11396
11578
  var SCRIPT_TIMEOUT_MS = 6e4;
11397
11579
  var CONNECT_WAIT_MS = 3e4;
11398
11580
  var FAUX_DIR3 = join4(homedir3(), ".faux");
11399
- var PluginWsServer = class {
11581
+ var PluginWsServer = class _PluginWsServer {
11400
11582
  name = "plugin-ws";
11401
11583
  wss = null;
11402
11584
  connections = /* @__PURE__ */ new Map();
@@ -11404,6 +11586,17 @@ var PluginWsServer = class {
11404
11586
  connectWaiters = [];
11405
11587
  _activeFileId = null;
11406
11588
  _port = 0;
11589
+ // File tracking state (mirrors CdpFileTracker API)
11590
+ _lastAcknowledgedFileId = null;
11591
+ _lastAcknowledgedFileName = null;
11592
+ _lastAcknowledgedAt = 0;
11593
+ _lastFocusEventAt = 0;
11594
+ /** How long before focus events are considered stale for high-confidence detection. */
11595
+ static FOCUS_FRESH_MS = 12e4;
11596
+ // 2 minutes
11597
+ /** How long before last-known file is considered stale and untrusted (matches CDP). */
11598
+ static LAST_KNOWN_STALE_MS = 5 * 6e4;
11599
+ // 5 minutes
11407
11600
  // -------------------------------------------------------------------------
11408
11601
  // Public Getters
11409
11602
  // -------------------------------------------------------------------------
@@ -11422,6 +11615,79 @@ var PluginWsServer = class {
11422
11615
  get hasConnections() {
11423
11616
  return this.connections.size > 0;
11424
11617
  }
11618
+ get activeFileName() {
11619
+ if (!this._activeFileId) return null;
11620
+ return this.connections.get(this._activeFileId)?.fileName ?? null;
11621
+ }
11622
+ get lastAcknowledgedFileName() {
11623
+ return this._lastAcknowledgedFileName;
11624
+ }
11625
+ // -------------------------------------------------------------------------
11626
+ // File Tracking
11627
+ // -------------------------------------------------------------------------
11628
+ /**
11629
+ * Detect the active file for the plugin transport.
11630
+ * Unlike CDP (which introspects shell.html), this is synchronous — the
11631
+ * plugin tells us via file-focus events. The cacheTtlMs parameter is
11632
+ * accepted for API compatibility but detection is always in-memory.
11633
+ */
11634
+ detectActiveFile(_cacheTtlMs = 2e3) {
11635
+ const allFiles = this.getAllFileInfos();
11636
+ if (this.connections.size === 0) {
11637
+ return { activeFile: null, allFiles, method: "none", confidence: "none" };
11638
+ }
11639
+ if (this.connections.size === 1) {
11640
+ const conn = this.connections.values().next().value;
11641
+ return { activeFile: this.connToFileInfo(conn), allFiles, method: "single-file", confidence: "high" };
11642
+ }
11643
+ if (this._activeFileId && this.connections.has(this._activeFileId)) {
11644
+ const conn = this.connections.get(this._activeFileId);
11645
+ const info = this.connToFileInfo(conn);
11646
+ const focusAge = Date.now() - this._lastFocusEventAt;
11647
+ if (this._lastFocusEventAt > 0 && focusAge < _PluginWsServer.FOCUS_FRESH_MS) {
11648
+ return { activeFile: info, allFiles, method: "focus-event", confidence: "high" };
11649
+ }
11650
+ if (this.isLastKnownStale()) {
11651
+ log("File tracker: focus and acknowledgment both stale \u2014 cannot detect active file");
11652
+ return { activeFile: null, allFiles, method: "none", confidence: "none" };
11653
+ }
11654
+ return { activeFile: info, allFiles, method: "last-known", confidence: "low" };
11655
+ }
11656
+ return { activeFile: null, allFiles, method: "none", confidence: "none" };
11657
+ }
11658
+ /** Whether the active file has changed since the last acknowledged execution. */
11659
+ hasFileChanged() {
11660
+ return this._activeFileId !== null && this._lastAcknowledgedFileId !== null && this._activeFileId !== this._lastAcknowledgedFileId;
11661
+ }
11662
+ /**
11663
+ * Acknowledge the current active file as the intended target.
11664
+ * Call AFTER successful script execution.
11665
+ */
11666
+ acknowledgeActiveFile() {
11667
+ if (this._activeFileId) {
11668
+ this._lastAcknowledgedFileId = this._activeFileId;
11669
+ this._lastAcknowledgedFileName = this.connections.get(this._activeFileId)?.fileName ?? null;
11670
+ this._lastAcknowledgedAt = Date.now();
11671
+ }
11672
+ }
11673
+ /** True if the last acknowledged execution was more than 5 minutes ago. */
11674
+ isLastKnownStale() {
11675
+ if (this._lastAcknowledgedAt === 0) return false;
11676
+ return Date.now() - this._lastAcknowledgedAt > _PluginWsServer.LAST_KNOWN_STALE_MS;
11677
+ }
11678
+ /** All connected files as FigmaFileInfo[]. */
11679
+ getAllFileInfos() {
11680
+ return Array.from(this.connections.values()).map((c) => this.connToFileInfo(c));
11681
+ }
11682
+ /** Build FigmaFileInfo from a PluginConnection. */
11683
+ connToFileInfo(conn) {
11684
+ return {
11685
+ fileKey: conn.fileId,
11686
+ fileName: conn.fileName,
11687
+ targetId: conn.fileId,
11688
+ wsUrl: ""
11689
+ };
11690
+ }
11425
11691
  // -------------------------------------------------------------------------
11426
11692
  // Server Lifecycle
11427
11693
  // -------------------------------------------------------------------------
@@ -11516,6 +11782,7 @@ var PluginWsServer = class {
11516
11782
  this.connections.set(fileId, { ws, fileId, fileName, connectedAt: Date.now() });
11517
11783
  if (this.connections.size === 1 || !this._activeFileId) {
11518
11784
  this._activeFileId = fileId;
11785
+ this._lastFocusEventAt = Date.now();
11519
11786
  }
11520
11787
  log(`Plugin connected: "${fileName}" (${fileId}) [${this.connections.size} file(s)]`);
11521
11788
  for (const waiter of this.connectWaiters) {
@@ -11576,6 +11843,7 @@ var PluginWsServer = class {
11576
11843
  const focusFileId = msg.fileId || fileId;
11577
11844
  if (this.connections.has(focusFileId)) {
11578
11845
  this._activeFileId = focusFileId;
11846
+ this._lastFocusEventAt = Date.now();
11579
11847
  }
11580
11848
  break;
11581
11849
  }
@@ -25988,10 +26256,10 @@ var RESOURCES = [
25988
26256
  var INSTRUCTIONS = `You are connected to Figma Desktop via faux-studio. You can create, modify, and inspect designs using the tools below.
25989
26257
 
25990
26258
  ## Transport
25991
- faux-studio connects to Figma Desktop via **CDP (Chrome DevTools Protocol)** by default \u2014 no plugin needed. It launches Figma with a debug port and communicates directly. If the Figma plugin is installed and running, it can also connect via plugin WebSocket as a fallback. CDP is the preferred zero-setup transport.
26259
+ faux-studio connects to Figma Desktop via the **faux-studio plugin** by default \u2014 install and run the plugin from Figma's Plugins menu. The plugin communicates via local WebSocket with real-time file focus tracking. If the plugin is not available, faux-studio falls back to CDP (Chrome DevTools Protocol) which requires launching Figma with a debug port. The plugin is the preferred transport.
25992
26260
 
25993
26261
  ## Workflow
25994
- 1. Call \`setup_figma\` first to ensure Figma is running and connected (via CDP or plugin).
26262
+ 1. Call \`setup_figma\` first to ensure Figma is running and connected (via plugin or CDP).
25995
26263
  2. Call \`get_page_structure\` or \`get_screenshot\` to understand what's on the canvas.
25996
26264
  3. Use \`create_from_schema\` for all creation \u2014 it accepts declarative JSON schemas.
25997
26265
  4. Use \`modify_via_schema\` to change existing nodes.
@@ -26031,7 +26299,7 @@ Resources provide quick read-only access to Figma state without tool calls:
26031
26299
  - Create components for reusable UI patterns.`;
26032
26300
  function createMcpServer(deps) {
26033
26301
  const server2 = new Server(
26034
- { name: "faux-studio", version: "0.4.7" },
26302
+ { name: "faux-studio", version: "0.5.1" },
26035
26303
  {
26036
26304
  capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
26037
26305
  instructions: INSTRUCTIONS
@@ -26046,7 +26314,7 @@ function createMcpServer(deps) {
26046
26314
  },
26047
26315
  {
26048
26316
  name: "setup_figma",
26049
- description: "Ensure Figma Desktop is running and connected via CDP (preferred) or plugin WebSocket. Call this before any design work. Launches Figma with CDP debug port if needed, detects open files and the active tab. Returns connection status, transport mode, active file, and list of all open design files. Idempotent \u2014 safe to call multiple times.",
26317
+ description: "Ensure Figma Desktop is running and connected via plugin WebSocket (preferred) or CDP (fallback). Call this before any design work. Detects open files and the active tab. Returns connection status, transport mode, active file, and list of all open design files. Idempotent \u2014 safe to call multiple times.",
26050
26318
  inputSchema: {
26051
26319
  type: "object",
26052
26320
  properties: {},
@@ -26133,6 +26401,13 @@ function createMcpServer(deps) {
26133
26401
  throw err;
26134
26402
  }
26135
26403
  }
26404
+ if (name === "search_icons") {
26405
+ log("search_icons called (server-side)");
26406
+ const result2 = await searchIcons(params);
26407
+ return {
26408
+ content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
26409
+ };
26410
+ }
26136
26411
  const intents = {
26137
26412
  meso: typeof params._intent_meso === "string" ? params._intent_meso : void 0,
26138
26413
  macro: typeof params._intent_macro === "string" ? params._intent_macro : void 0
@@ -26296,6 +26571,24 @@ async function tryConnectCdp() {
26296
26571
  }
26297
26572
  }
26298
26573
  async function executeScript(script, intents, opts) {
26574
+ if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26575
+ const detection = pluginServer.detectActiveFile(opts?.cacheTtlMs);
26576
+ if (!detection.activeFile && detection.allFiles.length > 1) {
26577
+ throw new FileUnknownError(detection.allFiles);
26578
+ }
26579
+ if (pluginServer.hasFileChanged()) {
26580
+ throw new FileChangedError(
26581
+ pluginServer.lastAcknowledgedFileName,
26582
+ pluginServer.activeFileName,
26583
+ pluginServer.activeFileId,
26584
+ detection.allFiles
26585
+ );
26586
+ }
26587
+ log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
26588
+ const result = await pluginServer.executeScript(script, void 0, intents);
26589
+ pluginServer.acknowledgeActiveFile();
26590
+ return result;
26591
+ }
26299
26592
  if (forceTransport !== "plugin") {
26300
26593
  const client = await tryConnectCdp();
26301
26594
  if (client) {
@@ -26332,10 +26625,6 @@ async function executeScript(script, intents, opts) {
26332
26625
  }
26333
26626
  }
26334
26627
  }
26335
- if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26336
- log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
26337
- return pluginServer.executeScript(script, void 0, intents);
26338
- }
26339
26628
  log("No transport available, waiting for plugin...");
26340
26629
  return pluginServer.executeScript(script, void 0, intents);
26341
26630
  }
@@ -26361,6 +26650,7 @@ async function recoverCdp(client, script) {
26361
26650
  async function generateWithAuth(toolName, params) {
26362
26651
  await lazyInit();
26363
26652
  auth = await refreshIfNeeded(auth);
26653
+ params = await preprocessIconsInParams(toolName, params);
26364
26654
  let result;
26365
26655
  try {
26366
26656
  result = await generateScript(auth.jwt, toolName, params);
@@ -26398,16 +26688,23 @@ async function waitForPlugin() {
26398
26688
  return false;
26399
26689
  }
26400
26690
  function pluginReadyResult() {
26691
+ const detection = pluginServer.detectActiveFile();
26692
+ const activeName = detection.activeFile?.fileName;
26693
+ const fileCount = detection.allFiles.length;
26401
26694
  return {
26402
26695
  status: "ready",
26403
26696
  transport: "plugin",
26404
- message: `Connected via plugin.${pluginServer.activeFileId ? ` Active file: ${pluginServer.activeFileId}` : ""} Ready to design.`,
26405
- activeFile: pluginServer.activeFileId || void 0,
26697
+ message: `Connected via plugin.${activeName ? ` Active file: "${activeName}".` : ""} ${fileCount} file(s) open. Ready to design.`,
26698
+ activeFile: activeName || void 0,
26406
26699
  pluginFiles: pluginServer.connectedFiles,
26700
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
26407
26701
  port: pluginServer.port
26408
26702
  };
26409
26703
  }
26410
26704
  async function setupFigma(params) {
26705
+ if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26706
+ return pluginReadyResult();
26707
+ }
26411
26708
  if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
26412
26709
  if (fileTracker) {
26413
26710
  const detection = await fileTracker.detectActiveFile();
@@ -26425,40 +26722,6 @@ async function setupFigma(params) {
26425
26722
  message: "Connected via CDP. Ready to design."
26426
26723
  };
26427
26724
  }
26428
- if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26429
- return pluginReadyResult();
26430
- }
26431
- if (forceTransport !== "plugin") {
26432
- const existing = await probeCdpPorts();
26433
- if (existing) {
26434
- const target = findFigmaDesignTarget(existing.targets);
26435
- if (target) {
26436
- try {
26437
- cdpClient?.close();
26438
- cdpClient = null;
26439
- const client = new CdpClient();
26440
- await client.connect(target.webSocketDebuggerUrl);
26441
- await client.discoverFigmaContext();
26442
- cdpClient = client;
26443
- log(`CDP connected: ${target.title}`);
26444
- if (!fileTracker) {
26445
- fileTracker = new CdpFileTracker(existing.port);
26446
- }
26447
- const detection = await fileTracker.detectActiveFile();
26448
- fileTracker.acknowledgeActiveFile();
26449
- return {
26450
- status: "ready",
26451
- transport: "cdp",
26452
- message: `Connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
26453
- activeFile: detection.activeFile?.fileName ?? target.title,
26454
- openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
26455
- port: existing.port
26456
- };
26457
- } catch {
26458
- }
26459
- }
26460
- }
26461
- }
26462
26725
  if (!findFigmaPath()) {
26463
26726
  return {
26464
26727
  status: "not_installed",
@@ -26467,17 +26730,7 @@ async function setupFigma(params) {
26467
26730
  };
26468
26731
  }
26469
26732
  if (!isFigmaRunning()) {
26470
- if (forceTransport === "plugin") {
26471
- try {
26472
- launchFigma();
26473
- } catch (err) {
26474
- return {
26475
- status: "not_installed",
26476
- transport: "none",
26477
- message: err instanceof Error ? err.message : "Failed to launch Figma."
26478
- };
26479
- }
26480
- } else {
26733
+ if (forceTransport === "cdp") {
26481
26734
  try {
26482
26735
  const connection = await launchFigmaWithCdp();
26483
26736
  const target = findFigmaDesignTarget(connection.targets);
@@ -26514,33 +26767,58 @@ async function setupFigma(params) {
26514
26767
  message: err instanceof Error ? err.message : "Failed to launch Figma."
26515
26768
  };
26516
26769
  }
26770
+ } else {
26771
+ try {
26772
+ launchFigma();
26773
+ } catch (err) {
26774
+ return {
26775
+ status: "not_installed",
26776
+ transport: "none",
26777
+ message: err instanceof Error ? err.message : "Failed to launch Figma."
26778
+ };
26779
+ }
26517
26780
  }
26518
26781
  }
26519
- if (forceTransport !== "plugin") {
26520
- return {
26521
- status: "waiting_for_plugin",
26522
- transport: "none",
26523
- message: `Figma is running but the CDP debug port is not enabled.
26524
-
26525
- To fix this:
26526
- 1. Quit Figma completely (Cmd+Q / Alt+F4)
26527
- 2. Call setup_figma again \u2014 it will relaunch Figma with CDP enabled
26528
-
26529
- Alternatively, install the faux-studio plugin:
26530
- 1. Download: ${PLUGIN_URL}
26531
- 2. In Figma: Plugins \u2192 Development \u2192 Import plugin from manifest
26532
- 3. Run: Plugins \u2192 Development \u2192 faux-studio
26533
- 4. Call setup_figma again once the plugin shows "Ready".`,
26534
- port: pluginServer.port
26535
- };
26782
+ if (forceTransport !== "cdp") {
26783
+ log("Waiting for plugin connection...");
26784
+ const connected = await waitForPlugin();
26785
+ if (connected) {
26786
+ return {
26787
+ ...pluginReadyResult(),
26788
+ status: isFigmaRunning() ? "ready" : "launched"
26789
+ };
26790
+ }
26536
26791
  }
26537
- log("Waiting for plugin connection...");
26538
- const connected = await waitForPlugin();
26539
- if (connected) {
26540
- return {
26541
- ...pluginReadyResult(),
26542
- status: isFigmaRunning() ? "ready" : "launched"
26543
- };
26792
+ if (forceTransport !== "plugin") {
26793
+ const existing = await probeCdpPorts();
26794
+ if (existing) {
26795
+ const target = findFigmaDesignTarget(existing.targets);
26796
+ if (target) {
26797
+ try {
26798
+ cdpClient?.close();
26799
+ cdpClient = null;
26800
+ const client = new CdpClient();
26801
+ await client.connect(target.webSocketDebuggerUrl);
26802
+ await client.discoverFigmaContext();
26803
+ cdpClient = client;
26804
+ log(`CDP connected: ${target.title}`);
26805
+ if (!fileTracker) {
26806
+ fileTracker = new CdpFileTracker(existing.port);
26807
+ }
26808
+ const detection = await fileTracker.detectActiveFile();
26809
+ fileTracker.acknowledgeActiveFile();
26810
+ return {
26811
+ status: "ready",
26812
+ transport: "cdp",
26813
+ message: `Connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
26814
+ activeFile: detection.activeFile?.fileName ?? target.title,
26815
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
26816
+ port: existing.port
26817
+ };
26818
+ } catch {
26819
+ }
26820
+ }
26821
+ }
26544
26822
  }
26545
26823
  return {
26546
26824
  status: "waiting_for_plugin",
@@ -26561,7 +26839,7 @@ Call setup_figma again once the plugin shows "Ready".`,
26561
26839
  }
26562
26840
  async function main() {
26563
26841
  const startupT0 = Date.now();
26564
- log(`faux-studio v${"0.4.7"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
26842
+ log(`faux-studio v${"0.5.1"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
26565
26843
  try {
26566
26844
  const port = await pluginServer.start();
26567
26845
  if (forceTransport === "plugin") {
@@ -26569,7 +26847,7 @@ async function main() {
26569
26847
  } else if (forceTransport === "cdp") {
26570
26848
  log(`Transport: CDP-only mode (FAUX_TRANSPORT=cdp) \u2014 plugin WS on port ${port} (standby)`);
26571
26849
  } else {
26572
- log(`Transport: auto \u2014 CDP preferred, plugin WS on port ${port} (fallback)`);
26850
+ log(`Transport: auto \u2014 plugin preferred (port ${port}), CDP fallback`);
26573
26851
  }
26574
26852
  } catch (err) {
26575
26853
  warn(`Plugin WS server failed to start: ${err instanceof Error ? err.message : err}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faux-studio",
3
- "version": "0.4.7",
3
+ "version": "0.5.1",
4
4
  "description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,5 +48,8 @@
48
48
  "darwin",
49
49
  "linux",
50
50
  "win32"
51
- ]
51
+ ],
52
+ "optionalDependencies": {
53
+ "@napi-rs/keyring": "^1.2.0"
54
+ }
52
55
  }