claude-code-workflow 6.2.2 → 6.2.4
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/ccw/dist/cli.d.ts +2 -0
- package/ccw/dist/cli.d.ts.map +1 -0
- package/ccw/dist/cli.js +219 -0
- package/ccw/dist/cli.js.map +1 -0
- package/ccw/dist/commands/cli.d.ts +32 -0
- package/ccw/dist/commands/cli.d.ts.map +1 -0
- package/ccw/dist/commands/cli.js +619 -0
- package/ccw/dist/commands/cli.js.map +1 -0
- package/ccw/dist/commands/core-memory.d.ts +32 -0
- package/ccw/dist/commands/core-memory.d.ts.map +1 -0
- package/ccw/dist/commands/core-memory.js +640 -0
- package/ccw/dist/commands/core-memory.js.map +1 -0
- package/ccw/dist/commands/hook.d.ts +16 -0
- package/ccw/dist/commands/hook.d.ts.map +1 -0
- package/ccw/dist/commands/hook.js +276 -0
- package/ccw/dist/commands/hook.js.map +1 -0
- package/ccw/dist/commands/install.d.ts +12 -0
- package/ccw/dist/commands/install.d.ts.map +1 -0
- package/ccw/dist/commands/install.js +443 -0
- package/ccw/dist/commands/install.js.map +1 -0
- package/ccw/dist/commands/list.d.ts +5 -0
- package/ccw/dist/commands/list.d.ts.map +1 -0
- package/ccw/dist/commands/list.js +32 -0
- package/ccw/dist/commands/list.js.map +1 -0
- package/ccw/dist/commands/memory.d.ts +57 -0
- package/ccw/dist/commands/memory.d.ts.map +1 -0
- package/ccw/dist/commands/memory.js +890 -0
- package/ccw/dist/commands/memory.js.map +1 -0
- package/ccw/dist/commands/serve.d.ts +12 -0
- package/ccw/dist/commands/serve.d.ts.map +1 -0
- package/ccw/dist/commands/serve.js +63 -0
- package/ccw/dist/commands/serve.js.map +1 -0
- package/ccw/dist/commands/session-path-resolver.d.ts +45 -0
- package/ccw/dist/commands/session-path-resolver.d.ts.map +1 -0
- package/ccw/dist/commands/session-path-resolver.js +302 -0
- package/ccw/dist/commands/session-path-resolver.js.map +1 -0
- package/ccw/dist/commands/session.d.ts +12 -0
- package/ccw/dist/commands/session.d.ts.map +1 -0
- package/ccw/dist/commands/session.js +954 -0
- package/ccw/dist/commands/session.js.map +1 -0
- package/ccw/dist/commands/stop.d.ts +11 -0
- package/ccw/dist/commands/stop.d.ts.map +1 -0
- package/ccw/dist/commands/stop.js +96 -0
- package/ccw/dist/commands/stop.js.map +1 -0
- package/ccw/dist/commands/tool.d.ts +29 -0
- package/ccw/dist/commands/tool.d.ts.map +1 -0
- package/ccw/dist/commands/tool.js +173 -0
- package/ccw/dist/commands/tool.js.map +1 -0
- package/ccw/dist/commands/uninstall.d.ts +9 -0
- package/ccw/dist/commands/uninstall.d.ts.map +1 -0
- package/ccw/dist/commands/uninstall.js +239 -0
- package/ccw/dist/commands/uninstall.js.map +1 -0
- package/ccw/dist/commands/upgrade.d.ts +10 -0
- package/ccw/dist/commands/upgrade.d.ts.map +1 -0
- package/ccw/dist/commands/upgrade.js +288 -0
- package/ccw/dist/commands/upgrade.js.map +1 -0
- package/ccw/dist/commands/view.d.ts +14 -0
- package/ccw/dist/commands/view.d.ts.map +1 -0
- package/ccw/dist/commands/view.js +100 -0
- package/ccw/dist/commands/view.js.map +1 -0
- package/ccw/dist/config/storage-paths.d.ts +184 -0
- package/ccw/dist/config/storage-paths.d.ts.map +1 -0
- package/ccw/dist/config/storage-paths.js +536 -0
- package/ccw/dist/config/storage-paths.js.map +1 -0
- package/ccw/dist/core/cache-manager.d.ts +80 -0
- package/ccw/dist/core/cache-manager.d.ts.map +1 -0
- package/ccw/dist/core/cache-manager.js +260 -0
- package/ccw/dist/core/cache-manager.js.map +1 -0
- package/ccw/dist/core/claude-freshness.d.ts +53 -0
- package/ccw/dist/core/claude-freshness.d.ts.map +1 -0
- package/ccw/dist/core/claude-freshness.js +232 -0
- package/ccw/dist/core/claude-freshness.js.map +1 -0
- package/ccw/dist/core/core-memory-store.d.ts +320 -0
- package/ccw/dist/core/core-memory-store.d.ts.map +1 -0
- package/ccw/dist/core/core-memory-store.js +1177 -0
- package/ccw/dist/core/core-memory-store.js.map +1 -0
- package/ccw/dist/core/dashboard-generator-patch.d.ts +2 -0
- package/ccw/dist/core/dashboard-generator-patch.d.ts.map +1 -0
- package/ccw/dist/core/dashboard-generator-patch.js +48 -0
- package/ccw/dist/core/dashboard-generator-patch.js.map +1 -0
- package/ccw/dist/core/dashboard-generator.d.ts +8 -0
- package/ccw/dist/core/dashboard-generator.d.ts.map +1 -0
- package/ccw/dist/core/dashboard-generator.js +695 -0
- package/ccw/dist/core/dashboard-generator.js.map +1 -0
- package/ccw/dist/core/data-aggregator.d.ts +145 -0
- package/ccw/dist/core/data-aggregator.d.ts.map +1 -0
- package/ccw/dist/core/data-aggregator.js +416 -0
- package/ccw/dist/core/data-aggregator.js.map +1 -0
- package/ccw/dist/core/history-importer.d.ts +102 -0
- package/ccw/dist/core/history-importer.d.ts.map +1 -0
- package/ccw/dist/core/history-importer.js +493 -0
- package/ccw/dist/core/history-importer.js.map +1 -0
- package/ccw/dist/core/lite-scanner-complete.d.ts +81 -0
- package/ccw/dist/core/lite-scanner-complete.d.ts.map +1 -0
- package/ccw/dist/core/lite-scanner-complete.js +368 -0
- package/ccw/dist/core/lite-scanner-complete.js.map +1 -0
- package/ccw/dist/core/lite-scanner.d.ts +81 -0
- package/ccw/dist/core/lite-scanner.d.ts.map +1 -0
- package/ccw/dist/core/lite-scanner.js +368 -0
- package/ccw/dist/core/lite-scanner.js.map +1 -0
- package/ccw/dist/core/manifest.d.ts +88 -0
- package/ccw/dist/core/manifest.d.ts.map +1 -0
- package/ccw/dist/core/manifest.js +214 -0
- package/ccw/dist/core/manifest.js.map +1 -0
- package/ccw/dist/core/memory-embedder-bridge.d.ts +83 -0
- package/ccw/dist/core/memory-embedder-bridge.d.ts.map +1 -0
- package/ccw/dist/core/memory-embedder-bridge.js +181 -0
- package/ccw/dist/core/memory-embedder-bridge.js.map +1 -0
- package/ccw/dist/core/memory-store.d.ts +249 -0
- package/ccw/dist/core/memory-store.d.ts.map +1 -0
- package/ccw/dist/core/memory-store.js +781 -0
- package/ccw/dist/core/memory-store.js.map +1 -0
- package/ccw/dist/core/routes/ccw-routes.d.ts +20 -0
- package/ccw/dist/core/routes/ccw-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/ccw-routes.js +70 -0
- package/ccw/dist/core/routes/ccw-routes.js.map +1 -0
- package/ccw/dist/core/routes/claude-routes.d.ts +19 -0
- package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/claude-routes.js +1017 -0
- package/ccw/dist/core/routes/claude-routes.js.map +1 -0
- package/ccw/dist/core/routes/cli-routes.d.ts +20 -0
- package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/cli-routes.js +468 -0
- package/ccw/dist/core/routes/cli-routes.js.map +1 -0
- package/ccw/dist/core/routes/codexlens-routes.d.ts +20 -0
- package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/codexlens-routes.js +754 -0
- package/ccw/dist/core/routes/codexlens-routes.js.map +1 -0
- package/ccw/dist/core/routes/core-memory-routes.d.ts +21 -0
- package/ccw/dist/core/routes/core-memory-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/core-memory-routes.js +520 -0
- package/ccw/dist/core/routes/core-memory-routes.js.map +1 -0
- package/ccw/dist/core/routes/files-routes.d.ts +20 -0
- package/ccw/dist/core/routes/files-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/files-routes.js +374 -0
- package/ccw/dist/core/routes/files-routes.js.map +1 -0
- package/ccw/dist/core/routes/graph-routes.d.ts +20 -0
- package/ccw/dist/core/routes/graph-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/graph-routes.js +517 -0
- package/ccw/dist/core/routes/graph-routes.js.map +1 -0
- package/ccw/dist/core/routes/help-routes.d.ts +20 -0
- package/ccw/dist/core/routes/help-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/help-routes.js +250 -0
- package/ccw/dist/core/routes/help-routes.js.map +1 -0
- package/ccw/dist/core/routes/hooks-routes.d.ts +21 -0
- package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/hooks-routes.js +346 -0
- package/ccw/dist/core/routes/hooks-routes.js.map +1 -0
- package/ccw/dist/core/routes/mcp-routes.d.ts +20 -0
- package/ccw/dist/core/routes/mcp-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/mcp-routes.js +1129 -0
- package/ccw/dist/core/routes/mcp-routes.js.map +1 -0
- package/ccw/dist/core/routes/mcp-templates-db.d.ts +54 -0
- package/ccw/dist/core/routes/mcp-templates-db.d.ts.map +1 -0
- package/ccw/dist/core/routes/mcp-templates-db.js +226 -0
- package/ccw/dist/core/routes/mcp-templates-db.js.map +1 -0
- package/ccw/dist/core/routes/memory-routes.d.ts +21 -0
- package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/memory-routes.js +1095 -0
- package/ccw/dist/core/routes/memory-routes.js.map +1 -0
- package/ccw/dist/core/routes/rules-routes.d.ts +20 -0
- package/ccw/dist/core/routes/rules-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/rules-routes.js +442 -0
- package/ccw/dist/core/routes/rules-routes.js.map +1 -0
- package/ccw/dist/core/routes/session-routes.d.ts +20 -0
- package/ccw/dist/core/routes/session-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/session-routes.js +423 -0
- package/ccw/dist/core/routes/session-routes.js.map +1 -0
- package/ccw/dist/core/routes/skills-routes.d.ts +20 -0
- package/ccw/dist/core/routes/skills-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/skills-routes.js +533 -0
- package/ccw/dist/core/routes/skills-routes.js.map +1 -0
- package/ccw/dist/core/routes/status-routes.d.ts +20 -0
- package/ccw/dist/core/routes/status-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/status-routes.js +38 -0
- package/ccw/dist/core/routes/status-routes.js.map +1 -0
- package/ccw/dist/core/routes/system-routes.d.ts +22 -0
- package/ccw/dist/core/routes/system-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/system-routes.js +354 -0
- package/ccw/dist/core/routes/system-routes.js.map +1 -0
- package/ccw/dist/core/server.d.ts +17 -0
- package/ccw/dist/core/server.d.ts.map +1 -0
- package/ccw/dist/core/server.js +386 -0
- package/ccw/dist/core/server.js.map +1 -0
- package/ccw/dist/core/session-clustering-service.d.ts +153 -0
- package/ccw/dist/core/session-clustering-service.d.ts.map +1 -0
- package/ccw/dist/core/session-clustering-service.js +1065 -0
- package/ccw/dist/core/session-clustering-service.js.map +1 -0
- package/ccw/dist/core/session-scanner.d.ts +32 -0
- package/ccw/dist/core/session-scanner.d.ts.map +1 -0
- package/ccw/dist/core/session-scanner.js +253 -0
- package/ccw/dist/core/session-scanner.js.map +1 -0
- package/ccw/dist/core/websocket.d.ts +23 -0
- package/ccw/dist/core/websocket.d.ts.map +1 -0
- package/ccw/dist/core/websocket.js +168 -0
- package/ccw/dist/core/websocket.js.map +1 -0
- package/ccw/dist/index.d.ts +10 -0
- package/ccw/dist/index.d.ts.map +1 -0
- package/ccw/dist/index.js +10 -0
- package/ccw/dist/index.js.map +1 -0
- package/ccw/dist/mcp-server/index.d.ts +7 -0
- package/ccw/dist/mcp-server/index.d.ts.map +1 -0
- package/ccw/dist/mcp-server/index.js +157 -0
- package/ccw/dist/mcp-server/index.js.map +1 -0
- package/ccw/dist/tools/classify-folders.d.ts +26 -0
- package/ccw/dist/tools/classify-folders.d.ts.map +1 -0
- package/ccw/dist/tools/classify-folders.js +201 -0
- package/ccw/dist/tools/classify-folders.js.map +1 -0
- package/ccw/dist/tools/cli-config-manager.d.ts +62 -0
- package/ccw/dist/tools/cli-config-manager.d.ts.map +1 -0
- package/ccw/dist/tools/cli-config-manager.js +221 -0
- package/ccw/dist/tools/cli-config-manager.js.map +1 -0
- package/ccw/dist/tools/cli-executor.d.ts +373 -0
- package/ccw/dist/tools/cli-executor.d.ts.map +1 -0
- package/ccw/dist/tools/cli-executor.js +1625 -0
- package/ccw/dist/tools/cli-executor.js.map +1 -0
- package/ccw/dist/tools/cli-history-store.d.ts +330 -0
- package/ccw/dist/tools/cli-history-store.d.ts.map +1 -0
- package/ccw/dist/tools/cli-history-store.js +916 -0
- package/ccw/dist/tools/cli-history-store.js.map +1 -0
- package/ccw/dist/tools/codex-lens.d.ts +118 -0
- package/ccw/dist/tools/codex-lens.d.ts.map +1 -0
- package/ccw/dist/tools/codex-lens.js +962 -0
- package/ccw/dist/tools/codex-lens.js.map +1 -0
- package/ccw/dist/tools/convert-tokens-to-css.d.ts +14 -0
- package/ccw/dist/tools/convert-tokens-to-css.d.ts.map +1 -0
- package/ccw/dist/tools/convert-tokens-to-css.js +244 -0
- package/ccw/dist/tools/convert-tokens-to-css.js.map +1 -0
- package/ccw/dist/tools/core-memory.d.ts +66 -0
- package/ccw/dist/tools/core-memory.d.ts.map +1 -0
- package/ccw/dist/tools/core-memory.js +324 -0
- package/ccw/dist/tools/core-memory.js.map +1 -0
- package/ccw/dist/tools/detect-changed-modules.d.ts +24 -0
- package/ccw/dist/tools/detect-changed-modules.d.ts.map +1 -0
- package/ccw/dist/tools/detect-changed-modules.js +277 -0
- package/ccw/dist/tools/detect-changed-modules.js.map +1 -0
- package/ccw/dist/tools/discover-design-files.d.ts +36 -0
- package/ccw/dist/tools/discover-design-files.d.ts.map +1 -0
- package/ccw/dist/tools/discover-design-files.js +147 -0
- package/ccw/dist/tools/discover-design-files.js.map +1 -0
- package/ccw/dist/tools/edit-file.d.ts +28 -0
- package/ccw/dist/tools/edit-file.d.ts.map +1 -0
- package/ccw/dist/tools/edit-file.js +479 -0
- package/ccw/dist/tools/edit-file.js.map +1 -0
- package/ccw/dist/tools/generate-module-docs.d.ts +22 -0
- package/ccw/dist/tools/generate-module-docs.d.ts.map +1 -0
- package/ccw/dist/tools/generate-module-docs.js +379 -0
- package/ccw/dist/tools/generate-module-docs.js.map +1 -0
- package/ccw/dist/tools/get-modules-by-depth.d.ts +15 -0
- package/ccw/dist/tools/get-modules-by-depth.d.ts.map +1 -0
- package/ccw/dist/tools/get-modules-by-depth.js +296 -0
- package/ccw/dist/tools/get-modules-by-depth.js.map +1 -0
- package/ccw/dist/tools/index.d.ts +55 -0
- package/ccw/dist/tools/index.d.ts.map +1 -0
- package/ccw/dist/tools/index.js +304 -0
- package/ccw/dist/tools/index.js.map +1 -0
- package/ccw/dist/tools/native-session-discovery.d.ts +97 -0
- package/ccw/dist/tools/native-session-discovery.d.ts.map +1 -0
- package/ccw/dist/tools/native-session-discovery.js +700 -0
- package/ccw/dist/tools/native-session-discovery.js.map +1 -0
- package/ccw/dist/tools/notifier.d.ts +50 -0
- package/ccw/dist/tools/notifier.d.ts.map +1 -0
- package/ccw/dist/tools/notifier.js +90 -0
- package/ccw/dist/tools/notifier.js.map +1 -0
- package/ccw/dist/tools/read-file.d.ts +32 -0
- package/ccw/dist/tools/read-file.d.ts.map +1 -0
- package/ccw/dist/tools/read-file.js +329 -0
- package/ccw/dist/tools/read-file.js.map +1 -0
- package/ccw/dist/tools/resume-strategy.d.ts +48 -0
- package/ccw/dist/tools/resume-strategy.d.ts.map +1 -0
- package/ccw/dist/tools/resume-strategy.js +248 -0
- package/ccw/dist/tools/resume-strategy.js.map +1 -0
- package/ccw/dist/tools/session-content-parser.d.ts +58 -0
- package/ccw/dist/tools/session-content-parser.d.ts.map +1 -0
- package/ccw/dist/tools/session-content-parser.js +420 -0
- package/ccw/dist/tools/session-content-parser.js.map +1 -0
- package/ccw/dist/tools/session-manager.d.ts +9 -0
- package/ccw/dist/tools/session-manager.d.ts.map +1 -0
- package/ccw/dist/tools/session-manager.js +834 -0
- package/ccw/dist/tools/session-manager.js.map +1 -0
- package/ccw/dist/tools/smart-context.d.ts +35 -0
- package/ccw/dist/tools/smart-context.d.ts.map +1 -0
- package/ccw/dist/tools/smart-context.js +182 -0
- package/ccw/dist/tools/smart-context.js.map +1 -0
- package/ccw/dist/tools/smart-search.d.ts +105 -0
- package/ccw/dist/tools/smart-search.d.ts.map +1 -0
- package/ccw/dist/tools/smart-search.js +1753 -0
- package/ccw/dist/tools/smart-search.js.map +1 -0
- package/ccw/dist/tools/storage-manager.d.ts +114 -0
- package/ccw/dist/tools/storage-manager.d.ts.map +1 -0
- package/ccw/dist/tools/storage-manager.js +392 -0
- package/ccw/dist/tools/storage-manager.js.map +1 -0
- package/ccw/dist/tools/ui-generate-preview.d.ts +39 -0
- package/ccw/dist/tools/ui-generate-preview.d.ts.map +1 -0
- package/ccw/dist/tools/ui-generate-preview.js +300 -0
- package/ccw/dist/tools/ui-generate-preview.js.map +1 -0
- package/ccw/dist/tools/ui-instantiate-prototypes.d.ts +75 -0
- package/ccw/dist/tools/ui-instantiate-prototypes.d.ts.map +1 -0
- package/ccw/dist/tools/ui-instantiate-prototypes.js +256 -0
- package/ccw/dist/tools/ui-instantiate-prototypes.js.map +1 -0
- package/ccw/dist/tools/update-module-claude.d.ts +80 -0
- package/ccw/dist/tools/update-module-claude.d.ts.map +1 -0
- package/ccw/dist/tools/update-module-claude.js +351 -0
- package/ccw/dist/tools/update-module-claude.js.map +1 -0
- package/ccw/dist/tools/write-file.d.ts +19 -0
- package/ccw/dist/tools/write-file.d.ts.map +1 -0
- package/ccw/dist/tools/write-file.js +193 -0
- package/ccw/dist/tools/write-file.js.map +1 -0
- package/ccw/dist/types/config.d.ts +11 -0
- package/ccw/dist/types/config.d.ts.map +1 -0
- package/ccw/dist/types/config.js +2 -0
- package/ccw/dist/types/config.js.map +1 -0
- package/ccw/dist/types/index.d.ts +4 -0
- package/ccw/dist/types/index.d.ts.map +1 -0
- package/ccw/dist/types/index.js +4 -0
- package/ccw/dist/types/index.js.map +1 -0
- package/ccw/dist/types/session.d.ts +20 -0
- package/ccw/dist/types/session.d.ts.map +1 -0
- package/ccw/dist/types/session.js +2 -0
- package/ccw/dist/types/session.js.map +1 -0
- package/ccw/dist/types/tool.d.ts +36 -0
- package/ccw/dist/types/tool.d.ts.map +1 -0
- package/ccw/dist/types/tool.js +11 -0
- package/ccw/dist/types/tool.js.map +1 -0
- package/ccw/dist/utils/browser-launcher.d.ts +13 -0
- package/ccw/dist/utils/browser-launcher.d.ts.map +1 -0
- package/ccw/dist/utils/browser-launcher.js +60 -0
- package/ccw/dist/utils/browser-launcher.js.map +1 -0
- package/ccw/dist/utils/file-utils.d.ts +25 -0
- package/ccw/dist/utils/file-utils.d.ts.map +1 -0
- package/ccw/dist/utils/file-utils.js +48 -0
- package/ccw/dist/utils/file-utils.js.map +1 -0
- package/ccw/dist/utils/path-resolver.d.ts +80 -0
- package/ccw/dist/utils/path-resolver.d.ts.map +1 -0
- package/ccw/dist/utils/path-resolver.js +260 -0
- package/ccw/dist/utils/path-resolver.js.map +1 -0
- package/ccw/dist/utils/path-validator.d.ts +49 -0
- package/ccw/dist/utils/path-validator.d.ts.map +1 -0
- package/ccw/dist/utils/path-validator.js +123 -0
- package/ccw/dist/utils/path-validator.js.map +1 -0
- package/ccw/dist/utils/ui.d.ts +62 -0
- package/ccw/dist/utils/ui.d.ts.map +1 -0
- package/ccw/dist/utils/ui.js +129 -0
- package/ccw/dist/utils/ui.js.map +1 -0
- package/ccw/package.json +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Clustering Service
|
|
3
|
+
* Intelligently groups related sessions into clusters using multi-dimensional similarity analysis
|
|
4
|
+
*/
|
|
5
|
+
import { CoreMemoryStore } from './core-memory-store.js';
|
|
6
|
+
import { CliHistoryStore } from '../tools/cli-history-store.js';
|
|
7
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
// Clustering dimension weights
|
|
10
|
+
const WEIGHTS = {
|
|
11
|
+
fileOverlap: 0.2,
|
|
12
|
+
temporalProximity: 0.15,
|
|
13
|
+
keywordSimilarity: 0.15,
|
|
14
|
+
vectorSimilarity: 0.3,
|
|
15
|
+
intentAlignment: 0.2,
|
|
16
|
+
};
|
|
17
|
+
// Clustering threshold (0.4 = moderate similarity required)
|
|
18
|
+
const CLUSTER_THRESHOLD = 0.4;
|
|
19
|
+
export class SessionClusteringService {
|
|
20
|
+
coreMemoryStore;
|
|
21
|
+
cliHistoryStore;
|
|
22
|
+
projectPath;
|
|
23
|
+
constructor(projectPath) {
|
|
24
|
+
this.projectPath = projectPath;
|
|
25
|
+
this.coreMemoryStore = new CoreMemoryStore(projectPath);
|
|
26
|
+
this.cliHistoryStore = new CliHistoryStore(projectPath);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Collect all session sources
|
|
30
|
+
*/
|
|
31
|
+
async collectSessions(options) {
|
|
32
|
+
const sessions = [];
|
|
33
|
+
// 1. Core Memories
|
|
34
|
+
const memories = this.coreMemoryStore.getMemories({ archived: false, limit: 1000 });
|
|
35
|
+
for (const memory of memories) {
|
|
36
|
+
const cached = this.coreMemoryStore.getSessionMetadata(memory.id);
|
|
37
|
+
if (cached) {
|
|
38
|
+
sessions.push(cached);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const metadata = this.extractMetadata(memory, 'core_memory');
|
|
42
|
+
sessions.push(metadata);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// 2. CLI History
|
|
46
|
+
const history = this.cliHistoryStore.getHistory({ limit: 1000 });
|
|
47
|
+
for (const exec of history.executions) {
|
|
48
|
+
const cached = this.coreMemoryStore.getSessionMetadata(exec.id);
|
|
49
|
+
if (cached) {
|
|
50
|
+
sessions.push(cached);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const conversation = this.cliHistoryStore.getConversation(exec.id);
|
|
54
|
+
if (conversation) {
|
|
55
|
+
const metadata = this.extractMetadata(conversation, 'cli_history');
|
|
56
|
+
sessions.push(metadata);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 3. Workflow Sessions (WFS-*)
|
|
61
|
+
const workflowSessions = await this.parseWorkflowSessions();
|
|
62
|
+
sessions.push(...workflowSessions);
|
|
63
|
+
// Apply scope filter
|
|
64
|
+
if (options?.scope === 'recent') {
|
|
65
|
+
// Last 30 days
|
|
66
|
+
const cutoff = new Date();
|
|
67
|
+
cutoff.setDate(cutoff.getDate() - 30);
|
|
68
|
+
const cutoffStr = cutoff.toISOString();
|
|
69
|
+
return sessions.filter(s => (s.created_at || '') >= cutoffStr);
|
|
70
|
+
}
|
|
71
|
+
else if (options?.scope === 'unclustered') {
|
|
72
|
+
// Only sessions not in any cluster
|
|
73
|
+
return sessions.filter(s => {
|
|
74
|
+
const clusters = this.coreMemoryStore.getSessionClusters(s.session_id);
|
|
75
|
+
return clusters.length === 0;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return sessions;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract metadata from a session
|
|
82
|
+
*/
|
|
83
|
+
extractMetadata(session, type) {
|
|
84
|
+
let content = '';
|
|
85
|
+
let title = '';
|
|
86
|
+
let created_at = '';
|
|
87
|
+
if (type === 'core_memory') {
|
|
88
|
+
content = session.content || '';
|
|
89
|
+
created_at = session.created_at;
|
|
90
|
+
// Extract title from first line
|
|
91
|
+
const lines = content.split('\n');
|
|
92
|
+
title = lines[0].replace(/^#+\s*/, '').trim().substring(0, 100);
|
|
93
|
+
}
|
|
94
|
+
else if (type === 'cli_history') {
|
|
95
|
+
// Extract from conversation turns
|
|
96
|
+
const turns = session.turns || [];
|
|
97
|
+
if (turns.length > 0) {
|
|
98
|
+
content = turns.map((t) => t.prompt).join('\n');
|
|
99
|
+
title = turns[0].prompt.substring(0, 100);
|
|
100
|
+
created_at = session.created_at || turns[0].timestamp;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (type === 'workflow') {
|
|
104
|
+
content = session.content || '';
|
|
105
|
+
title = session.title || 'Workflow Session';
|
|
106
|
+
created_at = session.created_at || '';
|
|
107
|
+
}
|
|
108
|
+
const summary = content.substring(0, 200).trim();
|
|
109
|
+
const keywords = this.extractKeywords(content);
|
|
110
|
+
const file_patterns = this.extractFilePatterns(content);
|
|
111
|
+
const token_estimate = Math.ceil(content.length / 4);
|
|
112
|
+
return {
|
|
113
|
+
session_id: session.id,
|
|
114
|
+
session_type: type,
|
|
115
|
+
title,
|
|
116
|
+
summary,
|
|
117
|
+
keywords,
|
|
118
|
+
token_estimate,
|
|
119
|
+
file_patterns,
|
|
120
|
+
created_at,
|
|
121
|
+
last_accessed: new Date().toISOString(),
|
|
122
|
+
access_count: 0
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extract keywords from content
|
|
127
|
+
*/
|
|
128
|
+
extractKeywords(content) {
|
|
129
|
+
const keywords = new Set();
|
|
130
|
+
// 1. File paths (src/xxx, .ts, .js, etc)
|
|
131
|
+
const filePathRegex = /(?:^|\s|["'`])((?:\.\/|\.\.\/|\/)?[\w-]+(?:\/[\w-]+)*\.[\w]+)(?:\s|["'`]|$)/g;
|
|
132
|
+
let match;
|
|
133
|
+
while ((match = filePathRegex.exec(content)) !== null) {
|
|
134
|
+
keywords.add(match[1]);
|
|
135
|
+
}
|
|
136
|
+
// 2. Function/Class names (camelCase, PascalCase)
|
|
137
|
+
const camelCaseRegex = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+|[a-z]+[A-Z][a-z]+(?:[A-Z][a-z]+)*)\b/g;
|
|
138
|
+
while ((match = camelCaseRegex.exec(content)) !== null) {
|
|
139
|
+
keywords.add(match[1]);
|
|
140
|
+
}
|
|
141
|
+
// 3. Technical terms (common frameworks/libraries/concepts)
|
|
142
|
+
const techTerms = [
|
|
143
|
+
// Frameworks
|
|
144
|
+
'react', 'vue', 'angular', 'typescript', 'javascript', 'node', 'express',
|
|
145
|
+
// Auth
|
|
146
|
+
'auth', 'authentication', 'jwt', 'oauth', 'session', 'token',
|
|
147
|
+
// Data
|
|
148
|
+
'api', 'rest', 'graphql', 'database', 'sql', 'mongodb', 'redis',
|
|
149
|
+
// Testing
|
|
150
|
+
'test', 'testing', 'jest', 'mocha', 'vitest',
|
|
151
|
+
// Development
|
|
152
|
+
'refactor', 'refactoring', 'optimization', 'performance',
|
|
153
|
+
'bug', 'fix', 'error', 'issue', 'debug',
|
|
154
|
+
// CCW-specific terms
|
|
155
|
+
'cluster', 'clustering', 'memory', 'hook', 'service', 'context',
|
|
156
|
+
'workflow', 'skill', 'prompt', 'embedding', 'vector', 'semantic',
|
|
157
|
+
'dashboard', 'view', 'route', 'command', 'cli', 'mcp'
|
|
158
|
+
];
|
|
159
|
+
const lowerContent = content.toLowerCase();
|
|
160
|
+
for (const term of techTerms) {
|
|
161
|
+
if (lowerContent.includes(term)) {
|
|
162
|
+
keywords.add(term);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 4. Generic word extraction (words >= 4 chars, not stopwords)
|
|
166
|
+
const stopwords = new Set([
|
|
167
|
+
'the', 'and', 'for', 'that', 'this', 'with', 'from', 'have', 'will',
|
|
168
|
+
'are', 'was', 'were', 'been', 'being', 'what', 'when', 'where', 'which',
|
|
169
|
+
'there', 'their', 'they', 'them', 'then', 'than', 'into', 'some', 'such',
|
|
170
|
+
'only', 'also', 'just', 'more', 'most', 'other', 'after', 'before'
|
|
171
|
+
]);
|
|
172
|
+
const wordRegex = /\b([a-z]{4,})\b/g;
|
|
173
|
+
let wordMatch;
|
|
174
|
+
while ((wordMatch = wordRegex.exec(lowerContent)) !== null) {
|
|
175
|
+
const word = wordMatch[1];
|
|
176
|
+
if (!stopwords.has(word)) {
|
|
177
|
+
keywords.add(word);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Return top 20 keywords
|
|
181
|
+
return Array.from(keywords).slice(0, 20);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Extract file patterns from content
|
|
185
|
+
*/
|
|
186
|
+
extractFilePatterns(content) {
|
|
187
|
+
const patterns = new Set();
|
|
188
|
+
// Extract directory patterns (src/xxx/, lib/xxx/)
|
|
189
|
+
const dirRegex = /\b((?:src|lib|test|dist|build|public|components|utils|services|config|core|tools)(?:\/[\w-]+)*)\//g;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = dirRegex.exec(content)) !== null) {
|
|
192
|
+
patterns.add(match[1] + '/**');
|
|
193
|
+
}
|
|
194
|
+
// Extract file extension patterns
|
|
195
|
+
const extRegex = /\.(\w+)(?:\s|$|["'`])/g;
|
|
196
|
+
const extensions = new Set();
|
|
197
|
+
while ((match = extRegex.exec(content)) !== null) {
|
|
198
|
+
extensions.add(match[1]);
|
|
199
|
+
}
|
|
200
|
+
// Add extension patterns
|
|
201
|
+
if (extensions.size > 0) {
|
|
202
|
+
patterns.add(`**/*.{${Array.from(extensions).join(',')}}`);
|
|
203
|
+
}
|
|
204
|
+
return Array.from(patterns).slice(0, 10);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Calculate relevance score between two sessions
|
|
208
|
+
*/
|
|
209
|
+
calculateRelevance(session1, session2) {
|
|
210
|
+
const fileScore = this.calculateFileOverlap(session1, session2);
|
|
211
|
+
const temporalScore = this.calculateTemporalProximity(session1, session2);
|
|
212
|
+
const keywordScore = this.calculateSemanticSimilarity(session1, session2);
|
|
213
|
+
const vectorScore = this.calculateVectorSimilarity(session1, session2);
|
|
214
|
+
const intentScore = this.calculateIntentAlignment(session1, session2);
|
|
215
|
+
return (fileScore * WEIGHTS.fileOverlap +
|
|
216
|
+
temporalScore * WEIGHTS.temporalProximity +
|
|
217
|
+
keywordScore * WEIGHTS.keywordSimilarity +
|
|
218
|
+
vectorScore * WEIGHTS.vectorSimilarity +
|
|
219
|
+
intentScore * WEIGHTS.intentAlignment);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Calculate file path overlap score (Jaccard similarity)
|
|
223
|
+
*/
|
|
224
|
+
calculateFileOverlap(s1, s2) {
|
|
225
|
+
const files1 = new Set(s1.file_patterns || []);
|
|
226
|
+
const files2 = new Set(s2.file_patterns || []);
|
|
227
|
+
if (files1.size === 0 || files2.size === 0)
|
|
228
|
+
return 0;
|
|
229
|
+
const intersection = new Set([...files1].filter(f => files2.has(f)));
|
|
230
|
+
const union = new Set([...files1, ...files2]);
|
|
231
|
+
return intersection.size / union.size;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Calculate temporal proximity score
|
|
235
|
+
* 24h: 1.0, 7d: 0.7, 30d: 0.4, >30d: 0.1
|
|
236
|
+
*/
|
|
237
|
+
calculateTemporalProximity(s1, s2) {
|
|
238
|
+
if (!s1.created_at || !s2.created_at)
|
|
239
|
+
return 0.1;
|
|
240
|
+
const t1 = new Date(s1.created_at).getTime();
|
|
241
|
+
const t2 = new Date(s2.created_at).getTime();
|
|
242
|
+
const diffMs = Math.abs(t1 - t2);
|
|
243
|
+
const diffHours = diffMs / (1000 * 60 * 60);
|
|
244
|
+
if (diffHours <= 24)
|
|
245
|
+
return 1.0;
|
|
246
|
+
if (diffHours <= 24 * 7)
|
|
247
|
+
return 0.7;
|
|
248
|
+
if (diffHours <= 24 * 30)
|
|
249
|
+
return 0.4;
|
|
250
|
+
return 0.1;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Calculate semantic similarity using keyword overlap (Jaccard similarity)
|
|
254
|
+
*/
|
|
255
|
+
calculateSemanticSimilarity(s1, s2) {
|
|
256
|
+
const kw1 = new Set(s1.keywords || []);
|
|
257
|
+
const kw2 = new Set(s2.keywords || []);
|
|
258
|
+
if (kw1.size === 0 || kw2.size === 0)
|
|
259
|
+
return 0;
|
|
260
|
+
const intersection = new Set([...kw1].filter(k => kw2.has(k)));
|
|
261
|
+
const union = new Set([...kw1, ...kw2]);
|
|
262
|
+
return intersection.size / union.size;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Calculate intent alignment score
|
|
266
|
+
* Based on title/summary keyword matching
|
|
267
|
+
*/
|
|
268
|
+
calculateIntentAlignment(s1, s2) {
|
|
269
|
+
const text1 = ((s1.title || '') + ' ' + (s1.summary || '')).toLowerCase();
|
|
270
|
+
const text2 = ((s2.title || '') + ' ' + (s2.summary || '')).toLowerCase();
|
|
271
|
+
if (!text1 || !text2)
|
|
272
|
+
return 0;
|
|
273
|
+
// Simple word-based TF-IDF approximation
|
|
274
|
+
const words1 = text1.split(/\s+/).filter(w => w.length > 3);
|
|
275
|
+
const words2 = text2.split(/\s+/).filter(w => w.length > 3);
|
|
276
|
+
const set1 = new Set(words1);
|
|
277
|
+
const set2 = new Set(words2);
|
|
278
|
+
const intersection = new Set([...set1].filter(w => set2.has(w)));
|
|
279
|
+
const union = new Set([...set1, ...set2]);
|
|
280
|
+
return intersection.size / union.size;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Calculate vector similarity using pre-computed embeddings from memory_chunks
|
|
284
|
+
* Returns average cosine similarity of chunk embeddings
|
|
285
|
+
*/
|
|
286
|
+
calculateVectorSimilarity(s1, s2) {
|
|
287
|
+
const embedding1 = this.getSessionEmbedding(s1.session_id);
|
|
288
|
+
const embedding2 = this.getSessionEmbedding(s2.session_id);
|
|
289
|
+
// Graceful fallback if no embeddings available
|
|
290
|
+
if (!embedding1 || !embedding2) {
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
return this.cosineSimilarity(embedding1, embedding2);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Get session embedding by averaging all chunk embeddings
|
|
297
|
+
*/
|
|
298
|
+
getSessionEmbedding(sessionId) {
|
|
299
|
+
const chunks = this.coreMemoryStore.getChunks(sessionId);
|
|
300
|
+
if (chunks.length === 0) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
// Filter chunks that have embeddings
|
|
304
|
+
const embeddedChunks = chunks.filter(chunk => chunk.embedding && chunk.embedding.length > 0);
|
|
305
|
+
if (embeddedChunks.length === 0) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
// Convert Buffer embeddings to number arrays and calculate average
|
|
309
|
+
const embeddings = embeddedChunks.map(chunk => {
|
|
310
|
+
// Convert Buffer to Float32Array
|
|
311
|
+
const buffer = chunk.embedding;
|
|
312
|
+
const float32Array = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
|
|
313
|
+
return Array.from(float32Array);
|
|
314
|
+
});
|
|
315
|
+
// Check all embeddings have same dimension
|
|
316
|
+
const dimension = embeddings[0].length;
|
|
317
|
+
if (!embeddings.every(emb => emb.length === dimension)) {
|
|
318
|
+
console.warn(`[VectorSimilarity] Inconsistent embedding dimensions for session ${sessionId}`);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
// Calculate average embedding
|
|
322
|
+
const avgEmbedding = new Array(dimension).fill(0);
|
|
323
|
+
for (const embedding of embeddings) {
|
|
324
|
+
for (let i = 0; i < dimension; i++) {
|
|
325
|
+
avgEmbedding[i] += embedding[i];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (let i = 0; i < dimension; i++) {
|
|
329
|
+
avgEmbedding[i] /= embeddings.length;
|
|
330
|
+
}
|
|
331
|
+
return avgEmbedding;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Calculate cosine similarity between two vectors
|
|
335
|
+
*/
|
|
336
|
+
cosineSimilarity(a, b) {
|
|
337
|
+
if (a.length !== b.length) {
|
|
338
|
+
console.warn('[VectorSimilarity] Vector dimension mismatch');
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
let dotProduct = 0;
|
|
342
|
+
let normA = 0;
|
|
343
|
+
let normB = 0;
|
|
344
|
+
for (let i = 0; i < a.length; i++) {
|
|
345
|
+
dotProduct += a[i] * b[i];
|
|
346
|
+
normA += a[i] * a[i];
|
|
347
|
+
normB += b[i] * b[i];
|
|
348
|
+
}
|
|
349
|
+
normA = Math.sqrt(normA);
|
|
350
|
+
normB = Math.sqrt(normB);
|
|
351
|
+
if (normA === 0 || normB === 0) {
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
return dotProduct / (normA * normB);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Find the most relevant existing cluster for a set of session IDs
|
|
358
|
+
* Returns the cluster with highest session overlap
|
|
359
|
+
*/
|
|
360
|
+
findExistingClusterForSessions(sessionIds) {
|
|
361
|
+
if (sessionIds.length === 0)
|
|
362
|
+
return null;
|
|
363
|
+
const clusterCounts = new Map();
|
|
364
|
+
let maxCount = 0;
|
|
365
|
+
let bestClusterId = null;
|
|
366
|
+
for (const sessionId of sessionIds) {
|
|
367
|
+
const clusters = this.coreMemoryStore.getSessionClusters(sessionId);
|
|
368
|
+
for (const cluster of clusters) {
|
|
369
|
+
if (cluster.status !== 'active')
|
|
370
|
+
continue;
|
|
371
|
+
const count = (clusterCounts.get(cluster.id) || 0) + 1;
|
|
372
|
+
clusterCounts.set(cluster.id, count);
|
|
373
|
+
if (count > maxCount) {
|
|
374
|
+
maxCount = count;
|
|
375
|
+
bestClusterId = cluster.id;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (bestClusterId) {
|
|
380
|
+
return this.coreMemoryStore.getCluster(bestClusterId);
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Determine if a new cluster should merge with an existing one
|
|
386
|
+
* Based on 70% session overlap threshold
|
|
387
|
+
*/
|
|
388
|
+
shouldMergeWithExisting(newClusterSessions, existingCluster) {
|
|
389
|
+
const MERGE_THRESHOLD = 0.7;
|
|
390
|
+
const existingMembers = this.coreMemoryStore.getClusterMembers(existingCluster.id);
|
|
391
|
+
const newSessionIds = new Set(newClusterSessions.map(s => s.session_id));
|
|
392
|
+
const existingSessionIds = new Set(existingMembers.map(m => m.session_id));
|
|
393
|
+
if (newSessionIds.size === 0)
|
|
394
|
+
return false;
|
|
395
|
+
const intersection = new Set([...newSessionIds].filter(id => existingSessionIds.has(id)));
|
|
396
|
+
const overlapRatio = intersection.size / newSessionIds.size;
|
|
397
|
+
return overlapRatio > MERGE_THRESHOLD;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Run auto-clustering algorithm
|
|
401
|
+
* Optimized to prevent duplicate clusters by checking existing clusters first
|
|
402
|
+
*/
|
|
403
|
+
async autocluster(options) {
|
|
404
|
+
// 1. Collect sessions based on user-specified scope (default: 'recent')
|
|
405
|
+
const allSessions = await this.collectSessions(options);
|
|
406
|
+
console.log(`[Clustering] Collected ${allSessions.length} sessions (scope: ${options?.scope || 'recent'})`);
|
|
407
|
+
// 2. Filter out already-clustered sessions to prevent duplicates
|
|
408
|
+
const sessions = allSessions.filter(s => {
|
|
409
|
+
const clusters = this.coreMemoryStore.getSessionClusters(s.session_id);
|
|
410
|
+
return clusters.length === 0;
|
|
411
|
+
});
|
|
412
|
+
console.log(`[Clustering] ${sessions.length} unclustered sessions after filtering`);
|
|
413
|
+
// 3. Update metadata cache
|
|
414
|
+
for (const session of sessions) {
|
|
415
|
+
this.coreMemoryStore.upsertSessionMetadata(session);
|
|
416
|
+
}
|
|
417
|
+
// 4. Calculate relevance matrix
|
|
418
|
+
const n = sessions.length;
|
|
419
|
+
const relevanceMatrix = Array(n).fill(0).map(() => Array(n).fill(0));
|
|
420
|
+
let maxScore = 0;
|
|
421
|
+
let avgScore = 0;
|
|
422
|
+
let pairCount = 0;
|
|
423
|
+
for (let i = 0; i < n; i++) {
|
|
424
|
+
for (let j = i + 1; j < n; j++) {
|
|
425
|
+
const score = this.calculateRelevance(sessions[i], sessions[j]);
|
|
426
|
+
relevanceMatrix[i][j] = score;
|
|
427
|
+
relevanceMatrix[j][i] = score;
|
|
428
|
+
if (score > maxScore)
|
|
429
|
+
maxScore = score;
|
|
430
|
+
avgScore += score;
|
|
431
|
+
pairCount++;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (pairCount > 0) {
|
|
435
|
+
avgScore = avgScore / pairCount;
|
|
436
|
+
console.log(`[Clustering] Relevance stats: max=${maxScore.toFixed(3)}, avg=${avgScore.toFixed(3)}, pairs=${pairCount}, threshold=${CLUSTER_THRESHOLD}`);
|
|
437
|
+
}
|
|
438
|
+
// 5. Agglomerative clustering
|
|
439
|
+
const minClusterSize = options?.minClusterSize || 2;
|
|
440
|
+
// Early return if not enough sessions
|
|
441
|
+
if (sessions.length < minClusterSize) {
|
|
442
|
+
console.log('[Clustering] Not enough unclustered sessions to form new clusters');
|
|
443
|
+
return { clustersCreated: 0, sessionsProcessed: allSessions.length, sessionsClustered: 0 };
|
|
444
|
+
}
|
|
445
|
+
const newPotentialClusters = this.agglomerativeClustering(sessions, relevanceMatrix, CLUSTER_THRESHOLD);
|
|
446
|
+
console.log(`[Clustering] Generated ${newPotentialClusters.length} potential clusters`);
|
|
447
|
+
// 6. Process clusters: create new or merge with existing
|
|
448
|
+
let clustersCreated = 0;
|
|
449
|
+
let clustersMerged = 0;
|
|
450
|
+
let sessionsClustered = 0;
|
|
451
|
+
for (const clusterSessions of newPotentialClusters) {
|
|
452
|
+
if (clusterSessions.length < minClusterSize) {
|
|
453
|
+
continue; // Skip small clusters
|
|
454
|
+
}
|
|
455
|
+
const sessionIds = clusterSessions.map(s => s.session_id);
|
|
456
|
+
const existingCluster = this.findExistingClusterForSessions(sessionIds);
|
|
457
|
+
// Check if we should merge with an existing cluster
|
|
458
|
+
if (existingCluster && this.shouldMergeWithExisting(clusterSessions, existingCluster)) {
|
|
459
|
+
const existingMembers = this.coreMemoryStore.getClusterMembers(existingCluster.id);
|
|
460
|
+
const existingSessionIds = new Set(existingMembers.map(m => m.session_id));
|
|
461
|
+
// Only add sessions not already in the cluster
|
|
462
|
+
const newSessions = clusterSessions.filter(s => !existingSessionIds.has(s.session_id));
|
|
463
|
+
if (newSessions.length > 0) {
|
|
464
|
+
newSessions.forEach((session, index) => {
|
|
465
|
+
this.coreMemoryStore.addClusterMember({
|
|
466
|
+
cluster_id: existingCluster.id,
|
|
467
|
+
session_id: session.session_id,
|
|
468
|
+
session_type: session.session_type,
|
|
469
|
+
sequence_order: existingMembers.length + index + 1,
|
|
470
|
+
relevance_score: 1.0
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
// Update cluster description
|
|
474
|
+
this.coreMemoryStore.updateCluster(existingCluster.id, {
|
|
475
|
+
description: `Auto-generated cluster with ${existingMembers.length + newSessions.length} sessions`
|
|
476
|
+
});
|
|
477
|
+
clustersMerged++;
|
|
478
|
+
sessionsClustered += newSessions.length;
|
|
479
|
+
console.log(`[Clustering] Merged ${newSessions.length} sessions into existing cluster '${existingCluster.name}'`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// Create new cluster
|
|
484
|
+
const clusterName = this.generateClusterName(clusterSessions);
|
|
485
|
+
const clusterIntent = this.generateClusterIntent(clusterSessions);
|
|
486
|
+
const clusterRecord = this.coreMemoryStore.createCluster({
|
|
487
|
+
name: clusterName,
|
|
488
|
+
description: `Auto-generated cluster with ${clusterSessions.length} sessions`,
|
|
489
|
+
intent: clusterIntent,
|
|
490
|
+
status: 'active'
|
|
491
|
+
});
|
|
492
|
+
// Add members
|
|
493
|
+
clusterSessions.forEach((session, index) => {
|
|
494
|
+
this.coreMemoryStore.addClusterMember({
|
|
495
|
+
cluster_id: clusterRecord.id,
|
|
496
|
+
session_id: session.session_id,
|
|
497
|
+
session_type: session.session_type,
|
|
498
|
+
sequence_order: index + 1,
|
|
499
|
+
relevance_score: 1.0
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
clustersCreated++;
|
|
503
|
+
sessionsClustered += clusterSessions.length;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
console.log(`[Clustering] Summary: ${clustersCreated} created, ${clustersMerged} merged, ${allSessions.length - sessions.length} already clustered`);
|
|
507
|
+
return {
|
|
508
|
+
clustersCreated,
|
|
509
|
+
sessionsProcessed: allSessions.length,
|
|
510
|
+
sessionsClustered
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Deduplicate clusters by merging similar ones
|
|
515
|
+
* Clusters with same name or >50% member overlap are merged
|
|
516
|
+
* @returns Statistics about deduplication
|
|
517
|
+
*/
|
|
518
|
+
async deduplicateClusters() {
|
|
519
|
+
const clusters = this.coreMemoryStore.listClusters('active');
|
|
520
|
+
console.log(`[Dedup] Analyzing ${clusters.length} active clusters`);
|
|
521
|
+
if (clusters.length < 2) {
|
|
522
|
+
return { merged: 0, deleted: 0, remaining: clusters.length };
|
|
523
|
+
}
|
|
524
|
+
// Group clusters by name (case-insensitive)
|
|
525
|
+
const byName = new Map();
|
|
526
|
+
for (const cluster of clusters) {
|
|
527
|
+
const key = cluster.name.toLowerCase().trim();
|
|
528
|
+
if (!byName.has(key)) {
|
|
529
|
+
byName.set(key, []);
|
|
530
|
+
}
|
|
531
|
+
byName.get(key).push(cluster);
|
|
532
|
+
}
|
|
533
|
+
let merged = 0;
|
|
534
|
+
let deleted = 0;
|
|
535
|
+
// Merge clusters with same name
|
|
536
|
+
for (const [name, group] of byName) {
|
|
537
|
+
if (group.length < 2)
|
|
538
|
+
continue;
|
|
539
|
+
// Sort by created_at (oldest first) to keep the original
|
|
540
|
+
group.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
541
|
+
const target = group[0];
|
|
542
|
+
const sources = group.slice(1).map(c => c.id);
|
|
543
|
+
console.log(`[Dedup] Merging ${sources.length} duplicate clusters named '${name}' into ${target.id}`);
|
|
544
|
+
try {
|
|
545
|
+
const membersMoved = this.coreMemoryStore.mergeClusters(target.id, sources);
|
|
546
|
+
merged += sources.length;
|
|
547
|
+
console.log(`[Dedup] Moved ${membersMoved} members, deleted ${sources.length} clusters`);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
console.warn(`[Dedup] Failed to merge: ${error.message}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Check for clusters with high member overlap
|
|
554
|
+
const remainingClusters = this.coreMemoryStore.listClusters('active');
|
|
555
|
+
const clusterMembers = new Map();
|
|
556
|
+
for (const cluster of remainingClusters) {
|
|
557
|
+
const members = this.coreMemoryStore.getClusterMembers(cluster.id);
|
|
558
|
+
clusterMembers.set(cluster.id, new Set(members.map(m => m.session_id)));
|
|
559
|
+
}
|
|
560
|
+
// Find and merge overlapping clusters
|
|
561
|
+
const processed = new Set();
|
|
562
|
+
for (let i = 0; i < remainingClusters.length; i++) {
|
|
563
|
+
const clusterA = remainingClusters[i];
|
|
564
|
+
if (processed.has(clusterA.id))
|
|
565
|
+
continue;
|
|
566
|
+
const membersA = clusterMembers.get(clusterA.id);
|
|
567
|
+
const toMerge = [];
|
|
568
|
+
for (let j = i + 1; j < remainingClusters.length; j++) {
|
|
569
|
+
const clusterB = remainingClusters[j];
|
|
570
|
+
if (processed.has(clusterB.id))
|
|
571
|
+
continue;
|
|
572
|
+
const membersB = clusterMembers.get(clusterB.id);
|
|
573
|
+
const intersection = new Set([...membersA].filter(m => membersB.has(m)));
|
|
574
|
+
// Calculate overlap ratio (based on smaller cluster)
|
|
575
|
+
const minSize = Math.min(membersA.size, membersB.size);
|
|
576
|
+
if (minSize > 0 && intersection.size / minSize >= 0.5) {
|
|
577
|
+
toMerge.push(clusterB.id);
|
|
578
|
+
processed.add(clusterB.id);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (toMerge.length > 0) {
|
|
582
|
+
console.log(`[Dedup] Merging ${toMerge.length} overlapping clusters into ${clusterA.id}`);
|
|
583
|
+
try {
|
|
584
|
+
this.coreMemoryStore.mergeClusters(clusterA.id, toMerge);
|
|
585
|
+
merged += toMerge.length;
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
console.warn(`[Dedup] Failed to merge overlapping: ${error.message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Delete empty clusters
|
|
593
|
+
const finalClusters = this.coreMemoryStore.listClusters('active');
|
|
594
|
+
for (const cluster of finalClusters) {
|
|
595
|
+
const members = this.coreMemoryStore.getClusterMembers(cluster.id);
|
|
596
|
+
if (members.length === 0) {
|
|
597
|
+
this.coreMemoryStore.deleteCluster(cluster.id);
|
|
598
|
+
deleted++;
|
|
599
|
+
console.log(`[Dedup] Deleted empty cluster: ${cluster.id}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const remaining = this.coreMemoryStore.listClusters('active').length;
|
|
603
|
+
console.log(`[Dedup] Complete: ${merged} merged, ${deleted} deleted, ${remaining} remaining`);
|
|
604
|
+
return { merged, deleted, remaining };
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Agglomerative clustering algorithm
|
|
608
|
+
* Returns array of clusters (each cluster is array of sessions)
|
|
609
|
+
*/
|
|
610
|
+
agglomerativeClustering(sessions, relevanceMatrix, threshold) {
|
|
611
|
+
const n = sessions.length;
|
|
612
|
+
// Initialize: each session is its own cluster
|
|
613
|
+
const clusters = sessions.map((_, i) => new Set([i]));
|
|
614
|
+
while (true) {
|
|
615
|
+
let maxScore = -1;
|
|
616
|
+
let mergeI = -1;
|
|
617
|
+
let mergeJ = -1;
|
|
618
|
+
// Find pair of clusters with highest average linkage
|
|
619
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
620
|
+
for (let j = i + 1; j < clusters.length; j++) {
|
|
621
|
+
const score = this.averageLinkage(clusters[i], clusters[j], relevanceMatrix);
|
|
622
|
+
if (score > maxScore) {
|
|
623
|
+
maxScore = score;
|
|
624
|
+
mergeI = i;
|
|
625
|
+
mergeJ = j;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// Stop if no pair exceeds threshold
|
|
630
|
+
if (maxScore < threshold)
|
|
631
|
+
break;
|
|
632
|
+
// Merge clusters
|
|
633
|
+
const merged = new Set([...clusters[mergeI], ...clusters[mergeJ]]);
|
|
634
|
+
clusters.splice(mergeJ, 1); // Remove j first (higher index)
|
|
635
|
+
clusters.splice(mergeI, 1);
|
|
636
|
+
clusters.push(merged);
|
|
637
|
+
}
|
|
638
|
+
// Convert cluster indices to sessions
|
|
639
|
+
return clusters.map(cluster => Array.from(cluster).map(i => sessions[i]));
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Calculate average linkage between two clusters
|
|
643
|
+
*/
|
|
644
|
+
averageLinkage(cluster1, cluster2, relevanceMatrix) {
|
|
645
|
+
let sum = 0;
|
|
646
|
+
let count = 0;
|
|
647
|
+
for (const i of cluster1) {
|
|
648
|
+
for (const j of cluster2) {
|
|
649
|
+
sum += relevanceMatrix[i][j];
|
|
650
|
+
count++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return count > 0 ? sum / count : 0;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Generate cluster name from members
|
|
657
|
+
*/
|
|
658
|
+
generateClusterName(members) {
|
|
659
|
+
// Count keyword frequency
|
|
660
|
+
const keywordFreq = new Map();
|
|
661
|
+
for (const member of members) {
|
|
662
|
+
for (const keyword of member.keywords || []) {
|
|
663
|
+
keywordFreq.set(keyword, (keywordFreq.get(keyword) || 0) + 1);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Get top 2 keywords
|
|
667
|
+
const sorted = Array.from(keywordFreq.entries())
|
|
668
|
+
.sort((a, b) => b[1] - a[1])
|
|
669
|
+
.map(([kw]) => kw);
|
|
670
|
+
if (sorted.length >= 2) {
|
|
671
|
+
return `${sorted[0]}-${sorted[1]}`;
|
|
672
|
+
}
|
|
673
|
+
else if (sorted.length === 1) {
|
|
674
|
+
return sorted[0];
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
return 'unnamed-cluster';
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Generate cluster intent from members
|
|
682
|
+
*/
|
|
683
|
+
generateClusterIntent(members) {
|
|
684
|
+
// Extract common action words from titles
|
|
685
|
+
const actionWords = ['implement', 'refactor', 'fix', 'add', 'create', 'update', 'optimize'];
|
|
686
|
+
const titles = members.map(m => (m.title || '').toLowerCase());
|
|
687
|
+
for (const action of actionWords) {
|
|
688
|
+
const count = titles.filter(t => t.includes(action)).length;
|
|
689
|
+
if (count >= members.length / 2) {
|
|
690
|
+
const topic = this.generateClusterName(members);
|
|
691
|
+
return `${action.charAt(0).toUpperCase() + action.slice(1)} ${topic}`;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return `Work on ${this.generateClusterName(members)}`;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Get progressive disclosure index for hook
|
|
698
|
+
* @param options - Configuration options
|
|
699
|
+
* @param options.type - 'session-start' returns recent sessions, 'context' returns intent-matched sessions
|
|
700
|
+
* @param options.sessionId - Current session ID (optional)
|
|
701
|
+
* @param options.prompt - User prompt for intent matching (required for 'context' type)
|
|
702
|
+
*/
|
|
703
|
+
async getProgressiveIndex(options) {
|
|
704
|
+
const { type, sessionId, prompt } = options;
|
|
705
|
+
// For session-start: return recent sessions by time
|
|
706
|
+
if (type === 'session-start') {
|
|
707
|
+
return this.getRecentSessionsIndex();
|
|
708
|
+
}
|
|
709
|
+
// For context: return intent-matched sessions based on prompt
|
|
710
|
+
if (type === 'context' && prompt) {
|
|
711
|
+
return this.getIntentMatchedIndex(prompt, sessionId);
|
|
712
|
+
}
|
|
713
|
+
// Fallback to recent sessions
|
|
714
|
+
return this.getRecentSessionsIndex();
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Get recent sessions index (for session-start)
|
|
718
|
+
* Shows sessions grouped by clusters with progressive disclosure
|
|
719
|
+
*/
|
|
720
|
+
async getRecentSessionsIndex() {
|
|
721
|
+
// 1. Get all active clusters
|
|
722
|
+
const allClusters = this.coreMemoryStore.listClusters('active');
|
|
723
|
+
// Sort clusters by most recent activity (based on member last_accessed)
|
|
724
|
+
const clustersWithActivity = allClusters.map(cluster => {
|
|
725
|
+
const members = this.coreMemoryStore.getClusterMembers(cluster.id);
|
|
726
|
+
const memberMetadata = members
|
|
727
|
+
.map(m => this.coreMemoryStore.getSessionMetadata(m.session_id))
|
|
728
|
+
.filter((m) => m !== null);
|
|
729
|
+
const lastActivity = memberMetadata.reduce((latest, m) => {
|
|
730
|
+
const accessed = m.last_accessed || m.created_at || '';
|
|
731
|
+
return accessed > latest ? accessed : latest;
|
|
732
|
+
}, '');
|
|
733
|
+
return { cluster, members, memberMetadata, lastActivity };
|
|
734
|
+
}).sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
|
735
|
+
// 2. Get unclustered recent sessions
|
|
736
|
+
const allSessions = await this.collectSessions({ scope: 'recent' });
|
|
737
|
+
const clusteredSessionIds = new Set();
|
|
738
|
+
clustersWithActivity.forEach(c => {
|
|
739
|
+
c.members.forEach(m => clusteredSessionIds.add(m.session_id));
|
|
740
|
+
});
|
|
741
|
+
const unclusteredSessions = allSessions
|
|
742
|
+
.filter(s => !clusteredSessionIds.has(s.session_id))
|
|
743
|
+
.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''))
|
|
744
|
+
.slice(0, 3);
|
|
745
|
+
// 3. Build output
|
|
746
|
+
let output = `<ccw-session-context>\n## 📋 Session Context (Progressive Disclosure)\n\n`;
|
|
747
|
+
// Show top 2 active clusters
|
|
748
|
+
const topClusters = clustersWithActivity.slice(0, 2);
|
|
749
|
+
if (topClusters.length > 0) {
|
|
750
|
+
output += `### 🔗 Active Clusters\n\n`;
|
|
751
|
+
for (const { cluster, memberMetadata } of topClusters) {
|
|
752
|
+
output += `**${cluster.name}** (${memberMetadata.length} sessions)\n`;
|
|
753
|
+
if (cluster.intent) {
|
|
754
|
+
output += `> Intent: ${cluster.intent}\n`;
|
|
755
|
+
}
|
|
756
|
+
output += `\n| Session | Type | Title |\n|---------|------|-------|\n`;
|
|
757
|
+
// Show top 3 members per cluster
|
|
758
|
+
const displayMembers = memberMetadata.slice(0, 3);
|
|
759
|
+
for (const m of displayMembers) {
|
|
760
|
+
const type = m.session_type === 'core_memory' ? 'Core' :
|
|
761
|
+
m.session_type === 'workflow' ? 'Workflow' : 'CLI';
|
|
762
|
+
const title = (m.title || '').substring(0, 35);
|
|
763
|
+
output += `| ${m.session_id} | ${type} | ${title} |\n`;
|
|
764
|
+
}
|
|
765
|
+
if (memberMetadata.length > 3) {
|
|
766
|
+
output += `| ... | ... | +${memberMetadata.length - 3} more |\n`;
|
|
767
|
+
}
|
|
768
|
+
output += `\n`;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Show unclustered recent sessions
|
|
772
|
+
if (unclusteredSessions.length > 0) {
|
|
773
|
+
output += `### 📝 Recent Sessions (Unclustered)\n\n`;
|
|
774
|
+
output += `| Session | Type | Title | Date |\n`;
|
|
775
|
+
output += `|---------|------|-------|------|\n`;
|
|
776
|
+
for (const s of unclusteredSessions) {
|
|
777
|
+
const type = s.session_type === 'core_memory' ? 'Core' :
|
|
778
|
+
s.session_type === 'workflow' ? 'Workflow' : 'CLI';
|
|
779
|
+
const title = (s.title || '').substring(0, 30);
|
|
780
|
+
const date = s.created_at ? new Date(s.created_at).toLocaleDateString() : '';
|
|
781
|
+
output += `| ${s.session_id} | ${type} | ${title} | ${date} |\n`;
|
|
782
|
+
}
|
|
783
|
+
output += `\n`;
|
|
784
|
+
}
|
|
785
|
+
// If nothing found
|
|
786
|
+
if (topClusters.length === 0 && unclusteredSessions.length === 0) {
|
|
787
|
+
output += `No recent sessions found. Start a new workflow to begin tracking.\n\n`;
|
|
788
|
+
}
|
|
789
|
+
// Add MCP tools reference
|
|
790
|
+
const topSession = topClusters[0]?.memberMetadata[0] || unclusteredSessions[0];
|
|
791
|
+
const topClusterId = topClusters[0]?.cluster.id;
|
|
792
|
+
output += `**MCP Tools**:\n\`\`\`\n`;
|
|
793
|
+
if (topSession) {
|
|
794
|
+
output += `# Resume session\nmcp__ccw-tools__core_memory({ "operation": "export", "id": "${topSession.session_id}" })\n\n`;
|
|
795
|
+
}
|
|
796
|
+
if (topClusterId) {
|
|
797
|
+
output += `# Load cluster context\nmcp__ccw-tools__core_memory({ "operation": "search", "query": "cluster:${topClusterId}" })\n`;
|
|
798
|
+
}
|
|
799
|
+
output += `\`\`\`\n</ccw-session-context>`;
|
|
800
|
+
return output;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Get intent-matched sessions index (for context with prompt)
|
|
804
|
+
* Shows sessions grouped by clusters and ranked by relevance
|
|
805
|
+
*/
|
|
806
|
+
async getIntentMatchedIndex(prompt, sessionId) {
|
|
807
|
+
const sessions = await this.collectSessions({ scope: 'all' });
|
|
808
|
+
if (sessions.length === 0) {
|
|
809
|
+
return `<ccw-session-context>
|
|
810
|
+
## 📋 Related Sessions
|
|
811
|
+
|
|
812
|
+
No sessions available for intent matching.
|
|
813
|
+
</ccw-session-context>`;
|
|
814
|
+
}
|
|
815
|
+
// Create a virtual session from the prompt for similarity calculation
|
|
816
|
+
const promptSession = {
|
|
817
|
+
session_id: 'prompt-virtual',
|
|
818
|
+
session_type: 'native',
|
|
819
|
+
title: prompt.substring(0, 100),
|
|
820
|
+
summary: prompt.substring(0, 200),
|
|
821
|
+
keywords: this.extractKeywords(prompt),
|
|
822
|
+
token_estimate: Math.ceil(prompt.length / 4),
|
|
823
|
+
file_patterns: this.extractFilePatterns(prompt),
|
|
824
|
+
created_at: new Date().toISOString(),
|
|
825
|
+
last_accessed: new Date().toISOString(),
|
|
826
|
+
access_count: 0
|
|
827
|
+
};
|
|
828
|
+
// Build session-to-cluster mapping
|
|
829
|
+
const sessionClusterMap = new Map();
|
|
830
|
+
const allClusters = this.coreMemoryStore.listClusters('active');
|
|
831
|
+
for (const cluster of allClusters) {
|
|
832
|
+
const members = this.coreMemoryStore.getClusterMembers(cluster.id);
|
|
833
|
+
for (const member of members) {
|
|
834
|
+
const existing = sessionClusterMap.get(member.session_id) || [];
|
|
835
|
+
existing.push(cluster);
|
|
836
|
+
sessionClusterMap.set(member.session_id, existing);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Calculate relevance scores for all sessions
|
|
840
|
+
const scoredSessions = sessions
|
|
841
|
+
.filter(s => s.session_id !== sessionId) // Exclude current session
|
|
842
|
+
.map(s => ({
|
|
843
|
+
session: s,
|
|
844
|
+
score: this.calculateRelevance(promptSession, s),
|
|
845
|
+
clusters: sessionClusterMap.get(s.session_id) || []
|
|
846
|
+
}))
|
|
847
|
+
.filter(item => item.score >= 0.15) // Minimum relevance threshold (lowered for file-path-based keywords)
|
|
848
|
+
.sort((a, b) => b.score - a.score)
|
|
849
|
+
.slice(0, 8); // Top 8 relevant sessions
|
|
850
|
+
if (scoredSessions.length === 0) {
|
|
851
|
+
return `<ccw-session-context>
|
|
852
|
+
## 📋 Related Sessions
|
|
853
|
+
|
|
854
|
+
No sessions match current intent. Consider:
|
|
855
|
+
- Starting fresh with a new approach
|
|
856
|
+
- Using \`search\` to find sessions by keyword
|
|
857
|
+
|
|
858
|
+
**MCP Tools**:
|
|
859
|
+
\`\`\`
|
|
860
|
+
mcp__ccw-tools__core_memory({ "operation": "search", "query": "<keyword>" })
|
|
861
|
+
\`\`\`
|
|
862
|
+
</ccw-session-context>`;
|
|
863
|
+
}
|
|
864
|
+
// Group sessions by cluster
|
|
865
|
+
const clusterGroups = new Map();
|
|
866
|
+
const unclusteredSessions = [];
|
|
867
|
+
for (const item of scoredSessions) {
|
|
868
|
+
if (item.clusters.length > 0) {
|
|
869
|
+
// Add to the highest-priority cluster
|
|
870
|
+
const primaryCluster = item.clusters[0];
|
|
871
|
+
const existing = clusterGroups.get(primaryCluster.id) || { cluster: primaryCluster, sessions: [] };
|
|
872
|
+
existing.sessions.push(item);
|
|
873
|
+
clusterGroups.set(primaryCluster.id, existing);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
unclusteredSessions.push(item);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Sort cluster groups by best session score
|
|
880
|
+
const sortedGroups = Array.from(clusterGroups.values())
|
|
881
|
+
.sort((a, b) => Math.max(...b.sessions.map(s => s.score)) - Math.max(...a.sessions.map(s => s.score)));
|
|
882
|
+
// Generate output
|
|
883
|
+
let output = `<ccw-session-context>\n## 📋 Intent-Matched Sessions\n\n`;
|
|
884
|
+
output += `**Detected Intent**: ${(promptSession.keywords || []).slice(0, 5).join(', ') || 'General'}\n\n`;
|
|
885
|
+
// Show clustered sessions
|
|
886
|
+
if (sortedGroups.length > 0) {
|
|
887
|
+
output += `### 🔗 Matched Clusters\n\n`;
|
|
888
|
+
for (const { cluster, sessions: clusterSessions } of sortedGroups.slice(0, 2)) {
|
|
889
|
+
const avgScore = Math.round(clusterSessions.reduce((sum, s) => sum + s.score, 0) / clusterSessions.length * 100);
|
|
890
|
+
output += `**${cluster.name}** (${avgScore}% avg match)\n`;
|
|
891
|
+
if (cluster.intent) {
|
|
892
|
+
output += `> ${cluster.intent}\n`;
|
|
893
|
+
}
|
|
894
|
+
output += `\n| Session | Match | Title |\n|---------|-------|-------|\n`;
|
|
895
|
+
for (const item of clusterSessions.slice(0, 3)) {
|
|
896
|
+
const matchPct = Math.round(item.score * 100);
|
|
897
|
+
const title = (item.session.title || '').substring(0, 35);
|
|
898
|
+
output += `| ${item.session.session_id} | ${matchPct}% | ${title} |\n`;
|
|
899
|
+
}
|
|
900
|
+
output += `\n`;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// Show unclustered sessions
|
|
904
|
+
if (unclusteredSessions.length > 0) {
|
|
905
|
+
output += `### 📝 Individual Matches\n\n`;
|
|
906
|
+
output += `| Session | Type | Match | Title |\n`;
|
|
907
|
+
output += `|---------|------|-------|-------|\n`;
|
|
908
|
+
for (const item of unclusteredSessions.slice(0, 4)) {
|
|
909
|
+
const type = item.session.session_type === 'core_memory' ? 'Core' :
|
|
910
|
+
item.session.session_type === 'workflow' ? 'Workflow' : 'CLI';
|
|
911
|
+
const matchPct = Math.round(item.score * 100);
|
|
912
|
+
const title = (item.session.title || '').substring(0, 30);
|
|
913
|
+
output += `| ${item.session.session_id} | ${type} | ${matchPct}% | ${title} |\n`;
|
|
914
|
+
}
|
|
915
|
+
output += `\n`;
|
|
916
|
+
}
|
|
917
|
+
// Add MCP tools reference
|
|
918
|
+
const topSession = scoredSessions[0];
|
|
919
|
+
const topCluster = sortedGroups[0]?.cluster;
|
|
920
|
+
output += `**MCP Tools**:\n\`\`\`\n`;
|
|
921
|
+
output += `# Resume top match\nmcp__ccw-tools__core_memory({ "operation": "export", "id": "${topSession.session.session_id}" })\n`;
|
|
922
|
+
if (topCluster) {
|
|
923
|
+
output += `\n# Load cluster context\nmcp__ccw-tools__core_memory({ "operation": "search", "query": "cluster:${topCluster.id}" })\n`;
|
|
924
|
+
}
|
|
925
|
+
output += `\`\`\`\n</ccw-session-context>`;
|
|
926
|
+
return output;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Legacy method for backward compatibility
|
|
930
|
+
* @deprecated Use getProgressiveIndex({ type, sessionId, prompt }) instead
|
|
931
|
+
*/
|
|
932
|
+
async getProgressiveIndexLegacy(sessionId) {
|
|
933
|
+
let activeCluster = null;
|
|
934
|
+
let members = [];
|
|
935
|
+
if (sessionId) {
|
|
936
|
+
const clusters = this.coreMemoryStore.getSessionClusters(sessionId);
|
|
937
|
+
if (clusters.length > 0) {
|
|
938
|
+
activeCluster = clusters[0];
|
|
939
|
+
const clusterMembers = this.coreMemoryStore.getClusterMembers(activeCluster.id);
|
|
940
|
+
members = clusterMembers
|
|
941
|
+
.map(m => this.coreMemoryStore.getSessionMetadata(m.session_id))
|
|
942
|
+
.filter((m) => m !== null)
|
|
943
|
+
.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!activeCluster || members.length === 0) {
|
|
947
|
+
return `<ccw-session-context>
|
|
948
|
+
## 📋 Related Sessions Index
|
|
949
|
+
|
|
950
|
+
No active cluster found. Start a new workflow or continue from recent sessions.
|
|
951
|
+
|
|
952
|
+
**MCP Tools**:
|
|
953
|
+
\`\`\`
|
|
954
|
+
# Search sessions
|
|
955
|
+
Use tool: mcp__ccw-tools__core_memory
|
|
956
|
+
Parameters: { "action": "search", "query": "<keyword>" }
|
|
957
|
+
|
|
958
|
+
# Trigger clustering
|
|
959
|
+
Parameters: { "action": "cluster", "scope": "auto" }
|
|
960
|
+
\`\`\`
|
|
961
|
+
</ccw-session-context>`;
|
|
962
|
+
}
|
|
963
|
+
// Generate table
|
|
964
|
+
let table = `| # | Session | Type | Summary | Tokens |\n`;
|
|
965
|
+
table += `|---|---------|------|---------|--------|\n`;
|
|
966
|
+
members.forEach((m, idx) => {
|
|
967
|
+
const type = m.session_type === 'core_memory' ? 'Core' :
|
|
968
|
+
m.session_type === 'workflow' ? 'Workflow' : 'CLI';
|
|
969
|
+
const summary = (m.summary || '').substring(0, 40);
|
|
970
|
+
const token = `~${m.token_estimate || 0}`;
|
|
971
|
+
table += `| ${idx + 1} | ${m.session_id} | ${type} | ${summary} | ${token} |\n`;
|
|
972
|
+
});
|
|
973
|
+
// Generate timeline - show multiple recent sessions
|
|
974
|
+
let timeline = '';
|
|
975
|
+
if (members.length > 0) {
|
|
976
|
+
const timelineEntries = [];
|
|
977
|
+
const displayCount = Math.min(members.length, 3); // Show last 3 sessions
|
|
978
|
+
for (let i = members.length - displayCount; i < members.length; i++) {
|
|
979
|
+
const member = members[i];
|
|
980
|
+
const date = member.created_at ? new Date(member.created_at).toLocaleDateString() : '';
|
|
981
|
+
const title = member.title?.substring(0, 30) || 'Untitled';
|
|
982
|
+
const isCurrent = i === members.length - 1;
|
|
983
|
+
const marker = isCurrent ? ' ← Current' : '';
|
|
984
|
+
timelineEntries.push(`${date} ─●─ ${member.session_id} (${title})${marker}`);
|
|
985
|
+
}
|
|
986
|
+
timeline = `\`\`\`\n${timelineEntries.join('\n │\n')}\n\`\`\``;
|
|
987
|
+
}
|
|
988
|
+
return `<ccw-session-context>
|
|
989
|
+
## 📋 Related Sessions Index
|
|
990
|
+
|
|
991
|
+
### 🔗 Active Cluster: ${activeCluster.name} (${members.length} sessions)
|
|
992
|
+
**Intent**: ${activeCluster.intent || 'No intent specified'}
|
|
993
|
+
|
|
994
|
+
${table}
|
|
995
|
+
|
|
996
|
+
**Resume via MCP**:
|
|
997
|
+
\`\`\`
|
|
998
|
+
Use tool: mcp__ccw-tools__core_memory
|
|
999
|
+
Parameters: { "action": "load", "id": "${members[members.length - 1].session_id}" }
|
|
1000
|
+
|
|
1001
|
+
Or load entire cluster:
|
|
1002
|
+
{ "action": "load-cluster", "clusterId": "${activeCluster.id}" }
|
|
1003
|
+
\`\`\`
|
|
1004
|
+
|
|
1005
|
+
### 📊 Timeline
|
|
1006
|
+
${timeline}
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
**Tip**: Use \`mcp__ccw-tools__core_memory({ action: "search", query: "<keyword>" })\` to find more sessions
|
|
1010
|
+
</ccw-session-context>`;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Parse workflow session files
|
|
1014
|
+
*/
|
|
1015
|
+
async parseWorkflowSessions() {
|
|
1016
|
+
const sessions = [];
|
|
1017
|
+
const workflowDir = join(this.projectPath, '.workflow', 'sessions');
|
|
1018
|
+
if (!existsSync(workflowDir)) {
|
|
1019
|
+
return sessions;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const sessionDirs = readdirSync(workflowDir).filter(d => d.startsWith('WFS-'));
|
|
1023
|
+
for (const sessionDir of sessionDirs) {
|
|
1024
|
+
const sessionFile = join(workflowDir, sessionDir, 'session.json');
|
|
1025
|
+
if (!existsSync(sessionFile))
|
|
1026
|
+
continue;
|
|
1027
|
+
try {
|
|
1028
|
+
const content = readFileSync(sessionFile, 'utf8');
|
|
1029
|
+
const sessionData = JSON.parse(content);
|
|
1030
|
+
const metadata = {
|
|
1031
|
+
session_id: sessionDir,
|
|
1032
|
+
session_type: 'workflow',
|
|
1033
|
+
title: sessionData.title || sessionDir,
|
|
1034
|
+
summary: (sessionData.description || '').substring(0, 200),
|
|
1035
|
+
keywords: this.extractKeywords(JSON.stringify(sessionData)),
|
|
1036
|
+
token_estimate: Math.ceil(JSON.stringify(sessionData).length / 4),
|
|
1037
|
+
file_patterns: this.extractFilePatterns(JSON.stringify(sessionData)),
|
|
1038
|
+
created_at: sessionData.created_at || statSync(sessionFile).mtime.toISOString(),
|
|
1039
|
+
last_accessed: new Date().toISOString(),
|
|
1040
|
+
access_count: 0
|
|
1041
|
+
};
|
|
1042
|
+
sessions.push(metadata);
|
|
1043
|
+
}
|
|
1044
|
+
catch (err) {
|
|
1045
|
+
console.warn(`[Clustering] Failed to parse ${sessionFile}:`, err);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
catch (err) {
|
|
1050
|
+
console.warn('[Clustering] Failed to read workflow sessions:', err);
|
|
1051
|
+
}
|
|
1052
|
+
return sessions;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Update metadata cache for all sessions
|
|
1056
|
+
*/
|
|
1057
|
+
async refreshMetadataCache() {
|
|
1058
|
+
const sessions = await this.collectSessions({ scope: 'all' });
|
|
1059
|
+
for (const session of sessions) {
|
|
1060
|
+
this.coreMemoryStore.upsertSessionMetadata(session);
|
|
1061
|
+
}
|
|
1062
|
+
return sessions.length;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
//# sourceMappingURL=session-clustering-service.js.map
|