@muloka/figma-console-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +898 -0
  3. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts +14 -0
  4. package/dist/apps/design-system-dashboard/scoring/accessibility.d.ts.map +1 -0
  5. package/dist/apps/design-system-dashboard/scoring/accessibility.js +278 -0
  6. package/dist/apps/design-system-dashboard/scoring/accessibility.js.map +1 -0
  7. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts +29 -0
  8. package/dist/apps/design-system-dashboard/scoring/component-metadata.d.ts.map +1 -0
  9. package/dist/apps/design-system-dashboard/scoring/component-metadata.js +358 -0
  10. package/dist/apps/design-system-dashboard/scoring/component-metadata.js.map +1 -0
  11. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts +14 -0
  12. package/dist/apps/design-system-dashboard/scoring/consistency.d.ts.map +1 -0
  13. package/dist/apps/design-system-dashboard/scoring/consistency.js +342 -0
  14. package/dist/apps/design-system-dashboard/scoring/consistency.js.map +1 -0
  15. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts +14 -0
  16. package/dist/apps/design-system-dashboard/scoring/coverage.d.ts.map +1 -0
  17. package/dist/apps/design-system-dashboard/scoring/coverage.js +231 -0
  18. package/dist/apps/design-system-dashboard/scoring/coverage.js.map +1 -0
  19. package/dist/apps/design-system-dashboard/scoring/engine.d.ts +27 -0
  20. package/dist/apps/design-system-dashboard/scoring/engine.d.ts.map +1 -0
  21. package/dist/apps/design-system-dashboard/scoring/engine.js +93 -0
  22. package/dist/apps/design-system-dashboard/scoring/engine.js.map +1 -0
  23. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts +14 -0
  24. package/dist/apps/design-system-dashboard/scoring/naming-semantics.d.ts.map +1 -0
  25. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js +309 -0
  26. package/dist/apps/design-system-dashboard/scoring/naming-semantics.js.map +1 -0
  27. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts +14 -0
  28. package/dist/apps/design-system-dashboard/scoring/token-architecture.d.ts.map +1 -0
  29. package/dist/apps/design-system-dashboard/scoring/token-architecture.js +350 -0
  30. package/dist/apps/design-system-dashboard/scoring/token-architecture.js.map +1 -0
  31. package/dist/apps/design-system-dashboard/scoring/types.d.ts +89 -0
  32. package/dist/apps/design-system-dashboard/scoring/types.d.ts.map +1 -0
  33. package/dist/apps/design-system-dashboard/scoring/types.js +41 -0
  34. package/dist/apps/design-system-dashboard/scoring/types.js.map +1 -0
  35. package/dist/apps/design-system-dashboard/server.d.ts +24 -0
  36. package/dist/apps/design-system-dashboard/server.d.ts.map +1 -0
  37. package/dist/apps/design-system-dashboard/server.js +160 -0
  38. package/dist/apps/design-system-dashboard/server.js.map +1 -0
  39. package/dist/apps/token-browser/server.d.ts +26 -0
  40. package/dist/apps/token-browser/server.d.ts.map +1 -0
  41. package/dist/apps/token-browser/server.js +137 -0
  42. package/dist/apps/token-browser/server.js.map +1 -0
  43. package/dist/browser/base.d.ts +58 -0
  44. package/dist/browser/base.d.ts.map +1 -0
  45. package/dist/browser/base.js +6 -0
  46. package/dist/browser/base.js.map +1 -0
  47. package/dist/browser/local.d.ts +87 -0
  48. package/dist/browser/local.d.ts.map +1 -0
  49. package/dist/browser/local.js +318 -0
  50. package/dist/browser/local.js.map +1 -0
  51. package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +277 -0
  52. package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +357 -0
  53. package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +341 -0
  54. package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +230 -0
  55. package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +92 -0
  56. package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +308 -0
  57. package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +349 -0
  58. package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +40 -0
  59. package/dist/cloudflare/apps/design-system-dashboard/server.js +159 -0
  60. package/dist/cloudflare/apps/token-browser/server.js +136 -0
  61. package/dist/cloudflare/browser-manager.js +157 -0
  62. package/dist/cloudflare/core/accessibility-tools.js +361 -0
  63. package/dist/cloudflare/core/annotation-tools.js +230 -0
  64. package/dist/cloudflare/core/cloud-websocket-connector.js +419 -0
  65. package/dist/cloudflare/core/cloud-websocket-relay.js +198 -0
  66. package/dist/cloudflare/core/comment-tools.js +292 -0
  67. package/dist/cloudflare/core/config.js +153 -0
  68. package/dist/cloudflare/core/console-monitor.js +427 -0
  69. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  70. package/dist/cloudflare/core/design-code-tools.js +2793 -0
  71. package/dist/cloudflare/core/design-system-manifest.js +265 -0
  72. package/dist/cloudflare/core/design-system-tools.js +882 -0
  73. package/dist/cloudflare/core/diagnose-tool.js +100 -0
  74. package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
  75. package/dist/cloudflare/core/diff/diff-engine.js +334 -0
  76. package/dist/cloudflare/core/diff/property-compare.js +36 -0
  77. package/dist/cloudflare/core/diff/version-cache.js +74 -0
  78. package/dist/cloudflare/core/enrichment/enrichment-service.js +374 -0
  79. package/dist/cloudflare/core/enrichment/index.js +7 -0
  80. package/dist/cloudflare/core/enrichment/relationship-mapper.js +351 -0
  81. package/dist/cloudflare/core/enrichment/style-resolver.js +346 -0
  82. package/dist/cloudflare/core/figjam-tools.js +547 -0
  83. package/dist/cloudflare/core/figma-api.js +509 -0
  84. package/dist/cloudflare/core/figma-connector.js +7 -0
  85. package/dist/cloudflare/core/figma-reconstruction-spec.js +402 -0
  86. package/dist/cloudflare/core/figma-style-extractor.js +311 -0
  87. package/dist/cloudflare/core/figma-tools.js +3302 -0
  88. package/dist/cloudflare/core/identity.js +96 -0
  89. package/dist/cloudflare/core/library-tools.js +580 -0
  90. package/dist/cloudflare/core/logger.js +53 -0
  91. package/dist/cloudflare/core/port-discovery.js +935 -0
  92. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  93. package/dist/cloudflare/core/slides-tools.js +714 -0
  94. package/dist/cloudflare/core/slot-tools.js +334 -0
  95. package/dist/cloudflare/core/snippet-injector.js +96 -0
  96. package/dist/cloudflare/core/tokens/alias-resolver.js +205 -0
  97. package/dist/cloudflare/core/tokens/config.js +294 -0
  98. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  99. package/dist/cloudflare/core/tokens/figma-converter.js +323 -0
  100. package/dist/cloudflare/core/tokens/formatters/css-vars.js +338 -0
  101. package/dist/cloudflare/core/tokens/formatters/dtcg.js +376 -0
  102. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  103. package/dist/cloudflare/core/tokens/formatters/json.js +205 -0
  104. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  105. package/dist/cloudflare/core/tokens/formatters/scss.js +258 -0
  106. package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
  107. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +213 -0
  108. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
  109. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +335 -0
  110. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +256 -0
  111. package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
  112. package/dist/cloudflare/core/tokens/index.js +16 -0
  113. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  114. package/dist/cloudflare/core/tokens/parsers/dtcg.js +280 -0
  115. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  116. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  117. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  118. package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
  119. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  120. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  121. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  122. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  123. package/dist/cloudflare/core/tokens/schemas.js +152 -0
  124. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  125. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  126. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  127. package/dist/cloudflare/core/tokens/types.js +18 -0
  128. package/dist/cloudflare/core/tokens-tools.js +1788 -0
  129. package/dist/cloudflare/core/types/design-code.js +4 -0
  130. package/dist/cloudflare/core/types/enriched.js +5 -0
  131. package/dist/cloudflare/core/types/index.js +4 -0
  132. package/dist/cloudflare/core/variable-resolver.js +85 -0
  133. package/dist/cloudflare/core/version-tools.js +1199 -0
  134. package/dist/cloudflare/core/websocket-connector.js +436 -0
  135. package/dist/cloudflare/core/websocket-server.js +956 -0
  136. package/dist/cloudflare/core/write-tools.js +2506 -0
  137. package/dist/cloudflare/index.js +3010 -0
  138. package/dist/core/accessibility-tools.d.ts +21 -0
  139. package/dist/core/accessibility-tools.d.ts.map +1 -0
  140. package/dist/core/accessibility-tools.js +362 -0
  141. package/dist/core/accessibility-tools.js.map +1 -0
  142. package/dist/core/annotation-tools.d.ts +14 -0
  143. package/dist/core/annotation-tools.d.ts.map +1 -0
  144. package/dist/core/annotation-tools.js +231 -0
  145. package/dist/core/annotation-tools.js.map +1 -0
  146. package/dist/core/comment-tools.d.ts +11 -0
  147. package/dist/core/comment-tools.d.ts.map +1 -0
  148. package/dist/core/comment-tools.js +293 -0
  149. package/dist/core/comment-tools.js.map +1 -0
  150. package/dist/core/config.d.ts +17 -0
  151. package/dist/core/config.d.ts.map +1 -0
  152. package/dist/core/config.js +154 -0
  153. package/dist/core/config.js.map +1 -0
  154. package/dist/core/console-monitor.d.ts +82 -0
  155. package/dist/core/console-monitor.d.ts.map +1 -0
  156. package/dist/core/console-monitor.js +428 -0
  157. package/dist/core/console-monitor.js.map +1 -0
  158. package/dist/core/deep-component-tools.d.ts +14 -0
  159. package/dist/core/deep-component-tools.d.ts.map +1 -0
  160. package/dist/core/deep-component-tools.js +129 -0
  161. package/dist/core/deep-component-tools.js.map +1 -0
  162. package/dist/core/design-code-tools.d.ts +116 -0
  163. package/dist/core/design-code-tools.d.ts.map +1 -0
  164. package/dist/core/design-code-tools.js +2794 -0
  165. package/dist/core/design-code-tools.js.map +1 -0
  166. package/dist/core/design-system-manifest.d.ts +273 -0
  167. package/dist/core/design-system-manifest.d.ts.map +1 -0
  168. package/dist/core/design-system-manifest.js +266 -0
  169. package/dist/core/design-system-manifest.js.map +1 -0
  170. package/dist/core/design-system-tools.d.ts +67 -0
  171. package/dist/core/design-system-tools.d.ts.map +1 -0
  172. package/dist/core/design-system-tools.js +883 -0
  173. package/dist/core/design-system-tools.js.map +1 -0
  174. package/dist/core/diagnose-tool.d.ts +41 -0
  175. package/dist/core/diagnose-tool.d.ts.map +1 -0
  176. package/dist/core/diagnose-tool.js +101 -0
  177. package/dist/core/diagnose-tool.js.map +1 -0
  178. package/dist/core/diff/changelog-formatter.d.ts +35 -0
  179. package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
  180. package/dist/core/diff/changelog-formatter.js +276 -0
  181. package/dist/core/diff/changelog-formatter.js.map +1 -0
  182. package/dist/core/diff/diff-engine.d.ts +127 -0
  183. package/dist/core/diff/diff-engine.d.ts.map +1 -0
  184. package/dist/core/diff/diff-engine.js +335 -0
  185. package/dist/core/diff/diff-engine.js.map +1 -0
  186. package/dist/core/diff/property-compare.d.ts +19 -0
  187. package/dist/core/diff/property-compare.d.ts.map +1 -0
  188. package/dist/core/diff/property-compare.js +37 -0
  189. package/dist/core/diff/property-compare.js.map +1 -0
  190. package/dist/core/diff/version-cache.d.ts +40 -0
  191. package/dist/core/diff/version-cache.d.ts.map +1 -0
  192. package/dist/core/diff/version-cache.js +75 -0
  193. package/dist/core/diff/version-cache.js.map +1 -0
  194. package/dist/core/enrichment/enrichment-service.d.ts +52 -0
  195. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -0
  196. package/dist/core/enrichment/enrichment-service.js +375 -0
  197. package/dist/core/enrichment/enrichment-service.js.map +1 -0
  198. package/dist/core/enrichment/index.d.ts +8 -0
  199. package/dist/core/enrichment/index.d.ts.map +1 -0
  200. package/dist/core/enrichment/index.js +8 -0
  201. package/dist/core/enrichment/index.js.map +1 -0
  202. package/dist/core/enrichment/relationship-mapper.d.ts +106 -0
  203. package/dist/core/enrichment/relationship-mapper.d.ts.map +1 -0
  204. package/dist/core/enrichment/relationship-mapper.js +352 -0
  205. package/dist/core/enrichment/relationship-mapper.js.map +1 -0
  206. package/dist/core/enrichment/style-resolver.d.ts +85 -0
  207. package/dist/core/enrichment/style-resolver.d.ts.map +1 -0
  208. package/dist/core/enrichment/style-resolver.js +347 -0
  209. package/dist/core/enrichment/style-resolver.js.map +1 -0
  210. package/dist/core/figjam-tools.d.ts +8 -0
  211. package/dist/core/figjam-tools.d.ts.map +1 -0
  212. package/dist/core/figjam-tools.js +548 -0
  213. package/dist/core/figjam-tools.js.map +1 -0
  214. package/dist/core/figma-api.d.ts +254 -0
  215. package/dist/core/figma-api.d.ts.map +1 -0
  216. package/dist/core/figma-api.js +510 -0
  217. package/dist/core/figma-api.js.map +1 -0
  218. package/dist/core/figma-connector.d.ts +214 -0
  219. package/dist/core/figma-connector.d.ts.map +1 -0
  220. package/dist/core/figma-connector.js +8 -0
  221. package/dist/core/figma-connector.js.map +1 -0
  222. package/dist/core/figma-desktop-connector.d.ts +312 -0
  223. package/dist/core/figma-desktop-connector.d.ts.map +1 -0
  224. package/dist/core/figma-desktop-connector.js +1298 -0
  225. package/dist/core/figma-desktop-connector.js.map +1 -0
  226. package/dist/core/figma-reconstruction-spec.d.ts +166 -0
  227. package/dist/core/figma-reconstruction-spec.d.ts.map +1 -0
  228. package/dist/core/figma-reconstruction-spec.js +403 -0
  229. package/dist/core/figma-reconstruction-spec.js.map +1 -0
  230. package/dist/core/figma-style-extractor.d.ts +76 -0
  231. package/dist/core/figma-style-extractor.d.ts.map +1 -0
  232. package/dist/core/figma-style-extractor.js +312 -0
  233. package/dist/core/figma-style-extractor.js.map +1 -0
  234. package/dist/core/figma-tools.d.ts +22 -0
  235. package/dist/core/figma-tools.d.ts.map +1 -0
  236. package/dist/core/figma-tools.js +3303 -0
  237. package/dist/core/figma-tools.js.map +1 -0
  238. package/dist/core/identity.d.ts +41 -0
  239. package/dist/core/identity.d.ts.map +1 -0
  240. package/dist/core/identity.js +97 -0
  241. package/dist/core/identity.js.map +1 -0
  242. package/dist/core/library-tools.d.ts +17 -0
  243. package/dist/core/library-tools.d.ts.map +1 -0
  244. package/dist/core/library-tools.js +581 -0
  245. package/dist/core/library-tools.js.map +1 -0
  246. package/dist/core/logger.d.ts +22 -0
  247. package/dist/core/logger.d.ts.map +1 -0
  248. package/dist/core/logger.js +54 -0
  249. package/dist/core/logger.js.map +1 -0
  250. package/dist/core/port-discovery.d.ts +211 -0
  251. package/dist/core/port-discovery.d.ts.map +1 -0
  252. package/dist/core/port-discovery.js +936 -0
  253. package/dist/core/port-discovery.js.map +1 -0
  254. package/dist/core/resolve-package-root.d.ts +2 -0
  255. package/dist/core/resolve-package-root.d.ts.map +1 -0
  256. package/dist/core/resolve-package-root.js +12 -0
  257. package/dist/core/resolve-package-root.js.map +1 -0
  258. package/dist/core/slides-tools.d.ts +8 -0
  259. package/dist/core/slides-tools.d.ts.map +1 -0
  260. package/dist/core/slides-tools.js +715 -0
  261. package/dist/core/slides-tools.js.map +1 -0
  262. package/dist/core/slot-tools.d.ts +7 -0
  263. package/dist/core/slot-tools.d.ts.map +1 -0
  264. package/dist/core/slot-tools.js +335 -0
  265. package/dist/core/slot-tools.js.map +1 -0
  266. package/dist/core/snippet-injector.d.ts +24 -0
  267. package/dist/core/snippet-injector.d.ts.map +1 -0
  268. package/dist/core/snippet-injector.js +97 -0
  269. package/dist/core/snippet-injector.js.map +1 -0
  270. package/dist/core/tokens/alias-resolver.d.ts +97 -0
  271. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  272. package/dist/core/tokens/alias-resolver.js +206 -0
  273. package/dist/core/tokens/alias-resolver.js.map +1 -0
  274. package/dist/core/tokens/config.d.ts +380 -0
  275. package/dist/core/tokens/config.d.ts.map +1 -0
  276. package/dist/core/tokens/config.js +295 -0
  277. package/dist/core/tokens/config.js.map +1 -0
  278. package/dist/core/tokens/dialect.d.ts +107 -0
  279. package/dist/core/tokens/dialect.d.ts.map +1 -0
  280. package/dist/core/tokens/dialect.js +233 -0
  281. package/dist/core/tokens/dialect.js.map +1 -0
  282. package/dist/core/tokens/figma-converter.d.ts +102 -0
  283. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  284. package/dist/core/tokens/figma-converter.js +324 -0
  285. package/dist/core/tokens/figma-converter.js.map +1 -0
  286. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  287. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  288. package/dist/core/tokens/formatters/css-vars.js +339 -0
  289. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  290. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  291. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  292. package/dist/core/tokens/formatters/dtcg.js +377 -0
  293. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  294. package/dist/core/tokens/formatters/index.d.ts +30 -0
  295. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  296. package/dist/core/tokens/formatters/index.js +46 -0
  297. package/dist/core/tokens/formatters/index.js.map +1 -0
  298. package/dist/core/tokens/formatters/json.d.ts +37 -0
  299. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  300. package/dist/core/tokens/formatters/json.js +206 -0
  301. package/dist/core/tokens/formatters/json.js.map +1 -0
  302. package/dist/core/tokens/formatters/less.d.ts +4 -0
  303. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  304. package/dist/core/tokens/formatters/less.js +5 -0
  305. package/dist/core/tokens/formatters/less.js.map +1 -0
  306. package/dist/core/tokens/formatters/scss.d.ts +26 -0
  307. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  308. package/dist/core/tokens/formatters/scss.js +259 -0
  309. package/dist/core/tokens/formatters/scss.js.map +1 -0
  310. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  311. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  312. package/dist/core/tokens/formatters/stubs.js +14 -0
  313. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  314. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
  315. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  316. package/dist/core/tokens/formatters/style-dictionary-v3.js +214 -0
  317. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  318. package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
  319. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  320. package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
  321. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  322. package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
  323. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  324. package/dist/core/tokens/formatters/tailwind-v4.js +336 -0
  325. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  326. package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
  327. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  328. package/dist/core/tokens/formatters/tokens-studio.js +257 -0
  329. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  330. package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
  331. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  332. package/dist/core/tokens/formatters/ts-module.js +199 -0
  333. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  334. package/dist/core/tokens/index.d.ts +18 -0
  335. package/dist/core/tokens/index.d.ts.map +1 -0
  336. package/dist/core/tokens/index.js +17 -0
  337. package/dist/core/tokens/index.js.map +1 -0
  338. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  339. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  340. package/dist/core/tokens/parsers/css-vars.js +5 -0
  341. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  342. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  343. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  344. package/dist/core/tokens/parsers/dtcg.js +281 -0
  345. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  346. package/dist/core/tokens/parsers/index.d.ts +37 -0
  347. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  348. package/dist/core/tokens/parsers/index.js +139 -0
  349. package/dist/core/tokens/parsers/index.js.map +1 -0
  350. package/dist/core/tokens/parsers/json.d.ts +4 -0
  351. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  352. package/dist/core/tokens/parsers/json.js +8 -0
  353. package/dist/core/tokens/parsers/json.js.map +1 -0
  354. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  355. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  356. package/dist/core/tokens/parsers/scss.js +5 -0
  357. package/dist/core/tokens/parsers/scss.js.map +1 -0
  358. package/dist/core/tokens/parsers/stubs.d.ts +15 -0
  359. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  360. package/dist/core/tokens/parsers/stubs.js +21 -0
  361. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  362. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  363. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  364. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  365. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  366. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  367. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  368. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  369. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  370. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  371. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  372. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  373. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  374. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  375. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  376. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  377. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  378. package/dist/core/tokens/schemas.d.ts +155 -0
  379. package/dist/core/tokens/schemas.d.ts.map +1 -0
  380. package/dist/core/tokens/schemas.js +153 -0
  381. package/dist/core/tokens/schemas.js.map +1 -0
  382. package/dist/core/tokens/transforms/color.d.ts +9 -0
  383. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  384. package/dist/core/tokens/transforms/color.js +13 -0
  385. package/dist/core/tokens/transforms/color.js.map +1 -0
  386. package/dist/core/tokens/transforms/index.d.ts +36 -0
  387. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  388. package/dist/core/tokens/transforms/index.js +30 -0
  389. package/dist/core/tokens/transforms/index.js.map +1 -0
  390. package/dist/core/tokens/transforms/size.d.ts +7 -0
  391. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  392. package/dist/core/tokens/transforms/size.js +8 -0
  393. package/dist/core/tokens/transforms/size.js.map +1 -0
  394. package/dist/core/tokens/types.d.ts +284 -0
  395. package/dist/core/tokens/types.d.ts.map +1 -0
  396. package/dist/core/tokens/types.js +19 -0
  397. package/dist/core/tokens/types.js.map +1 -0
  398. package/dist/core/tokens-tools.d.ts +285 -0
  399. package/dist/core/tokens-tools.d.ts.map +1 -0
  400. package/dist/core/tokens-tools.js +1789 -0
  401. package/dist/core/tokens-tools.js.map +1 -0
  402. package/dist/core/types/design-code.d.ts +271 -0
  403. package/dist/core/types/design-code.d.ts.map +1 -0
  404. package/dist/core/types/design-code.js +5 -0
  405. package/dist/core/types/design-code.js.map +1 -0
  406. package/dist/core/types/enriched.d.ts +213 -0
  407. package/dist/core/types/enriched.d.ts.map +1 -0
  408. package/dist/core/types/enriched.js +6 -0
  409. package/dist/core/types/enriched.js.map +1 -0
  410. package/dist/core/types/index.d.ts +104 -0
  411. package/dist/core/types/index.d.ts.map +1 -0
  412. package/dist/core/types/index.js +5 -0
  413. package/dist/core/types/index.js.map +1 -0
  414. package/dist/core/variable-resolver.d.ts +45 -0
  415. package/dist/core/variable-resolver.d.ts.map +1 -0
  416. package/dist/core/variable-resolver.js +86 -0
  417. package/dist/core/variable-resolver.js.map +1 -0
  418. package/dist/core/version-tools.d.ts +59 -0
  419. package/dist/core/version-tools.d.ts.map +1 -0
  420. package/dist/core/version-tools.js +1200 -0
  421. package/dist/core/version-tools.js.map +1 -0
  422. package/dist/core/websocket-connector.d.ts +247 -0
  423. package/dist/core/websocket-connector.d.ts.map +1 -0
  424. package/dist/core/websocket-connector.js +437 -0
  425. package/dist/core/websocket-connector.js.map +1 -0
  426. package/dist/core/websocket-server.d.ts +290 -0
  427. package/dist/core/websocket-server.d.ts.map +1 -0
  428. package/dist/core/websocket-server.js +957 -0
  429. package/dist/core/websocket-server.js.map +1 -0
  430. package/dist/core/write-tools.d.ts +7 -0
  431. package/dist/core/write-tools.d.ts.map +1 -0
  432. package/dist/core/write-tools.js +2507 -0
  433. package/dist/core/write-tools.js.map +1 -0
  434. package/dist/local.d.ts +105 -0
  435. package/dist/local.d.ts.map +1 -0
  436. package/dist/local.js +3314 -0
  437. package/dist/local.js.map +1 -0
  438. package/figma-desktop-bridge/README.md +365 -0
  439. package/figma-desktop-bridge/code.js +7359 -0
  440. package/figma-desktop-bridge/icon.png +0 -0
  441. package/figma-desktop-bridge/manifest.json +67 -0
  442. package/figma-desktop-bridge/ui.html +2783 -0
  443. package/package.json +103 -0
