@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,869 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* content-context.js — Universal content parsing for knowledge grounding.
|
|
4
|
+
*
|
|
5
|
+
* Extracts text from PDFs, DOCX files, web URLs, and plain text.
|
|
6
|
+
* The extracted content is stored as "knowledge sources" and injected
|
|
7
|
+
* as grounding context into any AI provider's chat messages.
|
|
8
|
+
*
|
|
9
|
+
* Supported formats:
|
|
10
|
+
* - PDF → pdf-parse (pure JS, no native binaries)
|
|
11
|
+
* - DOCX → mammoth (pure JS, JSZip-based)
|
|
12
|
+
* - URL → cheerio + @mozilla/readability
|
|
13
|
+
* - TXT/MD/CSV/JSON → plain UTF-8 text
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const https = require("https");
|
|
17
|
+
const http = require("http");
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
|
|
21
|
+
// Knowledge Hub directory (sibling to this file)
|
|
22
|
+
const KNOWLEDGE_HUB_DIR = path.join(__dirname, "knowledge-hub");
|
|
23
|
+
|
|
24
|
+
// Lazy-load heavy dependencies to keep startup fast
|
|
25
|
+
let _pdfParse, _mammoth, _cheerio, _Readability, _JSDOM;
|
|
26
|
+
|
|
27
|
+
function getPdfParse() {
|
|
28
|
+
if (!_pdfParse) _pdfParse = require("pdf-parse"); // exports { PDFParse, ... }
|
|
29
|
+
return _pdfParse;
|
|
30
|
+
}
|
|
31
|
+
function getMammoth() {
|
|
32
|
+
if (!_mammoth) _mammoth = require("mammoth");
|
|
33
|
+
return _mammoth;
|
|
34
|
+
}
|
|
35
|
+
function getCheerio() {
|
|
36
|
+
if (!_cheerio) _cheerio = require("cheerio");
|
|
37
|
+
return _cheerio;
|
|
38
|
+
}
|
|
39
|
+
function getReadability() {
|
|
40
|
+
if (!_Readability) _Readability = require("@mozilla/readability");
|
|
41
|
+
return _Readability;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── PDF Parsing ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract text from a PDF buffer.
|
|
48
|
+
* @param {Buffer} buffer — raw PDF bytes
|
|
49
|
+
* @returns {Promise<{ title: string, text: string, pages: number }>}
|
|
50
|
+
*/
|
|
51
|
+
async function parsePdfBuffer(buffer) {
|
|
52
|
+
const { PDFParse } = getPdfParse();
|
|
53
|
+
// pdf-parse v2+ requires Uint8Array, not Buffer
|
|
54
|
+
const uint8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
55
|
+
const parser = new PDFParse(uint8);
|
|
56
|
+
const result = await parser.getText();
|
|
57
|
+
// result = { pages: [{ text, num }], text: string, total: number }
|
|
58
|
+
// Clean up: join page texts, remove page markers like "-- 1 of 320 --"
|
|
59
|
+
const rawText = result.text || result.pages.map(p => p.text).join("\n\n");
|
|
60
|
+
const fullText = rawText.replace(/\n-- \d+ of \d+ --\n/g, "\n\n").trim();
|
|
61
|
+
let title = "";
|
|
62
|
+
// Try to get title from info
|
|
63
|
+
try {
|
|
64
|
+
const info = await parser.getInfo();
|
|
65
|
+
title = info?.Title || info?.title || "";
|
|
66
|
+
} catch {}
|
|
67
|
+
return {
|
|
68
|
+
title,
|
|
69
|
+
text: fullText,
|
|
70
|
+
pages: result.total || result.pages?.length || 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── DOCX Parsing ────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract text from a DOCX buffer.
|
|
78
|
+
* @param {Buffer} buffer — raw DOCX bytes
|
|
79
|
+
* @returns {Promise<{ title: string, text: string }>}
|
|
80
|
+
*/
|
|
81
|
+
async function parseDocxBuffer(buffer) {
|
|
82
|
+
const mammoth = getMammoth();
|
|
83
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
84
|
+
// Try to derive title from first line
|
|
85
|
+
const firstLine = (result.value || "").split("\n").find(l => l.trim().length > 0) || "";
|
|
86
|
+
return {
|
|
87
|
+
title: firstLine.length > 5 && firstLine.length < 120 ? firstLine.trim() : "",
|
|
88
|
+
text: result.value || "",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── URL Content Extraction ──────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Fetch a URL and extract its readable text content.
|
|
96
|
+
* @param {string} url — any HTTP/HTTPS URL
|
|
97
|
+
* @returns {Promise<{ title: string, text: string, url: string }>}
|
|
98
|
+
*/
|
|
99
|
+
async function fetchUrlContent(url) {
|
|
100
|
+
const html = await fetchHtml(url);
|
|
101
|
+
const cheerio = getCheerio();
|
|
102
|
+
const { Readability } = getReadability();
|
|
103
|
+
|
|
104
|
+
const $ = cheerio.load(html);
|
|
105
|
+
|
|
106
|
+
// Remove scripts, styles, nav, footer, ads
|
|
107
|
+
$("script, style, nav, footer, aside, [role='banner'], [role='navigation'], .ad, .ads, .advertisement").remove();
|
|
108
|
+
|
|
109
|
+
// Try @mozilla/readability first (needs a DOM-like object)
|
|
110
|
+
// Since Readability expects a DOM, we create a minimal one from cheerio
|
|
111
|
+
let title = $("title").text().trim() || $("h1").first().text().trim() || "";
|
|
112
|
+
let text = "";
|
|
113
|
+
|
|
114
|
+
// Extract text from main content areas
|
|
115
|
+
const mainSelectors = ["main", "article", "[role='main']", ".content", ".post-content", ".entry-content", "#content"];
|
|
116
|
+
for (const sel of mainSelectors) {
|
|
117
|
+
const el = $(sel);
|
|
118
|
+
if (el.length && el.text().trim().length > 100) {
|
|
119
|
+
text = el.text().trim();
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fallback: get body text
|
|
125
|
+
if (!text) {
|
|
126
|
+
text = $("body").text().trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clean up whitespace
|
|
130
|
+
text = text.replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
|
|
131
|
+
|
|
132
|
+
// Truncate very long pages
|
|
133
|
+
if (text.length > 50000) {
|
|
134
|
+
text = text.slice(0, 50000) + "\n\n[Content truncated at 50,000 characters]";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { title, text, url };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function fetchHtml(url, maxRedirects = 5) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
if (maxRedirects <= 0) return reject(new Error("Too many redirects"));
|
|
143
|
+
const mod = url.startsWith("https") ? https : http;
|
|
144
|
+
const req = mod.get(url, {
|
|
145
|
+
headers: {
|
|
146
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
147
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
148
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
149
|
+
},
|
|
150
|
+
}, (res) => {
|
|
151
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
152
|
+
const next = res.headers.location.startsWith("http")
|
|
153
|
+
? res.headers.location
|
|
154
|
+
: new URL(res.headers.location, url).href;
|
|
155
|
+
return resolve(fetchHtml(next, maxRedirects - 1));
|
|
156
|
+
}
|
|
157
|
+
if (res.statusCode !== 200 && res.statusCode !== 202) return reject(new Error(`HTTP ${res.statusCode}`));
|
|
158
|
+
let body = "";
|
|
159
|
+
res.on("data", (chunk) => { body += chunk.toString(); });
|
|
160
|
+
res.on("end", () => resolve(body));
|
|
161
|
+
res.on("error", reject);
|
|
162
|
+
});
|
|
163
|
+
req.on("error", reject);
|
|
164
|
+
req.setTimeout(15000, () => { req.destroy(); reject(new Error("Request timed out")); });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Content Source Management ───────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a content source entry from extracted text.
|
|
172
|
+
*/
|
|
173
|
+
function createContentSource(title, text, meta) {
|
|
174
|
+
const id = "ks-" + Date.now() + "-" + Math.random().toString(36).slice(2, 6);
|
|
175
|
+
return {
|
|
176
|
+
id,
|
|
177
|
+
title: title || "Untitled",
|
|
178
|
+
sources: [{
|
|
179
|
+
name: title || "Content",
|
|
180
|
+
content: text,
|
|
181
|
+
}],
|
|
182
|
+
meta: meta || {}, // { pages, url, fileType, fileName }
|
|
183
|
+
extractedAt: new Date().toISOString(),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extract keywords from a query string, including bigrams for compound concepts.
|
|
189
|
+
* E.g., "design system components" → ["design system", "design", "system", "components"]
|
|
190
|
+
*/
|
|
191
|
+
function extractKeywords(query) {
|
|
192
|
+
const words = (query || "")
|
|
193
|
+
.toLowerCase()
|
|
194
|
+
.replace(/[^\w\s-]/g, " ")
|
|
195
|
+
.split(/\s+/)
|
|
196
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
197
|
+
|
|
198
|
+
// Generate bigrams for multi-word concepts
|
|
199
|
+
const bigrams = [];
|
|
200
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
201
|
+
bigrams.push(words[i] + " " + words[i + 1]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Return bigrams first (higher specificity), then unigrams
|
|
205
|
+
return [...bigrams, ...words];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Score a text block against keywords using TF-IDF-like weighting.
|
|
210
|
+
* Bigrams score 4x (more specific), word-boundary matches score 2x,
|
|
211
|
+
* substring matches score 1x. Multiple occurrences add diminishing returns.
|
|
212
|
+
*/
|
|
213
|
+
function scoreText(text, keywords) {
|
|
214
|
+
const lower = text.toLowerCase();
|
|
215
|
+
let score = 0;
|
|
216
|
+
let matchedCount = 0;
|
|
217
|
+
const uniqueMatches = new Set();
|
|
218
|
+
|
|
219
|
+
for (const kw of keywords) {
|
|
220
|
+
if (!lower.includes(kw)) continue;
|
|
221
|
+
|
|
222
|
+
const isBigram = kw.includes(" ");
|
|
223
|
+
const regex = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
|
|
224
|
+
const matches = text.match(regex);
|
|
225
|
+
|
|
226
|
+
if (matches) {
|
|
227
|
+
// Count unique keyword roots matched
|
|
228
|
+
if (isBigram) {
|
|
229
|
+
kw.split(" ").forEach((w) => uniqueMatches.add(w));
|
|
230
|
+
} else {
|
|
231
|
+
uniqueMatches.add(kw);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Score: bigrams are worth more (more specific match)
|
|
235
|
+
const baseScore = isBigram ? 4 : 2;
|
|
236
|
+
// Diminishing returns for repeated matches: 1st=full, 2nd=half, 3rd+=quarter
|
|
237
|
+
const occurrences = Math.min(matches.length, 5);
|
|
238
|
+
score += baseScore + Math.min(occurrences - 1, 3) * (baseScore * 0.25);
|
|
239
|
+
} else {
|
|
240
|
+
// Substring match (less precise)
|
|
241
|
+
uniqueMatches.add(isBigram ? kw.split(" ")[0] : kw);
|
|
242
|
+
score += isBigram ? 2 : 0.5;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
matchedCount = uniqueMatches.size;
|
|
247
|
+
return { score, matchedCount };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract the most relevant passages from content using keyword scoring.
|
|
252
|
+
* Returns a string of the top passages within the char budget.
|
|
253
|
+
*/
|
|
254
|
+
function extractRelevantPassages(content, keywords, charBudget) {
|
|
255
|
+
if (!keywords || keywords.length === 0) {
|
|
256
|
+
// No keywords — fall back to truncation
|
|
257
|
+
return content.length > charBudget
|
|
258
|
+
? content.slice(0, charBudget) + "\n…[truncated]"
|
|
259
|
+
: content;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const paragraphs = content.split(/\n{2,}/).filter((p) => p.trim().length > 30);
|
|
263
|
+
if (paragraphs.length === 0) {
|
|
264
|
+
return content.slice(0, charBudget);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Score each paragraph
|
|
268
|
+
const scored = paragraphs.map((para) => {
|
|
269
|
+
const { score, matchedCount } = scoreText(para, keywords);
|
|
270
|
+
return { para: para.trim(), score, matchedCount };
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Sort by score descending, keep top paragraphs within budget
|
|
274
|
+
scored.sort((a, b) => b.score - a.score);
|
|
275
|
+
|
|
276
|
+
const selected = [];
|
|
277
|
+
let usedChars = 0;
|
|
278
|
+
for (const { para, score } of scored) {
|
|
279
|
+
if (score === 0 && selected.length > 0) break; // Stop adding irrelevant paragraphs
|
|
280
|
+
if (usedChars + para.length > charBudget) {
|
|
281
|
+
if (selected.length === 0) {
|
|
282
|
+
// At least include a truncated version of the best paragraph
|
|
283
|
+
selected.push(para.slice(0, charBudget) + "…");
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
selected.push(para);
|
|
288
|
+
usedChars += para.length;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return selected.join("\n\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Build a grounding context string from active content sources.
|
|
296
|
+
* Prepended to chat messages before routing to the AI provider.
|
|
297
|
+
*
|
|
298
|
+
* Optimizations:
|
|
299
|
+
* - Relevance filtering: only includes sources with keyword overlap
|
|
300
|
+
* - Adaptive truncation: extracts best paragraphs, not just first N chars
|
|
301
|
+
* - Budget-based: total grounding limited to ~4000 tokens (~16000 chars)
|
|
302
|
+
*/
|
|
303
|
+
const GROUNDING_CHAR_BUDGET = 16000; // ~4000 tokens total
|
|
304
|
+
|
|
305
|
+
function buildGroundingContext(contentSources, userQuery) {
|
|
306
|
+
if (!contentSources || contentSources.size === 0) return "";
|
|
307
|
+
|
|
308
|
+
const keywords = extractKeywords(userQuery);
|
|
309
|
+
|
|
310
|
+
// For chunked sources: score individual chunks and pick the best ones globally
|
|
311
|
+
// For non-chunked sources: sample multiple positions (not just first 2000 chars)
|
|
312
|
+
const allScoredChunks = []; // { sourceTitle, chunkTitle, content, score, meta }
|
|
313
|
+
|
|
314
|
+
for (const [id, data] of contentSources) {
|
|
315
|
+
if (data.isChunked && data.sources.length > 1) {
|
|
316
|
+
// Chunked source: score each chunk individually
|
|
317
|
+
// Extract unigrams for pre-filter (bigrams are checked in full scoring)
|
|
318
|
+
const unigrams = keywords.filter((k) => !k.includes(" "));
|
|
319
|
+
|
|
320
|
+
for (const src of data.sources) {
|
|
321
|
+
if (!src.content) continue;
|
|
322
|
+
// Pre-filter: check if any unigrams match stored chunk keywords
|
|
323
|
+
let preScore = 0;
|
|
324
|
+
if (src.keywords && unigrams.length > 0) {
|
|
325
|
+
for (const kw of unigrams) {
|
|
326
|
+
// Exact match on stored keywords (not substring)
|
|
327
|
+
if (src.keywords.includes(kw)) {
|
|
328
|
+
preScore += 2;
|
|
329
|
+
} else if (src.keywords.some((ck) => ck.includes(kw))) {
|
|
330
|
+
preScore += 0.5;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Full text scoring for chunks that passed pre-filter
|
|
335
|
+
if (preScore > 0 || keywords.length === 0) {
|
|
336
|
+
const { score: textScore, matchedCount } = scoreText(src.content.slice(0, 4000), keywords);
|
|
337
|
+
const totalScore = preScore + textScore;
|
|
338
|
+
if (totalScore > 0 || keywords.length === 0) {
|
|
339
|
+
allScoredChunks.push({
|
|
340
|
+
sourceTitle: data.title,
|
|
341
|
+
chunkTitle: src.title || data.title,
|
|
342
|
+
content: src.content,
|
|
343
|
+
score: totalScore,
|
|
344
|
+
matchedCount,
|
|
345
|
+
meta: data.meta,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
// Non-chunked source: sample multiple positions across the content
|
|
352
|
+
for (const src of data.sources) {
|
|
353
|
+
if (!src.content) continue;
|
|
354
|
+
let totalScore = 0;
|
|
355
|
+
const len = src.content.length;
|
|
356
|
+
const sampleSize = 2000;
|
|
357
|
+
const positions = [0, Math.floor(len * 0.25), Math.floor(len * 0.5), Math.floor(len * 0.75)];
|
|
358
|
+
for (const pos of positions) {
|
|
359
|
+
const { score } = scoreText(src.content.slice(pos, pos + sampleSize), keywords);
|
|
360
|
+
totalScore += score;
|
|
361
|
+
}
|
|
362
|
+
allScoredChunks.push({
|
|
363
|
+
sourceTitle: data.title,
|
|
364
|
+
chunkTitle: data.title,
|
|
365
|
+
content: src.content,
|
|
366
|
+
score: totalScore,
|
|
367
|
+
meta: data.meta,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (allScoredChunks.length === 0) return "";
|
|
374
|
+
|
|
375
|
+
// Sort all chunks by score, take the best ones within budget
|
|
376
|
+
allScoredChunks.sort((a, b) => b.score - a.score);
|
|
377
|
+
|
|
378
|
+
// Filter to relevant chunks (score > 0) if we have keywords
|
|
379
|
+
const relevant = keywords.length > 0
|
|
380
|
+
? allScoredChunks.filter((c) => c.score > 0)
|
|
381
|
+
: allScoredChunks;
|
|
382
|
+
|
|
383
|
+
const chunksToInclude = relevant.length > 0 ? relevant : allScoredChunks.slice(0, 5);
|
|
384
|
+
|
|
385
|
+
const parts = [];
|
|
386
|
+
parts.push("=== KNOWLEDGE CONTEXT ===");
|
|
387
|
+
parts.push("The following excerpts are from the user's knowledge library. Use them to provide accurate, well-sourced answers. Synthesize the information — do not dump raw text.\n");
|
|
388
|
+
|
|
389
|
+
let usedChars = 0;
|
|
390
|
+
let currentSource = "";
|
|
391
|
+
|
|
392
|
+
for (const chunk of chunksToInclude) {
|
|
393
|
+
if (usedChars >= GROUNDING_CHAR_BUDGET) break;
|
|
394
|
+
|
|
395
|
+
// Add source header if it changed
|
|
396
|
+
if (chunk.sourceTitle !== currentSource) {
|
|
397
|
+
const metaInfo = [];
|
|
398
|
+
if (chunk.meta?.url) metaInfo.push(chunk.meta.url);
|
|
399
|
+
const metaStr = metaInfo.length > 0 ? ` (${metaInfo.join(", ")})` : "";
|
|
400
|
+
parts.push(`\n--- "${chunk.sourceTitle}"${metaStr} ---`);
|
|
401
|
+
currentSource = chunk.sourceTitle;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Add chunk heading if different from source title
|
|
405
|
+
if (chunk.chunkTitle && chunk.chunkTitle !== chunk.sourceTitle) {
|
|
406
|
+
parts.push(`\n### ${chunk.chunkTitle}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const remaining = GROUNDING_CHAR_BUDGET - usedChars;
|
|
410
|
+
const passage = extractRelevantPassages(chunk.content, keywords, remaining);
|
|
411
|
+
parts.push(passage);
|
|
412
|
+
usedChars += passage.length;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
parts.push("\n=== END KNOWLEDGE CONTEXT ===\n");
|
|
416
|
+
return parts.join("\n");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ── Knowledge Hub ───────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
// Cache of parsed hub files: fileName → { id, title, text, meta, parsedAt }
|
|
422
|
+
const _hubCache = new Map();
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Scan the knowledge-hub/ folder and return a catalog of available files.
|
|
426
|
+
* Does NOT parse content — just lists what's available.
|
|
427
|
+
*/
|
|
428
|
+
function scanKnowledgeHub() {
|
|
429
|
+
if (!fs.existsSync(KNOWLEDGE_HUB_DIR)) return [];
|
|
430
|
+
|
|
431
|
+
const files = fs.readdirSync(KNOWLEDGE_HUB_DIR);
|
|
432
|
+
const supported = [".pdf", ".docx", ".doc", ".txt", ".md", ".csv", ".json", ".xml", ".html"];
|
|
433
|
+
|
|
434
|
+
return files
|
|
435
|
+
.filter((f) => {
|
|
436
|
+
if (f.startsWith(".")) return false;
|
|
437
|
+
// Prefer .chunks.json — hide the original PDF if a chunks file exists
|
|
438
|
+
if (f.endsWith(".chunks.json")) return true;
|
|
439
|
+
const ext = path.extname(f).toLowerCase();
|
|
440
|
+
if (!supported.includes(ext)) return false;
|
|
441
|
+
// Hide PDFs that have been converted to chunks
|
|
442
|
+
if (ext === ".pdf") {
|
|
443
|
+
const slug = f.replace(/\.pdf$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
444
|
+
if (files.includes(`${slug}.chunks.json`)) return false;
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
})
|
|
448
|
+
.map((f) => {
|
|
449
|
+
const isChunks = f.endsWith(".chunks.json");
|
|
450
|
+
const ext = isChunks ? "chunks" : path.extname(f).toLowerCase().replace(".", "");
|
|
451
|
+
const stat = fs.statSync(path.join(KNOWLEDGE_HUB_DIR, f));
|
|
452
|
+
const cached = _hubCache.get(f);
|
|
453
|
+
|
|
454
|
+
let title = f.replace(/\.chunks\.json$/, "").replace(/\.\w+$/, "").replace(/[-_]/g, " ");
|
|
455
|
+
let charCount = cached ? cached.totalChars || cached.text?.length : null;
|
|
456
|
+
let preview = cached ? (cached.preview || (cached.text || "").slice(0, 200)).replace(/\s+/g, " ").trim() : null;
|
|
457
|
+
|
|
458
|
+
// For chunks files, read title from the JSON metadata
|
|
459
|
+
if (isChunks && !cached) {
|
|
460
|
+
try {
|
|
461
|
+
const meta = JSON.parse(fs.readFileSync(path.join(KNOWLEDGE_HUB_DIR, f), "utf8"));
|
|
462
|
+
title = meta.title || title;
|
|
463
|
+
charCount = meta.totalChars || null;
|
|
464
|
+
preview = meta.chunks?.[0]?.text?.slice(0, 200)?.replace(/\s+/g, " ")?.trim() || null;
|
|
465
|
+
} catch {}
|
|
466
|
+
} else if (isChunks && cached) {
|
|
467
|
+
title = cached.title || title;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
fileName: f,
|
|
472
|
+
fileType: ext,
|
|
473
|
+
title,
|
|
474
|
+
sizeBytes: stat.size,
|
|
475
|
+
cached: !!cached,
|
|
476
|
+
preview,
|
|
477
|
+
charCount,
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Load and parse a specific file from the knowledge hub.
|
|
484
|
+
* Uses cache to avoid re-parsing on every request.
|
|
485
|
+
* Returns a content source ready to add to activeContentSources.
|
|
486
|
+
*/
|
|
487
|
+
async function loadHubFile(fileName) {
|
|
488
|
+
const filePath = path.join(KNOWLEDGE_HUB_DIR, fileName);
|
|
489
|
+
if (!fs.existsSync(filePath)) {
|
|
490
|
+
throw new Error(`File not found in knowledge hub: ${fileName}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check cache (invalidate if file changed)
|
|
494
|
+
const stat = fs.statSync(filePath);
|
|
495
|
+
const cached = _hubCache.get(fileName);
|
|
496
|
+
if (cached && cached.mtime === stat.mtimeMs) {
|
|
497
|
+
if (cached.chunks) {
|
|
498
|
+
// Return chunked source (multi-source for chunk-level scoring)
|
|
499
|
+
return createChunkedContentSource(cached.title, cached.chunks, cached.meta);
|
|
500
|
+
}
|
|
501
|
+
return createContentSource(cached.title, cached.text, cached.meta);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Handle .chunks.json files (pre-indexed, instant load)
|
|
505
|
+
if (fileName.endsWith(".chunks.json")) {
|
|
506
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
507
|
+
const meta = {
|
|
508
|
+
fileName,
|
|
509
|
+
fileType: "chunks",
|
|
510
|
+
hubFile: true,
|
|
511
|
+
pages: data.pages,
|
|
512
|
+
source: data.source,
|
|
513
|
+
chunkCount: data.chunks.length,
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Cache the chunks
|
|
517
|
+
_hubCache.set(fileName, {
|
|
518
|
+
title: data.title,
|
|
519
|
+
chunks: data.chunks,
|
|
520
|
+
totalChars: data.totalChars,
|
|
521
|
+
preview: data.chunks[0]?.text?.slice(0, 200) || "",
|
|
522
|
+
meta,
|
|
523
|
+
mtime: stat.mtimeMs,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return createChunkedContentSource(data.title, data.chunks, meta);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Parse the file (PDF, DOCX, plain text)
|
|
530
|
+
const buffer = fs.readFileSync(filePath);
|
|
531
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
532
|
+
let title, text;
|
|
533
|
+
const meta = { fileName, fileType: ext.replace(".", ""), hubFile: true };
|
|
534
|
+
|
|
535
|
+
if (ext === ".pdf") {
|
|
536
|
+
const result = await parsePdfBuffer(buffer);
|
|
537
|
+
title = result.title || fileName.replace(/\.\w+$/, "");
|
|
538
|
+
text = result.text;
|
|
539
|
+
meta.pages = result.pages;
|
|
540
|
+
} else if (ext === ".docx" || ext === ".doc") {
|
|
541
|
+
const result = await parseDocxBuffer(buffer);
|
|
542
|
+
title = result.title || fileName.replace(/\.\w+$/, "");
|
|
543
|
+
text = result.text;
|
|
544
|
+
} else {
|
|
545
|
+
title = fileName.replace(/\.\w+$/, "").replace(/[-_]/g, " ");
|
|
546
|
+
text = buffer.toString("utf-8");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!text || text.trim().length === 0) {
|
|
550
|
+
throw new Error(`No text content could be extracted from: ${fileName}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Cache the parsed result
|
|
554
|
+
_hubCache.set(fileName, { title, text, meta, mtime: stat.mtimeMs });
|
|
555
|
+
|
|
556
|
+
return createContentSource(title, text, meta);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Create a content source from pre-chunked data.
|
|
561
|
+
* Each chunk becomes a separate source entry for chunk-level scoring.
|
|
562
|
+
*/
|
|
563
|
+
function createChunkedContentSource(title, chunks, meta) {
|
|
564
|
+
return {
|
|
565
|
+
id: "hub-" + Date.now() + "-" + Math.random().toString(36).slice(2, 6),
|
|
566
|
+
title,
|
|
567
|
+
sources: chunks.map((chunk) => ({
|
|
568
|
+
title: chunk.heading || title,
|
|
569
|
+
content: chunk.text,
|
|
570
|
+
keywords: chunk.keywords || [],
|
|
571
|
+
})),
|
|
572
|
+
meta: meta || {},
|
|
573
|
+
extractedAt: new Date().toISOString(),
|
|
574
|
+
isChunked: true,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Search the knowledge hub for files relevant to a query.
|
|
580
|
+
* Simple keyword matching against file names and cached content.
|
|
581
|
+
*/
|
|
582
|
+
function searchHub(query) {
|
|
583
|
+
const catalog = scanKnowledgeHub();
|
|
584
|
+
if (!query || !query.trim()) return catalog;
|
|
585
|
+
|
|
586
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
587
|
+
return catalog
|
|
588
|
+
.map((item) => {
|
|
589
|
+
const haystack = (item.title + " " + item.fileName + " " + (item.preview || "")).toLowerCase();
|
|
590
|
+
const score = terms.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
591
|
+
return { ...item, score };
|
|
592
|
+
})
|
|
593
|
+
.filter((item) => item.score > 0)
|
|
594
|
+
.sort((a, b) => b.score - a.score);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Tier 1: Knowledge Hub Instant Answer ─────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
const STOP_WORDS = new Set([
|
|
600
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
601
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
602
|
+
"should", "may", "might", "shall", "can", "need", "must",
|
|
603
|
+
"i", "me", "my", "we", "our", "you", "your", "he", "she", "it",
|
|
604
|
+
"they", "them", "their", "this", "that", "these", "those",
|
|
605
|
+
"and", "or", "but", "not", "no", "nor", "so", "if", "then",
|
|
606
|
+
"of", "in", "on", "at", "to", "for", "with", "by", "from",
|
|
607
|
+
"about", "into", "through", "during", "before", "after",
|
|
608
|
+
"above", "below", "between", "under", "over",
|
|
609
|
+
"just", "also", "very", "too", "more", "most", "some", "any",
|
|
610
|
+
"all", "each", "every", "both", "few", "many", "much",
|
|
611
|
+
"tell", "explain", "describe", "show",
|
|
612
|
+
]);
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Search loaded content sources for a direct answer to a query.
|
|
616
|
+
* Scores ALL paragraphs globally, returns the top matches with surrounding context.
|
|
617
|
+
*/
|
|
618
|
+
function searchContentForAnswer(query, contentSources) {
|
|
619
|
+
if (!query || !contentSources || contentSources.size === 0) return null;
|
|
620
|
+
|
|
621
|
+
const keywords = extractKeywords(query);
|
|
622
|
+
if (keywords.length === 0) return null;
|
|
623
|
+
|
|
624
|
+
// Count only unique unigrams for threshold (bigrams are bonus)
|
|
625
|
+
const uniqueKeywords = keywords.filter((k) => !k.includes(" "));
|
|
626
|
+
const minMatches = Math.max(2, Math.ceil(uniqueKeywords.length * 0.4));
|
|
627
|
+
|
|
628
|
+
// Score ALL paragraphs globally
|
|
629
|
+
const candidates = [];
|
|
630
|
+
|
|
631
|
+
for (const [, source] of contentSources) {
|
|
632
|
+
for (const src of source.sources) {
|
|
633
|
+
if (!src.content) continue;
|
|
634
|
+
const paragraphs = src.content.split(/\n{2,}/).filter((p) => p.trim().length > 40);
|
|
635
|
+
|
|
636
|
+
for (let pi = 0; pi < paragraphs.length; pi++) {
|
|
637
|
+
const para = paragraphs[pi];
|
|
638
|
+
const { score, matchedCount } = scoreText(para, keywords);
|
|
639
|
+
|
|
640
|
+
if (matchedCount >= minMatches) {
|
|
641
|
+
// Include surrounding context (previous + next paragraph) for better answers
|
|
642
|
+
const contextParts = [];
|
|
643
|
+
if (pi > 0 && paragraphs[pi - 1].trim().length > 30) {
|
|
644
|
+
contextParts.push(paragraphs[pi - 1].trim());
|
|
645
|
+
}
|
|
646
|
+
contextParts.push(para.trim());
|
|
647
|
+
if (pi < paragraphs.length - 1 && paragraphs[pi + 1].trim().length > 30) {
|
|
648
|
+
contextParts.push(paragraphs[pi + 1].trim());
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
candidates.push({
|
|
652
|
+
text: contextParts.join("\n\n"),
|
|
653
|
+
mainPara: para.trim(),
|
|
654
|
+
score,
|
|
655
|
+
matchedCount,
|
|
656
|
+
title: src.title || source.title,
|
|
657
|
+
sourceTitle: source.title,
|
|
658
|
+
meta: source.meta,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (candidates.length === 0) return null;
|
|
666
|
+
|
|
667
|
+
// Sort by score descending, take top 3 for a richer answer
|
|
668
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
669
|
+
const topMatches = candidates.slice(0, 3);
|
|
670
|
+
|
|
671
|
+
// Deduplicate overlapping text
|
|
672
|
+
const seen = new Set();
|
|
673
|
+
const uniqueMatches = topMatches.filter((m) => {
|
|
674
|
+
const key = m.mainPara.slice(0, 100);
|
|
675
|
+
if (seen.has(key)) return false;
|
|
676
|
+
seen.add(key);
|
|
677
|
+
return true;
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Format the answer
|
|
681
|
+
const parts = [];
|
|
682
|
+
for (const match of uniqueMatches) {
|
|
683
|
+
const snippet = match.text.length > 1200
|
|
684
|
+
? match.text.slice(0, 1200) + "..."
|
|
685
|
+
: match.text;
|
|
686
|
+
|
|
687
|
+
const heading = match.title !== match.sourceTitle
|
|
688
|
+
? `**From "${match.sourceTitle}" — ${match.title}**`
|
|
689
|
+
: `**From "${match.sourceTitle}"**`;
|
|
690
|
+
|
|
691
|
+
parts.push(`${heading}:\n\n> ${snippet}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const metaInfo = [];
|
|
695
|
+
if (uniqueMatches[0].meta?.fileType) metaInfo.push(uniqueMatches[0].meta.fileType.toUpperCase());
|
|
696
|
+
if (uniqueMatches[0].meta?.pages) metaInfo.push(`${uniqueMatches[0].meta.pages} pages`);
|
|
697
|
+
const metaStr = metaInfo.length > 0 ? ` (${metaInfo.join(", ")})` : "";
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
text: parts.join("\n\n---\n\n") + `\n\n_Sources: ${[...new Set(uniqueMatches.map((m) => `"${m.sourceTitle}"`))].join(", ")}${metaStr}_`,
|
|
701
|
+
sources: uniqueMatches.map((m) => ({ title: m.sourceTitle, meta: m.meta })),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Tier 2: Web Reference Search ─────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
// Default reference sites — starts empty, users add via UI
|
|
708
|
+
// NN/g is suggested in the UI but not auto-enabled to avoid blocking chat
|
|
709
|
+
let referenceSites = [];
|
|
710
|
+
|
|
711
|
+
// Article cache: url → { title, text, fetchedAt }
|
|
712
|
+
const _articleCache = new Map();
|
|
713
|
+
const ARTICLE_CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
714
|
+
|
|
715
|
+
function getReferenceSites() {
|
|
716
|
+
return [...referenceSites];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function addReferenceSite(site) {
|
|
720
|
+
const id = "ref-" + Date.now() + "-" + Math.random().toString(36).slice(2, 6);
|
|
721
|
+
const entry = {
|
|
722
|
+
id,
|
|
723
|
+
name: site.name || site.searchDomain || "Reference",
|
|
724
|
+
baseUrl: site.baseUrl || site.url || "",
|
|
725
|
+
searchDomain: site.searchDomain || extractDomain(site.baseUrl || site.url || ""),
|
|
726
|
+
};
|
|
727
|
+
referenceSites.push(entry);
|
|
728
|
+
return entry;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function removeReferenceSite(id) {
|
|
732
|
+
referenceSites = referenceSites.filter((s) => s.id !== id);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function extractDomain(url) {
|
|
736
|
+
try {
|
|
737
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
738
|
+
} catch {
|
|
739
|
+
return url.replace(/^https?:\/\/(www\.)?/, "").split("/")[0];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Search configured reference sites for articles relevant to a query.
|
|
745
|
+
* Uses DuckDuckGo HTML search (no API key needed).
|
|
746
|
+
* Returns a formatted answer with link, or null if no match.
|
|
747
|
+
*/
|
|
748
|
+
async function searchReferenceSites(query) {
|
|
749
|
+
if (!query || referenceSites.length === 0) return null;
|
|
750
|
+
|
|
751
|
+
// Try each reference site
|
|
752
|
+
for (const site of referenceSites) {
|
|
753
|
+
try {
|
|
754
|
+
const result = await searchSingleSite(query, site);
|
|
755
|
+
if (result) return result;
|
|
756
|
+
} catch (err) {
|
|
757
|
+
console.error(`[web-ref] Error searching ${site.name}: ${err.message}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function searchSingleSite(query, site) {
|
|
764
|
+
const searchQuery = `site:${site.searchDomain} ${query}`;
|
|
765
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(searchQuery)}`;
|
|
766
|
+
|
|
767
|
+
// Fetch DuckDuckGo search results
|
|
768
|
+
let html;
|
|
769
|
+
try {
|
|
770
|
+
html = await fetchHtml(searchUrl);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
console.error(`[web-ref] DuckDuckGo search failed: ${err.message}`);
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Parse results to extract article URLs
|
|
777
|
+
const cheerio = getCheerio();
|
|
778
|
+
const $ = cheerio.load(html);
|
|
779
|
+
const resultLinks = [];
|
|
780
|
+
|
|
781
|
+
$("a.result__a").each((i, el) => {
|
|
782
|
+
const href = $(el).attr("href") || "";
|
|
783
|
+
const title = $(el).text().trim();
|
|
784
|
+
// DuckDuckGo wraps URLs in a redirect — extract the actual URL
|
|
785
|
+
const urlMatch = href.match(/uddg=([^&]+)/);
|
|
786
|
+
const actualUrl = urlMatch ? decodeURIComponent(urlMatch[1]) : href;
|
|
787
|
+
if (actualUrl.includes(site.searchDomain) && title) {
|
|
788
|
+
resultLinks.push({ url: actualUrl, title });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
if (resultLinks.length === 0) return null;
|
|
793
|
+
|
|
794
|
+
const topResult = resultLinks[0];
|
|
795
|
+
|
|
796
|
+
// Check article cache
|
|
797
|
+
const cached = _articleCache.get(topResult.url);
|
|
798
|
+
if (cached && Date.now() - cached.fetchedAt < ARTICLE_CACHE_TTL) {
|
|
799
|
+
return formatWebAnswer(cached.title, cached.text, topResult.url, site.name);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Fetch and extract article content
|
|
803
|
+
try {
|
|
804
|
+
const article = await fetchUrlContent(topResult.url);
|
|
805
|
+
const articleText = article.text.slice(0, 3000); // Truncate for answer
|
|
806
|
+
const articleTitle = article.title || topResult.title;
|
|
807
|
+
|
|
808
|
+
// Cache the article
|
|
809
|
+
_articleCache.set(topResult.url, {
|
|
810
|
+
title: articleTitle,
|
|
811
|
+
text: articleText,
|
|
812
|
+
fetchedAt: Date.now(),
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
return formatWebAnswer(articleTitle, articleText, topResult.url, site.name);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
console.error(`[web-ref] Failed to fetch article: ${err.message}`);
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function formatWebAnswer(title, text, url, siteName) {
|
|
823
|
+
// Extract the most relevant paragraphs (first ~1500 chars)
|
|
824
|
+
const excerpt = text.length > 1500 ? text.slice(0, 1500) + "..." : text;
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
text: `**From ${siteName}:**\n\n**[${title}](${url})**\n\n> ${excerpt}\n\n_[Read full article](${url})_`,
|
|
828
|
+
url,
|
|
829
|
+
title,
|
|
830
|
+
siteName,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Pre-warm the hub cache by loading all .chunks.json files.
|
|
836
|
+
* Call on relay startup for instant first-query response.
|
|
837
|
+
*/
|
|
838
|
+
async function prewarmHub() {
|
|
839
|
+
const catalog = scanKnowledgeHub();
|
|
840
|
+
let loaded = 0;
|
|
841
|
+
for (const file of catalog) {
|
|
842
|
+
if (file.fileName.endsWith(".chunks.json")) {
|
|
843
|
+
try {
|
|
844
|
+
await loadHubFile(file.fileName);
|
|
845
|
+
loaded++;
|
|
846
|
+
} catch {}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return loaded;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
module.exports = {
|
|
853
|
+
parsePdfBuffer,
|
|
854
|
+
parseDocxBuffer,
|
|
855
|
+
fetchUrlContent,
|
|
856
|
+
createContentSource,
|
|
857
|
+
createChunkedContentSource,
|
|
858
|
+
buildGroundingContext,
|
|
859
|
+
scanKnowledgeHub,
|
|
860
|
+
loadHubFile,
|
|
861
|
+
searchHub,
|
|
862
|
+
searchContentForAnswer,
|
|
863
|
+
searchReferenceSites,
|
|
864
|
+
getReferenceSites,
|
|
865
|
+
addReferenceSite,
|
|
866
|
+
removeReferenceSite,
|
|
867
|
+
prewarmHub,
|
|
868
|
+
KNOWLEDGE_HUB_DIR,
|
|
869
|
+
};
|