faux-studio 0.4.4 → 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 +194 -40
  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,43 +10481,99 @@ 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
- constructor(entry) {
10487
- this.entry = entry;
10488
- }
10494
+ cached = void 0;
10489
10495
  async load() {
10496
+ if (this.cached !== void 0) return this.cached;
10490
10497
  try {
10491
- const raw = await this.entry.getPassword();
10492
- if (!raw) return null;
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
+ }
10493
10511
  const creds = JSON.parse(raw);
10494
- if (!creds.jwt || !creds.refreshToken || !creds.user) return null;
10512
+ if (!creds.jwt || !creds.refreshToken || !creds.user) {
10513
+ this.cached = null;
10514
+ return null;
10515
+ }
10516
+ this.cached = creds;
10495
10517
  return creds;
10496
10518
  } catch {
10519
+ this.cached = null;
10497
10520
  return null;
10498
10521
  }
10499
10522
  }
10500
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
+ }
10501
10535
  try {
10502
- 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
+ );
10547
+ this.cached = creds;
10503
10548
  } catch (err) {
10504
10549
  warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
10505
10550
  }
10506
10551
  }
10507
10552
  async clear() {
10508
10553
  try {
10509
- await this.entry.deletePassword();
10554
+ await securityExec(
10555
+ "delete-generic-password",
10556
+ "-s",
10557
+ KEYCHAIN_SERVICE,
10558
+ "-a",
10559
+ KEYCHAIN_ACCOUNT
10560
+ );
10561
+ this.cached = null;
10510
10562
  } catch {
10511
10563
  }
10512
10564
  }
10513
10565
  };