@@ -0,0 +1,935 @@
1
+ /**
2
+ * Port Discovery Module
3
+ *
4
+ * Handles dynamic WebSocket port assignment with range-based fallback.
5
+ * When the preferred port (default 9223) is taken by another MCP server instance
6
+ * (e.g., Claude Desktop Chat tab vs Code tab), the server automatically tries
7
+ * the next port in a fixed range (9223-9232).
8
+ *
9
+ * Port advertisement files are written to /tmp so the Figma plugin can discover
10
+ * which port to connect to. Each instance writes its own file with PID for
11
+ * stale-file detection.
12
+ *
13
+ * Zombie process detection:
14
+ * Active servers refresh their port file every 30s (heartbeat).
15
+ * On startup, cleanupStalePortFiles() detects zombies via:
16
+ * 1. Dead PID — process no longer exists (existing behavior)
17
+ * 2. Stale heartbeat — lastSeen older than 5 minutes (process frozen/hung)
18
+ * 3. Age ceiling — startedAt older than 4 hours with no heartbeat (pre-v1.12 compat)
19
+ * Zombie processes are terminated with SIGTERM to free their ports.
20
+ *
21
+ * Cleanup exists in two flavors: synchronous (startup path, before the stdio
22
+ * transport serves requests — blocking is acceptable there) and async
23
+ * (periodic reaper — must never block the event loop, or in-flight MCP tool
24
+ * calls freeze while lsof/ps/curl run).
25
+ *
26
+ * Data flow:
27
+ * Server binds port → writes /tmp/figma-console-mcp-{port}.json
28
+ * Server heartbeat → refreshes lastSeen every 30s
29
+ * Plugin scans ports 9223-9232 → connects to first responding server
30
+ * External tools read port files for discovery
31
+ */
32
+ import { writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync } from 'fs';
33
+ import { join } from 'path';
34
+ import { tmpdir, devNull } from 'os';
35
+ import { execFile, execFileSync } from 'child_process';
36
+ import { promisify } from 'util';
37
+ import { createChildLogger } from './logger.js';
38
+ const execFileAsync = promisify(execFile);
39
+ const logger = createChildLogger({ component: 'port-discovery' });
40
+ /** Default preferred WebSocket port */
41
+ export const DEFAULT_WS_PORT = 9223;
42
+ /** Number of ports in the fallback range (9223-9232 = 10 ports) */
43
+ export const PORT_RANGE_SIZE = 10;
44
+ /** Prefix for port advertisement files in /tmp */
45
+ const PORT_FILE_PREFIX = 'figma-console-mcp-';
46
+ /** Directory for port advertisement files */
47
+ const PORT_FILE_DIR = tmpdir();
48
+ /** Maximum age before a port file without heartbeat is considered stale (4 hours) */
49
+ export const MAX_PORT_FILE_AGE_MS = 4 * 60 * 60 * 1000;
50
+ /** Maximum time since last heartbeat before a process is considered stale (5 minutes) */
51
+ export const HEARTBEAT_STALE_MS = 5 * 60 * 1000;
52
+ /** Grace period after SIGTERM before escalating to SIGKILL (ms) */
53
+ export const TERMINATE_GRACE_MS = 400;
54
+ /**
55
+ * Minimum process age before an orphan may be reaped (ms). Protects a sibling
56
+ * server that is mid-startup — it has bound a port but not yet written its
57
+ * advertisement file, so it would otherwise look like an orphan. By the time a
58
+ * real server is this old it has advertised and is in the known-PID set.
59
+ */
60
+ export const ORPHAN_MIN_AGE_MS = 60 * 1000;
61
+ /** Interval for the periodic background reaper (ms) */
62
+ export const REAP_INTERVAL_MS = 5 * 60 * 1000;
63
+ /** Minimum age before an instance can be evicted as last resort (2 minutes) */
64
+ export const EVICTION_MIN_AGE_MS = 2 * 60 * 1000;
65
+ /** Interval between heartbeat refreshes (30 seconds) */
66
+ export const HEARTBEAT_INTERVAL_MS = 30 * 1000;
67
+ /**
68
+ * Try to bind a WebSocket server to ports in a range, starting from the preferred port.
69
+ * Returns the first port that binds successfully.
70
+ *
71
+ * @param preferredPort - The port to try first (default 9223)
72
+ * @param host - The host to bind to (default 'localhost')
73
+ * @returns The actual port that was bound
74
+ * @throws If all ports in the range are exhausted
75
+ */
76
+ export function getPortRange(preferredPort = DEFAULT_WS_PORT) {
77
+ const ports = [];
78
+ for (let i = 0; i < PORT_RANGE_SIZE; i++) {
79
+ ports.push(preferredPort + i);
80
+ }
81
+ return ports;
82
+ }
83
+ /**
84
+ * Get the file path for a port advertisement file.
85
+ */
86
+ export function getPortFilePath(port) {
87
+ return join(PORT_FILE_DIR, `${PORT_FILE_PREFIX}${port}.json`);
88
+ }
89
+ /**
90
+ * Write a port advertisement file so clients can discover this server instance.
91
+ * Includes PID for stale-file detection and lastSeen for heartbeat tracking.
92
+ */
93
+ export function advertisePort(port, host = 'localhost') {
94
+ const now = new Date().toISOString();
95
+ const data = {
96
+ port,
97
+ pid: process.pid,
98
+ host,
99
+ startedAt: now,
100
+ lastSeen: now,
101
+ };
102
+ const filePath = getPortFilePath(port);
103
+ try {
104
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
105
+ logger.info({ port, filePath }, 'Port advertised');
106
+ }
107
+ catch (error) {
108
+ logger.warn({ port, filePath, error }, 'Failed to write port advertisement file');
109
+ }
110
+ }
111
+ /**
112
+ * Refresh the lastSeen timestamp in a port advertisement file.
113
+ * Called periodically as a heartbeat to prove this server is still active.
114
+ * Non-fatal — heartbeat failures are silently ignored.
115
+ */
116
+ export function refreshPortAdvertisement(port) {
117
+ const filePath = getPortFilePath(port);
118
+ try {
119
+ if (!existsSync(filePath))
120
+ return;
121
+ const raw = readFileSync(filePath, 'utf-8');
122
+ const data = JSON.parse(raw);
123
+ // Only refresh our own port file
124
+ if (data.pid !== process.pid)
125
+ return;
126
+ data.lastSeen = new Date().toISOString();
127
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
128
+ }
129
+ catch {
130
+ // Best-effort — heartbeat failures are non-fatal
131
+ }
132
+ }
133
+ /**
134
+ * Remove the port advertisement file for this instance.
135
+ * Call on clean shutdown.
136
+ */
137
+ export function unadvertisePort(port) {
138
+ const filePath = getPortFilePath(port);
139
+ try {
140
+ if (existsSync(filePath)) {
141
+ unlinkSync(filePath);
142
+ logger.debug({ port, filePath }, 'Port advertisement removed');
143
+ }
144
+ }
145
+ catch {
146
+ // Best-effort cleanup — file may already be gone
147
+ }
148
+ }
149
+ /**
150
+ * Check if a PID is still alive.
151
+ */
152
+ function isProcessAlive(pid) {
153
+ try {
154
+ process.kill(pid, 0); // Signal 0 = existence check, doesn't actually kill
155
+ return true;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }
161
+ /**
162
+ * Determine if a port file represents a zombie/stale MCP instance.
163
+ *
164
+ * Detection layers:
165
+ * 1. If lastSeen exists (v1.12+): stale if older than HEARTBEAT_STALE_MS (5 min)
166
+ * 2. If lastSeen is missing (pre-v1.12): stale if startedAt older than MAX_PORT_FILE_AGE_MS (4h)
167
+ *
168
+ * Assumes the owning process IS alive (PID check should happen before calling this).
169
+ */
170
+ export function isStaleInstance(data) {
171
+ const now = Date.now();
172
+ // If heartbeat exists, use it — active servers refresh every 30s
173
+ if (data.lastSeen) {
174
+ const lastSeenAge = now - new Date(data.lastSeen).getTime();
175
+ return lastSeenAge > HEARTBEAT_STALE_MS;
176
+ }
177
+ // No heartbeat (pre-v1.12 instance) — fall back to startup age
178
+ const startedAge = now - new Date(data.startedAt).getTime();
179
+ return startedAge > MAX_PORT_FILE_AGE_MS;
180
+ }
181
+ /**
182
+ * Extra staleness margin required before a live-but-stale instance becomes
183
+ * kill-eligible: it must have missed at least 2 further heartbeats PAST the
184
+ * stale threshold. After a laptop sleeps, every instance's lastSeen is old
185
+ * simultaneously — this margin gives freshly-woken siblings time to refresh
186
+ * their advertisement files before any reaper considers killing them.
187
+ */
188
+ export const KILL_ELIGIBLE_EXTRA_STALE_MS = 2 * HEARTBEAT_INTERVAL_MS;
189
+ /**
190
+ * Whether a stale instance has been stale long enough to be kill-eligible.
191
+ * Pre-v1.12 files (no lastSeen) already use the conservative 4-hour age
192
+ * ceiling, so plain staleness is sufficient there.
193
+ */
194
+ function isKillEligible(data) {
195
+ if (!data.lastSeen)
196
+ return isStaleInstance(data);
197
+ const lastSeenAge = Date.now() - new Date(data.lastSeen).getTime();
198
+ return lastSeenAge > HEARTBEAT_STALE_MS + KILL_ELIGIBLE_EXTRA_STALE_MS;
199
+ }
200
+ /**
201
+ * Ports reaching the probes come from JSON port files in a world-writable
202
+ * temp dir, so treat them as untrusted input. A garbage port means a garbage
203
+ * file — verdict is inconclusive (do NOT kill), same as any other ambiguity.
204
+ */
205
+ function isValidProbePort(port) {
206
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
207
+ }
208
+ /** curl arguments for the /health liveness probe against a sibling's port.
209
+ * `devNull` (not a literal /dev/null) — Windows ships curl.exe, and a bad
210
+ * output path there makes curl exit non-zero, which reads as "nothing
211
+ * responding" and would let the reaper kill a healthy sibling. */
212
+ function healthProbeArgs(port) {
213
+ return ['-s', '-o', devNull, '-m', '1', `http://127.0.0.1:${port}/health`];
214
+ }
215
+ /**
216
+ * Liveness probe against a sibling server's HTTP `/health` endpoint (the
217
+ * WebSocket server serves it on the same port). Synchronous via curl —
218
+ * startup-path only; the periodic reaper uses probeServerHealthAsync.
219
+ * execFileSync (no shell) so the untrusted port can never be interpreted
220
+ * as shell syntax.
221
+ *
222
+ * @returns true — server responded (alive, do NOT kill)
223
+ * false — probe ran and failed (nothing responding on the port)
224
+ * null — inconclusive (curl missing / probe ambiguous — do NOT kill)
225
+ */
226
+ function probeServerHealth(port) {
227
+ if (!isValidProbePort(port))
228
+ return null;
229
+ try {
230
+ execFileSync('curl', healthProbeArgs(port), {
231
+ timeout: 3000,
232
+ stdio: 'ignore',
233
+ });
234
+ return true;
235
+ }
236
+ catch (error) {
237
+ // curl binary not found (no shell, so ENOENT instead of exit 127/9009) — inconclusive
238
+ if (error?.code === 'ENOENT')
239
+ return null;
240
+ // execFileSync-level timeout (killed by signal, no exit status) — inconclusive
241
+ if (error?.signal)
242
+ return null;
243
+ // curl ran and exited non-zero (connection refused, curl -m timeout, …)
244
+ if (typeof error?.status === 'number')
245
+ return false;
246
+ return null; // unknown failure — err on the side of NOT killing
247
+ }
248
+ }
249
+ /**
250
+ * Async variant of probeServerHealth for the periodic reaper — identical
251
+ * verdict semantics, but never blocks the event loop (execFile, no shell).
252
+ */
253
+ async function probeServerHealthAsync(port) {
254
+ if (!isValidProbePort(port))
255
+ return null;
256
+ try {
257
+ await execFileAsync('curl', healthProbeArgs(port), { timeout: 3000 });
258
+ return true;
259
+ }
260
+ catch (error) {
261
+ // curl binary not found (no shell, so ENOENT instead of exit 127) — inconclusive
262
+ if (error?.code === 'ENOENT')
263
+ return null;
264
+ // execFile-level timeout (process killed by signal) — inconclusive
265
+ if (error?.killed || error?.signal)
266
+ return null;
267
+ // curl ran and exited non-zero (connection refused, curl -m timeout, …)
268
+ if (typeof error?.code === 'number')
269
+ return false;
270
+ return null; // unknown failure — err on the side of NOT killing
271
+ }
272
+ }
273
+ /**
274
+ * Block the current thread for `ms` milliseconds (synchronous).
275
+ * Used between SIGTERM and SIGKILL so terminateProcess can stay synchronous
276
+ * (its callers — the startup-path cleanup functions — are synchronous).
277
+ */
278
+ function sleepSyncMs(ms) {
279
+ if (ms <= 0)
280
+ return;
281
+ try {
282
+ // Cross-platform, no child process. SharedArrayBuffer/Atomics are standard in Node 18+.
283
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
284
+ }
285
+ catch {
286
+ const end = Date.now() + ms;
287
+ while (Date.now() < end) { /* best-effort spin fallback */ }
288
+ }
289
+ }
290
+ /**
291
+ * Non-blocking sleep for the async reaper path. The timer is unref'd so an
292
+ * in-flight reaper wait never keeps the process alive on its own.
293
+ */
294
+ function sleepMs(ms) {
295
+ if (ms <= 0)
296
+ return Promise.resolve();
297
+ return new Promise((resolve) => {
298
+ const timer = setTimeout(resolve, ms);
299
+ if (typeof timer.unref === 'function')
300
+ timer.unref();
301
+ });
302
+ }
303
+ /**
304
+ * Parse `ps -o etime=` output into milliseconds.
305
+ * `etime` (formatted elapsed time) is portable across macOS and Linux;
306
+ * `etimes` (seconds) is Linux-only — macOS ps rejects it. Format is
307
+ * [[DD-]HH:]MM:SS, e.g. "05:51", "55:27", "01:13:31", "09-14:15:41".
308
+ */
309
+ function parseEtimeMs(out) {
310
+ const m = out.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
311
+ if (!m)
312
+ return null;
313
+ const days = parseInt(m[1] || '0', 10);
314
+ const hours = parseInt(m[2] || '0', 10);
315
+ const mins = parseInt(m[3], 10);
316
+ const secs = parseInt(m[4], 10);
317
+ return ((((days * 24 + hours) * 60 + mins) * 60) + secs) * 1000;
318
+ }
319
+ /**
320
+ * Elapsed time since a process started, in milliseconds. Returns null if it
321
+ * cannot be determined (process gone, or `ps` unavailable/unparseable).
322
+ */
323
+ function getProcessAgeMs(pid) {
324
+ if (process.platform === 'win32')
325
+ return null;
326
+ try {
327
+ const { execSync } = require('child_process');
328
+ const out = execSync(`ps -p ${pid} -o etime= 2>/dev/null`, {
329
+ encoding: 'utf-8',
330
+ timeout: 2000,
331
+ });
332
+ return parseEtimeMs(out);
333
+ }
334
+ catch {
335
+ return null;
336
+ }
337
+ }
338
+ /** Async variant of getProcessAgeMs for the periodic reaper. */
339
+ async function getProcessAgeMsAsync(pid) {
340
+ if (process.platform === 'win32')
341
+ return null;
342
+ try {
343
+ const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'etime='], {
344
+ timeout: 2000,
345
+ });
346
+ return parseEtimeMs(stdout);
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ }
352
+ /**
353
+ * Terminate a process by PID, escalating to SIGKILL if it ignores SIGTERM.
354
+ *
355
+ * A hung MCP server can catch SIGTERM (its shutdown handler runs) yet never
356
+ * reach process.exit() — e.g. when graceful WebSocket/HTTP close blocks on a
357
+ * lingering connection. SIGTERM alone then leaves a zombie holding its port
358
+ * (often with its advertisement file already removed by the handler). We send
359
+ * SIGTERM, wait briefly, and force-kill with SIGKILL if it is still alive.
360
+ *
361
+ * @returns true if the process is confirmed gone afterwards, false if it survived.
362
+ */
363
+ function terminateProcess(pid, graceMs = TERMINATE_GRACE_MS) {
364
+ // SIGTERM first — give the process a chance to shut down gracefully.
365
+ try {
366
+ process.kill(pid, 'SIGTERM');
367
+ }
368
+ catch {
369
+ return true; // already gone — nothing to terminate
370
+ }
371
+ // Windows: SIGTERM maps to TerminateProcess (immediate, uncatchable).
372
+ if (process.platform === 'win32')
373
+ return !isProcessAlive(pid);
374
+ // POSIX: let the graceful handler run, then force-kill if it ignored SIGTERM.
375
+ sleepSyncMs(graceMs);
376
+ if (!isProcessAlive(pid))
377
+ return true;
378
+ try {
379
+ process.kill(pid, 'SIGKILL');
380
+ }
381
+ catch {
382
+ return true; // exited between the check and the kill
383
+ }
384
+ sleepSyncMs(Math.min(graceMs, 200));
385
+ return !isProcessAlive(pid);
386
+ }
387
+ /**
388
+ * Async variant of terminateProcess for the periodic reaper — identical
389
+ * SIGTERM→SIGKILL escalation, but the grace waits yield the event loop
390
+ * instead of blocking it.
391
+ *
392
+ * @returns true if the process is confirmed gone afterwards, false if it survived.
393
+ */
394
+ async function terminateProcessAsync(pid, graceMs = TERMINATE_GRACE_MS) {
395
+ // SIGTERM first — give the process a chance to shut down gracefully.
396
+ try {
397
+ process.kill(pid, 'SIGTERM');
398
+ }
399
+ catch {
400
+ return true; // already gone — nothing to terminate
401
+ }
402
+ // Windows: SIGTERM maps to TerminateProcess (immediate, uncatchable).
403
+ if (process.platform === 'win32')
404
+ return !isProcessAlive(pid);
405
+ // POSIX: let the graceful handler run, then force-kill if it ignored SIGTERM.
406
+ await sleepMs(graceMs);
407
+ if (!isProcessAlive(pid))
408
+ return true;
409
+ try {
410
+ process.kill(pid, 'SIGKILL');
411
+ }
412
+ catch {
413
+ return true; // exited between the check and the kill
414
+ }
415
+ await sleepMs(Math.min(graceMs, 200));
416
+ return !isProcessAlive(pid);
417
+ }
418
+ /**
419
+ * Read and validate a port advertisement file.
420
+ * Returns null if the file doesn't exist, is invalid, or the owning process is dead.
421
+ */
422
+ export function readPortFile(port) {
423
+ const filePath = getPortFilePath(port);
424
+ if (!existsSync(filePath))
425
+ return null;
426
+ try {
427
+ const raw = readFileSync(filePath, 'utf-8');
428
+ const data = JSON.parse(raw);
429
+ // Validate the owning process is still alive
430
+ if (!isProcessAlive(data.pid)) {
431
+ logger.debug({ port, pid: data.pid }, 'Stale port file detected (process dead), cleaning up');
432
+ try {
433
+ unlinkSync(filePath);
434
+ }
435
+ catch { /* best-effort */ }
436
+ return null;
437
+ }
438
+ return data;
439
+ }
440
+ catch {
441
+ return null;
442
+ }
443
+ }
444
+ /**
445
+ * Discover all active Figma Console MCP server instances by scanning port files.
446
+ * Validates each file's PID to filter out stale entries.
447
+ */
448
+ export function discoverActiveInstances(preferredPort = DEFAULT_WS_PORT) {
449
+ const instances = [];
450
+ for (const port of getPortRange(preferredPort)) {
451
+ const data = readPortFile(port);
452
+ if (data) {
453
+ instances.push(data);
454
+ }
455
+ }
456
+ return instances;
457
+ }
458
+ /**
459
+ * Clean up stale port files and terminate zombie MCP processes.
460
+ *
461
+ * Runs at startup before port binding. Detects stale instances via:
462
+ * 1. Dead PID — process no longer exists → delete file
463
+ * 2. Zombie process — alive but stale (no heartbeat or expired heartbeat)
464
+ * → send SIGTERM to free the port, then delete file
465
+ * 3. Corrupt file — invalid JSON → delete file
466
+ */
467
+ export function cleanupStalePortFiles() {
468
+ let cleaned = 0;
469
+ try {
470
+ const files = readdirSync(PORT_FILE_DIR);
471
+ for (const file of files) {
472
+ if (file.startsWith(PORT_FILE_PREFIX) && file.endsWith('.json')) {
473
+ const filePath = join(PORT_FILE_DIR, file);
474
+ try {
475
+ const raw = readFileSync(filePath, 'utf-8');
476
+ const data = JSON.parse(raw);
477
+ if (!isProcessAlive(data.pid)) {
478
+ // Dead PID — just clean up the file
479
+ unlinkSync(filePath);
480
+ cleaned++;
481
+ logger.debug({ port: data.port, pid: data.pid }, 'Cleaned up stale port file (dead process)');
482
+ }
483
+ else if (data.pid !== process.pid && isStaleInstance(data)) {
484
+ // Live PID but stale heartbeat. Staleness alone is NOT proof of
485
+ // death — after the machine sleeps longer than HEARTBEAT_STALE_MS,
486
+ // ALL instances look stale at once and whichever reaper ticks first
487
+ // would kill healthy siblings. Verify actual deadness before
488
+ // terminating: extra staleness margin + failed liveness probe.
489
+ if (!isKillEligible(data)) {
490
+ logger.debug({ port: data.port, pid: data.pid, lastSeen: data.lastSeen }, 'Stale instance within kill-eligibility margin — skipping');
491
+ }
492
+ else {
493
+ const health = probeServerHealth(data.port);
494
+ if (health !== false) {
495
+ logger.debug({ port: data.port, pid: data.pid, probe: health === true ? 'responding' : 'inconclusive' }, 'Stale-looking instance not confirmed dead by liveness probe — skipping kill');
496
+ }
497
+ else {
498
+ logger.info({ port: data.port, pid: data.pid, startedAt: data.startedAt, lastSeen: data.lastSeen }, 'Detected zombie MCP process (stale heartbeat, health probe failed) — sending SIGTERM to free port');
499
+ terminateProcess(data.pid);
500
+ try {
501
+ unlinkSync(filePath);
502
+ }
503
+ catch { /* best-effort */ }
504
+ cleaned++;
505
+ }
506
+ }
507
+ }
508
+ }
509
+ catch {
510
+ // Corrupt file — remove it
511
+ try {
512
+ unlinkSync(filePath);
513
+ cleaned++;
514
+ }
515
+ catch { /* ignore */ }
516
+ }
517
+ }
518
+ }
519
+ }
520
+ catch {
521
+ // Can't read /tmp — unusual but not fatal
522
+ }
523
+ return cleaned;
524
+ }
525
+ /**
526
+ * Async variant of cleanupStalePortFiles for the periodic reaper.
527
+ *
528
+ * Same detection layers and kill-safety gates as the sync version (dead PID →
529
+ * delete file; live-but-stale → kill-eligibility margin + failed liveness
530
+ * probe before terminating; corrupt → delete file), but the probe and the
531
+ * SIGTERM→SIGKILL grace waits are non-blocking so a slow pass never stalls
532
+ * the stdio MCP transport.
533
+ */
534
+ export async function cleanupStalePortFilesAsync() {
535
+ let cleaned = 0;
536
+ try {
537
+ const files = readdirSync(PORT_FILE_DIR);
538
+ for (const file of files) {
539
+ if (file.startsWith(PORT_FILE_PREFIX) && file.endsWith('.json')) {
540
+ const filePath = join(PORT_FILE_DIR, file);
541
+ try {
542
+ const raw = readFileSync(filePath, 'utf-8');
543
+ const data = JSON.parse(raw);
544
+ if (!isProcessAlive(data.pid)) {
545
+ // Dead PID — just clean up the file
546
+ unlinkSync(filePath);
547
+ cleaned++;
548
+ logger.debug({ port: data.port, pid: data.pid }, 'Cleaned up stale port file (dead process)');
549
+ }
550
+ else if (data.pid !== process.pid && isStaleInstance(data)) {
551
+ // Live PID but stale heartbeat. Staleness alone is NOT proof of
552
+ // death — after the machine sleeps longer than HEARTBEAT_STALE_MS,
553
+ // ALL instances look stale at once and whichever reaper ticks first
554
+ // would kill healthy siblings. Verify actual deadness before
555
+ // terminating: extra staleness margin + failed liveness probe.
556
+ if (!isKillEligible(data)) {
557
+ logger.debug({ port: data.port, pid: data.pid, lastSeen: data.lastSeen }, 'Stale instance within kill-eligibility margin — skipping');
558
+ }
559
+ else {
560
+ const health = await probeServerHealthAsync(data.port);
561
+ if (health !== false) {
562
+ logger.debug({ port: data.port, pid: data.pid, probe: health === true ? 'responding' : 'inconclusive' }, 'Stale-looking instance not confirmed dead by liveness probe — skipping kill');
563
+ }
564
+ else {
565
+ logger.info({ port: data.port, pid: data.pid, startedAt: data.startedAt, lastSeen: data.lastSeen }, 'Detected zombie MCP process (stale heartbeat, health probe failed) — sending SIGTERM to free port');
566
+ await terminateProcessAsync(data.pid);
567
+ try {
568
+ unlinkSync(filePath);
569
+ }
570
+ catch { /* best-effort */ }
571
+ cleaned++;
572
+ }
573
+ }
574
+ }
575
+ }
576
+ catch {
577
+ // Corrupt file — remove it
578
+ try {
579
+ unlinkSync(filePath);
580
+ cleaned++;
581
+ }
582
+ catch { /* ignore */ }
583
+ }
584
+ }
585
+ }
586
+ }
587
+ catch {
588
+ // Can't read /tmp — unusual but not fatal
589
+ }
590
+ return cleaned;
591
+ }
592
+ /**
593
+ * Deep scan for orphaned MCP server processes that hold ports but have no port files.
594
+ * These are processes left behind by Claude Desktop when tabs close without proper cleanup.
595
+ *
596
+ * Uses lsof (macOS/Linux) to find PIDs listening on each port in the range,
597
+ * then verifies they're figma-console-mcp before terminating.
598
+ *
599
+ * Call AFTER cleanupStalePortFiles() — that handles the port-file-based cleanup first,
600
+ * then this catches any remaining ghosts.
601
+ */
602
+ export function cleanupOrphanedProcesses(preferredPort = DEFAULT_WS_PORT, options = {}) {
603
+ // Only supported on macOS/Linux (lsof)
604
+ if (process.platform === 'win32')
605
+ return 0;
606
+ const minAgeMs = options.minAgeMs ?? ORPHAN_MIN_AGE_MS;
607
+ let cleaned = 0;
608
+ const myPid = process.pid;
609
+ const ports = getPortRange(preferredPort);
610
+ // Collect PIDs that have valid port files (known-good servers)
611
+ const knownPids = new Set();
612
+ for (const port of ports) {
613
+ const data = readPortFile(port);
614
+ if (data)
615
+ knownPids.add(data.pid);
616
+ }
617
+ knownPids.add(myPid); // Never kill ourselves
618
+ for (const port of ports) {
619
+ try {
620
+ // Find PIDs listening on this port via lsof
621
+ const { execSync } = require('child_process');
622
+ const output = execSync(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null`, {
623
+ encoding: 'utf-8',
624
+ timeout: 3000,
625
+ }).trim();
626
+ if (!output)
627
+ continue;
628
+ const pids = output.split('\n').map(Number).filter(Boolean);
629
+ for (const pid of pids) {
630
+ if (knownPids.has(pid))
631
+ continue; // Skip known-good servers
632
+ // Verify this is actually a figma-console-mcp process before killing
633
+ try {
634
+ const cmdline = execSync(`ps -p ${pid} -o command= 2>/dev/null`, {
635
+ encoding: 'utf-8',
636
+ timeout: 2000,
637
+ }).trim();
638
+ if (isMcpServerCommand(cmdline)) {
639
+ // Don't reap a sibling that is still starting up (bound a port but
640
+ // hasn't advertised yet). Real orphans are far older than this.
641
+ const ageMs = getProcessAgeMs(pid);
642
+ if (minAgeMs > 0 && ageMs !== null && ageMs < minAgeMs) {
643
+ continue;
644
+ }
645
+ logger.info({ port, pid, command: cmdline.substring(0, 120) }, 'Terminating orphaned MCP server (no port file, holding port)');
646
+ // terminateProcess escalates SIGTERM -> SIGKILL. Only count it as
647
+ // cleaned when the process is confirmed gone, so the log reflects
648
+ // reality (the old code counted attempts and reported success even
649
+ // when a SIGTERM-ignoring zombie survived).
650
+ if (terminateProcess(pid)) {
651
+ cleaned++;
652
+ }
653
+ else {
654
+ logger.warn({ port, pid }, 'Failed to terminate orphaned MCP server (survived SIGKILL)');
655
+ }
656
+ }
657
+ }
658
+ catch {
659
+ // Can't read process info — skip to be safe
660
+ }
661
+ }
662
+ }
663
+ catch {
664
+ // lsof failed for this port — skip
665
+ }
666
+ }
667
+ if (cleaned > 0) {
668
+ // Give terminated processes a moment to release their ports
669
+ try {
670
+ const { execSync } = require('child_process');
671
+ execSync('sleep 0.5', { timeout: 2000 });
672
+ }
673
+ catch { /* non-critical */ }
674
+ logger.info({ cleaned }, `Cleaned up ${cleaned} orphaned MCP server process(es)`);
675
+ }
676
+ return cleaned;
677
+ }
678
+ /** Whether a process command line identifies a figma-console-mcp server. */
679
+ function isMcpServerCommand(cmdline) {
680
+ return cmdline.includes('figma-console-mcp') || cmdline.includes('figma_console_mcp') || cmdline.includes('local.js');
681
+ }
682
+ /**
683
+ * List PIDs listening on ports in the range via a single batched lsof call.
684
+ * Returns a map of pid → first listening port seen (for logging). The sync
685
+ * startup path runs lsof once per port; batching matters here because the
686
+ * periodic reaper must not hold the event loop across 10 sequential calls.
687
+ */
688
+ async function listListeningPidsAsync(ports) {
689
+ const pidToPort = new Map();
690
+ const low = ports[0];
691
+ const high = ports[ports.length - 1];
692
+ let stdout = '';
693
+ try {
694
+ ({ stdout } = await execFileAsync('lsof', ['-nP', `-iTCP:${low}-${high}`, '-sTCP:LISTEN', '-Fpn'], { timeout: 5000 }));
695
+ }
696
+ catch (error) {
697
+ // lsof exits 1 when nothing matches; any partial output is still usable
698
+ stdout = typeof error?.stdout === 'string' ? error.stdout : '';
699
+ }
700
+ // -F output: `p<pid>` starts a process group, `n<host:port>` lines follow
701
+ let currentPid = null;
702
+ for (const line of stdout.split('\n')) {
703
+ if (line.startsWith('p')) {
704
+ const pid = parseInt(line.slice(1), 10);
705
+ currentPid = Number.isFinite(pid) ? pid : null;
706
+ }
707
+ else if (line.startsWith('n') && currentPid !== null) {
708
+ const m = line.match(/:(\d+)(?:\s|$)/);
709
+ if (!m)
710
+ continue;
711
+ const port = parseInt(m[1], 10);
712
+ if (ports.includes(port) && !pidToPort.has(currentPid)) {
713
+ pidToPort.set(currentPid, port);
714
+ }
715
+ }
716
+ }
717
+ return pidToPort;
718
+ }
719
+ /**
720
+ * Async variant of cleanupOrphanedProcesses for the periodic reaper.
721
+ *
722
+ * Same safety guards as the sync version (known-PID skip, MCP command-line
723
+ * check, ORPHAN_MIN_AGE_MS age guard, confirmed-kill counting), but all child
724
+ * processes run via async execFile and the lsof scan is a single batched call
725
+ * over the port range — a slow tick no longer freezes in-flight tool calls on
726
+ * the stdio transport.
727
+ */
728
+ export async function cleanupOrphanedProcessesAsync(preferredPort = DEFAULT_WS_PORT, options = {}) {
729
+ // Only supported on macOS/Linux (lsof)
730
+ if (process.platform === 'win32')
731
+ return 0;
732
+ const minAgeMs = options.minAgeMs ?? ORPHAN_MIN_AGE_MS;
733
+ let cleaned = 0;
734
+ const myPid = process.pid;
735
+ const ports = getPortRange(preferredPort);
736
+ // Collect PIDs that have valid port files (known-good servers)
737
+ const knownPids = new Set();
738
+ for (const port of ports) {
739
+ const data = readPortFile(port);
740
+ if (data)
741
+ knownPids.add(data.pid);
742
+ }
743
+ knownPids.add(myPid); // Never kill ourselves
744
+ const pidToPort = await listListeningPidsAsync(ports);
745
+ for (const [pid, port] of pidToPort) {
746
+ if (knownPids.has(pid))
747
+ continue; // Skip known-good servers
748
+ // Verify this is actually a figma-console-mcp process before killing
749
+ try {
750
+ const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'command='], {
751
+ timeout: 2000,
752
+ });
753
+ const cmdline = stdout.trim();
754
+ if (isMcpServerCommand(cmdline)) {
755
+ // Don't reap a sibling that is still starting up (bound a port but
756
+ // hasn't advertised yet). Real orphans are far older than this.
757
+ const ageMs = await getProcessAgeMsAsync(pid);
758
+ if (minAgeMs > 0 && ageMs !== null && ageMs < minAgeMs) {
759
+ continue;
760
+ }
761
+ logger.info({ port, pid, command: cmdline.substring(0, 120) }, 'Terminating orphaned MCP server (no port file, holding port)');
762
+ // Only count confirmed kills, matching the sync path.
763
+ if (await terminateProcessAsync(pid)) {
764
+ cleaned++;
765
+ }
766
+ else {
767
+ logger.warn({ port, pid }, 'Failed to terminate orphaned MCP server (survived SIGKILL)');
768
+ }
769
+ }
770
+ }
771
+ catch {
772
+ // Can't read process info — skip to be safe
773
+ }
774
+ }
775
+ if (cleaned > 0) {
776
+ // Give terminated processes a moment to release their ports
777
+ await sleepMs(500);
778
+ logger.info({ cleaned }, `Cleaned up ${cleaned} orphaned MCP server process(es)`);
779
+ }
780
+ return cleaned;
781
+ }
782
+ /**
783
+ * Last-resort eviction: terminate the oldest MCP server instance to free a port.
784
+ *
785
+ * Called ONLY when all ports in the range are exhausted after both cleanup phases
786
+ * (cleanupStalePortFiles + cleanupOrphanedProcesses) have already run. This handles
787
+ * the case where old instances are still alive and heartbeating but no longer needed
788
+ * (e.g., from yesterday's Claude Desktop session that was closed without terminating
789
+ * the MCP server process).
790
+ *
791
+ * Safety guards:
792
+ * - Only evicts instances older than EVICTION_MIN_AGE_MS (2 min) to prevent cascade
793
+ * - Never evicts our own PID
794
+ * - Re-reads port file before kill to avoid TOCTOU race
795
+ * - Uses SIGTERM for graceful shutdown
796
+ * - Waits briefly for port release before returning
797
+ *
798
+ * @returns true if an instance was evicted (caller should retry port binding), false otherwise
799
+ */
800
+ export function evictOldestInstance(preferredPort = DEFAULT_WS_PORT) {
801
+ const myPid = process.pid;
802
+ const ports = getPortRange(preferredPort);
803
+ const candidates = [];
804
+ // Collect all valid port file entries that aren't us
805
+ for (const port of ports) {
806
+ const filePath = getPortFilePath(port);
807
+ try {
808
+ if (!existsSync(filePath))
809
+ continue;
810
+ const raw = readFileSync(filePath, 'utf-8');
811
+ const data = JSON.parse(raw);
812
+ if (data.pid === myPid)
813
+ continue; // Never evict ourselves
814
+ if (!isProcessAlive(data.pid)) {
815
+ // Dead process — just clean up the file (port should already be free)
816
+ try {
817
+ unlinkSync(filePath);
818
+ }
819
+ catch { /* best-effort */ }
820
+ continue;
821
+ }
822
+ candidates.push({ ...data, filePath });
823
+ }
824
+ catch {
825
+ // Corrupt or unreadable — skip
826
+ }
827
+ }
828
+ if (candidates.length === 0) {
829
+ logger.debug('No eviction candidates — ports may be held by non-MCP processes');
830
+ return false;
831
+ }
832
+ // Sort by startedAt ascending — oldest first
833
+ candidates.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
834
+ const oldest = candidates[0];
835
+ const ageMs = Date.now() - new Date(oldest.startedAt).getTime();
836
+ // Safety: don't evict instances that started recently (prevents cascade)
837
+ if (ageMs < EVICTION_MIN_AGE_MS) {
838
+ logger.info({ port: oldest.port, pid: oldest.pid, ageSeconds: Math.round(ageMs / 1000) }, 'Oldest instance is too recent to evict — skipping');
839
+ return false;
840
+ }
841
+ // Re-read the port file to avoid TOCTOU race
842
+ try {
843
+ const raw = readFileSync(oldest.filePath, 'utf-8');
844
+ const freshData = JSON.parse(raw);
845
+ if (freshData.pid !== oldest.pid) {
846
+ // PID changed between reads — another process took over, skip
847
+ return false;
848
+ }
849
+ }
850
+ catch {
851
+ // File disappeared — port may already be free
852
+ return true;
853
+ }
854
+ logger.info({ port: oldest.port, pid: oldest.pid, startedAt: oldest.startedAt, ageHours: Math.round(ageMs / 3600000 * 10) / 10 }, 'Evicting oldest MCP server instance to free port (all ports exhausted)');
855
+ terminateProcess(oldest.pid);
856
+ try {
857
+ unlinkSync(oldest.filePath);
858
+ }
859
+ catch { /* best-effort */ }
860
+ // Brief wait for the port to be released by the OS
861
+ try {
862
+ const { execSync } = require('child_process');
863
+ execSync('sleep 0.5', { timeout: 2000 });
864
+ }
865
+ catch { /* non-critical */ }
866
+ return true;
867
+ }
868
+ /**
869
+ * Register process exit handlers to clean up port advertisement file.
870
+ * Should be called once after the port is successfully bound.
871
+ */
872
+ export function registerPortCleanup(port) {
873
+ const cleanup = () => unadvertisePort(port);
874
+ process.on('exit', cleanup);
875
+ // Prepend our cleanup — it runs first before the existing SIGINT/SIGTERM
876
+ // handlers in local.ts main() call process.exit()
877
+ process.prependListener('SIGINT', cleanup);
878
+ process.prependListener('SIGTERM', cleanup);
879
+ }
880
+ /**
881
+ * Start a periodic background reaper that re-runs the cleanup passes while this
882
+ * server is alive. Startup-only reaping leaves orphans to accumulate between
883
+ * launches (a sibling client that closes without its server exiting cleanly
884
+ * keeps holding a port until the *next* server starts). Periodic reaping keeps
885
+ * the range clean continuously.
886
+ *
887
+ * Safe against live siblings: they hold fresh advertisement files (heartbeat
888
+ * every 30s) so they are in the known-PID set and skipped, and the age guard in
889
+ * cleanupOrphanedProcesses protects mid-startup siblings.
890
+ *
891
+ * The tick runs the fully-async cleanup variants — the sync ones issue
892
+ * blocking lsof/ps/curl calls (seconds each under load), which would stall
893
+ * the stdio MCP transport and freeze in-flight tool calls. Only the startup
894
+ * path (before the transport is serving requests) uses the sync variants.
895
+ *
896
+ * The interval is unref'd so it never keeps the process alive on its own.
897
+ *
898
+ * @returns a stop function that clears the interval.
899
+ */
900
+ export function startPeriodicReaper(preferredPort = DEFAULT_WS_PORT) {
901
+ let running = false;
902
+ let stopped = false;
903
+ const tick = async () => {
904
+ if (running)
905
+ return; // previous tick still in flight — skip this one
906
+ running = true;
907
+ try {
908
+ // Refresh our OWN advertisement first so sibling reapers see us fresh.
909
+ // Post-sleep, every instance's lastSeen is stale simultaneously — the
910
+ // first reaper to tick must not look like a zombie to the others.
911
+ // refreshPortAdvertisement is a no-op for files owned by other PIDs,
912
+ // so scanning the whole range only touches our own file.
913
+ for (const port of getPortRange(preferredPort)) {
914
+ refreshPortAdvertisement(port);
915
+ }
916
+ await cleanupStalePortFilesAsync();
917
+ if (stopped)
918
+ return;
919
+ await cleanupOrphanedProcessesAsync(preferredPort);
920
+ }
921
+ catch (error) {
922
+ logger.warn({ error }, 'Periodic reaper tick failed');
923
+ }
924
+ finally {
925
+ running = false;
926
+ }
927
+ };
928
+ const interval = setInterval(() => { void tick(); }, REAP_INTERVAL_MS);
929
+ if (typeof interval.unref === 'function')
930
+ interval.unref();
931
+ return () => {
932
+ stopped = true;
933
+ clearInterval(interval);
934
+ };
935
+ }