@sarjallab09/figma-intelligence 1.1.0 → 1.2.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 (297) hide show
  1. package/README.md +67 -36
  2. package/dist/bin/cli.js +2 -0
  3. package/dist/design-bridge/bridge.js +2 -0
  4. package/dist/figma-bridge-plugin/bridge-relay.js +2 -0
  5. package/dist/figma-bridge-plugin/code.js +1 -0
  6. package/{figma-bridge-plugin → dist/figma-bridge-plugin}/package-lock.json +0 -3
  7. package/dist/figma-bridge-plugin/ui.html +4970 -0
  8. package/dist/figma-intelligence-layer/dist/index.js +2 -0
  9. package/dist/scripts/clean-existing-chunks.js +2 -0
  10. package/dist/scripts/connect-ai-tool.js +2 -0
  11. package/dist/scripts/convert-hub-pdfs.js +2 -0
  12. package/dist/scripts/figma-mcp-status.js +2 -0
  13. package/dist/scripts/register-codex-mcp.js +2 -0
  14. package/dist/scripts/test-copilot-chat.js +2 -0
  15. package/package.json +11 -8
  16. package/bin/cli.js +0 -859
  17. package/design-bridge/bridge.js +0 -196
  18. package/design-bridge/lib/assets.js +0 -367
  19. package/design-bridge/lib/prompt.js +0 -85
  20. package/design-bridge/lib/server.js +0 -66
  21. package/design-bridge/lib/stitch.js +0 -37
  22. package/design-bridge/lib/tokens.js +0 -82
  23. package/design-bridge/package-lock.json +0 -579
  24. package/figma-bridge-plugin/README.md +0 -97
  25. package/figma-bridge-plugin/anthropic-chat-runner.js +0 -192
  26. package/figma-bridge-plugin/bridge-relay.js +0 -2505
  27. package/figma-bridge-plugin/chat-runner.js +0 -485
  28. package/figma-bridge-plugin/code.js +0 -1534
  29. package/figma-bridge-plugin/codex-runner.js +0 -505
  30. package/figma-bridge-plugin/component-schemas.js +0 -110
  31. package/figma-bridge-plugin/content-context.js +0 -869
  32. package/figma-bridge-plugin/create-button.js +0 -216
  33. package/figma-bridge-plugin/gemini-cli-runner.js +0 -291
  34. package/figma-bridge-plugin/gemini-runner.js +0 -187
  35. package/figma-bridge-plugin/html-to-figma.js +0 -927
  36. package/figma-bridge-plugin/knowledge-hub/.gitkeep +0 -0
  37. package/figma-bridge-plugin/knowledge-hub/uspec-references/anatomy-spec.md +0 -159
  38. package/figma-bridge-plugin/knowledge-hub/uspec-references/api-spec.md +0 -162
  39. package/figma-bridge-plugin/knowledge-hub/uspec-references/color-spec.md +0 -148
  40. package/figma-bridge-plugin/knowledge-hub/uspec-references/full-spec-template.md +0 -314
  41. package/figma-bridge-plugin/knowledge-hub/uspec-references/property-spec.md +0 -175
  42. package/figma-bridge-plugin/knowledge-hub/uspec-references/screen-reader-spec.md +0 -180
  43. package/figma-bridge-plugin/knowledge-hub/uspec-references/structure-spec.md +0 -165
  44. package/figma-bridge-plugin/perplexity-runner.js +0 -188
  45. package/figma-bridge-plugin/references/SKILL.md +0 -178
  46. package/figma-bridge-plugin/references/anatomy-spec.md +0 -159
  47. package/figma-bridge-plugin/references/api-spec.md +0 -162
  48. package/figma-bridge-plugin/references/color-spec.md +0 -148
  49. package/figma-bridge-plugin/references/full-spec-template.md +0 -314
  50. package/figma-bridge-plugin/references/property-spec.md +0 -175
  51. package/figma-bridge-plugin/references/screen-reader-spec.md +0 -180
  52. package/figma-bridge-plugin/references/structure-spec.md +0 -165
  53. package/figma-bridge-plugin/shared-prompt-config.js +0 -645
  54. package/figma-bridge-plugin/spec-helpers/build-table.js +0 -269
  55. package/figma-bridge-plugin/spec-helpers/classify-elements.js +0 -189
  56. package/figma-bridge-plugin/spec-helpers/index.js +0 -35
  57. package/figma-bridge-plugin/spec-helpers/parse-figma-link.js +0 -49
  58. package/figma-bridge-plugin/spec-helpers/position-markers.js +0 -158
  59. package/figma-bridge-plugin/stitch-auth.js +0 -322
  60. package/figma-bridge-plugin/stitch-runner.js +0 -1427
  61. package/figma-bridge-plugin/token-resolver.js +0 -107
  62. package/figma-bridge-plugin/ui.html +0 -4542
  63. package/figma-intelligence-layer/.env.example +0 -39
  64. package/figma-intelligence-layer/docs/local-image-generation.md +0 -60
  65. package/figma-intelligence-layer/examples/comfyui-workflow-template.example.json +0 -101
  66. package/figma-intelligence-layer/jest.config.js +0 -14
  67. package/figma-intelligence-layer/mcp-config.json +0 -19
  68. package/figma-intelligence-layer/package-lock.json +0 -5892
  69. package/figma-intelligence-layer/scripts/setup-comfyui-local.sh +0 -67
  70. package/figma-intelligence-layer/scripts/start-comfyui.sh +0 -33
  71. package/figma-intelligence-layer/src/index.ts +0 -2233
  72. package/figma-intelligence-layer/src/shared/auto-layout-validator.ts +0 -404
  73. package/figma-intelligence-layer/src/shared/cache.ts +0 -187
  74. package/figma-intelligence-layer/src/shared/color-operations.ts +0 -533
  75. package/figma-intelligence-layer/src/shared/color-utils.ts +0 -138
  76. package/figma-intelligence-layer/src/shared/component-script-builder.ts +0 -413
  77. package/figma-intelligence-layer/src/shared/component-templates.ts +0 -2767
  78. package/figma-intelligence-layer/src/shared/concept-taxonomy.ts +0 -694
  79. package/figma-intelligence-layer/src/shared/decision-log.ts +0 -128
  80. package/figma-intelligence-layer/src/shared/design-system-context.ts +0 -568
  81. package/figma-intelligence-layer/src/shared/design-system-intelligence.ts +0 -131
  82. package/figma-intelligence-layer/src/shared/design-system-matcher.ts +0 -184
  83. package/figma-intelligence-layer/src/shared/design-system-normalizers.ts +0 -196
  84. package/figma-intelligence-layer/src/shared/design-system-tokens.ts +0 -295
  85. package/figma-intelligence-layer/src/shared/dtcg-validator.ts +0 -530
  86. package/figma-intelligence-layer/src/shared/enrichment-pipeline.ts +0 -671
  87. package/figma-intelligence-layer/src/shared/figma-bridge.ts +0 -1418
  88. package/figma-intelligence-layer/src/shared/font-config.ts +0 -126
  89. package/figma-intelligence-layer/src/shared/icon-catalog.ts +0 -360
  90. package/figma-intelligence-layer/src/shared/icon-fetch.ts +0 -80
  91. package/figma-intelligence-layer/src/shared/prototype-script-builder.ts +0 -162
  92. package/figma-intelligence-layer/src/shared/response-compression.ts +0 -440
  93. package/figma-intelligence-layer/src/shared/semantic-token-catalog.ts +0 -324
  94. package/figma-intelligence-layer/src/shared/token-binder.ts +0 -505
  95. package/figma-intelligence-layer/src/shared/token-math.ts +0 -427
  96. package/figma-intelligence-layer/src/shared/token-naming.ts +0 -468
  97. package/figma-intelligence-layer/src/shared/token-utils.ts +0 -420
  98. package/figma-intelligence-layer/src/shared/types.ts +0 -346
  99. package/figma-intelligence-layer/src/shared/typography-presets.ts +0 -94
  100. package/figma-intelligence-layer/src/shared/unsplash.ts +0 -165
  101. package/figma-intelligence-layer/src/shared/vision-client.ts +0 -607
  102. package/figma-intelligence-layer/src/shared/vision-provider-anthropic.ts +0 -334
  103. package/figma-intelligence-layer/src/shared/vision-provider-openai.ts +0 -446
  104. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-handler.ts +0 -782
  105. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotate-renderer.ts +0 -496
  106. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/a11y-annotation-kit.ts +0 -230
  107. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/colorblind-sim.ts +0 -66
  108. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/index.ts +0 -810
  109. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-analyzer.ts +0 -1191
  110. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-figma-page.ts +0 -1346
  111. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/keyboard-sr-order-handler.ts +0 -148
  112. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-figma-page.ts +0 -499
  113. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/vpat-report.ts +0 -910
  114. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-checker.ts +0 -989
  115. package/figma-intelligence-layer/src/tools/phase1-vision/a11y-audit/wcag-criteria.ts +0 -1160
  116. package/figma-intelligence-layer/src/tools/phase1-vision/design-from-ref/index.ts +0 -424
  117. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/component-recognizer.ts +0 -38
  118. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/ds-matcher.ts +0 -111
  119. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/font-matcher.ts +0 -114
  120. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/icon-resolver.ts +0 -103
  121. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/index.ts +0 -1060
  122. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/layout-segmenter.ts +0 -18
  123. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/token-inferencer.ts +0 -39
  124. package/figma-intelligence-layer/src/tools/phase1-vision/screen-cloner/vision-pipeline.ts +0 -58
  125. package/figma-intelligence-layer/src/tools/phase1-vision/sketch-to-design/index.ts +0 -298
  126. package/figma-intelligence-layer/src/tools/phase1-vision/visual-audit/index.ts +0 -197
  127. package/figma-intelligence-layer/src/tools/phase2-accuracy/component-audit/index.ts +0 -494
  128. package/figma-intelligence-layer/src/tools/phase2-accuracy/intent-translator/index.ts +0 -356
  129. package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/container-patterns.ts +0 -123
  130. package/figma-intelligence-layer/src/tools/phase2-accuracy/layout-intelligence/index.ts +0 -663
  131. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/built-in-rules.yaml +0 -56
  132. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/index.ts +0 -614
  133. package/figma-intelligence-layer/src/tools/phase2-accuracy/lint-rules/rule-engine.ts +0 -113
  134. package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/color-theory.ts +0 -178
  135. package/figma-intelligence-layer/src/tools/phase2-accuracy/theme-generator/index.ts +0 -470
  136. package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/index.ts +0 -429
  137. package/figma-intelligence-layer/src/tools/phase2-accuracy/variant-expander/token-override-maps.ts +0 -226
  138. package/figma-intelligence-layer/src/tools/phase3-generation/ai-image-insert/index.ts +0 -535
  139. package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/index.ts +0 -660
  140. package/figma-intelligence-layer/src/tools/phase3-generation/component-archaeologist/pattern-fingerprints.ts +0 -209
  141. package/figma-intelligence-layer/src/tools/phase3-generation/composition-builder/index.ts +0 -540
  142. package/figma-intelligence-layer/src/tools/phase3-generation/figma-animated-build.ts +0 -391
  143. package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/index.ts +0 -2019
  144. package/figma-intelligence-layer/src/tools/phase3-generation/page-architect/screen-templates.ts +0 -131
  145. package/figma-intelligence-layer/src/tools/phase3-generation/prototype-map/index.ts +0 -381
  146. package/figma-intelligence-layer/src/tools/phase3-generation/prototype-wire/index.ts +0 -565
  147. package/figma-intelligence-layer/src/tools/phase3-generation/swarm-build/index.ts +0 -764
  148. package/figma-intelligence-layer/src/tools/phase3-generation/system-drift/index.ts +0 -535
  149. package/figma-intelligence-layer/src/tools/phase3-generation/unsplash-search/index.ts +0 -84
  150. package/figma-intelligence-layer/src/tools/phase3-generation/url-to-frame/index.ts +0 -401
  151. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/css-animations.ts +0 -68
  152. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/framer-motion.ts +0 -78
  153. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/code-generators/swift-animations.ts +0 -93
  154. package/figma-intelligence-layer/src/tools/phase4-sync/animation-specifier/index.ts +0 -596
  155. package/figma-intelligence-layer/src/tools/phase4-sync/ci-check/index.ts +0 -462
  156. package/figma-intelligence-layer/src/tools/phase4-sync/export-tokens/index.ts +0 -1470
  157. package/figma-intelligence-layer/src/tools/phase4-sync/generate-component-code/index.ts +0 -829
  158. package/figma-intelligence-layer/src/tools/phase4-sync/handoff-spec/index.ts +0 -702
  159. package/figma-intelligence-layer/src/tools/phase4-sync/icon-library-sync/index.ts +0 -483
  160. package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/index.ts +0 -501
  161. package/figma-intelligence-layer/src/tools/phase4-sync/sync-from-code/storybook-parser.ts +0 -106
  162. package/figma-intelligence-layer/src/tools/phase4-sync/watch-docs/index.ts +0 -676
  163. package/figma-intelligence-layer/src/tools/phase4-sync/webhook-listener/index.ts +0 -560
  164. package/figma-intelligence-layer/src/tools/phase5-governance/apg-doc/index.ts +0 -1043
  165. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/component-detection.ts +0 -620
  166. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/anatomy.ts +0 -331
  167. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/color-tokens.ts +0 -77
  168. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/properties.ts +0 -54
  169. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/snapshot.ts +0 -287
  170. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/spacing.ts +0 -71
  171. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/states.ts +0 -43
  172. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/extractors/typography.ts +0 -71
  173. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/index.ts +0 -221
  174. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/_default.ts +0 -166
  175. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/accordion.ts +0 -232
  176. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/alert.ts +0 -234
  177. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar-group.ts +0 -270
  178. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/avatar.ts +0 -249
  179. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/badge.ts +0 -231
  180. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/banner.ts +0 -293
  181. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/breadcrumb.ts +0 -240
  182. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/button.ts +0 -243
  183. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/calendar.ts +0 -307
  184. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/card.ts +0 -143
  185. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/checkbox.ts +0 -227
  186. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/chip.ts +0 -233
  187. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/combobox.ts +0 -282
  188. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/datepicker.ts +0 -276
  189. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/divider.ts +0 -223
  190. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/drawer.ts +0 -255
  191. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/dropdown-menu.ts +0 -289
  192. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/empty-state.ts +0 -261
  193. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/file-uploader.ts +0 -290
  194. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/form.ts +0 -265
  195. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/grid.ts +0 -238
  196. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/icon.ts +0 -255
  197. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/index.ts +0 -128
  198. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-edit.ts +0 -286
  199. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/inline-message.ts +0 -255
  200. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/input.ts +0 -330
  201. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/link.ts +0 -247
  202. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/list.ts +0 -250
  203. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/menu.ts +0 -247
  204. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/modal.ts +0 -144
  205. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navbar.ts +0 -264
  206. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/navigation.ts +0 -251
  207. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/number-input.ts +0 -261
  208. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/pagination.ts +0 -248
  209. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/popover.ts +0 -270
  210. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/progress.ts +0 -251
  211. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/radio.ts +0 -142
  212. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/range-slider.ts +0 -282
  213. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/rating.ts +0 -250
  214. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/search.ts +0 -258
  215. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/segmented-control.ts +0 -265
  216. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/select.ts +0 -319
  217. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/skeleton.ts +0 -256
  218. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/slider.ts +0 -232
  219. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/spinner.ts +0 -239
  220. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/status-dot.ts +0 -252
  221. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/stepper.ts +0 -270
  222. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/table.ts +0 -244
  223. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tabs.ts +0 -143
  224. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tag.ts +0 -243
  225. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/textarea.ts +0 -259
  226. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/time-picker.ts +0 -293
  227. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toast.ts +0 -144
  228. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toggle.ts +0 -289
  229. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/toolbar.ts +0 -267
  230. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/tooltip.ts +0 -232
  231. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/treeview.ts +0 -257
  232. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/knowledge/typography.ts +0 -319
  233. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/legacy-compat.ts +0 -121
  234. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/anatomy-diagram.ts +0 -430
  235. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/figma-page.ts +0 -312
  236. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/json.ts +0 -129
  237. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/markdown.ts +0 -78
  238. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/renderers/visual-doc.ts +0 -2333
  239. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/accessibility.ts +0 -100
  240. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/anatomy.ts +0 -32
  241. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/color-tokens.ts +0 -59
  242. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/content-guidance.ts +0 -18
  243. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/design-tokens.ts +0 -53
  244. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/interaction-rules.ts +0 -19
  245. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/overview.ts +0 -91
  246. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/properties-api.ts +0 -71
  247. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/qa-criteria.ts +0 -19
  248. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/related-components.ts +0 -110
  249. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/responsive.ts +0 -19
  250. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/size-specs.ts +0 -67
  251. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/spacing-structure.ts +0 -58
  252. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/state-specs.ts +0 -79
  253. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/states.ts +0 -50
  254. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/type-hierarchy.ts +0 -33
  255. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/typography.ts +0 -55
  256. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/usage-guidelines.ts +0 -73
  257. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/sections/variants.ts +0 -81
  258. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec/types.ts +0 -409
  259. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/index.ts +0 -198
  260. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/renderer.ts +0 -701
  261. package/figma-intelligence-layer/src/tools/phase5-governance/component-spec-sheet/types.ts +0 -88
  262. package/figma-intelligence-layer/src/tools/phase5-governance/decision-log/index.ts +0 -135
  263. package/figma-intelligence-layer/src/tools/phase5-governance/design-decision-log/index.ts +0 -491
  264. package/figma-intelligence-layer/src/tools/phase5-governance/ds-primitives/index.ts +0 -416
  265. package/figma-intelligence-layer/src/tools/phase5-governance/ds-scaffolder/index.ts +0 -722
  266. package/figma-intelligence-layer/src/tools/phase5-governance/ds-variables/index.ts +0 -449
  267. package/figma-intelligence-layer/src/tools/phase5-governance/health-report/index.ts +0 -393
  268. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/index.ts +0 -406
  269. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/figma-page.ts +0 -292
  270. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/json.ts +0 -24
  271. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/markdown.ts +0 -172
  272. package/figma-intelligence-layer/src/tools/phase5-governance/taxonomy-docs/renderers/naming-guide.ts +0 -409
  273. package/figma-intelligence-layer/src/tools/phase5-governance/token-analytics/index.ts +0 -594
  274. package/figma-intelligence-layer/src/tools/phase5-governance/token-docs/index.ts +0 -710
  275. package/figma-intelligence-layer/src/tools/phase5-governance/token-migrate/index.ts +0 -458
  276. package/figma-intelligence-layer/src/tools/phase5-governance/token-naming/index.ts +0 -134
  277. package/figma-intelligence-layer/tests/apg-doc.test.ts +0 -101
  278. package/figma-intelligence-layer/tests/design-system-context.test.ts +0 -152
  279. package/figma-intelligence-layer/tests/design-system-matcher.test.ts +0 -144
  280. package/figma-intelligence-layer/tests/figma-bridge.test.ts +0 -83
  281. package/figma-intelligence-layer/tests/generate-image-and-insert.test.ts +0 -56
  282. package/figma-intelligence-layer/tests/screen-cloner-regression.test.ts +0 -69
  283. package/figma-intelligence-layer/tests/smoke.test.ts +0 -174
  284. package/figma-intelligence-layer/tests/spec-generator.test.ts +0 -127
  285. package/figma-intelligence-layer/tests/token-migrate.test.ts +0 -21
  286. package/figma-intelligence-layer/tests/token-naming.test.ts +0 -30
  287. package/figma-intelligence-layer/tsconfig.json +0 -19
  288. package/scripts/clean-existing-chunks.js +0 -179
  289. package/scripts/connect-ai-tool.js +0 -490
  290. package/scripts/convert-hub-pdfs.js +0 -425
  291. package/scripts/figma-mcp-status.js +0 -349
  292. package/scripts/register-codex-mcp.js +0 -96
  293. /package/{design-bridge → dist/design-bridge}/.env.example +0 -0
  294. /package/{design-bridge → dist/design-bridge}/package.json +0 -0
  295. /package/{figma-bridge-plugin → dist/figma-bridge-plugin}/manifest.json +0 -0
  296. /package/{figma-bridge-plugin → dist/figma-bridge-plugin}/package.json +0 -0
  297. /package/{figma-intelligence-layer → dist/figma-intelligence-layer}/package.json +0 -0
