figma-relai 0.7.5 → 0.7.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.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  registerPrompts,
10
10
  registerRoomTool,
11
11
  resolveFileIdentity
12
- } from "./chunk-BRUWHN2A.js";
12
+ } from "./chunk-PQGMCFD7.js";
13
13
  import "./chunk-SDINVROO.js";
14
14
  import {
15
15
  loadState,
@@ -122,6 +122,18 @@ function routeResponse(response, tracker) {
122
122
  tracker.resolve(response.id, response.result);
123
123
  }
124
124
  }
125
+ function chooseRepairRoom(staleRoom, rooms) {
126
+ const withPlugin = rooms.filter((r) => r.hasPlugin && r.room !== staleRoom);
127
+ if (withPlugin.length === 1) return { room: withPlugin[0].room };
128
+ if (withPlugin.length === 0) {
129
+ return {
130
+ error: "The Figma plugin is not open. Open the Relai plugin in Figma \u2014 it will reconnect automatically."
131
+ };
132
+ }
133
+ return {
134
+ error: `The plugin left room ${staleRoom} and several are now connected: ${withPlugin.map((r) => r.fileName ? `"${r.fileName}" (room ${r.room})` : `room ${r.room}`).join(", ")}. Call join_room with the one you want.`
135
+ };
136
+ }
125
137
  var FigmaConnection = class {
126
138
  ws = null;
127
139
  currentRoom = null;
@@ -136,6 +148,11 @@ var FigmaConnection = class {
136
148
  // Presence recorded per room: join-time broadcasts can arrive before
137
149
  // currentRoom is updated, so keying by room avoids the ordering race
138
150
  presenceByRoom = /* @__PURE__ */ new Map();
151
+ // Did the caller NAME this room, or did we pick it for them? A room the
152
+ // caller aimed at is never traded for a different one — they may be waiting
153
+ // for that file to open, and silently following the only live plugin would
154
+ // send their writes into somebody else's file.
155
+ roomWasChosenByCaller = false;
139
156
  // Designer activity piggybacked on plugin responses (selection changes etc.)
140
157
  eventQueue = [];
141
158
  constructor(serverUrl2 = "localhost", port2 = 9055, options = {}) {
@@ -274,7 +291,8 @@ var FigmaConnection = class {
274
291
  }
275
292
  }
276
293
  // Join a room for communication
277
- async joinRoom(roomName) {
294
+ async joinRoom(roomName, chosenByCaller = true) {
295
+ this.roomWasChosenByCaller = chosenByCaller;
278
296
  await this.sendCommand("join", { room: roomName });
279
297
  this.currentRoom = roomName;
280
298
  this.options.onRoomChanged?.(roomName);
@@ -297,10 +315,10 @@ var FigmaConnection = class {
297
315
  const withPlugin = rooms.filter((r) => r.hasPlugin);
298
316
  const saved = this.options.initialRoom;
299
317
  if (saved && withPlugin.some((r) => r.room === saved)) {
300
- return this.joinRoom(saved);
318
+ return this.joinRoom(saved, false);
301
319
  }
302
320
  if (withPlugin.length === 1) {
303
- return this.joinRoom(withPlugin[0].room);
321
+ return this.joinRoom(withPlugin[0].room, false);
304
322
  }
305
323
  if (withPlugin.length === 0) {
306
324
  throw new Error(
@@ -335,6 +353,34 @@ var FigmaConnection = class {
335
353
  } catch {
336
354
  }
337
355
  }
356
+ /**
357
+ * The paired room went quiet. Ask the relay who is actually there and, if
358
+ * exactly one plugin is connected, move to it.
359
+ *
360
+ * A plugin that is closed and reopened — or a file switched to one of its
361
+ * branches — comes back under a NEW room name. ensureRoom() only picks a
362
+ * room when there is none, so without this the session stays pinned to the
363
+ * dead room and every command reports "the plugin is not open" while
364
+ * join_room cheerfully lists the live one.
365
+ */
366
+ async repairPairing() {
367
+ const stale = this.currentRoom;
368
+ let rooms = [];
369
+ try {
370
+ rooms = await this.listRooms();
371
+ } catch {
372
+ throw new Error(
373
+ "The Figma plugin is not open, and the relay could not be asked which rooms are live. Open the Relai plugin in Figma and try again."
374
+ );
375
+ }
376
+ const choice = chooseRepairRoom(stale, rooms);
377
+ if (choice.room) {
378
+ await this.joinRoom(choice.room, false);
379
+ logger.info(`Re-paired: ${stale} \u2192 ${choice.room} (plugin moved rooms)`);
380
+ return;
381
+ }
382
+ throw new Error(choice.error);
383
+ }
338
384
  // Send a command to Figma plugin via the relay
339
385
  async sendCommand(command, params = {}, timeoutMs = 3e4) {
340
386
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
@@ -360,9 +406,13 @@ var FigmaConnection = class {
360
406
  await this.waitForPlugin(2500);
361
407
  }
362
408
  if (this.currentRoom && this.presenceByRoom.get(this.currentRoom) === false) {
363
- throw new Error(
364
- "The Figma plugin is not open. Open the Relai plugin in Figma \u2014 it will reconnect to the same room automatically."
365
- );
409
+ if (!this.roomWasChosenByCaller) {
410
+ await this.repairPairing();
411
+ } else {
412
+ throw new Error(
413
+ `No Figma plugin is in room ${this.currentRoom}. Open the file you want in Figma and run the plugin, or call join_room without a room to see which are live.`
414
+ );
415
+ }
366
416
  }
367
417
  }
368
418
  const result = await new Promise((resolve, reject) => {
@@ -481,7 +531,7 @@ if (subcommand === "login" || subcommand === "logout") {
481
531
  process.exit(exitCode);
482
532
  }
483
533
  if (subcommand === "manifest" || subcommand === "docs" || subcommand === "doctor") {
484
- const { buildManifest } = await import("./manifest-UX6MDMVV.js");
534
+ const { buildManifest } = await import("./manifest-ASJDLEEP.js");
485
535
  if (subcommand === "doctor") {
486
536
  const { runDoctor, renderDoctor } = await import("./doctor-MZDRG46R.js");
487
537
  const results = await runDoctor();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/connection.ts","../src/logger.ts","../src/request-tracker.ts","../src/embedded-relay.ts","../src/session-log.ts"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\nimport { FigmaConnection } from \"./connection.js\";\nimport { logger } from \"./logger.js\";\nimport {\n registerAllTools,\n registerRoomTool,\n registerEventsTool,\n listToolCatalog,\n} from \"./tools/index.js\";\nimport { startEmbeddedRelay, type EmbeddedRelay } from \"./embedded-relay.js\";\nimport { refreshTokenStatus } from \"./credentials.js\";\nimport { resolveFileIdentity } from \"./tools/core/file-identity.js\";\nimport { loadState, saveState } from \"./state.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { recordCommand, getSessionLog } from \"./session-log.js\";\nimport { VERSION } from \"./version.js\";\n\n// Parse CLI arguments\nconst args = process.argv.slice(2);\nconst serverArg = args.find((arg) => arg.startsWith(\"--server=\"));\nconst serverUrl = serverArg ? serverArg.split(\"=\")[1] : \"localhost\";\n\nconst portArg = args.find((arg) => arg.startsWith(\"--port=\"));\nconst port = portArg ? parseInt(portArg.split(\"=\")[1]) : 9055;\n\nconst roomArg = args.find((arg) => arg.startsWith(\"--room=\"));\n\n// Build-time inventory for the plugin UI tool list; exits without stdio setup\nif (args.includes(\"--list-tools\")) {\n console.log(JSON.stringify(listToolCatalog(), null, 2));\n process.exit(0);\n}\n\n// CLI subcommands (Astryx-style: the contract is executable, not prose).\n// `manifest` prints the machine-readable contract, `docs` renders it for\n// humans, `doctor` triages the local environment. All exit before stdio.\nconst subcommand = args.find((a) => !a.startsWith(\"--\"));\nif (subcommand === \"login\" || subcommand === \"logout\") {\n const { runLogin, runLogout } = await import(\"./cli/login.js\");\n const { exitCode, message } = subcommand === \"login\" ? await runLogin() : runLogout();\n console.log(message);\n process.exit(exitCode);\n}\nif (subcommand === \"manifest\" || subcommand === \"docs\" || subcommand === \"doctor\") {\n const { buildManifest } = await import(\"./cli/manifest.js\");\n if (subcommand === \"doctor\") {\n const { runDoctor, renderDoctor } = await import(\"./cli/doctor.js\");\n const results = await runDoctor();\n console.log(args.includes(\"--json\") ? JSON.stringify(results, null, 2) : renderDoctor(results));\n process.exit(results.some((r) => r.status === \"warn\") ? 1 : 0);\n }\n const manifest = await buildManifest(VERSION);\n if (subcommand === \"manifest\") {\n console.log(JSON.stringify(manifest, null, 2));\n } else {\n const { renderToolDoc, renderToolIndex } = await import(\"./cli/docs.js\");\n const toolName = args[args.indexOf(\"docs\") + 1];\n console.log(\n toolName && !toolName.startsWith(\"--\")\n ? renderToolDoc(manifest, toolName)\n : renderToolIndex(manifest)\n );\n }\n process.exit(0);\n}\n// A word we don't know is a mistake, not a request to start a server. Falling\n// through meant `figma-relai lgoin` — or any subcommand newer than the copy npx\n// happened to fetch — booted stdio and sat there in total silence.\nif (subcommand) {\n process.stderr.write(\n `Unknown command \"${subcommand}\" (figma-relai ${VERSION}).\\n` +\n \" login · logout · doctor · docs [tool] · manifest\\n\" +\n \" no command at all starts the MCP server over stdio\\n\\n\" +\n \"If your AI client shows this command, npx may be running an older\\n\" +\n \"published version — try: npx -y figma-relai@latest \" +\n `${subcommand}\\n`\n );\n process.exit(2);\n}\n\nasync function main() {\n // Create MCP server\n const server = createServer();\n\n // Ask Figma whether the stored token is still alive before anyone lights a\n // lamp for it. Fire-and-forget: nothing here should wait on the network.\n void refreshTokenStatus();\n\n // Host the relay in this process unless another instance already does\n // (bind-or-connect: first MCP server binds 9055, later ones connect to it).\n // Only for local relays — a remote --server means someone runs it there.\n let relay: EmbeddedRelay | null = null;\n if (serverUrl === \"localhost\") {\n relay = await startEmbeddedRelay(port, VERSION);\n if (!relay) {\n logger.info(`Port ${port} in use — connecting to the existing relay`);\n }\n }\n\n const initialRoom =\n roomArg?.split(\"=\")[1] ?? process.env.FIGMA_RELAI_ROOM ?? loadState().room ?? null;\n\n // Create WebSocket connection to relay\n const connection = new FigmaConnection(serverUrl, port, {\n initialRoom,\n onRoomChanged: (room) => saveState({ room }),\n beforeReconnect: async () => {\n // The hosting process may have exited; take over if the port is free\n if (serverUrl === \"localhost\" && !relay) {\n relay = await startEmbeddedRelay(port, VERSION);\n if (relay) logger.info(\"Took over relay hosting\");\n }\n },\n });\n\n // Register the join_room tool\n registerRoomTool(\n server,\n (room) => connection.joinRoom(room),\n () => connection.listRooms()\n );\n\n // Register designer-activity polling + the AI's own audit trail\n registerEventsTool(server, () => connection.consumeEvents(), getSessionLog);\n\n // Expose skill documents as MCP prompts (inlined at build time)\n registerPrompts(server);\n\n // Which file is this? Nothing used to ask until manage_comments did, so the\n // panel's lineage stayed blank in every session that never touched comments\n // — and blank forever on a machine that never touches them at all. The first\n // command that reaches the plugin is the earliest honest moment to find out.\n // A failure is not final: the designer may reload the plugin, open another\n // file, or store a token mid-session, and a latch set on the first attempt\n // would keep the panel blank for the rest of the process. Success latches;\n // failure gets a few more chances and then stops asking.\n let identityProbes = 0;\n let identityResolved = false;\n const probeIdentityOnce = () => {\n if (identityResolved || identityProbes >= 3) return;\n identityProbes += 1;\n void resolveFileIdentity((c, p2, t) => connection.sendCommand(c, p2, t))\n .then(() => {\n identityResolved = true;\n })\n .catch(() => {\n // No token, nothing published, no branch — all ordinary. The panel keeps\n // showing what it showed before and the caller is never told.\n });\n };\n\n // Register all domain tools; every plugin command lands in the session log\n registerAllTools(server, async (command, params, timeoutMs) => {\n const t0 = Date.now();\n const nodeId =\n typeof (params as { nodeId?: unknown })?.nodeId === \"string\"\n ? ((params as { nodeId: string }).nodeId)\n : undefined;\n try {\n const result = await connection.sendCommand(command, params, timeoutMs);\n recordCommand({ ts: t0, command, nodeId, ok: true, ms: Date.now() - t0 });\n // Not for the identity commands themselves, or the probe calls itself\n if (command !== \"get_file_identity\" && command !== \"set_file_identity\") {\n probeIdentityOnce();\n }\n return result;\n } catch (error) {\n recordCommand({\n ts: t0,\n command,\n nodeId,\n ok: false,\n ms: Date.now() - t0,\n error: error instanceof Error ? error.message.slice(0, 200) : String(error),\n });\n throw error;\n }\n });\n\n // Connect to relay (auto-reconnects on failure)\n try {\n await connection.connect();\n logger.info(\"Connected to relay successfully\");\n } catch (error) {\n logger.warn(\n `Could not connect initially: ${error instanceof Error ? error.message : String(error)}`\n );\n logger.warn(\"Will attempt to connect when the first command is sent\");\n }\n\n // Start MCP server with stdio transport\n const transport = new StdioServerTransport();\n await server.connect(transport);\n logger.info(\"Relai MCP server running on stdio\");\n}\n\nmain().catch((error) => {\n logger.error(\n `Fatal error: ${error instanceof Error ? error.message : String(error)}`\n );\n process.exit(1);\n});\n","import WebSocket from \"ws\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { logger } from \"./logger.js\";\nimport { RequestTracker } from \"./request-tracker.js\";\nimport type { FigmaCommand } from \"@figma-relai/shared\";\nimport type { CommandProgressUpdate, RoomSummary } from \"@figma-relai/shared\";\n\nexport interface FigmaConnectionOptions {\n // Room restored from persisted state; used to auto-pair on the first command\n initialRoom?: string | null;\n // Called whenever a room is joined so the caller can persist it\n onRoomChanged?: (room: string) => void;\n // Called before each reconnect attempt (e.g. to take over relay hosting)\n beforeReconnect?: () => Promise<void>;\n}\n\n// Error payload sent by the plugin: a plain string, or an object with context\ninterface PluginErrorPayload {\n message?: string;\n command?: string;\n nodeId?: string;\n nodeType?: string;\n}\n\n// Flatten a plugin error (string or structured object) into one readable line\nexport function formatFigmaError(e: unknown): string {\n if (typeof e === \"string\") return e;\n const { message, command, nodeId, nodeType } = (e ?? {}) as PluginErrorPayload;\n const prefix = command ? `[${command}] ` : \"\";\n const suffix = nodeId\n ? ` (node ${nodeId}${nodeType ? `, type ${nodeType}` : \"\"})`\n : \"\";\n return `${prefix}${message ?? JSON.stringify(e)}${suffix}`;\n}\n\n// Route a response message to its pending request. Error responses have no\n// `result` field, so the error check must come first and resolution must key\n// on field presence — falsy results (0, \"\", false) are valid.\nexport function routeResponse(\n response: { id?: string; result?: unknown; error?: unknown } | undefined,\n tracker: RequestTracker\n): void {\n if (!response?.id || !tracker.has(response.id)) return;\n if (response.error !== undefined) {\n tracker.reject(response.id, new Error(formatFigmaError(response.error)));\n } else if (\"result\" in response) {\n tracker.resolve(response.id, response.result);\n }\n}\n\n// Manages WebSocket connection to the relay server\nexport class FigmaConnection {\n private ws: WebSocket | null = null;\n private currentRoom: string | null = null;\n private tracker = new RequestTracker();\n private wsUrl: string;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private shouldReconnect = false;\n private connectPromise: Promise<void> | null = null;\n private commandHandler: ((command: string, params: unknown) => void) | null = null;\n private options: FigmaConnectionOptions;\n private relayVerified = false;\n // Presence recorded per room: join-time broadcasts can arrive before\n // currentRoom is updated, so keying by room avoids the ordering race\n private presenceByRoom = new Map<string, boolean>();\n // Designer activity piggybacked on plugin responses (selection changes etc.)\n private eventQueue: unknown[] = [];\n\n constructor(\n serverUrl: string = \"localhost\",\n port: number = 9055,\n options: FigmaConnectionOptions = {}\n ) {\n const protocol = serverUrl === \"localhost\" ? \"ws\" : \"wss\";\n this.wsUrl =\n serverUrl === \"localhost\"\n ? `${protocol}://${serverUrl}:${port}`\n : `${protocol}://${serverUrl}`;\n this.options = options;\n }\n\n // Connect to WebSocket relay server\n connect(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n logger.info(\"Already connected to relay\");\n return Promise.resolve();\n }\n\n // If already connecting, return existing promise\n if (this.connectPromise) {\n return this.connectPromise;\n }\n\n logger.info(`Connecting to relay at ${this.wsUrl}...`);\n\n this.connectPromise = new Promise<void>((resolve, reject) => {\n const ws = new WebSocket(this.wsUrl);\n this.ws = ws;\n\n const cleanup = () => {\n this.connectPromise = null;\n };\n\n ws.on(\"open\", () => {\n logger.info(\"Connected to relay server\");\n this.shouldReconnect = true;\n cleanup();\n resolve();\n\n // Verify we reached a Relai relay (and not some other port 9055 app)\n this.relayVerified = false;\n try {\n ws.send(JSON.stringify({ type: \"hello\" }));\n } catch {\n // Send failure surfaces via close/error handlers\n }\n setTimeout(() => {\n if (!this.relayVerified && this.ws === ws) {\n logger.warn(\n \"Connected endpoint did not identify as a figma-relai relay — is another app using the port?\"\n );\n }\n }, 1500);\n\n // Rejoin the previous room after a relay restart / reconnect so the\n // AI doesn't land in a silent \"must join a room\" dead state\n if (this.currentRoom) {\n const room = this.currentRoom;\n this.sendCommand(\"join\", { room }).then(\n () => logger.info(`Rejoined room: ${room}`),\n (err) =>\n logger.warn(\n `Failed to rejoin room ${room}: ${err instanceof Error ? err.message : String(err)}`\n )\n );\n }\n });\n\n ws.on(\"message\", (data: WebSocket.Data) => {\n try {\n const json = JSON.parse(data.toString());\n\n // Relay identity probe response\n if (json.type === \"hello\" && json.server === \"figma-relai\") {\n this.relayVerified = true;\n logger.debug(`Relay verified (version ${json.version})`);\n return;\n }\n\n // Peer presence per room (is the plugin there?)\n if (json.type === \"presence\") {\n if (typeof json.room === \"string\" && Array.isArray(json.peers)) {\n this.presenceByRoom.set(\n json.room,\n json.peers.some((p: { role?: string }) => p.role === \"plugin\")\n );\n }\n return;\n }\n\n // Room listing response (auto-pairing)\n if (json.type === \"list_rooms_result\" && json.id) {\n this.tracker.resolve(json.id, json.rooms ?? []);\n return;\n }\n\n // Handle incoming commands (from plugin UI broadcasts)\n if (json.type === \"broadcast\" && json.message?.command) {\n this.commandHandler?.(json.message.command, json.message.params);\n return;\n }\n\n // Handle progress updates\n if (json.type === \"progress_update\") {\n const progressData = json.message?.data as CommandProgressUpdate;\n const requestId = json.id || \"\";\n\n if (requestId && this.tracker.has(requestId)) {\n this.tracker.resetTimeout(requestId);\n logger.info(\n `Progress: ${progressData.commandType} ${progressData.progress}% - ${progressData.message}`\n );\n }\n return;\n }\n\n // Collect designer-activity events piggybacked on responses\n if (Array.isArray(json.message?.events)) {\n this.eventQueue.push(...json.message.events);\n if (this.eventQueue.length > 50) {\n this.eventQueue = this.eventQueue.slice(-50);\n }\n }\n\n // Handle regular responses\n routeResponse(json.message, this.tracker);\n } catch (error) {\n logger.error(\n `Error parsing message: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n });\n\n ws.on(\"error\", (error) => {\n logger.error(`WebSocket error: ${error.message}`);\n cleanup();\n reject(error);\n });\n\n ws.on(\"close\", () => {\n logger.info(\"Disconnected from relay\");\n this.ws = null;\n // A command that was already on the wire may have run to completion in\n // Figma before the socket dropped — so say so instead of implying it\n // failed. Retrying a write on the caller's behalf could apply it twice.\n this.tracker.rejectAll(\n \"The link to Figma dropped while this command was in flight — it may already have applied. \" +\n \"Re-read the file before retrying (the server reconnects on its own within a few seconds).\"\n );\n cleanup();\n\n // Only auto-reconnect if not manually disconnected. beforeReconnect\n // lets the host process try to re-bind the relay port first (host\n // takeover when the previous hosting MCP server exited).\n if (this.shouldReconnect) {\n this.reconnectTimer = setTimeout(async () => {\n try {\n await this.options.beforeReconnect?.();\n } catch {\n // Takeover failure is fine; we may connect to another host\n }\n this.connect().catch(() => {});\n }, 2000);\n }\n });\n });\n\n return this.connectPromise;\n }\n\n // Disconnect and stop reconnection\n disconnect(): void {\n this.shouldReconnect = false;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n }\n\n // Join a room for communication\n async joinRoom(roomName: string): Promise<void> {\n await this.sendCommand(\"join\", { room: roomName });\n this.currentRoom = roomName;\n this.options.onRoomChanged?.(roomName);\n logger.info(`Joined room: ${roomName}`);\n }\n\n // Ask the relay which rooms exist and who is in them\n listRooms(): Promise<RoomSummary[]> {\n return new Promise((resolve, reject) => {\n const id = uuidv4();\n this.tracker.add(id, resolve as (value: unknown) => void, reject, 5000);\n this.ws!.send(JSON.stringify({ type: \"list_rooms\", id }));\n });\n }\n\n // Auto-pair with the plugin so join_room is only needed for disambiguation:\n // prefer the persisted room if its plugin is present, otherwise join the\n // single plugin room, otherwise explain exactly what to do.\n private async ensureRoom(): Promise<void> {\n if (this.currentRoom) return;\n\n const rooms = await this.listRooms();\n const withPlugin = rooms.filter((r) => r.hasPlugin);\n const saved = this.options.initialRoom;\n\n if (saved && withPlugin.some((r) => r.room === saved)) {\n return this.joinRoom(saved);\n }\n if (withPlugin.length === 1) {\n return this.joinRoom(withPlugin[0].room);\n }\n if (withPlugin.length === 0) {\n throw new Error(\n \"No Figma plugin is connected. Open the Relai plugin in Figma (it connects automatically), then try again.\"\n );\n }\n throw new Error(\n `Multiple Figma plugins are connected: ${withPlugin\n .map((r) => (r.fileName ? `\"${r.fileName}\" (room ${r.room})` : `room ${r.room}`))\n .join(\", \")}. Call join_room with the room of the file you want to control.`\n );\n }\n\n /**\n * Poll the presence map briefly, then ASK.\n *\n * Presence is only ever written by a pushed `presence` event, so a plugin\n * that closes and reopens can leave the cache stuck on false: the close set\n * it, and the reopen's event never landed. Polling a cache that nothing is\n * refreshing just burns the timeout and then reports \"the plugin is not\n * open\" about a plugin sitting right there — while list_rooms, which asks\n * the relay instead of trusting the cache, happily lists it.\n */\n private async waitForPlugin(ms: number): Promise<void> {\n const deadline = Date.now() + ms;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 150));\n if (!this.currentRoom || this.presenceByRoom.get(this.currentRoom) !== false) return;\n }\n if (!this.currentRoom) return;\n try {\n const rooms = await this.listRooms();\n const live = rooms.find((r) => r.room === this.currentRoom);\n if (live?.hasPlugin) this.presenceByRoom.set(this.currentRoom, true);\n } catch {\n // Unreachable relay is the caller's problem, not this cache's\n }\n }\n\n // Send a command to Figma plugin via the relay\n async sendCommand(\n command: FigmaCommand,\n params: unknown = {},\n timeoutMs: number = 30000\n ): Promise<unknown> {\n // Wait for connection if not connected\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n try {\n await this.connect();\n } catch {\n // The relay host may have just exited (port-handoff race on rapid\n // server restarts): try to take over hosting, then dial once more.\n try {\n await this.options.beforeReconnect?.();\n } catch {\n // Takeover failure is fine; the retry below may reach another host\n }\n try {\n await this.connect();\n } catch {\n throw new Error(\n \"Not connected to the relay. The MCP server hosts it automatically — if this persists, another app may be occupying the port.\"\n );\n }\n }\n }\n\n if (command !== \"join\") {\n await this.ensureRoom();\n // Presence said the room has no plugin — fail fast, not a 30s timeout.\n // But a session that starts while Figma is still dialling would fail on\n // its very first question, so give the plugin a moment to appear before\n // believing the answer. Waiting costs seconds only when it is genuinely\n // absent; failing early costs the user their first command every time.\n if (this.currentRoom && this.presenceByRoom.get(this.currentRoom) === false) {\n await this.waitForPlugin(2500);\n }\n if (this.currentRoom && this.presenceByRoom.get(this.currentRoom) === false) {\n throw new Error(\n \"The Figma plugin is not open. Open the Relai plugin in Figma — it will reconnect to the same room automatically.\"\n );\n }\n }\n\n const result = await new Promise((resolve, reject) => {\n const id = uuidv4();\n const request = {\n id,\n type: command === \"join\" ? \"join\" : \"message\",\n room:\n command === \"join\"\n ? (params as { room: string }).room\n : this.currentRoom,\n ...(command === \"join\"\n ? { role: \"agent\", meta: { client: \"figma-relai-mcp\" } }\n : {}),\n message: {\n id,\n command,\n params: {\n ...(params as Record<string, unknown>),\n commandId: id,\n },\n },\n };\n\n this.tracker.add(id, resolve, reject, timeoutMs);\n\n logger.info(`Sending command: ${command}`);\n logger.debug(`Request: ${JSON.stringify(request)}`);\n this.ws!.send(JSON.stringify(request));\n });\n\n // Surface designer activity alongside object results so the AI sees it\n // without polling; get_events covers the remaining cases\n if (\n this.eventQueue.length > 0 &&\n result !== null &&\n typeof result === \"object\" &&\n !Array.isArray(result)\n ) {\n return { ...(result as Record<string, unknown>), designer_events: this.consumeEvents() };\n }\n return result;\n }\n\n // Drain buffered designer-activity events\n consumeEvents(): unknown[] {\n const drained = this.eventQueue;\n this.eventQueue = [];\n return drained;\n }\n\n // Set handler for incoming commands from other room members (e.g. plugin UI)\n setCommandHandler(handler: (command: string, params: unknown) => void): void {\n this.commandHandler = handler;\n }\n\n get isConnected(): boolean {\n return this.ws !== null && this.ws.readyState === WebSocket.OPEN;\n }\n\n get room(): string | null {\n return this.currentRoom;\n }\n}\n","// Logger that writes to stderr to keep stdout clean for MCP protocol\n\nexport const logger = {\n info: (message: string) => process.stderr.write(`[INFO] ${message}\\n`),\n debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\\n`),\n warn: (message: string) => process.stderr.write(`[WARN] ${message}\\n`),\n error: (message: string) => process.stderr.write(`[ERROR] ${message}\\n`),\n};\n","import { logger } from \"./logger.js\";\n\n// Pending request entry with resolve/reject callbacks and timeout tracking\nexport interface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n lastActivity: number;\n}\n\n// Manages pending command requests with timeout and progress update support\nexport class RequestTracker {\n private pending = new Map<string, PendingRequest>();\n\n // Register a new pending request with timeout\n add(\n id: string,\n resolve: (value: unknown) => void,\n reject: (reason: unknown) => void,\n timeoutMs: number = 30000\n ): void {\n const timeout = setTimeout(() => {\n if (this.pending.has(id)) {\n this.pending.delete(id);\n logger.error(`Request ${id} timed out after ${timeoutMs / 1000}s`);\n reject(new Error(\"Request to Figma timed out\"));\n }\n }, timeoutMs);\n\n this.pending.set(id, {\n resolve,\n reject,\n timeout,\n lastActivity: Date.now(),\n });\n }\n\n // Resolve a request with result\n resolve(id: string, result: unknown): boolean {\n const request = this.pending.get(id);\n if (!request) return false;\n\n clearTimeout(request.timeout);\n request.resolve(result);\n this.pending.delete(id);\n return true;\n }\n\n // Reject a request with error\n reject(id: string, error: Error): boolean {\n const request = this.pending.get(id);\n if (!request) return false;\n\n clearTimeout(request.timeout);\n request.reject(error);\n this.pending.delete(id);\n return true;\n }\n\n // Reset timeout on progress update to prevent timeout during long operations\n resetTimeout(id: string, extendedTimeoutMs: number = 60000): void {\n const request = this.pending.get(id);\n if (!request) return;\n\n request.lastActivity = Date.now();\n clearTimeout(request.timeout);\n request.timeout = setTimeout(() => {\n if (this.pending.has(id)) {\n this.pending.delete(id);\n logger.error(`Request ${id} timed out after extended inactivity`);\n request.reject(new Error(\"Request to Figma timed out\"));\n }\n }, extendedTimeoutMs);\n }\n\n // Check if a request is pending\n has(id: string): boolean {\n return this.pending.has(id);\n }\n\n // Reject all pending requests (e.g., on connection close)\n rejectAll(reason: string): void {\n for (const [id, request] of this.pending.entries()) {\n clearTimeout(request.timeout);\n request.reject(new Error(reason));\n }\n this.pending.clear();\n }\n\n get size(): number {\n return this.pending.size;\n }\n}\n","import { createServer } from \"node:http\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport { RelayCore } from \"@figma-relai/shared\";\nimport { logger } from \"./logger.js\";\nimport { tokenStatus } from \"./credentials.js\";\n\nexport interface EmbeddedRelay {\n close(): void;\n}\n\n// Host the relay in-process on 127.0.0.1:port. Returns null if the port is\n// already taken — another MCP server instance (e.g. Cursor and Claude Code\n// open at once) is hosting, and this process should just connect as a client.\nexport function startEmbeddedRelay(\n port: number,\n version: string\n): Promise<EmbeddedRelay | null> {\n return new Promise((resolve) => {\n const httpServer = createServer();\n\n httpServer.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n resolve(null);\n } else {\n logger.warn(`Embedded relay failed to start: ${err.message}`);\n resolve(null);\n }\n });\n\n httpServer.listen(port, \"127.0.0.1\", () => {\n const core = new RelayCore<WebSocket>({\n version,\n log: (msg) => logger.debug(`[relay] ${msg}`),\n // Status, not presence — the token itself never leaves this process.\n features: () => ({ token: tokenStatus() }),\n });\n const wss = new WebSocketServer({ server: httpServer });\n\n wss.on(\"connection\", (ws) => {\n core.handleOpen(ws);\n ws.on(\"message\", (data) => core.handleMessage(ws, data.toString()));\n ws.on(\"close\", () => core.handleClose(ws));\n ws.on(\"error\", () => {});\n });\n\n core.startStaleCleanup();\n logger.info(`Hosting embedded relay on 127.0.0.1:${port}`);\n\n resolve({\n close() {\n core.stop();\n wss.close();\n httpServer.close();\n },\n });\n });\n });\n}\n","// Ring buffer of every command this server sent to the plugin — the AI's own\n// audit trail, surfaced through get_events (scope: \"agent\"). Complements the\n// plugin's activity feed: the designer sees it live, the AI can review it.\n\nexport interface SessionLogEntry {\n ts: number;\n command: string;\n nodeId?: string;\n ok: boolean;\n ms: number;\n error?: string;\n}\n\nconst MAX_ENTRIES = 200;\nlet entries: SessionLogEntry[] = [];\n\nexport function recordCommand(entry: SessionLogEntry): void {\n entries.push(entry);\n if (entries.length > MAX_ENTRIES) entries = entries.slice(-MAX_ENTRIES);\n}\n\nexport function getSessionLog(): SessionLogEntry[] {\n return [...entries];\n}\n\nexport function clearSessionLog(): void {\n entries = [];\n}\n"],"mappings":"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","names":["serverUrl","port","createServer","port","createServer"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/connection.ts","../src/logger.ts","../src/request-tracker.ts","../src/embedded-relay.ts","../src/session-log.ts"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\nimport { FigmaConnection } from \"./connection.js\";\nimport { logger } from \"./logger.js\";\nimport {\n registerAllTools,\n registerRoomTool,\n registerEventsTool,\n listToolCatalog,\n} from \"./tools/index.js\";\nimport { startEmbeddedRelay, type EmbeddedRelay } from \"./embedded-relay.js\";\nimport { refreshTokenStatus } from \"./credentials.js\";\nimport { resolveFileIdentity } from \"./tools/core/file-identity.js\";\nimport { loadState, saveState } from \"./state.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { recordCommand, getSessionLog } from \"./session-log.js\";\nimport { VERSION } from \"./version.js\";\n\n// Parse CLI arguments\nconst args = process.argv.slice(2);\nconst serverArg = args.find((arg) => arg.startsWith(\"--server=\"));\nconst serverUrl = serverArg ? serverArg.split(\"=\")[1] : \"localhost\";\n\nconst portArg = args.find((arg) => arg.startsWith(\"--port=\"));\nconst port = portArg ? parseInt(portArg.split(\"=\")[1]) : 9055;\n\nconst roomArg = args.find((arg) => arg.startsWith(\"--room=\"));\n\n// Build-time inventory for the plugin UI tool list; exits without stdio setup\nif (args.includes(\"--list-tools\")) {\n console.log(JSON.stringify(listToolCatalog(), null, 2));\n process.exit(0);\n}\n\n// CLI subcommands (Astryx-style: the contract is executable, not prose).\n// `manifest` prints the machine-readable contract, `docs` renders it for\n// humans, `doctor` triages the local environment. All exit before stdio.\nconst subcommand = args.find((a) => !a.startsWith(\"--\"));\nif (subcommand === \"login\" || subcommand === \"logout\") {\n const { runLogin, runLogout } = await import(\"./cli/login.js\");\n const { exitCode, message } = subcommand === \"login\" ? await runLogin() : runLogout();\n console.log(message);\n process.exit(exitCode);\n}\nif (subcommand === \"manifest\" || subcommand === \"docs\" || subcommand === \"doctor\") {\n const { buildManifest } = await import(\"./cli/manifest.js\");\n if (subcommand === \"doctor\") {\n const { runDoctor, renderDoctor } = await import(\"./cli/doctor.js\");\n const results = await runDoctor();\n console.log(args.includes(\"--json\") ? JSON.stringify(results, null, 2) : renderDoctor(results));\n process.exit(results.some((r) => r.status === \"warn\") ? 1 : 0);\n }\n const manifest = await buildManifest(VERSION);\n if (subcommand === \"manifest\") {\n console.log(JSON.stringify(manifest, null, 2));\n } else {\n const { renderToolDoc, renderToolIndex } = await import(\"./cli/docs.js\");\n const toolName = args[args.indexOf(\"docs\") + 1];\n console.log(\n toolName && !toolName.startsWith(\"--\")\n ? renderToolDoc(manifest, toolName)\n : renderToolIndex(manifest)\n );\n }\n process.exit(0);\n}\n// A word we don't know is a mistake, not a request to start a server. Falling\n// through meant `figma-relai lgoin` — or any subcommand newer than the copy npx\n// happened to fetch — booted stdio and sat there in total silence.\nif (subcommand) {\n process.stderr.write(\n `Unknown command \"${subcommand}\" (figma-relai ${VERSION}).\\n` +\n \" login · logout · doctor · docs [tool] · manifest\\n\" +\n \" no command at all starts the MCP server over stdio\\n\\n\" +\n \"If your AI client shows this command, npx may be running an older\\n\" +\n \"published version — try: npx -y figma-relai@latest \" +\n `${subcommand}\\n`\n );\n process.exit(2);\n}\n\nasync function main() {\n // Create MCP server\n const server = createServer();\n\n // Ask Figma whether the stored token is still alive before anyone lights a\n // lamp for it. Fire-and-forget: nothing here should wait on the network.\n void refreshTokenStatus();\n\n // Host the relay in this process unless another instance already does\n // (bind-or-connect: first MCP server binds 9055, later ones connect to it).\n // Only for local relays — a remote --server means someone runs it there.\n let relay: EmbeddedRelay | null = null;\n if (serverUrl === \"localhost\") {\n relay = await startEmbeddedRelay(port, VERSION);\n if (!relay) {\n logger.info(`Port ${port} in use — connecting to the existing relay`);\n }\n }\n\n const initialRoom =\n roomArg?.split(\"=\")[1] ?? process.env.FIGMA_RELAI_ROOM ?? loadState().room ?? null;\n\n // Create WebSocket connection to relay\n const connection = new FigmaConnection(serverUrl, port, {\n initialRoom,\n onRoomChanged: (room) => saveState({ room }),\n beforeReconnect: async () => {\n // The hosting process may have exited; take over if the port is free\n if (serverUrl === \"localhost\" && !relay) {\n relay = await startEmbeddedRelay(port, VERSION);\n if (relay) logger.info(\"Took over relay hosting\");\n }\n },\n });\n\n // Register the join_room tool\n registerRoomTool(\n server,\n (room) => connection.joinRoom(room),\n () => connection.listRooms()\n );\n\n // Register designer-activity polling + the AI's own audit trail\n registerEventsTool(server, () => connection.consumeEvents(), getSessionLog);\n\n // Expose skill documents as MCP prompts (inlined at build time)\n registerPrompts(server);\n\n // Which file is this? Nothing used to ask until manage_comments did, so the\n // panel's lineage stayed blank in every session that never touched comments\n // — and blank forever on a machine that never touches them at all. The first\n // command that reaches the plugin is the earliest honest moment to find out.\n // A failure is not final: the designer may reload the plugin, open another\n // file, or store a token mid-session, and a latch set on the first attempt\n // would keep the panel blank for the rest of the process. Success latches;\n // failure gets a few more chances and then stops asking.\n let identityProbes = 0;\n let identityResolved = false;\n const probeIdentityOnce = () => {\n if (identityResolved || identityProbes >= 3) return;\n identityProbes += 1;\n void resolveFileIdentity((c, p2, t) => connection.sendCommand(c, p2, t))\n .then(() => {\n identityResolved = true;\n })\n .catch(() => {\n // No token, nothing published, no branch — all ordinary. The panel keeps\n // showing what it showed before and the caller is never told.\n });\n };\n\n // Register all domain tools; every plugin command lands in the session log\n registerAllTools(server, async (command, params, timeoutMs) => {\n const t0 = Date.now();\n const nodeId =\n typeof (params as { nodeId?: unknown })?.nodeId === \"string\"\n ? ((params as { nodeId: string }).nodeId)\n : undefined;\n try {\n const result = await connection.sendCommand(command, params, timeoutMs);\n recordCommand({ ts: t0, command, nodeId, ok: true, ms: Date.now() - t0 });\n // Not for the identity commands themselves, or the probe calls itself\n if (command !== \"get_file_identity\" && command !== \"set_file_identity\") {\n probeIdentityOnce();\n }\n return result;\n } catch (error) {\n recordCommand({\n ts: t0,\n command,\n nodeId,\n ok: false,\n ms: Date.now() - t0,\n error: error instanceof Error ? error.message.slice(0, 200) : String(error),\n });\n throw error;\n }\n });\n\n // Connect to relay (auto-reconnects on failure)\n try {\n await connection.connect();\n logger.info(\"Connected to relay successfully\");\n } catch (error) {\n logger.warn(\n `Could not connect initially: ${error instanceof Error ? error.message : String(error)}`\n );\n logger.warn(\"Will attempt to connect when the first command is sent\");\n }\n\n // Start MCP server with stdio transport\n const transport = new StdioServerTransport();\n await server.connect(transport);\n logger.info(\"Relai MCP server running on stdio\");\n}\n\nmain().catch((error) => {\n logger.error(\n `Fatal error: ${error instanceof Error ? error.message : String(error)}`\n );\n process.exit(1);\n});\n","import WebSocket from \"ws\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { logger } from \"./logger.js\";\nimport { RequestTracker } from \"./request-tracker.js\";\nimport type { FigmaCommand } from \"@figma-relai/shared\";\nimport type { CommandProgressUpdate, RoomSummary } from \"@figma-relai/shared\";\n\nexport interface FigmaConnectionOptions {\n // Room restored from persisted state; used to auto-pair on the first command\n initialRoom?: string | null;\n // Called whenever a room is joined so the caller can persist it\n onRoomChanged?: (room: string) => void;\n // Called before each reconnect attempt (e.g. to take over relay hosting)\n beforeReconnect?: () => Promise<void>;\n}\n\n// Error payload sent by the plugin: a plain string, or an object with context\ninterface PluginErrorPayload {\n message?: string;\n command?: string;\n nodeId?: string;\n nodeType?: string;\n}\n\n// Flatten a plugin error (string or structured object) into one readable line\nexport function formatFigmaError(e: unknown): string {\n if (typeof e === \"string\") return e;\n const { message, command, nodeId, nodeType } = (e ?? {}) as PluginErrorPayload;\n const prefix = command ? `[${command}] ` : \"\";\n const suffix = nodeId\n ? ` (node ${nodeId}${nodeType ? `, type ${nodeType}` : \"\"})`\n : \"\";\n return `${prefix}${message ?? JSON.stringify(e)}${suffix}`;\n}\n\n// Route a response message to its pending request. Error responses have no\n// `result` field, so the error check must come first and resolution must key\n// on field presence — falsy results (0, \"\", false) are valid.\nexport function routeResponse(\n response: { id?: string; result?: unknown; error?: unknown } | undefined,\n tracker: RequestTracker\n): void {\n if (!response?.id || !tracker.has(response.id)) return;\n if (response.error !== undefined) {\n tracker.reject(response.id, new Error(formatFigmaError(response.error)));\n } else if (\"result\" in response) {\n tracker.resolve(response.id, response.result);\n }\n}\n\n// Manages WebSocket connection to the relay server\n/**\n * Given the room that went quiet and who the relay says is live, decide where\n * to re-pair. Pure so the decision can be tested without a relay.\n */\nexport function chooseRepairRoom(\n staleRoom: string | null,\n rooms: RoomSummary[]\n): { room: string; error?: undefined } | { room?: undefined; error: string } {\n const withPlugin = rooms.filter((r) => r.hasPlugin && r.room !== staleRoom);\n if (withPlugin.length === 1) return { room: withPlugin[0].room };\n if (withPlugin.length === 0) {\n return {\n error:\n \"The Figma plugin is not open. Open the Relai plugin in Figma — it will reconnect automatically.\",\n };\n }\n return {\n error: `The plugin left room ${staleRoom} and several are now connected: ${withPlugin\n .map((r) => (r.fileName ? `\"${r.fileName}\" (room ${r.room})` : `room ${r.room}`))\n .join(\", \")}. Call join_room with the one you want.`,\n };\n}\n\nexport class FigmaConnection {\n private ws: WebSocket | null = null;\n private currentRoom: string | null = null;\n private tracker = new RequestTracker();\n private wsUrl: string;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private shouldReconnect = false;\n private connectPromise: Promise<void> | null = null;\n private commandHandler: ((command: string, params: unknown) => void) | null = null;\n private options: FigmaConnectionOptions;\n private relayVerified = false;\n // Presence recorded per room: join-time broadcasts can arrive before\n // currentRoom is updated, so keying by room avoids the ordering race\n private presenceByRoom = new Map<string, boolean>();\n // Did the caller NAME this room, or did we pick it for them? A room the\n // caller aimed at is never traded for a different one — they may be waiting\n // for that file to open, and silently following the only live plugin would\n // send their writes into somebody else's file.\n private roomWasChosenByCaller = false;\n // Designer activity piggybacked on plugin responses (selection changes etc.)\n private eventQueue: unknown[] = [];\n\n constructor(\n serverUrl: string = \"localhost\",\n port: number = 9055,\n options: FigmaConnectionOptions = {}\n ) {\n const protocol = serverUrl === \"localhost\" ? \"ws\" : \"wss\";\n this.wsUrl =\n serverUrl === \"localhost\"\n ? `${protocol}://${serverUrl}:${port}`\n : `${protocol}://${serverUrl}`;\n this.options = options;\n }\n\n // Connect to WebSocket relay server\n connect(): Promise<void> {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n logger.info(\"Already connected to relay\");\n return Promise.resolve();\n }\n\n // If already connecting, return existing promise\n if (this.connectPromise) {\n return this.connectPromise;\n }\n\n logger.info(`Connecting to relay at ${this.wsUrl}...`);\n\n this.connectPromise = new Promise<void>((resolve, reject) => {\n const ws = new WebSocket(this.wsUrl);\n this.ws = ws;\n\n const cleanup = () => {\n this.connectPromise = null;\n };\n\n ws.on(\"open\", () => {\n logger.info(\"Connected to relay server\");\n this.shouldReconnect = true;\n cleanup();\n resolve();\n\n // Verify we reached a Relai relay (and not some other port 9055 app)\n this.relayVerified = false;\n try {\n ws.send(JSON.stringify({ type: \"hello\" }));\n } catch {\n // Send failure surfaces via close/error handlers\n }\n setTimeout(() => {\n if (!this.relayVerified && this.ws === ws) {\n logger.warn(\n \"Connected endpoint did not identify as a figma-relai relay — is another app using the port?\"\n );\n }\n }, 1500);\n\n // Rejoin the previous room after a relay restart / reconnect so the\n // AI doesn't land in a silent \"must join a room\" dead state\n if (this.currentRoom) {\n const room = this.currentRoom;\n this.sendCommand(\"join\", { room }).then(\n () => logger.info(`Rejoined room: ${room}`),\n (err) =>\n logger.warn(\n `Failed to rejoin room ${room}: ${err instanceof Error ? err.message : String(err)}`\n )\n );\n }\n });\n\n ws.on(\"message\", (data: WebSocket.Data) => {\n try {\n const json = JSON.parse(data.toString());\n\n // Relay identity probe response\n if (json.type === \"hello\" && json.server === \"figma-relai\") {\n this.relayVerified = true;\n logger.debug(`Relay verified (version ${json.version})`);\n return;\n }\n\n // Peer presence per room (is the plugin there?)\n if (json.type === \"presence\") {\n if (typeof json.room === \"string\" && Array.isArray(json.peers)) {\n this.presenceByRoom.set(\n json.room,\n json.peers.some((p: { role?: string }) => p.role === \"plugin\")\n );\n }\n return;\n }\n\n // Room listing response (auto-pairing)\n if (json.type === \"list_rooms_result\" && json.id) {\n this.tracker.resolve(json.id, json.rooms ?? []);\n return;\n }\n\n // Handle incoming commands (from plugin UI broadcasts)\n if (json.type === \"broadcast\" && json.message?.command) {\n this.commandHandler?.(json.message.command, json.message.params);\n return;\n }\n\n // Handle progress updates\n if (json.type === \"progress_update\") {\n const progressData = json.message?.data as CommandProgressUpdate;\n const requestId = json.id || \"\";\n\n if (requestId && this.tracker.has(requestId)) {\n this.tracker.resetTimeout(requestId);\n logger.info(\n `Progress: ${progressData.commandType} ${progressData.progress}% - ${progressData.message}`\n );\n }\n return;\n }\n\n // Collect designer-activity events piggybacked on responses\n if (Array.isArray(json.message?.events)) {\n this.eventQueue.push(...json.message.events);\n if (this.eventQueue.length > 50) {\n this.eventQueue = this.eventQueue.slice(-50);\n }\n }\n\n // Handle regular responses\n routeResponse(json.message, this.tracker);\n } catch (error) {\n logger.error(\n `Error parsing message: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n });\n\n ws.on(\"error\", (error) => {\n logger.error(`WebSocket error: ${error.message}`);\n cleanup();\n reject(error);\n });\n\n ws.on(\"close\", () => {\n logger.info(\"Disconnected from relay\");\n this.ws = null;\n // A command that was already on the wire may have run to completion in\n // Figma before the socket dropped — so say so instead of implying it\n // failed. Retrying a write on the caller's behalf could apply it twice.\n this.tracker.rejectAll(\n \"The link to Figma dropped while this command was in flight — it may already have applied. \" +\n \"Re-read the file before retrying (the server reconnects on its own within a few seconds).\"\n );\n cleanup();\n\n // Only auto-reconnect if not manually disconnected. beforeReconnect\n // lets the host process try to re-bind the relay port first (host\n // takeover when the previous hosting MCP server exited).\n if (this.shouldReconnect) {\n this.reconnectTimer = setTimeout(async () => {\n try {\n await this.options.beforeReconnect?.();\n } catch {\n // Takeover failure is fine; we may connect to another host\n }\n this.connect().catch(() => {});\n }, 2000);\n }\n });\n });\n\n return this.connectPromise;\n }\n\n // Disconnect and stop reconnection\n disconnect(): void {\n this.shouldReconnect = false;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n }\n\n // Join a room for communication\n async joinRoom(roomName: string, chosenByCaller = true): Promise<void> {\n this.roomWasChosenByCaller = chosenByCaller;\n await this.sendCommand(\"join\", { room: roomName });\n this.currentRoom = roomName;\n this.options.onRoomChanged?.(roomName);\n logger.info(`Joined room: ${roomName}`);\n }\n\n // Ask the relay which rooms exist and who is in them\n listRooms(): Promise<RoomSummary[]> {\n return new Promise((resolve, reject) => {\n const id = uuidv4();\n this.tracker.add(id, resolve as (value: unknown) => void, reject, 5000);\n this.ws!.send(JSON.stringify({ type: \"list_rooms\", id }));\n });\n }\n\n // Auto-pair with the plugin so join_room is only needed for disambiguation:\n // prefer the persisted room if its plugin is present, otherwise join the\n // single plugin room, otherwise explain exactly what to do.\n private async ensureRoom(): Promise<void> {\n if (this.currentRoom) return;\n\n const rooms = await this.listRooms();\n const withPlugin = rooms.filter((r) => r.hasPlugin);\n const saved = this.options.initialRoom;\n\n if (saved && withPlugin.some((r) => r.room === saved)) {\n return this.joinRoom(saved, false);\n }\n if (withPlugin.length === 1) {\n return this.joinRoom(withPlugin[0].room, false);\n }\n if (withPlugin.length === 0) {\n throw new Error(\n \"No Figma plugin is connected. Open the Relai plugin in Figma (it connects automatically), then try again.\"\n );\n }\n throw new Error(\n `Multiple Figma plugins are connected: ${withPlugin\n .map((r) => (r.fileName ? `\"${r.fileName}\" (room ${r.room})` : `room ${r.room}`))\n .join(\", \")}. Call join_room with the room of the file you want to control.`\n );\n }\n\n /**\n * Poll the presence map briefly, then ASK.\n *\n * Presence is only ever written by a pushed `presence` event, so a plugin\n * that closes and reopens can leave the cache stuck on false: the close set\n * it, and the reopen's event never landed. Polling a cache that nothing is\n * refreshing just burns the timeout and then reports \"the plugin is not\n * open\" about a plugin sitting right there — while list_rooms, which asks\n * the relay instead of trusting the cache, happily lists it.\n */\n private async waitForPlugin(ms: number): Promise<void> {\n const deadline = Date.now() + ms;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 150));\n if (!this.currentRoom || this.presenceByRoom.get(this.currentRoom) !== false) return;\n }\n if (!this.currentRoom) return;\n try {\n const rooms = await this.listRooms();\n const live = rooms.find((r) => r.room === this.currentRoom);\n if (live?.hasPlugin) this.presenceByRoom.set(this.currentRoom, true);\n } catch {\n // Unreachable relay is the caller's problem, not this cache's\n }\n }\n\n /**\n * The paired room went quiet. Ask the relay who is actually there and, if\n * exactly one plugin is connected, move to it.\n *\n * A plugin that is closed and reopened — or a file switched to one of its\n * branches — comes back under a NEW room name. ensureRoom() only picks a\n * room when there is none, so without this the session stays pinned to the\n * dead room and every command reports \"the plugin is not open\" while\n * join_room cheerfully lists the live one.\n */\n private async repairPairing(): Promise<void> {\n const stale = this.currentRoom;\n let rooms: RoomSummary[] = [];\n try {\n rooms = await this.listRooms();\n } catch {\n throw new Error(\n \"The Figma plugin is not open, and the relay could not be asked which rooms are live. Open the Relai plugin in Figma and try again.\"\n );\n }\n const choice = chooseRepairRoom(stale, rooms);\n if (choice.room) {\n await this.joinRoom(choice.room, false);\n logger.info(`Re-paired: ${stale} → ${choice.room} (plugin moved rooms)`);\n return;\n }\n throw new Error(choice.error);\n }\n\n // Send a command to Figma plugin via the relay\n async sendCommand(\n command: FigmaCommand,\n params: unknown = {},\n timeoutMs: number = 30000\n ): Promise<unknown> {\n // Wait for connection if not connected\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n try {\n await this.connect();\n } catch {\n // The relay host may have just exited (port-handoff race on rapid\n // server restarts): try to take over hosting, then dial once more.\n try {\n await this.options.beforeReconnect?.();\n } catch {\n // Takeover failure is fine; the retry below may reach another host\n }\n try {\n await this.connect();\n } catch {\n throw new Error(\n \"Not connected to the relay. The MCP server hosts it automatically — if this persists, another app may be occupying the port.\"\n );\n }\n }\n }\n\n if (command !== \"join\") {\n await this.ensureRoom();\n // Presence said the room has no plugin — fail fast, not a 30s timeout.\n // But a session that starts while Figma is still dialling would fail on\n // its very first question, so give the plugin a moment to appear before\n // believing the answer. Waiting costs seconds only when it is genuinely\n // absent; failing early costs the user their first command every time.\n if (this.currentRoom && this.presenceByRoom.get(this.currentRoom) === false) {\n await this.waitForPlugin(2500);\n }\n if (this.currentRoom && this.presenceByRoom.get(this.currentRoom) === false) {\n // The plugin may not be gone — it may have MOVED. Reopening it, or\n // switching to a branch (a different file), gives it a fresh room,\n // and ensureRoom() never looks again once currentRoom is set. Re-pair\n // rather than reporting \"not open\" about a plugin sitting right there.\n //\n // Only for a room WE picked. When the caller named it they are aiming\n // at one file, and quietly moving them to whichever plugin happens to\n // be live would land their writes in the wrong document.\n if (!this.roomWasChosenByCaller) {\n await this.repairPairing();\n } else {\n throw new Error(\n `No Figma plugin is in room ${this.currentRoom}. Open the file you want in Figma and run the plugin, or call join_room without a room to see which are live.`\n );\n }\n }\n }\n\n const result = await new Promise((resolve, reject) => {\n const id = uuidv4();\n const request = {\n id,\n type: command === \"join\" ? \"join\" : \"message\",\n room:\n command === \"join\"\n ? (params as { room: string }).room\n : this.currentRoom,\n ...(command === \"join\"\n ? { role: \"agent\", meta: { client: \"figma-relai-mcp\" } }\n : {}),\n message: {\n id,\n command,\n params: {\n ...(params as Record<string, unknown>),\n commandId: id,\n },\n },\n };\n\n this.tracker.add(id, resolve, reject, timeoutMs);\n\n logger.info(`Sending command: ${command}`);\n logger.debug(`Request: ${JSON.stringify(request)}`);\n this.ws!.send(JSON.stringify(request));\n });\n\n // Surface designer activity alongside object results so the AI sees it\n // without polling; get_events covers the remaining cases\n if (\n this.eventQueue.length > 0 &&\n result !== null &&\n typeof result === \"object\" &&\n !Array.isArray(result)\n ) {\n return { ...(result as Record<string, unknown>), designer_events: this.consumeEvents() };\n }\n return result;\n }\n\n // Drain buffered designer-activity events\n consumeEvents(): unknown[] {\n const drained = this.eventQueue;\n this.eventQueue = [];\n return drained;\n }\n\n // Set handler for incoming commands from other room members (e.g. plugin UI)\n setCommandHandler(handler: (command: string, params: unknown) => void): void {\n this.commandHandler = handler;\n }\n\n get isConnected(): boolean {\n return this.ws !== null && this.ws.readyState === WebSocket.OPEN;\n }\n\n get room(): string | null {\n return this.currentRoom;\n }\n}\n","// Logger that writes to stderr to keep stdout clean for MCP protocol\n\nexport const logger = {\n info: (message: string) => process.stderr.write(`[INFO] ${message}\\n`),\n debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\\n`),\n warn: (message: string) => process.stderr.write(`[WARN] ${message}\\n`),\n error: (message: string) => process.stderr.write(`[ERROR] ${message}\\n`),\n};\n","import { logger } from \"./logger.js\";\n\n// Pending request entry with resolve/reject callbacks and timeout tracking\nexport interface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n lastActivity: number;\n}\n\n// Manages pending command requests with timeout and progress update support\nexport class RequestTracker {\n private pending = new Map<string, PendingRequest>();\n\n // Register a new pending request with timeout\n add(\n id: string,\n resolve: (value: unknown) => void,\n reject: (reason: unknown) => void,\n timeoutMs: number = 30000\n ): void {\n const timeout = setTimeout(() => {\n if (this.pending.has(id)) {\n this.pending.delete(id);\n logger.error(`Request ${id} timed out after ${timeoutMs / 1000}s`);\n reject(new Error(\"Request to Figma timed out\"));\n }\n }, timeoutMs);\n\n this.pending.set(id, {\n resolve,\n reject,\n timeout,\n lastActivity: Date.now(),\n });\n }\n\n // Resolve a request with result\n resolve(id: string, result: unknown): boolean {\n const request = this.pending.get(id);\n if (!request) return false;\n\n clearTimeout(request.timeout);\n request.resolve(result);\n this.pending.delete(id);\n return true;\n }\n\n // Reject a request with error\n reject(id: string, error: Error): boolean {\n const request = this.pending.get(id);\n if (!request) return false;\n\n clearTimeout(request.timeout);\n request.reject(error);\n this.pending.delete(id);\n return true;\n }\n\n // Reset timeout on progress update to prevent timeout during long operations\n resetTimeout(id: string, extendedTimeoutMs: number = 60000): void {\n const request = this.pending.get(id);\n if (!request) return;\n\n request.lastActivity = Date.now();\n clearTimeout(request.timeout);\n request.timeout = setTimeout(() => {\n if (this.pending.has(id)) {\n this.pending.delete(id);\n logger.error(`Request ${id} timed out after extended inactivity`);\n request.reject(new Error(\"Request to Figma timed out\"));\n }\n }, extendedTimeoutMs);\n }\n\n // Check if a request is pending\n has(id: string): boolean {\n return this.pending.has(id);\n }\n\n // Reject all pending requests (e.g., on connection close)\n rejectAll(reason: string): void {\n for (const [id, request] of this.pending.entries()) {\n clearTimeout(request.timeout);\n request.reject(new Error(reason));\n }\n this.pending.clear();\n }\n\n get size(): number {\n return this.pending.size;\n }\n}\n","import { createServer } from \"node:http\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport { RelayCore } from \"@figma-relai/shared\";\nimport { logger } from \"./logger.js\";\nimport { tokenStatus } from \"./credentials.js\";\n\nexport interface EmbeddedRelay {\n close(): void;\n}\n\n// Host the relay in-process on 127.0.0.1:port. Returns null if the port is\n// already taken — another MCP server instance (e.g. Cursor and Claude Code\n// open at once) is hosting, and this process should just connect as a client.\nexport function startEmbeddedRelay(\n port: number,\n version: string\n): Promise<EmbeddedRelay | null> {\n return new Promise((resolve) => {\n const httpServer = createServer();\n\n httpServer.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\") {\n resolve(null);\n } else {\n logger.warn(`Embedded relay failed to start: ${err.message}`);\n resolve(null);\n }\n });\n\n httpServer.listen(port, \"127.0.0.1\", () => {\n const core = new RelayCore<WebSocket>({\n version,\n log: (msg) => logger.debug(`[relay] ${msg}`),\n // Status, not presence — the token itself never leaves this process.\n features: () => ({ token: tokenStatus() }),\n });\n const wss = new WebSocketServer({ server: httpServer });\n\n wss.on(\"connection\", (ws) => {\n core.handleOpen(ws);\n ws.on(\"message\", (data) => core.handleMessage(ws, data.toString()));\n ws.on(\"close\", () => core.handleClose(ws));\n ws.on(\"error\", () => {});\n });\n\n core.startStaleCleanup();\n logger.info(`Hosting embedded relay on 127.0.0.1:${port}`);\n\n resolve({\n close() {\n core.stop();\n wss.close();\n httpServer.close();\n },\n });\n });\n });\n}\n","// Ring buffer of every command this server sent to the plugin — the AI's own\n// audit trail, surfaced through get_events (scope: \"agent\"). Complements the\n// plugin's activity feed: the designer sees it live, the AI can review it.\n\nexport interface SessionLogEntry {\n ts: number;\n command: string;\n nodeId?: string;\n ok: boolean;\n ms: number;\n error?: string;\n}\n\nconst MAX_ENTRIES = 200;\nlet entries: SessionLogEntry[] = [];\n\nexport function recordCommand(entry: SessionLogEntry): void {\n entries.push(entry);\n if (entries.length > MAX_ENTRIES) entries = entries.slice(-MAX_ENTRIES);\n}\n\nexport function getSessionLog(): SessionLogEntry[] {\n return [...entries];\n}\n\nexport function clearSessionLog(): void {\n entries = [];\n}\n"],"mappings":"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","names":["serverUrl","port","createServer","port","createServer"]}
@@ -8,7 +8,7 @@ import {
8
8
  registerEventsTool,
9
9
  registerPrompts,
10
10
  registerRoomTool
11
- } from "./chunk-BRUWHN2A.js";
11
+ } from "./chunk-PQGMCFD7.js";
12
12
  import "./chunk-SDINVROO.js";
13
13
  import "./chunk-VS2FNFYC.js";
14
14
  import "./chunk-2H7UOFLK.js";
@@ -59,4 +59,4 @@ async function buildManifest(version) {
59
59
  export {
60
60
  buildManifest
61
61
  };
62
- //# sourceMappingURL=manifest-UX6MDMVV.js.map
62
+ //# sourceMappingURL=manifest-ASJDLEEP.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-relai",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {