figma-relai 0.2.3 → 0.2.4
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/{chunk-VQYNT7L4.js → chunk-2T3Z7E6C.js} +8 -3
- package/dist/chunk-2T3Z7E6C.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{manifest-77WTPHLS.js → manifest-APMQYUMR.js} +2 -2
- package/package.json +2 -3
- package/LICENSE +0 -21
- package/README.md +0 -138
- package/dist/chunk-VQYNT7L4.js.map +0 -1
- /package/dist/{manifest-77WTPHLS.js.map → manifest-APMQYUMR.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
registerEventsTool,
|
|
8
8
|
registerPrompts,
|
|
9
9
|
registerRoomTool
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2T3Z7E6C.js";
|
|
11
11
|
import {
|
|
12
12
|
loadState,
|
|
13
13
|
saveState
|
|
@@ -427,7 +427,7 @@ function getSessionLog() {
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
// src/index.ts
|
|
430
|
-
var VERSION = "0.2.
|
|
430
|
+
var VERSION = "0.2.4";
|
|
431
431
|
var args = process.argv.slice(2);
|
|
432
432
|
var serverArg = args.find((arg) => arg.startsWith("--server="));
|
|
433
433
|
var serverUrl = serverArg ? serverArg.split("=")[1] : "localhost";
|
|
@@ -440,7 +440,7 @@ if (args.includes("--list-tools")) {
|
|
|
440
440
|
}
|
|
441
441
|
var subcommand = args.find((a) => !a.startsWith("--"));
|
|
442
442
|
if (subcommand === "manifest" || subcommand === "docs" || subcommand === "doctor") {
|
|
443
|
-
const { buildManifest } = await import("./manifest-
|
|
443
|
+
const { buildManifest } = await import("./manifest-APMQYUMR.js");
|
|
444
444
|
if (subcommand === "doctor") {
|
|
445
445
|
const { runDoctor, renderDoctor } = await import("./doctor-ZN2PKAPH.js");
|
|
446
446
|
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 { loadState, saveState } from \"./state.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { recordCommand, getSessionLog } from \"./session-log.js\";\n\nconst VERSION = \"0.2.3\";\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 === \"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\nasync function main() {\n // Create MCP server\n const server = createServer();\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(server, (room) => connection.joinRoom(room));\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 // 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 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 this.tracker.rejectAll(\"Connection closed\");\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 // 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 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\";\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 });\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 { loadState, saveState } from \"./state.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { recordCommand, getSessionLog } from \"./session-log.js\";\n\nconst VERSION = \"0.2.4\";\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 === \"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\nasync function main() {\n // Create MCP server\n const server = createServer();\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(server, (room) => connection.joinRoom(room));\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 // 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 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 this.tracker.rejectAll(\"Connection closed\");\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 // 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 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\";\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 });\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-
|
|
11
|
+
} from "./chunk-2T3Z7E6C.js";
|
|
12
12
|
import "./chunk-2H7UOFLK.js";
|
|
13
13
|
|
|
14
14
|
// src/cli/manifest.ts
|
|
@@ -52,4 +52,4 @@ async function buildManifest(version) {
|
|
|
52
52
|
export {
|
|
53
53
|
buildManifest
|
|
54
54
|
};
|
|
55
|
-
//# sourceMappingURL=manifest-
|
|
55
|
+
//# sourceMappingURL=manifest-APMQYUMR.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-relai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bun run scripts/inject-pitfalls.ts && tsup && node dist/index.js manifest > ../../docs/manifest.json",
|
|
11
11
|
"dev": "tsup --watch",
|
|
12
|
-
"
|
|
13
|
-
"prepack": "node -e \"const f=require('fs');f.copyFileSync('../../README.md','README.md');f.copyFileSync('../../LICENSE','LICENSE')\"",
|
|
12
|
+
"release": "node -e \"const f=require('fs');f.copyFileSync('../../README.md','README.md');f.copyFileSync('../../LICENSE','LICENSE')\" && npm publish",
|
|
14
13
|
"postpack": "node -e \"const f=require('fs');f.rmSync('README.md',{force:true});f.rmSync('LICENSE',{force:true})\""
|
|
15
14
|
},
|
|
16
15
|
"dependencies": {
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 figma-relai contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
<img src="assets/relai-logo.svg" alt="Relai" height="36" />
|
|
2
|
-
|
|
3
|
-
English | [日本語](README.ja.md) | [中文](README.zh.md)
|
|
4
|
-
|
|
5
|
-
**Your AI, on the canvas.** Relai connects Claude Code, Cursor, Codex — any MCP client — to Figma, so you can read, edit, audit, and build design systems by talking to the model you already use. It works on every Figma plan, because writes go through a Figma plugin rather than the paid REST API.
|
|
6
|
-
|
|
7
|
-
<img src="assets/plugin-ui.png" alt="The Relai plugin: activity feed, connection status, and a Stop button" width="380" />
|
|
8
|
-
|
|
9
|
-
## What a session looks like
|
|
10
|
-
|
|
11
|
-
> **You:** Make the CTA pop and round it off.
|
|
12
|
-
>
|
|
13
|
-
> **AI:** `set_properties · 3 nodes · 0.4s ✓` → `verify_visual · match ✓`
|
|
14
|
-
>
|
|
15
|
-
> **You:** Now sweep the whole screen for dark mode.
|
|
16
|
-
>
|
|
17
|
-
> **AI:** `set_properties · 24 nodes · 1.2s ✓` → `analyze_design · overall → 92/100`
|
|
18
|
-
|
|
19
|
-
Every command shows up in the plugin as it runs, with timing and success or failure. Click an entry to jump to that layer on the canvas. Press **Stop** if you change your mind — the rest of the batch is cancelled.
|
|
20
|
-
|
|
21
|
-
## Get started
|
|
22
|
-
|
|
23
|
-
You need [Figma Desktop](https://www.figma.com/downloads/), [Node.js](https://nodejs.org/) 18+, and an MCP client.
|
|
24
|
-
|
|
25
|
-
**1. Install the plugin.** Get it from [Figma Community](https://www.figma.com/community/plugin/1662131506342078142) and run it. It connects on its own and remembers its room across restarts.
|
|
26
|
-
|
|
27
|
-
**2. Register the server** with your AI client:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
claude mcp add Relai -- npx -y figma-relai # Claude Code
|
|
31
|
-
codex mcp add Relai -- npx -y figma-relai # Codex CLI
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
For Cursor, add this to `.cursor/mcp.json`:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{ "mcpServers": { "Relai": { "command": "npx", "args": ["-y", "figma-relai"] } } }
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**3. Ask for something.** Pairing is automatic; there is nothing to copy between windows. The `join_room` tool exists for one rare case only: two Figma files running the plugin at the same time.
|
|
41
|
-
|
|
42
|
-
## What it's good at
|
|
43
|
-
|
|
44
|
-
Understanding a design. "How is this screen put together?" gets you structure, colors, layout, and token usage in one pass, and the AI can take a screenshot to actually look at the canvas rather than guess.
|
|
45
|
-
|
|
46
|
-
Bulk edits. "Translate every button label to English" or "recolor this for dark mode" become one round-trip across dozens of nodes instead of an afternoon of clicking.
|
|
47
|
-
|
|
48
|
-
Audits. `analyze_design` checks color-token coverage, auto-layout quality, component health, and accessibility (WCAG contrast, touch targets, text sizes) — or all four at once as a weighted 0–100 health score you can put in a review.
|
|
49
|
-
|
|
50
|
-
Design systems. Variable collections with modes, token binding, shared styles, components with proper variants, team-library imports. `get_design_system` inventories what the file — and the libraries it uses — already has, so the AI builds from your components instead of redrawing near-copies; `analyze_design`'s tokens aspect finds hardcoded values that visually match an existing variable, and one `tokenize` call binds them all. These run as declarative operations with precondition checks, so the same request behaves the same way every time, and a failure tells the AI what to do next ("call set_layout_mode first") instead of dumping a stack trace.
|
|
51
|
-
|
|
52
|
-
Everything else. `execute_figma` runs JavaScript against the Figma Plugin API directly — the same escape-hatch approach as Figma's official MCP — with a `relai.*` helper library that makes the correct pattern the shortest one, hints attached to known errors, and a lint that flags silent mistakes. If you'd rather the AI never ran code, turn it off with the plugin's "Allow code execution" toggle.
|
|
53
|
-
|
|
54
|
-
## You stay in control
|
|
55
|
-
|
|
56
|
-
The plugin is the designer's side of the deal: a live activity feed of everything the AI does, an "AI connected" indicator that means an agent is actually paired (not just that a server is running), and a Stop button that cancels pending work. Selection and page changes you make flow back to the AI as events, so "now do the same to this one" works without re-explaining.
|
|
57
|
-
|
|
58
|
-
Three dials go further when you want them. **Approvals** ("Ask before big edits") holds bulk writes and code execution until you press Approve in the panel. **Lock to selection** rejects edits outside whatever you've selected — the AI gets a clear error, not a silent pass. And **file conventions** are a little CLAUDE.md stored inside the Figma file itself: naming rules, spacing habits, do-not-touch pages — every future session, from any AI client, reads it before working. The UI speaks English, 日本語, and 中文.
|
|
59
|
-
|
|
60
|
-
## How it works
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
AI (any MCP client)
|
|
64
|
-
↕ stdio
|
|
65
|
-
MCP server 30 tools · analysis · verification
|
|
66
|
-
(embedded relay) WebSocket room hub on 127.0.0.1:9055
|
|
67
|
-
↕ WebSocket
|
|
68
|
-
Figma plugin executes Plugin API calls
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
The relay lives inside the MCP server, so there is no extra process to keep alive. When several MCP clients run at once, the first one hosts the relay and the others connect to it; if the host exits, a survivor takes over. Both sides remember their room, rejoin after restarts or sleep, and find each other without any copy-pasting.
|
|
72
|
-
|
|
73
|
-
Ports are fixed by Figma's plugin sandbox: the manifest allowlists `ws://localhost:9055–9057`, and other ports cannot work without editing `manifest.json`. That's why there is no port setting in the UI.
|
|
74
|
-
|
|
75
|
-
## The tools
|
|
76
|
-
|
|
77
|
-
| Group | Tools |
|
|
78
|
-
|-------|-------|
|
|
79
|
-
| Context | `get_document_overview` · `get_selection_context` · `get_node_details` · `search_nodes` · `get_design_tokens` · `screenshot` · `get_events` |
|
|
80
|
-
| Analysis | `analyze_design` (color / layout / components / accessibility / overall) · `diff_nodes` (compare, or checkpoint save/compare) |
|
|
81
|
-
| Verification | `verify_changes` · `validate_design_rules` · `verify_visual` |
|
|
82
|
-
| Read | `get_node_data` (summary / tree / full / css / variables) |
|
|
83
|
-
| Create & edit | `create_node` · `set_properties` · `set_text` · `edit_structure` |
|
|
84
|
-
| Components | `manage_components` |
|
|
85
|
-
| Design system | `get_design_system` · `manage_variables` · `manage_styles` · `import_from_library` · `manage_conventions` |
|
|
86
|
-
| Document | `manage_pages` · `navigate` |
|
|
87
|
-
| Assets | `export_asset` · `add_image` |
|
|
88
|
-
| Annotations | `annotate` |
|
|
89
|
-
| Comments | `manage_comments` (needs a token — see below) |
|
|
90
|
-
| Advanced | `batch_execute` · `execute_figma` · `join_room` |
|
|
91
|
-
|
|
92
|
-
Each tool is self-describing, so the AI sees full parameter docs. The same contract also exists as a file: `npx figma-relai manifest` prints a machine-readable JSON of every tool schema, plugin command, and known pitfall — generated from the running code on every build (committed as `docs/manifest.json`), so it cannot drift — and `npx figma-relai docs <tool>` renders it for humans. Nine skill documents ship alongside as MCP prompts: token strategy, component conventions, audit workflows, a Plugin API cheat sheet for `execute_figma`, and recipes for design-system-first building, bulk cleanup, and comment-driven collaboration.
|
|
93
|
-
|
|
94
|
-
## Relai and Figma's official MCP
|
|
95
|
-
|
|
96
|
-
Figma's own AI has grown fast — the official MCP server now writes to the canvas, and the Figma Design Agent collaborates right inside the editor. Both are excellent, and both belong to full seats on paid plans, with usage metered in AI credits and models chosen by Figma. Relai is the open-source counterpart on the other side of that line: every plan including free, whatever model and subscription you already use, everything running on your machine, and the designer holding the controls. If you have the seats, the two coexist happily — run both.
|
|
97
|
-
|
|
98
|
-
## Optional: comments
|
|
99
|
-
|
|
100
|
-
Comments live behind Figma's REST API, which needs a personal access token. Generate one at figma.com → Settings → Security (enable comment scopes), then add it to your MCP config:
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
{ "mcpServers": { "Relai": { "command": "npx", "args": ["-y", "figma-relai"],
|
|
104
|
-
"env": { "FIGMA_TOKEN": "figd_..." } } } }
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
The token stays in your config file and is sent only to `api.figma.com`. Every other tool works without it. With it, "apply the feedback in the comments" becomes a thing the AI can actually do: read the threads, make the edits, reply. It also unlocks a quiet workflow: leave an @-comment on the canvas as a task, then tell your AI to "check the comments" — it claims the thread, does the work, and reports back on it.
|
|
108
|
-
|
|
109
|
-
## Troubleshooting
|
|
110
|
-
|
|
111
|
-
Start with `npx figma-relai doctor` — one command that checks Node, the relay ports (and whether something foreign is squatting on them), plugin presence, the saved room, and the comments token, each with its fix.
|
|
112
|
-
|
|
113
|
-
**The plugin shows NO SERVER.** No MCP server is listening on ports 9055–9057, which usually means your AI client isn't running or Relai isn't registered in it. The panel shows the exact registration command; the plugin keeps dialing and connects the moment a server appears.
|
|
114
|
-
|
|
115
|
-
**RELAY says LINK but AGENT says WAITING.** The plumbing is fine — the AI just hasn't called a Figma tool yet in this session. Ask it something about the file.
|
|
116
|
-
|
|
117
|
-
**"Multiple Figma plugins are connected."** Two files are running the plugin. Tell the AI to `join_room` with the room name shown in the plugin you want to control.
|
|
118
|
-
|
|
119
|
-
**The first `npx` run is slow.** It downloads the package once; later starts are fast.
|
|
120
|
-
|
|
121
|
-
## Security
|
|
122
|
-
|
|
123
|
-
The relay binds to `127.0.0.1` only and has no authentication beyond room names, which carry a crypto-random suffix. `execute_figma` runs AI-written code inside Figma's plugin sandbox; it is on by default, every run is visible in the activity feed, and the designer can disable it. Scripts are not atomic — a failed script's earlier changes persist. Full threat model: [SECURITY.md](SECURITY.md).
|
|
124
|
-
|
|
125
|
-
## For contributors
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
git clone https://github.com/syoooo/figma-relai.git
|
|
129
|
-
cd figma-relai
|
|
130
|
-
bun setup # install, build, write local MCP configs
|
|
131
|
-
bun test
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Requires [Bun](https://bun.sh/) v1.0+ (the setup script is bash; on Windows use WSL). Load the plugin via **Plugins → Development → Import plugin from manifest…** → `packages/figma-plugin/manifest.json`. A standalone relay (`bun socket`) exists for the unusual case where the relay must run on another machine. More in [CONTRIBUTING.md](CONTRIBUTING.md); manual QA lives in [docs/smoke-checklist.md](docs/smoke-checklist.md).
|
|
135
|
-
|
|
136
|
-
## License
|
|
137
|
-
|
|
138
|
-
MIT — see [LICENSE](LICENSE).
|