@sarjallab09/figma-intelligence 1.0.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 (286) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +327 -0
  3. package/bin/cli.js +859 -0
  4. package/design-bridge/.env.example +5 -0
  5. package/design-bridge/bridge.js +196 -0
  6. package/design-bridge/lib/assets.js +367 -0
  7. package/design-bridge/lib/prompt.js +85 -0
  8. package/design-bridge/lib/server.js +66 -0
  9. package/design-bridge/lib/stitch.js +37 -0
  10. package/design-bridge/lib/tokens.js +82 -0
  11. package/design-bridge/package-lock.json +579 -0
  12. package/design-bridge/package.json +19 -0
  13. package/figma-bridge-plugin/README.md +97 -0
  14. package/figma-bridge-plugin/anthropic-chat-runner.js +192 -0
  15. package/figma-bridge-plugin/bridge-relay.js +2363 -0
  16. package/figma-bridge-plugin/chat-runner.js +459 -0
  17. package/figma-bridge-plugin/code.js +1528 -0
  18. package/figma-bridge-plugin/codex-runner.js +505 -0
  19. package/figma-bridge-plugin/component-schemas.js +110 -0
  20. package/figma-bridge-plugin/content-context.js +869 -0
  21. package/figma-bridge-plugin/create-button.js +216 -0
  22. package/figma-bridge-plugin/gemini-cli-runner.js +291 -0
  23. package/figma-bridge-plugin/gemini-runner.js +187 -0
  24. package/figma-bridge-plugin/html-to-figma.js +927 -0
  25. package/figma-bridge-plugin/knowledge-hub/.gitkeep +0 -0
  26. package/figma-bridge-plugin/knowledge-hub/uspec-references/anatomy-spec.md +159 -0
  27. package/figma-bridge-plugin/knowledge-hub/uspec-references/api-spec.md +162 -0
  28. package/figma-bridge-plugin/knowledge-hub/uspec-references/color-spec.md +148 -0
  29. package/figma-bridge-plugin/knowledge-hub/uspec-references/full-spec-template.md +314 -0
  30. package/figma-bridge-plugin/knowledge-hub/uspec-references/property-spec.md +175 -0
  31. package/figma-bridge-plugin/knowledge-hub/uspec-references/screen-reader-spec.md +180 -0
  32. package/figma-bridge-plugin/knowledge-hub/uspec-references/structure-spec.md +165 -0
  33. package/figma-bridge-plugin/manifest.json +21 -0
  34. package/figma-bridge-plugin/package-lock.json +1936 -0
  35. package/figma-bridge-plugin/package.json +20 -0
  36. package/figma-bridge-plugin/perplexity-runner.js +188 -0
  37. package/figma-bridge-plugin/references/SKILL.md +178 -0
  38. package/figma-bridge-plugin/references/anatomy-spec.md +159 -0
  39. package/figma-bridge-plugin/references/api-spec.md +162 -0
  40. package/figma-bridge-plugin/references/color-spec.md +148 -0
  41. package/figma-bridge-plugin/references/full-spec-template.md +314 -0
  42. package/figma-bridge-plugin/references/property-spec.md +175 -0
  43. package/figma-bridge-plugin/references/screen-reader-spec.md +180 -0
  44. package/figma-bridge-plugin/references/structure-spec.md +165 -0
  45. package/figma-bridge-plugin/shared-prompt-config.js +604 -0
  46. package/figma-bridge-plugin/spec-helpers/build-table.js +269 -0
  47. package/figma-bridge-plugin/spec-helpers/classify-elements.js +189 -0
  48. package/figma-bridge-plugin/spec-helpers/index.js +35 -0
  49. package/figma-bridge-plugin/spec-helpers/parse-figma-link.js +49 -0
  50. package/figma-bridge-plugin/spec-helpers/position-markers.js +158 -0
  51. package/figma-bridge-plugin/stitch-auth.js +322 -0
  52. package/figma-bridge-plugin/stitch-runner.js +1427 -0
  53. package/figma-bridge-plugin/token-resolver.js +107 -0
  54. package/figma-bridge-plugin/ui.html +4467 -0
  55. package/figma-intelligence-layer/.env.example +39 -0
  56. package/figma-intelligence-layer/docs/local-image-generation.md +60 -0
  57. package/figma-intelligence-layer/examples/comfyui-workflow-template.example.json +101 -0
  58. package/figma-intelligence-layer/jest.config.js +14 -0
  59. package/figma-intelligence-layer/mcp-config.json +19 -0
  60. package/figma-intelligence-layer/package-lock.json +5892 -0
  61. package/figma-intelligence-layer/package.json +48 -0
  62. package/figma-intelligence-layer/scripts/setup-comfyui-local.sh +67 -0
  63. package/figma-intelligence-layer/scripts/start-comfyui.sh +33 -0
  64. package/figma-intelligence-layer/src/index.ts +2233 -0
  65. package/figma-intelligence-layer/src/shared/auto-layout-validator.ts +404 -0
  66. package/figma-intelligence-layer/src/shared/cache.ts +187 -0
  67. package/figma-intelligence-layer/src/shared/color-operations.ts +533 -0
  68. package/figma-intelligence-layer/src/shared/color-utils.ts +138 -0
  69. package/figma-intelligence-layer/src/shared/component-script-builder.ts +413 -0
  70. package/figma-intelligence-layer/src/shared/component-templates.ts +2767 -0
  71. package/figma-intelligence-layer/src/shared/concept-taxonomy.ts +694 -0
  72. package/figma-intelligence-layer/src/shared/decision-log.ts +128 -0
  73. package/figma-intelligence-layer/src/shared/design-system-context.ts +568 -0
  74. package/figma-intelligence-layer/src/shared/design-system-intelligence.ts +131 -0
  75. package/figma-intelligence-layer/src/shared/design-system-matcher.ts +184 -0
  76. package/figma-intelligence-layer/src/shared/design-system-normalizers.ts +196 -0
  77. package/figma-intelligence-layer/src/shared/design-system-tokens.ts +295 -0
  78. package/figma-intelligence-layer/src/shared/dtcg-validator.ts +530 -0
  79. package/figma-intelligence-layer/src/shared/enrichment-pipeline.ts +671 -0
  80. package/figma-intelligence-layer/src/shared/figma-bridge.ts +1408 -0
  81. package/figma-intelligence-layer/src/shared/font-config.ts +126 -0
  82. package/figma-intelligence-layer/src/shared/icon-catalog.ts +360 -0
  83. package/figma-intelligence-layer/src/shared/icon-fetch.ts +80 -0
  84. package/figma-intelligence-layer/src/shared/prototype-script-builder.ts +162 -0
  85. package/figma-intelligence-layer/src/shared/response-compression.ts +440 -0
  86. package/figma-intelligence-layer/src/shared/semantic-token-catalog.ts +324 -0
  87. package/figma-intelligence-layer/src/shared/token-binder.ts +505 -0
  88. package/figma-intelligence-layer/src/shared/token-math.ts +427 -0
  89. package/figma-intelligence-layer/src/shared/token-naming.ts +468 -0
  90. package/figma-intelligence-layer/src/shared/token-utils.ts +420 -0
  91. package/figma-intelligence-layer/src/shared/types.ts +346 -0
  92. package/figma-intelligence-layer/src/shared/typography-presets.ts +94 -0
  93. package/figma-intelligence-layer/src/shared/unsplash.ts +165 -0
  94. package/figma-intelligence-layer/src/shared/vision-client.ts +607 -0
  95. package/figma-intelligence-layer/src/shared/vision-provider-anthropic.ts +334 -0
  96. package/figma-intelligence-layer/src/shared/vision-provider-openai.ts +446 -0
  97. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-handler.ts +782 -0
  98. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-renderer.ts +496 -0
  99. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotation-kit.ts +230 -0
  100. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/colorblind-sim.ts +66 -0
  101. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/index.ts +810 -0
  102. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts +1191 -0
  103. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-figma-page.ts +1346 -0
  104. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-handler.ts +148 -0
  105. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-figma-page.ts +499 -0
  106. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-report.ts +910 -0
  107. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-checker.ts +989 -0
  108. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-criteria.ts +1160 -0
  109. package/figma-intelligence-layer/src/tools/phase1-vision/design-from-ref/index.ts +424 -0
  110. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/component-recognizer.ts +38 -0
  111. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/ds-matcher.ts +111 -0
  112. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/font-matcher.ts +114 -0
  113. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/icon-resolver.ts +103 -0
  114. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/index.ts +1060 -0
  115. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/layout-segmenter.ts +18 -0
  116. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/token-inferencer.ts +39 -0
  117. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/vision-pipeline.ts +58 -0
  118. package/figma-intelligence-layer/src/tools/phase1-vision/sketch-to-design/index.ts +298 -0
  119. package/figma-intelligence-layer/src/tools/phase1-vision/visual-audit/index.ts +197 -0
  120. package/figma-intelligence-layer/src/tools/phase2-accuracy/component-audit/index.ts +494 -0
  121. package/figma-intelligence-layer/src/tools/phase2-accuracy/intent-translator/index.ts +356 -0
  122. package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/container-patterns.ts +123 -0
  123. package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/index.ts +663 -0
  124. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/built-in-rules.yaml +56 -0
  125. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/index.ts +614 -0
  126. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/rule-engine.ts +113 -0
  127. package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/color-theory.ts +178 -0
  128. package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/index.ts +470 -0
  129. package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/index.ts +429 -0
  130. package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/token-override-maps.ts +226 -0
  131. package/figma-intelligence-layer/src/tools/phase3-generation/ai-image-insert/index.ts +535 -0
  132. package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/index.ts +660 -0
  133. package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/pattern-fingerprints.ts +209 -0
  134. package/figma-intelligence-layer/src/tools/phase3-generation/composition-builder/index.ts +540 -0
  135. package/figma-intelligence-layer/src/tools/phase3-generation/figma-animated-build.ts +391 -0
  136. package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/index.ts +2019 -0
  137. package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/screen-templates.ts +131 -0
  138. package/figma-intelligence-layer/src/tools/phase3-generation/prototype-map/index.ts +381 -0
  139. package/figma-intelligence-layer/src/tools/phase3-generation/prototype-wire/index.ts +565 -0
  140. package/figma-intelligence-layer/src/tools/phase3-generation/swarm-build/index.ts +764 -0
  141. package/figma-intelligence-layer/src/tools/phase3-generation/system-drift/index.ts +535 -0
  142. package/figma-intelligence-layer/src/tools/phase3-generation/unsplash-search/index.ts +84 -0
  143. package/figma-intelligence-layer/src/tools/phase3-generation/url-to-frame/index.ts +401 -0
  144. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/css-animations.ts +68 -0
  145. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/framer-motion.ts +78 -0
  146. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/swift-animations.ts +93 -0
  147. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/index.ts +596 -0
  148. package/figma-intelligence-layer/src/tools/phase4-sync/ci-check/index.ts +462 -0
  149. package/figma-intelligence-layer/src/tools/phase4-sync/export-tokens/index.ts +1470 -0
  150. package/figma-intelligence-layer/src/tools/phase4-sync/generate-component-code/index.ts +829 -0
  151. package/figma-intelligence-layer/src/tools/phase4-sync/handoff-spec/index.ts +702 -0
  152. package/figma-intelligence-layer/src/tools/phase4-sync/icon-library-sync/index.ts +483 -0
  153. package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/index.ts +501 -0
  154. package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/storybook-parser.ts +106 -0
  155. package/figma-intelligence-layer/src/tools/phase4-sync/watch-docs/index.ts +676 -0
  156. package/figma-intelligence-layer/src/tools/phase4-sync/webhook-listener/index.ts +560 -0
  157. package/figma-intelligence-layer/src/tools/phase5-governance/apg-doc/index.ts +1043 -0
  158. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/component-detection.ts +620 -0
  159. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/anatomy.ts +331 -0
  160. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/color-tokens.ts +77 -0
  161. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/properties.ts +54 -0
  162. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/snapshot.ts +287 -0
  163. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/spacing.ts +71 -0
  164. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/states.ts +43 -0
  165. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/typography.ts +71 -0
  166. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/index.ts +221 -0
  167. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/_default.ts +166 -0
  168. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/accordion.ts +232 -0
  169. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/alert.ts +234 -0
  170. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar-group.ts +270 -0
  171. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar.ts +249 -0
  172. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/badge.ts +231 -0
  173. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/banner.ts +293 -0
  174. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/breadcrumb.ts +240 -0
  175. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/button.ts +243 -0
  176. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/calendar.ts +307 -0
  177. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/card.ts +143 -0
  178. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/checkbox.ts +227 -0
  179. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/chip.ts +233 -0
  180. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/combobox.ts +282 -0
  181. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/datepicker.ts +276 -0
  182. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/divider.ts +223 -0
  183. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/drawer.ts +255 -0
  184. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/dropdown-menu.ts +289 -0
  185. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/empty-state.ts +261 -0
  186. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/file-uploader.ts +290 -0
  187. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/form.ts +265 -0
  188. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/grid.ts +238 -0
  189. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/icon.ts +255 -0
  190. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/index.ts +128 -0
  191. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-edit.ts +286 -0
  192. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-message.ts +255 -0
  193. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/input.ts +330 -0
  194. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/link.ts +247 -0
  195. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/list.ts +250 -0
  196. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/menu.ts +247 -0
  197. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/modal.ts +144 -0
  198. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navbar.ts +264 -0
  199. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navigation.ts +251 -0
  200. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/number-input.ts +261 -0
  201. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/pagination.ts +248 -0
  202. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/popover.ts +270 -0
  203. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/progress.ts +251 -0
  204. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/radio.ts +142 -0
  205. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/range-slider.ts +282 -0
  206. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/rating.ts +250 -0
  207. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/search.ts +258 -0
  208. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/segmented-control.ts +265 -0
  209. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/select.ts +319 -0
  210. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/skeleton.ts +256 -0
  211. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/slider.ts +232 -0
  212. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/spinner.ts +239 -0
  213. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/status-dot.ts +252 -0
  214. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/stepper.ts +270 -0
  215. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/table.ts +244 -0
  216. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tabs.ts +143 -0
  217. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tag.ts +243 -0
  218. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/textarea.ts +259 -0
  219. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/time-picker.ts +293 -0
  220. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toast.ts +144 -0
  221. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toggle.ts +289 -0
  222. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toolbar.ts +267 -0
  223. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tooltip.ts +232 -0
  224. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/treeview.ts +257 -0
  225. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/typography.ts +319 -0
  226. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/legacy-compat.ts +121 -0
  227. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/anatomy-diagram.ts +430 -0
  228. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/figma-page.ts +312 -0
  229. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/json.ts +129 -0
  230. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/markdown.ts +78 -0
  231. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/visual-doc.ts +2333 -0
  232. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/accessibility.ts +100 -0
  233. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/anatomy.ts +32 -0
  234. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/color-tokens.ts +59 -0
  235. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/content-guidance.ts +18 -0
  236. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/design-tokens.ts +53 -0
  237. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/interaction-rules.ts +19 -0
  238. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/overview.ts +91 -0
  239. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/properties-api.ts +71 -0
  240. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/qa-criteria.ts +19 -0
  241. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/related-components.ts +110 -0
  242. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/responsive.ts +19 -0
  243. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/size-specs.ts +67 -0
  244. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/spacing-structure.ts +58 -0
  245. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/state-specs.ts +79 -0
  246. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/states.ts +50 -0
  247. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/type-hierarchy.ts +33 -0
  248. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/typography.ts +55 -0
  249. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/usage-guidelines.ts +73 -0
  250. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/variants.ts +81 -0
  251. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/types.ts +409 -0
  252. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/index.ts +198 -0
  253. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/renderer.ts +701 -0
  254. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/types.ts +88 -0
  255. package/figma-intelligence-layer/src/tools/phase5-governance/decision-log/index.ts +135 -0
  256. package/figma-intelligence-layer/src/tools/phase5-governance/design-decision-log/index.ts +491 -0
  257. package/figma-intelligence-layer/src/tools/phase5-governance/ds-primitives/index.ts +416 -0
  258. package/figma-intelligence-layer/src/tools/phase5-governance/ds-scaffolder/index.ts +722 -0
  259. package/figma-intelligence-layer/src/tools/phase5-governance/ds-variables/index.ts +449 -0
  260. package/figma-intelligence-layer/src/tools/phase5-governance/health-report/index.ts +393 -0
  261. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/index.ts +406 -0
  262. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/figma-page.ts +292 -0
  263. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/json.ts +24 -0
  264. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/markdown.ts +172 -0
  265. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/naming-guide.ts +409 -0
  266. package/figma-intelligence-layer/src/tools/phase5-governance/token-analytics/index.ts +594 -0
  267. package/figma-intelligence-layer/src/tools/phase5-governance/token-docs/index.ts +710 -0
  268. package/figma-intelligence-layer/src/tools/phase5-governance/token-migrate/index.ts +458 -0
  269. package/figma-intelligence-layer/src/tools/phase5-governance/token-naming/index.ts +134 -0
  270. package/figma-intelligence-layer/tests/apg-doc.test.ts +101 -0
  271. package/figma-intelligence-layer/tests/design-system-context.test.ts +152 -0
  272. package/figma-intelligence-layer/tests/design-system-matcher.test.ts +144 -0
  273. package/figma-intelligence-layer/tests/figma-bridge.test.ts +83 -0
  274. package/figma-intelligence-layer/tests/generate-image-and-insert.test.ts +56 -0
  275. package/figma-intelligence-layer/tests/screen-cloner-regression.test.ts +69 -0
  276. package/figma-intelligence-layer/tests/smoke.test.ts +174 -0
  277. package/figma-intelligence-layer/tests/spec-generator.test.ts +127 -0
  278. package/figma-intelligence-layer/tests/token-migrate.test.ts +21 -0
  279. package/figma-intelligence-layer/tests/token-naming.test.ts +30 -0
  280. package/figma-intelligence-layer/tsconfig.json +19 -0
  281. package/package.json +35 -0
  282. package/scripts/clean-existing-chunks.js +179 -0
  283. package/scripts/connect-ai-tool.js +490 -0
  284. package/scripts/convert-hub-pdfs.js +425 -0
  285. package/scripts/figma-mcp-status.js +349 -0
  286. package/scripts/register-codex-mcp.js +96 -0
