faux-studio 0.4.5 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +29 -6
  2. package/dist/index.js +454 -43
  3. package/package.json +6 -1
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
@@ -10447,6 +10447,7 @@ function error(message) {
10447
10447
 
10448
10448
  // src/credential-store.ts
10449
10449
  import { readFile, writeFile, mkdir, unlink } from "fs/promises";
10450
+ import { execFile } from "child_process";
10450
10451
  import { join } from "path";
10451
10452
  import { homedir } from "os";
10452
10453
  var FAUX_DIR = join(homedir(), ".faux");
@@ -10480,7 +10481,89 @@ var FileCredentialStore = class {
10480
10481
  }
10481
10482
  }
10482
10483
  };
10483
- var KeychainCredentialStore = class {
10484
+ function securityExec(...args) {
10485
+ return new Promise((resolve, reject) => {
10486
+ execFile("/usr/bin/security", args, { timeout: 1e4 }, (err, stdout, stderr) => {
10487
+ if (err) return reject(err);
10488
+ resolve({ stdout, stderr });
10489
+ });
10490
+ });
10491
+ }
10492
+ var SecurityCliKeychainStore = class {
10493
+ name = "keychain";
10494
+ cached = void 0;
10495
+ async load() {
10496
+ if (this.cached !== void 0) return this.cached;
10497
+ try {
10498
+ const { stdout } = await securityExec(
10499
+ "find-generic-password",
10500
+ "-s",
10501
+ KEYCHAIN_SERVICE,
10502
+ "-a",
10503
+ KEYCHAIN_ACCOUNT,
10504
+ "-w"
10505
+ );
10506
+ const raw = stdout.trim();
10507
+ if (!raw) {
10508
+ this.cached = null;
10509
+ return null;
10510
+ }
10511
+ const creds = JSON.parse(raw);
10512
+ if (!creds.jwt || !creds.refreshToken || !creds.user) {
10513
+ this.cached = null;
10514
+ return null;
10515
+ }
10516
+ this.cached = creds;
10517
+ return creds;
10518
+ } catch {
10519
+ this.cached = null;
10520
+ return null;
10521
+ }
10522
+ }
10523
+ async save(creds) {
10524
+ const value = JSON.stringify(creds);
10525
+ try {
10526
+ await securityExec(
10527
+ "delete-generic-password",
10528
+ "-s",
10529
+ KEYCHAIN_SERVICE,
10530
+ "-a",
10531
+ KEYCHAIN_ACCOUNT
10532
+ );
10533
+ } catch {
10534
+ }
10535
+ try {
10536
+ await securityExec(
10537
+ "add-generic-password",
10538
+ "-s",
10539
+ KEYCHAIN_SERVICE,
10540
+ "-a",
10541
+ KEYCHAIN_ACCOUNT,
10542
+ "-w",
10543
+ value,
10544
+ "-A",
10545
+ "-U"
10546
+ );
10547
+ this.cached = creds;
10548
+ } catch (err) {
10549
+ warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
10550
+ }
10551
+ }
10552
+ async clear() {
10553
+ try {
10554
+ await securityExec(
10555
+ "delete-generic-password",
10556
+ "-s",
10557
+ KEYCHAIN_SERVICE,
10558
+ "-a",
10559
+ KEYCHAIN_ACCOUNT
10560
+ );
10561
+ this.cached = null;
10562
+ } catch {
10563
+ }
10564
+ }
10565
+ };
10566
+ var KeyringCredentialStore = class {
10484
10567
  name = "keychain";
10485
10568
  entry;
10486
10569
  cached = void 0;
@@ -10524,11 +10607,28 @@ var KeychainCredentialStore = class {
10524
10607
  }
10525
10608
  };
10526
10609
  async function tryCreateKeychainStore() {
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
+ }
10623
+ }
10527
10624
  try {
10625
+ log("[keychain] importing @napi-rs/keyring...");
10626
+ const t0 = Date.now();
10528
10627
  const { AsyncEntry } = await import("@napi-rs/keyring");
10529
10628
  const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
10530
10629
  await entry.getPassword();
10531
- return new KeychainCredentialStore(entry);
10630
+ log(`[keychain] keyring available (${Date.now() - t0}ms)`);
10631
+ return new KeyringCredentialStore(entry);
10532
10632
  } catch (err) {
10533
10633
  warn(
10534
10634
  `OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
@@ -10544,9 +10644,12 @@ function createCredentialStore() {
10544
10644
  return initPromise;
10545
10645
  }
10546
10646
  async function initStore() {
10647
+ log("[credential-store] initializing...");
10648
+ const t0 = Date.now();
10547
10649
  const fileStore = new FileCredentialStore();
10548
10650
  const keychainStore = await tryCreateKeychainStore();
10549
10651
  if (!keychainStore) {
10652
+ log(`[credential-store] using file store (${Date.now() - t0}ms)`);
10550
10653
  return fileStore;
10551
10654
  }
10552
10655
  const keychainCreds = await keychainStore.load();
@@ -10554,6 +10657,7 @@ async function initStore() {
10554
10657
  const fileCreds = await fileStore.load();
10555
10658
  if (fileCreds) {
10556
10659
  await keychainStore.save(fileCreds);
10660
+ keychainStore.cached = void 0;
10557
10661
  const verified = await keychainStore.load();
10558
10662
  if (verified) {
10559
10663
  await fileStore.clear();
@@ -10564,6 +10668,7 @@ async function initStore() {
10564
10668
  }
10565
10669
  }
10566
10670
  }
10671
+ log(`[credential-store] using keychain store (${Date.now() - t0}ms)`);
10567
10672
  return keychainStore;
10568
10673
  }
10569
10674
 
@@ -10660,6 +10765,8 @@ async function authenticate() {
10660
10765
  );
10661
10766
  }
10662
10767
  async function ensureAuth() {
10768
+ log("[auth] ensureAuth() started");
10769
+ const t0 = Date.now();
10663
10770
  const apiKey = process.env.FAUX_API_KEY;
10664
10771
  if (apiKey) {
10665
10772
  log("Using FAUX_API_KEY from environment");
@@ -10670,7 +10777,9 @@ async function ensureAuth() {
10670
10777
  source: "api-key"
10671
10778
  };
10672
10779
  }
10780
+ log("[auth] creating credential store...");
10673
10781
  const credStore = await createCredentialStore();
10782
+ log(`[auth] credential store ready (${credStore.name}) in ${Date.now() - t0}ms`);
10674
10783
  const saved = await credStore.load();
10675
10784
  if (saved) {
10676
10785
  if (!isExpiringSoon(saved)) {
@@ -10865,6 +10974,124 @@ async function generateScript(jwt2, toolName, params) {
10865
10974
  return { script: data.script, signature: data.signature };
10866
10975
  }
10867
10976
 
10977
+ // src/icons.ts
10978
+ function parseIconName(iconName) {
10979
+ if (iconName.includes(":")) {
10980
+ const [prefix, name] = iconName.split(":");
10981
+ return { prefix, name };
10982
+ }
10983
+ const parts = iconName.split("-");
10984
+ if (parts.length > 1) {
10985
+ return { prefix: parts[0], name: parts.slice(1).join("-") };
10986
+ }
10987
+ return { prefix: "lucide", name: iconName };
10988
+ }
10989
+ async function fetchIconSvg(iconName, size = 24) {
10990
+ const { prefix, name } = parseIconName(iconName);
10991
+ const url2 = `https://api.iconify.design/${prefix}/${name}.svg?width=${size}&height=${size}`;
10992
+ const response = await fetch(url2);
10993
+ if (!response.ok) {
10994
+ if (response.status === 404) throw new Error(`Icon not found: ${iconName}`);
10995
+ throw new Error(`Failed to fetch icon: HTTP ${response.status}`);
10996
+ }
10997
+ return await response.text();
10998
+ }
10999
+ function detectIconType(svgContent) {
11000
+ if (svgContent.includes('stroke="currentColor"') || svgContent.includes("stroke='currentColor'")) {
11001
+ return "stroke";
11002
+ }
11003
+ if (svgContent.includes('opacity="0.') || svgContent.includes("opacity='0.")) {
11004
+ return "duotone";
11005
+ }
11006
+ return "fill";
11007
+ }
11008
+ async function searchSingleQuery(query, limit, prefix, category) {
11009
+ let url2 = `https://api.iconify.design/search?query=${encodeURIComponent(query)}&limit=${limit}`;
11010
+ if (prefix) url2 += `&prefix=${encodeURIComponent(prefix)}`;
11011
+ if (category) url2 += `&category=${encodeURIComponent(category)}`;
11012
+ const response = await fetch(url2);
11013
+ if (!response.ok) {
11014
+ if (response.status === 404) throw new Error("Icon search is currently disabled");
11015
+ throw new Error(`Search failed: HTTP ${response.status}`);
11016
+ }
11017
+ const data = await response.json();
11018
+ const icons = data.icons || [];
11019
+ return { query, total: icons.length, icons };
11020
+ }
11021
+ async function searchIcons(params) {
11022
+ const { limit = 3, prefix, category } = params;
11023
+ const effectiveLimit = Math.min(limit, 100);
11024
+ if (params.queries && params.queries.length > 0) {
11025
+ const results = await Promise.all(
11026
+ params.queries.map((q) => searchSingleQuery(q.trim(), effectiveLimit, prefix, category))
11027
+ );
11028
+ return { results, totalQueries: results.length };
11029
+ }
11030
+ const query = params.query;
11031
+ if (!query || query.trim().length === 0) {
11032
+ throw new Error("Search query is required (provide `query` or `queries`)");
11033
+ }
11034
+ return searchSingleQuery(query.trim(), effectiveLimit, prefix, category);
11035
+ }
11036
+ async function resolveIconsInNode(node) {
11037
+ if (node.$icon && typeof node.$icon === "string") {
11038
+ const searchTerm = node.$icon;
11039
+ const size = typeof node.size === "number" ? node.size : 24;
11040
+ try {
11041
+ const searchResult = await searchIcons({ query: searchTerm, limit: 1 });
11042
+ if (searchResult.icons.length === 0) {
11043
+ warn(`No icons found for: ${searchTerm}`);
11044
+ const { $icon: $icon2, ...rest2 } = node;
11045
+ return { ...rest2, $iconError: `No icons found for: ${searchTerm}` };
11046
+ }
11047
+ const iconName = searchResult.icons[0];
11048
+ const svgContent = await fetchIconSvg(iconName, size);
11049
+ const iconType = detectIconType(svgContent);
11050
+ const { $icon, ...rest } = node;
11051
+ return {
11052
+ ...rest,
11053
+ $iconResolved: { svgContent, iconType, iconName, searchTerm }
11054
+ };
11055
+ } catch (error3) {
11056
+ error(`Failed to resolve icon "${searchTerm}": ${error3 instanceof Error ? error3.message : String(error3)}`);
11057
+ const { $icon, ...rest } = node;
11058
+ return { ...rest, $iconError: `Failed to load icon: ${error3 instanceof Error ? error3.message : "Unknown error"}` };
11059
+ }
11060
+ }
11061
+ let result = { ...node };
11062
+ if (node.children && Array.isArray(node.children)) {
11063
+ result.children = await Promise.all(
11064
+ node.children.map(async (child) => {
11065
+ if (child && typeof child === "object") {
11066
+ return resolveIconsInNode(child);
11067
+ }
11068
+ return child;
11069
+ })
11070
+ );
11071
+ }
11072
+ if (node.template && typeof node.template === "object") {
11073
+ result.template = await resolveIconsInNode(node.template);
11074
+ }
11075
+ return result;
11076
+ }
11077
+ var ICON_TOOLS = /* @__PURE__ */ new Set(["create_from_schema", "modify_via_schema"]);
11078
+ async function preprocessIconsInParams(toolName, params) {
11079
+ if (!ICON_TOOLS.has(toolName)) return params;
11080
+ const result = { ...params };
11081
+ if (result.schema && typeof result.schema === "object") {
11082
+ result.schema = await resolveIconsInNode(result.schema);
11083
+ }
11084
+ if (result.modifications && Array.isArray(result.modifications)) {
11085
+ result.modifications = await Promise.all(
11086
+ result.modifications.map(async (mod) => ({
11087
+ ...mod,
11088
+ schema: await resolveIconsInNode(mod.schema)
11089
+ }))
11090
+ );
11091
+ }
11092
+ return result;
11093
+ }
11094
+
10868
11095
  // node_modules/ws/wrapper.mjs
10869
11096
  var import_stream = __toESM(require_stream(), 1);
10870
11097
  var import_receiver = __toESM(require_receiver(), 1);
@@ -11227,6 +11454,31 @@ async function probeCdpPorts() {
11227
11454
  }
11228
11455
  return null;
11229
11456
  }
