faux-studio 0.3.12 → 0.4.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 +21 -0
  2. package/dist/index.js +359 -13
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -67,6 +67,27 @@ faux-studio runs locally and bridges your AI client to Figma Desktop:
67
67
  - Setup guide: [faux.design/docs/setup](https://faux.design/docs/setup)
68
68
  - Issues: [github.com/uxfreak/faux-studio/issues](https://github.com/uxfreak/faux-studio/issues)
69
69
 
70
+ ## Publishing a New Version
71
+
72
+ To publish a new version to npm:
73
+
74
+ 1. Bump the version in `package.json`:
75
+ ```bash
76
+ npm version patch # 0.4.0 → 0.4.1
77
+ npm version minor # 0.4.0 → 0.5.0
78
+ npm version major # 0.4.0 → 1.0.0
79
+ ```
80
+ This updates `package.json` and creates a git commit + tag automatically.
81
+
82
+ 2. Push the commit and tag:
83
+ ```bash
84
+ git push && git push --tags
85
+ ```
86
+
87
+ 3. The [publish workflow](.github/workflows/publish.yml) will build and publish to npm automatically on any `v*` tag push.
88
+
89
+ > **Note:** Ensure the `NPM_TOKEN` secret is configured in the repo's GitHub Settings → Secrets and variables → Actions.
90
+
70
91
  ## License
71
92
 
72
93
  MIT
package/dist/index.js CHANGED
@@ -10451,7 +10451,7 @@ function error(message) {
10451
10451
  // src/auth.ts
10452
10452
  var FAUX_DIR = join(homedir(), ".faux");
10453
10453
  var CREDENTIALS_PATH = join(FAUX_DIR, "credentials.json");
10454
- var AUTH_BASE = "https://auth.faux.design";
10454
+ var AUTH_BASE = process.env.FAUX_AUTH_URL || "https://auth.faux.design";
10455
10455
  var POLL_INTERVAL_MS = 2e3;
10456
10456
  var POLL_TIMEOUT_MS = 5 * 60 * 1e3;
10457
10457
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
@@ -10630,7 +10630,7 @@ import { join as join2 } from "path";
10630
10630
  import { homedir as homedir2 } from "os";
10631
10631
  var FAUX_DIR2 = join2(homedir2(), ".faux");
10632
10632
  var CACHE_PATH = join2(FAUX_DIR2, "tool-cache.json");
10633
- var API_BASE = "https://mcp.faux.design";
10633
+ var API_BASE = process.env.FAUX_API_URL || "https://api.faux.design";
10634
10634
  async function loadCachedTools() {
10635
10635
  try {
10636
10636
  const raw = await readFile2(CACHE_PATH, "utf-8");
@@ -10756,10 +10756,12 @@ var CdpClient = class {
10756
10756
  pending = /* @__PURE__ */ new Map();
10757
10757
  eventHandlers = /* @__PURE__ */ new Map();
10758
10758
  figmaContextId = null;
10759
+ _targetWsUrl = null;
10759
10760
  // -------------------------------------------------------------------------
10760
10761
  // Connection
10761
10762
  // -------------------------------------------------------------------------
10762
10763
  async connect(wsUrl) {
10764
+ this._targetWsUrl = wsUrl;
10763
10765
  return new Promise((resolve, reject) => {
10764
10766
  const ws = new wrapper_default(wsUrl);
10765
10767
  const timeout = setTimeout(() => {
@@ -10865,7 +10867,7 @@ var CdpClient = class {
10865
10867
  this.on("Runtime.executionContextCreated", handler);
10866
10868
  try {
10867
10869
  await this.send("Runtime.enable");
10868
- await new Promise((r) => setTimeout(r, 500));
10870
+ await new Promise((r) => setTimeout(r, 150));
10869
10871
  } finally {
10870
10872
  this.off("Runtime.executionContextCreated", handler);
10871
10873
  }
@@ -10928,6 +10930,15 @@ ${script}
10928
10930
  })()`);
10929
10931
  }
10930
10932
  // -------------------------------------------------------------------------
10933
+ // Reconnection
10934
+ // -------------------------------------------------------------------------
10935
+ /** Close current connection and reconnect to a different CDP target. */
10936
+ async reconnectToTarget(wsUrl) {
10937
+ this.close();
10938
+ await this.connect(wsUrl);
10939
+ await this.discoverFigmaContext();
10940
+ }
10941
+ // -------------------------------------------------------------------------
10931
10942
  // State
10932
10943
  // -------------------------------------------------------------------------
10933
10944
  get connected() {
@@ -10936,6 +10947,10 @@ ${script}
10936
10947
  get hasContext() {
10937
10948
  return this.figmaContextId !== null;
10938
10949
  }
10950
+ /** The WebSocket URL this client is currently connected to. */
10951
+ get targetWsUrl() {
10952
+ return this._targetWsUrl;
10953
+ }
10939
10954
  resetContext() {
10940
10955
  this.figmaContextId = null;
10941
10956
  }
@@ -10946,6 +10961,7 @@ ${script}
10946
10961
  this.ws?.close();
10947
10962
  this.ws = null;
10948
10963
  this.figmaContextId = null;
10964
+ this._targetWsUrl = null;
10949
10965
  }
10950
10966
  };
10951
10967
 
@@ -11057,10 +11073,20 @@ async function listTargets(port) {
11057
11073
  if (!res.ok) throw new Error(`Failed to list CDP targets (HTTP ${res.status})`);
11058
11074
  return await res.json();
11059
11075
  }
11060
- var FIGMA_DESIGN_URL_RE = /figma\.com\/(file|design)\//;
11076
+ var FIGMA_DESIGN_URL_RE = /figma\.com\/(file|design)\/([a-zA-Z0-9]+)/;
11061
11077
  function findFigmaDesignTarget(targets) {
11062
11078
  return targets.find((t) => t.type === "page" && FIGMA_DESIGN_URL_RE.test(t.url)) || targets.find((t) => t.type === "page") || null;
11063
11079
  }
11080
+ function findShellTarget(targets) {
11081
+ return targets.find((t) => t.type === "page" && t.url?.includes("shell.html")) || null;
11082
+ }
11083
+ function listDesignTargets(targets) {
11084
+ return targets.filter((t) => t.type === "page" && FIGMA_DESIGN_URL_RE.test(t.url)).map((t) => {
11085
+ const fileKey = t.url.match(FIGMA_DESIGN_URL_RE)[2];
11086
+ const fileName = t.title.replace(/\s[–—-]\s*Figma\s*$/u, "").trim();
11087
+ return { ...t, fileKey, fileName };
11088
+ });
11089
+ }
11064
11090
  async function probeCdpPorts() {
11065
11091
  for (const port of KNOWN_CDP_PORTS) {
11066
11092
  const { alive, isFigma } = await isCdpAlive(port);
@@ -11438,6 +11464,229 @@ var PluginWsServer = class {
11438
11464
  }
11439
11465
  };
11440
11466
 
11467
+ // src/file-tracker.ts
11468
+ var FileChangedError = class extends Error {
11469
+ previousFileName;
11470
+ currentFileName;
11471
+ currentFileKey;
11472
+ allFiles;
11473
+ constructor(previousFileName, currentFileName, currentFileKey, allFiles) {
11474
+ super(`Active Figma file changed from "${previousFileName}" to "${currentFileName}".`);
11475
+ this.name = "FileChangedError";
11476
+ this.previousFileName = previousFileName;
11477
+ this.currentFileName = currentFileName;
11478
+ this.currentFileKey = currentFileKey;
11479
+ this.allFiles = allFiles;
11480
+ }
11481
+ };
11482
+ var FileUnknownError = class extends Error {
11483
+ allFiles;
11484
+ constructor(allFiles) {
11485
+ super("Cannot detect which Figma file is active. Please confirm the target file.");
11486
+ this.name = "FileUnknownError";
11487
+ this.allFiles = allFiles;
11488
+ }
11489
+ };
11490
+ var LAST_KNOWN_STALE_MS = 5 * 6e4;
11491
+ var SHELL_DETECT_TIMEOUT_MS = 500;
11492
+ var CdpFileTracker = class {
11493
+ cdpPort;
11494
+ shellWsUrl = null;
11495
+ _activeFileKey = null;
11496
+ _lastKnownFileKey = null;
11497
+ _lastKnownFileName = null;
11498
+ _lastAcknowledgedAt = 0;
11499
+ _files = /* @__PURE__ */ new Map();
11500
+ _lastDetectTime = 0;
11501
+ _cachedResult = null;
11502
+ constructor(cdpPort) {
11503
+ this.cdpPort = cdpPort;
11504
+ }
11505
+ // -------------------------------------------------------------------------
11506
+ // Public API
11507
+ // -------------------------------------------------------------------------
11508
+ /**
11509
+ * Detect the currently active file. Uses cached result if fresh.
11510
+ * @param cacheTtlMs How long to trust cached results (0 = always re-detect).
11511
+ */
11512
+ async detectActiveFile(cacheTtlMs = 2e3) {
11513
+ if (this._cachedResult && cacheTtlMs > 0 && Date.now() - this._lastDetectTime < cacheTtlMs) {
11514
+ return this._cachedResult;
11515
+ }
11516
+ const result = await this.detect();
11517
+ this._cachedResult = result;
11518
+ this._lastDetectTime = Date.now();
11519
+ if (result.activeFile) {
11520
+ this._activeFileKey = result.activeFile.fileKey;
11521
+ }
11522
+ return result;
11523
+ }
11524
+ /** List all open design files from /json/list. */
11525
+ async listOpenFiles() {
11526
+ try {
11527
+ const targets = await listTargets(this.cdpPort);
11528
+ return this.parseDesignTargets(targets);
11529
+ } catch {
11530
+ return Array.from(this._files.values());
11531
+ }
11532
+ }
11533
+ /** Get cached file info by fileKey. */
11534
+ getFileTarget(fileKey) {
11535
+ return this._files.get(fileKey) || null;
11536
+ }
11537
+ /** Whether the detected active file differs from the last acknowledged file. */
11538
+ hasFileChanged() {
11539
+ return this._activeFileKey !== null && this._lastKnownFileKey !== null && this._activeFileKey !== this._lastKnownFileKey;
11540
+ }
11541
+ /**
11542
+ * Acknowledge the current active file as the intended target.
11543
+ * Call AFTER successful execution (AR-3).
11544
+ */
11545
+ acknowledgeActiveFile() {
11546
+ if (this._activeFileKey) {
11547
+ this._lastKnownFileKey = this._activeFileKey;
11548
+ this._lastKnownFileName = this._files.get(this._activeFileKey)?.fileName ?? null;
11549
+ this._lastAcknowledgedAt = Date.now();
11550
+ }
11551
+ }
11552
+ get activeFileKey() {
11553
+ return this._activeFileKey;
11554
+ }
11555
+ get lastKnownFileKey() {
11556
+ return this._lastKnownFileKey;
11557
+ }
11558
+ get lastKnownFileName() {
11559
+ return this._lastKnownFileName;
11560
+ }
11561
+ get activeFileName() {
11562
+ return this._activeFileKey ? this._files.get(this._activeFileKey)?.fileName ?? null : null;
11563
+ }
11564
+ // -------------------------------------------------------------------------
11565
+ // Private: Core Detection
11566
+ // -------------------------------------------------------------------------
11567
+ async detect() {
11568
+ let targets;
11569
+ try {
11570
+ targets = await listTargets(this.cdpPort);
11571
+ if (!Array.isArray(targets)) targets = [];
11572
+ } catch {
11573
+ return this.fallbackToLastKnown([]);
11574
+ }
11575
+ const designFiles = this.parseDesignTargets(targets);
11576
+ this._files.clear();
11577
+ for (const f of designFiles) {
11578
+ this._files.set(f.fileKey, f);
11579
+ }
11580
+ if (designFiles.length === 0) {
11581
+ return { activeFile: null, allFiles: [], method: "none", confidence: "none" };
11582
+ }
11583
+ if (designFiles.length === 1) {
11584
+ this._activeFileKey = designFiles[0].fileKey;
11585
+ return { activeFile: designFiles[0], allFiles: designFiles, method: "single-file", confidence: "high" };
11586
+ }
11587
+ const shellTabName = await this.detectViaShellTabBar(targets);
11588
+ if (shellTabName) {
11589
+ const matched = this.matchTabNameToFile(shellTabName, designFiles);
11590
+ if (matched) {
11591
+ this._activeFileKey = matched.fileKey;
11592
+ log(`File tracker: active file "${matched.fileName}" (${matched.fileKey}) via shell tab bar`);
11593
+ return { activeFile: matched, allFiles: designFiles, method: "shell-tab-bar", confidence: "high" };
11594
+ }
11595
+ }
11596
+ return this.fallbackToLastKnown(designFiles);
11597
+ }
11598
+ // -------------------------------------------------------------------------
11599
+ // Private: Shell.html Tab Bar Detection (PRIMARY)
11600
+ // -------------------------------------------------------------------------
11601
+ /**
11602
+ * Read the active tab name from Figma's shell.html tab bar.
11603
+ * Security: uses a fixed literal expression — never interpolate user input (SR-1).
11604
+ */
11605
+ async detectViaShellTabBar(targets) {
11606
+ if (!this.shellWsUrl) {
11607
+ const shell = findShellTarget(targets);
11608
+ if (!shell?.webSocketDebuggerUrl) return null;
11609
+ if (!shell.webSocketDebuggerUrl.startsWith("ws://127.0.0.1:")) return null;
11610
+ this.shellWsUrl = shell.webSocketDebuggerUrl;
11611
+ }
11612
+ const client = new CdpClient();
11613
+ try {
11614
+ const connectPromise = client.connect(this.shellWsUrl);
11615
+ const timeoutPromise = new Promise(
11616
+ (_, reject) => setTimeout(() => reject(new Error("shell detect timeout")), SHELL_DETECT_TIMEOUT_MS)
11617
+ );
11618
+ await Promise.race([connectPromise, timeoutPromise]);
11619
+ const result = await client.send("Runtime.evaluate", {
11620
+ expression: `document.querySelector('[role="tab"][aria-selected="true"]')?.getAttribute('aria-label')`,
11621
+ returnByValue: true
11622
+ });
11623
+ const raw = result?.result?.value;
11624
+ return this.parseTabNameFromCdpResult(raw);
11625
+ } catch {
11626
+ this.shellWsUrl = null;
11627
+ return null;
11628
+ } finally {
11629
+ client.close();
11630
+ }
11631
+ }
11632
+ // -------------------------------------------------------------------------
11633
+ // Private: Fallback — Last Known
11634
+ // -------------------------------------------------------------------------
11635
+ fallbackToLastKnown(designFiles) {
11636
+ if (this._lastKnownFileKey && !this.isLastKnownStale()) {
11637
+ const existing = designFiles.find((f) => f.fileKey === this._lastKnownFileKey);
11638
+ if (existing) {
11639
+ this._activeFileKey = existing.fileKey;
11640
+ log(`File tracker: using last-known file "${existing.fileName}" (low confidence)`);
11641
+ return { activeFile: existing, allFiles: designFiles, method: "last-known", confidence: "low" };
11642
+ }
11643
+ }
11644
+ log("File tracker: cannot detect active file \u2014 shell detection failed and last-known is stale or missing");
11645
+ return { activeFile: null, allFiles: designFiles, method: "none", confidence: "none" };
11646
+ }
11647
+ /** True if the last acknowledged execution was more than 5 minutes ago. */
11648
+ isLastKnownStale() {
11649
+ if (this._lastAcknowledgedAt === 0) return false;
11650
+ return Date.now() - this._lastAcknowledgedAt > LAST_KNOWN_STALE_MS;
11651
+ }
11652
+ // -------------------------------------------------------------------------
11653
+ // Private: Matching & Validation
11654
+ // -------------------------------------------------------------------------
11655
+ /**
11656
+ * Match a tab name from shell.html against design file targets.
11657
+ * Returns null if ambiguous (multiple files with same name) — AR-9.
11658
+ */
11659
+ matchTabNameToFile(tabName, designFiles) {
11660
+ const matches = designFiles.filter((f) => f.fileName === tabName);
11661
+ if (matches.length === 1) return matches[0];
11662
+ if (matches.length > 1) {
11663
+ warn(`File tracker: ${matches.length} files named "${tabName}" \u2014 cannot disambiguate`);
11664
+ return null;
11665
+ }
11666
+ return null;
11667
+ }
11668
+ /**
11669
+ * Validate and clean the CDP result from shell.html evaluation (SR-3).
11670
+ * Rejects non-string, empty, oversized, or control-character values.
11671
+ */
11672
+ parseTabNameFromCdpResult(raw) {
11673
+ if (typeof raw !== "string") return null;
11674
+ const trimmed = raw.trim();
11675
+ if (trimmed.length === 0 || trimmed.length > 512) return null;
11676
+ if (/[\x00-\x1f\x7f]/.test(trimmed)) return null;
11677
+ return trimmed;
11678
+ }
11679
+ /** Parse design targets from /json/list into FigmaFileInfo[]. */
11680
+ parseDesignTargets(targets) {
11681
+ return listDesignTargets(targets).map((t) => ({
11682
+ fileKey: t.fileKey,
11683
+ fileName: t.fileName,
11684
+ targetId: t.id,
11685
+ wsUrl: t.webSocketDebuggerUrl
11686
+ }));
11687
+ }
11688
+ };
11689
+
11441
11690
  // node_modules/zod/v3/helpers/util.js
11442
11691
  var util;
11443
11692
  (function(util2) {
@@ -25585,7 +25834,7 @@ function createMcpServer(deps) {
25585
25834
  },
25586
25835
  {
25587
25836
  name: "setup_figma",
25588
- description: "Ensure Figma Desktop is running and the plugin is connected. Call this before any design work. Launches Figma if needed, waits for the plugin to connect, and provides setup guidance. Idempotent \u2014 safe to call multiple times. Returns connection status and active file info.",
25837
+ 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.",
25589
25838
  inputSchema: {
25590
25839
  type: "object",
25591
25840
  properties: {},
@@ -25675,7 +25924,10 @@ function createMcpServer(deps) {
25675
25924
  macro: typeof params._intent_macro === "string" ? params._intent_macro : void 0
25676
25925
  };
25677
25926
  const script = await deps.generateScript(name, params);
25678
- const result = await deps.executeScript(script, intents);
25927
+ const annotation = TOOL_ANNOTATIONS[name];
25928
+ const isReadOnly = annotation?.readOnlyHint === true;
25929
+ const cacheTtlMs = isReadOnly ? 2e3 : 0;
25930
+ const result = await deps.executeScript(script, intents, { cacheTtlMs });
25679
25931
  const imageData = extractImageData(result);
25680
25932
  if (imageData) {
25681
25933
  const content = [
@@ -25691,6 +25943,37 @@ function createMcpServer(deps) {
25691
25943
  content: [{ type: "text", text }]
25692
25944
  };
25693
25945
  } catch (err) {
25946
+ if (err instanceof FileChangedError) {
25947
+ const annotation = TOOL_ANNOTATIONS[name];
25948
+ const isReadOnly = annotation?.readOnlyHint === true;
25949
+ const payload = JSON.stringify({
25950
+ status: "confirmation_required",
25951
+ reason: "ACTIVE_FILE_CHANGED",
25952
+ previousFile: err.previousFileName,
25953
+ currentFile: err.currentFileName,
25954
+ currentFileKey: err.currentFileKey,
25955
+ message: `The active Figma file changed from "${err.previousFileName}" to "${err.currentFileName}". Please confirm which file you want to work on.`,
25956
+ availableFiles: err.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName }))
25957
+ }, null, 2);
25958
+ if (isReadOnly) {
25959
+ return { content: [{ type: "text", text: payload }] };
25960
+ }
25961
+ return { content: [{ type: "text", text: payload }], isError: true };
25962
+ }
25963
+ if (err instanceof FileUnknownError) {
25964
+ return {
25965
+ content: [{
25966
+ type: "text",
25967
+ text: JSON.stringify({
25968
+ status: "confirmation_required",
25969
+ reason: "ACTIVE_FILE_UNKNOWN",
25970
+ message: "Cannot detect which Figma file is active. Please confirm the target file.",
25971
+ availableFiles: err.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName }))
25972
+ }, null, 2)
25973
+ }],
25974
+ isError: true
25975
+ };
25976
+ }
25694
25977
  const message = err instanceof Error ? err.message : String(err);
25695
25978
  const isAuthError = message.includes("auth") || message.includes("401") || message.includes("AUTH_EXPIRED");
25696
25979
  return {
@@ -25740,6 +26023,7 @@ async function startServer(server2) {
25740
26023
  // src/index.ts
25741
26024
  var auth;
25742
26025
  var cdpClient = null;
26026
+ var fileTracker = null;
25743
26027
  var pluginServer = new PluginWsServer();
25744
26028
  var forceTransport = process.env.FAUX_TRANSPORT;
25745
26029
  async function tryConnectCdp() {
@@ -25747,20 +26031,23 @@ async function tryConnectCdp() {
25747
26031
  if (cdpClient?.connected && cdpClient.hasContext) return cdpClient;
25748
26032
  cdpClient?.close();
25749
26033
  cdpClient = null;
25750
- const { targets } = await ensureFigma();
25751
- const target = findFigmaDesignTarget(targets);
26034
+ const connection = await ensureFigma();
26035
+ const target = findFigmaDesignTarget(connection.targets);
25752
26036
  if (!target) return null;
25753
26037
  const client = new CdpClient();
25754
26038
  await client.connect(target.webSocketDebuggerUrl);
25755
26039
  await client.discoverFigmaContext();
25756
26040
  log(`CDP connected: ${target.title}`);
25757
26041
  cdpClient = client;
26042
+ if (!fileTracker) {
26043
+ fileTracker = new CdpFileTracker(connection.port);
26044
+ }
25758
26045
  return client;
25759
26046
  } catch {
25760
26047
  return null;
25761
26048
  }
25762
26049
  }
25763
- async function executeScript(script, intents) {
26050
+ async function executeScript(script, intents, opts) {
25764
26051
  if (forceTransport !== "cdp" && pluginServer.hasConnections) {
25765
26052
  log(`Executing via plugin (file: ${pluginServer.activeFileId || "active"})`);
25766
26053
  return pluginServer.executeScript(script, void 0, intents);
@@ -25768,12 +26055,34 @@ async function executeScript(script, intents) {
25768
26055
  if (forceTransport !== "plugin") {
25769
26056
  const client = await tryConnectCdp();
25770
26057
  if (client) {
26058
+ if (fileTracker) {
26059
+ const detection = await fileTracker.detectActiveFile(opts?.cacheTtlMs);
26060
+ if (!detection.activeFile && detection.allFiles.length > 1) {
26061
+ throw new FileUnknownError(detection.allFiles);
26062
+ }
26063
+ if (fileTracker.hasFileChanged()) {
26064
+ throw new FileChangedError(
26065
+ fileTracker.lastKnownFileName,
26066
+ fileTracker.activeFileName,
26067
+ fileTracker.activeFileKey,
26068
+ detection.allFiles
26069
+ );
26070
+ }
26071
+ if (detection.activeFile && client.targetWsUrl !== detection.activeFile.wsUrl) {
26072
+ log(`Switching CDP target to "${detection.activeFile.fileName}" (${detection.activeFile.fileKey})`);
26073
+ await client.reconnectToTarget(detection.activeFile.wsUrl);
26074
+ }
26075
+ }
25771
26076
  log("Executing via CDP");
25772
26077
  try {
25773
- return await client.executeScript(script);
26078
+ const result = await client.executeScript(script);
26079
+ fileTracker?.acknowledgeActiveFile();
26080
+ return result;
25774
26081
  } catch (err) {
25775
26082
  if (err instanceof ContextDestroyedError) {
25776
- return await recoverCdp(client, script);
26083
+ const result = await recoverCdp(client, script);
26084
+ fileTracker?.acknowledgeActiveFile();
26085
+ return result;
25777
26086
  }
25778
26087
  throw err;
25779
26088
  }
@@ -25841,6 +26150,16 @@ async function setupFigma(params) {
25841
26150
  return pluginReadyResult();
25842
26151
  }
25843
26152
  if (forceTransport !== "plugin" && cdpClient?.connected && cdpClient.hasContext) {
26153
+ if (fileTracker) {
26154
+ const detection = await fileTracker.detectActiveFile();
26155
+ return {
26156
+ status: "ready",
26157
+ transport: "cdp",
26158
+ message: `Connected via CDP. Active file: "${detection.activeFile?.fileName ?? "unknown"}". ${detection.allFiles.length} file(s) open. Ready to design.`,
26159
+ activeFile: detection.activeFile?.fileName,
26160
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName }))
26161
+ };
26162
+ }
25844
26163
  return {
25845
26164
  status: "ready",
25846
26165
  transport: "cdp",
@@ -25860,11 +26179,17 @@ async function setupFigma(params) {
25860
26179
  await client.discoverFigmaContext();
25861
26180
  cdpClient = client;
25862
26181
  log(`CDP connected: ${target.title}`);
26182
+ if (!fileTracker) {
26183
+ fileTracker = new CdpFileTracker(existing.port);
26184
+ }
26185
+ const detection = await fileTracker.detectActiveFile();
26186
+ fileTracker.acknowledgeActiveFile();
25863
26187
  return {
25864
26188
  status: "ready",
25865
26189
  transport: "cdp",
25866
- message: `Connected via CDP. Active file: ${target.title}. Ready to design.`,
25867
- activeFile: target.title,
26190
+ message: `Connected via CDP. Active file: "${detection.activeFile?.fileName ?? target.title}". ${detection.allFiles.length} file(s) open. Ready to design.`,
26191
+ activeFile: detection.activeFile?.fileName ?? target.title,
26192
+ openFiles: detection.allFiles.map((f) => ({ fileKey: f.fileKey, fileName: f.fileName })),
25868
26193
  port: existing.port
25869
26194
  };
25870
26195
  } catch {
@@ -26000,10 +26325,31 @@ async function main() {
26000
26325
  function shutdown() {
26001
26326
  pluginServer.close();
26002
26327
  cdpClient?.close();
26328
+ fileTracker = null;
26003
26329
  process.exit(0);
26004
26330
  }
26005
26331
  process.on("SIGINT", shutdown);
26006
26332
  process.on("SIGTERM", shutdown);
26333
+ var ORPHAN_CHECK_MS = 5e3;
26334
+ var originalPpid = process.ppid;
26335
+ function isParentAlive() {
26336
+ try {
26337
+ process.kill(originalPpid, 0);
26338
+ return true;
26339
+ } catch {
26340
+ return false;
26341
+ }
26342
+ }
26343
+ process.stdin.on("end", () => {
26344
+ log("stdin closed \u2014 parent process gone, shutting down");
26345
+ shutdown();
26346
+ });
26347
+ setInterval(() => {
26348
+ if (!isParentAlive()) {
26349
+ log(`Parent process (PID ${originalPpid}) gone \u2014 shutting down`);
26350
+ shutdown();
26351
+ }
26352
+ }, ORPHAN_CHECK_MS).unref();
26007
26353
  main().catch((err) => {
26008
26354
  error(err instanceof Error ? err.message : String(err));
26009
26355
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faux-studio",
3
- "version": "0.3.12",
3
+ "version": "0.4.0",
4
4
  "description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsup",
17
+ "build:staging": "tsup --env.FAUX_API_URL https://staging-api.faux.design --env.FAUX_AUTH_URL https://staging-auth.faux.design",
17
18
  "build:plugin": "cd plugin && npm run build",
18
19
  "build:all": "npm run build && npm run build:plugin",
19
20
  "dev": "tsup --watch",