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