@@ -1,2505 +0,0 @@
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
- // ── UX Researcher: capture current Figma selection as compact context ────────
1224
-
1225
- const SELECTION_CONTEXT_MAX_CHARS = 3000;
1226
- const SELECTION_MAX_NODES = 5;
1227
-
1228
- async function captureSelectionContext() {
1229
- const selection = await requestFromPlugin("getSelection", {});
1230
- if (!selection || !Array.isArray(selection) || selection.length === 0) return null;
1231
-
1232
- const nodesToFetch = selection.slice(0, SELECTION_MAX_NODES);
1233
- const nodeDetails = await Promise.all(
1234
- nodesToFetch.map((n) => requestFromPlugin("getNode", { nodeId: n.id }).catch(() => null))
1235
- );
1236
-
1237
- const lines = [];
1238
- for (const node of nodeDetails) {
1239
- if (!node) continue;
1240
- lines.push(formatNodeForContext(node, 0));
1241
- }
1242
- if (selection.length > SELECTION_MAX_NODES) {
1243
- lines.push(`[... ${selection.length - SELECTION_MAX_NODES} more selected nodes omitted]`);
1244
- }
1245
-
1246
- let body = lines.join("\n");
1247
- if (body.length > SELECTION_CONTEXT_MAX_CHARS) {
1248
- body = body.slice(0, SELECTION_CONTEXT_MAX_CHARS) + "\n[... truncated for brevity]";
1249
- }
1250
-
1251
- return `=== CURRENT FIGMA SELECTION ===\n${body}\n=== END SELECTION ===`;
1252
- }
1253
-
1254
- function formatNodeForContext(node, depth) {
1255
- const indent = " ".repeat(depth);
1256
- const parts = [`${indent}[${node.type}] "${node.name}"`];
1257
-
1258
- // Dimensions
1259
- if (node.width != null && node.height != null) {
1260
- parts[0] += ` (${Math.round(node.width)}×${Math.round(node.height)})`;
1261
- }
1262
-
1263
- // Auto-layout
1264
- if (node.layoutMode && node.layoutMode !== "NONE") {
1265
- parts.push(`${indent} layout: ${node.layoutMode}, spacing: ${node.itemSpacing}px, padding: ${node.paddingTop}/${node.paddingRight}/${node.paddingBottom}/${node.paddingLeft}`);
1266
- }
1267
-
1268
- // Text content
1269
- if (node.characters) {
1270
- const text = node.characters.length > 120 ? node.characters.slice(0, 120) + "…" : node.characters;
1271
- parts.push(`${indent} text: "${text}"`);
1272
- if (node.fontSize) parts.push(`${indent} fontSize: ${node.fontSize}`);
1273
- }
1274
-
1275
- // Component instance
1276
- if (node.mainComponentName) {
1277
- parts.push(`${indent} component: ${node.mainComponentName}`);
1278
- }
1279
-
1280
- // Corner radius
1281
- if (node.cornerRadius != null && node.cornerRadius > 0) {
1282
- parts.push(`${indent} radius: ${node.cornerRadius}`);
1283
- }
1284
-
1285
- // Children (1 level deep)
1286
- if (node.children && node.children.length > 0) {
1287
- const maxChildren = 10;
1288
- const shown = node.children.slice(0, maxChildren);
1289
- for (const child of shown) {
1290
- parts.push(`${indent} - [${child.type}] "${child.name}"`);
1291
- }
1292
- if (node.children.length > maxChildren) {
1293
- parts.push(`${indent} ... +${node.children.length - maxChildren} more children`);
1294
- }
1295
- }
1296
-
1297
- return parts.join("\n");
1298
- }
1299
-
1300
- function sendToVscode(payload, targetWs) {
1301
- if (targetWs && targetWs.readyState === 1) {
1302
- targetWs.send(JSON.stringify(payload));
1303
- }
1304
- }
1305
-
1306
- function broadcastToVscodeSockets(payload) {
1307
- const data = JSON.stringify(payload);
1308
- for (const ws of vscodeSockets) {
1309
- if (ws.readyState === 1) ws.send(data);
1310
- }
1311
- }
1312
-
1313
- // ── P3: Grace period — retain plugin state briefly on disconnect ──────────────
1314
- const PLUGIN_GRACE_PERIOD_MS = 5000;
1315
- let pluginGraceTimer = null;
1316
- let pluginGraceState = null; // stashed state during grace period
1317
-
1318
- // ── P3: Heartbeat — detect dead connections ──────────────────────────────────
1319
- const HEARTBEAT_INTERVAL_MS = 30000;
1320
-
1321
- function setupHeartbeat(wss) {
1322
- const interval = setInterval(() => {
1323
- wss.clients.forEach((ws) => {
1324
- if (ws._isAlive === false) {
1325
- console.log(" ⚠ Terminating unresponsive connection");
1326
- return ws.terminate();
1327
- }
1328
- ws._isAlive = false;
1329
- ws.ping();
1330
- });
1331
- }, HEARTBEAT_INTERVAL_MS);
1332
- wss.on("close", () => clearInterval(interval));
1333
- }
1334
-
1335
- // ── P3: Port fallback — try ports 9001-9010 ──────────────────────────────────
1336
- function createServerWithFallback(basePort, maxRetries = 9) {
1337
- return new Promise((resolve, reject) => {
1338
- let attempt = 0;
1339
- function tryPort(port) {
1340
- const server = new WebSocketServer({ port });
1341
- server.on("listening", () => {
1342
- PORT = port;
1343
- resolve(server);
1344
- });
1345
- server.on("error", (err) => {
1346
- if (err.code === "EADDRINUSE" && attempt < maxRetries) {
1347
- attempt++;
1348
- console.log(` ⚠ Port ${port} in use, trying ${port + 1}…`);
1349
- tryPort(port + 1);
1350
- } else {
1351
- reject(err);
1352
- }
1353
- });
1354
- }
1355
- tryPort(basePort);
1356
- });
1357
- }
1358
-
1359
- // ── WebSocket Server ─────────────────────────────────────────────────────────
1360
- (async () => {
1361
- let wss;
1362
- try {
1363
- wss = await createServerWithFallback(BASE_PORT);
1364
- } catch (err) {
1365
- console.error(`Fatal: could not bind to any port in range ${BASE_PORT}-${BASE_PORT + 9}:`, err.message);
1366
- process.exit(1);
1367
- }
1368
-
1369
- console.log(`\n🔌 Figma Intelligence Bridge Relay`);
1370
- console.log(` Listening on ws://localhost:${PORT}`);
1371
- console.log(` MCP server → connects to ws://localhost:${PORT}`);
1372
- console.log(` Figma plugin → connects to ws://localhost:${PORT}/plugin`);
1373
- console.log(` VS Code ext → connects to ws://localhost:${PORT}/vscode`);
1374
- console.log(` Waiting for connections…\n`);
1375
-
1376
- // Rewrite MCP config with the actual port (chat-runner wrote initial config with default port)
1377
- writeMcpConfig(PORT);
1378
-
1379
- // Start heartbeat monitoring
1380
- setupHeartbeat(wss);
1381
-
1382
- // Pre-warm knowledge hub (load .chunks.json files into cache for instant first query)
1383
- prewarmHub().then((count) => {
1384
- if (count > 0) console.log(` 📚 Knowledge hub pre-warmed: ${count} chunked source(s) cached`);
1385
- }).catch(() => {});
1386
-
1387
- // Start the MCP server as a persistent child process so the plugin
1388
- // always shows "Connected" — not just during active chat requests.
1389
- startPersistentMcpServer();
1390
-
1391
- wss.on("connection", (ws, req) => {
1392
- const path = req.url || "/";
1393
- const isPlugin = path.includes("/plugin");
1394
- const isVscode = path.includes("/vscode");
1395
-
1396
- // P3: Heartbeat — mark connection alive on pong
1397
- ws._isAlive = true;
1398
- ws.on("pong", () => { ws._isAlive = true; });
1399
-
1400
- if (isVscode) {
1401
- vscodeSockets.add(ws);
1402
- console.log("✅ VS Code client connected");
1403
- sendRelayStatus(ws, hasConnectedMcpSocket());
1404
- refreshAuthState().catch(() => {});
1405
- // Notify plugin that VS Code is connected
1406
- sendToPlugin({ type: "vscode-connected", connected: true, count: vscodeSockets.size });
1407
- ws.on("close", () => {
1408
- vscodeSockets.delete(ws);
1409
- console.log(" ↺ VS Code client disconnected");
1410
- sendToPlugin({ type: "vscode-connected", connected: vscodeSockets.size > 0, count: vscodeSockets.size });
1411
- });
1412
- } else if (isPlugin) {
1413
- // P3: Cancel grace timer if plugin reconnects within grace period
1414
- if (pluginGraceTimer) {
1415
- clearTimeout(pluginGraceTimer);
1416
- pluginGraceTimer = null;
1417
- pluginGraceState = null;
1418
- console.log(" ↺ Plugin reconnected within grace period");
1419
- }
1420
- pluginSocket = ws;
1421
- console.log("✅ Figma plugin connected");
1422
- sendRelayStatus(ws, hasConnectedMcpSocket());
1423
- refreshAuthState().catch(() => {});
1424
- } else {
1425
- mcpSockets.add(ws);
1426
- console.log("✅ MCP server connected");
1427
- sendRelayStatus(pluginSocket, true);
1428
- }
1429
-
1430
- ws.on("message", (data) => {
1431
- const raw = data.toString();
1432
- let msg;
1433
- try { msg = JSON.parse(raw); } catch { return; }
1434
-
1435
- // Global debug: log every message type
1436
- 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 {}
1437
-
1438
- // ── Messages from VS Code extension ────────────────────────────────────
1439
- if (isVscode) {
1440
-
1441
- if (msg.type === "vscode-hello") {
1442
- console.log(` VS Code client: ${msg.clientType || "unknown"} v${msg.version || "?"}`);
1443
- return;
1444
- }
1445
-
1446
- // Set AI provider
1447
- if (msg.type === "set-provider") {
1448
- providerConfig = { provider: msg.provider || "claude", apiKey: msg.apiKey || null, projectId: msg.projectId || null };
1449
- saveProviderConfig();
1450
- console.log(` 🔑 provider set (vscode): ${providerConfig.provider}`);
1451
- sendToVscode({ type: "provider-stored", provider: providerConfig.provider }, ws);
1452
- refreshAuthState().catch(() => {});
1453
- return;
1454
- }
1455
-
1456
- // Set design system
1457
- if (msg.type === "set-design-system") {
1458
- const newId = msg.designSystemId || null;
1459
- if (newId !== activeDesignSystemId) {
1460
- activeDesignSystemId = newId;
1461
- resetSession();
1462
- resetCodexSession();
1463
- console.log(` 🎨 design system (vscode): ${newId || "none"} (sessions reset)`);
1464
- }
1465
- sendToVscode({ type: "design-system-stored", designSystemId: activeDesignSystemId }, ws);
1466
- // Broadcast DS change to all connected MCP sockets so intelligence layer stays in sync
1467
- broadcastToMcpSockets(JSON.stringify({ type: "design-system-changed", designSystemId: activeDesignSystemId }));
1468
- return;
1469
- }
1470
-
1471
- // Chat message from VS Code (supports mode: "dual", "code", "chat")
1472
- if (msg.type === "chat") {
1473
- const requestId = msg.id;
1474
- const prov = providerConfig.provider || "claude";
1475
- const chatMode = msg.mode || "dual";
1476
- let chatMessage = msg.message || "";
1477
-
1478
- // Pre-parse Figma links so the AI doesn't need to extract file_key/node_id
1479
- const figmaLinkMatch = chatMessage.match(/https:\/\/www\.figma\.com\/(?:design|file)\/[^\s]+/);
1480
- if (figmaLinkMatch) {
1481
- try {
1482
- const { parseFigmaLink } = require("./spec-helpers/parse-figma-link");
1483
- const parsed = parseFigmaLink(figmaLinkMatch[0]);
1484
- chatMessage += `\n\n[Pre-parsed Figma link: file_key="${parsed.file_key}", node_id="${parsed.node_id}"]`;
1485
- } catch (e) { /* ignore parse errors */ }
1486
- }
1487
-
1488
- // ── Component Doc Generator: handle follow-up after chooser (VS Code) ─
1489
- if (pendingDocGenChooser && (chatMode === "code" || chatMode === "dual")) {
1490
- const elapsed = Date.now() - pendingDocGenChooser.shownAt;
1491
- if (elapsed < 10 * 60 * 1000) {
1492
- const reply = (chatMessage || "").trim().toLowerCase();
1493
- const specMap = {
1494
- "1": "anatomy", "anatomy": "anatomy",
1495
- "2": "api", "api": "api",
1496
- "3": "property", "properties": "property", "property": "property",
1497
- "4": "color", "color": "color",
1498
- "5": "structure", "structure": "structure",
1499
- "6": "screen-reader", "screen reader": "screen-reader", "screen-reader": "screen-reader",
1500
- "all": "all",
1501
- };
1502
- const matched = specMap[reply];
1503
- const multiMatch = reply.match(/^[\d,\s]+$/);
1504
- if (matched || multiMatch) {
1505
- const original = pendingDocGenChooser.originalMessage;
1506
- pendingDocGenChooser = null;
1507
- // CRITICAL: Reset session so the AI starts fresh with the correct
1508
- // system prompt containing the spec-type skill addendum.
1509
- // Without this, --resume reuses the old system prompt which lacks
1510
- // the tool restrictions and spec reference instructions.
1511
- resetSession(chatMode);
1512
- console.log(` 📋 Component Doc Generator (vscode): reset ${chatMode} session for fresh system prompt`);
1513
- if (matched === "all" || (multiMatch && reply.replace(/\s/g, "").split(",").length >= 6)) {
1514
- 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.`;
1515
- console.log(` 📋 Component Doc Generator (vscode): user chose ALL`);
1516
- } else if (multiMatch) {
1517
- const nums = reply.replace(/\s/g, "").split(",").filter(Boolean);
1518
- const types = nums.map(n => specMap[n]).filter(Boolean);
1519
- chatMessage = `${original}\n\nThe user selected these spec types: ${types.join(", ")}. Generate a create ${types[0]} spec for the component first.`;
1520
- console.log(` 📋 Component Doc Generator (vscode): user chose [${types.join(", ")}]`);
1521
- } else {
1522
- chatMessage = `${original}\n\nThe user selected: create ${matched} spec for this component.`;
1523
- console.log(` 📋 Component Doc Generator (vscode): user chose "${matched}"`);
1524
- }
1525
- } else {
1526
- pendingDocGenChooser = null;
1527
- }
1528
- } else {
1529
- pendingDocGenChooser = null;
1530
- }
1531
- }
1532
-
1533
- // ── Component Doc Generator chooser intercept (VS Code) ──────────
1534
- if (chatMode === "code" || chatMode === "dual") {
1535
- const { detectActiveSkills } = require("./shared-prompt-config");
1536
- const detectedSkills = detectActiveSkills(chatMessage);
1537
- if (detectedSkills.some(s => s === "Component Doc Generator:all")) {
1538
- console.log(` 📋 Component Doc Generator (vscode): presenting spec type chooser`);
1539
- pendingDocGenChooser = { originalMessage: chatMessage, shownAt: Date.now() };
1540
- const chooserText = `I can generate the following detailed spec types for your component:\n\n` +
1541
- `1. **Anatomy** — Numbered markers on each element + attribute table with semantic notes\n` +
1542
- `2. **API** — Property tables with values, defaults, required/optional status, and configuration examples\n` +
1543
- `3. **Properties** — Visual exhibits for variant axes, boolean toggles, variable modes, and child properties\n` +
1544
- `4. **Color** — Design token mapping for every element across states and variants\n` +
1545
- `5. **Structure** — Dimensions, spacing, padding tables across size/density variants\n` +
1546
- `6. **Screen Reader** — VoiceOver, TalkBack, and ARIA accessibility specs per platform\n\n` +
1547
- `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.`;
1548
- sendToVscode({ type: "phase_start", id: requestId, phase: "Skills: Component Doc Generator" }, ws);
1549
- sendToVscode({ type: "text_delta", id: requestId, delta: chooserText }, ws);
1550
- sendToVscode({ type: "done", id: requestId, fullText: chooserText }, ws);
1551
- return;
1552
- }
1553
- }
1554
-
1555
- // Inject knowledge grounding if active (relevance-filtered)
1556
- if (activeContentSources.size > 0) {
1557
- const groundingCtx = buildGroundingContext(activeContentSources, msg.message);
1558
- if (groundingCtx) {
1559
- chatMessage = groundingCtx + "\n---\n\nUser question: " + chatMessage;
1560
- }
1561
- }
1562
-
1563
- console.log(` 💬 vscode chat [${prov}/${chatMode}] (id: ${requestId}): ${chatMessage.slice(0, 60)}…`);
1564
-
1565
- const onEvent = (event) => {
1566
- // Send to VS Code client AND plugin (so both see the Figma actions)
1567
- sendToVscode(event, ws);
1568
- sendToPlugin(event);
1569
- };
1570
-
1571
- let proc;
1572
- if (prov === "claude" || !prov || prov === "bridge") {
1573
- const anthropicKey = getAnthropicApiKey();
1574
- if (chatMode === "chat" && anthropicKey) {
1575
- const { buildChatPrompt } = require("./shared-prompt-config");
1576
- proc = runAnthropicChat({
1577
- message: chatMessage,
1578
- attachments: msg.attachments,
1579
- conversation: msg.conversation,
1580
- requestId,
1581
- apiKey: anthropicKey,
1582
- model: msg.model,
1583
- systemPrompt: buildChatPrompt(),
1584
- onEvent,
1585
- });
1586
- } else {
1587
- proc = runClaude({
1588
- message: chatMessage,
1589
- attachments: msg.attachments,
1590
- conversation: msg.conversation,
1591
- requestId,
1592
- model: msg.model,
1593
- designSystemId: activeDesignSystemId,
1594
- mode: chatMode,
1595
- frameworkConfig: msg.frameworkConfig,
1596
- onEvent,
1597
- });
1598
- }
1599
- } else if (prov === "openai") {
1600
- proc = runCodex({
1601
- message: chatMessage,
1602
- attachments: msg.attachments,
1603
- requestId,
1604
- model: msg.model,
1605
- designSystemId: activeDesignSystemId,
1606
- mode: chatMode,
1607
- onEvent,
1608
- });
1609
- } else if (prov === "gemini") {
1610
- if (geminiCliAuthInfo.loggedIn) {
1611
- proc = runGeminiCli({
1612
- message: chatMessage,
1613
- attachments: msg.attachments,
1614
- conversation: msg.conversation,
1615
- requestId,
1616
- model: msg.model,
1617
- designSystemId: activeDesignSystemId,
1618
- mode: chatMode,
1619
- onEvent,
1620
- });
1621
- } else {
1622
- proc = runGemini({
1623
- message: chatMessage,
1624
- attachments: msg.attachments,
1625
- conversation: msg.conversation,
1626
- requestId,
1627
- apiKey: providerConfig.apiKey,
1628
- model: msg.model,
1629
- designSystemId: activeDesignSystemId,
1630
- mode: chatMode,
1631
- onEvent,
1632
- });
1633
- }
1634
- } else if (prov === "stitch") {
1635
- proc = runStitch({
1636
- message: chatMessage,
1637
- requestId,
1638
- apiKey: providerConfig.apiKey,
1639
- projectId: providerConfig.projectId,
1640
- model: msg.model,
1641
- onEvent,
1642
- });
1643
- } else {
1644
- sendToVscode({ type: "error", id: requestId, error: `Unsupported provider: ${prov}` }, ws);
1645
- sendToVscode({ type: "done", id: requestId, fullText: "" }, ws);
1646
- return;
1647
- }
1648
-
1649
- activeChatProcesses.set(requestId, proc);
1650
- proc.on("close", () => activeChatProcesses.delete(requestId));
1651
- return;
1652
- }
1653
-
1654
- // Abort chat
1655
- if (msg.type === "abort-chat") {
1656
- const proc = activeChatProcesses.get(msg.id);
1657
- if (proc) {
1658
- proc.kill("SIGTERM");
1659
- activeChatProcesses.delete(msg.id);
1660
- console.log(` ⛔ vscode chat aborted (id: ${msg.id})`);
1661
- }
1662
- return;
1663
- }
1664
-
1665
- // New conversation
1666
- if (msg.type === "new-conversation") {
1667
- const resetMode = msg.mode || null;
1668
- resetSession(resetMode);
1669
- resetCodexSession(resetMode);
1670
- console.log(` 🔄 vscode session reset${resetMode ? ` (${resetMode})` : " (all)"}`);
1671
- return;
1672
- }
1673
-
1674
- return;
1675
- }
1676
-
1677
- // ── Messages from the Figma plugin ──────────────────────────────────────
1678
- if (isPlugin) {
1679
-
1680
- // Stitch Google OAuth — "Sign in with Google" button
1681
- if (msg.type === "stitch-auth") {
1682
- (async () => {
1683
- try {
1684
- sendToPlugin({ type: "stitch-auth-status", status: "signing-in" });
1685
- console.log(" Stitch: starting Google OAuth flow...");
1686
- const accessToken = await startStitchAuth();
1687
- providerConfig.apiKey = accessToken; // store as apiKey for relay compatibility
1688
- saveProviderConfig();
1689
- const email = getStitchEmail();
1690
- console.log(` Stitch: authenticated as ${email || "unknown"}`);
1691
- sendToPlugin({ type: "stitch-auth-status", status: "success", email });
1692
- } catch (err) {
1693
- console.error(" Stitch auth failed:", err.message);
1694
- sendToPlugin({ type: "stitch-auth-status", status: "error", error: err.message });
1695
- }
1696
- })();
1697
- return;
1698
- }
1699
-
1700
- // Stitch sign-out
1701
- if (msg.type === "stitch-signout") {
1702
- clearStitchAuth();
1703
- console.log(" Stitch: signed out");
1704
- sendToPlugin({ type: "stitch-auth-status", status: "signed-out" });
1705
- return;
1706
- }
1707
-
1708
- // Set AI provider / API key
1709
- if (msg.type === "set-provider") {
1710
- providerConfig = {
1711
- provider: msg.provider || "claude",
1712
- apiKey: msg.apiKey || null,
1713
- projectId: msg.projectId || null,
1714
- };
1715
- saveProviderConfig();
1716
- console.log(` 🔑 provider set: ${providerConfig.provider}`);
1717
- sendToPlugin({ type: "provider-stored", provider: providerConfig.provider });
1718
- refreshAuthState().catch(() => {});
1719
- return;
1720
- }
1721
-
1722
- // Set active design system
1723
- if (msg.type === "set-design-system") {
1724
- const newId = msg.designSystemId || null;
1725
- if (newId !== activeDesignSystemId) {
1726
- activeDesignSystemId = newId;
1727
- resetSession();
1728
- resetCodexSession();
1729
- console.log(` 🎨 design system: ${newId || "none"} (sessions reset)`);
1730
- }
1731
- sendToPlugin({ type: "design-system-stored", designSystemId: activeDesignSystemId });
1732
- // Broadcast DS change to all connected MCP sockets so intelligence layer stays in sync
1733
- broadcastToMcpSockets(JSON.stringify({ type: "design-system-changed", designSystemId: activeDesignSystemId }));
1734
- return;
1735
- }
1736
-
1737
- // ── Knowledge source management ──────────────────────────────────
1738
- if (msg.type === "add-content-file") {
1739
- const fileName = msg.name || "file";
1740
- const dataUrl = msg.data || "";
1741
- console.log(` 📄 Adding file: ${fileName}`);
1742
-
1743
- (async () => {
1744
- try {
1745
- // Decode base64 DataURL to buffer
1746
- const b64Match = dataUrl.match(/^data:[^;]*;base64,(.+)$/);
1747
- if (!b64Match) throw new Error("Invalid file data");
1748
- const buffer = Buffer.from(b64Match[1], "base64");
1749
-
1750
- const ext = (fileName.match(/\.(\w+)$/)?.[1] || "").toLowerCase();
1751
- let title, text, meta = { fileName, fileType: ext };
1752
-
1753
- if (ext === "pdf") {
1754
- const result = await parsePdfBuffer(buffer);
1755
- title = result.title || fileName.replace(/\.\w+$/, "");
1756
- text = result.text;
1757
- meta.pages = result.pages;
1758
- } else if (ext === "docx" || ext === "doc") {
1759
- const result = await parseDocxBuffer(buffer);
1760
- title = result.title || fileName.replace(/\.\w+$/, "");
1761
- text = result.text;
1762
- } else {
1763
- // Plain text formats (txt, md, csv, json, etc.)
1764
- title = fileName.replace(/\.\w+$/, "");
1765
- text = buffer.toString("utf-8");
1766
- }
1767
-
1768
- if (!text || text.trim().length === 0) {
1769
- throw new Error("No text content could be extracted from this file");
1770
- }
1771
-
1772
- const source = createContentSource(title, text, meta);
1773
- activeContentSources.set(source.id, source);
1774
- console.log(` ✅ File added: "${title}" (${text.length} chars${meta.pages ? `, ${meta.pages} pages` : ""})`);
1775
- sendToPlugin({
1776
- type: "content-added",
1777
- source: {
1778
- id: source.id, title: source.title, sourceCount: source.sources.length,
1779
- meta: source.meta, extractedAt: source.extractedAt,
1780
- charCount: text.length,
1781
- preview: text.slice(0, 500).replace(/\s+/g, " ").trim(),
1782
- },
1783
- });
1784
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1785
- } catch (err) {
1786
- console.log(` ⚠ File error: ${err.message}`);
1787
- sendToPlugin({ type: "content-error", error: err.message, fileName });
1788
- }
1789
- })();
1790
- return;
1791
- }
1792
-
1793
- if (msg.type === "add-content-url") {
1794
- const url = (msg.url || "").trim();
1795
- console.log(` 🔗 Fetching URL: ${url.slice(0, 60)}…`);
1796
-
1797
- (async () => {
1798
- try {
1799
- if (!url || !/^https?:\/\//i.test(url)) throw new Error("Invalid URL");
1800
- const result = await fetchUrlContent(url);
1801
- if (!result.text || result.text.trim().length < 20) {
1802
- throw new Error("Could not extract meaningful content from this URL");
1803
- }
1804
- const source = createContentSource(
1805
- result.title || url,
1806
- result.text,
1807
- { url, fileType: "url" }
1808
- );
1809
- activeContentSources.set(source.id, source);
1810
- console.log(` ✅ URL added: "${source.title}" (${result.text.length} chars)`);
1811
- sendToPlugin({
1812
- type: "content-added",
1813
- source: {
1814
- id: source.id, title: source.title, sourceCount: source.sources.length,
1815
- meta: source.meta, extractedAt: source.extractedAt,
1816
- charCount: result.text.length,
1817
- preview: result.text.slice(0, 500).replace(/\s+/g, " ").trim(),
1818
- },
1819
- });
1820
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1821
- } catch (err) {
1822
- console.log(` ⚠ URL error: ${err.message}`);
1823
- sendToPlugin({ type: "content-error", error: err.message, url });
1824
- }
1825
- })();
1826
- return;
1827
- }
1828
-
1829
- if (msg.type === "add-content-text") {
1830
- const title = msg.title || "Pasted Content";
1831
- const content = msg.content || "";
1832
- if (!content.trim()) {
1833
- sendToPlugin({ type: "content-error", error: "No content provided" });
1834
- return;
1835
- }
1836
- const source = createContentSource(title, content, { fileType: "text" });
1837
- activeContentSources.set(source.id, source);
1838
- console.log(` ✅ Text pasted: "${title}" (${content.length} chars)`);
1839
- sendToPlugin({
1840
- type: "content-added",
1841
- source: {
1842
- id: source.id, title: source.title, sourceCount: source.sources.length,
1843
- meta: source.meta, extractedAt: source.extractedAt,
1844
- charCount: content.length,
1845
- preview: content.slice(0, 500).replace(/\s+/g, " ").trim(),
1846
- },
1847
- });
1848
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1849
- return;
1850
- }
1851
-
1852
- if (msg.type === "remove-content") {
1853
- const id = msg.sourceId;
1854
- if (activeContentSources.has(id)) {
1855
- const title = activeContentSources.get(id).title;
1856
- activeContentSources.delete(id);
1857
- console.log(` 📄 Source removed: "${title}"`);
1858
- sendToPlugin({ type: "content-removed", sourceId: id });
1859
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1860
- }
1861
- return;
1862
- }
1863
-
1864
- if (msg.type === "list-content") {
1865
- sendToPlugin({
1866
- type: "content-list",
1867
- sources: Array.from(activeContentSources.values()).map(s => ({
1868
- id: s.id, title: s.title, sourceCount: s.sources.length, meta: s.meta || {}, extractedAt: s.extractedAt,
1869
- })),
1870
- });
1871
- return;
1872
- }
1873
-
1874
- // ── Knowledge Hub ────────────────────────────────────────────────
1875
- if (msg.type === "hub-scan") {
1876
- const catalog = scanKnowledgeHub();
1877
- console.log(` 📚 Knowledge Hub: ${catalog.length} file(s) found`);
1878
- sendToPlugin({ type: "hub-catalog", files: catalog });
1879
- return;
1880
- }
1881
-
1882
- if (msg.type === "hub-load") {
1883
- const fileName = msg.fileName;
1884
- console.log(` 📚 Loading hub file: ${fileName}`);
1885
- (async () => {
1886
- try {
1887
- const source = await loadHubFile(fileName);
1888
- activeContentSources.set(source.id, source);
1889
- const text = source.sources[0]?.content || "";
1890
- console.log(` ✅ Hub file loaded: "${source.title}" (${text.length} chars)`);
1891
- sendToPlugin({
1892
- type: "content-added",
1893
- source: {
1894
- id: source.id, title: source.title, sourceCount: source.sources.length,
1895
- meta: source.meta, extractedAt: source.extractedAt,
1896
- charCount: text.length,
1897
- preview: text.slice(0, 500).replace(/\s+/g, " ").trim(),
1898
- },
1899
- });
1900
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1901
- } catch (err) {
1902
- console.log(` ⚠ Hub error: ${err.message}`);
1903
- sendToPlugin({ type: "content-error", error: err.message, fileName });
1904
- }
1905
- })();
1906
- return;
1907
- }
1908
-
1909
- if (msg.type === "hub-search") {
1910
- const results = searchHub(msg.query || "");
1911
- sendToPlugin({ type: "hub-search-results", files: results, query: msg.query });
1912
- return;
1913
- }
1914
-
1915
- // ── Web Reference Site management ───────────────────────────────
1916
- if (msg.type === "add-reference-site") {
1917
- const site = addReferenceSite({ name: msg.name, baseUrl: msg.baseUrl || msg.url, searchDomain: msg.searchDomain });
1918
- console.log(` 🌐 Reference site added: ${site.name} (${site.searchDomain})`);
1919
- sendToPlugin({ type: "reference-site-added", site });
1920
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1921
- return;
1922
- }
1923
-
1924
- if (msg.type === "remove-reference-site") {
1925
- removeReferenceSite(msg.id);
1926
- console.log(` 🌐 Reference site removed: ${msg.id}`);
1927
- sendToPlugin({ type: "reference-site-removed", id: msg.id });
1928
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1929
- return;
1930
- }
1931
-
1932
- if (msg.type === "list-reference-sites") {
1933
- sendToPlugin({ type: "reference-sites-list", sites: getReferenceSites() });
1934
- return;
1935
- }
1936
-
1937
- // Chat message → route to the configured AI runner
1938
- if (msg.type === "chat") {
1939
- const requestId = msg.id;
1940
- const prov = providerConfig.provider || "claude";
1941
- const chatMode = msg.mode || "code";
1942
- let chatMessage = msg.message || "";
1943
-
1944
- // Debug: log all incoming chat messages
1945
- 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 {}
1946
- try { appendFileSync("/tmp/import-vars-debug.log", `${new Date().toISOString()} isImport=${isImportVariablesIntent(chatMessage, msg.attachments)}\n`); } catch {}
1947
-
1948
- // Pre-parse Figma links so the AI doesn't need to extract file_key/node_id
1949
- const figmaLinkMatch2 = chatMessage.match(/https:\/\/www\.figma\.com\/(?:design|file)\/[^\s]+/);
1950
- if (figmaLinkMatch2) {
1951
- try {
1952
- const { parseFigmaLink } = require("./spec-helpers/parse-figma-link");
1953
- const parsed = parseFigmaLink(figmaLinkMatch2[0]);
1954
- chatMessage += `\n\n[Pre-parsed Figma link: file_key="${parsed.file_key}", node_id="${parsed.node_id}"]`;
1955
- } catch (e) { /* ignore parse errors */ }
1956
- }
1957
-
1958
- // /knowledge command — intercept and handle via knowledge hub
1959
- if (/^\s*\/knowledge\b/i.test(chatMessage)) {
1960
- const query = chatMessage.replace(/^\s*\/knowledge\s*/i, "").trim();
1961
- const catalog = scanKnowledgeHub();
1962
- console.log(` 📚 /knowledge command: ${catalog.length} files in hub${query ? `, searching: "${query}"` : ""}`);
1963
-
1964
- if (query) {
1965
- // Auto-search and load matching hub files
1966
- const matches = searchHub(query);
1967
- if (matches.length > 0) {
1968
- (async () => {
1969
- try {
1970
- const source = await loadHubFile(matches[0].fileName);
1971
- activeContentSources.set(source.id, source);
1972
- const text = source.sources[0]?.content || "";
1973
- sendToPlugin({
1974
- type: "content-added",
1975
- source: {
1976
- id: source.id, title: source.title, sourceCount: source.sources.length,
1977
- meta: source.meta, extractedAt: source.extractedAt,
1978
- charCount: text.length,
1979
- preview: text.slice(0, 500).replace(/\s+/g, " ").trim(),
1980
- },
1981
- });
1982
- // Send a visible chat response
1983
- const otherNames = matches.slice(1, 4).map(m => `"${m.title}"`).join(", ");
1984
- 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.`;
1985
- if (matches.length > 1) responseText += `\n\n_${matches.length - 1} other match(es): ${otherNames}_`;
1986
- sendToPlugin({ type: "text_delta", id: requestId, delta: responseText });
1987
- sendToPlugin({ type: "done", id: requestId, fullText: responseText });
1988
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
1989
- } catch (err) {
1990
- sendToPlugin({ type: "text_delta", id: requestId, delta: `⚠️ Could not load: ${err.message}` });
1991
- sendToPlugin({ type: "done", id: requestId, fullText: err.message });
1992
- }
1993
- })();
1994
- } else {
1995
- // No matches — show what's available
1996
- const fileList = catalog.map(f => `• ${f.title} (${f.fileType.toUpperCase()})`).join("\n");
1997
- 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._`;
1998
- sendToPlugin({ type: "text_delta", id: requestId, delta: responseText });
1999
- sendToPlugin({ type: "done", id: requestId, fullText: responseText });
2000
- sendToPlugin({ type: "hub-catalog", files: catalog, query });
2001
- }
2002
- } else {
2003
- // Just "/knowledge" — show catalog as chat response + open panel
2004
- const fileList = catalog.map(f => `• **${f.title}** (${f.fileType.toUpperCase()}, ${(f.sizeBytes / 1024).toFixed(0)} KB)`).join("\n");
2005
- const activeList = Array.from(activeContentSources.values()).map(s => `• ✅ ${s.title}`).join("\n");
2006
- let responseText = `📚 **Knowledge Hub** — ${catalog.length} file(s) available\n\n`;
2007
- if (catalog.length > 0) {
2008
- responseText += `**Library:**\n${fileList}\n\n`;
2009
- responseText += `_Use \`/knowledge <keyword>\` to load a specific file, or click the 📖 icon to browse and activate._`;
2010
- } else {
2011
- responseText += `No files yet. Add PDFs, DOCX, or TXT files to:\n\`figma-bridge-plugin/knowledge-hub/\``;
2012
- }
2013
- if (activeList) responseText += `\n\n**Currently active sources:**\n${activeList}`;
2014
- sendToPlugin({ type: "text_delta", id: requestId, delta: responseText });
2015
- sendToPlugin({ type: "done", id: requestId, fullText: responseText });
2016
- sendToPlugin({ type: "hub-catalog", files: catalog });
2017
- }
2018
- return;
2019
- }
2020
-
2021
- // ── Import .md → Figma variables (works from any provider) ──────
2022
- if (isImportVariablesIntent(chatMessage, msg.attachments)) {
2023
- handleImportVariables(requestId, chatMessage, msg.attachments, (ev) => sendToPlugin(ev));
2024
- return;
2025
- }
2026
-
2027
- // ── Component Doc Generator: handle follow-up after chooser ─────
2028
- // If the chooser was shown and user replies with a type selection,
2029
- // rewrite the message to include the original context + specific spec type.
2030
- if (pendingDocGenChooser && (chatMode === "code" || chatMode === "dual")) {
2031
- const elapsed = Date.now() - pendingDocGenChooser.shownAt;
2032
- if (elapsed < 10 * 60 * 1000) { // within 10 minutes
2033
- const reply = (chatMessage || "").trim().toLowerCase();
2034
- const specMap = {
2035
- "1": "anatomy", "anatomy": "anatomy",
2036
- "2": "api", "api": "api",
2037
- "3": "property", "properties": "property", "property": "property",
2038
- "4": "color", "color": "color",
2039
- "5": "structure", "structure": "structure",
2040
- "6": "screen-reader", "screen reader": "screen-reader", "screen-reader": "screen-reader",
2041
- "all": "all",
2042
- };
2043
- // Check if reply matches a spec type choice
2044
- const matched = specMap[reply];
2045
- // Also check for multi-select like "1, 3, 5" or "1 3 5"
2046
- const multiMatch = reply.match(/^[\d,\s]+$/);
2047
- if (matched || multiMatch) {
2048
- const original = pendingDocGenChooser.originalMessage;
2049
- pendingDocGenChooser = null;
2050
- // CRITICAL: Reset session so the AI starts fresh with the correct
2051
- // system prompt containing the spec-type skill addendum.
2052
- resetSession(chatMode);
2053
- console.log(` 📋 Component Doc Generator (plugin): reset ${chatMode} session for fresh system prompt`);
2054
- if (matched === "all" || (multiMatch && reply.replace(/\s/g, "").split(",").length >= 6)) {
2055
- // User wants all specs — send each type
2056
- 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.`;
2057
- console.log(` 📋 Component Doc Generator: user chose ALL — rewriting message`);
2058
- } else if (multiMatch) {
2059
- const nums = reply.replace(/\s/g, "").split(",").filter(Boolean);
2060
- const types = nums.map(n => specMap[n]).filter(Boolean);
2061
- chatMessage = `${original}\n\nThe user selected these spec types: ${types.join(", ")}. Generate a create ${types[0]} spec for the component first.`;
2062
- console.log(` 📋 Component Doc Generator: user chose [${types.join(", ")}] — rewriting message`);
2063
- } else {
2064
- chatMessage = `${original}\n\nThe user selected: create ${matched} spec for this component.`;
2065
- console.log(` 📋 Component Doc Generator: user chose "${matched}" — rewriting message`);
2066
- }
2067
- } else {
2068
- // Reply doesn't look like a spec choice — clear pending state
2069
- pendingDocGenChooser = null;
2070
- }
2071
- } else {
2072
- pendingDocGenChooser = null; // expired
2073
- }
2074
- }
2075
-
2076
- // ── Component Doc Generator — no chooser, generate complete spec directly ────────
2077
- // The tool now auto-enriches all sections from the knowledge base in a single call.
2078
- // No need to present options or use a 2-phase workflow.
2079
-
2080
- // ── Design Decision: auto-register NN Group + proactive article fetch ──
2081
- {
2082
- const { detectActiveSkills } = require("./shared-prompt-config");
2083
- const msgSkills = detectActiveSkills(chatMessage);
2084
- if (msgSkills.includes("Design Decision")) {
2085
- const sites = getReferenceSites();
2086
- if (!sites.some(s => s.searchDomain === "nngroup.com")) {
2087
- addReferenceSite({ name: "Nielsen Norman Group", searchDomain: "nngroup.com" });
2088
- console.log(" 📖 Auto-registered nngroup.com as reference site for Design Decision");
2089
- }
2090
- // Proactive search — fetch NN Group article and inject as grounding
2091
- (async () => {
2092
- try {
2093
- const nnResult = await Promise.race([
2094
- searchReferenceSites(chatMessage),
2095
- new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 8000)),
2096
- ]);
2097
- if (nnResult) {
2098
- const nnSource = createContentSource(nnResult.title, nnResult.text, { url: nnResult.url, source: "nngroup.com" });
2099
- activeContentSources.set(nnSource.id, nnSource);
2100
- console.log(` 📖 NN Group article loaded: "${nnResult.title}"`);
2101
- }
2102
- } catch (e) {
2103
- console.error(` ⚠ NN Group search failed: ${e.message}`);
2104
- }
2105
- })();
2106
- }
2107
- }
2108
-
2109
- // ── Chat Tiers: always route through AI with grounding ─────────
2110
- const rawMessage = chatMessage; // preserve original for knowledge/web search
2111
-
2112
- // Tier 1: Fetch web reference articles async, then route to AI
2113
- // (No more raw text "instant answers" — AI always synthesizes the response)
2114
- if (chatMode === "chat" && getReferenceSites().length > 0) {
2115
- (async () => {
2116
- try {
2117
- const timeoutPromise = new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 5000));
2118
- const webAnswer = await Promise.race([searchReferenceSites(rawMessage), timeoutPromise]);
2119
- if (webAnswer) {
2120
- // Add fetched article as a content source for grounding, don't return it raw
2121
- const webSource = createContentSource(webAnswer.title, webAnswer.text || "", {
2122
- url: webAnswer.url,
2123
- source: webAnswer.siteName,
2124
- });
2125
- activeContentSources.set(webSource.id, webSource);
2126
- console.log(` 🌐 Web reference loaded: ${webAnswer.siteName} — ${webAnswer.title}`);
2127
- }
2128
- } catch (err) {
2129
- console.error(` ⚠ Web reference search error: ${err.message}`);
2130
- }
2131
- routeToAiProvider();
2132
- })();
2133
- return; // async — routeToAiProvider called inside the async block
2134
- }
2135
-
2136
- routeToAiProvider();
2137
- return;
2138
-
2139
- function routeToAiProvider() {
2140
-
2141
- // Inject knowledge source grounding context if sources are active
2142
- if (activeContentSources.size > 0) {
2143
- const groundingCtx = buildGroundingContext(activeContentSources, rawMessage);
2144
- if (groundingCtx) {
2145
- chatMessage = groundingCtx +
2146
- "\n---\n\n" +
2147
- "INSTRUCTIONS: Use the knowledge context above to answer the user's question. " +
2148
- "Synthesize the information into a clear, structured answer — do NOT just quote raw text. " +
2149
- "Cite the source name when referencing specific information. " +
2150
- "If the context doesn't contain relevant information, say so and answer from your general knowledge.\n\n" +
2151
- "User question: " + chatMessage;
2152
- console.log(` 📄 Injected ${activeContentSources.size} knowledge source(s) as grounding context`);
2153
- }
2154
- }
2155
-
2156
- console.log(` 💬 chat [${prov}/${chatMode}] (id: ${requestId}): ${(chatMessage).slice(0, 60)}…`);
2157
-
2158
- const onEvent = (event) => {
2159
- // Intercept figma_command events — forward as bridge-request to plugin
2160
- if (event.type === "figma_command") {
2161
- const cmdId = `stitch-cmd-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2162
- sendToPlugin({
2163
- type: "bridge-request",
2164
- id: cmdId,
2165
- method: event.method,
2166
- params: event.params || {},
2167
- });
2168
- console.log(` 🎨 stitch → figma: ${event.method}`);
2169
- return;
2170
- }
2171
-
2172
- sendToPlugin(event);
2173
- // In dual mode, also forward to VS Code clients for code extraction
2174
- if (chatMode === "dual") {
2175
- broadcastToVscodeSockets(event);
2176
- }
2177
- if (event.type === "tool_start") {
2178
- console.log(` 🔧 tool_start: ${event.tool}`);
2179
- } else if (event.type === "tool_done") {
2180
- console.log(` ✅ tool_done: ${event.tool}${event.isError ? " [ERROR]" : ""}`);
2181
- } else if (event.type === "phase_start") {
2182
- console.log(` 📋 phase: ${event.phase}`);
2183
- }
2184
- };
2185
-
2186
- let proc;
2187
- if (prov === "openai") {
2188
- // Use Codex CLI (subscription-based) — no API key needed
2189
- proc = runCodex({
2190
- message: chatMessage,
2191
- attachments: msg.attachments,
2192
- conversation: msg.conversation,
2193
- requestId,
2194
- model: msg.model,
2195
- designSystemId: activeDesignSystemId,
2196
- mode: chatMode,
2197
- onEvent,
2198
- });
2199
- } else if (prov === "gemini") {
2200
- if (geminiCliAuthInfo.loggedIn) {
2201
- // Subscription mode — use Gemini CLI (Google One AI Premium / Gemini Advanced)
2202
- proc = runGeminiCli({
2203
- message: chatMessage,
2204
- attachments: msg.attachments,
2205
- conversation: msg.conversation,
2206
- requestId,
2207
- model: msg.model,
2208
- designSystemId: activeDesignSystemId,
2209
- mode: chatMode,
2210
- onEvent,
2211
- });
2212
- } else {
2213
- // API key mode — fallback for users without subscription CLI auth
2214
- proc = runGemini({
2215
- message: chatMessage,
2216
- attachments: msg.attachments,
2217
- conversation: msg.conversation,
2218
- requestId,
2219
- apiKey: providerConfig.apiKey,
2220
- model: msg.model,
2221
- designSystemId: activeDesignSystemId,
2222
- mode: chatMode,
2223
- onEvent,
2224
- });
2225
- }
2226
- } else if (prov === "perplexity") {
2227
- proc = runPerplexity({
2228
- message: chatMessage,
2229
- attachments: msg.attachments,
2230
- conversation: msg.conversation,
2231
- requestId,
2232
- apiKey: providerConfig.apiKey,
2233
- model: msg.model,
2234
- mode: "chat",
2235
- onEvent,
2236
- });
2237
- } else if (prov === "stitch") {
2238
- // ── Check for "sync design system" intent before routing to Stitch ──
2239
- if (isSyncDesignIntent(chatMessage)) {
2240
- handleSyncDesignSystem(requestId, chatMessage, onEvent);
2241
- return;
2242
- }
2243
-
2244
- if (isImportVariablesIntent(chatMessage, msg.attachments)) {
2245
- handleImportVariables(requestId, chatMessage, msg.attachments, onEvent);
2246
- return;
2247
- }
2248
-
2249
- // If a .md file is attached, extract its content as design context for generation
2250
- let designContext = null;
2251
- if (msg.attachments?.length) {
2252
- const mdFile = msg.attachments.find(a => /\.md$/i.test(a.name));
2253
- if (mdFile?.data) designContext = mdFile.data;
2254
- }
2255
-
2256
- console.log(` 🎨 Routing to Stitch runner (apiKey: ${providerConfig.apiKey ? "set" : "MISSING"}${designContext ? ", with .md design context" : ""})`);
2257
- proc = runStitch({
2258
- message: chatMessage,
2259
- requestId,
2260
- apiKey: providerConfig.apiKey,
2261
- projectId: providerConfig.projectId,
2262
- model: msg.model,
2263
- designContext,
2264
- onEvent,
2265
- });
2266
- } else if (prov === "bridge") {
2267
- // Bridge-only mode: no built-in AI — tell the plugin immediately
2268
- sendToPlugin({
2269
- type: "error",
2270
- id: requestId,
2271
- error: "Bridge mode is active. Chat is handled by your external AI tool (VS Code, Cursor, etc.) via MCP — not by the plugin itself.",
2272
- });
2273
- sendToPlugin({ type: "done", id: requestId, fullText: "" });
2274
- return;
2275
- } else {
2276
- // Default: Claude
2277
- const anthropicKey = getAnthropicApiKey();
2278
- if (chatMode === "chat" && anthropicKey) {
2279
- // Tier 3: Direct Anthropic API — fast streaming (~200ms first token)
2280
- const isResearcher = msg.researcherMode === true;
2281
-
2282
- if (isResearcher) {
2283
- // UX Researcher mode: async — capture selection, force Haiku, use researcher prompt
2284
- (async () => {
2285
- try {
2286
- const selectionCtx = await captureSelectionContext();
2287
- if (selectionCtx) {
2288
- chatMessage = selectionCtx + "\n\n" + chatMessage;
2289
- console.log(" 🔬 Researcher: attached Figma selection context");
2290
- }
2291
- } catch (e) {
2292
- console.error(" ⚠ Researcher selection capture failed:", e.message);
2293
- }
2294
-
2295
- const { buildUxResearcherPrompt } = require("./shared-prompt-config");
2296
- proc = runAnthropicChat({
2297
- message: chatMessage,
2298
- attachments: msg.attachments,
2299
- conversation: msg.conversation,
2300
- requestId,
2301
- apiKey: anthropicKey,
2302
- model: "claude-haiku-4-5-20251001",
2303
- systemPrompt: buildUxResearcherPrompt(),
2304
- onEvent,
2305
- });
2306
- activeChatProcesses.set(requestId, proc);
2307
- proc.on("close", () => activeChatProcesses.delete(requestId));
2308
- })();
2309
- return;
2310
- }
2311
-
2312
- const { buildChatPrompt } = require("./shared-prompt-config");
2313
- proc = runAnthropicChat({
2314
- message: chatMessage,
2315
- attachments: msg.attachments,
2316
- conversation: msg.conversation,
2317
- requestId,
2318
- apiKey: anthropicKey,
2319
- model: msg.model,
2320
- systemPrompt: buildChatPrompt(),
2321
- onEvent,
2322
- });
2323
- } else {
2324
- // Tier 4: Claude CLI subprocess (code/dual mode, or no API key)
2325
- const cliResearcher = msg.researcherMode === true && chatMode === "chat";
2326
-
2327
- if (cliResearcher) {
2328
- // Researcher mode via CLI: async — capture selection, then spawn CLI
2329
- (async () => {
2330
- try {
2331
- const selectionCtx = await captureSelectionContext();
2332
- if (selectionCtx) {
2333
- chatMessage = selectionCtx + "\n\n" + chatMessage;
2334
- console.log(" 🔬 Researcher (CLI): attached Figma selection context");
2335
- }
2336
- } catch (e) {
2337
- console.error(" ⚠ Researcher selection capture failed:", e.message);
2338
- }
2339
-
2340
- proc = runClaude({
2341
- message: chatMessage,
2342
- attachments: msg.attachments,
2343
- conversation: msg.conversation,
2344
- requestId,
2345
- model: msg.model,
2346
- designSystemId: activeDesignSystemId,
2347
- mode: chatMode,
2348
- frameworkConfig: msg.frameworkConfig || {},
2349
- researcherMode: true,
2350
- onEvent,
2351
- });
2352
- activeChatProcesses.set(requestId, proc);
2353
- proc.on("close", () => activeChatProcesses.delete(requestId));
2354
- })();
2355
- return;
2356
- }
2357
-
2358
- proc = runClaude({
2359
- message: chatMessage,
2360
- attachments: msg.attachments,
2361
- conversation: msg.conversation,
2362
- requestId,
2363
- model: msg.model,
2364
- designSystemId: activeDesignSystemId,
2365
- mode: chatMode,
2366
- frameworkConfig: msg.frameworkConfig || {},
2367
- onEvent,
2368
- });
2369
- }
2370
- }
2371
-
2372
- activeChatProcesses.set(requestId, proc);
2373
- proc.on("close", () => activeChatProcesses.delete(requestId));
2374
- return;
2375
- } // end routeToAiProvider
2376
- }
2377
-
2378
- // Abort a running chat
2379
- if (msg.type === "abort-chat") {
2380
- const proc = activeChatProcesses.get(msg.id);
2381
- if (proc) {
2382
- proc.kill("SIGTERM");
2383
- activeChatProcesses.delete(msg.id);
2384
- console.log(` ⛔ chat aborted (id: ${msg.id})`);
2385
- }
2386
- return;
2387
- }
2388
-
2389
- // Reset conversation session (user clicked "New Chat" in plugin UI)
2390
- if (msg.type === "new-conversation" || msg.type === "clear-history") {
2391
- const resetMode = msg.mode || null; // null = reset all modes
2392
- resetSession(resetMode);
2393
- resetCodexSession(resetMode);
2394
- console.log(` 🔄 conversation session reset${resetMode ? ` (${resetMode})` : " (all)"}`);
2395
- return;
2396
- }
2397
-
2398
- // Bridge events (selection change, doc change etc.) → forward to MCP
2399
- if (msg.type === "bridge-event") {
2400
- broadcastToMcpSockets(raw);
2401
- console.log(` ↺ plugin event: ${msg.eventType || "unknown"}`);
2402
- return;
2403
- }
2404
-
2405
- // Plugin hello
2406
- if (msg.type === "plugin-hello") {
2407
- console.log(` Plugin identified: ${msg.fileName || "unknown"}`);
2408
- return;
2409
- }
2410
-
2411
-
2412
-
2413
- // MCP tool response from plugin → route back to the requesting MCP socket
2414
- if (msg.id && !msg.method) {
2415
- // Check if this is a relay-initiated request first
2416
- const relayReq = pendingRelayRequests.get(msg.id);
2417
- if (relayReq) {
2418
- clearTimeout(relayReq.timer);
2419
- pendingRelayRequests.delete(msg.id);
2420
- if (msg.error) {
2421
- relayReq.reject(new Error(msg.error));
2422
- } else {
2423
- relayReq.resolve(msg.result);
2424
- }
2425
- console.log(` ← relay request response (id: ${msg.id})`);
2426
- return;
2427
- }
2428
-
2429
- const targetSocket = pendingRequests.get(msg.id);
2430
- if (targetSocket && targetSocket.readyState === 1) {
2431
- targetSocket.send(raw);
2432
- console.log(` ← plugin response (id: ${msg.id})`);
2433
- } else {
2434
- broadcastToMcpSockets(raw);
2435
- }
2436
- pendingRequests.delete(msg.id);
2437
- }
2438
- return;
2439
- }
2440
-
2441
- // ── Messages from an MCP server ─────────────────────────────────────────
2442
- if (msg.id && msg.method) {
2443
- // Handle getActiveDesignSystemId directly — no need to forward to plugin
2444
- if (msg.method === "getActiveDesignSystemId") {
2445
- ws.send(JSON.stringify({ id: msg.id, result: activeDesignSystemId }));
2446
- console.log(` ← relay responded: getActiveDesignSystemId = ${activeDesignSystemId || "none"}`);
2447
- return;
2448
- }
2449
- if (pluginSocket && pluginSocket.readyState === 1) {
2450
- pendingRequests.set(msg.id, ws);
2451
- pluginSocket.send(JSON.stringify({
2452
- type: "bridge-request",
2453
- id: msg.id,
2454
- method: msg.method,
2455
- params: msg.params || {},
2456
- }));
2457
- console.log(` → mcp request: ${msg.method} (id: ${msg.id})`);
2458
- } else {
2459
- ws.send(JSON.stringify({
2460
- id: msg.id,
2461
- error: "Figma plugin is not connected. Open Figma and run the Intelligence Bridge plugin.",
2462
- }));
2463
- console.log(` ✗ No plugin connected for: ${msg.method}`);
2464
- }
2465
- }
2466
- });
2467
-
2468
- ws.on("close", () => {
2469
- if (isPlugin) {
2470
- // P3: Grace period — wait before fully disconnecting plugin
2471
- console.log(`⚠ Figma plugin disconnected — ${PLUGIN_GRACE_PERIOD_MS / 1000}s grace period`);
2472
- pluginGraceState = { activeDesignSystemId };
2473
- pluginGraceTimer = setTimeout(() => {
2474
- pluginSocket = null;
2475
- pluginGraceTimer = null;
2476
- pluginGraceState = null;
2477
- console.log("⚠ Plugin grace period expired — fully disconnected");
2478
- sendRelayStatus(null, hasConnectedMcpSocket());
2479
- }, PLUGIN_GRACE_PERIOD_MS);
2480
- } else {
2481
- mcpSockets.delete(ws);
2482
- for (const [requestId, requestSocket] of pendingRequests.entries()) {
2483
- if (requestSocket === ws) pendingRequests.delete(requestId);
2484
- }
2485
- console.log("⚠ MCP server disconnected");
2486
- sendRelayStatus(pluginSocket, hasConnectedMcpSocket());
2487
- }
2488
- });
2489
-
2490
- ws.on("error", (err) => {
2491
- console.error(`WebSocket error (${isPlugin ? "plugin" : "mcp"}):`, err.message);
2492
- });
2493
- });
2494
-
2495
- // ── Graceful shutdown ────────────────────────────────────────────────────────
2496
- process.on("SIGINT", () => {
2497
- console.log("\nShutting down relay…");
2498
- for (const proc of activeChatProcesses.values()) proc.kill();
2499
- if (_mcpProc) _mcpProc.kill();
2500
- if (pluginGraceTimer) clearTimeout(pluginGraceTimer);
2501
- wss.close();
2502
- process.exit(0);
2503
- });
2504
-
2505
- })(); // end async IIFE for port fallback