@@ -0,0 +1,2363 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * figma-bridge-relay β€” Local WebSocket relay server
4
+ *
5
+ * Architecture:
6
+ * MCP Server (figma-bridge.ts) β†’ connects to ws://localhost:PORT as a client
7
+ * Figma Plugin UI (ui.html) β†’ connects to ws://localhost:PORT/plugin as a client
8
+ * Chat (plugin UI) β†’ sends { type:"chat" } β†’ relay spawns claude subprocess
9
+ *
10
+ * Usage:
11
+ * node bridge-relay.js # default port 9001
12
+ * node bridge-relay.js 9002 # custom port
13
+ * BRIDGE_PORT=9001 node bridge-relay.js
14
+ */
15
+
16
+ const { WebSocketServer } = require("ws");
17
+ const { spawn } = require("child_process");
18
+ const { readFileSync, writeFileSync, appendFileSync, existsSync } = require("fs");
19
+ const { homedir } = require("os");
20
+ const { join, resolve } = require("path");
21
+ const { runClaude, resetSession, isClaudeAvailable, getClaudeAuthInfo, writeMcpConfig } = require("./chat-runner");
22
+ const { runCodex, isCodexAvailable, getCodexAuthInfo, resetCodexSession } = require("./codex-runner");
23
+ const { runGemini } = require("./gemini-runner");
24
+ const { runGeminiCli, isGeminiCliAvailable, getGeminiCliAuthInfo } = require("./gemini-cli-runner");
25
+ const { runPerplexity } = require("./perplexity-runner");
26
+ const { runStitch } = require("./stitch-runner");
27
+ const { startStitchAuth, getStitchAccessToken, hasStitchAuth, getStitchEmail, clearStitchAuth } = require("./stitch-auth");
28
+ const { runAnthropicChat } = require("./anthropic-chat-runner");
29
+ const { parsePdfBuffer, parseDocxBuffer, fetchUrlContent, createContentSource, createChunkedContentSource, buildGroundingContext, scanKnowledgeHub, loadHubFile, searchHub, searchContentForAnswer, searchReferenceSites, getReferenceSites, addReferenceSite, removeReferenceSite, prewarmHub } = require("./content-context");
30
+
31
+ // ── Sync Figma Design System β†’ Stitch design.md ────────────────────────────
32
+ const { mkdirSync } = require("fs"); // readFileSync, writeFileSync, existsSync already imported above
33
+ const STITCH_DIR = join(homedir(), ".claude", "stitch");
34
+
35
+ function isSyncDesignIntent(message) {
36
+ const m = message.toLowerCase();
37
+ return /sync\s+(figma\s+)?design\s*(system|tokens|variables)?|export\s+(figma\s+)?design\s*(system|tokens|variables)?.*stitch|figma\s+variables?\s+to\s+stitch|push\s+design\s*(system)?\s+to\s+stitch|create\s+design\s*(system)?\s+(from|using)\s+figma|figma\s+to\s+stitch\s+design|design\s+system\s+sync/.test(m);
38
+ }
39
+
40
+ function isImportVariablesIntent(message, attachments) {
41
+ const m = message.toLowerCase();
42
+ // Check for explicit "convert/import to figma variables" intent
43
+ if (/convert\s+(these?\s+)?(to\s+)?figma\s+variables|import\s+(these?\s+)?(as\s+)?figma\s+variables|create\s+(figma\s+)?variables\s+(from|using)|make\s+(these?\s+)?figma\s+variables|to\s+figma\s+variables|\.md\s+to\s+(figma\s+)?variables|design\s+tokens?\s+to\s+figma|variables?\s+from\s+(this|the)\s+(file|md|markdown)/.test(m)) {
44
+ return true;
45
+ }
46
+ // If there's an .md attachment and the message explicitly mentions variables/tokens
47
+ if (attachments?.length && attachments.some(a => /\.md$/i.test(a.name))) {
48
+ return /variables|tokens|import\s+(as\s+)?variables|convert\s+(to\s+)?variables|\.md\s+to\s+variables/.test(m);
49
+ }
50
+ return false;
51
+ }
52
+
53
+ // ── Parse design.md β†’ structured variable data ──────────────────────────────
54
+
55
+ function parseDesignMd(mdContent) {
56
+ const collections = [];
57
+ let currentCollection = null;
58
+ let currentSection = null;
59
+
60
+ // Normalize line endings
61
+ const lines = mdContent.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
62
+
63
+ for (const line of lines) {
64
+ const trimmed = line.trim();
65
+
66
+ // ## Collection Name (but not ### which is a sub-section)
67
+ const colMatch = trimmed.match(/^##(?!#)\s+(.+)$/);
68
+ if (colMatch) {
69
+ const name = colMatch[1].trim();
70
+ // Skip non-variable sections
71
+ if (/^(Paint Styles|Typography|Usage Guide)/i.test(name)) {
72
+ currentCollection = null;
73
+ continue;
74
+ }
75
+ currentCollection = { name, variables: [] };
76
+ collections.push(currentCollection);
77
+ currentSection = null;
78
+ continue;
79
+ }
80
+
81
+ // ### Section Name (Colors, Strings, Toggles, or a FLOAT sub-group)
82
+ const secMatch = trimmed.match(/^###\s+(.+)$/);
83
+ if (secMatch) {
84
+ currentSection = secMatch[1].trim();
85
+ continue;
86
+ }
87
+
88
+ // Variable line formats:
89
+ // - **name**: `value` (standard)
90
+ // - **name**: value (no backticks)
91
+ // - **name**: β†’ `alias` (alias)
92
+ // - * **name**: value (asterisk list)
93
+ // Also handle lines starting with "- " or "* " or "- [ ]" etc
94
+ const varMatch = trimmed.match(/^[-*]\s+\*\*(.+?)\*\*:\s*(.+)$/);
95
+ if (varMatch && currentCollection) {
96
+ const varName = varMatch[1].trim();
97
+ const rawValue = varMatch[2].trim();
98
+
99
+ const parsed = parseVariableValue(rawValue, currentSection);
100
+ currentCollection.variables.push({
101
+ name: varName,
102
+ ...parsed,
103
+ });
104
+ continue;
105
+ }
106
+
107
+ // Fallback: lines like "name: value" under a collection (no bold)
108
+ // e.g., "color/primary: #FF0000" or " spacing/sm: 8px"
109
+ const plainMatch = trimmed.match(/^[-*]?\s*([a-zA-Z][\w/.-]+)\s*:\s*(.+)$/);
110
+ if (plainMatch && currentCollection && !trimmed.startsWith("#") && !trimmed.startsWith(">")) {
111
+ const varName = plainMatch[1].trim();
112
+ const rawValue = plainMatch[2].trim();
113
+ const parsed = parseVariableValue(rawValue, currentSection);
114
+ currentCollection.variables.push({
115
+ name: varName,
116
+ ...parsed,
117
+ });
118
+ }
119
+ }
120
+
121
+ // If the standard parser found 0 variables, try the Stitch narrative parser
122
+ const totalVars = collections.reduce((sum, c) => sum + c.variables.length, 0);
123
+ if (totalVars === 0) {
124
+ return parseStitchNarrative(mdContent);
125
+ }
126
+
127
+ return collections;
128
+ }
129
+
130
+ /**
131
+ * Parse a Stitch-generated design system narrative (.md) into variable collections.
132
+ * The narrative format embeds colors, fonts, and spacing inline in prose like:
133
+ * `primary` (#b20070), `surface` (#f9f9ff), etc.
134
+ * Or in labeled lists:
135
+ * * **Primary (#b10075):** Used for critical actions
136
+ *
137
+ * Generates 3 collections: Primitives, Semantic, Component
138
+ */
139
+ function parseStitchNarrative(mdContent) {
140
+ const primitives = { name: "Primitives", variables: [] };
141
+ const semantic = { name: "Semantic", variables: [] };
142
+ const component = { name: "Component", variables: [] };
143
+
144
+ // Track seen variable names to avoid duplicates
145
+ const seen = new Set();
146
+
147
+ function addVar(collection, name, value) {
148
+ if (seen.has(name)) return;
149
+ seen.add(name);
150
+ collection.variables.push({ name, ...value });
151
+ }
152
+
153
+ // ── Extract named colors from inline patterns ──
154
+ // Pattern: `token_name` (#hexval) or `token_name` (#hexval)
155
+ const inlineColorRe = /[`"](\w[\w_]*)[`"]\s*\(?\s*#([0-9a-fA-F]{6,8})\s*\)?/g;
156
+ let match;
157
+ while ((match = inlineColorRe.exec(mdContent)) !== null) {
158
+ const name = "color/" + match[1].replace(/_/g, "/");
159
+ const hex = "#" + match[2];
160
+ addVar(primitives, name, { type: "COLOR", value: hexToRgb(hex, 1), rawValue: hex });
161
+ }
162
+
163
+ // Pattern: **`token_name`** (#hexval) or **token_name** (#hexval) or **Name (#hexval)**
164
+ const boldColorRe = /\*\*`?(\w[\w_/]*)`?\*\*\s*\(?#([0-9a-fA-F]{6,8})\)?/g;
165
+ while ((match = boldColorRe.exec(mdContent)) !== null) {
166
+ const name = "color/" + match[1].replace(/_/g, "/").toLowerCase();
167
+ const hex = "#" + match[2];
168
+ addVar(primitives, name, { type: "COLOR", value: hexToRgb(hex, 1), rawValue: hex });
169
+ }
170
+ // Pattern: **Name (#hexval)** or **Name (#hexval):**
171
+ const boldParenColorRe = /\*\*(\w[\w_/\s]*?)\s*\(#([0-9a-fA-F]{6,8})\)/g;
172
+ while ((match = boldParenColorRe.exec(mdContent)) !== null) {
173
+ const raw = match[1].trim().replace(/\s+/g, "_").toLowerCase();
174
+ const name = "color/" + raw.replace(/_/g, "/");
175
+ const hex = "#" + match[2];
176
+ addVar(primitives, name, { type: "COLOR", value: hexToRgb(hex, 1), rawValue: hex });
177
+ }
178
+
179
+ // Pattern: `token_name` (hex) in backtick-name format used in Stitch narratives
180
+ // e.g., `on_surface` (#25181e)
181
+ const backtickHexRe = /`([\w_/]+)`\s*\(?#([0-9a-fA-F]{6,8})\)?/g;
182
+ while ((match = backtickHexRe.exec(mdContent)) !== null) {
183
+ const name = "color/" + match[1].replace(/_/g, "/");
184
+ const hex = "#" + match[2];
185
+ addVar(primitives, name, { type: "COLOR", value: hexToRgb(hex, 1), rawValue: hex });
186
+ }
187
+
188
+ // Pattern: (#hexval) with preceding word as token name
189
+ // e.g., "primary (#b20070)" or "Primary: #b20070"
190
+ const wordHexRe = /(?:^|\s)([\w]+(?:[_/][\w]+)*)\s*[:=]?\s*\(?#([0-9a-fA-F]{6,8})\)?/gm;
191
+ while ((match = wordHexRe.exec(mdContent)) !== null) {
192
+ const word = match[1].toLowerCase();
193
+ // Skip generic words that aren't token names
194
+ if (/^(the|and|or|for|with|use|from|set|at|in|to|of|is|it|a|an|hex|rgb|hsl|css|html|style|color|rule|using)$/.test(word)) continue;
195
+ if (word.length < 3) continue;
196
+ const name = "color/" + word.replace(/_/g, "/");
197
+ const hex = "#" + match[2];
198
+ addVar(primitives, name, { type: "COLOR", value: hexToRgb(hex, 1), rawValue: hex });
199
+ }
200
+
201
+ // ── Build semantic aliases for common role-based names ──
202
+ // Map role names to their primitive counterparts
203
+ const roleMap = {
204
+ "primary": "color/primary",
205
+ "secondary": "color/secondary",
206
+ "tertiary": "color/tertiary",
207
+ "error": "color/error",
208
+ "surface": "color/surface",
209
+ "background": "color/background",
210
+ "on_primary": "color/on/primary",
211
+ "on_secondary": "color/on/secondary",
212
+ "on_surface": "color/on/surface",
213
+ "on_background": "color/on/background",
214
+ "on_error": "color/on/error",
215
+ "outline": "color/outline",
216
+ };
217
+
218
+ for (const [role, target] of Object.entries(roleMap)) {
219
+ const primName = "color/" + role.replace(/_/g, "/");
220
+ if (seen.has(primName)) {
221
+ const semName = "color/action/" + role.replace(/_/g, "/");
222
+ if (!seen.has(semName)) {
223
+ addVar(semantic, semName, { type: "ALIAS", aliasTarget: primName, rawValue: `β†’ ${primName}` });
224
+ }
225
+ }
226
+ }
227
+
228
+ // ── Extract typography ──
229
+ const fontRe = /(?:font|typeface|family)\s*[:=]?\s*["']?([A-Z][\w\s]*?)["']?\s*(?:\(|,|\.|;|\n)/gi;
230
+ const fonts = new Set();
231
+ while ((match = fontRe.exec(mdContent)) !== null) {
232
+ const font = match[1].trim();
233
+ if (font.length > 2 && font.length < 40 && !/^(The|This|That|For|With|And|Use|CSS|HTML|Style)$/i.test(font)) {
234
+ fonts.add(font);
235
+ }
236
+ }
237
+ let fontIdx = 0;
238
+ const fontRoles = ["sans", "mono", "serif", "display"];
239
+ for (const font of fonts) {
240
+ const role = fontRoles[fontIdx] || `font${fontIdx}`;
241
+ addVar(primitives, `fontFamily/${role}`, { type: "STRING", value: font, rawValue: font });
242
+ fontIdx++;
243
+ if (fontIdx >= 4) break;
244
+ }
245
+
246
+ // ── Extract spacing/radius values ──
247
+ const spacingRe = /(?:spacing|gap|padding|margin)\s*[:=]?\s*(\d+(?:\.\d+)?)\s*(px|rem)/gi;
248
+ const spacingValues = new Set();
249
+ while ((match = spacingRe.exec(mdContent)) !== null) {
250
+ const val = match[2] === "rem" ? parseFloat(match[1]) * 16 : parseFloat(match[1]);
251
+ spacingValues.add(val);
252
+ }
253
+ const sortedSpacing = [...spacingValues].sort((a, b) => a - b);
254
+ const spacingNames = ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"];
255
+ sortedSpacing.forEach((val, i) => {
256
+ const name = `spacing/${spacingNames[i] || i}`;
257
+ addVar(primitives, name, { type: "FLOAT", value: val, rawValue: `${val}px` });
258
+ });
259
+
260
+ // ── Extract radius ──
261
+ const radiusRe = /(?:radius|border-radius|rounded)\s*[:=]?\s*(\d+(?:\.\d+)?)\s*(px)/gi;
262
+ const radiusValues = new Set();
263
+ while ((match = radiusRe.exec(mdContent)) !== null) {
264
+ radiusValues.add(parseFloat(match[1]));
265
+ }
266
+ // Check for 0px radius mentions (common in Stitch narratives)
267
+ if (/0\s*px\s*(?:radius|border-radius)/i.test(mdContent) || /radius.*0\s*px/i.test(mdContent)) {
268
+ radiusValues.add(0);
269
+ }
270
+ const sortedRadius = [...radiusValues].sort((a, b) => a - b);
271
+ const radiusNames = ["none", "sm", "md", "lg", "xl", "full"];
272
+ sortedRadius.forEach((val, i) => {
273
+ const name = `radius/${radiusNames[i] || i}`;
274
+ addVar(primitives, name, { type: "FLOAT", value: val, rawValue: `${val}px` });
275
+ });
276
+
277
+ // Build result β€” only include collections that have variables
278
+ const result = [];
279
+ if (primitives.variables.length) result.push(primitives);
280
+ if (semantic.variables.length) result.push(semantic);
281
+ if (component.variables.length) result.push(component);
282
+
283
+ return result;
284
+ }
285
+
286
+ function parseVariableValue(rawValue, sectionName) {
287
+ // Strip backticks if present: `value` β†’ value
288
+ let clean = rawValue.replace(/^`|`$/g, "").trim();
289
+ // Also handle: `value` (extra text) β€” extract just the backtick content
290
+ const backtickMatch = rawValue.match(/`(.+?)`/);
291
+ if (backtickMatch) clean = backtickMatch[1];
292
+
293
+ // Alias: β†’ `some/variable/name` or β†’ some/variable/name
294
+ const aliasMatch = rawValue.match(/^β†’\s*`?(.+?)`?\s*$/);
295
+ if (aliasMatch && rawValue.includes("β†’")) {
296
+ return {
297
+ type: "ALIAS",
298
+ aliasTarget: aliasMatch[1].trim(),
299
+ rawValue,
300
+ };
301
+ }
302
+
303
+ // Color: #RRGGBB or #RRGGBBAA (with optional opacity note)
304
+ const hexMatch = clean.match(/^(#[0-9a-fA-F]{6,8})/);
305
+ if (hexMatch) {
306
+ const hex = hexMatch[1];
307
+ const opacityMatch = rawValue.match(/opacity:\s*(\d+)%/);
308
+ const opacity = opacityMatch ? parseInt(opacityMatch[1]) / 100 : 1;
309
+ return {
310
+ type: "COLOR",
311
+ value: hexToRgb(hex, opacity),
312
+ rawValue,
313
+ };
314
+ }
315
+
316
+ // RGB color: rgb(R, G, B) or rgba(R, G, B, A)
317
+ const rgbMatch = clean.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/);
318
+ if (rgbMatch) {
319
+ return {
320
+ type: "COLOR",
321
+ value: {
322
+ r: parseInt(rgbMatch[1]) / 255,
323
+ g: parseInt(rgbMatch[2]) / 255,
324
+ b: parseInt(rgbMatch[3]) / 255,
325
+ a: rgbMatch[4] ? parseFloat(rgbMatch[4]) : 1,
326
+ },
327
+ rawValue,
328
+ };
329
+ }
330
+
331
+ // Boolean: true or false
332
+ if (/^(true|false)$/i.test(clean)) {
333
+ return {
334
+ type: "BOOLEAN",
335
+ value: clean.toLowerCase() === "true",
336
+ rawValue,
337
+ };
338
+ }
339
+
340
+ // Float/number: NNpx or NN or NN% (only if it's purely numeric)
341
+ const numMatch = clean.match(/^(-?[\d.]+)\s*(?:px|rem|em|pt|%)?$/);
342
+ if (numMatch) {
343
+ return {
344
+ type: "FLOAT",
345
+ value: parseFloat(numMatch[1]),
346
+ rawValue,
347
+ };
348
+ }
349
+
350
+ // If section name hints at color, try harder
351
+ if (sectionName && /^colors?$/i.test(sectionName)) {
352
+ const hexInStr = clean.match(/#[0-9a-fA-F]{6,8}/);
353
+ if (hexInStr) return { type: "COLOR", value: hexToRgb(hexInStr[0], 1), rawValue };
354
+ }
355
+
356
+ // String fallback
357
+ return { type: "STRING", value: clean, rawValue };
358
+ }
359
+
360
+ function hexToRgb(hex, alpha = 1) {
361
+ hex = hex.replace("#", "");
362
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
363
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
364
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
365
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : alpha;
366
+ return { r, g, b, a };
367
+ }
368
+
369
+ // ── Import handler: .md β†’ Figma variables with aliasing ─────────────────────
370
+
371
+ async function handleImportVariables(requestId, message, attachments, onEvent) {
372
+ console.log(" πŸ“₯ Import variables: .md β†’ Figma");
373
+ // Debug: write to temp file so we can see logs regardless of which process runs
374
+ const _debugLog = (msg) => { try { appendFileSync("/tmp/import-vars-debug.log", `${new Date().toISOString()} ${msg}\n`); } catch {} console.log(msg); };
375
+ _debugLog(" πŸ“₯ handleImportVariables called");
376
+ _debugLog(` πŸ“₯ message: ${message?.slice(0, 200)}`);
377
+ _debugLog(` πŸ“₯ attachments: ${JSON.stringify((attachments || []).map(a => ({ name: a.name, type: a.type, dataLen: a.data?.length })))}`);
378
+
379
+ onEvent({ type: "phase_start", id: requestId, phase: "Parsing design tokens..." });
380
+
381
+ try {
382
+ // 1. Get the .md content from attachment or message code block
383
+ let mdContent = null;
384
+ let sourceName = "design.md";
385
+
386
+ // Check attachments first
387
+ if (attachments?.length) {
388
+ const mdFile = attachments.find(a => /\.md$/i.test(a.name));
389
+ if (mdFile) {
390
+ mdContent = mdFile.data;
391
+ sourceName = mdFile.name;
392
+ }
393
+ }
394
+
395
+ // If no attachment, look for a code block in the message
396
+ if (!mdContent) {
397
+ const codeBlockMatch = message.match(/```(?:\w*)\n([\s\S]+?)```/);
398
+ if (codeBlockMatch) {
399
+ mdContent = codeBlockMatch[1];
400
+ sourceName = "pasted content";
401
+ }
402
+ }
403
+
404
+ // If still nothing, check if there's a saved design.md to import
405
+ if (!mdContent) {
406
+ const { readdirSync } = require("fs");
407
+ const stitchDir = join(homedir(), ".claude", "stitch");
408
+ if (existsSync(stitchDir)) {
409
+ const dirs = readdirSync(stitchDir, { withFileTypes: true }).filter(d => d.isDirectory());
410
+ for (const dir of dirs) {
411
+ const mdPath = join(stitchDir, dir.name, "design.md");
412
+ if (existsSync(mdPath)) {
413
+ mdContent = readFileSync(mdPath, "utf8");
414
+ sourceName = `${dir.name}/design.md`;
415
+ break;
416
+ }
417
+ }
418
+ }
419
+ }
420
+
421
+ if (!mdContent) {
422
+ onEvent({
423
+ type: "text_delta",
424
+ id: requestId,
425
+ delta: "No design system file found. Please either:\n" +
426
+ "- Attach a `.md` file using the paperclip button\n" +
427
+ "- Paste the design tokens in a code block in your message\n" +
428
+ "- First run **\"sync figma design system\"** to export, then import\n",
429
+ });
430
+ onEvent({ type: "done", id: requestId, fullText: "" });
431
+ return;
432
+ }
433
+
434
+ // 2. Parse the markdown
435
+ _debugLog(` πŸ“₯ MD content length: ${mdContent.length}`);
436
+ _debugLog(` πŸ“₯ MD first 500 chars: ${mdContent.slice(0, 500)}`);
437
+ const parsed = parseDesignMd(mdContent);
438
+ const totalVars = parsed.reduce((sum, c) => sum + c.variables.length, 0);
439
+ _debugLog(` πŸ“₯ Parsed: ${parsed.length} collection(s), ${totalVars} variable(s)`);
440
+
441
+ if (parsed.length === 0 || totalVars === 0) {
442
+ onEvent({
443
+ type: "text_delta",
444
+ id: requestId,
445
+ delta: "Could not find any variables in the file. Make sure the .md file follows the design system format:\n\n" +
446
+ "```\n## Collection Name\n### Colors\n- **color/primary**: `#FF0000`\n### Spacing\n- **spacing/sm**: `8px`\n```\n",
447
+ });
448
+ onEvent({ type: "done", id: requestId, fullText: "" });
449
+ return;
450
+ }
451
+
452
+ console.log(` Parsed ${parsed.length} collection(s) with ${totalVars} variable(s) from ${sourceName}`);
453
+ onEvent({ type: "phase_start", id: requestId, phase: `Creating ${totalVars} variables in ${parsed.length} collection(s)...` });
454
+
455
+ // 3. Get existing Figma variables to check for duplicates and resolve aliases
456
+ let existingCollections = [];
457
+ try {
458
+ existingCollections = await requestFromPlugin("getVariables", { verbosity: "full" });
459
+ if (!Array.isArray(existingCollections)) existingCollections = [];
460
+ } catch (err) {
461
+ console.log(" Could not fetch existing variables:", err.message);
462
+ }
463
+
464
+ // Build a lookup: variable name β†’ variable id (for alias resolution)
465
+ const existingVarMap = new Map(); // name β†’ { id, collectionId }
466
+ for (const col of existingCollections) {
467
+ for (const v of (col.variables || [])) {
468
+ existingVarMap.set(v.name, { id: v.id, collectionId: col.id });
469
+ }
470
+ }
471
+
472
+ // Build existing collection name β†’ id lookup
473
+ const existingColMap = new Map();
474
+ for (const col of existingCollections) {
475
+ existingColMap.set(col.name.toLowerCase(), col);
476
+ }
477
+
478
+ // 4. Create collections and variables
479
+ const results = { created: 0, skipped: 0, aliased: 0, collections: 0, errors: [] };
480
+ // Track newly created variable names β†’ IDs for alias resolution within the import
481
+ const newVarMap = new Map(); // name β†’ { id, collectionId }
482
+ // Deferred aliases (need all variables created first)
483
+ const deferredAliases = [];
484
+
485
+ for (const col of parsed) {
486
+ let collectionId;
487
+ let modeId;
488
+
489
+ // Check if collection already exists
490
+ const existing = existingColMap.get(col.name.toLowerCase());
491
+ if (existing) {
492
+ collectionId = existing.id;
493
+ modeId = existing.modes?.[0]?.modeId;
494
+ console.log(` Using existing collection: ${col.name} (${collectionId})`);
495
+ } else {
496
+ // Create new collection
497
+ try {
498
+ const newCol = await requestFromPlugin("createVariableCollection", { name: col.name });
499
+ collectionId = newCol.id;
500
+ modeId = newCol.modes?.[0]?.modeId;
501
+ results.collections++;
502
+ console.log(` Created collection: ${col.name} (${collectionId})`);
503
+ } catch (err) {
504
+ results.errors.push(`Failed to create collection "${col.name}": ${err.message}`);
505
+ continue;
506
+ }
507
+ }
508
+
509
+ // Separate aliases from direct values
510
+ const directVars = [];
511
+ const aliasVars = [];
512
+
513
+ for (const v of col.variables) {
514
+ // Skip if variable already exists
515
+ if (existingVarMap.has(v.name)) {
516
+ results.skipped++;
517
+ // Still record it for alias resolution
518
+ const ev = existingVarMap.get(v.name);
519
+ newVarMap.set(v.name, { id: ev.id, collectionId: ev.collectionId });
520
+ continue;
521
+ }
522
+
523
+ if (v.type === "ALIAS") {
524
+ aliasVars.push(v);
525
+ } else {
526
+ directVars.push(v);
527
+ }
528
+ }
529
+
530
+ // Batch-create direct (non-alias) variables
531
+ if (directVars.length > 0) {
532
+ const specs = directVars.map(v => ({
533
+ collectionId,
534
+ name: v.name,
535
+ resolvedType: v.type,
536
+ valuesByMode: modeId ? { [modeId]: v.value } : undefined,
537
+ }));
538
+
539
+ try {
540
+ const batchResult = await requestFromPlugin("batchCreateVariables", { variables: specs });
541
+ results.created += batchResult.created || specs.length;
542
+
543
+ // Record newly created variable IDs
544
+ if (batchResult.variables) {
545
+ for (const nv of batchResult.variables) {
546
+ newVarMap.set(nv.name, { id: nv.id, collectionId });
547
+ }
548
+ }
549
+ } catch (err) {
550
+ // Fallback: create one-by-one
551
+ console.log(` Batch create failed, falling back to individual: ${err.message}`);
552
+ for (const spec of specs) {
553
+ try {
554
+ const result = await requestFromPlugin("createVariable", spec);
555
+ results.created++;
556
+ newVarMap.set(spec.name, { id: result.id, collectionId });
557
+ } catch (err2) {
558
+ results.errors.push(`"${spec.name}": ${err2.message}`);
559
+ }
560
+ }
561
+ }
562
+ }
563
+
564
+ // Queue alias variables for deferred creation
565
+ for (const v of aliasVars) {
566
+ deferredAliases.push({ ...v, collectionId, modeId });
567
+ }
568
+ }
569
+
570
+ // 5. Create alias variables (now that all targets should exist)
571
+ if (deferredAliases.length > 0) {
572
+ onEvent({ type: "phase_start", id: requestId, phase: `Setting up ${deferredAliases.length} alias references...` });
573
+
574
+ for (const alias of deferredAliases) {
575
+ const targetName = alias.aliasTarget;
576
+ const target = newVarMap.get(targetName) || existingVarMap.get(targetName);
577
+
578
+ if (!target) {
579
+ results.errors.push(`Alias "${alias.name}" β†’ "${targetName}": target not found`);
580
+ continue;
581
+ }
582
+
583
+ // Determine the resolved type from the target variable
584
+ // We need to look it up from existing or parsed data
585
+ let resolvedType = "COLOR"; // default
586
+ for (const col of parsed) {
587
+ for (const v of col.variables) {
588
+ if (v.name === targetName && v.type !== "ALIAS") {
589
+ resolvedType = v.type;
590
+ break;
591
+ }
592
+ }
593
+ }
594
+ // Also check existing variables
595
+ for (const col of existingCollections) {
596
+ for (const v of (col.variables || [])) {
597
+ if (v.name === targetName) {
598
+ resolvedType = v.resolvedType || v.type || resolvedType;
599
+ break;
600
+ }
601
+ }
602
+ }
603
+
604
+ try {
605
+ const result = await requestFromPlugin("createVariable", {
606
+ collectionId: alias.collectionId,
607
+ name: alias.name,
608
+ resolvedType,
609
+ valuesByMode: alias.modeId ? {
610
+ [alias.modeId]: { type: "VARIABLE_ALIAS", variableId: target.id },
611
+ } : undefined,
612
+ });
613
+ results.created++;
614
+ results.aliased++;
615
+ newVarMap.set(alias.name, { id: result.id, collectionId: alias.collectionId });
616
+ } catch (err) {
617
+ results.errors.push(`Alias "${alias.name}": ${err.message}`);
618
+ }
619
+ }
620
+ }
621
+
622
+ // 6. Report results
623
+ const report =
624
+ `Figma variables imported from **${sourceName}**!\n\n` +
625
+ `**Results:**\n` +
626
+ `- ${results.created} variable(s) created\n` +
627
+ (results.aliased ? `- ${results.aliased} alias reference(s) linked\n` : "") +
628
+ (results.skipped ? `- ${results.skipped} existing variable(s) skipped\n` : "") +
629
+ (results.collections ? `- ${results.collections} new collection(s) created\n` : "") +
630
+ (results.errors.length ? `\n**Warnings:**\n${results.errors.map(e => `- ${e}`).join("\n")}\n` : "") +
631
+ `\nYou can now use these variables in your Figma designs. Open the **Variables** panel to see them.\n`;
632
+
633
+ onEvent({ type: "text_delta", id: requestId, delta: report });
634
+ onEvent({ type: "done", id: requestId, fullText: `Imported ${results.created} variables` });
635
+
636
+ } catch (err) {
637
+ console.error(" ❌ Import variables failed:", err.message);
638
+ onEvent({ type: "error", id: requestId, error: `Import failed: ${err.message}` });
639
+ onEvent({ type: "done", id: requestId, fullText: "" });
640
+ }
641
+ }
642
+
643
+ async function handleSyncDesignSystem(requestId, message, onEvent) {
644
+ console.log(" πŸ”„ Sync design system: Figma β†’ Stitch");
645
+
646
+ onEvent({ type: "phase_start", id: requestId, phase: "Extracting Figma variables..." });
647
+
648
+ try {
649
+ // 1. Request variables from Figma plugin
650
+ // getVariables returns an ARRAY of collections, each with a .variables array
651
+ const varsResult = await requestFromPlugin("getVariables", { verbosity: "full" });
652
+
653
+ // varsResult is an array of collection objects
654
+ const collections = Array.isArray(varsResult) ? varsResult : [];
655
+ const totalVars = collections.reduce((sum, c) => sum + (c.variables || []).length, 0);
656
+
657
+ if (collections.length === 0 || totalVars === 0) {
658
+ onEvent({ type: "text_delta", id: requestId, delta: "No Figma variables found in this file. Create some variables first (colors, spacing, typography) and try again.\n" });
659
+ onEvent({ type: "done", id: requestId, fullText: "" });
660
+ return;
661
+ }
662
+
663
+ console.log(` Found ${collections.length} collection(s) with ${totalVars} variable(s)`);
664
+ onEvent({ type: "phase_start", id: requestId, phase: `Converting ${totalVars} variables to design system...` });
665
+
666
+ // 2. Also try to get paint/text/effect styles
667
+ // getStyles returns { paint: [...], text: [...], effect: [...] }
668
+ let paintStyles = [];
669
+ let textStyles = [];
670
+ try {
671
+ const stylesResult = await requestFromPlugin("getStyles", {});
672
+ if (stylesResult?.paint) paintStyles = stylesResult.paint;
673
+ if (stylesResult?.text) textStyles = stylesResult.text;
674
+ } catch (err) {
675
+ console.log(" Could not fetch styles:", err.message);
676
+ }
677
+
678
+ // 3. Convert to design.md
679
+ const designMd = figmaVariablesToDesignMd(collections, paintStyles, textStyles);
680
+
681
+ // 4. Extract project name from message or use default
682
+ let projectName = "Figma Intelligence";
683
+ const projMatch = message.match(/(?:for|to|in)\s+(?:project\s+)?["']?([^"',\n]+?)["']?\s*(?:project)?(?:\s*$|\s+(?:design|sync|push))/i);
684
+ if (projMatch) projectName = projMatch[1].trim();
685
+
686
+ // 5. Save design.md
687
+ const projDir = join(STITCH_DIR, projectName.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 60));
688
+ if (!existsSync(projDir)) mkdirSync(projDir, { recursive: true });
689
+ const designMdPath = join(projDir, "design.md");
690
+ writeFileSync(designMdPath, designMd, "utf8");
691
+
692
+ console.log(` βœ… Design system saved: ${designMdPath} (${designMd.length} chars)`);
693
+
694
+ // 6. Report back β€” emit full design.md as a downloadable code block
695
+ onEvent({
696
+ type: "text_delta",
697
+ id: requestId,
698
+ delta: `Design system synced from Figma to Stitch!\n\n` +
699
+ `**Extracted:**\n` +
700
+ `- ${collections.length} variable collection(s): ${collections.map(c => c.name).join(", ")}\n` +
701
+ `- ${totalVars} variable(s)\n` +
702
+ (paintStyles.length ? `- ${paintStyles.length} paint style(s)\n` : "") +
703
+ (textStyles.length ? `- ${textStyles.length} text style(s)\n` : "") +
704
+ `\n**Saved to:** \`${designMdPath}\`\n` +
705
+ `**Project:** "${projectName}"\n\n` +
706
+ `All future Stitch generations in "${projectName}" will automatically use these design tokens for visual consistency.\n\n` +
707
+ `---\n\n` +
708
+ `**design.md** (hover to copy or download):\n\n` +
709
+ "```download:design.md\n" + designMd + "\n```\n",
710
+ });
711
+ onEvent({ type: "done", id: requestId, fullText: `Design system synced β€” ${totalVars} variables` });
712
+
713
+ } catch (err) {
714
+ console.error(" ❌ Design system sync failed:", err.message);
715
+ onEvent({ type: "error", id: requestId, error: `Design system sync failed: ${err.message}` });
716
+ onEvent({ type: "done", id: requestId, fullText: "" });
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Convert Figma variables (collections + variables) to a design.md format
722
+ * that Stitch can use for consistent generation.
723
+ */
724
+ function figmaVariablesToDesignMd(collections, paintStyles, textStyles) {
725
+ const totalVars = collections.reduce((sum, c) => sum + (c.variables || []).length, 0);
726
+ const lines = [];
727
+
728
+ lines.push("# Design System β€” Figma Variables");
729
+ lines.push("");
730
+ lines.push(`> Auto-synced from Figma on ${new Date().toISOString().split("T")[0]}`);
731
+ lines.push(`> ${totalVars} variables across ${collections.length} collection(s)`);
732
+ lines.push("");
733
+
734
+ // Process each collection (each already has .variables array)
735
+ for (const col of collections) {
736
+ const colName = col.name || "Unnamed Collection";
737
+ lines.push(`## ${colName}`);
738
+ lines.push("");
739
+
740
+ // Group by resolved type
741
+ const byType = {};
742
+ for (const v of (col.variables || [])) {
743
+ const type = v.resolvedType || v.type || "OTHER";
744
+ if (!byType[type]) byType[type] = [];
745
+ byType[type].push(v);
746
+ }
747
+
748
+ // Colors
749
+ if (byType.COLOR) {
750
+ lines.push("### Colors");
751
+ lines.push("");
752
+ for (const v of byType.COLOR) {
753
+ const name = v.name || "unnamed";
754
+ const value = formatColorValue(v, collections);
755
+ lines.push(`- **${name}**: ${value}`);
756
+ }
757
+ lines.push("");
758
+ }
759
+
760
+ // Numbers (spacing, sizing, border-radius, etc.)
761
+ if (byType.FLOAT) {
762
+ // Sub-group by name prefix (e.g., spacing/sm, radius/md)
763
+ const subGroups = {};
764
+ for (const v of byType.FLOAT) {
765
+ const prefix = (v.name || "").split("/")[0] || "Values";
766
+ if (!subGroups[prefix]) subGroups[prefix] = [];
767
+ subGroups[prefix].push(v);
768
+ }
769
+
770
+ for (const [prefix, vars] of Object.entries(subGroups)) {
771
+ lines.push(`### ${prefix}`);
772
+ lines.push("");
773
+ for (const v of vars) {
774
+ const name = v.name || "unnamed";
775
+ const value = formatFloatValue(v, collections);
776
+ lines.push(`- **${name}**: ${value}`);
777
+ }
778
+ lines.push("");
779
+ }
780
+ }
781
+
782
+ // Strings (font families, etc.)
783
+ if (byType.STRING) {
784
+ lines.push("### Strings");
785
+ lines.push("");
786
+ for (const v of byType.STRING) {
787
+ const name = v.name || "unnamed";
788
+ const value = formatStringValue(v, collections);
789
+ lines.push(`- **${name}**: ${value}`);
790
+ }
791
+ lines.push("");
792
+ }
793
+
794
+ // Booleans
795
+ if (byType.BOOLEAN) {
796
+ lines.push("### Toggles");
797
+ lines.push("");
798
+ for (const v of byType.BOOLEAN) {
799
+ const name = v.name || "unnamed";
800
+ const value = formatBooleanValue(v, collections);
801
+ lines.push(`- **${name}**: ${value}`);
802
+ }
803
+ lines.push("");
804
+ }
805
+ }
806
+
807
+ // Paint styles (if available)
808
+ if (paintStyles.length > 0) {
809
+ lines.push("## Paint Styles");
810
+ lines.push("");
811
+ for (const s of paintStyles) {
812
+ const name = s.name || "unnamed";
813
+ const fills = (s.paints || []).map(p => {
814
+ if (p.type === "SOLID" && p.color) {
815
+ const { r, g, b } = p.color;
816
+ return rgbToHex(r, g, b);
817
+ }
818
+ return p.type || "gradient";
819
+ }).join(", ");
820
+ lines.push(`- **${name}**: ${fills}`);
821
+ }
822
+ lines.push("");
823
+ }
824
+
825
+ // Text styles (if available)
826
+ if (textStyles.length > 0) {
827
+ lines.push("## Typography");
828
+ lines.push("");
829
+ for (const s of textStyles) {
830
+ const name = s.name || "unnamed";
831
+ const family = s.fontName?.family || s.fontFamily || "";
832
+ const size = s.fontSize || "";
833
+ const weight = s.fontName?.style || s.fontWeight || "";
834
+ const lh = s.lineHeight?.value ? `/${s.lineHeight.value}${s.lineHeight.unit === "PERCENT" ? "%" : "px"}` : "";
835
+ lines.push(`- **${name}**: ${family} ${size}px${lh} ${weight}`);
836
+ }
837
+ lines.push("");
838
+ }
839
+
840
+ // Usage guidance for Stitch
841
+ lines.push("---");
842
+ lines.push("");
843
+ lines.push("## Usage Guide for Generation");
844
+ lines.push("");
845
+ lines.push("When generating UI screens, use the exact color values, spacing values, and typography");
846
+ lines.push("defined above. This ensures visual consistency between Figma designs and generated screens.");
847
+ lines.push("");
848
+ lines.push("- Use the COLOR variables for all backgrounds, text, borders, and accents");
849
+ lines.push("- Use the spacing/sizing FLOAT variables for padding, margins, gaps, and dimensions");
850
+ lines.push("- Match typography settings (font family, size, weight) to the styles above");
851
+ lines.push("- Maintain the design language: if colors are dark/muted, generate dark-themed UIs");
852
+ lines.push("");
853
+
854
+ return lines.join("\n");
855
+ }
856
+
857
+ function formatColorValue(v, collections) {
858
+ // Try to extract the color from valuesByMode or value
859
+ const modes = v.valuesByMode || {};
860
+ const firstMode = Object.values(modes)[0];
861
+ const val = firstMode || v.value;
862
+ if (val && typeof val === "object" && "r" in val) {
863
+ return `\`${rgbToHex(val.r, val.g, val.b)}\`${val.a !== undefined && val.a < 1 ? ` (opacity: ${Math.round(val.a * 100)}%)` : ""}`;
864
+ }
865
+ // Variable alias β€” show the referenced variable name
866
+ if (val && typeof val === "object" && val.type === "VARIABLE_ALIAS") {
867
+ const refName = findVariableName(val.id, collections);
868
+ return refName ? `β†’ \`${refName}\`` : `β†’ alias(${val.id})`;
869
+ }
870
+ if (typeof val === "string") return `\`${val}\``;
871
+ return JSON.stringify(val);
872
+ }
873
+
874
+ function findVariableName(varId, collections) {
875
+ for (const col of (collections || [])) {
876
+ for (const v of (col.variables || [])) {
877
+ if (v.id === varId) return v.name;
878
+ }
879
+ }
880
+ return null;
881
+ }
882
+
883
+ function formatFloatValue(v, collections) {
884
+ const modes = v.valuesByMode || {};
885
+ const firstMode = Object.values(modes)[0];
886
+ const val = firstMode !== undefined ? firstMode : v.value;
887
+ if (val && typeof val === "object" && val.type === "VARIABLE_ALIAS") {
888
+ const refName = findVariableName(val.id, collections);
889
+ return refName ? `β†’ \`${refName}\`` : `β†’ alias`;
890
+ }
891
+ return `\`${val}px\``;
892
+ }
893
+
894
+ function formatStringValue(v, collections) {
895
+ const modes = v.valuesByMode || {};
896
+ const firstMode = Object.values(modes)[0];
897
+ const val = firstMode || v.value;
898
+ if (val && typeof val === "object" && val.type === "VARIABLE_ALIAS") {
899
+ const refName = findVariableName(val.id, collections);
900
+ return refName ? `β†’ \`${refName}\`` : `β†’ alias`;
901
+ }
902
+ return `\`${val}\``;
903
+ }
904
+
905
+ function formatBooleanValue(v, collections) {
906
+ const modes = v.valuesByMode || {};
907
+ const firstMode = Object.values(modes)[0];
908
+ const val = firstMode !== undefined ? firstMode : v.value;
909
+ if (val && typeof val === "object" && val.type === "VARIABLE_ALIAS") {
910
+ const refName = findVariableName(val.id, collections);
911
+ return refName ? `β†’ \`${refName}\`` : `β†’ alias`;
912
+ }
913
+ return `\`${val}\``;
914
+ }
915
+
916
+ function rgbToHex(r, g, b) {
917
+ const toHex = (c) => {
918
+ const v = Math.round((typeof c === "number" && c <= 1 ? c * 255 : c));
919
+ return v.toString(16).padStart(2, "0");
920
+ };
921
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
922
+ }
923
+
924
+ // P3: Port fallback β€” try PORT, then PORT+1 through PORT+9
925
+ const BASE_PORT = parseInt(process.argv[2] || process.env.BRIDGE_PORT || "9001", 10);
926
+ let PORT = BASE_PORT;
927
+ const MCP_SERVER_PATH = resolve(__dirname, "../figma-intelligence-layer/dist/index.js");
928
+ const DEFAULT_CODEX_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
929
+
930
+ if (!process.env.CODEX_BIN_PATH && existsSync(DEFAULT_CODEX_APP_BIN)) {
931
+ process.env.CODEX_BIN_PATH = DEFAULT_CODEX_APP_BIN;
932
+ }
933
+
934
+ function readMcpEnv() {
935
+ try {
936
+ const settingsPath = join(homedir(), ".claude", "settings.json");
937
+ if (existsSync(settingsPath)) {
938
+ const s = JSON.parse(readFileSync(settingsPath, "utf8"));
939
+ const figmaEnv = s?.mcpServers?.["figma-intelligence-layer"]?.env || {};
940
+ const bridgeEnv = s?.mcpServers?.["design-bridge"]?.env || {};
941
+ return {
942
+ // Merge figma-intelligence-layer env (has UNSPLASH, GEMINI, ANTHROPIC keys etc.)
943
+ ...figmaEnv,
944
+ // Pull Stitch/Unsplash/Pexels from design-bridge as a fallback
945
+ ...(bridgeEnv.UNSPLASH_ACCESS_KEY && !figmaEnv.UNSPLASH_ACCESS_KEY
946
+ ? { UNSPLASH_ACCESS_KEY: bridgeEnv.UNSPLASH_ACCESS_KEY } : {}),
947
+ ...(bridgeEnv.PEXELS_API_KEY && !figmaEnv.PEXELS_API_KEY
948
+ ? { PEXELS_API_KEY: bridgeEnv.PEXELS_API_KEY } : {}),
949
+ ...(bridgeEnv.STITCH_API_KEY && !figmaEnv.STITCH_API_KEY
950
+ ? { STITCH_API_KEY: bridgeEnv.STITCH_API_KEY } : {}),
951
+ ...(bridgeEnv.GOOGLE_CLOUD_PROJECT && !figmaEnv.GOOGLE_CLOUD_PROJECT
952
+ ? { GOOGLE_CLOUD_PROJECT: bridgeEnv.GOOGLE_CLOUD_PROJECT } : {}),
953
+ };
954
+ }
955
+ } catch {}
956
+ return {};
957
+ }
958
+
959
+ let _mcpProc = null;
960
+ function startPersistentMcpServer() {
961
+ if (!existsSync(MCP_SERVER_PATH)) {
962
+ console.log("⚠ MCP server not built β€” run setup.sh");
963
+ return;
964
+ }
965
+ const savedEnv = readMcpEnv();
966
+ _mcpProc = spawn("node", [MCP_SERVER_PATH], {
967
+ stdio: ["ignore", "pipe", "pipe"],
968
+ env: {
969
+ ...process.env,
970
+ ...savedEnv,
971
+ FIGMA_BRIDGE_PORT: String(PORT),
972
+ ENABLE_DECISION_LOG: "true",
973
+ },
974
+ });
975
+ _mcpProc.stderr.on("data", (d) => {
976
+ const t = d.toString().trim();
977
+ if (t) console.log("[mcp]", t);
978
+ });
979
+ _mcpProc.on("close", (code) => {
980
+ _mcpProc = null;
981
+ if (code !== 0 && code !== null) {
982
+ console.log("⚠ MCP server exited β€” restarting in 3s…");
983
+ setTimeout(startPersistentMcpServer, 3000);
984
+ }
985
+ });
986
+ _mcpProc.on("error", () => {
987
+ _mcpProc = null;
988
+ setTimeout(startPersistentMcpServer, 3000);
989
+ });
990
+ }
991
+
992
+ let pluginSocket = null;
993
+ const mcpSockets = new Set();
994
+ const vscodeSockets = new Set(); // VS Code chat extension clients
995
+ const pendingRequests = new Map();
996
+ const activeChatProcesses = new Map(); // requestId β†’ ChildProcess | EventEmitter
997
+
998
+ // Auth info populated on startup and sent to plugin on connect
999
+ let authInfo = { loggedIn: false, email: null };
1000
+ let openaiAuthInfo = { loggedIn: false, email: null };
1001
+ let geminiCliAuthInfo = { loggedIn: false, email: null };
1002
+
1003
+ // TTL cache for auth refresh β€” avoid spawning auth subprocesses on every plugin connect
1004
+ const AUTH_REFRESH_TTL_MS = 5 * 60 * 1000; // 5 minutes
1005
+ let _lastAuthRefresh = 0;
1006
+ let _authRefreshInFlight = null;
1007
+
1008
+ // ── Active design system ─────────────────────────────────────────────────────
1009
+ let activeDesignSystemId = null;
1010
+
1011
+ // ── Component Doc Generator chooser state ────────────────────────────────────
1012
+ // When the chooser is shown, we stash the original message (with Figma link)
1013
+ // so when the user picks a type, we can prepend it to the follow-up.
1014
+ let pendingDocGenChooser = null; // { originalMessage: string, shownAt: number }
1015
+
1016
+ // ── Provider config (persisted to ~/.claude/settings.json) ───────────────────
1017
+ let providerConfig = { provider: "claude", apiKey: null, projectId: null };
1018
+
1019
+ function loadProviderConfig() {
1020
+ try {
1021
+ const settingsPath = join(homedir(), ".claude", "settings.json");
1022
+ if (existsSync(settingsPath)) {
1023
+ const s = JSON.parse(readFileSync(settingsPath, "utf8"));
1024
+ const saved = s?.figmaIntelligenceProvider;
1025
+ if (saved?.provider) {
1026
+ providerConfig = { provider: saved.provider, apiKey: saved.apiKey || null, projectId: saved.projectId || null };
1027
+ console.log(` Provider loaded: ${providerConfig.provider}`);
1028
+ }
1029
+ }
1030
+ } catch {}
1031
+ }
1032
+
1033
+ function saveProviderConfig() {
1034
+ try {
1035
+ const settingsPath = join(homedir(), ".claude", "settings.json");
1036
+ let settings = {};
1037
+ if (existsSync(settingsPath)) {
1038
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch {}
1039
+ }
1040
+ settings.figmaIntelligenceProvider = {
1041
+ provider: providerConfig.provider,
1042
+ apiKey: providerConfig.apiKey || null,
1043
+ projectId: providerConfig.projectId || null,
1044
+ };
1045
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1046
+ } catch (err) {
1047
+ console.error(" ⚠ Could not save provider config:", err.message);
1048
+ }
1049
+ }
1050
+
1051
+ loadProviderConfig();
1052
+
1053
+ // ── Anthropic API Key (for fast chat mode β€” Tier 3) ─────────────────────────
1054
+ function getAnthropicApiKey() {
1055
+ // 1. Provider-level API key (set via UI)
1056
+ if (providerConfig.apiKey && providerConfig.provider === "claude") return providerConfig.apiKey;
1057
+ // 2. Environment variable
1058
+ if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
1059
+ // 3. From settings file
1060
+ try {
1061
+ const settingsPath = join(homedir(), ".claude", "settings.json");
1062
+ if (existsSync(settingsPath)) {
1063
+ const s = JSON.parse(readFileSync(settingsPath, "utf8"));
1064
+ if (s?.figmaIntelligenceProvider?.anthropicApiKey) return s.figmaIntelligenceProvider.anthropicApiKey;
1065
+ }
1066
+ } catch {}
1067
+ return null;
1068
+ }
1069
+
1070
+ // ── Knowledge sources grounding context ─────────────────────────────────────
1071
+ const activeContentSources = new Map(); // sourceId β†’ { id, title, sources, meta, extractedAt }
1072
+
1073
+ async function refreshAuthState({ log = false, force = false } = {}) {
1074
+ // Return cached auth if within TTL (unless forced or startup log)
1075
+ const now = Date.now();
1076
+ if (!force && !log && (now - _lastAuthRefresh) < AUTH_REFRESH_TTL_MS) {
1077
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1078
+ return;
1079
+ }
1080
+ // Deduplicate concurrent refresh calls
1081
+ if (_authRefreshInFlight) {
1082
+ await _authRefreshInFlight;
1083
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1084
+ return;
1085
+ }
1086
+ _authRefreshInFlight = _doRefreshAuthState({ log });
1087
+ try {
1088
+ await _authRefreshInFlight;
1089
+ _lastAuthRefresh = Date.now();
1090
+ } finally {
1091
+ _authRefreshInFlight = null;
1092
+ }
1093
+ }
1094
+
1095
+ async function _doRefreshAuthState({ log = false } = {}) {
1096
+ const claudeAvailable = await isClaudeAvailable();
1097
+ if (claudeAvailable) {
1098
+ authInfo = await getClaudeAuthInfo();
1099
+ if (log) {
1100
+ if (authInfo.loggedIn) {
1101
+ console.log(`βœ… Claude: logged in${authInfo.email ? " as " + authInfo.email : ""}`);
1102
+ } else {
1103
+ console.log("⚠ Claude: not logged in β€” run 'claude login'");
1104
+ }
1105
+ }
1106
+ } else {
1107
+ authInfo = { loggedIn: false, email: null };
1108
+ if (log) console.log("⚠ Claude CLI not found β€” Claude chat unavailable");
1109
+ }
1110
+
1111
+ const codexAvailable = await isCodexAvailable();
1112
+ if (codexAvailable) {
1113
+ openaiAuthInfo = await getCodexAuthInfo();
1114
+ if (log) {
1115
+ if (openaiAuthInfo.loggedIn) {
1116
+ console.log(`βœ… OpenAI Codex: logged in${openaiAuthInfo.email ? " as " + openaiAuthInfo.email : ""}`);
1117
+ } else {
1118
+ console.log("⚠ OpenAI Codex: not logged in β€” run 'codex login'");
1119
+ }
1120
+ }
1121
+ } else {
1122
+ openaiAuthInfo = { loggedIn: false, email: null };
1123
+ if (log) console.log("⚠ OpenAI Codex CLI not found β€” run: npm install -g @openai/codex");
1124
+ }
1125
+
1126
+ const geminiCliAvailable = await isGeminiCliAvailable();
1127
+ if (geminiCliAvailable) {
1128
+ geminiCliAuthInfo = await getGeminiCliAuthInfo();
1129
+ if (log) {
1130
+ if (geminiCliAuthInfo.loggedIn) {
1131
+ console.log(`βœ… Gemini CLI: logged in${geminiCliAuthInfo.email ? " as " + geminiCliAuthInfo.email : ""}`);
1132
+ } else {
1133
+ console.log("⚠ Gemini CLI: not logged in β€” run 'gemini auth login'");
1134
+ }
1135
+ }
1136
+ } else {
1137
+ geminiCliAuthInfo = { loggedIn: false, email: null };
1138
+ if (log) console.log("β„Ή Gemini CLI not found β€” Gemini will use API key mode (install: npm install -g @google/gemini-cli)");
1139
+ }
1140
+
1141
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1142
+ // Also send status to all VS Code clients
1143
+ for (const vsWs of vscodeSockets) {
1144
+ sendRelayStatus(vsWs, hasConnectedMcpSocket());
1145
+ }
1146
+ }
1147
+
1148
+ // ── Auth check on startup ───────────────────────────────────────────────────
1149
+ (async () => {
1150
+ await refreshAuthState({ log: true, force: true });
1151
+ })();
1152
+
1153
+ // ── Helpers ─────────────────────────────────────────────────────────────────
1154
+ function sendRelayStatus(ws, mcpConnected) {
1155
+ if (!ws || ws.readyState !== 1) return;
1156
+ ws.send(JSON.stringify({
1157
+ type: "bridge-status",
1158
+ mcpConnected,
1159
+ claudeLoggedIn: authInfo.loggedIn,
1160
+ claudeEmail: authInfo.email,
1161
+ openaiLoggedIn: openaiAuthInfo.loggedIn,
1162
+ openaiEmail: openaiAuthInfo.email,
1163
+ provider: providerConfig.provider,
1164
+ hasApiKey: !!(providerConfig.apiKey),
1165
+ hasStitchOAuth: hasStitchAuth(),
1166
+ stitchEmail: getStitchEmail(),
1167
+ geminiLoggedIn: geminiCliAuthInfo.loggedIn,
1168
+ geminiEmail: geminiCliAuthInfo.email,
1169
+ activeDesignSystemId,
1170
+ hasAnthropicKey: !!getAnthropicApiKey(),
1171
+ referenceSites: getReferenceSites(),
1172
+ knowledgeSources: Array.from(activeContentSources.values()).map(s => ({
1173
+ id: s.id, title: s.title, sourceCount: s.sources.length, meta: s.meta || {}, extractedAt: s.extractedAt,
1174
+ })),
1175
+ }));
1176
+ }
1177
+
1178
+ function hasConnectedMcpSocket() {
1179
+ for (const socket of mcpSockets) {
1180
+ if (socket.readyState === 1) return true;
1181
+ }
1182
+ return false;
1183
+ }
1184
+
1185
+ function broadcastToMcpSockets(raw) {
1186
+ for (const socket of mcpSockets) {
1187
+ if (socket.readyState === 1) {
1188
+ socket.send(raw);
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ function sendToPlugin(payload) {
1194
+ if (pluginSocket && pluginSocket.readyState === 1) {
1195
+ pluginSocket.send(JSON.stringify(payload));
1196
+ }
1197
+ }
1198
+
1199
+ // ── Relay-initiated plugin requests (for sync design system etc.) ──────────
1200
+ const pendingRelayRequests = new Map();
1201
+
1202
+ /**
1203
+ * Send a bridge-request to the Figma plugin and wait for the response.
1204
+ * Returns a Promise that resolves with the result or rejects on error/timeout.
1205
+ */
1206
+ function requestFromPlugin(method, params, timeoutMs = 15000) {
1207
+ return new Promise((resolve, reject) => {
1208
+ if (!pluginSocket || pluginSocket.readyState !== 1) {
1209
+ reject(new Error("Figma plugin not connected"));
1210
+ return;
1211
+ }
1212
+ const id = `relay-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
1213
+ const timer = setTimeout(() => {
1214
+ pendingRelayRequests.delete(id);
1215
+ reject(new Error(`Plugin request "${method}" timed out`));
1216
+ }, timeoutMs);
1217
+
1218
+ pendingRelayRequests.set(id, { resolve, reject, timer });
1219
+ sendToPlugin({ type: "bridge-request", id, method, params: params || {} });
1220
+ });
1221
+ }
1222
+
1223
+ function sendToVscode(payload, targetWs) {
1224
+ if (targetWs && targetWs.readyState === 1) {
1225
+ targetWs.send(JSON.stringify(payload));
1226
+ }
1227
+ }
1228
+
1229
+ function broadcastToVscodeSockets(payload) {
1230
+ const data = JSON.stringify(payload);
1231
+ for (const ws of vscodeSockets) {
1232
+ if (ws.readyState === 1) ws.send(data);
1233
+ }
1234
+ }
1235
+
1236
+ // ── P3: Grace period β€” retain plugin state briefly on disconnect ──────────────
1237
+ const PLUGIN_GRACE_PERIOD_MS = 5000;
1238
+ let pluginGraceTimer = null;
1239
+ let pluginGraceState = null; // stashed state during grace period
1240
+
1241
+ // ── P3: Heartbeat β€” detect dead connections ──────────────────────────────────
1242
+ const HEARTBEAT_INTERVAL_MS = 30000;
1243
+
1244
+ function setupHeartbeat(wss) {
1245
+ const interval = setInterval(() => {
1246
+ wss.clients.forEach((ws) => {
1247
+ if (ws._isAlive === false) {
1248
+ console.log(" ⚠ Terminating unresponsive connection");
1249
+ return ws.terminate();
1250
+ }
1251
+ ws._isAlive = false;
1252
+ ws.ping();
1253
+ });
1254
+ }, HEARTBEAT_INTERVAL_MS);
1255
+ wss.on("close", () => clearInterval(interval));
1256
+ }
1257
+
1258
+ // ── P3: Port fallback β€” try ports 9001-9010 ──────────────────────────────────
1259
+ function createServerWithFallback(basePort, maxRetries = 9) {
1260
+ return new Promise((resolve, reject) => {
1261
+ let attempt = 0;
1262
+ function tryPort(port) {
1263
+ const server = new WebSocketServer({ port });
1264
+ server.on("listening", () => {
1265
+ PORT = port;
1266
+ resolve(server);
1267
+ });
1268
+ server.on("error", (err) => {
1269
+ if (err.code === "EADDRINUSE" && attempt < maxRetries) {
1270
+ attempt++;
1271
+ console.log(` ⚠ Port ${port} in use, trying ${port + 1}…`);
1272
+ tryPort(port + 1);
1273
+ } else {
1274
+ reject(err);
1275
+ }
1276
+ });
1277
+ }
1278
+ tryPort(basePort);
1279
+ });
1280
+ }
1281
+
1282
+ // ── WebSocket Server ─────────────────────────────────────────────────────────
1283
+ (async () => {
1284
+ let wss;
1285
+ try {
1286
+ wss = await createServerWithFallback(BASE_PORT);
1287
+ } catch (err) {
1288
+ console.error(`Fatal: could not bind to any port in range ${BASE_PORT}-${BASE_PORT + 9}:`, err.message);
1289
+ process.exit(1);
1290
+ }
1291
+
1292
+ console.log(`\nπŸ”Œ Figma Intelligence Bridge Relay`);
1293
+ console.log(` Listening on ws://localhost:${PORT}`);
1294
+ console.log(` MCP server β†’ connects to ws://localhost:${PORT}`);
1295
+ console.log(` Figma plugin β†’ connects to ws://localhost:${PORT}/plugin`);
1296
+ console.log(` VS Code ext β†’ connects to ws://localhost:${PORT}/vscode`);
1297
+ console.log(` Waiting for connections…\n`);
1298
+
1299
+ // Rewrite MCP config with the actual port (chat-runner wrote initial config with default port)
1300
+ writeMcpConfig(PORT);
1301
+
1302
+ // Start heartbeat monitoring
1303
+ setupHeartbeat(wss);
1304
+
1305
+ // Pre-warm knowledge hub (load .chunks.json files into cache for instant first query)
1306
+ prewarmHub().then((count) => {
1307
+ if (count > 0) console.log(` πŸ“š Knowledge hub pre-warmed: ${count} chunked source(s) cached`);
1308
+ }).catch(() => {});
1309
+
1310
+ // Start the MCP server as a persistent child process so the plugin
1311
+ // always shows "Connected" β€” not just during active chat requests.
1312
+ startPersistentMcpServer();
1313
+
1314
+ wss.on("connection", (ws, req) => {
1315
+ const path = req.url || "/";
1316
+ const isPlugin = path.includes("/plugin");
1317
+ const isVscode = path.includes("/vscode");
1318
+
1319
+ // P3: Heartbeat β€” mark connection alive on pong
1320
+ ws._isAlive = true;
1321
+ ws.on("pong", () => { ws._isAlive = true; });
1322
+
1323
+ if (isVscode) {
1324
+ vscodeSockets.add(ws);
1325
+ console.log("βœ… VS Code client connected");
1326
+ sendRelayStatus(ws, hasConnectedMcpSocket());
1327
+ refreshAuthState().catch(() => {});
1328
+ // Notify plugin that VS Code is connected
1329
+ sendToPlugin({ type: "vscode-connected", connected: true, count: vscodeSockets.size });
1330
+ ws.on("close", () => {
1331
+ vscodeSockets.delete(ws);
1332
+ console.log(" β†Ί VS Code client disconnected");
1333
+ sendToPlugin({ type: "vscode-connected", connected: vscodeSockets.size > 0, count: vscodeSockets.size });
1334
+ });
1335
+ } else if (isPlugin) {
1336
+ // P3: Cancel grace timer if plugin reconnects within grace period
1337
+ if (pluginGraceTimer) {
1338
+ clearTimeout(pluginGraceTimer);
1339
+ pluginGraceTimer = null;
1340
+ pluginGraceState = null;
1341
+ console.log(" β†Ί Plugin reconnected within grace period");
1342
+ }
1343
+ pluginSocket = ws;
1344
+ console.log("βœ… Figma plugin connected");
1345
+ sendRelayStatus(ws, hasConnectedMcpSocket());
1346
+ refreshAuthState().catch(() => {});
1347
+ } else {
1348
+ mcpSockets.add(ws);
1349
+ console.log("βœ… MCP server connected");
1350
+ sendRelayStatus(pluginSocket, true);
1351
+ }
1352
+
1353
+ ws.on("message", (data) => {
1354
+ const raw = data.toString();
1355
+ let msg;
1356
+ try { msg = JSON.parse(raw); } catch { return; }
1357
+
1358
+ // Global debug: log every message type
1359
+ try { appendFileSync("/tmp/import-vars-debug.log", `${new Date().toISOString()} MSG type=${msg.type} isVscode=${isVscode} isPlugin=${isPlugin} keys=${Object.keys(msg).join(",")}\n`); } catch {}
1360
+
1361
+ // ── Messages from VS Code extension ────────────────────────────────────
1362
+ if (isVscode) {
1363
+
1364
+ if (msg.type === "vscode-hello") {
1365
+ console.log(` VS Code client: ${msg.clientType || "unknown"} v${msg.version || "?"}`);
1366
+ return;
1367
+ }
1368
+
1369
+ // Set AI provider
1370
+ if (msg.type === "set-provider") {
1371
+ providerConfig = { provider: msg.provider || "claude", apiKey: msg.apiKey || null, projectId: msg.projectId || null };
1372
+ saveProviderConfig();
1373
+ console.log(` πŸ”‘ provider set (vscode): ${providerConfig.provider}`);
1374
+ sendToVscode({ type: "provider-stored", provider: providerConfig.provider }, ws);
1375
+ refreshAuthState().catch(() => {});
1376
+ return;
1377
+ }
1378
+
1379
+ // Set design system
1380
+ if (msg.type === "set-design-system") {
1381
+ const newId = msg.designSystemId || null;
1382
+ if (newId !== activeDesignSystemId) {
1383
+ activeDesignSystemId = newId;
1384
+ resetSession();
1385
+ resetCodexSession();
1386
+ console.log(` 🎨 design system (vscode): ${newId || "none"} (sessions reset)`);
1387
+ }
1388
+ sendToVscode({ type: "design-system-stored", designSystemId: activeDesignSystemId }, ws);
1389
+ // Broadcast DS change to all connected MCP sockets so intelligence layer stays in sync
1390
+ broadcastToMcpSockets(JSON.stringify({ type: "design-system-changed", designSystemId: activeDesignSystemId }));
1391
+ return;
1392
+ }
1393
+
1394
+ // Chat message from VS Code (supports mode: "dual", "code", "chat")
1395
+ if (msg.type === "chat") {
1396
+ const requestId = msg.id;
1397
+ const prov = providerConfig.provider || "claude";
1398
+ const chatMode = msg.mode || "dual";
1399
+ let chatMessage = msg.message || "";
1400
+
1401
+ // Pre-parse Figma links so the AI doesn't need to extract file_key/node_id
1402
+ const figmaLinkMatch = chatMessage.match(/https:\/\/www\.figma\.com\/(?:design|file)\/[^\s]+/);
1403
+ if (figmaLinkMatch) {
1404
+ try {
1405
+ const { parseFigmaLink } = require("./spec-helpers/parse-figma-link");
1406
+ const parsed = parseFigmaLink(figmaLinkMatch[0]);
1407
+ chatMessage += `\n\n[Pre-parsed Figma link: file_key="${parsed.file_key}", node_id="${parsed.node_id}"]`;
1408
+ } catch (e) { /* ignore parse errors */ }
1409
+ }
1410
+
1411
+ // ── Component Doc Generator: handle follow-up after chooser (VS Code) ─
1412
+ if (pendingDocGenChooser && (chatMode === "code" || chatMode === "dual")) {
1413
+ const elapsed = Date.now() - pendingDocGenChooser.shownAt;
1414
+ if (elapsed < 10 * 60 * 1000) {
1415
+ const reply = (chatMessage || "").trim().toLowerCase();
1416
+ const specMap = {
1417
+ "1": "anatomy", "anatomy": "anatomy",
1418
+ "2": "api", "api": "api",
1419
+ "3": "property", "properties": "property", "property": "property",
1420
+ "4": "color", "color": "color",
1421
+ "5": "structure", "structure": "structure",
1422
+ "6": "screen-reader", "screen reader": "screen-reader", "screen-reader": "screen-reader",
1423
+ "all": "all",
1424
+ };
1425
+ const matched = specMap[reply];
1426
+ const multiMatch = reply.match(/^[\d,\s]+$/);
1427
+ if (matched || multiMatch) {
1428
+ const original = pendingDocGenChooser.originalMessage;
1429
+ pendingDocGenChooser = null;
1430
+ // CRITICAL: Reset session so the AI starts fresh with the correct
1431
+ // system prompt containing the spec-type skill addendum.
1432
+ // Without this, --resume reuses the old system prompt which lacks
1433
+ // the tool restrictions and spec reference instructions.
1434
+ resetSession(chatMode);
1435
+ console.log(` πŸ“‹ Component Doc Generator (vscode): reset ${chatMode} session for fresh system prompt`);
1436
+ if (matched === "all" || (multiMatch && reply.replace(/\s/g, "").split(",").length >= 6)) {
1437
+ chatMessage = `${original}\n\nThe user selected ALL spec types. Generate all 6 specification documents: anatomy, api, property, color, structure, and screen-reader specs. Start with the anatomy spec, then proceed to each subsequent type.`;
1438
+ console.log(` πŸ“‹ Component Doc Generator (vscode): user chose ALL`);
1439
+ } else if (multiMatch) {
1440
+ const nums = reply.replace(/\s/g, "").split(",").filter(Boolean);
1441
+ const types = nums.map(n => specMap[n]).filter(Boolean);
1442
+ chatMessage = `${original}\n\nThe user selected these spec types: ${types.join(", ")}. Generate a create ${types[0]} spec for the component first.`;
1443
+ console.log(` πŸ“‹ Component Doc Generator (vscode): user chose [${types.join(", ")}]`);
1444
+ } else {
1445
+ chatMessage = `${original}\n\nThe user selected: create ${matched} spec for this component.`;
1446
+ console.log(` πŸ“‹ Component Doc Generator (vscode): user chose "${matched}"`);
1447
+ }
1448
+ } else {
1449
+ pendingDocGenChooser = null;
1450
+ }
1451
+ } else {
1452
+ pendingDocGenChooser = null;
1453
+ }
1454
+ }
1455
+
1456
+ // ── Component Doc Generator chooser intercept (VS Code) ──────────
1457
+ if (chatMode === "code" || chatMode === "dual") {
1458
+ const { detectActiveSkills } = require("./shared-prompt-config");
1459
+ const detectedSkills = detectActiveSkills(chatMessage);
1460
+ if (detectedSkills.some(s => s === "Component Doc Generator:all")) {
1461
+ console.log(` πŸ“‹ Component Doc Generator (vscode): presenting spec type chooser`);
1462
+ pendingDocGenChooser = { originalMessage: chatMessage, shownAt: Date.now() };
1463
+ const chooserText = `I can generate the following detailed spec types for your component:\n\n` +
1464
+ `1. **Anatomy** β€” Numbered markers on each element + attribute table with semantic notes\n` +
1465
+ `2. **API** β€” Property tables with values, defaults, required/optional status, and configuration examples\n` +
1466
+ `3. **Properties** β€” Visual exhibits for variant axes, boolean toggles, variable modes, and child properties\n` +
1467
+ `4. **Color** β€” Design token mapping for every element across states and variants\n` +
1468
+ `5. **Structure** β€” Dimensions, spacing, padding tables across size/density variants\n` +
1469
+ `6. **Screen Reader** β€” VoiceOver, TalkBack, and ARIA accessibility specs per platform\n\n` +
1470
+ `Which spec(s) would you like me to generate? You can pick one, multiple (e.g. 1, 3, 5), or say **all** to generate everything.`;
1471
+ sendToVscode({ type: "phase_start", id: requestId, phase: "Skills: Component Doc Generator" }, ws);
1472
+ sendToVscode({ type: "text_delta", id: requestId, delta: chooserText }, ws);
1473
+ sendToVscode({ type: "done", id: requestId, fullText: chooserText }, ws);
1474
+ return;
1475
+ }
1476
+ }
1477
+
1478
+ // Inject knowledge grounding if active (relevance-filtered)
1479
+ if (activeContentSources.size > 0) {
1480
+ const groundingCtx = buildGroundingContext(activeContentSources, msg.message);
1481
+ if (groundingCtx) {
1482
+ chatMessage = groundingCtx + "\n---\n\nUser question: " + chatMessage;
1483
+ }
1484
+ }
1485
+
1486
+ console.log(` πŸ’¬ vscode chat [${prov}/${chatMode}] (id: ${requestId}): ${chatMessage.slice(0, 60)}…`);
1487
+
1488
+ const onEvent = (event) => {
1489
+ // Send to VS Code client AND plugin (so both see the Figma actions)
1490
+ sendToVscode(event, ws);
1491
+ sendToPlugin(event);
1492
+ };
1493
+
1494
+ let proc;
1495
+ if (prov === "claude" || !prov || prov === "bridge") {
1496
+ const anthropicKey = getAnthropicApiKey();
1497
+ if (chatMode === "chat" && anthropicKey) {
1498
+ const { buildChatPrompt } = require("./shared-prompt-config");
1499
+ proc = runAnthropicChat({
1500
+ message: chatMessage,
1501
+ attachments: msg.attachments,
1502
+ conversation: msg.conversation,
1503
+ requestId,
1504
+ apiKey: anthropicKey,
1505
+ model: msg.model,
1506
+ systemPrompt: buildChatPrompt(),
1507
+ onEvent,
1508
+ });
1509
+ } else {
1510
+ proc = runClaude({
1511
+ message: chatMessage,
1512
+ attachments: msg.attachments,
1513
+ conversation: msg.conversation,
1514
+ requestId,
1515
+ model: msg.model,
1516
+ designSystemId: activeDesignSystemId,
1517
+ mode: chatMode,
1518
+ frameworkConfig: msg.frameworkConfig,
1519
+ onEvent,
1520
+ });
1521
+ }
1522
+ } else if (prov === "openai") {
1523
+ proc = runCodex({
1524
+ message: chatMessage,
1525
+ attachments: msg.attachments,
1526
+ requestId,
1527
+ model: msg.model,
1528
+ designSystemId: activeDesignSystemId,
1529
+ mode: chatMode,
1530
+ onEvent,
1531
+ });
1532
+ } else if (prov === "gemini") {
1533
+ if (geminiCliAuthInfo.loggedIn) {
1534
+ proc = runGeminiCli({
1535
+ message: chatMessage,
1536
+ attachments: msg.attachments,
1537
+ conversation: msg.conversation,
1538
+ requestId,
1539
+ model: msg.model,
1540
+ designSystemId: activeDesignSystemId,
1541
+ mode: chatMode,
1542
+ onEvent,
1543
+ });
1544
+ } else {
1545
+ proc = runGemini({
1546
+ message: chatMessage,
1547
+ attachments: msg.attachments,
1548
+ conversation: msg.conversation,
1549
+ requestId,
1550
+ apiKey: providerConfig.apiKey,
1551
+ model: msg.model,
1552
+ designSystemId: activeDesignSystemId,
1553
+ mode: chatMode,
1554
+ onEvent,
1555
+ });
1556
+ }
1557
+ } else if (prov === "stitch") {
1558
+ proc = runStitch({
1559
+ message: chatMessage,
1560
+ requestId,
1561
+ apiKey: providerConfig.apiKey,
1562
+ projectId: providerConfig.projectId,
1563
+ model: msg.model,
1564
+ onEvent,
1565
+ });
1566
+ } else {
1567
+ sendToVscode({ type: "error", id: requestId, error: `Unsupported provider: ${prov}` }, ws);
1568
+ sendToVscode({ type: "done", id: requestId, fullText: "" }, ws);
1569
+ return;
1570
+ }
1571
+
1572
+ activeChatProcesses.set(requestId, proc);
1573
+ proc.on("close", () => activeChatProcesses.delete(requestId));
1574
+ return;
1575
+ }
1576
+
1577
+ // Abort chat
1578
+ if (msg.type === "abort-chat") {
1579
+ const proc = activeChatProcesses.get(msg.id);
1580
+ if (proc) {
1581
+ proc.kill("SIGTERM");
1582
+ activeChatProcesses.delete(msg.id);
1583
+ console.log(` β›” vscode chat aborted (id: ${msg.id})`);
1584
+ }
1585
+ return;
1586
+ }
1587
+
1588
+ // New conversation
1589
+ if (msg.type === "new-conversation") {
1590
+ const resetMode = msg.mode || null;
1591
+ resetSession(resetMode);
1592
+ resetCodexSession(resetMode);
1593
+ console.log(` πŸ”„ vscode session reset${resetMode ? ` (${resetMode})` : " (all)"}`);
1594
+ return;
1595
+ }
1596
+
1597
+ return;
1598
+ }
1599
+
1600
+ // ── Messages from the Figma plugin ──────────────────────────────────────
1601
+ if (isPlugin) {
1602
+
1603
+ // Stitch Google OAuth β€” "Sign in with Google" button
1604
+ if (msg.type === "stitch-auth") {
1605
+ (async () => {
1606
+ try {
1607
+ sendToPlugin({ type: "stitch-auth-status", status: "signing-in" });
1608
+ console.log(" Stitch: starting Google OAuth flow...");
1609
+ const accessToken = await startStitchAuth();
1610
+ providerConfig.apiKey = accessToken; // store as apiKey for relay compatibility
1611
+ saveProviderConfig();
1612
+ const email = getStitchEmail();
1613
+ console.log(` Stitch: authenticated as ${email || "unknown"}`);
1614
+ sendToPlugin({ type: "stitch-auth-status", status: "success", email });
1615
+ } catch (err) {
1616
+ console.error(" Stitch auth failed:", err.message);
1617
+ sendToPlugin({ type: "stitch-auth-status", status: "error", error: err.message });
1618
+ }
1619
+ })();
1620
+ return;
1621
+ }
1622
+
1623
+ // Stitch sign-out
1624
+ if (msg.type === "stitch-signout") {
1625
+ clearStitchAuth();
1626
+ console.log(" Stitch: signed out");
1627
+ sendToPlugin({ type: "stitch-auth-status", status: "signed-out" });
1628
+ return;
1629
+ }
1630
+
1631
+ // Set AI provider / API key
1632
+ if (msg.type === "set-provider") {
1633
+ providerConfig = {
1634
+ provider: msg.provider || "claude",
1635
+ apiKey: msg.apiKey || null,
1636
+ projectId: msg.projectId || null,
1637
+ };
1638
+ saveProviderConfig();
1639
+ console.log(` πŸ”‘ provider set: ${providerConfig.provider}`);
1640
+ sendToPlugin({ type: "provider-stored", provider: providerConfig.provider });
1641
+ refreshAuthState().catch(() => {});
1642
+ return;
1643
+ }
1644
+
1645
+ // Set active design system
1646
+ if (msg.type === "set-design-system") {
1647
+ const newId = msg.designSystemId || null;
1648
+ if (newId !== activeDesignSystemId) {
1649
+ activeDesignSystemId = newId;
1650
+ resetSession();
1651
+ resetCodexSession();
1652
+ console.log(` 🎨 design system: ${newId || "none"} (sessions reset)`);
1653
+ }
1654
+ sendToPlugin({ type: "design-system-stored", designSystemId: activeDesignSystemId });
1655
+ // Broadcast DS change to all connected MCP sockets so intelligence layer stays in sync
1656
+ broadcastToMcpSockets(JSON.stringify({ type: "design-system-changed", designSystemId: activeDesignSystemId }));
1657
+ return;
1658
+ }
1659
+
1660
+ // ── Knowledge source management ──────────────────────────────────
1661
+ if (msg.type === "add-content-file") {
1662
+ const fileName = msg.name || "file";
1663
+ const dataUrl = msg.data || "";
1664
+ console.log(` πŸ“„ Adding file: ${fileName}`);
1665
+
1666
+ (async () => {
1667
+ try {
1668
+ // Decode base64 DataURL to buffer
1669
+ const b64Match = dataUrl.match(/^data:[^;]*;base64,(.+)$/);
1670
+ if (!b64Match) throw new Error("Invalid file data");
1671
+ const buffer = Buffer.from(b64Match[1], "base64");
1672
+
1673
+ const ext = (fileName.match(/\.(\w+)$/)?.[1] || "").toLowerCase();
1674
+ let title, text, meta = { fileName, fileType: ext };
1675
+
1676
+ if (ext === "pdf") {
1677
+ const result = await parsePdfBuffer(buffer);
1678
+ title = result.title || fileName.replace(/\.\w+$/, "");
1679
+ text = result.text;
1680
+ meta.pages = result.pages;
1681
+ } else if (ext === "docx" || ext === "doc") {
1682
+ const result = await parseDocxBuffer(buffer);
1683
+ title = result.title || fileName.replace(/\.\w+$/, "");
1684
+ text = result.text;
1685
+ } else {
1686
+ // Plain text formats (txt, md, csv, json, etc.)
1687
+ title = fileName.replace(/\.\w+$/, "");
1688
+ text = buffer.toString("utf-8");
1689
+ }
1690
+
1691
+ if (!text || text.trim().length === 0) {
1692
+ throw new Error("No text content could be extracted from this file");
1693
+ }
1694
+
1695
+ const source = createContentSource(title, text, meta);
1696
+ activeContentSources.set(source.id, source);
1697
+ console.log(` βœ… File added: "${title}" (${text.length} chars${meta.pages ? `, ${meta.pages} pages` : ""})`);
1698
+ sendToPlugin({
1699
+ type: "content-added",
1700
+ source: {
1701
+ id: source.id, title: source.title, sourceCount: source.sources.length,
1702
+ meta: source.meta, extractedAt: source.extractedAt,
1703
+ charCount: text.length,
1704
+ preview: text.slice(0, 500).replace(/\s+/g, " ").trim(),
1705
+ },
1706
+ });
1707
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1708
+ } catch (err) {
1709
+ console.log(` ⚠ File error: ${err.message}`);
1710
+ sendToPlugin({ type: "content-error", error: err.message, fileName });
1711
+ }
1712
+ })();
1713
+ return;
1714
+ }
1715
+
1716
+ if (msg.type === "add-content-url") {
1717
+ const url = (msg.url || "").trim();
1718
+ console.log(` πŸ”— Fetching URL: ${url.slice(0, 60)}…`);
1719
+
1720
+ (async () => {
1721
+ try {
1722
+ if (!url || !/^https?:\/\//i.test(url)) throw new Error("Invalid URL");
1723
+ const result = await fetchUrlContent(url);
1724
+ if (!result.text || result.text.trim().length < 20) {
1725
+ throw new Error("Could not extract meaningful content from this URL");
1726
+ }
1727
+ const source = createContentSource(
1728
+ result.title || url,
1729
+ result.text,
1730
+ { url, fileType: "url" }
1731
+ );
1732
+ activeContentSources.set(source.id, source);
1733
+ console.log(` βœ… URL added: "${source.title}" (${result.text.length} chars)`);
1734
+ sendToPlugin({
1735
+ type: "content-added",
1736
+ source: {
1737
+ id: source.id, title: source.title, sourceCount: source.sources.length,
1738
+ meta: source.meta, extractedAt: source.extractedAt,
1739
+ charCount: result.text.length,
1740
+ preview: result.text.slice(0, 500).replace(/\s+/g, " ").trim(),
1741
+ },
1742
+ });
1743
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1744
+ } catch (err) {
1745
+ console.log(` ⚠ URL error: ${err.message}`);
1746
+ sendToPlugin({ type: "content-error", error: err.message, url });
1747
+ }
1748
+ })();
1749
+ return;
1750
+ }
1751
+
1752
+ if (msg.type === "add-content-text") {
1753
+ const title = msg.title || "Pasted Content";
1754
+ const content = msg.content || "";
1755
+ if (!content.trim()) {
1756
+ sendToPlugin({ type: "content-error", error: "No content provided" });
1757
+ return;
1758
+ }
1759
+ const source = createContentSource(title, content, { fileType: "text" });
1760
+ activeContentSources.set(source.id, source);
1761
+ console.log(` βœ… Text pasted: "${title}" (${content.length} chars)`);
1762
+ sendToPlugin({
1763
+ type: "content-added",
1764
+ source: {
1765
+ id: source.id, title: source.title, sourceCount: source.sources.length,
1766
+ meta: source.meta, extractedAt: source.extractedAt,
1767
+ charCount: content.length,
1768
+ preview: content.slice(0, 500).replace(/\s+/g, " ").trim(),
1769
+ },
1770
+ });
1771
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1772
+ return;
1773
+ }
1774
+
1775
+ if (msg.type === "remove-content") {
1776
+ const id = msg.sourceId;
1777
+ if (activeContentSources.has(id)) {
1778
+ const title = activeContentSources.get(id).title;
1779
+ activeContentSources.delete(id);
1780
+ console.log(` πŸ“„ Source removed: "${title}"`);
1781
+ sendToPlugin({ type: "content-removed", sourceId: id });
1782
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1783
+ }
1784
+ return;
1785
+ }
1786
+
1787
+ if (msg.type === "list-content") {
1788
+ sendToPlugin({
1789
+ type: "content-list",
1790
+ sources: Array.from(activeContentSources.values()).map(s => ({
1791
+ id: s.id, title: s.title, sourceCount: s.sources.length, meta: s.meta || {}, extractedAt: s.extractedAt,
1792
+ })),
1793
+ });
1794
+ return;
1795
+ }
1796
+
1797
+ // ── Knowledge Hub ────────────────────────────────────────────────
1798
+ if (msg.type === "hub-scan") {
1799
+ const catalog = scanKnowledgeHub();
1800
+ console.log(` πŸ“š Knowledge Hub: ${catalog.length} file(s) found`);
1801
+ sendToPlugin({ type: "hub-catalog", files: catalog });
1802
+ return;
1803
+ }
1804
+
1805
+ if (msg.type === "hub-load") {
1806
+ const fileName = msg.fileName;
1807
+ console.log(` πŸ“š Loading hub file: ${fileName}`);
1808
+ (async () => {
1809
+ try {
1810
+ const source = await loadHubFile(fileName);
1811
+ activeContentSources.set(source.id, source);
1812
+ const text = source.sources[0]?.content || "";
1813
+ console.log(` βœ… Hub file loaded: "${source.title}" (${text.length} chars)`);
1814
+ sendToPlugin({
1815
+ type: "content-added",
1816
+ source: {
1817
+ id: source.id, title: source.title, sourceCount: source.sources.length,
1818
+ meta: source.meta, extractedAt: source.extractedAt,
1819
+ charCount: text.length,
1820
+ preview: text.slice(0, 500).replace(/\s+/g, " ").trim(),
1821
+ },
1822
+ });
1823
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1824
+ } catch (err) {
1825
+ console.log(` ⚠ Hub error: ${err.message}`);
1826
+ sendToPlugin({ type: "content-error", error: err.message, fileName });
1827
+ }
1828
+ })();
1829
+ return;
1830
+ }
1831
+
1832
+ if (msg.type === "hub-search") {
1833
+ const results = searchHub(msg.query || "");
1834
+ sendToPlugin({ type: "hub-search-results", files: results, query: msg.query });
1835
+ return;
1836
+ }
1837
+
1838
+ // ── Web Reference Site management ───────────────────────────────
1839
+ if (msg.type === "add-reference-site") {
1840
+ const site = addReferenceSite({ name: msg.name, baseUrl: msg.baseUrl || msg.url, searchDomain: msg.searchDomain });
1841
+ console.log(` 🌐 Reference site added: ${site.name} (${site.searchDomain})`);
1842
+ sendToPlugin({ type: "reference-site-added", site });
1843
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1844
+ return;
1845
+ }
1846
+
1847
+ if (msg.type === "remove-reference-site") {
1848
+ removeReferenceSite(msg.id);
1849
+ console.log(` 🌐 Reference site removed: ${msg.id}`);
1850
+ sendToPlugin({ type: "reference-site-removed", id: msg.id });
1851
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1852
+ return;
1853
+ }
1854
+
1855
+ if (msg.type === "list-reference-sites") {
1856
+ sendToPlugin({ type: "reference-sites-list", sites: getReferenceSites() });
1857
+ return;
1858
+ }
1859
+
1860
+ // Chat message β†’ route to the configured AI runner
1861
+ if (msg.type === "chat") {
1862
+ const requestId = msg.id;
1863
+ const prov = providerConfig.provider || "claude";
1864
+ const chatMode = msg.mode || "code";
1865
+ let chatMessage = msg.message || "";
1866
+
1867
+ // Debug: log all incoming chat messages
1868
+ try { appendFileSync("/tmp/import-vars-debug.log", `${new Date().toISOString()} CHAT prov=${prov} msg="${chatMessage.slice(0,100)}" attachments=${JSON.stringify((msg.attachments||[]).map(a=>({name:a.name,len:a.data?.length})))}\n`); } catch {}
1869
+ try { appendFileSync("/tmp/import-vars-debug.log", `${new Date().toISOString()} isImport=${isImportVariablesIntent(chatMessage, msg.attachments)}\n`); } catch {}
1870
+
1871
+ // Pre-parse Figma links so the AI doesn't need to extract file_key/node_id
1872
+ const figmaLinkMatch2 = chatMessage.match(/https:\/\/www\.figma\.com\/(?:design|file)\/[^\s]+/);
1873
+ if (figmaLinkMatch2) {
1874
+ try {
1875
+ const { parseFigmaLink } = require("./spec-helpers/parse-figma-link");
1876
+ const parsed = parseFigmaLink(figmaLinkMatch2[0]);
1877
+ chatMessage += `\n\n[Pre-parsed Figma link: file_key="${parsed.file_key}", node_id="${parsed.node_id}"]`;
1878
+ } catch (e) { /* ignore parse errors */ }
1879
+ }
1880
+
1881
+ // /knowledge command β€” intercept and handle via knowledge hub
1882
+ if (/^\s*\/knowledge\b/i.test(chatMessage)) {
1883
+ const query = chatMessage.replace(/^\s*\/knowledge\s*/i, "").trim();
1884
+ const catalog = scanKnowledgeHub();
1885
+ console.log(` πŸ“š /knowledge command: ${catalog.length} files in hub${query ? `, searching: "${query}"` : ""}`);
1886
+
1887
+ if (query) {
1888
+ // Auto-search and load matching hub files
1889
+ const matches = searchHub(query);
1890
+ if (matches.length > 0) {
1891
+ (async () => {
1892
+ try {
1893
+ const source = await loadHubFile(matches[0].fileName);
1894
+ activeContentSources.set(source.id, source);
1895
+ const text = source.sources[0]?.content || "";
1896
+ sendToPlugin({
1897
+ type: "content-added",
1898
+ source: {
1899
+ id: source.id, title: source.title, sourceCount: source.sources.length,
1900
+ meta: source.meta, extractedAt: source.extractedAt,
1901
+ charCount: text.length,
1902
+ preview: text.slice(0, 500).replace(/\s+/g, " ").trim(),
1903
+ },
1904
+ });
1905
+ // Send a visible chat response
1906
+ const otherNames = matches.slice(1, 4).map(m => `"${m.title}"`).join(", ");
1907
+ let responseText = `πŸ“š **Loaded "${source.title}"** from Knowledge Hub (${text.length.toLocaleString()} chars).\n\nYou can now ask me questions about this source β€” I'll ground my answers in its content.`;
1908
+ if (matches.length > 1) responseText += `\n\n_${matches.length - 1} other match(es): ${otherNames}_`;
1909
+ sendToPlugin({ type: "text_delta", id: requestId, delta: responseText });
1910
+ sendToPlugin({ type: "done", id: requestId, fullText: responseText });
1911
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1912
+ } catch (err) {
1913
+ sendToPlugin({ type: "text_delta", id: requestId, delta: `⚠️ Could not load: ${err.message}` });
1914
+ sendToPlugin({ type: "done", id: requestId, fullText: err.message });
1915
+ }
1916
+ })();
1917
+ } else {
1918
+ // No matches β€” show what's available
1919
+ const fileList = catalog.map(f => `β€’ ${f.title} (${f.fileType.toUpperCase()})`).join("\n");
1920
+ const responseText = `πŸ“š No files matching "${query}" found in the Knowledge Hub.\n\n**Available files (${catalog.length}):**\n${fileList || "(empty)"}\n\n_Try: \`/knowledge <keyword>\` to search, or click the πŸ“– icon to browse._`;
1921
+ sendToPlugin({ type: "text_delta", id: requestId, delta: responseText });
1922
+ sendToPlugin({ type: "done", id: requestId, fullText: responseText });
1923
+ sendToPlugin({ type: "hub-catalog", files: catalog, query });
1924
+ }
1925
+ } else {
1926
+ // Just "/knowledge" β€” show catalog as chat response + open panel
1927
+ const fileList = catalog.map(f => `β€’ **${f.title}** (${f.fileType.toUpperCase()}, ${(f.sizeBytes / 1024).toFixed(0)} KB)`).join("\n");
1928
+ const activeList = Array.from(activeContentSources.values()).map(s => `β€’ βœ… ${s.title}`).join("\n");
1929
+ let responseText = `πŸ“š **Knowledge Hub** β€” ${catalog.length} file(s) available\n\n`;
1930
+ if (catalog.length > 0) {
1931
+ responseText += `**Library:**\n${fileList}\n\n`;
1932
+ responseText += `_Use \`/knowledge <keyword>\` to load a specific file, or click the πŸ“– icon to browse and activate._`;
1933
+ } else {
1934
+ responseText += `No files yet. Add PDFs, DOCX, or TXT files to:\n\`figma-bridge-plugin/knowledge-hub/\``;
1935
+ }
1936
+ if (activeList) responseText += `\n\n**Currently active sources:**\n${activeList}`;
1937
+ sendToPlugin({ type: "text_delta", id: requestId, delta: responseText });
1938
+ sendToPlugin({ type: "done", id: requestId, fullText: responseText });
1939
+ sendToPlugin({ type: "hub-catalog", files: catalog });
1940
+ }
1941
+ return;
1942
+ }
1943
+
1944
+ // ── Import .md β†’ Figma variables (works from any provider) ──────
1945
+ if (isImportVariablesIntent(chatMessage, msg.attachments)) {
1946
+ handleImportVariables(requestId, chatMessage, msg.attachments, (ev) => sendToPlugin(ev));
1947
+ return;
1948
+ }
1949
+
1950
+ // ── Component Doc Generator: handle follow-up after chooser ─────
1951
+ // If the chooser was shown and user replies with a type selection,
1952
+ // rewrite the message to include the original context + specific spec type.
1953
+ if (pendingDocGenChooser && (chatMode === "code" || chatMode === "dual")) {
1954
+ const elapsed = Date.now() - pendingDocGenChooser.shownAt;
1955
+ if (elapsed < 10 * 60 * 1000) { // within 10 minutes
1956
+ const reply = (chatMessage || "").trim().toLowerCase();
1957
+ const specMap = {
1958
+ "1": "anatomy", "anatomy": "anatomy",
1959
+ "2": "api", "api": "api",
1960
+ "3": "property", "properties": "property", "property": "property",
1961
+ "4": "color", "color": "color",
1962
+ "5": "structure", "structure": "structure",
1963
+ "6": "screen-reader", "screen reader": "screen-reader", "screen-reader": "screen-reader",
1964
+ "all": "all",
1965
+ };
1966
+ // Check if reply matches a spec type choice
1967
+ const matched = specMap[reply];
1968
+ // Also check for multi-select like "1, 3, 5" or "1 3 5"
1969
+ const multiMatch = reply.match(/^[\d,\s]+$/);
1970
+ if (matched || multiMatch) {
1971
+ const original = pendingDocGenChooser.originalMessage;
1972
+ pendingDocGenChooser = null;
1973
+ // CRITICAL: Reset session so the AI starts fresh with the correct
1974
+ // system prompt containing the spec-type skill addendum.
1975
+ resetSession(chatMode);
1976
+ console.log(` πŸ“‹ Component Doc Generator (plugin): reset ${chatMode} session for fresh system prompt`);
1977
+ if (matched === "all" || (multiMatch && reply.replace(/\s/g, "").split(",").length >= 6)) {
1978
+ // User wants all specs β€” send each type
1979
+ chatMessage = `${original}\n\nThe user selected ALL spec types. Generate all 6 specification documents: anatomy, api, property, color, structure, and screen-reader specs. Start with the anatomy spec, then proceed to each subsequent type.`;
1980
+ console.log(` πŸ“‹ Component Doc Generator: user chose ALL β€” rewriting message`);
1981
+ } else if (multiMatch) {
1982
+ const nums = reply.replace(/\s/g, "").split(",").filter(Boolean);
1983
+ const types = nums.map(n => specMap[n]).filter(Boolean);
1984
+ chatMessage = `${original}\n\nThe user selected these spec types: ${types.join(", ")}. Generate a create ${types[0]} spec for the component first.`;
1985
+ console.log(` πŸ“‹ Component Doc Generator: user chose [${types.join(", ")}] β€” rewriting message`);
1986
+ } else {
1987
+ chatMessage = `${original}\n\nThe user selected: create ${matched} spec for this component.`;
1988
+ console.log(` πŸ“‹ Component Doc Generator: user chose "${matched}" β€” rewriting message`);
1989
+ }
1990
+ } else {
1991
+ // Reply doesn't look like a spec choice β€” clear pending state
1992
+ pendingDocGenChooser = null;
1993
+ }
1994
+ } else {
1995
+ pendingDocGenChooser = null; // expired
1996
+ }
1997
+ }
1998
+
1999
+ // ── Component Doc Generator β€” no chooser, generate complete spec directly ────────
2000
+ // The tool now auto-enriches all sections from the knowledge base in a single call.
2001
+ // No need to present options or use a 2-phase workflow.
2002
+
2003
+ // ── Design Decision: auto-register NN Group + proactive article fetch ──
2004
+ {
2005
+ const { detectActiveSkills } = require("./shared-prompt-config");
2006
+ const msgSkills = detectActiveSkills(chatMessage);
2007
+ if (msgSkills.includes("Design Decision")) {
2008
+ const sites = getReferenceSites();
2009
+ if (!sites.some(s => s.searchDomain === "nngroup.com")) {
2010
+ addReferenceSite({ name: "Nielsen Norman Group", searchDomain: "nngroup.com" });
2011
+ console.log(" πŸ“– Auto-registered nngroup.com as reference site for Design Decision");
2012
+ }
2013
+ // Proactive search β€” fetch NN Group article and inject as grounding
2014
+ (async () => {
2015
+ try {
2016
+ const nnResult = await Promise.race([
2017
+ searchReferenceSites(chatMessage),
2018
+ new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 8000)),
2019
+ ]);
2020
+ if (nnResult) {
2021
+ const nnSource = createContentSource(nnResult.title, nnResult.text, { url: nnResult.url, source: "nngroup.com" });
2022
+ activeContentSources.set(nnSource.id, nnSource);
2023
+ console.log(` πŸ“– NN Group article loaded: "${nnResult.title}"`);
2024
+ }
2025
+ } catch (e) {
2026
+ console.error(` ⚠ NN Group search failed: ${e.message}`);
2027
+ }
2028
+ })();
2029
+ }
2030
+ }
2031
+
2032
+ // ── Chat Tiers: always route through AI with grounding ─────────
2033
+ const rawMessage = chatMessage; // preserve original for knowledge/web search
2034
+
2035
+ // Tier 1: Fetch web reference articles async, then route to AI
2036
+ // (No more raw text "instant answers" β€” AI always synthesizes the response)
2037
+ if (chatMode === "chat" && getReferenceSites().length > 0) {
2038
+ (async () => {
2039
+ try {
2040
+ const timeoutPromise = new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 5000));
2041
+ const webAnswer = await Promise.race([searchReferenceSites(rawMessage), timeoutPromise]);
2042
+ if (webAnswer) {
2043
+ // Add fetched article as a content source for grounding, don't return it raw
2044
+ const webSource = createContentSource(webAnswer.title, webAnswer.text || "", {
2045
+ url: webAnswer.url,
2046
+ source: webAnswer.siteName,
2047
+ });
2048
+ activeContentSources.set(webSource.id, webSource);
2049
+ console.log(` 🌐 Web reference loaded: ${webAnswer.siteName} β€” ${webAnswer.title}`);
2050
+ }
2051
+ } catch (err) {
2052
+ console.error(` ⚠ Web reference search error: ${err.message}`);
2053
+ }
2054
+ routeToAiProvider();
2055
+ })();
2056
+ return; // async β€” routeToAiProvider called inside the async block
2057
+ }
2058
+
2059
+ routeToAiProvider();
2060
+ return;
2061
+
2062
+ function routeToAiProvider() {
2063
+
2064
+ // Inject knowledge source grounding context if sources are active
2065
+ if (activeContentSources.size > 0) {
2066
+ const groundingCtx = buildGroundingContext(activeContentSources, rawMessage);
2067
+ if (groundingCtx) {
2068
+ chatMessage = groundingCtx +
2069
+ "\n---\n\n" +
2070
+ "INSTRUCTIONS: Use the knowledge context above to answer the user's question. " +
2071
+ "Synthesize the information into a clear, structured answer β€” do NOT just quote raw text. " +
2072
+ "Cite the source name when referencing specific information. " +
2073
+ "If the context doesn't contain relevant information, say so and answer from your general knowledge.\n\n" +
2074
+ "User question: " + chatMessage;
2075
+ console.log(` πŸ“„ Injected ${activeContentSources.size} knowledge source(s) as grounding context`);
2076
+ }
2077
+ }
2078
+
2079
+ console.log(` πŸ’¬ chat [${prov}/${chatMode}] (id: ${requestId}): ${(chatMessage).slice(0, 60)}…`);
2080
+
2081
+ const onEvent = (event) => {
2082
+ // Intercept figma_command events β€” forward as bridge-request to plugin
2083
+ if (event.type === "figma_command") {
2084
+ const cmdId = `stitch-cmd-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2085
+ sendToPlugin({
2086
+ type: "bridge-request",
2087
+ id: cmdId,
2088
+ method: event.method,
2089
+ params: event.params || {},
2090
+ });
2091
+ console.log(` 🎨 stitch β†’ figma: ${event.method}`);
2092
+ return;
2093
+ }
2094
+
2095
+ sendToPlugin(event);
2096
+ // In dual mode, also forward to VS Code clients for code extraction
2097
+ if (chatMode === "dual") {
2098
+ broadcastToVscodeSockets(event);
2099
+ }
2100
+ if (event.type === "tool_start") {
2101
+ console.log(` πŸ”§ tool_start: ${event.tool}`);
2102
+ } else if (event.type === "tool_done") {
2103
+ console.log(` βœ… tool_done: ${event.tool}${event.isError ? " [ERROR]" : ""}`);
2104
+ } else if (event.type === "phase_start") {
2105
+ console.log(` πŸ“‹ phase: ${event.phase}`);
2106
+ }
2107
+ };
2108
+
2109
+ let proc;
2110
+ if (prov === "openai") {
2111
+ // Use Codex CLI (subscription-based) β€” no API key needed
2112
+ proc = runCodex({
2113
+ message: chatMessage,
2114
+ attachments: msg.attachments,
2115
+ conversation: msg.conversation,
2116
+ requestId,
2117
+ model: msg.model,
2118
+ designSystemId: activeDesignSystemId,
2119
+ mode: chatMode,
2120
+ onEvent,
2121
+ });
2122
+ } else if (prov === "gemini") {
2123
+ if (geminiCliAuthInfo.loggedIn) {
2124
+ // Subscription mode β€” use Gemini CLI (Google One AI Premium / Gemini Advanced)
2125
+ proc = runGeminiCli({
2126
+ message: chatMessage,
2127
+ attachments: msg.attachments,
2128
+ conversation: msg.conversation,
2129
+ requestId,
2130
+ model: msg.model,
2131
+ designSystemId: activeDesignSystemId,
2132
+ mode: chatMode,
2133
+ onEvent,
2134
+ });
2135
+ } else {
2136
+ // API key mode β€” fallback for users without subscription CLI auth
2137
+ proc = runGemini({
2138
+ message: chatMessage,
2139
+ attachments: msg.attachments,
2140
+ conversation: msg.conversation,
2141
+ requestId,
2142
+ apiKey: providerConfig.apiKey,
2143
+ model: msg.model,
2144
+ designSystemId: activeDesignSystemId,
2145
+ mode: chatMode,
2146
+ onEvent,
2147
+ });
2148
+ }
2149
+ } else if (prov === "perplexity") {
2150
+ proc = runPerplexity({
2151
+ message: chatMessage,
2152
+ attachments: msg.attachments,
2153
+ conversation: msg.conversation,
2154
+ requestId,
2155
+ apiKey: providerConfig.apiKey,
2156
+ model: msg.model,
2157
+ mode: "chat",
2158
+ onEvent,
2159
+ });
2160
+ } else if (prov === "stitch") {
2161
+ // ── Check for "sync design system" intent before routing to Stitch ──
2162
+ if (isSyncDesignIntent(chatMessage)) {
2163
+ handleSyncDesignSystem(requestId, chatMessage, onEvent);
2164
+ return;
2165
+ }
2166
+
2167
+ if (isImportVariablesIntent(chatMessage, msg.attachments)) {
2168
+ handleImportVariables(requestId, chatMessage, msg.attachments, onEvent);
2169
+ return;
2170
+ }
2171
+
2172
+ // If a .md file is attached, extract its content as design context for generation
2173
+ let designContext = null;
2174
+ if (msg.attachments?.length) {
2175
+ const mdFile = msg.attachments.find(a => /\.md$/i.test(a.name));
2176
+ if (mdFile?.data) designContext = mdFile.data;
2177
+ }
2178
+
2179
+ console.log(` 🎨 Routing to Stitch runner (apiKey: ${providerConfig.apiKey ? "set" : "MISSING"}${designContext ? ", with .md design context" : ""})`);
2180
+ proc = runStitch({
2181
+ message: chatMessage,
2182
+ requestId,
2183
+ apiKey: providerConfig.apiKey,
2184
+ projectId: providerConfig.projectId,
2185
+ model: msg.model,
2186
+ designContext,
2187
+ onEvent,
2188
+ });
2189
+ } else if (prov === "bridge") {
2190
+ // Bridge-only mode: no built-in AI β€” tell the plugin immediately
2191
+ sendToPlugin({
2192
+ type: "error",
2193
+ id: requestId,
2194
+ error: "Bridge mode is active. Chat is handled by your external AI tool (VS Code, Cursor, etc.) via MCP β€” not by the plugin itself.",
2195
+ });
2196
+ sendToPlugin({ type: "done", id: requestId, fullText: "" });
2197
+ return;
2198
+ } else {
2199
+ // Default: Claude
2200
+ const anthropicKey = getAnthropicApiKey();
2201
+ if (chatMode === "chat" && anthropicKey) {
2202
+ // Tier 3: Direct Anthropic API β€” fast streaming (~200ms first token)
2203
+ const { buildChatPrompt } = require("./shared-prompt-config");
2204
+ proc = runAnthropicChat({
2205
+ message: chatMessage,
2206
+ attachments: msg.attachments,
2207
+ conversation: msg.conversation,
2208
+ requestId,
2209
+ apiKey: anthropicKey,
2210
+ model: msg.model,
2211
+ systemPrompt: buildChatPrompt(),
2212
+ onEvent,
2213
+ });
2214
+ } else {
2215
+ // Tier 4: Claude CLI subprocess (code/dual mode, or no API key)
2216
+ proc = runClaude({
2217
+ message: chatMessage,
2218
+ attachments: msg.attachments,
2219
+ conversation: msg.conversation,
2220
+ requestId,
2221
+ model: msg.model,
2222
+ designSystemId: activeDesignSystemId,
2223
+ mode: chatMode,
2224
+ frameworkConfig: msg.frameworkConfig || {},
2225
+ onEvent,
2226
+ });
2227
+ }
2228
+ }
2229
+
2230
+ activeChatProcesses.set(requestId, proc);
2231
+ proc.on("close", () => activeChatProcesses.delete(requestId));
2232
+ return;
2233
+ } // end routeToAiProvider
2234
+ }
2235
+
2236
+ // Abort a running chat
2237
+ if (msg.type === "abort-chat") {
2238
+ const proc = activeChatProcesses.get(msg.id);
2239
+ if (proc) {
2240
+ proc.kill("SIGTERM");
2241
+ activeChatProcesses.delete(msg.id);
2242
+ console.log(` β›” chat aborted (id: ${msg.id})`);
2243
+ }
2244
+ return;
2245
+ }
2246
+
2247
+ // Reset conversation session (user clicked "New Chat" in plugin UI)
2248
+ if (msg.type === "new-conversation" || msg.type === "clear-history") {
2249
+ const resetMode = msg.mode || null; // null = reset all modes
2250
+ resetSession(resetMode);
2251
+ resetCodexSession(resetMode);
2252
+ console.log(` πŸ”„ conversation session reset${resetMode ? ` (${resetMode})` : " (all)"}`);
2253
+ return;
2254
+ }
2255
+
2256
+ // Bridge events (selection change, doc change etc.) β†’ forward to MCP
2257
+ if (msg.type === "bridge-event") {
2258
+ broadcastToMcpSockets(raw);
2259
+ console.log(` β†Ί plugin event: ${msg.eventType || "unknown"}`);
2260
+ return;
2261
+ }
2262
+
2263
+ // Plugin hello
2264
+ if (msg.type === "plugin-hello") {
2265
+ console.log(` Plugin identified: ${msg.fileName || "unknown"}`);
2266
+ return;
2267
+ }
2268
+
2269
+
2270
+
2271
+ // MCP tool response from plugin β†’ route back to the requesting MCP socket
2272
+ if (msg.id && !msg.method) {
2273
+ // Check if this is a relay-initiated request first
2274
+ const relayReq = pendingRelayRequests.get(msg.id);
2275
+ if (relayReq) {
2276
+ clearTimeout(relayReq.timer);
2277
+ pendingRelayRequests.delete(msg.id);
2278
+ if (msg.error) {
2279
+ relayReq.reject(new Error(msg.error));
2280
+ } else {
2281
+ relayReq.resolve(msg.result);
2282
+ }
2283
+ console.log(` ← relay request response (id: ${msg.id})`);
2284
+ return;
2285
+ }
2286
+
2287
+ const targetSocket = pendingRequests.get(msg.id);
2288
+ if (targetSocket && targetSocket.readyState === 1) {
2289
+ targetSocket.send(raw);
2290
+ console.log(` ← plugin response (id: ${msg.id})`);
2291
+ } else {
2292
+ broadcastToMcpSockets(raw);
2293
+ }
2294
+ pendingRequests.delete(msg.id);
2295
+ }
2296
+ return;
2297
+ }
2298
+
2299
+ // ── Messages from an MCP server ─────────────────────────────────────────
2300
+ if (msg.id && msg.method) {
2301
+ // Handle getActiveDesignSystemId directly β€” no need to forward to plugin
2302
+ if (msg.method === "getActiveDesignSystemId") {
2303
+ ws.send(JSON.stringify({ id: msg.id, result: activeDesignSystemId }));
2304
+ console.log(` ← relay responded: getActiveDesignSystemId = ${activeDesignSystemId || "none"}`);
2305
+ return;
2306
+ }
2307
+ if (pluginSocket && pluginSocket.readyState === 1) {
2308
+ pendingRequests.set(msg.id, ws);
2309
+ pluginSocket.send(JSON.stringify({
2310
+ type: "bridge-request",
2311
+ id: msg.id,
2312
+ method: msg.method,
2313
+ params: msg.params || {},
2314
+ }));
2315
+ console.log(` β†’ mcp request: ${msg.method} (id: ${msg.id})`);
2316
+ } else {
2317
+ ws.send(JSON.stringify({
2318
+ id: msg.id,
2319
+ error: "Figma plugin is not connected. Open Figma and run the Intelligence Bridge plugin.",
2320
+ }));
2321
+ console.log(` βœ— No plugin connected for: ${msg.method}`);
2322
+ }
2323
+ }
2324
+ });
2325
+
2326
+ ws.on("close", () => {
2327
+ if (isPlugin) {
2328
+ // P3: Grace period β€” wait before fully disconnecting plugin
2329
+ console.log(`⚠ Figma plugin disconnected β€” ${PLUGIN_GRACE_PERIOD_MS / 1000}s grace period`);
2330
+ pluginGraceState = { activeDesignSystemId };
2331
+ pluginGraceTimer = setTimeout(() => {
2332
+ pluginSocket = null;
2333
+ pluginGraceTimer = null;
2334
+ pluginGraceState = null;
2335
+ console.log("⚠ Plugin grace period expired β€” fully disconnected");
2336
+ sendRelayStatus(null, hasConnectedMcpSocket());
2337
+ }, PLUGIN_GRACE_PERIOD_MS);
2338
+ } else {
2339
+ mcpSockets.delete(ws);
2340
+ for (const [requestId, requestSocket] of pendingRequests.entries()) {
2341
+ if (requestSocket === ws) pendingRequests.delete(requestId);
2342
+ }
2343
+ console.log("⚠ MCP server disconnected");
2344
+ sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
2345
+ }
2346
+ });
2347
+
2348
+ ws.on("error", (err) => {
2349
+ console.error(`WebSocket error (${isPlugin ? "plugin" : "mcp"}):`, err.message);
2350
+ });
2351
+ });
2352
+
2353
+ // ── Graceful shutdown ────────────────────────────────────────────────────────
2354
+ process.on("SIGINT", () => {
2355
+ console.log("\nShutting down relay…");
2356
+ for (const proc of activeChatProcesses.values()) proc.kill();
2357
+ if (_mcpProc) _mcpProc.kill();
2358
+ if (pluginGraceTimer) clearTimeout(pluginGraceTimer);
2359
+ wss.close();
2360
+ process.exit(0);
2361
+ });
2362
+
2363
+ })(); // end async IIFE for port fallback