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,1753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Search Tool - Unified intelligent search with CodexLens integration
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Intent classification with automatic mode selection
|
|
6
|
+
* - CodexLens integration (init, hybrid, vector, semantic)
|
|
7
|
+
* - Ripgrep fallback for exact mode
|
|
8
|
+
* - Index status checking and warnings
|
|
9
|
+
* - Multi-backend search routing with RRF ranking
|
|
10
|
+
*
|
|
11
|
+
* Actions:
|
|
12
|
+
* - init: Initialize CodexLens index
|
|
13
|
+
* - search: Intelligent search with auto mode selection
|
|
14
|
+
* - status: Check index status
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { spawn, execSync } from 'child_process';
|
|
18
|
+
import { ensureReady as ensureCodexLensReady, executeCodexLens, } from './codex-lens.js';
|
|
19
|
+
import { getProjectRoot } from '../utils/path-validator.js';
|
|
20
|
+
// Define Zod schema for validation
|
|
21
|
+
const ParamsSchema = z.object({
|
|
22
|
+
// Action: search (content), find_files (path/name pattern), init, status
|
|
23
|
+
// Note: search_files is deprecated, use search with output_mode='files_only'
|
|
24
|
+
action: z.enum(['init', 'search', 'search_files', 'find_files', 'status']).default('search'),
|
|
25
|
+
query: z.string().optional().describe('Content search query (for action="search")'),
|
|
26
|
+
pattern: z.string().optional().describe('Glob pattern for path matching (for action="find_files")'),
|
|
27
|
+
mode: z.enum(['auto', 'hybrid', 'exact', 'ripgrep', 'priority']).default('auto'),
|
|
28
|
+
output_mode: z.enum(['full', 'files_only', 'count']).default('full'),
|
|
29
|
+
path: z.string().optional(),
|
|
30
|
+
paths: z.array(z.string()).default([]),
|
|
31
|
+
contextLines: z.number().default(0),
|
|
32
|
+
maxResults: z.number().default(20), // Increased default
|
|
33
|
+
includeHidden: z.boolean().default(false),
|
|
34
|
+
languages: z.array(z.string()).optional(),
|
|
35
|
+
limit: z.number().default(20), // Increased default
|
|
36
|
+
offset: z.number().default(0), // NEW: Pagination offset (start_index)
|
|
37
|
+
enrich: z.boolean().default(false),
|
|
38
|
+
// Search modifiers for ripgrep mode
|
|
39
|
+
regex: z.boolean().default(true), // Use regex pattern matching (default: enabled)
|
|
40
|
+
caseSensitive: z.boolean().default(true), // Case sensitivity (default: case-sensitive)
|
|
41
|
+
tokenize: z.boolean().default(true), // Tokenize multi-word queries for OR matching (default: enabled)
|
|
42
|
+
// Fuzzy matching is implicit in hybrid mode (RRF fusion)
|
|
43
|
+
});
|
|
44
|
+
// Search mode constants
|
|
45
|
+
const SEARCH_MODES = ['auto', 'hybrid', 'exact', 'ripgrep', 'priority'];
|
|
46
|
+
// Classification confidence threshold
|
|
47
|
+
const CONFIDENCE_THRESHOLD = 0.7;
|
|
48
|
+
// File filtering configuration (ported from code-index)
|
|
49
|
+
const FILTER_CONFIG = {
|
|
50
|
+
exclude_directories: new Set([
|
|
51
|
+
'.git', '.svn', '.hg', '.bzr',
|
|
52
|
+
'node_modules', '__pycache__', '.venv', 'venv', 'vendor', 'bower_components',
|
|
53
|
+
'dist', 'build', 'target', 'out', 'bin', 'obj',
|
|
54
|
+
'.idea', '.vscode', '.vs', '.sublime-workspace',
|
|
55
|
+
'.pytest_cache', '.coverage', '.tox', '.nyc_output', 'coverage', 'htmlcov',
|
|
56
|
+
'.next', '.nuxt', '.cache', '.parcel-cache',
|
|
57
|
+
'.DS_Store', 'Thumbs.db',
|
|
58
|
+
]),
|
|
59
|
+
exclude_files: new Set([
|
|
60
|
+
'*.tmp', '*.temp', '*.swp', '*.swo', '*.bak', '*~', '*.orig', '*.log',
|
|
61
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Pipfile.lock',
|
|
62
|
+
]),
|
|
63
|
+
// Windows device files - must use **/ pattern to match in any directory
|
|
64
|
+
// These cause "os error 1" on Windows when accessed
|
|
65
|
+
windows_device_files: new Set([
|
|
66
|
+
'nul', 'con', 'aux', 'prn',
|
|
67
|
+
'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
|
|
68
|
+
'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
|
|
69
|
+
]),
|
|
70
|
+
};
|
|
71
|
+
function buildExcludeArgs() {
|
|
72
|
+
const args = [];
|
|
73
|
+
for (const dir of FILTER_CONFIG.exclude_directories) {
|
|
74
|
+
args.push('--glob', `!**/${dir}/**`);
|
|
75
|
+
}
|
|
76
|
+
for (const pattern of FILTER_CONFIG.exclude_files) {
|
|
77
|
+
args.push('--glob', `!${pattern}`);
|
|
78
|
+
}
|
|
79
|
+
// Windows device files need case-insensitive matching in any directory
|
|
80
|
+
for (const device of FILTER_CONFIG.windows_device_files) {
|
|
81
|
+
args.push('--glob', `!**/${device}`);
|
|
82
|
+
args.push('--glob', `!**/${device.toUpperCase()}`);
|
|
83
|
+
}
|
|
84
|
+
return args;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Tokenize query for multi-word OR matching
|
|
88
|
+
* Splits on whitespace and common delimiters, filters stop words and short tokens
|
|
89
|
+
* @param query - The search query
|
|
90
|
+
* @returns Array of tokens
|
|
91
|
+
*/
|
|
92
|
+
function tokenizeQuery(query) {
|
|
93
|
+
// Stop words for filtering (common English + programming keywords)
|
|
94
|
+
const stopWords = new Set([
|
|
95
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
96
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
97
|
+
'should', 'may', 'might', 'must', 'can', 'to', 'of', 'in', 'for', 'on',
|
|
98
|
+
'with', 'at', 'by', 'from', 'as', 'into', 'through', 'and', 'but', 'if',
|
|
99
|
+
'or', 'not', 'this', 'that', 'these', 'those', 'it', 'its', 'how', 'what',
|
|
100
|
+
'where', 'when', 'why', 'which', 'who', 'whom',
|
|
101
|
+
]);
|
|
102
|
+
// Split on whitespace and common delimiters, keep meaningful tokens
|
|
103
|
+
const tokens = query
|
|
104
|
+
.split(/[\s,;:]+/)
|
|
105
|
+
.map(token => token.trim())
|
|
106
|
+
.filter(token => {
|
|
107
|
+
// Keep tokens that are:
|
|
108
|
+
// - At least 2 characters long
|
|
109
|
+
// - Not a stop word (case-insensitive)
|
|
110
|
+
// - Or look like identifiers (contain underscore/camelCase)
|
|
111
|
+
if (token.length < 2)
|
|
112
|
+
return false;
|
|
113
|
+
if (stopWords.has(token.toLowerCase()) && !token.includes('_') && !/[A-Z]/.test(token)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
return tokens;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Score results based on token match count for ranking
|
|
122
|
+
* @param results - Search results
|
|
123
|
+
* @param tokens - Query tokens
|
|
124
|
+
* @returns Results with match scores
|
|
125
|
+
*/
|
|
126
|
+
function scoreByTokenMatch(results, tokens) {
|
|
127
|
+
if (tokens.length <= 1)
|
|
128
|
+
return results;
|
|
129
|
+
// Create case-insensitive patterns for each token
|
|
130
|
+
const tokenPatterns = tokens.map(t => {
|
|
131
|
+
const escaped = t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
132
|
+
return new RegExp(escaped, 'i');
|
|
133
|
+
});
|
|
134
|
+
return results.map(r => {
|
|
135
|
+
const content = r.content || '';
|
|
136
|
+
const file = r.file || '';
|
|
137
|
+
const searchText = `${file} ${content}`;
|
|
138
|
+
// Count how many tokens match
|
|
139
|
+
let matchCount = 0;
|
|
140
|
+
for (const pattern of tokenPatterns) {
|
|
141
|
+
if (pattern.test(searchText)) {
|
|
142
|
+
matchCount++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Calculate match ratio (0 to 1)
|
|
146
|
+
const matchRatio = matchCount / tokens.length;
|
|
147
|
+
return {
|
|
148
|
+
...r,
|
|
149
|
+
matchScore: matchRatio,
|
|
150
|
+
matchCount,
|
|
151
|
+
};
|
|
152
|
+
}).sort((a, b) => {
|
|
153
|
+
// Sort by match ratio (descending), then by line number
|
|
154
|
+
if (b.matchScore !== a.matchScore) {
|
|
155
|
+
return b.matchScore - a.matchScore;
|
|
156
|
+
}
|
|
157
|
+
return (a.line || 0) - (b.line || 0);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Strip ANSI color codes from string (for JSON parsing)
|
|
162
|
+
*/
|
|
163
|
+
function stripAnsi(str) {
|
|
164
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if CodexLens index exists for current directory
|
|
168
|
+
* @param path - Directory path to check
|
|
169
|
+
* @returns Index status
|
|
170
|
+
*/
|
|
171
|
+
async function checkIndexStatus(path = '.') {
|
|
172
|
+
try {
|
|
173
|
+
const result = await executeCodexLens(['status', '--json'], { cwd: path });
|
|
174
|
+
if (!result.success) {
|
|
175
|
+
return {
|
|
176
|
+
indexed: false,
|
|
177
|
+
has_embeddings: false,
|
|
178
|
+
warning: 'No CodexLens index found. Run smart_search(action="init") to create index for better search results.',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Parse status output
|
|
182
|
+
try {
|
|
183
|
+
// Strip ANSI color codes from JSON output
|
|
184
|
+
const cleanOutput = stripAnsi(result.output || '{}');
|
|
185
|
+
const parsed = JSON.parse(cleanOutput);
|
|
186
|
+
// Handle both direct and nested response formats (status returns {success, result: {...}})
|
|
187
|
+
const status = parsed.result || parsed;
|
|
188
|
+
const indexed = status.projects_count > 0 || status.total_files > 0;
|
|
189
|
+
// Get embeddings coverage from comprehensive status
|
|
190
|
+
const embeddingsData = status.embeddings || {};
|
|
191
|
+
const embeddingsCoverage = embeddingsData.coverage_percent || 0;
|
|
192
|
+
const has_embeddings = embeddingsCoverage >= 50; // Threshold: 50%
|
|
193
|
+
let warning;
|
|
194
|
+
if (!indexed) {
|
|
195
|
+
warning = 'No CodexLens index found. Run smart_search(action="init") to create index for better search results.';
|
|
196
|
+
}
|
|
197
|
+
else if (embeddingsCoverage === 0) {
|
|
198
|
+
warning = 'Index exists but no embeddings generated. Run: codexlens embeddings-generate --recursive';
|
|
199
|
+
}
|
|
200
|
+
else if (embeddingsCoverage < 50) {
|
|
201
|
+
warning = `Embeddings coverage is ${embeddingsCoverage.toFixed(1)}% (below 50%). Hybrid search will use exact mode. Run: codexlens embeddings-generate --recursive`;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
indexed,
|
|
205
|
+
has_embeddings,
|
|
206
|
+
file_count: status.total_files,
|
|
207
|
+
embeddings_coverage_percent: embeddingsCoverage,
|
|
208
|
+
warning,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return {
|
|
213
|
+
indexed: false,
|
|
214
|
+
has_embeddings: false,
|
|
215
|
+
warning: 'Failed to parse index status',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return {
|
|
221
|
+
indexed: false,
|
|
222
|
+
has_embeddings: false,
|
|
223
|
+
warning: 'CodexLens not available',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Detection heuristics for intent classification
|
|
229
|
+
*/
|
|
230
|
+
/**
|
|
231
|
+
* Detect literal string query (simple alphanumeric or quoted strings)
|
|
232
|
+
*/
|
|
233
|
+
function detectLiteral(query) {
|
|
234
|
+
return /^[a-zA-Z0-9_-]+$/.test(query) || /^["'].*["']$/.test(query);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Detect regex pattern (contains regex metacharacters)
|
|
238
|
+
*/
|
|
239
|
+
function detectRegex(query) {
|
|
240
|
+
return /[.*+?^${}()|[\]\\]/.test(query);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Detect natural language query (sentence structure, questions, multi-word phrases)
|
|
244
|
+
*/
|
|
245
|
+
function detectNaturalLanguage(query) {
|
|
246
|
+
return query.split(/\s+/).length >= 3 || /\?$/.test(query);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Detect file path query (path separators, file extensions)
|
|
250
|
+
*/
|
|
251
|
+
function detectFilePath(query) {
|
|
252
|
+
return /[/\\]/.test(query) || /\.[a-z]{2,4}$/i.test(query);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Detect relationship query (import, export, dependency keywords)
|
|
256
|
+
*/
|
|
257
|
+
function detectRelationship(query) {
|
|
258
|
+
return /(import|export|uses?|depends?|calls?|extends?)\s/i.test(query);
|
|
259
|
+
}
|
|
260
|
+
function looksLikeCodeQuery(query) {
|
|
261
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(query))
|
|
262
|
+
return true;
|
|
263
|
+
if (/[:.<>\-=(){}[\]]/.test(query) && query.split(/\s+/).length <= 2)
|
|
264
|
+
return true;
|
|
265
|
+
if (/\.\*|\\\(|\\\[|\\s/.test(query))
|
|
266
|
+
return true;
|
|
267
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/.test(query))
|
|
268
|
+
return true;
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Classify query intent and recommend search mode
|
|
273
|
+
* Simple mapping: hybrid (NL + index + embeddings) | exact (index or insufficient embeddings) | ripgrep (no index)
|
|
274
|
+
* @param query - Search query string
|
|
275
|
+
* @param hasIndex - Whether CodexLens index exists
|
|
276
|
+
* @param hasSufficientEmbeddings - Whether embeddings coverage >= 50%
|
|
277
|
+
* @returns Classification result
|
|
278
|
+
*/
|
|
279
|
+
function classifyIntent(query, hasIndex = false, hasSufficientEmbeddings = false) {
|
|
280
|
+
const isNaturalLanguage = detectNaturalLanguage(query);
|
|
281
|
+
const isCodeQuery = looksLikeCodeQuery(query);
|
|
282
|
+
const isRegexPattern = detectRegex(query);
|
|
283
|
+
let mode;
|
|
284
|
+
let confidence;
|
|
285
|
+
if (!hasIndex) {
|
|
286
|
+
mode = 'ripgrep';
|
|
287
|
+
confidence = 1.0;
|
|
288
|
+
}
|
|
289
|
+
else if (isCodeQuery || isRegexPattern) {
|
|
290
|
+
mode = 'exact';
|
|
291
|
+
confidence = 0.95;
|
|
292
|
+
}
|
|
293
|
+
else if (isNaturalLanguage && hasSufficientEmbeddings) {
|
|
294
|
+
mode = 'hybrid';
|
|
295
|
+
confidence = 0.9;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
mode = 'exact';
|
|
299
|
+
confidence = 0.8;
|
|
300
|
+
}
|
|
301
|
+
const detectedPatterns = [];
|
|
302
|
+
if (detectLiteral(query))
|
|
303
|
+
detectedPatterns.push('literal');
|
|
304
|
+
if (detectRegex(query))
|
|
305
|
+
detectedPatterns.push('regex');
|
|
306
|
+
if (detectNaturalLanguage(query))
|
|
307
|
+
detectedPatterns.push('natural language');
|
|
308
|
+
if (detectFilePath(query))
|
|
309
|
+
detectedPatterns.push('file path');
|
|
310
|
+
if (detectRelationship(query))
|
|
311
|
+
detectedPatterns.push('relationship');
|
|
312
|
+
if (isCodeQuery)
|
|
313
|
+
detectedPatterns.push('code identifier');
|
|
314
|
+
const reasoning = `Query classified as ${mode} (confidence: ${confidence.toFixed(2)}, detected: ${detectedPatterns.join(', ')}, index: ${hasIndex ? 'available' : 'not available'}, embeddings: ${hasSufficientEmbeddings ? 'sufficient' : 'insufficient'})`;
|
|
315
|
+
return { mode, confidence, reasoning };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Check if a tool is available in PATH
|
|
319
|
+
* @param toolName - Tool executable name
|
|
320
|
+
* @returns True if available
|
|
321
|
+
*/
|
|
322
|
+
function checkToolAvailability(toolName) {
|
|
323
|
+
try {
|
|
324
|
+
const isWindows = process.platform === 'win32';
|
|
325
|
+
const command = isWindows ? 'where' : 'which';
|
|
326
|
+
execSync(`${command} ${toolName}`, { stdio: 'ignore' });
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Build ripgrep command arguments
|
|
335
|
+
* Supports tokenized multi-word queries with OR matching
|
|
336
|
+
* @param params - Search parameters
|
|
337
|
+
* @returns Command, arguments, and tokens used
|
|
338
|
+
*/
|
|
339
|
+
function buildRipgrepCommand(params) {
|
|
340
|
+
const { query, paths = ['.'], contextLines = 0, maxResults = 10, includeHidden = false, regex = false, caseSensitive = true, tokenize = true } = params;
|
|
341
|
+
const args = [
|
|
342
|
+
'-n',
|
|
343
|
+
'--color=never',
|
|
344
|
+
'--json',
|
|
345
|
+
];
|
|
346
|
+
// Add file filtering (unless includeHidden is true)
|
|
347
|
+
if (!includeHidden) {
|
|
348
|
+
args.push(...buildExcludeArgs());
|
|
349
|
+
}
|
|
350
|
+
// Case sensitivity
|
|
351
|
+
if (!caseSensitive) {
|
|
352
|
+
args.push('--ignore-case');
|
|
353
|
+
}
|
|
354
|
+
if (contextLines > 0) {
|
|
355
|
+
args.push('-C', contextLines.toString());
|
|
356
|
+
}
|
|
357
|
+
if (maxResults > 0) {
|
|
358
|
+
args.push('--max-count', maxResults.toString());
|
|
359
|
+
}
|
|
360
|
+
if (includeHidden) {
|
|
361
|
+
args.push('--hidden');
|
|
362
|
+
}
|
|
363
|
+
// Tokenize query for multi-word OR matching
|
|
364
|
+
const tokens = tokenize ? tokenizeQuery(query) : [query];
|
|
365
|
+
if (tokens.length > 1) {
|
|
366
|
+
// Multi-token: use multiple -e patterns (OR matching)
|
|
367
|
+
// Each token is escaped for regex safety unless regex mode is enabled
|
|
368
|
+
for (const token of tokens) {
|
|
369
|
+
if (regex) {
|
|
370
|
+
args.push('-e', token);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// Escape regex special chars for literal matching
|
|
374
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
375
|
+
args.push('-e', escaped);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Single token or no tokenization: use original behavior
|
|
381
|
+
if (regex) {
|
|
382
|
+
args.push('-e', query);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
args.push('-F', query);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
args.push(...paths);
|
|
389
|
+
return { command: 'rg', args, tokens };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Action: init - Initialize CodexLens index (FTS only, no embeddings)
|
|
393
|
+
* For semantic/vector search, use ccw view dashboard or codexlens CLI directly
|
|
394
|
+
*/
|
|
395
|
+
async function executeInitAction(params) {
|
|
396
|
+
const { path = '.', languages } = params;
|
|
397
|
+
// Check CodexLens availability
|
|
398
|
+
const readyStatus = await ensureCodexLensReady();
|
|
399
|
+
if (!readyStatus.ready) {
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
error: `CodexLens not available: ${readyStatus.error}. CodexLens will be auto-installed on first use.`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
// Build args with --no-embeddings for FTS-only index (faster)
|
|
406
|
+
const args = ['init', path, '--no-embeddings'];
|
|
407
|
+
if (languages && languages.length > 0) {
|
|
408
|
+
args.push('--languages', languages.join(','));
|
|
409
|
+
}
|
|
410
|
+
// Track progress updates
|
|
411
|
+
const progressUpdates = [];
|
|
412
|
+
let lastProgress = null;
|
|
413
|
+
const result = await executeCodexLens(args, {
|
|
414
|
+
cwd: path,
|
|
415
|
+
timeout: 1800000, // 30 minutes for large codebases
|
|
416
|
+
onProgress: (progress) => {
|
|
417
|
+
progressUpdates.push(progress);
|
|
418
|
+
lastProgress = progress;
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
// Build metadata with progress info
|
|
422
|
+
const metadata = {
|
|
423
|
+
action: 'init',
|
|
424
|
+
path,
|
|
425
|
+
};
|
|
426
|
+
if (lastProgress !== null) {
|
|
427
|
+
const p = lastProgress;
|
|
428
|
+
metadata.progress = {
|
|
429
|
+
stage: p.stage,
|
|
430
|
+
message: p.message,
|
|
431
|
+
percent: p.percent,
|
|
432
|
+
filesProcessed: p.filesProcessed,
|
|
433
|
+
totalFiles: p.totalFiles,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (progressUpdates.length > 0) {
|
|
437
|
+
metadata.progressHistory = progressUpdates.slice(-5); // Keep last 5 progress updates
|
|
438
|
+
}
|
|
439
|
+
const successMessage = result.success
|
|
440
|
+
? `FTS index created for ${path}. Note: For semantic/vector search, create vector index via "ccw view" dashboard or run "codexlens init ${path}" (without --no-embeddings).`
|
|
441
|
+
: undefined;
|
|
442
|
+
return {
|
|
443
|
+
success: result.success,
|
|
444
|
+
error: result.error,
|
|
445
|
+
message: successMessage,
|
|
446
|
+
metadata,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Action: status - Check CodexLens index status
|
|
451
|
+
*/
|
|
452
|
+
async function executeStatusAction(params) {
|
|
453
|
+
const { path = '.' } = params;
|
|
454
|
+
const indexStatus = await checkIndexStatus(path);
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
status: indexStatus,
|
|
458
|
+
message: indexStatus.warning || `Index status: ${indexStatus.indexed ? 'indexed' : 'not indexed'}, embeddings: ${indexStatus.has_embeddings ? 'available' : 'not available'}`,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Mode: auto - Intent classification and mode selection
|
|
463
|
+
* Routes to: hybrid (NL + index) | exact (index) | ripgrep (no index)
|
|
464
|
+
*/
|
|
465
|
+
async function executeAutoMode(params) {
|
|
466
|
+
const { query, path = '.' } = params;
|
|
467
|
+
if (!query) {
|
|
468
|
+
return {
|
|
469
|
+
success: false,
|
|
470
|
+
error: 'Query is required for search action',
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
// Check index status
|
|
474
|
+
const indexStatus = await checkIndexStatus(path);
|
|
475
|
+
// Classify intent with index and embeddings awareness
|
|
476
|
+
const classification = classifyIntent(query, indexStatus.indexed, indexStatus.has_embeddings // This now considers 50% threshold
|
|
477
|
+
);
|
|
478
|
+
// Route to appropriate mode based on classification
|
|
479
|
+
let result;
|
|
480
|
+
switch (classification.mode) {
|
|
481
|
+
case 'hybrid':
|
|
482
|
+
result = await executeHybridMode(params);
|
|
483
|
+
break;
|
|
484
|
+
case 'exact':
|
|
485
|
+
result = await executeCodexLensExactMode(params);
|
|
486
|
+
break;
|
|
487
|
+
case 'ripgrep':
|
|
488
|
+
result = await executeRipgrepMode(params);
|
|
489
|
+
break;
|
|
490
|
+
default:
|
|
491
|
+
// Fallback to ripgrep
|
|
492
|
+
result = await executeRipgrepMode(params);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
// Add classification metadata
|
|
496
|
+
if (result.metadata) {
|
|
497
|
+
result.metadata.classified_as = classification.mode;
|
|
498
|
+
result.metadata.confidence = classification.confidence;
|
|
499
|
+
result.metadata.reasoning = classification.reasoning;
|
|
500
|
+
result.metadata.embeddings_coverage_percent = indexStatus.embeddings_coverage_percent;
|
|
501
|
+
result.metadata.index_status = indexStatus.indexed
|
|
502
|
+
? (indexStatus.has_embeddings ? 'indexed' : 'partial')
|
|
503
|
+
: 'not_indexed';
|
|
504
|
+
// Add warning if needed
|
|
505
|
+
if (indexStatus.warning) {
|
|
506
|
+
result.metadata.warning = indexStatus.warning;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Mode: ripgrep - Fast literal string matching using ripgrep
|
|
513
|
+
* No index required, fallback to CodexLens if ripgrep unavailable
|
|
514
|
+
* Supports tokenized multi-word queries with OR matching and result ranking
|
|
515
|
+
*/
|
|
516
|
+
async function executeRipgrepMode(params) {
|
|
517
|
+
const { query, paths = [], contextLines = 0, maxResults = 10, includeHidden = false, path = '.', regex = true, caseSensitive = true, tokenize = true } = params;
|
|
518
|
+
if (!query) {
|
|
519
|
+
return {
|
|
520
|
+
success: false,
|
|
521
|
+
error: 'Query is required for search',
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Check if ripgrep is available
|
|
525
|
+
const hasRipgrep = checkToolAvailability('rg');
|
|
526
|
+
// If ripgrep not available, fall back to CodexLens exact mode
|
|
527
|
+
if (!hasRipgrep) {
|
|
528
|
+
const readyStatus = await ensureCodexLensReady();
|
|
529
|
+
if (!readyStatus.ready) {
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
error: 'Neither ripgrep nor CodexLens available. Install ripgrep (rg) or CodexLens for search functionality.',
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
// Use CodexLens exact mode as fallback
|
|
536
|
+
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'exact', '--json'];
|
|
537
|
+
const result = await executeCodexLens(args, { cwd: path });
|
|
538
|
+
if (!result.success) {
|
|
539
|
+
return {
|
|
540
|
+
success: false,
|
|
541
|
+
error: result.error,
|
|
542
|
+
metadata: {
|
|
543
|
+
mode: 'ripgrep',
|
|
544
|
+
backend: 'codexlens-fallback',
|
|
545
|
+
count: 0,
|
|
546
|
+
query,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// Parse results
|
|
551
|
+
let results = [];
|
|
552
|
+
try {
|
|
553
|
+
const parsed = JSON.parse(stripAnsi(result.output || '{}'));
|
|
554
|
+
const data = parsed.result?.results || parsed.results || parsed;
|
|
555
|
+
results = (Array.isArray(data) ? data : []).map((item) => ({
|
|
556
|
+
file: item.path || item.file,
|
|
557
|
+
score: item.score || 0,
|
|
558
|
+
content: item.excerpt || item.content || '',
|
|
559
|
+
symbol: item.symbol || null,
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Keep empty results
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
success: true,
|
|
567
|
+
results,
|
|
568
|
+
metadata: {
|
|
569
|
+
mode: 'ripgrep',
|
|
570
|
+
backend: 'codexlens-fallback',
|
|
571
|
+
count: results.length,
|
|
572
|
+
query,
|
|
573
|
+
note: 'Using CodexLens exact mode (ripgrep not available)',
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
// Use ripgrep
|
|
578
|
+
const { command, args, tokens } = buildRipgrepCommand({
|
|
579
|
+
query,
|
|
580
|
+
paths: paths.length > 0 ? paths : [path],
|
|
581
|
+
contextLines,
|
|
582
|
+
maxResults,
|
|
583
|
+
includeHidden,
|
|
584
|
+
regex,
|
|
585
|
+
caseSensitive,
|
|
586
|
+
tokenize,
|
|
587
|
+
});
|
|
588
|
+
return new Promise((resolve) => {
|
|
589
|
+
const child = spawn(command, args, {
|
|
590
|
+
cwd: path || getProjectRoot(),
|
|
591
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
592
|
+
});
|
|
593
|
+
let stdout = '';
|
|
594
|
+
let stderr = '';
|
|
595
|
+
let resultLimitReached = false;
|
|
596
|
+
child.stdout.on('data', (data) => {
|
|
597
|
+
stdout += data.toString();
|
|
598
|
+
});
|
|
599
|
+
child.stderr.on('data', (data) => {
|
|
600
|
+
stderr += data.toString();
|
|
601
|
+
});
|
|
602
|
+
child.on('close', (code) => {
|
|
603
|
+
const results = [];
|
|
604
|
+
const lines = stdout.split('\n').filter((line) => line.trim());
|
|
605
|
+
// Limit total results to prevent memory overflow (--max-count only limits per-file)
|
|
606
|
+
const effectiveLimit = maxResults > 0 ? maxResults : 500;
|
|
607
|
+
for (const line of lines) {
|
|
608
|
+
// Stop collecting if we've reached the limit
|
|
609
|
+
if (results.length >= effectiveLimit) {
|
|
610
|
+
resultLimitReached = true;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const item = JSON.parse(line);
|
|
615
|
+
if (item.type === 'match') {
|
|
616
|
+
const match = {
|
|
617
|
+
file: item.data.path.text,
|
|
618
|
+
line: item.data.line_number,
|
|
619
|
+
column: item.data.submatches && item.data.submatches[0]
|
|
620
|
+
? item.data.submatches[0].start + 1
|
|
621
|
+
: 1,
|
|
622
|
+
content: item.data.lines.text.trim(),
|
|
623
|
+
};
|
|
624
|
+
results.push(match);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Handle Windows device file errors gracefully (os error 1)
|
|
632
|
+
// If we have results despite the error, return them as partial success
|
|
633
|
+
const isWindowsDeviceError = stderr.includes('os error 1') || stderr.includes('函数不正确');
|
|
634
|
+
// Apply token-based scoring and sorting for multi-word queries
|
|
635
|
+
// Results matching more tokens are ranked higher (exact matches first)
|
|
636
|
+
const scoredResults = tokens.length > 1 ? scoreByTokenMatch(results, tokens) : results;
|
|
637
|
+
if (code === 0 || code === 1 || (isWindowsDeviceError && scoredResults.length > 0)) {
|
|
638
|
+
// Build warning message for various conditions
|
|
639
|
+
const warnings = [];
|
|
640
|
+
if (resultLimitReached) {
|
|
641
|
+
warnings.push(`Result limit reached (${effectiveLimit}). Use a more specific query or increase limit.`);
|
|
642
|
+
}
|
|
643
|
+
if (isWindowsDeviceError) {
|
|
644
|
+
warnings.push('Some Windows device files were skipped');
|
|
645
|
+
}
|
|
646
|
+
resolve({
|
|
647
|
+
success: true,
|
|
648
|
+
results: scoredResults,
|
|
649
|
+
metadata: {
|
|
650
|
+
mode: 'ripgrep',
|
|
651
|
+
backend: 'ripgrep',
|
|
652
|
+
count: scoredResults.length,
|
|
653
|
+
query,
|
|
654
|
+
tokens: tokens.length > 1 ? tokens : undefined, // Include tokens in metadata for debugging
|
|
655
|
+
tokenized: tokens.length > 1,
|
|
656
|
+
...(warnings.length > 0 && { warning: warnings.join('; ') }),
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
else if (isWindowsDeviceError && results.length === 0) {
|
|
661
|
+
// Windows device error but no results - might be the only issue
|
|
662
|
+
resolve({
|
|
663
|
+
success: true,
|
|
664
|
+
results: [],
|
|
665
|
+
metadata: {
|
|
666
|
+
mode: 'ripgrep',
|
|
667
|
+
backend: 'ripgrep',
|
|
668
|
+
count: 0,
|
|
669
|
+
query,
|
|
670
|
+
warning: 'No matches found (some Windows device files were skipped)',
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
resolve({
|
|
676
|
+
success: false,
|
|
677
|
+
error: `ripgrep execution failed with code ${code}: ${stderr}`,
|
|
678
|
+
results: [],
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
child.on('error', (error) => {
|
|
683
|
+
resolve({
|
|
684
|
+
success: false,
|
|
685
|
+
error: `Failed to spawn ripgrep: ${error.message}`,
|
|
686
|
+
results: [],
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Mode: exact - CodexLens exact/FTS search
|
|
693
|
+
* Requires index
|
|
694
|
+
*/
|
|
695
|
+
async function executeCodexLensExactMode(params) {
|
|
696
|
+
const { query, path = '.', maxResults = 10, enrich = false } = params;
|
|
697
|
+
if (!query) {
|
|
698
|
+
return {
|
|
699
|
+
success: false,
|
|
700
|
+
error: 'Query is required for search',
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
// Check CodexLens availability
|
|
704
|
+
const readyStatus = await ensureCodexLensReady();
|
|
705
|
+
if (!readyStatus.ready) {
|
|
706
|
+
return {
|
|
707
|
+
success: false,
|
|
708
|
+
error: `CodexLens not available: ${readyStatus.error}`,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
// Check index status
|
|
712
|
+
const indexStatus = await checkIndexStatus(path);
|
|
713
|
+
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'exact', '--json'];
|
|
714
|
+
if (enrich) {
|
|
715
|
+
args.push('--enrich');
|
|
716
|
+
}
|
|
717
|
+
const result = await executeCodexLens(args, { cwd: path });
|
|
718
|
+
if (!result.success) {
|
|
719
|
+
return {
|
|
720
|
+
success: false,
|
|
721
|
+
error: result.error,
|
|
722
|
+
metadata: {
|
|
723
|
+
mode: 'exact',
|
|
724
|
+
backend: 'codexlens',
|
|
725
|
+
count: 0,
|
|
726
|
+
query,
|
|
727
|
+
warning: indexStatus.warning,
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
// Parse results
|
|
732
|
+
let results = [];
|
|
733
|
+
try {
|
|
734
|
+
const parsed = JSON.parse(stripAnsi(result.output || '{}'));
|
|
735
|
+
const data = parsed.result?.results || parsed.results || parsed;
|
|
736
|
+
results = (Array.isArray(data) ? data : []).map((item) => ({
|
|
737
|
+
file: item.path || item.file,
|
|
738
|
+
score: item.score || 0,
|
|
739
|
+
content: item.excerpt || item.content || '',
|
|
740
|
+
symbol: item.symbol || null,
|
|
741
|
+
}));
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
// Keep empty results
|
|
745
|
+
}
|
|
746
|
+
// Fallback to fuzzy mode if exact returns no results
|
|
747
|
+
if (results.length === 0) {
|
|
748
|
+
const fuzzyArgs = ['search', query, '--limit', maxResults.toString(), '--mode', 'fuzzy', '--json'];
|
|
749
|
+
if (enrich) {
|
|
750
|
+
fuzzyArgs.push('--enrich');
|
|
751
|
+
}
|
|
752
|
+
const fuzzyResult = await executeCodexLens(fuzzyArgs, { cwd: path });
|
|
753
|
+
if (fuzzyResult.success) {
|
|
754
|
+
try {
|
|
755
|
+
const parsed = JSON.parse(stripAnsi(fuzzyResult.output || '{}'));
|
|
756
|
+
const data = parsed.result?.results || parsed.results || parsed;
|
|
757
|
+
results = (Array.isArray(data) ? data : []).map((item) => ({
|
|
758
|
+
file: item.path || item.file,
|
|
759
|
+
score: item.score || 0,
|
|
760
|
+
content: item.excerpt || item.content || '',
|
|
761
|
+
symbol: item.symbol || null,
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// Keep empty results
|
|
766
|
+
}
|
|
767
|
+
if (results.length > 0) {
|
|
768
|
+
return {
|
|
769
|
+
success: true,
|
|
770
|
+
results,
|
|
771
|
+
metadata: {
|
|
772
|
+
mode: 'exact',
|
|
773
|
+
backend: 'codexlens',
|
|
774
|
+
count: results.length,
|
|
775
|
+
query,
|
|
776
|
+
warning: indexStatus.warning,
|
|
777
|
+
note: 'No exact matches found, showing fuzzy results',
|
|
778
|
+
fallback: 'fuzzy',
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
success: true,
|
|
786
|
+
results,
|
|
787
|
+
metadata: {
|
|
788
|
+
mode: 'exact',
|
|
789
|
+
backend: 'codexlens',
|
|
790
|
+
count: results.length,
|
|
791
|
+
query,
|
|
792
|
+
warning: indexStatus.warning,
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Mode: hybrid - Best quality search with RRF fusion
|
|
798
|
+
* Uses CodexLens hybrid mode (exact + fuzzy + vector)
|
|
799
|
+
* Requires index with embeddings
|
|
800
|
+
*/
|
|
801
|
+
async function executeHybridMode(params) {
|
|
802
|
+
const { query, path = '.', maxResults = 10, enrich = false } = params;
|
|
803
|
+
if (!query) {
|
|
804
|
+
return {
|
|
805
|
+
success: false,
|
|
806
|
+
error: 'Query is required for search',
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
// Check CodexLens availability
|
|
810
|
+
const readyStatus = await ensureCodexLensReady();
|
|
811
|
+
if (!readyStatus.ready) {
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
error: `CodexLens not available: ${readyStatus.error}`,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
// Check index status
|
|
818
|
+
const indexStatus = await checkIndexStatus(path);
|
|
819
|
+
const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'hybrid', '--json'];
|
|
820
|
+
if (enrich) {
|
|
821
|
+
args.push('--enrich');
|
|
822
|
+
}
|
|
823
|
+
const result = await executeCodexLens(args, { cwd: path });
|
|
824
|
+
if (!result.success) {
|
|
825
|
+
return {
|
|
826
|
+
success: false,
|
|
827
|
+
error: result.error,
|
|
828
|
+
metadata: {
|
|
829
|
+
mode: 'hybrid',
|
|
830
|
+
backend: 'codexlens',
|
|
831
|
+
count: 0,
|
|
832
|
+
query,
|
|
833
|
+
warning: indexStatus.warning,
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// Parse results
|
|
838
|
+
let results = [];
|
|
839
|
+
let baselineInfo = null;
|
|
840
|
+
let initialCount = 0;
|
|
841
|
+
try {
|
|
842
|
+
const parsed = JSON.parse(stripAnsi(result.output || '{}'));
|
|
843
|
+
const data = parsed.result?.results || parsed.results || parsed;
|
|
844
|
+
results = (Array.isArray(data) ? data : []).map((item) => {
|
|
845
|
+
const rawScore = item.score || 0;
|
|
846
|
+
// Hybrid mode returns distance scores (lower is better).
|
|
847
|
+
// Convert to similarity scores (higher is better) for consistency.
|
|
848
|
+
// Formula: similarity = 1 / (1 + distance)
|
|
849
|
+
const similarityScore = rawScore > 0 ? 1 / (1 + rawScore) : 1;
|
|
850
|
+
return {
|
|
851
|
+
file: item.path || item.file,
|
|
852
|
+
score: similarityScore,
|
|
853
|
+
content: item.excerpt || item.content || '',
|
|
854
|
+
symbol: item.symbol || null,
|
|
855
|
+
};
|
|
856
|
+
});
|
|
857
|
+
initialCount = results.length;
|
|
858
|
+
// Post-processing pipeline to improve semantic search quality
|
|
859
|
+
// 0. Filter dominant baseline scores (hot spot detection)
|
|
860
|
+
const baselineResult = filterDominantBaselineScores(results);
|
|
861
|
+
results = baselineResult.filteredResults;
|
|
862
|
+
baselineInfo = baselineResult.baselineInfo;
|
|
863
|
+
// 1. Filter noisy files (coverage, node_modules, etc.)
|
|
864
|
+
results = filterNoisyFiles(results);
|
|
865
|
+
// 2. Boost results containing query keywords
|
|
866
|
+
results = applyKeywordBoosting(results, query);
|
|
867
|
+
// 3. Enforce score diversity (penalize identical scores)
|
|
868
|
+
results = enforceScoreDiversity(results);
|
|
869
|
+
// 4. Re-sort by adjusted scores
|
|
870
|
+
results.sort((a, b) => b.score - a.score);
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return {
|
|
874
|
+
success: true,
|
|
875
|
+
results: [],
|
|
876
|
+
output: result.output,
|
|
877
|
+
metadata: {
|
|
878
|
+
mode: 'hybrid',
|
|
879
|
+
backend: 'codexlens',
|
|
880
|
+
count: 0,
|
|
881
|
+
query,
|
|
882
|
+
warning: indexStatus.warning || 'Failed to parse JSON output',
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
// Build metadata with baseline info if detected
|
|
887
|
+
let note = 'Hybrid mode uses RRF fusion (exact + fuzzy + vector) for best results';
|
|
888
|
+
if (baselineInfo) {
|
|
889
|
+
note += ` | Filtered ${initialCount - results.length} hot-spot results with baseline score ~${baselineInfo.score.toFixed(4)}`;
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
success: true,
|
|
893
|
+
results,
|
|
894
|
+
metadata: {
|
|
895
|
+
mode: 'hybrid',
|
|
896
|
+
backend: 'codexlens',
|
|
897
|
+
count: results.length,
|
|
898
|
+
query,
|
|
899
|
+
note,
|
|
900
|
+
warning: indexStatus.warning,
|
|
901
|
+
suggested_weights: getRRFWeights(query),
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
const RRF_WEIGHTS = {
|
|
906
|
+
code: { exact: 0.7, fuzzy: 0.2, vector: 0.1 },
|
|
907
|
+
natural: { exact: 0.4, fuzzy: 0.2, vector: 0.4 },
|
|
908
|
+
default: { exact: 0.5, fuzzy: 0.2, vector: 0.3 },
|
|
909
|
+
};
|
|
910
|
+
function getRRFWeights(query) {
|
|
911
|
+
const isCode = looksLikeCodeQuery(query);
|
|
912
|
+
const isNatural = detectNaturalLanguage(query);
|
|
913
|
+
if (isCode)
|
|
914
|
+
return RRF_WEIGHTS.code;
|
|
915
|
+
if (isNatural)
|
|
916
|
+
return RRF_WEIGHTS.natural;
|
|
917
|
+
return RRF_WEIGHTS.default;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Post-processing: Filter noisy files from semantic search results
|
|
921
|
+
* Uses FILTER_CONFIG patterns to remove irrelevant files.
|
|
922
|
+
* Optimized: pre-compiled regexes, accurate path segment matching.
|
|
923
|
+
*/
|
|
924
|
+
// Pre-compile file exclusion regexes once (avoid recompilation in loop)
|
|
925
|
+
const FILE_EXCLUDE_REGEXES = [...FILTER_CONFIG.exclude_files].map(pattern => new RegExp('^' + pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*') + '$'));
|
|
926
|
+
function filterNoisyFiles(results) {
|
|
927
|
+
return results.filter(r => {
|
|
928
|
+
const filePath = r.file || '';
|
|
929
|
+
if (!filePath)
|
|
930
|
+
return true;
|
|
931
|
+
const segments = filePath.split(/[/\\]/);
|
|
932
|
+
// Accurate directory check: segment must exactly match excluded directory
|
|
933
|
+
if (segments.some(segment => FILTER_CONFIG.exclude_directories.has(segment))) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
// Accurate file check: pattern matches filename only (not full path)
|
|
937
|
+
const filename = segments.pop() || '';
|
|
938
|
+
if (FILE_EXCLUDE_REGEXES.some(regex => regex.test(filename))) {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
return true;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Post-processing: Boost results containing query keywords
|
|
946
|
+
* Extracts keywords from query and boosts matching results.
|
|
947
|
+
* Optimized: uses whole-word matching with regex for accuracy.
|
|
948
|
+
*/
|
|
949
|
+
// Helper to escape regex special characters
|
|
950
|
+
function escapeRegExp(str) {
|
|
951
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
952
|
+
}
|
|
953
|
+
function applyKeywordBoosting(results, query) {
|
|
954
|
+
// Extract meaningful keywords (ignore common words)
|
|
955
|
+
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'and', 'but', 'if', 'or', 'because', 'until', 'while', 'although', 'though', 'after', 'before', 'when', 'whenever', 'where', 'wherever', 'whether', 'which', 'who', 'whom', 'whose', 'what', 'whatever', 'whichever', 'whoever', 'whomever', 'this', 'that', 'these', 'those', 'it', 'its']);
|
|
956
|
+
const keywords = query
|
|
957
|
+
.toLowerCase()
|
|
958
|
+
.split(/[\s,.;:()"{}[\]-]+/) // More robust splitting on punctuation
|
|
959
|
+
.filter(word => word.length > 2 && !stopWords.has(word));
|
|
960
|
+
if (keywords.length === 0)
|
|
961
|
+
return results;
|
|
962
|
+
// Create case-insensitive regexes for whole-word matching
|
|
963
|
+
const keywordRegexes = keywords.map(kw => new RegExp(`\\b${escapeRegExp(kw)}\\b`, 'i'));
|
|
964
|
+
return results.map(r => {
|
|
965
|
+
const content = r.content || '';
|
|
966
|
+
const file = r.file || '';
|
|
967
|
+
// Count keyword matches using whole-word regex
|
|
968
|
+
let matchCount = 0;
|
|
969
|
+
for (const regex of keywordRegexes) {
|
|
970
|
+
if (regex.test(content) || regex.test(file)) {
|
|
971
|
+
matchCount++;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// Apply boost only if there are matches
|
|
975
|
+
if (matchCount > 0) {
|
|
976
|
+
const matchRatio = matchCount / keywords.length;
|
|
977
|
+
const boost = 1 + (matchRatio * 0.3); // Up to 30% boost for full match
|
|
978
|
+
return {
|
|
979
|
+
...r,
|
|
980
|
+
score: r.score * boost,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
return r;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Post-processing: Enforce score diversity
|
|
988
|
+
* Penalizes results with identical scores (indicates undifferentiated matching)
|
|
989
|
+
*/
|
|
990
|
+
function enforceScoreDiversity(results) {
|
|
991
|
+
if (results.length < 2)
|
|
992
|
+
return results;
|
|
993
|
+
// Count occurrences of each score (rounded to 3 decimal places for comparison)
|
|
994
|
+
const scoreCounts = new Map();
|
|
995
|
+
for (const r of results) {
|
|
996
|
+
const roundedScore = Math.round(r.score * 1000) / 1000;
|
|
997
|
+
scoreCounts.set(roundedScore, (scoreCounts.get(roundedScore) || 0) + 1);
|
|
998
|
+
}
|
|
999
|
+
// Apply penalty to scores that appear more than twice
|
|
1000
|
+
return results.map(r => {
|
|
1001
|
+
const roundedScore = Math.round(r.score * 1000) / 1000;
|
|
1002
|
+
const count = scoreCounts.get(roundedScore) || 1;
|
|
1003
|
+
if (count > 2) {
|
|
1004
|
+
// Progressive penalty: more duplicates = bigger penalty
|
|
1005
|
+
const penalty = Math.max(0.7, 1 - (count * 0.05));
|
|
1006
|
+
return { ...r, score: r.score * penalty };
|
|
1007
|
+
}
|
|
1008
|
+
return r;
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Post-processing: Filter results with dominant baseline score (hot spot detection)
|
|
1013
|
+
* When backend returns default "hot spot" files with identical high scores,
|
|
1014
|
+
* this function detects and removes them.
|
|
1015
|
+
*
|
|
1016
|
+
* Detection criteria:
|
|
1017
|
+
* - A single score appears in >50% of results
|
|
1018
|
+
* - That score is suspiciously high (>0.9)
|
|
1019
|
+
* - This indicates fallback mechanism returned placeholder results
|
|
1020
|
+
*/
|
|
1021
|
+
function filterDominantBaselineScores(results) {
|
|
1022
|
+
if (results.length < 4) {
|
|
1023
|
+
return { filteredResults: results, baselineInfo: null };
|
|
1024
|
+
}
|
|
1025
|
+
// Count occurrences of each score (rounded to 4 decimal places)
|
|
1026
|
+
const scoreCounts = new Map();
|
|
1027
|
+
results.forEach(r => {
|
|
1028
|
+
const rounded = Math.round(r.score * 10000) / 10000;
|
|
1029
|
+
scoreCounts.set(rounded, (scoreCounts.get(rounded) || 0) + 1);
|
|
1030
|
+
});
|
|
1031
|
+
// Find the most dominant score
|
|
1032
|
+
let dominantScore = null;
|
|
1033
|
+
let dominantCount = 0;
|
|
1034
|
+
scoreCounts.forEach((count, score) => {
|
|
1035
|
+
if (count > dominantCount) {
|
|
1036
|
+
dominantCount = count;
|
|
1037
|
+
dominantScore = score;
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
// If a single score is present in >50% of results and is high (>0.9),
|
|
1041
|
+
// treat it as a suspicious baseline score and filter it out
|
|
1042
|
+
const BASELINE_THRESHOLD = 0.5; // >50% of results have same score
|
|
1043
|
+
const HIGH_SCORE_THRESHOLD = 0.9; // Score above 0.9 is suspiciously high
|
|
1044
|
+
if (dominantScore !== null &&
|
|
1045
|
+
dominantCount > results.length * BASELINE_THRESHOLD &&
|
|
1046
|
+
dominantScore > HIGH_SCORE_THRESHOLD) {
|
|
1047
|
+
const filteredResults = results.filter(r => {
|
|
1048
|
+
const rounded = Math.round(r.score * 10000) / 10000;
|
|
1049
|
+
return rounded !== dominantScore;
|
|
1050
|
+
});
|
|
1051
|
+
return {
|
|
1052
|
+
filteredResults,
|
|
1053
|
+
baselineInfo: { score: dominantScore, count: dominantCount },
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
return { filteredResults: results, baselineInfo: null };
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* TypeScript implementation of Reciprocal Rank Fusion
|
|
1060
|
+
* Reference: codex-lens/src/codexlens/search/ranking.py
|
|
1061
|
+
* Formula: score(d) = Σ weight_source / (k + rank_source(d))
|
|
1062
|
+
*/
|
|
1063
|
+
function applyRRFFusion(resultsMap, weights, limit, k = 60) {
|
|
1064
|
+
const pathScores = new Map();
|
|
1065
|
+
resultsMap.forEach((results, source) => {
|
|
1066
|
+
const weight = weights[source] || 0;
|
|
1067
|
+
if (weight === 0 || !results)
|
|
1068
|
+
return;
|
|
1069
|
+
results.forEach((result, rank) => {
|
|
1070
|
+
const path = result.file || result.path;
|
|
1071
|
+
if (!path)
|
|
1072
|
+
return;
|
|
1073
|
+
const rrfContribution = weight / (k + rank + 1);
|
|
1074
|
+
if (!pathScores.has(path)) {
|
|
1075
|
+
pathScores.set(path, { score: 0, result, sources: [] });
|
|
1076
|
+
}
|
|
1077
|
+
const entry = pathScores.get(path);
|
|
1078
|
+
entry.score += rrfContribution;
|
|
1079
|
+
if (!entry.sources.includes(source)) {
|
|
1080
|
+
entry.sources.push(source);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
// Sort by fusion score descending
|
|
1085
|
+
return Array.from(pathScores.values())
|
|
1086
|
+
.sort((a, b) => b.score - a.score)
|
|
1087
|
+
.slice(0, limit)
|
|
1088
|
+
.map(item => ({
|
|
1089
|
+
...item.result,
|
|
1090
|
+
fusion_score: item.score,
|
|
1091
|
+
matched_backends: item.sources,
|
|
1092
|
+
}));
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Promise wrapper with timeout support
|
|
1096
|
+
* @param promise - The promise to wrap
|
|
1097
|
+
* @param ms - Timeout in milliseconds
|
|
1098
|
+
* @param modeName - Name of the mode for error message
|
|
1099
|
+
* @returns A new promise that rejects on timeout
|
|
1100
|
+
*/
|
|
1101
|
+
function withTimeout(promise, ms, modeName) {
|
|
1102
|
+
return new Promise((resolve, reject) => {
|
|
1103
|
+
const timer = setTimeout(() => {
|
|
1104
|
+
reject(new Error(`'${modeName}' search timed out after ${ms}ms`));
|
|
1105
|
+
}, ms);
|
|
1106
|
+
promise
|
|
1107
|
+
.then(resolve)
|
|
1108
|
+
.catch(reject)
|
|
1109
|
+
.finally(() => clearTimeout(timer));
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Mode: priority - Fallback search strategy: hybrid -> exact -> ripgrep
|
|
1114
|
+
* Returns results from the first backend that succeeds and provides results.
|
|
1115
|
+
* More efficient than parallel mode - stops as soon as valid results are found.
|
|
1116
|
+
*/
|
|
1117
|
+
async function executePriorityFallbackMode(params) {
|
|
1118
|
+
const { query, path = '.' } = params;
|
|
1119
|
+
const fallbackHistory = [];
|
|
1120
|
+
if (!query) {
|
|
1121
|
+
return { success: false, error: 'Query is required for search' };
|
|
1122
|
+
}
|
|
1123
|
+
// Check index status first
|
|
1124
|
+
const indexStatus = await checkIndexStatus(path);
|
|
1125
|
+
// 1. Try Hybrid search (highest priority) - 90s timeout for large indexes
|
|
1126
|
+
if (indexStatus.indexed && indexStatus.has_embeddings) {
|
|
1127
|
+
try {
|
|
1128
|
+
const hybridResult = await withTimeout(executeHybridMode(params), 90000, 'hybrid');
|
|
1129
|
+
if (hybridResult.success && hybridResult.results && hybridResult.results.length > 0) {
|
|
1130
|
+
fallbackHistory.push('hybrid: success');
|
|
1131
|
+
return {
|
|
1132
|
+
...hybridResult,
|
|
1133
|
+
metadata: {
|
|
1134
|
+
...hybridResult.metadata,
|
|
1135
|
+
mode: 'priority',
|
|
1136
|
+
note: 'Result from hybrid search (semantic + vector).',
|
|
1137
|
+
fallback_history: fallbackHistory,
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
fallbackHistory.push('hybrid: no results');
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
fallbackHistory.push(`hybrid: ${error.message}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
fallbackHistory.push(`hybrid: skipped (${!indexStatus.indexed ? 'no index' : 'no embeddings'})`);
|
|
1149
|
+
}
|
|
1150
|
+
// 2. Fallback to Exact search - 10s timeout
|
|
1151
|
+
if (indexStatus.indexed) {
|
|
1152
|
+
try {
|
|
1153
|
+
const exactResult = await withTimeout(executeCodexLensExactMode(params), 10000, 'exact');
|
|
1154
|
+
if (exactResult.success && exactResult.results && exactResult.results.length > 0) {
|
|
1155
|
+
fallbackHistory.push('exact: success');
|
|
1156
|
+
return {
|
|
1157
|
+
...exactResult,
|
|
1158
|
+
metadata: {
|
|
1159
|
+
...exactResult.metadata,
|
|
1160
|
+
mode: 'priority',
|
|
1161
|
+
note: 'Result from exact/FTS search (fallback from hybrid).',
|
|
1162
|
+
fallback_history: fallbackHistory,
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
fallbackHistory.push('exact: no results');
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
fallbackHistory.push(`exact: ${error.message}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
fallbackHistory.push('exact: skipped (no index)');
|
|
1174
|
+
}
|
|
1175
|
+
// 3. Final fallback to Ripgrep - 5s timeout
|
|
1176
|
+
try {
|
|
1177
|
+
const ripgrepResult = await withTimeout(executeRipgrepMode(params), 5000, 'ripgrep');
|
|
1178
|
+
fallbackHistory.push(ripgrepResult.success ? 'ripgrep: success' : 'ripgrep: failed');
|
|
1179
|
+
return {
|
|
1180
|
+
...ripgrepResult,
|
|
1181
|
+
metadata: {
|
|
1182
|
+
...ripgrepResult.metadata,
|
|
1183
|
+
mode: 'priority',
|
|
1184
|
+
note: 'Result from ripgrep search (final fallback).',
|
|
1185
|
+
fallback_history: fallbackHistory,
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
fallbackHistory.push(`ripgrep: ${error.message}`);
|
|
1191
|
+
}
|
|
1192
|
+
// All modes failed
|
|
1193
|
+
return {
|
|
1194
|
+
success: false,
|
|
1195
|
+
error: 'All search backends in priority mode failed or returned no results.',
|
|
1196
|
+
metadata: {
|
|
1197
|
+
mode: 'priority',
|
|
1198
|
+
query,
|
|
1199
|
+
fallback_history: fallbackHistory,
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
// Tool schema for MCP
|
|
1204
|
+
export const schema = {
|
|
1205
|
+
name: 'smart_search',
|
|
1206
|
+
description: `Unified code search tool with content search, file discovery, and semantic search capabilities.
|
|
1207
|
+
|
|
1208
|
+
**Actions:**
|
|
1209
|
+
- search: Search file content (default)
|
|
1210
|
+
- find_files: Find files by path/name pattern (glob matching)
|
|
1211
|
+
- init: Create FTS index
|
|
1212
|
+
- status: Check index status
|
|
1213
|
+
|
|
1214
|
+
**Content Search (action="search"):**
|
|
1215
|
+
smart_search(query="authentication logic") # auto mode - routes to best backend
|
|
1216
|
+
smart_search(query="MyClass", mode="exact") # exact mode - precise FTS matching
|
|
1217
|
+
smart_search(query="auth", mode="ripgrep") # ripgrep mode - fast literal search
|
|
1218
|
+
smart_search(query="how to auth", mode="hybrid") # hybrid mode - semantic + fuzzy search
|
|
1219
|
+
|
|
1220
|
+
**File Discovery (action="find_files"):**
|
|
1221
|
+
smart_search(action="find_files", pattern="*.ts") # find all TypeScript files
|
|
1222
|
+
smart_search(action="find_files", pattern="src/**/*.js") # recursive glob pattern
|
|
1223
|
+
smart_search(action="find_files", pattern="test_*.py") # find test files
|
|
1224
|
+
smart_search(action="find_files", pattern="*.tsx", offset=20, limit=10) # pagination
|
|
1225
|
+
|
|
1226
|
+
**Pagination:** All actions support offset/limit for paginated results:
|
|
1227
|
+
smart_search(query="auth", limit=10, offset=0) # first page
|
|
1228
|
+
smart_search(query="auth", limit=10, offset=10) # second page
|
|
1229
|
+
|
|
1230
|
+
**Multi-Word Search (ripgrep mode with tokenization):**
|
|
1231
|
+
smart_search(query="CCW_PROJECT_ROOT CCW_ALLOWED_DIRS", mode="ripgrep") # tokenized OR matching
|
|
1232
|
+
smart_search(query="auth login user", mode="ripgrep") # matches any token, ranks by match count
|
|
1233
|
+
smart_search(query="exact phrase", mode="ripgrep", tokenize=false) # disable tokenization
|
|
1234
|
+
|
|
1235
|
+
**Regex Search (ripgrep mode):**
|
|
1236
|
+
smart_search(query="class.*Builder") # auto-detects regex pattern
|
|
1237
|
+
smart_search(query="def.*\\(.*\\):") # find function definitions
|
|
1238
|
+
smart_search(query="import.*from", caseSensitive=false) # case-insensitive
|
|
1239
|
+
|
|
1240
|
+
**Modes:** auto (intelligent routing), hybrid (semantic+fuzzy), exact (FTS), ripgrep (fast with tokenization), priority (fallback chain)`,
|
|
1241
|
+
inputSchema: {
|
|
1242
|
+
type: 'object',
|
|
1243
|
+
properties: {
|
|
1244
|
+
action: {
|
|
1245
|
+
type: 'string',
|
|
1246
|
+
enum: ['init', 'search', 'find_files', 'status', 'search_files'],
|
|
1247
|
+
description: 'Action: search (content search), find_files (path pattern matching), init (create index), status (check index). Note: search_files is deprecated.',
|
|
1248
|
+
default: 'search',
|
|
1249
|
+
},
|
|
1250
|
+
query: {
|
|
1251
|
+
type: 'string',
|
|
1252
|
+
description: 'Content search query (for action="search")',
|
|
1253
|
+
},
|
|
1254
|
+
pattern: {
|
|
1255
|
+
type: 'string',
|
|
1256
|
+
description: 'Glob pattern for file discovery (for action="find_files"). Examples: "*.ts", "src/**/*.js", "test_*.py"',
|
|
1257
|
+
},
|
|
1258
|
+
mode: {
|
|
1259
|
+
type: 'string',
|
|
1260
|
+
enum: SEARCH_MODES,
|
|
1261
|
+
description: 'Search mode: auto (default), hybrid (best quality), exact (CodexLens FTS), ripgrep (fast, no index), priority (fallback: hybrid->exact->ripgrep)',
|
|
1262
|
+
default: 'auto',
|
|
1263
|
+
},
|
|
1264
|
+
output_mode: {
|
|
1265
|
+
type: 'string',
|
|
1266
|
+
enum: ['full', 'files_only', 'count'],
|
|
1267
|
+
description: 'Output format: full (default), files_only (paths only), count (per-file counts)',
|
|
1268
|
+
default: 'full',
|
|
1269
|
+
},
|
|
1270
|
+
path: {
|
|
1271
|
+
type: 'string',
|
|
1272
|
+
description: 'Directory path for init/search actions (default: current directory)',
|
|
1273
|
+
},
|
|
1274
|
+
paths: {
|
|
1275
|
+
type: 'array',
|
|
1276
|
+
description: 'Multiple paths to search within (for search action)',
|
|
1277
|
+
items: {
|
|
1278
|
+
type: 'string',
|
|
1279
|
+
},
|
|
1280
|
+
default: [],
|
|
1281
|
+
},
|
|
1282
|
+
contextLines: {
|
|
1283
|
+
type: 'number',
|
|
1284
|
+
description: 'Number of context lines around matches (exact mode only)',
|
|
1285
|
+
default: 0,
|
|
1286
|
+
},
|
|
1287
|
+
maxResults: {
|
|
1288
|
+
type: 'number',
|
|
1289
|
+
description: 'Maximum number of results (default: 20)',
|
|
1290
|
+
default: 20,
|
|
1291
|
+
},
|
|
1292
|
+
limit: {
|
|
1293
|
+
type: 'number',
|
|
1294
|
+
description: 'Alias for maxResults (default: 20)',
|
|
1295
|
+
default: 20,
|
|
1296
|
+
},
|
|
1297
|
+
offset: {
|
|
1298
|
+
type: 'number',
|
|
1299
|
+
description: 'Pagination offset - skip first N results (default: 0)',
|
|
1300
|
+
default: 0,
|
|
1301
|
+
},
|
|
1302
|
+
includeHidden: {
|
|
1303
|
+
type: 'boolean',
|
|
1304
|
+
description: 'Include hidden files/directories',
|
|
1305
|
+
default: false,
|
|
1306
|
+
},
|
|
1307
|
+
languages: {
|
|
1308
|
+
type: 'array',
|
|
1309
|
+
items: { type: 'string' },
|
|
1310
|
+
description: 'Languages to index (for init action). Example: ["javascript", "typescript"]',
|
|
1311
|
+
},
|
|
1312
|
+
enrich: {
|
|
1313
|
+
type: 'boolean',
|
|
1314
|
+
description: 'Enrich search results with code graph relationships (calls, imports, called_by, imported_by).',
|
|
1315
|
+
default: false,
|
|
1316
|
+
},
|
|
1317
|
+
regex: {
|
|
1318
|
+
type: 'boolean',
|
|
1319
|
+
description: 'Use regex pattern matching instead of literal string (ripgrep mode only). Default: enabled. Example: smart_search(query="class.*Builder")',
|
|
1320
|
+
default: true,
|
|
1321
|
+
},
|
|
1322
|
+
caseSensitive: {
|
|
1323
|
+
type: 'boolean',
|
|
1324
|
+
description: 'Case-sensitive search (default: true). Set to false for case-insensitive matching.',
|
|
1325
|
+
default: true,
|
|
1326
|
+
},
|
|
1327
|
+
tokenize: {
|
|
1328
|
+
type: 'boolean',
|
|
1329
|
+
description: 'Tokenize multi-word queries for OR matching (ripgrep mode). Default: true. Results are ranked by token match count (exact matches first).',
|
|
1330
|
+
default: true,
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
required: [],
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
/**
|
|
1337
|
+
* Action: find_files - Find files by path/name pattern (glob matching)
|
|
1338
|
+
* Unlike search which looks inside file content, find_files matches file paths
|
|
1339
|
+
*/
|
|
1340
|
+
async function executeFindFilesAction(params) {
|
|
1341
|
+
const { pattern, path = '.', limit = 20, offset = 0, includeHidden = false, caseSensitive = true } = params;
|
|
1342
|
+
if (!pattern) {
|
|
1343
|
+
return {
|
|
1344
|
+
success: false,
|
|
1345
|
+
error: 'Pattern is required for find_files action. Use glob patterns like "*.ts", "src/**/*.js", or "test_*.py"',
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
// Use ripgrep with --files flag for fast file listing with glob pattern
|
|
1349
|
+
const hasRipgrep = checkToolAvailability('rg');
|
|
1350
|
+
if (!hasRipgrep) {
|
|
1351
|
+
// Fallback to CodexLens file listing if available
|
|
1352
|
+
const readyStatus = await ensureCodexLensReady();
|
|
1353
|
+
if (!readyStatus.ready) {
|
|
1354
|
+
return {
|
|
1355
|
+
success: false,
|
|
1356
|
+
error: 'Neither ripgrep nor CodexLens available for file discovery.',
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
// Try CodexLens file list command
|
|
1360
|
+
const args = ['list-files', '--json'];
|
|
1361
|
+
const result = await executeCodexLens(args, { cwd: path });
|
|
1362
|
+
if (!result.success) {
|
|
1363
|
+
return {
|
|
1364
|
+
success: false,
|
|
1365
|
+
error: `Failed to list files: ${result.error}`,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
// Parse and filter results by pattern
|
|
1369
|
+
let files = [];
|
|
1370
|
+
try {
|
|
1371
|
+
const parsed = JSON.parse(stripAnsi(result.output || '[]'));
|
|
1372
|
+
files = Array.isArray(parsed) ? parsed : (parsed.files || []);
|
|
1373
|
+
}
|
|
1374
|
+
catch {
|
|
1375
|
+
return {
|
|
1376
|
+
success: false,
|
|
1377
|
+
error: 'Failed to parse file list from CodexLens',
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
// Apply glob pattern matching using minimatch-style regex
|
|
1381
|
+
const globRegex = globToRegex(pattern, caseSensitive);
|
|
1382
|
+
const matchedFiles = files.filter(f => globRegex.test(f));
|
|
1383
|
+
// Apply pagination
|
|
1384
|
+
const total = matchedFiles.length;
|
|
1385
|
+
const paginatedFiles = matchedFiles.slice(offset, offset + limit);
|
|
1386
|
+
const results = paginatedFiles.map(filePath => {
|
|
1387
|
+
const parts = filePath.split(/[/\\]/);
|
|
1388
|
+
const name = parts[parts.length - 1] || '';
|
|
1389
|
+
const ext = name.includes('.') ? name.split('.').pop() : undefined;
|
|
1390
|
+
return {
|
|
1391
|
+
path: filePath,
|
|
1392
|
+
type: 'file',
|
|
1393
|
+
name,
|
|
1394
|
+
extension: ext,
|
|
1395
|
+
};
|
|
1396
|
+
});
|
|
1397
|
+
return {
|
|
1398
|
+
success: true,
|
|
1399
|
+
results,
|
|
1400
|
+
metadata: {
|
|
1401
|
+
pattern,
|
|
1402
|
+
backend: 'codexlens',
|
|
1403
|
+
count: results.length,
|
|
1404
|
+
pagination: {
|
|
1405
|
+
offset,
|
|
1406
|
+
limit,
|
|
1407
|
+
total,
|
|
1408
|
+
has_more: offset + limit < total,
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
// Use ripgrep --files with glob pattern for fast file discovery
|
|
1414
|
+
return new Promise((resolve) => {
|
|
1415
|
+
const args = ['--files'];
|
|
1416
|
+
// Add exclude patterns
|
|
1417
|
+
if (!includeHidden) {
|
|
1418
|
+
args.push(...buildExcludeArgs());
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
args.push('--hidden');
|
|
1422
|
+
}
|
|
1423
|
+
// Add glob pattern
|
|
1424
|
+
args.push('--glob', pattern);
|
|
1425
|
+
// Case sensitivity for glob matching
|
|
1426
|
+
if (!caseSensitive) {
|
|
1427
|
+
args.push('--iglob', pattern);
|
|
1428
|
+
// Remove the case-sensitive glob and use iglob instead
|
|
1429
|
+
const globIndex = args.indexOf('--glob');
|
|
1430
|
+
if (globIndex !== -1) {
|
|
1431
|
+
args.splice(globIndex, 2);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const child = spawn('rg', args, {
|
|
1435
|
+
cwd: path || getProjectRoot(),
|
|
1436
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1437
|
+
});
|
|
1438
|
+
let stdout = '';
|
|
1439
|
+
let stderr = '';
|
|
1440
|
+
child.stdout.on('data', (data) => {
|
|
1441
|
+
stdout += data.toString();
|
|
1442
|
+
});
|
|
1443
|
+
child.stderr.on('data', (data) => {
|
|
1444
|
+
stderr += data.toString();
|
|
1445
|
+
});
|
|
1446
|
+
child.on('close', (code) => {
|
|
1447
|
+
// ripgrep returns 1 when no matches found, which is not an error
|
|
1448
|
+
if (code !== 0 && code !== 1 && !stderr.includes('os error 1')) {
|
|
1449
|
+
resolve({
|
|
1450
|
+
success: false,
|
|
1451
|
+
error: `ripgrep file search failed: ${stderr}`,
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const allFiles = stdout.split('\n').filter(line => line.trim());
|
|
1456
|
+
const total = allFiles.length;
|
|
1457
|
+
// Apply pagination
|
|
1458
|
+
const paginatedFiles = allFiles.slice(offset, offset + limit);
|
|
1459
|
+
const results = paginatedFiles.map(filePath => {
|
|
1460
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
1461
|
+
const parts = normalizedPath.split('/');
|
|
1462
|
+
const name = parts[parts.length - 1] || '';
|
|
1463
|
+
const ext = name.includes('.') ? name.split('.').pop() : undefined;
|
|
1464
|
+
return {
|
|
1465
|
+
path: normalizedPath,
|
|
1466
|
+
type: 'file',
|
|
1467
|
+
name,
|
|
1468
|
+
extension: ext,
|
|
1469
|
+
};
|
|
1470
|
+
});
|
|
1471
|
+
resolve({
|
|
1472
|
+
success: true,
|
|
1473
|
+
results,
|
|
1474
|
+
metadata: {
|
|
1475
|
+
pattern,
|
|
1476
|
+
backend: 'ripgrep',
|
|
1477
|
+
count: results.length,
|
|
1478
|
+
pagination: {
|
|
1479
|
+
offset,
|
|
1480
|
+
limit,
|
|
1481
|
+
total,
|
|
1482
|
+
has_more: offset + limit < total,
|
|
1483
|
+
},
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
child.on('error', (error) => {
|
|
1488
|
+
resolve({
|
|
1489
|
+
success: false,
|
|
1490
|
+
error: `Failed to spawn ripgrep: ${error.message}`,
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Convert glob pattern to regex for file matching
|
|
1497
|
+
* Supports: *, **, ?, [abc], [!abc]
|
|
1498
|
+
*/
|
|
1499
|
+
function globToRegex(pattern, caseSensitive = true) {
|
|
1500
|
+
let i = 0;
|
|
1501
|
+
const out = [];
|
|
1502
|
+
const special = '.^$+{}|()';
|
|
1503
|
+
while (i < pattern.length) {
|
|
1504
|
+
const c = pattern[i];
|
|
1505
|
+
if (c === '*') {
|
|
1506
|
+
if (i + 1 < pattern.length && pattern[i + 1] === '*') {
|
|
1507
|
+
// ** matches any path including /
|
|
1508
|
+
out.push('.*');
|
|
1509
|
+
i += 2;
|
|
1510
|
+
// Skip following / if present
|
|
1511
|
+
if (pattern[i] === '/') {
|
|
1512
|
+
i++;
|
|
1513
|
+
}
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
// * matches any character except /
|
|
1518
|
+
out.push('[^/]*');
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
else if (c === '?') {
|
|
1522
|
+
out.push('[^/]');
|
|
1523
|
+
}
|
|
1524
|
+
else if (c === '[') {
|
|
1525
|
+
// Character class
|
|
1526
|
+
let j = i + 1;
|
|
1527
|
+
let negated = false;
|
|
1528
|
+
if (pattern[j] === '!' || pattern[j] === '^') {
|
|
1529
|
+
negated = true;
|
|
1530
|
+
j++;
|
|
1531
|
+
}
|
|
1532
|
+
let classContent = '';
|
|
1533
|
+
while (j < pattern.length && pattern[j] !== ']') {
|
|
1534
|
+
classContent += pattern[j];
|
|
1535
|
+
j++;
|
|
1536
|
+
}
|
|
1537
|
+
if (negated) {
|
|
1538
|
+
out.push(`[^${classContent}]`);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
out.push(`[${classContent}]`);
|
|
1542
|
+
}
|
|
1543
|
+
i = j;
|
|
1544
|
+
}
|
|
1545
|
+
else if (special.includes(c)) {
|
|
1546
|
+
out.push('\\' + c);
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
out.push(c);
|
|
1550
|
+
}
|
|
1551
|
+
i++;
|
|
1552
|
+
}
|
|
1553
|
+
const flags = caseSensitive ? '' : 'i';
|
|
1554
|
+
return new RegExp('^' + out.join('') + '$', flags);
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Apply pagination to search results and add pagination metadata
|
|
1558
|
+
*/
|
|
1559
|
+
function applyPagination(results, offset, limit) {
|
|
1560
|
+
const total = results.length;
|
|
1561
|
+
const paginatedResults = results.slice(offset, offset + limit);
|
|
1562
|
+
return {
|
|
1563
|
+
paginatedResults,
|
|
1564
|
+
pagination: {
|
|
1565
|
+
offset,
|
|
1566
|
+
limit,
|
|
1567
|
+
total,
|
|
1568
|
+
has_more: offset + limit < total,
|
|
1569
|
+
},
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Transform results based on output_mode
|
|
1574
|
+
*/
|
|
1575
|
+
function transformOutput(results, outputMode) {
|
|
1576
|
+
if (!Array.isArray(results)) {
|
|
1577
|
+
return results;
|
|
1578
|
+
}
|
|
1579
|
+
switch (outputMode) {
|
|
1580
|
+
case 'files_only': {
|
|
1581
|
+
// Extract unique file paths
|
|
1582
|
+
const files = [...new Set(results.map((r) => r.file))].filter(Boolean);
|
|
1583
|
+
return { files, count: files.length };
|
|
1584
|
+
}
|
|
1585
|
+
case 'count': {
|
|
1586
|
+
// Count matches per file
|
|
1587
|
+
const counts = {};
|
|
1588
|
+
for (const r of results) {
|
|
1589
|
+
const file = r.file;
|
|
1590
|
+
if (file) {
|
|
1591
|
+
counts[file] = (counts[file] || 0) + 1;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
files: Object.entries(counts).map(([file, count]) => ({ file, count })),
|
|
1596
|
+
total: results.length,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
case 'full':
|
|
1600
|
+
default:
|
|
1601
|
+
return results;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
// Handler function
|
|
1605
|
+
export async function handler(params) {
|
|
1606
|
+
const parsed = ParamsSchema.safeParse(params);
|
|
1607
|
+
if (!parsed.success) {
|
|
1608
|
+
return { success: false, error: `Invalid params: ${parsed.error.message}` };
|
|
1609
|
+
}
|
|
1610
|
+
const { action, mode, output_mode, offset = 0 } = parsed.data;
|
|
1611
|
+
// Sync limit and maxResults - use the larger of the two if both provided
|
|
1612
|
+
// This ensures user-provided values take precedence over defaults
|
|
1613
|
+
const effectiveLimit = Math.max(parsed.data.limit || 20, parsed.data.maxResults || 20);
|
|
1614
|
+
parsed.data.maxResults = effectiveLimit;
|
|
1615
|
+
parsed.data.limit = effectiveLimit;
|
|
1616
|
+
// Track if search_files was used (deprecated)
|
|
1617
|
+
let deprecationWarning;
|
|
1618
|
+
try {
|
|
1619
|
+
let result;
|
|
1620
|
+
// Handle actions
|
|
1621
|
+
switch (action) {
|
|
1622
|
+
case 'init':
|
|
1623
|
+
result = await executeInitAction(parsed.data);
|
|
1624
|
+
break;
|
|
1625
|
+
case 'status':
|
|
1626
|
+
result = await executeStatusAction(parsed.data);
|
|
1627
|
+
break;
|
|
1628
|
+
case 'find_files':
|
|
1629
|
+
// NEW: File path/name pattern matching (glob-based)
|
|
1630
|
+
result = await executeFindFilesAction(parsed.data);
|
|
1631
|
+
break;
|
|
1632
|
+
case 'search_files':
|
|
1633
|
+
// DEPRECATED: Redirect to search with files_only output
|
|
1634
|
+
deprecationWarning = 'action="search_files" is deprecated. Use action="search" with output_mode="files_only" for content-to-files search, or action="find_files" for path pattern matching.';
|
|
1635
|
+
parsed.data.output_mode = 'files_only';
|
|
1636
|
+
// Fall through to search
|
|
1637
|
+
case 'search':
|
|
1638
|
+
default:
|
|
1639
|
+
// Handle search modes: auto | hybrid | exact | ripgrep | priority
|
|
1640
|
+
switch (mode) {
|
|
1641
|
+
case 'auto':
|
|
1642
|
+
result = await executeAutoMode(parsed.data);
|
|
1643
|
+
break;
|
|
1644
|
+
case 'hybrid':
|
|
1645
|
+
result = await executeHybridMode(parsed.data);
|
|
1646
|
+
break;
|
|
1647
|
+
case 'exact':
|
|
1648
|
+
result = await executeCodexLensExactMode(parsed.data);
|
|
1649
|
+
break;
|
|
1650
|
+
case 'ripgrep':
|
|
1651
|
+
result = await executeRipgrepMode(parsed.data);
|
|
1652
|
+
break;
|
|
1653
|
+
case 'priority':
|
|
1654
|
+
result = await executePriorityFallbackMode(parsed.data);
|
|
1655
|
+
break;
|
|
1656
|
+
default:
|
|
1657
|
+
throw new Error(`Unsupported mode: ${mode}. Use: auto, hybrid, exact, ripgrep, or priority`);
|
|
1658
|
+
}
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
// Transform output based on output_mode (for search actions only)
|
|
1662
|
+
if (action === 'search' || action === 'search_files') {
|
|
1663
|
+
if (result.success && result.results && output_mode !== 'full') {
|
|
1664
|
+
result.results = transformOutput(result.results, output_mode);
|
|
1665
|
+
}
|
|
1666
|
+
// Add pagination metadata for search results if not already present
|
|
1667
|
+
if (result.success && result.results && Array.isArray(result.results)) {
|
|
1668
|
+
const totalResults = result.results.length;
|
|
1669
|
+
if (!result.metadata) {
|
|
1670
|
+
result.metadata = {};
|
|
1671
|
+
}
|
|
1672
|
+
if (!result.metadata.pagination) {
|
|
1673
|
+
result.metadata.pagination = {
|
|
1674
|
+
offset: 0,
|
|
1675
|
+
limit: effectiveLimit,
|
|
1676
|
+
total: totalResults,
|
|
1677
|
+
has_more: false, // Already limited by backend
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
// Add deprecation warning if applicable
|
|
1683
|
+
if (deprecationWarning && result.metadata) {
|
|
1684
|
+
result.metadata.warning = deprecationWarning;
|
|
1685
|
+
}
|
|
1686
|
+
return result.success ? { success: true, result } : { success: false, error: result.error };
|
|
1687
|
+
}
|
|
1688
|
+
catch (error) {
|
|
1689
|
+
return { success: false, error: error.message };
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Execute init action with external progress callback
|
|
1694
|
+
* Used by MCP server for streaming progress
|
|
1695
|
+
*/
|
|
1696
|
+
export async function executeInitWithProgress(params, onProgress) {
|
|
1697
|
+
const path = params.path || '.';
|
|
1698
|
+
const languages = params.languages;
|
|
1699
|
+
// Check CodexLens availability
|
|
1700
|
+
const readyStatus = await ensureCodexLensReady();
|
|
1701
|
+
if (!readyStatus.ready) {
|
|
1702
|
+
return {
|
|
1703
|
+
success: false,
|
|
1704
|
+
error: `CodexLens not available: ${readyStatus.error}. CodexLens will be auto-installed on first use.`,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
const args = ['init', path];
|
|
1708
|
+
if (languages && languages.length > 0) {
|
|
1709
|
+
args.push('--languages', languages.join(','));
|
|
1710
|
+
}
|
|
1711
|
+
// Track progress updates
|
|
1712
|
+
const progressUpdates = [];
|
|
1713
|
+
let lastProgress = null;
|
|
1714
|
+
const result = await executeCodexLens(args, {
|
|
1715
|
+
cwd: path,
|
|
1716
|
+
timeout: 1800000, // 30 minutes for large codebases
|
|
1717
|
+
onProgress: (progress) => {
|
|
1718
|
+
progressUpdates.push(progress);
|
|
1719
|
+
lastProgress = progress;
|
|
1720
|
+
// Call external progress callback if provided
|
|
1721
|
+
if (onProgress) {
|
|
1722
|
+
onProgress(progress);
|
|
1723
|
+
}
|
|
1724
|
+
},
|
|
1725
|
+
});
|
|
1726
|
+
// Build metadata with progress info
|
|
1727
|
+
const metadata = {
|
|
1728
|
+
action: 'init',
|
|
1729
|
+
path,
|
|
1730
|
+
};
|
|
1731
|
+
if (lastProgress !== null) {
|
|
1732
|
+
const p = lastProgress;
|
|
1733
|
+
metadata.progress = {
|
|
1734
|
+
stage: p.stage,
|
|
1735
|
+
message: p.message,
|
|
1736
|
+
percent: p.percent,
|
|
1737
|
+
filesProcessed: p.filesProcessed,
|
|
1738
|
+
totalFiles: p.totalFiles,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
if (progressUpdates.length > 0) {
|
|
1742
|
+
metadata.progressHistory = progressUpdates.slice(-5);
|
|
1743
|
+
}
|
|
1744
|
+
return {
|
|
1745
|
+
success: result.success,
|
|
1746
|
+
error: result.error,
|
|
1747
|
+
message: result.success
|
|
1748
|
+
? `CodexLens index created successfully for ${path}`
|
|
1749
|
+
: undefined,
|
|
1750
|
+
metadata,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
//# sourceMappingURL=smart-search.js.map
|