faux-studio 0.4.5 → 0.4.7

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 (2) hide show
  1. package/dist/index.js +180 -38
  2. package/package.json +6 -4
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,17 +10481,29 @@ 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 {
10484
10493
  name = "keychain";
10485
- entry;
10486
10494
  cached = void 0;
10487
- constructor(entry) {
10488
- this.entry = entry;
10489
- }
10490
10495
  async load() {
10491
10496
  if (this.cached !== void 0) return this.cached;
10492
10497
  try {
10493
- const raw = await this.entry.getPassword();
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();
10494
10507
  if (!raw) {
10495
10508
  this.cached = null;
10496
10509
  return null;
@@ -10508,8 +10521,29 @@ var KeychainCredentialStore = class {
10508
10521
  }
10509
10522
  }
10510
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
+ }
10511
10535
  try {
10512
- await this.entry.setPassword(JSON.stringify(creds));
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
+ );
10513
10547
  this.cached = creds;
10514
10548
  } catch (err) {
10515
10549
  warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
@@ -10517,18 +10551,29 @@ var KeychainCredentialStore = class {
10517
10551
  }
10518
10552
  async clear() {
10519
10553
  try {
10520
- await this.entry.deletePassword();
10554
+ await securityExec(
10555
+ "delete-generic-password",
10556
+ "-s",
10557
+ KEYCHAIN_SERVICE,
10558
+ "-a",
10559
+ KEYCHAIN_ACCOUNT
10560
+ );
10521
10561
  this.cached = null;
10522
10562
  } catch {
10523
10563
  }
10524
10564
  }
10525
10565
  };
10526
10566
  async function tryCreateKeychainStore() {
10567
+ if (process.platform !== "darwin") {
10568
+ log("[keychain] not macOS \u2014 skipping keychain");
10569
+ return null;
10570
+ }
10527
10571
  try {
10528
- const { AsyncEntry } = await import("@napi-rs/keyring");
10529
- const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
10530
- await entry.getPassword();
10531
- return new KeychainCredentialStore(entry);
10572
+ log("[keychain] probing macOS security CLI...");
10573
+ const t0 = Date.now();
10574
+ await securityExec("help");
10575
+ log(`[keychain] security CLI available (${Date.now() - t0}ms)`);
10576
+ return new SecurityCliKeychainStore();
10532
10577
  } catch (err) {
10533
10578
  warn(
10534
10579
  `OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
@@ -10544,9 +10589,12 @@ function createCredentialStore() {
10544
10589
  return initPromise;
10545
10590
  }
10546
10591
  async function initStore() {
10592
+ log("[credential-store] initializing...");
10593
+ const t0 = Date.now();
10547
10594
  const fileStore = new FileCredentialStore();
10548
10595
  const keychainStore = await tryCreateKeychainStore();
10549
10596
  if (!keychainStore) {
10597
+ log(`[credential-store] using file store (${Date.now() - t0}ms)`);
10550
10598
  return fileStore;
10551
10599
  }
10552
10600
  const keychainCreds = await keychainStore.load();
@@ -10554,6 +10602,7 @@ async function initStore() {
10554
10602
  const fileCreds = await fileStore.load();
10555
10603
  if (fileCreds) {
10556
10604
  await keychainStore.save(fileCreds);
10605
+ keychainStore["cached"] = void 0;
10557
10606
  const verified = await keychainStore.load();
10558
10607
  if (verified) {
10559
10608
  await fileStore.clear();
@@ -10564,6 +10613,7 @@ async function initStore() {
10564
10613
  }
10565
10614
  }
10566
10615
  }
10616
+ log(`[credential-store] using keychain store (${Date.now() - t0}ms)`);
10567
10617
  return keychainStore;
10568
10618
  }
10569
10619
 
@@ -10660,6 +10710,8 @@ async function authenticate() {
10660
10710
  );
10661
10711
  }
10662
10712
  async function ensureAuth() {
10713
+ log("[auth] ensureAuth() started");
10714
+ const t0 = Date.now();
10663
10715
  const apiKey = process.env.FAUX_API_KEY;
10664
10716
  if (apiKey) {
10665
10717
  log("Using FAUX_API_KEY from environment");
@@ -10670,7 +10722,9 @@ async function ensureAuth() {
10670
10722
  source: "api-key"
10671
10723
  };
10672
10724
  }
10725
+ log("[auth] creating credential store...");
10673
10726
  const credStore = await createCredentialStore();
10727
+ log(`[auth] credential store ready (${credStore.name}) in ${Date.now() - t0}ms`);
10674
10728
  const saved = await credStore.load();
10675
10729
  if (saved) {
10676
10730
  if (!isExpiringSoon(saved)) {
@@ -11227,6 +11281,31 @@ async function probeCdpPorts() {
11227
11281
  }
11228
11282
  return null;
11229
11283
  }
11284
+ async function launchFigmaWithCdp() {
11285
+ const figmaPath = findFigmaPath();
11286
+ if (!figmaPath) {
11287
+ throw new Error(
11288
+ "Figma Desktop is not installed. Download from https://figma.com/downloads"
11289
+ );
11290
+ }
11291
+ const port = await findAvailablePort();
11292
+ log(`Launching Figma Desktop (port ${port})...`);
11293
+ launchFigmaProcess(figmaPath, port);
11294
+ const startTime = Date.now();
11295
+ while (Date.now() - startTime < CDP_WAIT_TIMEOUT_MS) {
11296
+ await new Promise((r) => setTimeout(r, CDP_POLL_INTERVAL_MS));
11297
+ const { alive, isFigma } = await isCdpAlive(port);
11298
+ if (alive && isFigma) {
11299
+ await new Promise((r) => setTimeout(r, 2e3));
11300
+ const targets = await listTargets(port);
11301
+ log("Figma Desktop started");
11302
+ return { port, targets };
11303
+ }
11304
+ }
11305
+ throw new Error(
11306
+ `Figma did not respond on port ${port} within ${CDP_WAIT_TIMEOUT_MS / 1e3}s. Try again.`
11307
+ );
11308
+ }
11230
11309
  function launchFigmaProcess(figmaPath, port) {
11231
11310
  if (process.platform === "darwin") {
11232
11311
  const binary = `${figmaPath}/Contents/MacOS/Figma`;
@@ -25908,8 +25987,11 @@ var RESOURCES = [
25908
25987
  ];
25909
25988
  var INSTRUCTIONS = `You are connected to Figma Desktop via faux-studio. You can create, modify, and inspect designs using the tools below.
25910
25989
 
25990
+ ## 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.
25992
+
25911
25993
  ## Workflow
25912
- 1. Call \`setup_figma\` first to ensure Figma is running and connected.
25994
+ 1. Call \`setup_figma\` first to ensure Figma is running and connected (via CDP or plugin).
25913
25995
  2. Call \`get_page_structure\` or \`get_screenshot\` to understand what's on the canvas.
25914
25996
  3. Use \`create_from_schema\` for all creation \u2014 it accepts declarative JSON schemas.
25915
25997
  4. Use \`modify_via_schema\` to change existing nodes.
@@ -25949,7 +26031,7 @@ Resources provide quick read-only access to Figma state without tool calls:
25949
26031
  - Create components for reusable UI patterns.`;
25950
26032
  function createMcpServer(deps) {
25951
26033
  const server2 = new Server(
25952
- { name: "faux-studio", version: "0.4.5" },
26034
+ { name: "faux-studio", version: "0.4.7" },
25953
26035
  {
25954
26036
  capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
25955
26037
  instructions: INSTRUCTIONS
@@ -25964,7 +26046,7 @@ function createMcpServer(deps) {
25964
26046
  },
25965
26047
  {
25966
26048
  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.",
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.",
25968
26050
  inputSchema: {
25969
26051
  type: "object",
25970
26052
  properties: {},
@@ -26178,13 +26260,19 @@ function lazyInit() {
26178
26260
  return initPromise2;
26179
26261
  }
26180
26262
  async function doInit() {
26263
+ log("[init] lazyInit triggered \u2014 starting auth + tool fetch");
26264
+ const t0 = Date.now();
26181
26265
  auth = await ensureAuth();
26266
+ log(`[init] auth complete in ${Date.now() - t0}ms`);
26182
26267
  try {
26268
+ const t1 = Date.now();
26183
26269
  tools = await getTools(auth.jwt);
26270
+ log(`[init] tools fetched (${tools.length}) in ${Date.now() - t1}ms`);
26184
26271
  } catch (err) {
26185
26272
  error(err instanceof Error ? err.message : String(err));
26186
26273
  process.exit(1);
26187
26274
  }
26275
+ log(`[init] full initialization complete in ${Date.now() - t0}ms`);
26188
26276
  }
26189
26277
  async function tryConnectCdp() {
26190
26278
  try {
@@ -26208,10 +26296,6 @@ async function tryConnectCdp() {
26208
26296
  }
26209
26297
  }
26210
26298
  async function executeScript(script, intents, opts) {
26211
- if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26212
- log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
26213
- return pluginServer.executeScript(script, void 0, intents);
26214
- }
26215
26299
  if (forceTransport !== "plugin") {
26216
26300
  const client = await tryConnectCdp();
26217
26301
  if (client) {
@@ -26248,6 +26332,10 @@ async function executeScript(script, intents, opts) {
26248
26332
  }
26249
26333
  }
26250
26334
  }
26335
+ if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26336
+ log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
26337
+ return pluginServer.executeScript(script, void 0, intents);
26338
+ }
26251
26339
  log("No transport available, waiting for plugin...");
26252
26340
  return pluginServer.executeScript(script, void 0, intents);
26253
26341
  }
@@ -26320,9 +26408,6 @@ function pluginReadyResult() {
26320
26408
  };
26321
26409
  }
26322
26410
  async function setupFigma(params) {
26323
- if (pluginServer.hasConnections) {
26324
- return pluginReadyResult();
26325
- }
26326
26411
  if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
26327
26412
  if (fileTracker) {
26328
26413
  const detection = await fileTracker.detectActiveFile();
@@ -26340,6 +26425,9 @@ async function setupFigma(params) {
26340
26425
  message: "Connected via CDP. Ready to design."
26341
26426
  };
26342
26427
  }
26428
+ if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26429
+ return pluginReadyResult();
26430
+ }
26343
26431
  if (forceTransport !== "plugin") {
26344
26432
  const existing = await probeCdpPorts();
26345
26433
  if (existing) {
@@ -26375,25 +26463,77 @@ async function setupFigma(params) {
26375
26463
  return {
26376
26464
  status: "not_installed",
26377
26465
  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`
26466
+ message: "Figma Desktop is not installed.\n\n1. Download Figma: https://figma.com/downloads\n2. Install and open it\n3. Call setup_figma again"
26384
26467
  };
26385
26468
  }
26386
26469
  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
- };
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 {
26481
+ try {
26482
+ const connection = await launchFigmaWithCdp();
26483
+ const target = findFigmaDesignTarget(connection.targets);
26484
+ if (target) {
26485
+ const client = new CdpClient();
26486
+ await client.connect(target.webSocketDebuggerUrl);
26487
+ await client.discoverFigmaContext();
26488
+ cdpClient = client;
26489
+ log(`CDP connected after launch: ${target.title}`);
26490
+ if (!fileTracker) {
26491
+ fileTracker = new CdpFileTracker(connection.port);
26492
+ }
26493
+ const detection = await fileTracker.detectActiveFile();
26494
+ fileTracker.acknowledgeActiveFile();
26495
+ return {
26496
+ status: "launched",
26497
+ transport: "cdp",
26498
+ message: `Launched Figma and connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
26499
+ activeFile: detection.activeFile?.fileName ?? target.title,
26500
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
26501
+ port: connection.port
26502
+ };
26503
+ }
26504
+ return {
26505
+ status: "launched",
26506
+ transport: "cdp",
26507
+ 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.",
26508
+ port: connection.port
26509
+ };
26510
+ } catch (err) {
26511
+ return {
26512
+ status: "not_installed",
26513
+ transport: "none",
26514
+ message: err instanceof Error ? err.message : "Failed to launch Figma."
26515
+ };
26516
+ }
26395
26517
  }
26396
26518
  }
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
+ };
26536
+ }
26397
26537
  log("Waiting for plugin connection...");
26398
26538
  const connected = await waitForPlugin();
26399
26539
  if (connected) {
@@ -26420,7 +26560,8 @@ Call setup_figma again once the plugin shows "Ready".`,
26420
26560
  };
26421
26561
  }
26422
26562
  async function main() {
26423
- log(`faux-studio v${"0.4.5"}`);
26563
+ const startupT0 = Date.now();
26564
+ log(`faux-studio v${"0.4.7"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
26424
26565
  try {
26425
26566
  const port = await pluginServer.start();
26426
26567
  if (forceTransport === "plugin") {
@@ -26428,7 +26569,7 @@ async function main() {
26428
26569
  } else if (forceTransport === "cdp") {
26429
26570
  log(`Transport: CDP-only mode (FAUX_TRANSPORT=cdp) \u2014 plugin WS on port ${port} (standby)`);
26430
26571
  } else {
26431
- log(`Transport: auto \u2014 plugin WS on port ${port}, CDP on-demand`);
26572
+ log(`Transport: auto \u2014 CDP preferred, plugin WS on port ${port} (fallback)`);
26432
26573
  }
26433
26574
  } catch (err) {
26434
26575
  warn(`Plugin WS server failed to start: ${err instanceof Error ? err.message : err}`);
@@ -26472,6 +26613,7 @@ async function main() {
26472
26613
  });
26473
26614
  await startServer(server2);
26474
26615
  setServer(server2);
26616
+ log(`[startup] MCP server ready in ${Date.now() - startupT0}ms \u2014 handshake complete`);
26475
26617
  if (forceTransport !== "plugin") {
26476
26618
  try {
26477
26619
  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.4.7",
4
4
  "description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,9 @@
44
44
  "url": "https://github.com/uxfreak/faux-studio.git"
45
45
  },
46
46
  "homepage": "https://faux.design",
47
- "optionalDependencies": {
48
- "@napi-rs/keyring": "^1.2.0"
49
- }
47
+ "os": [
48
+ "darwin",
49
+ "linux",
50
+ "win32"
51
+ ]
50
52
  }