10514
10566
  async function tryCreateKeychainStore() {
10567
+ if (process.platform !== "darwin") {
10568
+ log("[keychain] not macOS \u2014 skipping keychain");
10569
+ return null;
10570
+ }
10515
10571
  try {
10516
- const { AsyncEntry } = await import("@napi-rs/keyring");
10517
- const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
10518
- await entry.getPassword();
10519
- 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();
10520
10577
  } catch (err) {
10521
10578
  warn(
10522
10579
  `OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
@@ -10532,9 +10589,12 @@ function createCredentialStore() {
10532
10589
  return initPromise;
10533
10590
  }
10534
10591
  async function initStore() {
10592
+ log("[credential-store] initializing...");
10593
+ const t0 = Date.now();
10535
10594
  const fileStore = new FileCredentialStore();
10536
10595
  const keychainStore = await tryCreateKeychainStore();
10537
10596
  if (!keychainStore) {
10597
+ log(`[credential-store] using file store (${Date.now() - t0}ms)`);
10538
10598
  return fileStore;
10539
10599
  }
10540
10600
  const keychainCreds = await keychainStore.load();
@@ -10542,6 +10602,7 @@ async function initStore() {
10542
10602
  const fileCreds = await fileStore.load();
10543
10603
  if (fileCreds) {
10544
10604
  await keychainStore.save(fileCreds);
10605
+ keychainStore["cached"] = void 0;
10545
10606
  const verified = await keychainStore.load();
10546
10607
  if (verified) {
10547
10608
  await fileStore.clear();
@@ -10552,6 +10613,7 @@ async function initStore() {
10552
10613
  }
10553
10614
  }
10554
10615
  }
10616
+ log(`[credential-store] using keychain store (${Date.now() - t0}ms)`);
10555
10617
  return keychainStore;
10556
10618
  }
10557
10619
 
@@ -10648,6 +10710,8 @@ async function authenticate() {
10648
10710
  );
10649
10711
  }
10650
10712
  async function ensureAuth() {
10713
+ log("[auth] ensureAuth() started");
10714
+ const t0 = Date.now();
10651
10715
  const apiKey = process.env.FAUX_API_KEY;
10652
10716
  if (apiKey) {
10653
10717
  log("Using FAUX_API_KEY from environment");
@@ -10658,7 +10722,9 @@ async function ensureAuth() {
10658
10722
  source: "api-key"
10659
10723
  };
10660
10724
  }
10725
+ log("[auth] creating credential store...");
10661
10726
  const credStore = await createCredentialStore();
10727
+ log(`[auth] credential store ready (${credStore.name}) in ${Date.now() - t0}ms`);
10662
10728
  const saved = await credStore.load();
10663
10729
  if (saved) {
10664
10730
  if (!isExpiringSoon(saved)) {
@@ -11215,6 +11281,31 @@ async function probeCdpPorts() {
11215
11281
  }
11216
11282
  return null;
11217
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
+ }
11218
11309
  function launchFigmaProcess(figmaPath, port) {
11219
11310
  if (process.platform === "darwin") {
11220
11311
  const binary = `${figmaPath}/Contents/MacOS/Figma`;
@@ -25896,8 +25987,11 @@ var RESOURCES = [
25896
25987
  ];
25897
25988
  var INSTRUCTIONS = `You are connected to Figma Desktop via faux-studio. You can create, modify, and inspect designs using the tools below.
25898
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
+
25899
25993
  ## Workflow
25900
- 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).
25901
25995
  2. Call \`get_page_structure\` or \`get_screenshot\` to understand what's on the canvas.
25902
25996
  3. Use \`create_from_schema\` for all creation \u2014 it accepts declarative JSON schemas.
25903
25997
  4. Use \`modify_via_schema\` to change existing nodes.
@@ -25937,7 +26031,7 @@ Resources provide quick read-only access to Figma state without tool calls:
25937
26031
  - Create components for reusable UI patterns.`;
25938
26032
  function createMcpServer(deps) {
25939
26033
  const server2 = new Server(
25940
- { name: "faux-studio", version: "0.4.4" },
26034
+ { name: "faux-studio", version: "0.4.7" },
25941
26035
  {
25942
26036
  capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
25943
26037
  instructions: INSTRUCTIONS
@@ -25952,7 +26046,7 @@ function createMcpServer(deps) {
25952
26046
  },
25953
26047
  {
25954
26048
  name: "setup_figma",
25955
- 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.",
25956
26050
  inputSchema: {
25957
26051
  type: "object",
25958
26052
  properties: {},
@@ -26166,13 +26260,19 @@ function lazyInit() {
26166
26260
  return initPromise2;
26167
26261
  }
26168
26262
  async function doInit() {
26263
+ log("[init] lazyInit triggered \u2014 starting auth + tool fetch");
26264
+ const t0 = Date.now();
26169
26265
  auth = await ensureAuth();
26266
+ log(`[init] auth complete in ${Date.now() - t0}ms`);
26170
26267
  try {
26268
+ const t1 = Date.now();
26171
26269
  tools = await getTools(auth.jwt);
26270
+ log(`[init] tools fetched (${tools.length}) in ${Date.now() - t1}ms`);
26172
26271
  } catch (err) {
26173
26272
  error(err instanceof Error ? err.message : String(err));
26174
26273
  process.exit(1);
26175
26274
  }
26275
+ log(`[init] full initialization complete in ${Date.now() - t0}ms`);
26176
26276
  }
26177
26277
  async function tryConnectCdp() {
26178
26278
  try {
@@ -26196,10 +26296,6 @@ async function tryConnectCdp() {
26196
26296
  }
26197
26297
  }
26198
26298
  async function executeScript(script, intents, opts) {
26199
- if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26200
- log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
26201
- return pluginServer.executeScript(script, void 0, intents);
26202
- }
26203
26299
  if (forceTransport !== "plugin") {
26204
26300
  const client = await tryConnectCdp();
26205
26301
  if (client) {
@@ -26236,6 +26332,10 @@ async function executeScript(script, intents, opts) {
26236
26332
  }
26237
26333
  }
26238
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
+ }
26239
26339
  log("No transport available, waiting for plugin...");
26240
26340
  return pluginServer.executeScript(script, void 0, intents);
26241
26341
  }
@@ -26308,9 +26408,6 @@ function pluginReadyResult() {
26308
26408
  };
26309
26409
  }
26310
26410
  async function setupFigma(params) {
26311
- if (pluginServer.hasConnections) {
26312
- return pluginReadyResult();
26313
- }
26314
26411
  if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
26315
26412
  if (fileTracker) {
26316
26413
  const detection = await fileTracker.detectActiveFile();
@@ -26328,6 +26425,9 @@ async function setupFigma(params) {
26328
26425
  message: "Connected via CDP. Ready to design."
26329
26426
  };
26330
26427
  }
26428
+ if (forceTransport !== "cdp" && pluginServer.hasConnections) {
26429
+ return pluginReadyResult();
26430
+ }
26331
26431
  if (forceTransport !== "plugin") {
26332
26432
  const existing = await probeCdpPorts();
26333
26433
  if (existing) {
@@ -26363,25 +26463,77 @@ async function setupFigma(params) {
26363
26463
  return {
26364
26464
  status: "not_installed",
26365
26465
  transport: "none",
26366
- message: `Figma Desktop is not installed.
26367
-
26368
- 1. Download Figma: https://figma.com/downloads
26369
- 2. Install and open it
26370
- 3. Install the faux-studio plugin: ${PLUGIN_URL}
26371
- 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"
26372
26467
  };
26373
26468
  }
26374
26469
  if (!isFigmaRunning()) {
26375
- try {
26376
- launchFigma();
26377
- } catch (err) {
26378
- return {
26379
- status: "not_installed",
26380
- transport: "none",
26381
- message: err instanceof Error ? err.message : "Failed to launch Figma."
26382
- };
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
+ }
26383
26517
  }
26384
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
+ }
26385
26537
  log("Waiting for plugin connection...");
26386
26538
  const connected = await waitForPlugin();
26387
26539
  if (connected) {
@@ -26408,7 +26560,8 @@ Call setup_figma again once the plugin shows "Ready".`,
26408
26560
  };
26409
26561
  }
26410
26562
  async function main() {
26411
- log(`faux-studio v${"0.4.4"}`);
26563
+ const startupT0 = Date.now();
26564
+ log(`faux-studio v${"0.4.7"} \u2014 process started (PID ${process.pid}, PPID ${process.ppid})`);
26412
26565
  try {
26413
26566
  const port = await pluginServer.start();
26414
26567
  if (forceTransport === "plugin") {
@@ -26416,7 +26569,7 @@ async function main() {
26416
26569
  } else if (forceTransport === "cdp") {
26417
26570
  log(`Transport: CDP-only mode (FAUX_TRANSPORT=cdp) \u2014 plugin WS on port ${port} (standby)`);
26418
26571
  } else {
26419
- 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)`);
26420
26573
  }
26421
26574
  } catch (err) {
26422
26575
  warn(`Plugin WS server failed to start: ${err instanceof Error ? err.message : err}`);
@@ -26460,6 +26613,7 @@ async function main() {
26460
26613
  });
26461
26614
  await startServer(server2);
26462
26615
  setServer(server2);
26616
+ log(`[startup] MCP server ready in ${Date.now() - startupT0}ms \u2014 handshake complete`);
26463
26617
  if (forceTransport !== "plugin") {
26464
26618
  try {
26465
26619
  await tryConnectCdp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faux-studio",
3
- "version": "0.4.4",
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
  }