11457
+ async function launchFigmaWithCdp() {
11458
+ const figmaPath = findFigmaPath();
11459
+ if (!figmaPath) {
11460
+ throw new Error(
11461
+ "Figma Desktop is not installed. Download from https://figma.com/downloads"
11462
+ );
11463
+ }
11464
+ const port = await findAvailablePort();
11465
+ log(`Launching Figma Desktop (port ${port})...`);
11466
+ launchFigmaProcess(figmaPath, port);
11467
+ const startTime = Date.now();
11468
+ while (Date.now() - startTime < CDP_WAIT_TIMEOUT_MS) {
11469
+ await new Promise((r) => setTimeout(r, CDP_POLL_INTERVAL_MS));
11470
+ const { alive, isFigma } = await isCdpAlive(port);
11471
+ if (alive && isFigma) {
11472
+ await new Promise((r) => setTimeout(r, 2e3));
11473
+ const targets = await listTargets(port);
11474
+ log("Figma Desktop started");
11475
+ return { port, targets };
11476
+ }
11477
+ }
11478
+ throw new Error(
11479
+ `Figma did not respond on port ${port} within ${CDP_WAIT_TIMEOUT_MS / 1e3}s. Try again.`
11480
+ );
11481
+ }
11230
11482
  function launchFigmaProcess(figmaPath, port) {
11231
11483
  if (process.platform === "darwin") {
11232
11484
  const binary = `${figmaPath}/Contents/MacOS/Figma`;
@@ -11317,7 +11569,7 @@ var PORT_RANGE = 8;
11317
11569
  var SCRIPT_TIMEOUT_MS = 6e4;
11318
11570
  var CONNECT_WAIT_MS = 3e4;
11319
11571
  var FAUX_DIR3 = join4(homedir3(), ".faux");
11320
- var PluginWsServer = class {
11572
+ var PluginWsServer = class _PluginWsServer {
11321
11573
  name = "plugin-ws";
11322
11574
  wss = null;
11323
11575
  connections = /* @__PURE__ */ new Map();
@@ -11325,6 +11577,17 @@ var PluginWsServer = class {
11325
11577
  connectWaiters = [];
11326
11578
  _activeFileId = null;
11327
11579
  _port = 0;
11580
+ // File tracking state (mirrors CdpFileTracker API)
11581
+ _lastAcknowledgedFileId = null;
11582
+ _lastAcknowledgedFileName = null;
11583
+ _lastAcknowledgedAt = 0;
11584
+ _lastFocusEventAt = 0;
11585
+ /** How long before focus events are considered stale for high-confidence detection. */
11586
+ static FOCUS_FRESH_MS = 12e4;
11587
+ // 2 minutes
11588
+ /** How long before last-known file is considered stale and untrusted (matches CDP). */
11589
+ static LAST_KNOWN_STALE_MS = 5 * 6e4;
11590
+ // 5 minutes
11328
11591
  // -------------------------------------------------------------------------
11329
11592
  // Public Getters
11330
11593
  // -------------------------------------------------------------------------
@@ -11343,6 +11606,79 @@ var PluginWsServer = class {
11343
11606
  get hasConnections() {
11344
11607
  return this.connections.size > 0;
11345
11608
  }
11609
+ get activeFileName() {
11610
+ if (!this._activeFileId) return null;
11611
+ return this.connections.get(this._activeFileId)?.fileName ?? null;
11612
+ }
11613
+ get lastAcknowledgedFileName() {
11614
+ return this._lastAcknowledgedFileName;
11615
+ }
11616
+ // -------------------------------------------------------------------------
11617
+ // File Tracking
11618
+ // -------------------------------------------------------------------------
11619
+ /**
11620
+ * Detect the active file for the plugin transport.
11621
+ * Unlike CDP (which introspects shell.html), this is synchronous — the
11622
+ * plugin tells us via file-focus events. The cacheTtlMs parameter is
11623
+ * accepted for API compatibility but detection is always in-memory.
11624
+ */
11625
+ detectActiveFile(_cacheTtlMs = 2e3) {
11626
+ const allFiles = this.getAllFileInfos();
11627
+ if (this.connections.size === 0) {
11628
+ return { activeFile: null, allFiles, method: "none", confidence: "none" };
11629
+ }
11630
+ if (this.connections.size === 1) {
11631
+ const conn = this.connections.values().next().value;
11632
+ return { activeFile: this.connToFileInfo(conn), allFiles, method: "single-file", confidence: "high" };
11633
+ }
11634
+ if (this._activeFileId && this.connections.has(this._activeFileId)) {
11635
+ const conn = this.connections.get(this._activeFileId);
11636
+ const info = this.connToFileInfo(conn);
11637
+ const focusAge = Date.now() - this._lastFocusEventAt;
11638
+ if (this._lastFocusEventAt > 0 && focusAge < _PluginWsServer.FOCUS_FRESH_MS) {
11639
+ return { activeFile: info, allFiles, method: "focus-event", confidence: "high" };
11640
+ }
11641
+ if (this.isLastKnownStale()) {
11642
+ log("File tracker: focus and acknowledgment both stale \u2014 cannot detect active file");
11643
+ return { activeFile: null, allFiles, method: "none", confidence: "none" };
11644
+ }
11645
+ return { activeFile: info, allFiles, method: "last-known", confidence: "low" };
11646
+ }
11647
+ return { activeFile: null, allFiles, method: "none", confidence: "none" };
11648
+ }
11649
+ /** Whether the active file has changed since the last acknowledged execution. */
11650
+ hasFileChanged() {
11651
+ return this._activeFileId !== null && this._lastAcknowledgedFileId !== null && this._activeFileId !== this._lastAcknowledgedFileId;
11652
+ }
11653
+ /**
11654
+ * Acknowledge the current active file as the intended target.
11655
+ * Call AFTER successful script execution.
11656
+ */
11657
+ acknowledgeActiveFile() {
11658
+ if (this._activeFileId) {
11659
+ this._lastAcknowledgedFileId = this._activeFileId;
11660
+ this._lastAcknowledgedFileName = this.connections.get(this._activeFileId)?.fileName ?? null;
11661
+ this._lastAcknowledgedAt = Date.now();
11662
+ }
11663
+ }
11664
+ /** True if the last acknowledged execution was more than 5 minutes ago. */
11665
+ isLastKnownStale() {
11666
+ if (this._lastAcknowledgedAt === 0) return false;
11667
+ return Date.now() - this._lastAcknowledgedAt > _PluginWsServer.LAST_KNOWN_STALE_MS;
11668
+ }
11669
+ /** All connected files as FigmaFileInfo[]. */
11670
+ getAllFileInfos() {
11671
+ return Array.from(this.connections.values()).map((c) => this.connToFileInfo(c));
11672
+ }
11673
+ /** Build FigmaFileInfo from a PluginConnection. */
11674
+ connToFileInfo(conn) {
11675
+ return {
11676
+ fileKey: conn.fileId,
11677
+ fileName: conn.fileName,
11678
+ targetId: conn.fileId,
11679
+ wsUrl: ""
11680
+ };
11681
+ }
11346
11682
  // -------------------------------------------------------------------------
11347
11683
  // Server Lifecycle
11348
11684
  // -------------------------------------------------------------------------
@@ -11437,6 +11773,7 @@ var PluginWsServer = class {
11437
11773
  this.connections.set(fileId, { ws, fileId, fileName, connectedAt: Date.now() });
11438
11774
  if (this.connections.size === 1 || !this._activeFileId) {
11439
11775
  this._activeFileId = fileId;
11776
+ this._lastFocusEventAt = Date.now();
11440
11777
  }
11441
11778
  log(`Plugin connected: "${fileName}" (${fileId}) [${this.connections.size} file(s)]`);
11442
11779
  for (const waiter of this.connectWaiters) {
@@ -11497,6 +11834,7 @@ var PluginWsServer = class {
11497
11834
  const focusFileId = msg.fileId || fileId;
11498
11835
  if (this.connections.has(focusFileId)) {
11499
11836
  this._activeFileId = focusFileId;
11837
+ this._lastFocusEventAt = Date.now();
11500
11838
  }
11501
11839
  break;
11502
11840
  }
@@ -25908,8 +26246,11 @@ var RESOURCES = [
25908
26246
  ];
25909
26247
  var INSTRUCTIONS = `You are connected to Figma Desktop via faux-studio. You can create, modify, and inspect designs using the tools below.
25910
26248
 
26249
+ ## Transport
26250
+ 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.
26251
+
25911
26252
  ## Workflow
25912
- 1. Call \`setup_figma\` first to ensure Figma is running and connected.
26253
+ 1. Call \`setup_figma\` first to ensure Figma is running and connected (via plugin or CDP).
25913
26254
  2. Call \`get_page_structure\` or \`get_screenshot\` to understand what's on the canvas.
25914
26255
  3. Use \`create_from_schema\` for all creation \u2014 it accepts declarative JSON schemas.
25915
26256
  4. Use \`modify_via_schema\` to change existing nodes.
@@ -25949,7 +26290,7 @@ Resources provide quick read-only access to Figma state without tool calls:
25949
26290
  - Create components for reusable UI patterns.`;
25950
26291
  function createMcpServer(deps) {
25951
26292
  const server2 = new Server(
25952
- { name: "faux-studio", version: "0.4.5" },
26293
+ { name: "faux-studio", version: "0.5.0" },
25953
26294
  {
25954
26295
  capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
25955
26296
  instructions: INSTRUCTIONS
@@ -25964,7 +26305,7 @@ function createMcpServer(deps) {
25964
26305
  },
25965
26306
  {
25966
26307
  name: "setup_figma",
25967
- description: "Ensure Figma Desktop is running and connected. Call this before any design work. Launches Figma if needed, detects open files and the active tab, and provides setup guidance. Returns connection status, active file, and list of all open design files. Idempotent \u2014 safe to call multiple times.",
26308
+ 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.",
25968
26309
  inputSchema: {
25969
26310
  type: "object",
25970
26311
  properties: {},
@@ -26051,6 +26392,13 @@ function createMcpServer(deps) {
26051
26392
  throw err;
26052
26393
  }
26053
26394
  }
26395
+ if (name === "search_icons") {
26396
+ log("search_icons called (server-side)");
26397
+ const result2 = await searchIcons(params);
26398
+ return {
26399
+ content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
26400
+ };
26401
+ }
26054
26402
  const intents = {
26055
26403
  meso: typeof params._intent_meso === "string" ? params._intent_meso : void 0,
26056
26404
  macro: typeof params._intent_macro === "string" ? params._intent_macro : void 0
@@ -26178,13 +26526,19 @@ function lazyInit() {
26178
26526
  return initPromise2;
26179
26527
  }
26180
26528
  async function doInit() {
26529
+ log("[init] lazyInit triggered \u2014 starting auth + tool fetch");
26530
+ const t0 = Date.now();
26181
26531
  auth = await ensureAuth();
26532
+ log(`[init] auth complete in ${Date.now() - t0}ms`);
26182
26533
  try {
26534
+ const t1 = Date.now();
26183
26535
  tools = await getTools(auth.jwt);
26536
+ log(`[init] tools fetched (${tools.length}) in ${Date.now() - t1}ms`);
26184
26537
  } catch (err) {
26185
26538
  error(err instanceof Error ? err.message : String(err));
26186
26539
  process.exit(1);
26187
26540
  }
26541
+ log(`[init] full initialization complete in ${Date.now() - t0}ms`);
26188
26542
  }
26189
26543
  async function tryConnectCdp() {
26190
26544
  try {
@@ -26209,8 +26563,22 @@ async function tryConnectCdp() {
26209
26563
  }
26210
26564
  async function executeScript(script, intents, opts) {
26211
26565
  if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26566
+ const detection = pluginServer.detectActiveFile(opts?.cacheTtlMs);
26567
+ if (!detection.activeFile && detection.allFiles.length > 1) {
26568
+ throw new FileUnknownError(detection.allFiles);
26569
+ }
26570
+ if (pluginServer.hasFileChanged()) {
26571
+ throw new FileChangedError(
26572
+ pluginServer.lastAcknowledgedFileName,
26573
+ pluginServer.activeFileName,
26574
+ pluginServer.activeFileId,
26575
+ detection.allFiles
26576
+ );
26577
+ }
26212
26578
  log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
26213
- return pluginServer.executeScript(script, void 0, intents);
26579
+ const result = await pluginServer.executeScript(script, void 0, intents);
26580
+ pluginServer.acknowledgeActiveFile();
26581
+ return result;
26214
26582
  }
26215
26583
  if (forceTransport !== "plugin") {
26216
26584
  const client = await tryConnectCdp();
@@ -26273,6 +26641,7 @@ async function recoverCdp(client, script) {
26273
26641
  async function generateWithAuth(toolName, params) {
26274
26642
  await lazyInit();
26275
26643
  auth = await refreshIfNeeded(auth);
26644
+ params = await preprocessIconsInParams(toolName, params);
26276
26645
  let result;
26277
26646
  try {
26278
26647
  result = await generateScript(auth.jwt, toolName, params);
@@ -26310,17 +26679,21 @@ async function waitForPlugin() {
26310
26679
  return false;
26311
26680
  }
26312
26681
  function pluginReadyResult() {
26682
+ const detection = pluginServer.detectActiveFile();
26683
+ const activeName = detection.activeFile?.fileName;
26684
+ const fileCount = detection.allFiles.length;
26313
26685
  return {
26314
26686
  status: "ready",
26315
26687
  transport: "plugin",
26316
- message: `Connected via plugin.${pluginServer.activeFileId ? ` Active file: ${pluginServer.activeFileId}` : ""} Ready to design.`,
26317
- activeFile: pluginServer.activeFileId || void 0,
26688
+ message: `Connected via plugin.${activeName ? ` Active file: "${activeName}".` : ""} ${fileCount} file(s) open. Ready to design.`,
26689
+ activeFile: activeName || void 0,
26318
26690
  pluginFiles: pluginServer.connectedFiles,
26691
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
26319
26692
  port: pluginServer.port
26320
26693
  };
26321
26694
  }
26322
26695
  async function setupFigma(params) {
26323
- if (pluginServer.hasConnections) {
26696
+ if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26324
26697
  return pluginReadyResult();
26325
26698
  }
26326
26699
  if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
@@ -26340,6 +26713,73 @@ async function setupFigma(params) {
26340
26713
  message: "Connected via CDP. Ready to design."
26341
26714
  };
26342
26715
  }
26716
+ if (!findFigmaPath()) {
26717
+ return {
26718
+ status: "not_installed",
26719
+ transport: "none",
26720
+ message: "Figma Desktop is not installed.\n\n1. Download Figma: https://figma.com/downloads\n2. Install and open it\n3. Call setup_figma again"
26721
+ };
26722
+ }
26723
+ if (!isFigmaRunning()) {
26724
+ if (forceTransport === "cdp") {
26725
+ try {
26726
+ const connection = await launchFigmaWithCdp();
26727
+ const target = findFigmaDesignTarget(connection.targets);
26728
+ if (target) {
26729
+ const client = new CdpClient();
26730
+ await client.connect(target.webSocketDebuggerUrl);
26731
+ await client.discoverFigmaContext();
26732
+ cdpClient = client;
26733
+ log(`CDP connected after launch: ${target.title}`);
26734
+ if (!fileTracker) {
26735
+ fileTracker = new CdpFileTracker(connection.port);
26736
+ }
26737
+ const detection = await fileTracker.detectActiveFile();
26738
+ fileTracker.acknowledgeActiveFile();
26739
+ return {
26740
+ status: "launched",
26741
+ transport: "cdp",
26742
+ message: `Launched Figma and connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
26743
+ activeFile: detection.activeFile?.fileName ?? target.title,
26744
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
26745
+ port: connection.port
26746
+ };
26747
+ }
26748
+ return {
26749
+ status: "launched",
26750
+ transport: "cdp",
26751
+ message: "Launched Figma with CDP enabled but no design file is open.\n\nOpen or create a design file in Figma, then call setup_figma again.",
26752
+ port: connection.port
26753
+ };
26754
+ } catch (err) {
26755
+ return {
26756
+ status: "not_installed",
26757
+ transport: "none",
26758
+ message: err instanceof Error ? err.message : "Failed to launch Figma."
26759
+ };
26760
+ }
26761
+ } else {
26762
+ try {
26763
+ launchFigma();
26764
+ } catch (err) {
26765
+ return {
26766
+ status: "not_installed",
26767
+ transport: "none",
26768
+ message: err instanceof Error ? err.message : "Failed to launch Figma."
26769
+ };
26770
+ }
26771
+ }
26772
+ }
26773
+ if (forceTransport !== "cdp") {
26774
+ log("Waiting for plugin connection...");
26775
+ const connected = await waitForPlugin();
26776
+ if (connected) {
26777
+ return {
26778
+ ...pluginReadyResult(),
26779
+ status: isFigmaRunning() ? "ready" : "launched"
26780
+ };
26781
+ }
26782
+ }
26343
26783
  if (forceTransport !== "plugin") {
26344
26784
  const existing = await probeCdpPorts();
26345
26785
  if (existing) {
@@ -26371,37 +26811,6 @@ async function setupFigma(params) {
26371
26811
  }
26372
26812
  }
26373
26813
  }
26374
- if (!findFigmaPath()) {
26375
- return {
26376
- status: "not_installed",
26377
- transport: "none",
26378
- message: `Figma Desktop is not installed.
26379
-
26380
- 1. Download Figma: https://figma.com/downloads
26381
- 2. Install and open it
26382
- 3. Install the faux-studio plugin: ${PLUGIN_URL}
26383
- 4. Call setup_figma again`
26384
- };
26385
- }
26386
- if (!isFigmaRunning()) {
26387
- try {
26388
- launchFigma();
26389
- } catch (err) {
26390
- return {
26391
- status: "not_installed",
26392
- transport: "none",
26393
- message: err instanceof Error ? err.message : "Failed to launch Figma."
26394
- };
26395
- }
26396
- }
26397
- log("Waiting for plugin connection...");
26398
- const connected = await waitForPlugin();
26399
- if (connected) {
26400
- return {
26401
- ...pluginReadyResult(),
26402
- status: isFigmaRunning() ? "ready" : "launched"
26403
- };
26404
- }
26405
26814
  return {
26406
26815
  status: "waiting_for_plugin",
26407
26816
  transport: "none",
@@ -26420,7 +26829,8 @@ Call setup_figma again once the plugin shows "Ready".`,
26420
26829
  };
26421
26830
  }
26422
26831
  async function main() {
26423
- log(`faux-studio v${"0.4.5"}`);
26832
+ const startupT0 = Date.now();
26833
+ log(`faux-studio v${"0.5.0"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
26424
26834
  try {
26425
26835
  const port = await pluginServer.start();
26426
26836
  if (forceTransport === "plugin") {
@@ -26428,7 +26838,7 @@ async function main() {
26428
26838
  } else if (forceTransport === "cdp") {
26429
26839
  log(`Transport: CDP-only mode (FAUX_TRANSPORT=cdp) \u2014 plugin WS on port ${port} (standby)`);
26430
26840
  } else {
26431
- log(`Transport: auto \u2014 plugin WS on port ${port}, CDP on-demand`);
26841
+ log(`Transport: auto \u2014 plugin preferred (port ${port}), CDP fallback`);
26432
26842
  }
26433
26843
  } catch (err) {
26434
26844
  warn(`Plugin WS server failed to start: ${err instanceof Error ? err.message : err}`);
@@ -26472,6 +26882,7 @@ async function main() {
26472
26882
  });
26473
26883
  await startServer(server2);
26474
26884
  setServer(server2);
26885
+ log(`[startup] MCP server ready in ${Date.now() - startupT0}ms \u2014 handshake complete`);
26475
26886
  if (forceTransport !== "plugin") {
26476
26887
  try {
26477
26888
  await tryConnectCdp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faux-studio",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,11 @@
44
44
  "url": "https://github.com/uxfreak/faux-studio.git"
45
45
  },
46
46
  "homepage": "https://faux.design",
47
+ "os": [
48
+ "darwin",
49
+ "linux",
50
+ "win32"
51
+ ],
47
52
  "optionalDependencies": {
48
53
  "@napi-rs/keyring": "^1.2.0"
49
54
  }