modular-studio 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -122
- package/dist/assets/Badge-Bsy2H_p2.js +1 -0
- package/dist/assets/GraphPanel-D4X3faxA.js +47 -0
- package/dist/assets/{Input-Bgp734xs.js → Input-Dyb88Erk.js} +1 -1
- package/dist/assets/KnowledgeTab-BccWz7Np.js +5 -0
- package/dist/assets/MemoryTab-Y_66cE01.js +16 -0
- package/dist/assets/QualificationTab-Dm9dEIpM.js +1 -0
- package/dist/assets/ReviewTab-BrfXSyyf.js +104 -0
- package/dist/assets/{Section-DoJrmytO.js → Section-68XDCFTl.js} +1 -1
- package/dist/assets/TestTab-CLKRT63X.js +42 -0
- package/dist/assets/ToolsTab-xumi9Uds.js +1 -0
- package/dist/assets/icons-CS8RUPBi.js +1 -0
- package/dist/assets/index-B2bm0161.css +1 -0
- package/dist/assets/index-C626nWuA.js +422 -0
- package/dist/assets/services-BDk6yY4o.js +369 -0
- package/dist/index.html +18 -18
- package/dist-server/bin/modular-mcp.js +1 -0
- package/dist-server/server/index.d.ts.map +1 -1
- package/dist-server/server/index.js +34 -0
- package/dist-server/server/mcp/manager.d.ts +3 -0
- package/dist-server/server/mcp/manager.d.ts.map +1 -1
- package/dist-server/server/mcp/manager.js +80 -5
- package/dist-server/server/migrations/index.d.ts +11 -0
- package/dist-server/server/migrations/index.d.ts.map +1 -0
- package/dist-server/server/migrations/index.js +57 -0
- package/dist-server/server/routes/agents.d.ts.map +1 -1
- package/dist-server/server/routes/agents.js +27 -0
- package/dist-server/server/routes/analytics.d.ts +3 -0
- package/dist-server/server/routes/analytics.d.ts.map +1 -0
- package/dist-server/server/routes/analytics.js +24 -0
- package/dist-server/server/routes/cache.d.ts +3 -0
- package/dist-server/server/routes/cache.d.ts.map +1 -0
- package/dist-server/server/routes/cache.js +55 -0
- package/dist-server/server/routes/connectors/airtable.d.ts +7 -0
- package/dist-server/server/routes/connectors/airtable.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/airtable.js +119 -0
- package/dist-server/server/routes/connectors/confluence.d.ts +7 -0
- package/dist-server/server/routes/connectors/confluence.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/confluence.js +176 -0
- package/dist-server/server/routes/connectors/github.d.ts +7 -0
- package/dist-server/server/routes/connectors/github.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/github.js +195 -0
- package/dist-server/server/routes/connectors/gmail.d.ts +7 -0
- package/dist-server/server/routes/connectors/gmail.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/gmail.js +115 -0
- package/dist-server/server/routes/connectors/google-docs.d.ts +10 -0
- package/dist-server/server/routes/connectors/google-docs.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-docs.js +165 -0
- package/dist-server/server/routes/connectors/google-drive.d.ts +7 -0
- package/dist-server/server/routes/connectors/google-drive.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-drive.js +163 -0
- package/dist-server/server/routes/connectors/google-sheets.d.ts +7 -0
- package/dist-server/server/routes/connectors/google-sheets.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/google-sheets.js +90 -0
- package/dist-server/server/routes/connectors/hubspot.d.ts +7 -0
- package/dist-server/server/routes/connectors/hubspot.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/hubspot.js +134 -0
- package/dist-server/server/routes/connectors/index.d.ts +6 -0
- package/dist-server/server/routes/connectors/index.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/index.js +38 -0
- package/dist-server/server/routes/connectors/jira.d.ts +7 -0
- package/dist-server/server/routes/connectors/jira.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/jira.js +151 -0
- package/dist-server/server/routes/connectors/linear.d.ts +7 -0
- package/dist-server/server/routes/connectors/linear.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/linear.js +154 -0
- package/dist-server/server/routes/connectors/notion.d.ts +10 -0
- package/dist-server/server/routes/connectors/notion.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/notion.js +201 -0
- package/dist-server/server/routes/connectors/plane.d.ts +10 -0
- package/dist-server/server/routes/connectors/plane.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/plane.js +189 -0
- package/dist-server/server/routes/connectors/shared.d.ts +25 -0
- package/dist-server/server/routes/connectors/shared.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/shared.js +202 -0
- package/dist-server/server/routes/connectors/slack.d.ts +7 -0
- package/dist-server/server/routes/connectors/slack.d.ts.map +1 -0
- package/dist-server/server/routes/connectors/slack.js +153 -0
- package/dist-server/server/routes/connectors.d.ts.map +1 -1
- package/dist-server/server/routes/connectors.js +47 -17
- package/dist-server/server/routes/cost.d.ts +3 -0
- package/dist-server/server/routes/cost.d.ts.map +1 -0
- package/dist-server/server/routes/cost.js +113 -0
- package/dist-server/server/routes/graph.d.ts +11 -0
- package/dist-server/server/routes/graph.d.ts.map +1 -0
- package/dist-server/server/routes/graph.js +213 -0
- package/dist-server/server/routes/lessons.d.ts +3 -0
- package/dist-server/server/routes/lessons.d.ts.map +1 -0
- package/dist-server/server/routes/lessons.js +160 -0
- package/dist-server/server/routes/llm.d.ts.map +1 -1
- package/dist-server/server/routes/llm.js +85 -18
- package/dist-server/server/routes/memory.d.ts.map +1 -1
- package/dist-server/server/routes/memory.js +31 -0
- package/dist-server/server/routes/metaprompt-v2.d.ts +3 -0
- package/dist-server/server/routes/metaprompt-v2.d.ts.map +1 -0
- package/dist-server/server/routes/metaprompt-v2.js +104 -0
- package/dist-server/server/routes/qualification.d.ts.map +1 -1
- package/dist-server/server/routes/qualification.js +342 -334
- package/dist-server/server/routes/repo-index.d.ts.map +1 -1
- package/dist-server/server/routes/repo-index.js +7 -0
- package/dist-server/server/routes/skills-search.d.ts.map +1 -1
- package/dist-server/server/routes/skills-search.js +192 -26
- package/dist-server/server/routes/tool-analytics.d.ts +3 -0
- package/dist-server/server/routes/tool-analytics.d.ts.map +1 -0
- package/dist-server/server/routes/tool-analytics.js +47 -0
- package/dist-server/server/services/adapters/hindsightAdapter.d.ts +28 -0
- package/dist-server/server/services/adapters/hindsightAdapter.d.ts.map +1 -0
- package/dist-server/server/services/adapters/hindsightAdapter.js +63 -0
- package/dist-server/server/services/adapters/postgresAdapter.js +30 -30
- package/dist-server/server/services/adapters/sqliteAdapter.d.ts +1 -0
- package/dist-server/server/services/adapters/sqliteAdapter.d.ts.map +1 -1
- package/dist-server/server/services/adapters/sqliteAdapter.js +66 -36
- package/dist-server/server/services/agentStore.d.ts +2 -1
- package/dist-server/server/services/agentStore.d.ts.map +1 -1
- package/dist-server/server/services/agentStore.js +2 -1
- package/dist-server/server/services/correctionDetector.d.ts +22 -0
- package/dist-server/server/services/correctionDetector.d.ts.map +1 -0
- package/dist-server/server/services/correctionDetector.js +91 -0
- package/dist-server/server/services/credentialStore.d.ts +10 -0
- package/dist-server/server/services/credentialStore.d.ts.map +1 -0
- package/dist-server/server/services/credentialStore.js +123 -0
- package/dist-server/server/services/hindsightClient.d.ts +15 -0
- package/dist-server/server/services/hindsightClient.d.ts.map +1 -0
- package/dist-server/server/services/hindsightClient.js +48 -0
- package/dist-server/server/services/lessonExtractor.d.ts +21 -0
- package/dist-server/server/services/lessonExtractor.d.ts.map +1 -0
- package/dist-server/server/services/lessonExtractor.js +92 -0
- package/dist-server/server/services/repoIndexer.d.ts +7 -1
- package/dist-server/server/services/repoIndexer.d.ts.map +1 -1
- package/dist-server/server/services/repoIndexer.js +295 -94
- package/dist-server/server/services/responseCache.d.ts +24 -0
- package/dist-server/server/services/responseCache.d.ts.map +1 -0
- package/dist-server/server/services/responseCache.js +163 -0
- package/dist-server/server/services/sqliteStore.d.ts +72 -0
- package/dist-server/server/services/sqliteStore.d.ts.map +1 -1
- package/dist-server/server/services/sqliteStore.js +291 -13
- package/dist-server/src/config.d.ts +2 -0
- package/dist-server/src/config.d.ts.map +1 -0
- package/dist-server/src/config.js +3 -0
- package/dist-server/src/graph/db.d.ts +46 -0
- package/dist-server/src/graph/db.d.ts.map +1 -0
- package/dist-server/src/graph/db.js +241 -0
- package/dist-server/src/graph/extractors/code.d.ts +12 -0
- package/dist-server/src/graph/extractors/code.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/code.js +239 -0
- package/dist-server/src/graph/extractors/cross-type.d.ts +16 -0
- package/dist-server/src/graph/extractors/cross-type.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/cross-type.js +67 -0
- package/dist-server/src/graph/extractors/markdown.d.ts +29 -0
- package/dist-server/src/graph/extractors/markdown.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/markdown.js +224 -0
- package/dist-server/src/graph/extractors/yaml.d.ts +15 -0
- package/dist-server/src/graph/extractors/yaml.d.ts.map +1 -0
- package/dist-server/src/graph/extractors/yaml.js +104 -0
- package/dist-server/src/graph/index.d.ts +62 -0
- package/dist-server/src/graph/index.d.ts.map +1 -0
- package/dist-server/src/graph/index.js +67 -0
- package/dist-server/src/graph/packer.d.ts +19 -0
- package/dist-server/src/graph/packer.d.ts.map +1 -0
- package/dist-server/src/graph/packer.js +134 -0
- package/dist-server/src/graph/resolver.d.ts +12 -0
- package/dist-server/src/graph/resolver.d.ts.map +1 -0
- package/dist-server/src/graph/resolver.js +81 -0
- package/dist-server/src/graph/scanner.d.ts +34 -0
- package/dist-server/src/graph/scanner.d.ts.map +1 -0
- package/dist-server/src/graph/scanner.js +252 -0
- package/dist-server/src/graph/traverser.d.ts +17 -0
- package/dist-server/src/graph/traverser.d.ts.map +1 -0
- package/dist-server/src/graph/traverser.js +185 -0
- package/dist-server/src/graph/types.d.ts +117 -0
- package/dist-server/src/graph/types.d.ts.map +1 -0
- package/dist-server/src/graph/types.js +63 -0
- package/dist-server/src/metaprompt/v2/assembler.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/assembler.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/assembler.js +261 -0
- package/dist-server/src/metaprompt/v2/context-strategist.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/context-strategist.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/context-strategist.js +173 -0
- package/dist-server/src/metaprompt/v2/evaluator.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/evaluator.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/evaluator.js +281 -0
- package/dist-server/src/metaprompt/v2/index.d.ts +41 -0
- package/dist-server/src/metaprompt/v2/index.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/index.js +90 -0
- package/dist-server/src/metaprompt/v2/parser.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/parser.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/parser.js +138 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/pattern-selector.js +154 -0
- package/dist-server/src/metaprompt/v2/researcher.d.ts +3 -0
- package/dist-server/src/metaprompt/v2/researcher.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/researcher.js +194 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.d.ts +74 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/tool-discovery.js +290 -0
- package/dist-server/src/metaprompt/v2/types.d.ts +154 -0
- package/dist-server/src/metaprompt/v2/types.d.ts.map +1 -0
- package/dist-server/src/metaprompt/v2/types.js +2 -0
- package/dist-server/src/services/contradictionDetector.js +1 -1
- package/dist-server/src/services/llmService.d.ts +61 -0
- package/dist-server/src/services/llmService.d.ts.map +1 -0
- package/dist-server/src/services/llmService.js +222 -0
- package/dist-server/src/store/knowledgeBase.d.ts +6 -1
- package/dist-server/src/store/knowledgeBase.d.ts.map +1 -1
- package/dist-server/src/store/knowledgeBase.js +0 -1
- package/dist-server/src/store/lessonStore.d.ts +26 -0
- package/dist-server/src/store/lessonStore.d.ts.map +1 -0
- package/dist-server/src/store/lessonStore.js +64 -0
- package/dist-server/src/store/mcp-registry.d.ts +29 -0
- package/dist-server/src/store/mcp-registry.d.ts.map +1 -0
- package/dist-server/src/store/mcp-registry.js +1303 -0
- package/dist-server/src/store/memoryStore.d.ts +12 -1
- package/dist-server/src/store/memoryStore.d.ts.map +1 -1
- package/dist-server/src/store/memoryStore.js +9 -0
- package/dist-server/src/types/registry.types.d.ts +13 -0
- package/dist-server/src/types/registry.types.d.ts.map +1 -0
- package/dist-server/src/types/registry.types.js +2 -0
- package/dist-server/tsconfig.server.tsbuildinfo +1 -1
- package/package.json +15 -1
- package/scripts/cleanup-worktrees.ps1 +29 -0
- package/dist/assets/Badge-22Ai0eyi.js +0 -1
- package/dist/assets/KnowledgeTab-DABxirZh.js +0 -4
- package/dist/assets/MemoryTab-DZeYElIT.js +0 -16
- package/dist/assets/QualificationTab-Dfpy3J30.js +0 -1
- package/dist/assets/ReviewTab-SD8lQuCc.js +0 -103
- package/dist/assets/TestTab-PDyMF8Fw.js +0 -33
- package/dist/assets/ToolsTab-B83qGCmG.js +0 -1
- package/dist/assets/icons-C2EV-le6.js +0 -1
- package/dist/assets/index-DkpMAxX7.css +0 -1
- package/dist/assets/index-q24ug5Qs.js +0 -143
- package/dist/assets/services-BaKotDf0.js +0 -343
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repoIndexer.d.ts","sourceRoot":"","sources":["../../../server/services/repoIndexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,OAAO,GACP,SAAS,GACT,MAAM,GACN,OAAO,GACP,QAAQ,GACR,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,
|
|
1
|
+
{"version":3,"file":"repoIndexer.d.ts","sourceRoot":"","sources":["../../../server/services/repoIndexer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAOH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,OAAO,GACP,SAAS,GACT,MAAM,GACN,OAAO,GACP,QAAQ,GACR,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,WAAW,EAAE,cAAc,EAAE,CAAC;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAmfD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAoErD;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAsD1D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CAkD/E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAezE"}
|
|
@@ -34,6 +34,22 @@ const CODE_EXTENSIONS = new Set([
|
|
|
34
34
|
]);
|
|
35
35
|
const MAX_FILE_SIZE = 100_000; // 100KB
|
|
36
36
|
const MAX_FILES = 2000;
|
|
37
|
+
// ── Feature Clustering Constants ──
|
|
38
|
+
const GENERIC_DIRS = new Set([
|
|
39
|
+
'src', 'lib', 'app', 'server', 'client', 'api',
|
|
40
|
+
'services', 'service', 'store', 'stores', 'state',
|
|
41
|
+
'components', 'component', 'pages', 'page', 'views', 'view',
|
|
42
|
+
'routes', 'route', 'controllers', 'controller',
|
|
43
|
+
'utils', 'util', 'helpers', 'helper', 'common', 'shared',
|
|
44
|
+
'models', 'model', 'types', 'interfaces',
|
|
45
|
+
'panels', 'panel', 'tabs', 'tab', 'layouts', 'layout',
|
|
46
|
+
'middleware', 'middlewares', 'guards', 'pipes',
|
|
47
|
+
'config', 'configs', 'constants',
|
|
48
|
+
'hooks', 'composables', 'providers',
|
|
49
|
+
'assets', 'static', 'public',
|
|
50
|
+
'__tests__', '__mocks__', 'test', 'tests', 'spec', 'specs',
|
|
51
|
+
]);
|
|
52
|
+
const FUNCTIONAL_SUFFIXES = /(?:Service|Store|Controller|Route|Router|Form|Panel|Component|View|Page|Tab|Utils?|Helpers?|Handler|Manager|Provider|Factory|Adapter|Middleware|Guard|Pipe|Module|Config|Spec|Test|Mock|Fixture|Schema|Model|Entity|DTO|Repository|Gateway|Client|Api|Hook|Composable|Plugin|Decorator|Interceptor|Filter|Resolver|Validator|Transformer|Mapper|Builder|Strategy|Observer|Subscriber|Listener|Worker|Job|Task|Queue|Cache|Logger|Monitor|Migration|Seed|Index)$/;
|
|
37
53
|
// ── Scanner ──
|
|
38
54
|
function categorizeFile(path, content) {
|
|
39
55
|
const base = basename(path).toLowerCase();
|
|
@@ -218,62 +234,259 @@ function detectConventions(files) {
|
|
|
218
234
|
}
|
|
219
235
|
return conventions;
|
|
220
236
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
//
|
|
227
|
-
|
|
237
|
+
// ── Feature Clustering Helpers ──
|
|
238
|
+
function extractDomainStem(filePath) {
|
|
239
|
+
const parts = filePath.replace(/\\/g, '/').split('/');
|
|
240
|
+
const fileName = parts[parts.length - 1];
|
|
241
|
+
const baseName = fileName.replace(/\.[^.]+$/, ''); // strip extension
|
|
242
|
+
// Find last non-generic directory
|
|
243
|
+
let specificDir = '';
|
|
244
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
245
|
+
const dir = parts[i].toLowerCase().replace(/[-_]/g, '');
|
|
246
|
+
if (!GENERIC_DIRS.has(dir) && dir.length > 1) {
|
|
247
|
+
specificDir = parts[i];
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Strip functional suffix from filename
|
|
252
|
+
const strippedName = baseName.replace(FUNCTIONAL_SUFFIXES, '');
|
|
253
|
+
// If filename is generic (index, main, app, types, utils), use directory
|
|
254
|
+
const GENERIC_NAMES = new Set(['index', 'main', 'app', 'types', 'utils', 'helpers', 'constants', 'config', 'mod']);
|
|
255
|
+
const nameToUse = GENERIC_NAMES.has(strippedName.toLowerCase()) && specificDir
|
|
256
|
+
? specificDir
|
|
257
|
+
: strippedName;
|
|
258
|
+
// Normalize: camelCase/PascalCase → kebab → lower
|
|
259
|
+
return nameToUse
|
|
260
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2') // camelCase → kebab
|
|
261
|
+
.replace(/[-_]+/g, '-')
|
|
262
|
+
.replace(/\bv\d+$/, '') // strip version suffix (v2, v3)
|
|
263
|
+
.toLowerCase()
|
|
264
|
+
.replace(/-+$/, '');
|
|
265
|
+
}
|
|
266
|
+
function resolveImportPath(fromFile, importPath, allFiles) {
|
|
267
|
+
if (!importPath.startsWith('.'))
|
|
268
|
+
return null; // skip node_modules
|
|
269
|
+
const fromDir = fromFile.split('/').slice(0, -1).join('/');
|
|
270
|
+
let resolved = importPath;
|
|
271
|
+
// Resolve relative path
|
|
272
|
+
if (resolved.startsWith('./'))
|
|
273
|
+
resolved = resolved.slice(2);
|
|
274
|
+
if (resolved.startsWith('../')) {
|
|
275
|
+
const parts = fromDir.split('/');
|
|
276
|
+
for (const seg of resolved.split('/')) {
|
|
277
|
+
if (seg === '..')
|
|
278
|
+
parts.pop();
|
|
279
|
+
else if (seg !== '.')
|
|
280
|
+
parts.push(seg);
|
|
281
|
+
}
|
|
282
|
+
resolved = parts.join('/');
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
resolved = fromDir ? fromDir + '/' + resolved : resolved;
|
|
286
|
+
}
|
|
287
|
+
// Try exact match, then with extensions
|
|
288
|
+
const candidates = [resolved, resolved + '.ts', resolved + '.tsx', resolved + '.js', resolved + '.jsx', resolved + '/index.ts', resolved + '/index.js'];
|
|
289
|
+
for (const c of candidates) {
|
|
290
|
+
if (allFiles.some(f => f.path === c))
|
|
291
|
+
return c;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function humanizeStem(stem) {
|
|
296
|
+
return stem
|
|
297
|
+
.replace(/-/g, ' ')
|
|
298
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
299
|
+
}
|
|
300
|
+
function clusterByDomainAndImports(files) {
|
|
301
|
+
// A) Seed clusters by domain stem
|
|
302
|
+
const stemMap = new Map();
|
|
228
303
|
for (const f of files) {
|
|
229
|
-
if (f.category === '
|
|
304
|
+
if (f.category === 'config' || f.category === 'style')
|
|
230
305
|
continue;
|
|
231
|
-
const
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
306
|
+
const stem = extractDomainStem(f.path);
|
|
307
|
+
if (!stemMap.has(stem))
|
|
308
|
+
stemMap.set(stem, []);
|
|
309
|
+
stemMap.get(stem).push(f);
|
|
235
310
|
}
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
311
|
+
// B) Build reverse import index: file path → which files import it
|
|
312
|
+
const importedBy = new Map();
|
|
313
|
+
for (const f of files) {
|
|
314
|
+
for (const imp of f.imports) {
|
|
315
|
+
const resolved = resolveImportPath(f.path, imp, files);
|
|
316
|
+
if (!resolved)
|
|
317
|
+
continue;
|
|
318
|
+
if (!importedBy.has(resolved))
|
|
319
|
+
importedBy.set(resolved, new Set());
|
|
320
|
+
importedBy.get(resolved).add(f.path);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// C) Absorb orphans: files in clusters of size 1
|
|
324
|
+
// If imported by ≥2 files from the same cluster → join that cluster
|
|
325
|
+
const orphanStems = [...stemMap.entries()].filter(([, fs]) => fs.length === 1);
|
|
326
|
+
for (const [stem, [orphan]] of orphanStems) {
|
|
327
|
+
const importers = importedBy.get(orphan.path);
|
|
328
|
+
if (!importers)
|
|
239
329
|
continue;
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
330
|
+
// Count how many files from each OTHER cluster import this orphan
|
|
331
|
+
const clusterHits = new Map();
|
|
332
|
+
for (const importerPath of importers) {
|
|
333
|
+
for (const [otherStem, otherFiles] of stemMap) {
|
|
334
|
+
if (otherStem === stem)
|
|
335
|
+
continue;
|
|
336
|
+
if (otherFiles.some(f => f.path === importerPath)) {
|
|
337
|
+
clusterHits.set(otherStem, (clusterHits.get(otherStem) ?? 0) + 1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Join the cluster with most imports (≥2 threshold)
|
|
342
|
+
let bestCluster = '';
|
|
343
|
+
let bestCount = 1; // threshold: needs ≥2
|
|
344
|
+
for (const [cs, count] of clusterHits) {
|
|
345
|
+
if (count > bestCount) {
|
|
346
|
+
bestCluster = cs;
|
|
347
|
+
bestCount = count;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (bestCluster) {
|
|
351
|
+
stemMap.get(bestCluster).push(orphan);
|
|
352
|
+
stemMap.delete(stem);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return stemMap;
|
|
356
|
+
}
|
|
357
|
+
function buildFeatures(stemMap, allFiles) {
|
|
358
|
+
const features = [];
|
|
359
|
+
const sharedFiles = [];
|
|
360
|
+
// A) Merge micro-clusters (<3 files)
|
|
361
|
+
const microStems = [...stemMap.entries()].filter(([, fs]) => fs.length < 3);
|
|
362
|
+
for (const [stem, fs] of microStems) {
|
|
363
|
+
// Find most-connected larger cluster
|
|
364
|
+
let bestTarget = '';
|
|
365
|
+
let bestConnections = 0;
|
|
366
|
+
for (const [otherStem, otherFiles] of stemMap) {
|
|
367
|
+
if (otherStem === stem || otherFiles.length < 3)
|
|
368
|
+
continue;
|
|
369
|
+
let connections = 0;
|
|
370
|
+
for (const f of fs) {
|
|
371
|
+
for (const imp of f.imports) {
|
|
372
|
+
const resolved = resolveImportPath(f.path, imp, allFiles);
|
|
373
|
+
if (resolved && otherFiles.some(of => of.path === resolved))
|
|
374
|
+
connections++;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (connections > bestConnections) {
|
|
378
|
+
bestTarget = otherStem;
|
|
379
|
+
bestConnections = connections;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (bestTarget) {
|
|
383
|
+
stemMap.get(bestTarget).push(...fs);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
sharedFiles.push(...fs);
|
|
387
|
+
}
|
|
388
|
+
stemMap.delete(stem);
|
|
389
|
+
}
|
|
390
|
+
// Add "shared" cluster if any orphans remain
|
|
391
|
+
if (sharedFiles.length > 0) {
|
|
392
|
+
stemMap.set('shared', sharedFiles);
|
|
393
|
+
}
|
|
394
|
+
// B) Build RepoFeature for each cluster
|
|
395
|
+
for (const [stem, clusterFiles] of stemMap) {
|
|
396
|
+
if (clusterFiles.length === 0)
|
|
397
|
+
continue;
|
|
398
|
+
// Compute import fan-in within cluster
|
|
399
|
+
const fanIn = new Map();
|
|
400
|
+
for (const f of clusterFiles) {
|
|
401
|
+
for (const imp of f.imports) {
|
|
402
|
+
const resolved = resolveImportPath(f.path, imp, allFiles);
|
|
403
|
+
if (resolved && clusterFiles.some(cf => cf.path === resolved)) {
|
|
404
|
+
fanIn.set(resolved, (fanIn.get(resolved) ?? 0) + 1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const keyFiles = [...clusterFiles]
|
|
409
|
+
.sort((a, b) => (fanIn.get(b.path) ?? 0) - (fanIn.get(a.path) ?? 0))
|
|
410
|
+
.slice(0, 5)
|
|
411
|
+
.map(f => f.path);
|
|
412
|
+
// Internal imports
|
|
245
413
|
const internalImports = new Map();
|
|
246
|
-
for (const f of
|
|
247
|
-
const deps = f.imports
|
|
414
|
+
for (const f of clusterFiles) {
|
|
415
|
+
const deps = f.imports
|
|
416
|
+
.map(imp => resolveImportPath(f.path, imp, allFiles))
|
|
417
|
+
.filter((r) => r !== null && clusterFiles.some(cf => cf.path === r));
|
|
248
418
|
if (deps.length > 0)
|
|
249
419
|
internalImports.set(f.path, deps);
|
|
250
420
|
}
|
|
251
|
-
//
|
|
252
|
-
const
|
|
253
|
-
for (const f of
|
|
421
|
+
// Cross-feature deps
|
|
422
|
+
const crossDeps = new Set();
|
|
423
|
+
for (const f of clusterFiles) {
|
|
254
424
|
for (const imp of f.imports) {
|
|
255
|
-
const resolved =
|
|
256
|
-
|
|
425
|
+
const resolved = resolveImportPath(f.path, imp, allFiles);
|
|
426
|
+
if (!resolved)
|
|
427
|
+
continue;
|
|
428
|
+
if (clusterFiles.some(cf => cf.path === resolved))
|
|
429
|
+
continue; // internal
|
|
430
|
+
// Find which feature owns this file
|
|
431
|
+
for (const [otherStem, otherFiles] of stemMap) {
|
|
432
|
+
if (otherStem === stem)
|
|
433
|
+
continue;
|
|
434
|
+
if (otherFiles.some(of => of.path === resolved)) {
|
|
435
|
+
crossDeps.add(humanizeStem(otherStem));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
257
438
|
}
|
|
258
439
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
name: humanizeDirName(dir),
|
|
265
|
-
description: '', // filled by LLM later
|
|
266
|
-
modules: modules.filter(m => m.path.startsWith(dir)).map(m => m.path),
|
|
440
|
+
features.push({
|
|
441
|
+
name: humanizeStem(stem),
|
|
442
|
+
domainStem: stem,
|
|
443
|
+
description: '',
|
|
444
|
+
files: clusterFiles.map(f => f.path),
|
|
267
445
|
keyFiles,
|
|
268
|
-
stores:
|
|
269
|
-
routes:
|
|
270
|
-
components:
|
|
446
|
+
stores: clusterFiles.filter(f => f.category === 'store').map(f => f.path),
|
|
447
|
+
routes: clusterFiles.filter(f => f.category === 'route').map(f => f.path),
|
|
448
|
+
components: clusterFiles.filter(f => f.category === 'component').map(f => f.path),
|
|
449
|
+
services: clusterFiles.filter(f => f.category === 'service').map(f => f.path),
|
|
450
|
+
tests: clusterFiles.filter(f => f.category === 'test').map(f => f.path),
|
|
271
451
|
imports: internalImports,
|
|
452
|
+
crossFeatureDeps: [...crossDeps],
|
|
453
|
+
fileCount: clusterFiles.length,
|
|
454
|
+
tokenCount: clusterFiles.reduce((sum, f) => sum + f.tokens, 0),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// Cap at 50 features, merge smallest
|
|
458
|
+
features.sort((a, b) => b.fileCount - a.fileCount);
|
|
459
|
+
if (features.length > 50) {
|
|
460
|
+
const kept = features.slice(0, 49);
|
|
461
|
+
const merged = features.slice(49);
|
|
462
|
+
const overflow = {
|
|
463
|
+
name: 'Other',
|
|
464
|
+
domainStem: 'other',
|
|
465
|
+
description: `${merged.length} small feature clusters merged`,
|
|
466
|
+
files: merged.flatMap(f => f.files),
|
|
467
|
+
keyFiles: merged.flatMap(f => f.keyFiles).slice(0, 5),
|
|
468
|
+
stores: merged.flatMap(f => f.stores),
|
|
469
|
+
routes: merged.flatMap(f => f.routes),
|
|
470
|
+
components: merged.flatMap(f => f.components),
|
|
471
|
+
services: merged.flatMap(f => f.services),
|
|
472
|
+
tests: merged.flatMap(f => f.tests),
|
|
473
|
+
imports: new Map(),
|
|
474
|
+
crossFeatureDeps: [],
|
|
475
|
+
fileCount: merged.reduce((s, f) => s + f.fileCount, 0),
|
|
476
|
+
tokenCount: merged.reduce((s, f) => s + f.tokenCount, 0),
|
|
272
477
|
};
|
|
273
|
-
|
|
478
|
+
kept.push(overflow);
|
|
479
|
+
return kept;
|
|
274
480
|
}
|
|
275
481
|
return features;
|
|
276
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Cluster files into feature groups using a 3-pass domain+import algorithm.
|
|
485
|
+
*/
|
|
486
|
+
function clusterFeatures(files, _modules) {
|
|
487
|
+
const stemMap = clusterByDomainAndImports(files);
|
|
488
|
+
return buildFeatures(stemMap, files);
|
|
489
|
+
}
|
|
277
490
|
function humanizeDirName(dir) {
|
|
278
491
|
return dir
|
|
279
492
|
.replace(/^src\//, '')
|
|
@@ -415,16 +628,15 @@ export function generateOverviewDoc(scan) {
|
|
|
415
628
|
}
|
|
416
629
|
}
|
|
417
630
|
// Feature list
|
|
418
|
-
lines.push(`## Features`);
|
|
631
|
+
lines.push(`## Features (${scan.features.length} clusters)`);
|
|
632
|
+
lines.push('');
|
|
419
633
|
for (const f of scan.features) {
|
|
420
|
-
|
|
421
|
-
lines.push(
|
|
422
|
-
if (f.
|
|
423
|
-
lines.push(`
|
|
424
|
-
if (f.
|
|
425
|
-
lines.push(`
|
|
426
|
-
if (f.routes.length)
|
|
427
|
-
lines.push(`Routes: ${f.routes.join(', ')}`);
|
|
634
|
+
const tokK = (f.tokenCount / 1000).toFixed(1);
|
|
635
|
+
lines.push(`### ${f.name} (${f.fileCount} files · ~${tokK}K tokens)`);
|
|
636
|
+
if (f.keyFiles.length > 0)
|
|
637
|
+
lines.push(`Key: ${f.keyFiles.map(p => p.split('/').pop()).join(', ')}`);
|
|
638
|
+
if (f.crossFeatureDeps.length > 0)
|
|
639
|
+
lines.push(`Deps: ${f.crossFeatureDeps.join(', ')}`);
|
|
428
640
|
lines.push('');
|
|
429
641
|
}
|
|
430
642
|
return lines.join('\n');
|
|
@@ -434,59 +646,48 @@ export function generateOverviewDoc(scan) {
|
|
|
434
646
|
*/
|
|
435
647
|
export function generateFeatureDoc(scan, feature) {
|
|
436
648
|
const lines = [];
|
|
649
|
+
const tokK = (feature.tokenCount / 1000).toFixed(1);
|
|
437
650
|
lines.push(`# Feature: ${feature.name}`);
|
|
651
|
+
lines.push(`${feature.fileCount} files · ~${tokK}K tokens`);
|
|
438
652
|
lines.push('');
|
|
653
|
+
// Key Files
|
|
654
|
+
if (feature.keyFiles.length > 0) {
|
|
655
|
+
lines.push(`## Key Files`);
|
|
656
|
+
for (const fp of feature.keyFiles) {
|
|
657
|
+
const file = scan.files.find(f => f.path === fp);
|
|
658
|
+
if (file) {
|
|
659
|
+
const symbols = [...file.exports, ...file.functions.slice(0, 5)].filter(Boolean).slice(0, 8);
|
|
660
|
+
const symStr = symbols.length > 0 ? ` — ${symbols.join(', ')}` : '';
|
|
661
|
+
lines.push(`- ${basename(fp)} (${file.category})${symStr}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
lines.push('');
|
|
665
|
+
}
|
|
666
|
+
// Architecture
|
|
439
667
|
lines.push(`## Architecture`);
|
|
440
|
-
|
|
668
|
+
if (feature.stores.length > 0)
|
|
669
|
+
lines.push(`- Stores: ${feature.stores.map(p => basename(p)).join(', ')}`);
|
|
670
|
+
if (feature.routes.length > 0)
|
|
671
|
+
lines.push(`- Routes: ${feature.routes.map(p => basename(p)).join(', ')}`);
|
|
672
|
+
if (feature.services.length > 0)
|
|
673
|
+
lines.push(`- Services: ${feature.services.map(p => basename(p)).join(', ')}`);
|
|
674
|
+
if (feature.components.length > 0)
|
|
675
|
+
lines.push(`- Components: ${feature.components.map(p => basename(p)).join(', ')}`);
|
|
676
|
+
if (feature.tests.length > 0)
|
|
677
|
+
lines.push(`- Tests: ${feature.tests.map(p => basename(p)).join(', ')}`);
|
|
441
678
|
lines.push('');
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
lines.push(`### ${fp}`);
|
|
448
|
-
lines.push(`- Category: ${file.category}`);
|
|
449
|
-
lines.push(`- Size: ${file.size} bytes (~${file.tokens} tokens)`);
|
|
450
|
-
if (symbols.length > 0)
|
|
451
|
-
lines.push(`- Exports: \`${symbols.join('`, `')}\``);
|
|
452
|
-
if (file.classes.length > 0)
|
|
453
|
-
lines.push(`- Classes: \`${file.classes.join('`, `')}\``);
|
|
454
|
-
if (file.types.length > 0)
|
|
455
|
-
lines.push(`- Types: \`${file.types.join('`, `')}\``);
|
|
456
|
-
lines.push('');
|
|
457
|
-
}
|
|
679
|
+
// Dependencies
|
|
680
|
+
if (feature.crossFeatureDeps.length > 0) {
|
|
681
|
+
lines.push(`## Dependencies`);
|
|
682
|
+
lines.push(`- Imports from: ${feature.crossFeatureDeps.join(', ')}`);
|
|
683
|
+
lines.push('');
|
|
458
684
|
}
|
|
459
|
-
//
|
|
685
|
+
// Internal Data Flow
|
|
460
686
|
if (feature.imports.size > 0) {
|
|
461
|
-
lines.push(`## Data Flow`);
|
|
462
|
-
lines.push('Internal import relationships:');
|
|
463
|
-
lines.push('');
|
|
687
|
+
lines.push(`## Internal Data Flow`);
|
|
464
688
|
for (const [file, deps] of feature.imports) {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
lines.push('');
|
|
468
|
-
}
|
|
469
|
-
// Stores
|
|
470
|
-
if (feature.stores.length > 0) {
|
|
471
|
-
lines.push(`## State Management`);
|
|
472
|
-
for (const sp of feature.stores) {
|
|
473
|
-
const file = scan.files.find(f => f.path === sp);
|
|
474
|
-
if (file) {
|
|
475
|
-
lines.push(`### ${basename(sp)}`);
|
|
476
|
-
lines.push(`- Path: ${sp}`);
|
|
477
|
-
if (file.exports.length > 0)
|
|
478
|
-
lines.push(`- Actions/Selectors: \`${file.exports.join('`, `')}\``);
|
|
479
|
-
lines.push('');
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
// Components
|
|
484
|
-
if (feature.components.length > 0) {
|
|
485
|
-
lines.push(`## Components`);
|
|
486
|
-
for (const cp of feature.components.slice(0, 10)) {
|
|
487
|
-
const file = scan.files.find(f => f.path === cp);
|
|
488
|
-
if (file) {
|
|
489
|
-
lines.push(`- \`${cp}\` — exports: ${file.exports.slice(0, 3).map(e => `\`${e}\``).join(', ') || 'default'}`);
|
|
689
|
+
for (const dep of deps) {
|
|
690
|
+
lines.push(`${basename(file)} → ${basename(dep)}`);
|
|
490
691
|
}
|
|
491
692
|
}
|
|
492
693
|
lines.push('');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface CachedResponse {
|
|
2
|
+
id: string;
|
|
3
|
+
queryHash: string;
|
|
4
|
+
queryEmbedding: number[] | null;
|
|
5
|
+
query: string;
|
|
6
|
+
response: string;
|
|
7
|
+
model: string;
|
|
8
|
+
agentId: string;
|
|
9
|
+
systemPromptHash: string;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
hitCount: number;
|
|
12
|
+
ttl: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function checkCache(query: string, agentId: string, model: string, systemPromptHash: string, _ttlSeconds?: number): Promise<CachedResponse | null>;
|
|
15
|
+
export declare function storeResponse(query: string, response: string, agentId: string, model: string, systemPromptHash: string, ttlSeconds?: number): Promise<void>;
|
|
16
|
+
export declare function evictExpired(): Promise<void>;
|
|
17
|
+
export declare function evictLRU(maxEntries?: number): Promise<void>;
|
|
18
|
+
export declare function getCacheStats(): Promise<{
|
|
19
|
+
totalEntries: number;
|
|
20
|
+
hitRate: number;
|
|
21
|
+
estimatedSavings: number;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function purgeCache(agentId?: string): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=responseCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responseCache.d.ts","sourceRoot":"","sources":["../../../server/services/responseCache.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAwDD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,SAAO,GAC1F,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAgEhC;AAED,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAC/D,gBAAgB,EAAE,MAAM,EAAE,UAAU,SAAO,GAC1C,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAKlD;AAED,wBAAsB,QAAQ,CAAC,UAAU,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAa/D;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAC,CAUlH;AAED,wBAAsB,UAAU,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhE"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getDb, saveDb } from './sqliteStore.js';
|
|
3
|
+
import embeddingService from './embeddingService.js';
|
|
4
|
+
function toStr(v) { return typeof v === 'string' ? v : ''; }
|
|
5
|
+
function toNum(v) { return typeof v === 'number' ? v : 0; }
|
|
6
|
+
function normalizeQuery(q) {
|
|
7
|
+
return q.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
8
|
+
}
|
|
9
|
+
function sha256(text) {
|
|
10
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
11
|
+
}
|
|
12
|
+
function levenshtein(a, b) {
|
|
13
|
+
const prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
14
|
+
const curr = new Array(b.length + 1).fill(0);
|
|
15
|
+
for (let i = 1; i <= a.length; i++) {
|
|
16
|
+
curr[0] = i;
|
|
17
|
+
for (let j = 1; j <= b.length; j++) {
|
|
18
|
+
curr[j] = a[i - 1] === b[j - 1] ? prev[j - 1] : 1 + Math.min(prev[j], curr[j - 1], prev[j - 1]);
|
|
19
|
+
}
|
|
20
|
+
prev.splice(0, b.length + 1, ...curr);
|
|
21
|
+
}
|
|
22
|
+
return prev[b.length];
|
|
23
|
+
}
|
|
24
|
+
function blobToVec(v) {
|
|
25
|
+
if (!(v instanceof Uint8Array))
|
|
26
|
+
return null;
|
|
27
|
+
return Array.from(new Float32Array(v.buffer, v.byteOffset, v.byteLength / 4));
|
|
28
|
+
}
|
|
29
|
+
function rowToCache(row) {
|
|
30
|
+
return {
|
|
31
|
+
id: toStr(row[0]),
|
|
32
|
+
queryHash: toStr(row[1]),
|
|
33
|
+
queryEmbedding: blobToVec(row[2]),
|
|
34
|
+
query: toStr(row[3]),
|
|
35
|
+
response: toStr(row[4]),
|
|
36
|
+
model: toStr(row[5]),
|
|
37
|
+
agentId: toStr(row[6]),
|
|
38
|
+
systemPromptHash: toStr(row[7]),
|
|
39
|
+
createdAt: toNum(row[8]),
|
|
40
|
+
hitCount: toNum(row[9]),
|
|
41
|
+
ttl: toNum(row[10]),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function markHit(id) {
|
|
45
|
+
const d = await getDb();
|
|
46
|
+
const now = Math.floor(Date.now() / 1000);
|
|
47
|
+
d.run(`UPDATE response_cache SET hit_count = hit_count + 1, last_hit_at = ? WHERE id = ?`, [now, id]);
|
|
48
|
+
saveDb();
|
|
49
|
+
}
|
|
50
|
+
export async function checkCache(query, agentId, model, systemPromptHash, _ttlSeconds = 3600) {
|
|
51
|
+
const d = await getDb();
|
|
52
|
+
const now = Math.floor(Date.now() / 1000);
|
|
53
|
+
const norm = normalizeQuery(query);
|
|
54
|
+
const qHash = sha256(norm + agentId + model + systemPromptHash);
|
|
55
|
+
// Tier 1: exact hash match
|
|
56
|
+
const exact = d.exec(`SELECT id,query_hash,query_embedding,query,response,model,agent_id,system_prompt_hash,created_at,hit_count,ttl
|
|
57
|
+
FROM response_cache WHERE query_hash=? AND agent_id=? AND model=? AND system_prompt_hash=?
|
|
58
|
+
AND (ttl=0 OR created_at+ttl>?) LIMIT 1`, [qHash, agentId, model, systemPromptHash, now]);
|
|
59
|
+
if (exact.length > 0 && exact[0].values.length > 0) {
|
|
60
|
+
const hit = rowToCache(exact[0].values[0]);
|
|
61
|
+
await markHit(hit.id);
|
|
62
|
+
return hit;
|
|
63
|
+
}
|
|
64
|
+
// Tier 2: Levenshtein distance < 10% of query length
|
|
65
|
+
const threshold = Math.max(3, Math.floor(norm.length * 0.1));
|
|
66
|
+
const recent = d.exec(`SELECT id,query_hash,query_embedding,query,response,model,agent_id,system_prompt_hash,created_at,hit_count,ttl
|
|
67
|
+
FROM response_cache WHERE agent_id=? AND model=? AND system_prompt_hash=? AND (ttl=0 OR created_at+ttl>?)
|
|
68
|
+
ORDER BY created_at DESC LIMIT 200`, [agentId, model, systemPromptHash, now]);
|
|
69
|
+
if (recent.length > 0) {
|
|
70
|
+
for (const row of recent[0].values) {
|
|
71
|
+
if (levenshtein(norm, normalizeQuery(toStr(row[3]))) <= threshold) {
|
|
72
|
+
const hit = rowToCache(row);
|
|
73
|
+
await markHit(hit.id);
|
|
74
|
+
return hit;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Tier 3: cosine similarity > 0.92 using embeddings
|
|
79
|
+
try {
|
|
80
|
+
const [queryVec] = await embeddingService.embedBatch([norm]);
|
|
81
|
+
const withEmb = d.exec(`SELECT id,query_hash,query_embedding,query,response,model,agent_id,system_prompt_hash,created_at,hit_count,ttl
|
|
82
|
+
FROM response_cache WHERE agent_id=? AND model=? AND system_prompt_hash=?
|
|
83
|
+
AND query_embedding IS NOT NULL AND (ttl=0 OR created_at+ttl>?) ORDER BY created_at DESC LIMIT 500`, [agentId, model, systemPromptHash, now]);
|
|
84
|
+
if (withEmb.length > 0) {
|
|
85
|
+
let bestScore = 0;
|
|
86
|
+
let bestRow = null;
|
|
87
|
+
for (const row of withEmb[0].values) {
|
|
88
|
+
const vec = blobToVec(row[2]);
|
|
89
|
+
if (!vec)
|
|
90
|
+
continue;
|
|
91
|
+
const score = embeddingService.similarity(queryVec, vec);
|
|
92
|
+
if (score > bestScore) {
|
|
93
|
+
bestScore = score;
|
|
94
|
+
bestRow = row;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (bestScore > 0.92 && bestRow) {
|
|
98
|
+
const hit = rowToCache(bestRow);
|
|
99
|
+
await markHit(hit.id);
|
|
100
|
+
return hit;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* embedding service unavailable — skip tier 3 */ }
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
export async function storeResponse(query, response, agentId, model, systemPromptHash, ttlSeconds = 3600) {
|
|
108
|
+
const d = await getDb();
|
|
109
|
+
const norm = normalizeQuery(query);
|
|
110
|
+
const qHash = sha256(norm + agentId + model + systemPromptHash);
|
|
111
|
+
const id = `rc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
112
|
+
const now = Math.floor(Date.now() / 1000);
|
|
113
|
+
let embeddingBlob = null;
|
|
114
|
+
try {
|
|
115
|
+
const [vec] = await embeddingService.embedBatch([norm]);
|
|
116
|
+
embeddingBlob = Buffer.from(new Float32Array(vec).buffer);
|
|
117
|
+
}
|
|
118
|
+
catch { /* skip embedding if unavailable */ }
|
|
119
|
+
d.run(`INSERT OR REPLACE INTO response_cache
|
|
120
|
+
(id,query_hash,query_embedding,query,response,model,agent_id,system_prompt_hash,created_at,hit_count,last_hit_at,ttl)
|
|
121
|
+
VALUES (?,?,?,?,?,?,?,?,?,0,?,?)`, [id, qHash, embeddingBlob, norm, response, model, agentId, systemPromptHash, now, now, ttlSeconds]);
|
|
122
|
+
saveDb();
|
|
123
|
+
}
|
|
124
|
+
export async function evictExpired() {
|
|
125
|
+
const d = await getDb();
|
|
126
|
+
const now = Math.floor(Date.now() / 1000);
|
|
127
|
+
d.run(`DELETE FROM response_cache WHERE ttl > 0 AND created_at + ttl <= ?`, [now]);
|
|
128
|
+
saveDb();
|
|
129
|
+
}
|
|
130
|
+
export async function evictLRU(maxEntries = 1000) {
|
|
131
|
+
const d = await getDb();
|
|
132
|
+
const agents = d.exec(`SELECT DISTINCT agent_id FROM response_cache`);
|
|
133
|
+
if (agents.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
for (const row of agents[0].values) {
|
|
136
|
+
const aid = toStr(row[0]);
|
|
137
|
+
d.run(`DELETE FROM response_cache WHERE agent_id=? AND id NOT IN
|
|
138
|
+
(SELECT id FROM response_cache WHERE agent_id=? ORDER BY last_hit_at DESC LIMIT ?)`, [aid, aid, maxEntries]);
|
|
139
|
+
}
|
|
140
|
+
saveDb();
|
|
141
|
+
}
|
|
142
|
+
export async function getCacheStats() {
|
|
143
|
+
const d = await getDb();
|
|
144
|
+
const r = d.exec(`SELECT COUNT(*), COALESCE(SUM(hit_count),0), COALESCE(SUM(CAST(LENGTH(response) AS REAL)/4*hit_count),0) FROM response_cache`);
|
|
145
|
+
if (r.length === 0 || r[0].values.length === 0)
|
|
146
|
+
return { totalEntries: 0, hitRate: 0, estimatedSavings: 0 };
|
|
147
|
+
const row = r[0].values[0];
|
|
148
|
+
const totalEntries = toNum(row[0]);
|
|
149
|
+
const totalHits = toNum(row[1]);
|
|
150
|
+
const estimatedSavings = toNum(row[2]) * 0.000015;
|
|
151
|
+
const hitRate = totalHits + totalEntries > 0 ? totalHits / (totalHits + totalEntries) : 0;
|
|
152
|
+
return { totalEntries, hitRate, estimatedSavings };
|
|
153
|
+
}
|
|
154
|
+
export async function purgeCache(agentId) {
|
|
155
|
+
const d = await getDb();
|
|
156
|
+
if (agentId) {
|
|
157
|
+
d.run(`DELETE FROM response_cache WHERE agent_id = ?`, [agentId]);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
d.run(`DELETE FROM response_cache`);
|
|
161
|
+
}
|
|
162
|
+
saveDb();
|
|
163
|
+
}
|