@jagilber-org/index-server 1.19.1
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/CHANGELOG.md +1218 -0
- package/CODE_OF_CONDUCT.md +49 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +523 -0
- package/SECURITY.md +50 -0
- package/dist/config/configUtils.d.ts +11 -0
- package/dist/config/configUtils.js +87 -0
- package/dist/config/dashboardConfig.d.ts +45 -0
- package/dist/config/dashboardConfig.js +63 -0
- package/dist/config/defaultValues.d.ts +61 -0
- package/dist/config/defaultValues.js +70 -0
- package/dist/config/dirConstants.d.ts +17 -0
- package/dist/config/dirConstants.js +28 -0
- package/dist/config/featureConfig.d.ts +61 -0
- package/dist/config/featureConfig.js +121 -0
- package/dist/config/runtimeConfig.d.ts +145 -0
- package/dist/config/runtimeConfig.js +334 -0
- package/dist/config/serverConfig.d.ts +90 -0
- package/dist/config/serverConfig.js +164 -0
- package/dist/dashboard/analytics/AnalyticsEngine.d.ts +142 -0
- package/dist/dashboard/analytics/AnalyticsEngine.js +373 -0
- package/dist/dashboard/analytics/BusinessIntelligence.d.ts +187 -0
- package/dist/dashboard/analytics/BusinessIntelligence.js +594 -0
- package/dist/dashboard/client/admin.html +2150 -0
- package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/chunk-SP2CHFBE.mjs +1 -0
- package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/render-T6MDALS3.mjs +27 -0
- package/dist/dashboard/client/css/admin.css +1466 -0
- package/dist/dashboard/client/js/admin.boot.js +359 -0
- package/dist/dashboard/client/js/admin.config.js +196 -0
- package/dist/dashboard/client/js/admin.embeddings.js +425 -0
- package/dist/dashboard/client/js/admin.graph.js +583 -0
- package/dist/dashboard/client/js/admin.instances.js +120 -0
- package/dist/dashboard/client/js/admin.instructions.js +552 -0
- package/dist/dashboard/client/js/admin.logs.js +113 -0
- package/dist/dashboard/client/js/admin.maintenance.js +354 -0
- package/dist/dashboard/client/js/admin.messaging.js +635 -0
- package/dist/dashboard/client/js/admin.monitor.js +181 -0
- package/dist/dashboard/client/js/admin.overview.js +221 -0
- package/dist/dashboard/client/js/admin.performance.js +61 -0
- package/dist/dashboard/client/js/admin.sessions.js +293 -0
- package/dist/dashboard/client/js/admin.sqlite.js +366 -0
- package/dist/dashboard/client/js/admin.utils.js +49 -0
- package/dist/dashboard/client/js/chart.umd.js +14 -0
- package/dist/dashboard/client/js/elk.bundled.js +6696 -0
- package/dist/dashboard/client/js/marked.umd.js +74 -0
- package/dist/dashboard/client/js/mermaid.min.js +3022 -0
- package/dist/dashboard/client/mermaid-layout-elk.esm.min.mjs +1 -0
- package/dist/dashboard/export/DataExporter.d.ts +169 -0
- package/dist/dashboard/export/DataExporter.js +737 -0
- package/dist/dashboard/export/exporters/csvExporter.d.ts +11 -0
- package/dist/dashboard/export/exporters/csvExporter.js +46 -0
- package/dist/dashboard/export/exporters/exportTypes.d.ts +89 -0
- package/dist/dashboard/export/exporters/exportTypes.js +5 -0
- package/dist/dashboard/export/exporters/jsonExporter.d.ts +7 -0
- package/dist/dashboard/export/exporters/jsonExporter.js +22 -0
- package/dist/dashboard/export/exporters/xmlExporter.d.ts +17 -0
- package/dist/dashboard/export/exporters/xmlExporter.js +175 -0
- package/dist/dashboard/integration/APIIntegration.d.ts +41 -0
- package/dist/dashboard/integration/APIIntegration.js +95 -0
- package/dist/dashboard/security/SecurityMonitor.d.ts +167 -0
- package/dist/dashboard/security/SecurityMonitor.js +559 -0
- package/dist/dashboard/server/AdminPanel.d.ts +183 -0
- package/dist/dashboard/server/AdminPanel.js +792 -0
- package/dist/dashboard/server/AdminPanelConfig.d.ts +42 -0
- package/dist/dashboard/server/AdminPanelConfig.js +80 -0
- package/dist/dashboard/server/AdminPanelState.d.ts +47 -0
- package/dist/dashboard/server/AdminPanelState.js +214 -0
- package/dist/dashboard/server/ApiRoutes.d.ts +17 -0
- package/dist/dashboard/server/ApiRoutes.js +149 -0
- package/dist/dashboard/server/DashboardServer.d.ts +49 -0
- package/dist/dashboard/server/DashboardServer.js +159 -0
- package/dist/dashboard/server/FileMetricsStorage.d.ts +49 -0
- package/dist/dashboard/server/FileMetricsStorage.js +195 -0
- package/dist/dashboard/server/HttpTransport.d.ts +23 -0
- package/dist/dashboard/server/HttpTransport.js +116 -0
- package/dist/dashboard/server/InstanceManager.d.ts +53 -0
- package/dist/dashboard/server/InstanceManager.js +284 -0
- package/dist/dashboard/server/KnowledgeStore.d.ts +35 -0
- package/dist/dashboard/server/KnowledgeStore.js +105 -0
- package/dist/dashboard/server/LeaderElection.d.ts +81 -0
- package/dist/dashboard/server/LeaderElection.js +268 -0
- package/dist/dashboard/server/MetricsCollector.d.ts +200 -0
- package/dist/dashboard/server/MetricsCollector.js +803 -0
- package/dist/dashboard/server/SessionPersistenceManager.d.ts +88 -0
- package/dist/dashboard/server/SessionPersistenceManager.js +457 -0
- package/dist/dashboard/server/ThinClient.d.ts +64 -0
- package/dist/dashboard/server/ThinClient.js +237 -0
- package/dist/dashboard/server/WebSocketManager.d.ts +161 -0
- package/dist/dashboard/server/WebSocketManager.js +463 -0
- package/dist/dashboard/server/httpLifecycle.d.ts +17 -0
- package/dist/dashboard/server/httpLifecycle.js +35 -0
- package/dist/dashboard/server/legacyDashboardHtml.d.ts +9 -0
- package/dist/dashboard/server/legacyDashboardHtml.js +618 -0
- package/dist/dashboard/server/legacyDashboardStyles.d.ts +5 -0
- package/dist/dashboard/server/legacyDashboardStyles.js +490 -0
- package/dist/dashboard/server/metricsAggregation.d.ts +252 -0
- package/dist/dashboard/server/metricsAggregation.js +206 -0
- package/dist/dashboard/server/metricsSerializer.d.ts +25 -0
- package/dist/dashboard/server/metricsSerializer.js +195 -0
- package/dist/dashboard/server/routes/admin.routes.d.ts +16 -0
- package/dist/dashboard/server/routes/admin.routes.js +596 -0
- package/dist/dashboard/server/routes/alerts.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/alerts.routes.js +93 -0
- package/dist/dashboard/server/routes/api.feedback.routes.d.ts +73 -0
- package/dist/dashboard/server/routes/api.feedback.routes.js +171 -0
- package/dist/dashboard/server/routes/api.instructions.routes.d.ts +101 -0
- package/dist/dashboard/server/routes/api.instructions.routes.js +213 -0
- package/dist/dashboard/server/routes/api.usage.routes.d.ts +57 -0
- package/dist/dashboard/server/routes/api.usage.routes.js +374 -0
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/embeddings.routes.js +246 -0
- package/dist/dashboard/server/routes/graph.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/graph.routes.js +280 -0
- package/dist/dashboard/server/routes/index.d.ts +38 -0
- package/dist/dashboard/server/routes/index.js +194 -0
- package/dist/dashboard/server/routes/instances.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/instances.routes.js +35 -0
- package/dist/dashboard/server/routes/instructions.routes.d.ts +8 -0
- package/dist/dashboard/server/routes/instructions.routes.js +336 -0
- package/dist/dashboard/server/routes/knowledge.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/knowledge.routes.js +82 -0
- package/dist/dashboard/server/routes/logs.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/logs.routes.js +164 -0
- package/dist/dashboard/server/routes/messaging.routes.d.ts +16 -0
- package/dist/dashboard/server/routes/messaging.routes.js +293 -0
- package/dist/dashboard/server/routes/metrics.routes.d.ts +10 -0
- package/dist/dashboard/server/routes/metrics.routes.js +346 -0
- package/dist/dashboard/server/routes/scripts.routes.d.ts +9 -0
- package/dist/dashboard/server/routes/scripts.routes.js +84 -0
- package/dist/dashboard/server/routes/sqlite.routes.d.ts +9 -0
- package/dist/dashboard/server/routes/sqlite.routes.js +569 -0
- package/dist/dashboard/server/routes/status.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/status.routes.js +183 -0
- package/dist/dashboard/server/routes/synthetic.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/synthetic.routes.js +195 -0
- package/dist/dashboard/server/routes/tools.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/tools.routes.js +46 -0
- package/dist/dashboard/server/routes/usage.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/usage.routes.js +25 -0
- package/dist/dashboard/server/wsInit.d.ts +16 -0
- package/dist/dashboard/server/wsInit.js +35 -0
- package/dist/externalClientLib.d.ts +1 -0
- package/dist/externalClientLib.js +2 -0
- package/dist/minimal/index.d.ts +1 -0
- package/dist/minimal/index.js +140 -0
- package/dist/models/SessionPersistence.d.ts +115 -0
- package/dist/models/SessionPersistence.js +66 -0
- package/dist/models/instruction.d.ts +45 -0
- package/dist/models/instruction.js +2 -0
- package/dist/perf/benchmark.d.ts +1 -0
- package/dist/perf/benchmark.js +50 -0
- package/dist/portableClientWrapper.d.ts +1 -0
- package/dist/portableClientWrapper.js +2 -0
- package/dist/schemas/index.d.ts +128 -0
- package/dist/schemas/index.js +371 -0
- package/dist/scripts/runPerformanceBaseline.d.ts +1 -0
- package/dist/scripts/runPerformanceBaseline.js +17 -0
- package/dist/server/handshakeManager.d.ts +25 -0
- package/dist/server/handshakeManager.js +472 -0
- package/dist/server/index-server.d.ts +56 -0
- package/dist/server/index-server.js +822 -0
- package/dist/server/registry.d.ts +44 -0
- package/dist/server/registry.js +236 -0
- package/dist/server/sdkServer.d.ts +8 -0
- package/dist/server/sdkServer.js +299 -0
- package/dist/server/shutdownGuard.d.ts +41 -0
- package/dist/server/shutdownGuard.js +52 -0
- package/dist/server/thin-client.d.ts +22 -0
- package/dist/server/thin-client.js +111 -0
- package/dist/server/transport.d.ts +41 -0
- package/dist/server/transport.js +312 -0
- package/dist/server/transportFactory.d.ts +21 -0
- package/dist/server/transportFactory.js +429 -0
- package/dist/services/atomicFs.d.ts +22 -0
- package/dist/services/atomicFs.js +103 -0
- package/dist/services/auditLog.d.ts +38 -0
- package/dist/services/auditLog.js +142 -0
- package/dist/services/autoBackup.d.ts +14 -0
- package/dist/services/autoBackup.js +171 -0
- package/dist/services/autoSplit.d.ts +32 -0
- package/dist/services/autoSplit.js +113 -0
- package/dist/services/backupZip.d.ts +25 -0
- package/dist/services/backupZip.js +110 -0
- package/dist/services/bootstrapGating.d.ts +123 -0
- package/dist/services/bootstrapGating.js +221 -0
- package/dist/services/canonical.d.ts +23 -0
- package/dist/services/canonical.js +65 -0
- package/dist/services/categoryRules.d.ts +7 -0
- package/dist/services/categoryRules.js +37 -0
- package/dist/services/classificationService.d.ts +42 -0
- package/dist/services/classificationService.js +168 -0
- package/dist/services/embeddingService.d.ts +62 -0
- package/dist/services/embeddingService.js +259 -0
- package/dist/services/errors.d.ts +22 -0
- package/dist/services/errors.js +31 -0
- package/dist/services/featureFlags.d.ts +25 -0
- package/dist/services/featureFlags.js +89 -0
- package/dist/services/features.d.ts +13 -0
- package/dist/services/features.js +35 -0
- package/dist/services/handlers/instructions.add.d.ts +1 -0
- package/dist/services/handlers/instructions.add.js +496 -0
- package/dist/services/handlers/instructions.groom.d.ts +1 -0
- package/dist/services/handlers/instructions.groom.js +523 -0
- package/dist/services/handlers/instructions.import.d.ts +1 -0
- package/dist/services/handlers/instructions.import.js +173 -0
- package/dist/services/handlers/instructions.patch.d.ts +1 -0
- package/dist/services/handlers/instructions.patch.js +167 -0
- package/dist/services/handlers/instructions.query.d.ts +163 -0
- package/dist/services/handlers/instructions.query.js +522 -0
- package/dist/services/handlers/instructions.reload.d.ts +1 -0
- package/dist/services/handlers/instructions.reload.js +13 -0
- package/dist/services/handlers/instructions.remove.d.ts +1 -0
- package/dist/services/handlers/instructions.remove.js +118 -0
- package/dist/services/handlers/instructions.shared.d.ts +31 -0
- package/dist/services/handlers/instructions.shared.js +124 -0
- package/dist/services/handlers.activation.d.ts +1 -0
- package/dist/services/handlers.activation.js +203 -0
- package/dist/services/handlers.bootstrap.d.ts +1 -0
- package/dist/services/handlers.bootstrap.js +38 -0
- package/dist/services/handlers.dashboardConfig.d.ts +34 -0
- package/dist/services/handlers.dashboardConfig.js +108 -0
- package/dist/services/handlers.diagnostics.d.ts +1 -0
- package/dist/services/handlers.diagnostics.js +64 -0
- package/dist/services/handlers.feedback.d.ts +15 -0
- package/dist/services/handlers.feedback.js +378 -0
- package/dist/services/handlers.gates.d.ts +1 -0
- package/dist/services/handlers.gates.js +46 -0
- package/dist/services/handlers.graph.d.ts +53 -0
- package/dist/services/handlers.graph.js +231 -0
- package/dist/services/handlers.help.d.ts +1 -0
- package/dist/services/handlers.help.js +119 -0
- package/dist/services/handlers.instructionSchema.d.ts +1 -0
- package/dist/services/handlers.instructionSchema.js +227 -0
- package/dist/services/handlers.instructions.d.ts +8 -0
- package/dist/services/handlers.instructions.js +14 -0
- package/dist/services/handlers.instructionsDiagnostics.d.ts +1 -0
- package/dist/services/handlers.instructionsDiagnostics.js +14 -0
- package/dist/services/handlers.integrity.d.ts +1 -0
- package/dist/services/handlers.integrity.js +35 -0
- package/dist/services/handlers.manifest.d.ts +1 -0
- package/dist/services/handlers.manifest.js +24 -0
- package/dist/services/handlers.messaging.d.ts +12 -0
- package/dist/services/handlers.messaging.js +203 -0
- package/dist/services/handlers.metrics.d.ts +1 -0
- package/dist/services/handlers.metrics.js +43 -0
- package/dist/services/handlers.promote.d.ts +1 -0
- package/dist/services/handlers.promote.js +306 -0
- package/dist/services/handlers.prompt.d.ts +1 -0
- package/dist/services/handlers.prompt.js +7 -0
- package/dist/services/handlers.search.d.ts +69 -0
- package/dist/services/handlers.search.js +645 -0
- package/dist/services/handlers.testPrimitive.d.ts +1 -0
- package/dist/services/handlers.testPrimitive.js +5 -0
- package/dist/services/handlers.trace.d.ts +1 -0
- package/dist/services/handlers.trace.js +31 -0
- package/dist/services/handlers.usage.d.ts +1 -0
- package/dist/services/handlers.usage.js +11 -0
- package/dist/services/hotScore.d.ts +137 -0
- package/dist/services/hotScore.js +244 -0
- package/dist/services/indexContext.d.ts +117 -0
- package/dist/services/indexContext.js +968 -0
- package/dist/services/indexLoader.d.ts +44 -0
- package/dist/services/indexLoader.js +921 -0
- package/dist/services/indexRepository.d.ts +32 -0
- package/dist/services/indexRepository.js +71 -0
- package/dist/services/indexingService.d.ts +1 -0
- package/dist/services/indexingService.js +2 -0
- package/dist/services/instructions.dispatcher.d.ts +1 -0
- package/dist/services/instructions.dispatcher.js +231 -0
- package/dist/services/logPrefix.d.ts +1 -0
- package/dist/services/logPrefix.js +30 -0
- package/dist/services/logger.d.ts +52 -0
- package/dist/services/logger.js +268 -0
- package/dist/services/manifestManager.d.ts +82 -0
- package/dist/services/manifestManager.js +200 -0
- package/dist/services/messaging/agentMailbox.d.ts +60 -0
- package/dist/services/messaging/agentMailbox.js +353 -0
- package/dist/services/messaging/messagingPersistence.d.ts +20 -0
- package/dist/services/messaging/messagingPersistence.js +111 -0
- package/dist/services/messaging/messagingTypes.d.ts +150 -0
- package/dist/services/messaging/messagingTypes.js +66 -0
- package/dist/services/ownershipService.d.ts +1 -0
- package/dist/services/ownershipService.js +38 -0
- package/dist/services/performanceBaseline.d.ts +19 -0
- package/dist/services/performanceBaseline.js +210 -0
- package/dist/services/preflight.d.ts +12 -0
- package/dist/services/preflight.js +79 -0
- package/dist/services/promptReviewService.d.ts +44 -0
- package/dist/services/promptReviewService.js +101 -0
- package/dist/services/responseEnvelope.d.ts +6 -0
- package/dist/services/responseEnvelope.js +25 -0
- package/dist/services/seedBootstrap.d.ts +34 -0
- package/dist/services/seedBootstrap.js +427 -0
- package/dist/services/storage/factory.d.ts +17 -0
- package/dist/services/storage/factory.js +35 -0
- package/dist/services/storage/hashUtils.d.ts +11 -0
- package/dist/services/storage/hashUtils.js +35 -0
- package/dist/services/storage/index.d.ts +12 -0
- package/dist/services/storage/index.js +18 -0
- package/dist/services/storage/jsonFileStore.d.ts +32 -0
- package/dist/services/storage/jsonFileStore.js +241 -0
- package/dist/services/storage/migrationEngine.d.ts +35 -0
- package/dist/services/storage/migrationEngine.js +93 -0
- package/dist/services/storage/sqliteMessageStore.d.ts +53 -0
- package/dist/services/storage/sqliteMessageStore.js +146 -0
- package/dist/services/storage/sqliteSchema.d.ts +12 -0
- package/dist/services/storage/sqliteSchema.js +122 -0
- package/dist/services/storage/sqliteStore.d.ts +41 -0
- package/dist/services/storage/sqliteStore.js +339 -0
- package/dist/services/storage/sqliteUsageStore.d.ts +35 -0
- package/dist/services/storage/sqliteUsageStore.js +94 -0
- package/dist/services/storage/types.d.ts +171 -0
- package/dist/services/storage/types.js +12 -0
- package/dist/services/toolHandlers.d.ts +23 -0
- package/dist/services/toolHandlers.js +50 -0
- package/dist/services/toolRegistry.d.ts +20 -0
- package/dist/services/toolRegistry.js +490 -0
- package/dist/services/toolRegistry.zod.d.ts +10 -0
- package/dist/services/toolRegistry.zod.js +323 -0
- package/dist/services/tracing.d.ts +26 -0
- package/dist/services/tracing.js +260 -0
- package/dist/services/usageBuckets.d.ts +161 -0
- package/dist/services/usageBuckets.js +364 -0
- package/dist/services/validationService.d.ts +38 -0
- package/dist/services/validationService.js +125 -0
- package/dist/utils/BufferRing.d.ts +203 -0
- package/dist/utils/BufferRing.js +551 -0
- package/dist/utils/BufferRingExamples.d.ts +55 -0
- package/dist/utils/BufferRingExamples.js +188 -0
- package/dist/utils/envUtils.d.ts +42 -0
- package/dist/utils/envUtils.js +80 -0
- package/dist/utils/memoryMonitor.d.ts +83 -0
- package/dist/utils/memoryMonitor.js +275 -0
- package/dist/versioning/schemaVersion.d.ts +6 -0
- package/dist/versioning/schemaVersion.js +93 -0
- package/package.json +134 -0
- package/schemas/README.md +13 -0
- package/schemas/feedback-entry.schema.json +27 -0
- package/schemas/graph-export-v2.schema.json +60 -0
- package/schemas/index-server.code-schema.json +38477 -0
- package/schemas/instruction.schema.json +262 -0
- package/schemas/json-schema/SessionPersistence-persisted-admin-session.schema.json +54 -0
- package/schemas/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +51 -0
- package/schemas/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +54 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-config.schema.json +110 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-data.schema.json +229 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-manifest.schema.json +109 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-metadata.schema.json +55 -0
- package/schemas/json-schema/instruction-audience-scope.schema.json +14 -0
- package/schemas/json-schema/instruction-content-type.schema.json +17 -0
- package/schemas/json-schema/instruction-instruction-entry.schema.json +206 -0
- package/schemas/json-schema/instruction-requirement-level.schema.json +16 -0
- package/schemas/manifest.json +78 -0
- package/schemas/manifest.schema.json +33 -0
- package/schemas/usage-batch.schema.json +16 -0
- package/schemas/usage-buckets.schema.json +30 -0
- package/schemas/usage-event.schema.json +17 -0
- package/scripts/copy-dashboard-assets.mjs +170 -0
- package/scripts/setup-hooks.cjs +28 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP Instructions Search Handler
|
|
4
|
+
*
|
|
5
|
+
* Provides keyword-based search functionality for discovering instruction IDs.
|
|
6
|
+
* This is the PRIMARY discovery tool for MCP clients to find relevant instructions
|
|
7
|
+
* before retrieving detailed content via instructions/get or index_dispatch.
|
|
8
|
+
*
|
|
9
|
+
* Search Strategy:
|
|
10
|
+
* - Multi-keyword support with configurable matching
|
|
11
|
+
* - Searches instruction titles, bodies, and optionally categories
|
|
12
|
+
* - Returns lightweight ID list for efficient follow-up queries
|
|
13
|
+
* - Case-insensitive by default with case-sensitive option
|
|
14
|
+
* - Relevance scoring based on match frequency and location
|
|
15
|
+
*
|
|
16
|
+
* MCP Compliance:
|
|
17
|
+
* - Full JSON Schema validation
|
|
18
|
+
* - Structured error responses
|
|
19
|
+
* - Proper tool registration
|
|
20
|
+
* - Input sanitization and limits
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.handleInstructionsSearch = handleInstructionsSearch;
|
|
24
|
+
exports.buildAfterRetrievalMeta = buildAfterRetrievalMeta;
|
|
25
|
+
const registry_1 = require("../server/registry");
|
|
26
|
+
const logger_1 = require("./logger");
|
|
27
|
+
const indexContext_1 = require("./indexContext");
|
|
28
|
+
const errors_1 = require("./errors");
|
|
29
|
+
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
30
|
+
const embeddingService_1 = require("./embeddingService");
|
|
31
|
+
const SEARCH_SCHEMA = {
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: ['keywords'],
|
|
34
|
+
properties: {
|
|
35
|
+
keywords: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 100 }, minItems: 1, maxItems: 10, description: 'Array of search keywords (each word separately for best results)' },
|
|
36
|
+
mode: { type: 'string', enum: ['keyword', 'regex', 'semantic'], default: 'keyword', description: 'Search mode: keyword (substring match), regex (treat keywords as regex patterns), or semantic (embedding-based similarity). Default is semantic when INDEX_SERVER_SEMANTIC_ENABLED=1, otherwise keyword.' },
|
|
37
|
+
limit: { type: 'number', minimum: 1, maximum: 100, default: 50 },
|
|
38
|
+
includeCategories: { type: 'boolean', default: false },
|
|
39
|
+
caseSensitive: { type: 'boolean', default: false },
|
|
40
|
+
contentType: { type: 'string', enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'] }
|
|
41
|
+
},
|
|
42
|
+
example: { keywords: ['build', 'validate', 'discipline'], limit: 10, includeCategories: true }
|
|
43
|
+
};
|
|
44
|
+
const VALID_MODES = ['keyword', 'regex', 'semantic'];
|
|
45
|
+
function normalizeSearchText(text, caseSensitive) {
|
|
46
|
+
const normalized = text
|
|
47
|
+
.normalize('NFD')
|
|
48
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
49
|
+
.replace(/[-_/\\.:]+/g, ' ')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim();
|
|
52
|
+
return caseSensitive ? normalized : normalized.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
function countSubstringMatches(text, searchTerm) {
|
|
55
|
+
if (!searchTerm || !text)
|
|
56
|
+
return 0;
|
|
57
|
+
let count = 0;
|
|
58
|
+
let offset = 0;
|
|
59
|
+
while (offset <= text.length) {
|
|
60
|
+
const index = text.indexOf(searchTerm, offset);
|
|
61
|
+
if (index === -1)
|
|
62
|
+
break;
|
|
63
|
+
count++;
|
|
64
|
+
offset = index + Math.max(searchTerm.length, 1);
|
|
65
|
+
}
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
function findOrderedKeywordSpan(text, keywords) {
|
|
69
|
+
if (!text || keywords.length < 2)
|
|
70
|
+
return undefined;
|
|
71
|
+
let searchFrom = 0;
|
|
72
|
+
let firstIndex;
|
|
73
|
+
let lastEnd = 0;
|
|
74
|
+
for (const keyword of keywords) {
|
|
75
|
+
const index = text.indexOf(keyword, searchFrom);
|
|
76
|
+
if (index === -1)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (firstIndex === undefined)
|
|
79
|
+
firstIndex = index;
|
|
80
|
+
lastEnd = index + keyword.length;
|
|
81
|
+
searchFrom = index + keyword.length;
|
|
82
|
+
}
|
|
83
|
+
return firstIndex === undefined ? undefined : lastEnd - firstIndex;
|
|
84
|
+
}
|
|
85
|
+
function calculateOrderedProximityBonus(text, keywords, maxBonus) {
|
|
86
|
+
const span = findOrderedKeywordSpan(text, keywords);
|
|
87
|
+
if (span === undefined)
|
|
88
|
+
return 0;
|
|
89
|
+
const minimumSpan = keywords.reduce((total, keyword) => total + keyword.length, 0);
|
|
90
|
+
const gap = Math.max(0, span - minimumSpan);
|
|
91
|
+
return Math.max(0, maxBonus - Math.floor(gap / 6));
|
|
92
|
+
}
|
|
93
|
+
function buildInstructionSearchText(instruction, caseSensitive, includeCategories) {
|
|
94
|
+
const parts = [
|
|
95
|
+
instruction.id,
|
|
96
|
+
instruction.title,
|
|
97
|
+
instruction.semanticSummary || '',
|
|
98
|
+
instruction.body,
|
|
99
|
+
];
|
|
100
|
+
if (includeCategories) {
|
|
101
|
+
parts.push((instruction.categories || []).join(' '));
|
|
102
|
+
}
|
|
103
|
+
return normalizeSearchText(parts.join(' '), caseSensitive);
|
|
104
|
+
}
|
|
105
|
+
function buildKeywordSearchContext(instructions, keywords, caseSensitive, includeCategories) {
|
|
106
|
+
const normalizedKeywords = [...new Set(keywords
|
|
107
|
+
.map(keyword => normalizeSearchText(keyword, caseSensitive))
|
|
108
|
+
.filter(keyword => keyword.length > 0))];
|
|
109
|
+
const keywordWeights = new Map();
|
|
110
|
+
if (normalizedKeywords.length === 0) {
|
|
111
|
+
return { normalizedKeywords, keywordWeights, totalKeywordWeight: 0 };
|
|
112
|
+
}
|
|
113
|
+
const searchableDocuments = instructions.map(instruction => buildInstructionSearchText(instruction, caseSensitive, includeCategories));
|
|
114
|
+
const documentCount = Math.max(searchableDocuments.length, 1);
|
|
115
|
+
for (const keyword of normalizedKeywords) {
|
|
116
|
+
const documentFrequency = searchableDocuments.reduce((count, documentText) => (documentText.includes(keyword) ? count + 1 : count), 0);
|
|
117
|
+
const weight = 1 + (Math.log((documentCount + 1) / (documentFrequency + 1)) * 1.5);
|
|
118
|
+
keywordWeights.set(keyword, Math.min(4, Math.max(1, weight)));
|
|
119
|
+
}
|
|
120
|
+
const totalKeywordWeight = normalizedKeywords.reduce((sum, keyword) => sum + (keywordWeights.get(keyword) ?? 1), 0);
|
|
121
|
+
return { normalizedKeywords, keywordWeights, totalKeywordWeight };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Calculate relevance score for an instruction based on keyword matches
|
|
125
|
+
*/
|
|
126
|
+
function calculateRelevance(instruction, keywords, caseSensitive, includeCategories, mode = 'keyword', keywordContext) {
|
|
127
|
+
let score = 0;
|
|
128
|
+
const matchedFieldSet = new Set();
|
|
129
|
+
const prepareText = (text) => caseSensitive ? text : text.toLowerCase();
|
|
130
|
+
const preparedKeywords = keywords.map(k => mode === 'keyword' ? prepareText(k) : k);
|
|
131
|
+
const regexFlags = caseSensitive ? 'g' : 'gi';
|
|
132
|
+
// Build matchers: regex mode uses raw patterns, keyword mode uses substring/escaped regex
|
|
133
|
+
const testMatch = (text, keyword) => {
|
|
134
|
+
if (mode === 'regex') {
|
|
135
|
+
return new RegExp(keyword, caseSensitive ? '' : 'i').test(text);
|
|
136
|
+
}
|
|
137
|
+
return prepareText(text).includes(prepareText(keyword));
|
|
138
|
+
};
|
|
139
|
+
if (mode === 'keyword') {
|
|
140
|
+
const normalizedId = normalizeSearchText(instruction.id, caseSensitive);
|
|
141
|
+
const normalizedTitle = normalizeSearchText(instruction.title, caseSensitive);
|
|
142
|
+
const normalizedSummary = normalizeSearchText(instruction.semanticSummary || '', caseSensitive);
|
|
143
|
+
const normalizedBody = normalizeSearchText(instruction.body, caseSensitive);
|
|
144
|
+
const normalizedCategoryValues = (instruction.categories || []).map(category => normalizeSearchText(category, caseSensitive));
|
|
145
|
+
const normalizedCategoryText = normalizedCategoryValues.join(' ');
|
|
146
|
+
const normalizedKeywords = keywordContext?.normalizedKeywords ?? keywords
|
|
147
|
+
.map(keyword => normalizeSearchText(keyword, caseSensitive))
|
|
148
|
+
.filter(keyword => keyword.length > 0);
|
|
149
|
+
const keywordWeights = keywordContext?.keywordWeights ?? new Map();
|
|
150
|
+
const uniqueMatches = new Set();
|
|
151
|
+
for (const normalizedKeyword of normalizedKeywords) {
|
|
152
|
+
if (!normalizedKeyword)
|
|
153
|
+
continue;
|
|
154
|
+
const keywordWeight = keywordWeights.get(normalizedKeyword) ?? 1;
|
|
155
|
+
let keywordMatched = false;
|
|
156
|
+
if (normalizedId === normalizedKeyword) {
|
|
157
|
+
score += 40 * keywordWeight;
|
|
158
|
+
matchedFieldSet.add('id');
|
|
159
|
+
keywordMatched = true;
|
|
160
|
+
}
|
|
161
|
+
else if (normalizedId.startsWith(normalizedKeyword)) {
|
|
162
|
+
score += 28 * keywordWeight;
|
|
163
|
+
matchedFieldSet.add('id');
|
|
164
|
+
keywordMatched = true;
|
|
165
|
+
}
|
|
166
|
+
else if (normalizedId.includes(normalizedKeyword)) {
|
|
167
|
+
score += 18 * keywordWeight;
|
|
168
|
+
matchedFieldSet.add('id');
|
|
169
|
+
keywordMatched = true;
|
|
170
|
+
}
|
|
171
|
+
const titleMatches = countSubstringMatches(normalizedTitle, normalizedKeyword);
|
|
172
|
+
if (titleMatches > 0) {
|
|
173
|
+
score += (normalizedTitle === normalizedKeyword ? 20 : Math.min(titleMatches * 10, 20)) * keywordWeight;
|
|
174
|
+
matchedFieldSet.add('title');
|
|
175
|
+
keywordMatched = true;
|
|
176
|
+
}
|
|
177
|
+
const summaryMatches = countSubstringMatches(normalizedSummary, normalizedKeyword);
|
|
178
|
+
if (summaryMatches > 0) {
|
|
179
|
+
score += (normalizedSummary === normalizedKeyword ? 14 : Math.min(summaryMatches * 7, 14)) * keywordWeight;
|
|
180
|
+
matchedFieldSet.add('semanticSummary');
|
|
181
|
+
keywordMatched = true;
|
|
182
|
+
}
|
|
183
|
+
const bodyMatches = countSubstringMatches(normalizedBody, normalizedKeyword);
|
|
184
|
+
if (bodyMatches > 0) {
|
|
185
|
+
score += Math.min(bodyMatches * 2, 20) * keywordWeight;
|
|
186
|
+
matchedFieldSet.add('body');
|
|
187
|
+
keywordMatched = true;
|
|
188
|
+
}
|
|
189
|
+
if (includeCategories && normalizedCategoryValues.length > 0) {
|
|
190
|
+
const exactCategoryMatch = normalizedCategoryValues.some(category => category === normalizedKeyword);
|
|
191
|
+
const categoryMatches = exactCategoryMatch
|
|
192
|
+
? 1
|
|
193
|
+
: countSubstringMatches(normalizedCategoryText, normalizedKeyword);
|
|
194
|
+
if (categoryMatches > 0) {
|
|
195
|
+
score += (exactCategoryMatch ? 8 : Math.min(categoryMatches * 3, 9)) * keywordWeight;
|
|
196
|
+
matchedFieldSet.add('categories');
|
|
197
|
+
keywordMatched = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (keywordMatched)
|
|
201
|
+
uniqueMatches.add(normalizedKeyword);
|
|
202
|
+
}
|
|
203
|
+
if (uniqueMatches.size > 1) {
|
|
204
|
+
const matchedWeight = Array.from(uniqueMatches).reduce((sum, keyword) => sum + (keywordWeights.get(keyword) ?? 1), 0);
|
|
205
|
+
const strongestWeight = Math.max(...Array.from(uniqueMatches).map(keyword => keywordWeights.get(keyword) ?? 1));
|
|
206
|
+
score += (matchedWeight - strongestWeight) * 5;
|
|
207
|
+
if (keywordContext && keywordContext.totalKeywordWeight > 0) {
|
|
208
|
+
score += (matchedWeight / keywordContext.totalKeywordWeight) * 8;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (normalizedKeywords.length > 1) {
|
|
212
|
+
score += calculateOrderedProximityBonus(normalizedId, normalizedKeywords, 18);
|
|
213
|
+
score += calculateOrderedProximityBonus(normalizedTitle, normalizedKeywords, 14);
|
|
214
|
+
score += calculateOrderedProximityBonus(normalizedSummary, normalizedKeywords, 10);
|
|
215
|
+
score += calculateOrderedProximityBonus(normalizedBody, normalizedKeywords, 8);
|
|
216
|
+
if (includeCategories) {
|
|
217
|
+
score += calculateOrderedProximityBonus(normalizedCategoryText, normalizedKeywords, 4);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { score, matchedFields: Array.from(matchedFieldSet) };
|
|
221
|
+
}
|
|
222
|
+
const countMatches = (text, keyword) => {
|
|
223
|
+
if (mode === 'regex') {
|
|
224
|
+
return (text.match(new RegExp(keyword, regexFlags)) || []).length;
|
|
225
|
+
}
|
|
226
|
+
return (text.match(new RegExp(escapeRegex(keyword), regexFlags)) || []).length;
|
|
227
|
+
};
|
|
228
|
+
let idMatches = 0;
|
|
229
|
+
for (const keyword of preparedKeywords) {
|
|
230
|
+
if (testMatch(instruction.id, keyword)) {
|
|
231
|
+
idMatches += countMatches(instruction.id, keyword);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (idMatches > 0) {
|
|
235
|
+
score += idMatches * 18;
|
|
236
|
+
matchedFieldSet.add('id');
|
|
237
|
+
}
|
|
238
|
+
let titleMatches = 0;
|
|
239
|
+
for (const keyword of preparedKeywords) {
|
|
240
|
+
titleMatches += countMatches(instruction.title, keyword);
|
|
241
|
+
}
|
|
242
|
+
if (titleMatches > 0) {
|
|
243
|
+
score += titleMatches * 10;
|
|
244
|
+
matchedFieldSet.add('title');
|
|
245
|
+
}
|
|
246
|
+
let summaryMatches = 0;
|
|
247
|
+
for (const keyword of preparedKeywords) {
|
|
248
|
+
summaryMatches += countMatches(instruction.semanticSummary || '', keyword);
|
|
249
|
+
}
|
|
250
|
+
if (summaryMatches > 0) {
|
|
251
|
+
score += Math.min(summaryMatches * 7, 14);
|
|
252
|
+
matchedFieldSet.add('semanticSummary');
|
|
253
|
+
}
|
|
254
|
+
let bodyMatches = 0;
|
|
255
|
+
for (const keyword of preparedKeywords) {
|
|
256
|
+
bodyMatches += countMatches(instruction.body, keyword);
|
|
257
|
+
}
|
|
258
|
+
if (bodyMatches > 0) {
|
|
259
|
+
score += Math.min(bodyMatches * 2, 20);
|
|
260
|
+
matchedFieldSet.add('body');
|
|
261
|
+
}
|
|
262
|
+
if (includeCategories && instruction.categories?.length) {
|
|
263
|
+
const categoryText = instruction.categories.join(' ');
|
|
264
|
+
let categoryMatches = 0;
|
|
265
|
+
for (const keyword of preparedKeywords) {
|
|
266
|
+
categoryMatches += countMatches(categoryText, keyword);
|
|
267
|
+
}
|
|
268
|
+
if (categoryMatches > 0) {
|
|
269
|
+
score += Math.min(categoryMatches * 3, 9);
|
|
270
|
+
matchedFieldSet.add('categories');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const uniqueMatches = new Set();
|
|
274
|
+
for (const keyword of preparedKeywords) {
|
|
275
|
+
if (testMatch(instruction.id, keyword) ||
|
|
276
|
+
testMatch(instruction.title, keyword) ||
|
|
277
|
+
testMatch(instruction.semanticSummary || '', keyword) ||
|
|
278
|
+
testMatch(instruction.body, keyword) ||
|
|
279
|
+
(includeCategories && instruction.categories?.some((cat) => testMatch(cat, keyword)))) {
|
|
280
|
+
uniqueMatches.add(keyword);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (uniqueMatches.size > 1) {
|
|
284
|
+
score += (uniqueMatches.size - 1) * 5;
|
|
285
|
+
}
|
|
286
|
+
return { score, matchedFields: Array.from(matchedFieldSet) };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Escape special regex characters
|
|
290
|
+
*/
|
|
291
|
+
function escapeRegex(string) {
|
|
292
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Load and search instructions from the index
|
|
296
|
+
*/
|
|
297
|
+
function performSearch(params) {
|
|
298
|
+
const startTime = performance.now();
|
|
299
|
+
// Load instruction index state
|
|
300
|
+
const state = (0, indexContext_1.ensureLoaded)();
|
|
301
|
+
if (!state || !state.list) {
|
|
302
|
+
throw new Error('instruction index not available');
|
|
303
|
+
}
|
|
304
|
+
// Ensure defaults are explicitly applied
|
|
305
|
+
const keywords = params.keywords;
|
|
306
|
+
const mode = params.mode ?? ((0, runtimeConfig_1.getRuntimeConfig)().semantic.enabled ? 'semantic' : 'keyword');
|
|
307
|
+
const limit = params.limit ?? 50;
|
|
308
|
+
const includeCategories = params.includeCategories ?? false;
|
|
309
|
+
const caseSensitive = params.caseSensitive ?? false;
|
|
310
|
+
const contentType = params.contentType;
|
|
311
|
+
// Validate contentType if provided
|
|
312
|
+
const validContentTypes = ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'];
|
|
313
|
+
if (contentType && !validContentTypes.includes(contentType)) {
|
|
314
|
+
throw new Error(`Invalid contentType: must be one of ${validContentTypes.join(', ')}`);
|
|
315
|
+
}
|
|
316
|
+
// Validate and sanitize keywords
|
|
317
|
+
const sanitizedKeywords = keywords
|
|
318
|
+
.filter(k => typeof k === 'string' && k.trim().length > 0)
|
|
319
|
+
.map(k => k.trim())
|
|
320
|
+
.slice(0, 10); // Enforce max 10 keywords
|
|
321
|
+
if (sanitizedKeywords.length === 0) {
|
|
322
|
+
throw new Error('At least one valid keyword is required');
|
|
323
|
+
}
|
|
324
|
+
const results = [];
|
|
325
|
+
const searchableInstructions = state.list.filter(instruction => {
|
|
326
|
+
if (!contentType)
|
|
327
|
+
return true;
|
|
328
|
+
const instrContentType = instruction.contentType || 'instruction';
|
|
329
|
+
return instrContentType === contentType;
|
|
330
|
+
});
|
|
331
|
+
const keywordContext = mode === 'keyword'
|
|
332
|
+
? buildKeywordSearchContext(searchableInstructions, sanitizedKeywords, caseSensitive, includeCategories)
|
|
333
|
+
: undefined;
|
|
334
|
+
// Search through all instructions
|
|
335
|
+
for (const instruction of state.list) {
|
|
336
|
+
// Filter by contentType if specified
|
|
337
|
+
if (contentType) {
|
|
338
|
+
const instrContentType = instruction.contentType || 'instruction';
|
|
339
|
+
if (instrContentType !== contentType) {
|
|
340
|
+
continue; // Skip instructions that don't match the contentType filter
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const { score, matchedFields } = calculateRelevance(instruction, sanitizedKeywords, caseSensitive, includeCategories, mode, keywordContext);
|
|
344
|
+
if (score > 0) {
|
|
345
|
+
results.push({
|
|
346
|
+
instructionId: instruction.id,
|
|
347
|
+
relevanceScore: score,
|
|
348
|
+
matchedFields
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Sort by relevance score (descending) and apply limit
|
|
353
|
+
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
354
|
+
const limitedResults = results.slice(0, Math.min(limit, 100));
|
|
355
|
+
const executionTime = performance.now() - startTime;
|
|
356
|
+
(0, logger_1.logInfo)(`[search] Search completed: ${sanitizedKeywords.length} keywords, ${limitedResults.length}/${results.length} results, ${executionTime}ms`);
|
|
357
|
+
return {
|
|
358
|
+
results: limitedResults,
|
|
359
|
+
totalMatches: results.length,
|
|
360
|
+
query: {
|
|
361
|
+
keywords: sanitizedKeywords,
|
|
362
|
+
mode,
|
|
363
|
+
limit: Math.min(limit, 100),
|
|
364
|
+
includeCategories,
|
|
365
|
+
caseSensitive,
|
|
366
|
+
contentType
|
|
367
|
+
},
|
|
368
|
+
executionTimeMs: executionTime
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Perform semantic (embedding-based) search.
|
|
373
|
+
* Computes cosine similarity between query embedding and instruction embeddings.
|
|
374
|
+
*/
|
|
375
|
+
async function performSemanticSearch(params) {
|
|
376
|
+
const startTime = performance.now();
|
|
377
|
+
const state = (0, indexContext_1.ensureLoaded)();
|
|
378
|
+
if (!state || !state.list) {
|
|
379
|
+
throw new Error('instruction index not available');
|
|
380
|
+
}
|
|
381
|
+
const cfg = (0, runtimeConfig_1.getRuntimeConfig)().semantic;
|
|
382
|
+
const queryText = params.keywords.join(' ');
|
|
383
|
+
const limit = Math.min(params.limit ?? 50, 100);
|
|
384
|
+
const contentType = params.contentType;
|
|
385
|
+
(0, logger_1.logInfo)(`[search] Semantic search starting: query="${queryText}", device=${cfg.device}, model=${cfg.model}, index=${state.list.length} entries`);
|
|
386
|
+
// Get embeddings for query and index
|
|
387
|
+
const embedStart = performance.now();
|
|
388
|
+
const queryVec = await (0, embeddingService_1.embedText)(queryText, cfg.model, cfg.cacheDir, cfg.device, cfg.localOnly);
|
|
389
|
+
(0, logger_1.logDebug)(`[search] Query embedding computed in ${(performance.now() - embedStart).toFixed(1)}ms, dimensions=${queryVec.length}`);
|
|
390
|
+
const indexEmbedStart = performance.now();
|
|
391
|
+
const instrEmbeddings = await (0, embeddingService_1.getInstructionEmbeddings)(state.list, state.hash, cfg.embeddingPath, cfg.model, cfg.cacheDir, cfg.device, cfg.localOnly);
|
|
392
|
+
(0, logger_1.logDebug)(`[search] index embeddings ready in ${(performance.now() - indexEmbedStart).toFixed(1)}ms, entries=${Object.keys(instrEmbeddings).length}`);
|
|
393
|
+
// Score each instruction
|
|
394
|
+
const scored = [];
|
|
395
|
+
for (const instruction of state.list) {
|
|
396
|
+
if (contentType) {
|
|
397
|
+
const instrContentType = instruction.contentType || 'instruction';
|
|
398
|
+
if (instrContentType !== contentType)
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const vec = instrEmbeddings[instruction.id];
|
|
402
|
+
if (!vec)
|
|
403
|
+
continue;
|
|
404
|
+
const similarity = (0, embeddingService_1.cosineSimilarity)(queryVec, vec);
|
|
405
|
+
if (similarity > 0) {
|
|
406
|
+
scored.push({
|
|
407
|
+
instructionId: instruction.id,
|
|
408
|
+
relevanceScore: Math.round(similarity * 100), // normalize to 0-100 scale
|
|
409
|
+
matchedFields: ['body'], // semantic matches are conceptual, mark as body
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
scored.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
414
|
+
const limitedResults = scored.slice(0, limit);
|
|
415
|
+
const executionTime = performance.now() - startTime;
|
|
416
|
+
(0, logger_1.logInfo)(`[search] Semantic search completed: "${queryText}", ${limitedResults.length}/${scored.length} results, ${executionTime}ms`);
|
|
417
|
+
return {
|
|
418
|
+
results: limitedResults,
|
|
419
|
+
totalMatches: scored.length,
|
|
420
|
+
query: {
|
|
421
|
+
keywords: params.keywords,
|
|
422
|
+
mode: 'semantic',
|
|
423
|
+
limit,
|
|
424
|
+
includeCategories: params.includeCategories ?? false,
|
|
425
|
+
caseSensitive: params.caseSensitive ?? false,
|
|
426
|
+
contentType,
|
|
427
|
+
},
|
|
428
|
+
executionTimeMs: executionTime,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* MCP Handler for index_search
|
|
433
|
+
*/
|
|
434
|
+
async function handleInstructionsSearch(params) {
|
|
435
|
+
try {
|
|
436
|
+
// Input validation
|
|
437
|
+
if (!params || typeof params !== 'object') {
|
|
438
|
+
throw new Error('Invalid parameters: expected object');
|
|
439
|
+
}
|
|
440
|
+
if (!Array.isArray(params.keywords)) {
|
|
441
|
+
throw new Error('Invalid keywords: expected array');
|
|
442
|
+
}
|
|
443
|
+
if (params.keywords.length === 0) {
|
|
444
|
+
throw new Error('At least one keyword is required');
|
|
445
|
+
}
|
|
446
|
+
if (params.keywords.length > 10) {
|
|
447
|
+
throw new Error('Maximum 10 keywords allowed');
|
|
448
|
+
}
|
|
449
|
+
// Validate keyword strings
|
|
450
|
+
for (const keyword of params.keywords) {
|
|
451
|
+
if (typeof keyword !== 'string') {
|
|
452
|
+
throw new Error('All keywords must be strings');
|
|
453
|
+
}
|
|
454
|
+
if (keyword.trim().length === 0) {
|
|
455
|
+
throw new Error('Keywords cannot be empty');
|
|
456
|
+
}
|
|
457
|
+
if (keyword.length > 100 && params.mode !== 'regex') {
|
|
458
|
+
throw new Error('Keywords cannot exceed 100 characters');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Validate optional parameters
|
|
462
|
+
if (params.limit !== undefined) {
|
|
463
|
+
if (typeof params.limit !== 'number' || params.limit < 1 || params.limit > 100) {
|
|
464
|
+
throw new Error('Limit must be a number between 1 and 100');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (params.includeCategories !== undefined && typeof params.includeCategories !== 'boolean') {
|
|
468
|
+
throw new Error('includeCategories must be a boolean');
|
|
469
|
+
}
|
|
470
|
+
if (params.caseSensitive !== undefined && typeof params.caseSensitive !== 'boolean') {
|
|
471
|
+
throw new Error('caseSensitive must be a boolean');
|
|
472
|
+
}
|
|
473
|
+
if (params.contentType !== undefined) {
|
|
474
|
+
if (typeof params.contentType !== 'string') {
|
|
475
|
+
throw new Error('contentType must be a string');
|
|
476
|
+
}
|
|
477
|
+
const validContentTypes = ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'];
|
|
478
|
+
if (!validContentTypes.includes(params.contentType)) {
|
|
479
|
+
throw new Error(`contentType must be one of: ${validContentTypes.join(', ')}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Validate mode parameter
|
|
483
|
+
const mode = params.mode ?? ((0, runtimeConfig_1.getRuntimeConfig)().semantic.enabled ? 'semantic' : 'keyword');
|
|
484
|
+
if (!VALID_MODES.includes(mode)) {
|
|
485
|
+
throw new Error(`Invalid mode: must be one of ${VALID_MODES.join(', ')}`);
|
|
486
|
+
}
|
|
487
|
+
(0, logger_1.logInfo)(`[search] Search request: mode=${mode}, keywords=[${params.keywords.join(', ')}], limit=${params.limit ?? 50}, contentType=${params.contentType ?? 'any'}`);
|
|
488
|
+
// Regex mode validation: pattern safety checks
|
|
489
|
+
if (mode === 'regex') {
|
|
490
|
+
for (const keyword of params.keywords) {
|
|
491
|
+
if (keyword.length > 200) {
|
|
492
|
+
throw new Error('Regex patterns must not exceed 200 characters to prevent ReDoS');
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
new RegExp(keyword);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
throw new Error(`Invalid regex pattern "${keyword}": check syntax and try again`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Semantic mode: check feature flag
|
|
503
|
+
if (mode === 'semantic') {
|
|
504
|
+
const cfg = (0, runtimeConfig_1.getRuntimeConfig)().semantic;
|
|
505
|
+
(0, logger_1.logDebug)(`[search] Semantic config: enabled=${cfg.enabled}, device=${cfg.device}, model=${cfg.model}, localOnly=${cfg.localOnly}`);
|
|
506
|
+
if (!cfg.enabled) {
|
|
507
|
+
(0, logger_1.logWarn)('[search] Semantic search requested but INDEX_SERVER_SEMANTIC_ENABLED is not set');
|
|
508
|
+
throw new Error('Semantic search mode is disabled. Set INDEX_SERVER_SEMANTIC_ENABLED=1 to enable.');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Ensure case-insensitive search by default
|
|
512
|
+
const searchParams = {
|
|
513
|
+
keywords: params.keywords,
|
|
514
|
+
mode: mode,
|
|
515
|
+
limit: params.limit,
|
|
516
|
+
includeCategories: params.includeCategories,
|
|
517
|
+
caseSensitive: params.caseSensitive ?? false, // Explicit default to false for case-insensitive search
|
|
518
|
+
contentType: params.contentType
|
|
519
|
+
};
|
|
520
|
+
// Semantic mode: embedding-based similarity search
|
|
521
|
+
if (mode === 'semantic') {
|
|
522
|
+
try {
|
|
523
|
+
const result = await performSemanticSearch(searchParams);
|
|
524
|
+
if (result.totalMatches > 0) {
|
|
525
|
+
result._meta = buildAfterRetrievalMeta();
|
|
526
|
+
autoTrackSearchResults(result.results);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
result.hints = buildSearchHints(searchParams);
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
// Graceful degradation: fall back to keyword mode
|
|
535
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
536
|
+
const errStack = err instanceof Error ? err.stack : undefined;
|
|
537
|
+
(0, logger_1.logError)(`[search] Semantic search failed, falling back to keyword mode: ${errMsg}`);
|
|
538
|
+
if (errStack) {
|
|
539
|
+
(0, logger_1.logDebug)(`[search] Semantic fallback stack trace: ${errStack}`);
|
|
540
|
+
}
|
|
541
|
+
searchParams.mode = 'keyword';
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const result = performSearch(searchParams);
|
|
545
|
+
// Auto-tokenize fallback: if no results and any keyword contains whitespace,
|
|
546
|
+
// split multi-word keywords into individual tokens and retry.
|
|
547
|
+
// Skip auto-tokenize for regex mode — patterns should be used as-is.
|
|
548
|
+
if (result.totalMatches === 0 && mode !== 'regex') {
|
|
549
|
+
const hasMultiWord = searchParams.keywords.some(k => k.trim().includes(' '));
|
|
550
|
+
if (hasMultiWord) {
|
|
551
|
+
const tokenized = searchParams.keywords
|
|
552
|
+
.flatMap(k => k.split(/\s+/))
|
|
553
|
+
.map(t => t.trim())
|
|
554
|
+
.filter(t => t.length > 0);
|
|
555
|
+
// Deduplicate and enforce limits
|
|
556
|
+
const unique = [...new Set(tokenized)].slice(0, 10);
|
|
557
|
+
if (unique.length > 0 && unique.length !== searchParams.keywords.length || unique.some((t, i) => t !== searchParams.keywords[i])) {
|
|
558
|
+
(0, logger_1.logInfo)(`[search] Auto-tokenizing keywords: [${searchParams.keywords.join(', ')}] -> [${unique.join(', ')}]`);
|
|
559
|
+
const retryParams = { ...searchParams, keywords: unique };
|
|
560
|
+
const retryResult = performSearch(retryParams);
|
|
561
|
+
retryResult.autoTokenized = true;
|
|
562
|
+
if (retryResult.totalMatches === 0) {
|
|
563
|
+
retryResult.hints = buildSearchHints(retryParams);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
autoTrackSearchResults(retryResult.results);
|
|
567
|
+
}
|
|
568
|
+
return retryResult;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Attach hints on zero-result responses
|
|
573
|
+
if (result.totalMatches === 0) {
|
|
574
|
+
result.hints = buildSearchHints(searchParams);
|
|
575
|
+
}
|
|
576
|
+
// Attach _meta hints on successful (non-empty) responses
|
|
577
|
+
if (result.totalMatches > 0) {
|
|
578
|
+
result._meta = buildAfterRetrievalMeta();
|
|
579
|
+
autoTrackSearchResults(result.results);
|
|
580
|
+
}
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown search error';
|
|
585
|
+
(0, logger_1.logWarn)(`[search] Search error: ${errorMessage}`);
|
|
586
|
+
(0, errors_1.semanticError)(-32602, `Search failed: ${errorMessage}`, {
|
|
587
|
+
reason: 'invalid_params',
|
|
588
|
+
hint: 'Use q as a single phrase for index_dispatch search, or pass keywords as an array for index_search. Multi-word phrases are auto-tokenized only on retry.',
|
|
589
|
+
schema: SEARCH_SCHEMA,
|
|
590
|
+
example: { keywords: ['build', 'validate'], includeCategories: true }
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Fire-and-forget usage tracking for search results.
|
|
596
|
+
* Tracks top results only to avoid excessive writes.
|
|
597
|
+
*/
|
|
598
|
+
function autoTrackSearchResults(results) {
|
|
599
|
+
if (!(0, runtimeConfig_1.getRuntimeConfig)().index?.autoUsageTrack)
|
|
600
|
+
return;
|
|
601
|
+
const top = results.slice(0, 10);
|
|
602
|
+
for (const r of top) {
|
|
603
|
+
try {
|
|
604
|
+
(0, indexContext_1.incrementUsage)(r.instructionId, { action: 'search' });
|
|
605
|
+
}
|
|
606
|
+
catch { /* fire-and-forget */ }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Build _meta.afterRetrieval hints for MCP clients.
|
|
611
|
+
* Encourages callers to record usage and submit feedback.
|
|
612
|
+
*/
|
|
613
|
+
function buildAfterRetrievalMeta() {
|
|
614
|
+
return {
|
|
615
|
+
afterRetrieval: [
|
|
616
|
+
'Call usage_track with the instruction id and signal (helpful|not-relevant|outdated|applied) to record usage quality.',
|
|
617
|
+
'Call feedback_submit to report issues, request features, or flag outdated content.',
|
|
618
|
+
],
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Build actionable hints for zero-result searches
|
|
623
|
+
*/
|
|
624
|
+
function buildSearchHints(params) {
|
|
625
|
+
const hints = [];
|
|
626
|
+
if (!params.includeCategories) {
|
|
627
|
+
hints.push('Try setting includeCategories: true to also search category tags.');
|
|
628
|
+
}
|
|
629
|
+
if (params.keywords.length === 1) {
|
|
630
|
+
hints.push('Try using multiple shorter keywords instead of one long phrase, e.g. ["build", "validate"] instead of ["build validate"].');
|
|
631
|
+
}
|
|
632
|
+
if (params.keywords.some(k => k.length > 15)) {
|
|
633
|
+
hints.push('Try shorter or more general keywords — substring matching is used, not fuzzy/stemming.');
|
|
634
|
+
}
|
|
635
|
+
if (params.contentType) {
|
|
636
|
+
hints.push(`Remove the contentType filter ("${params.contentType}") to search across all content types.`);
|
|
637
|
+
}
|
|
638
|
+
if (params.mode !== 'regex') {
|
|
639
|
+
hints.push('Try mode: "regex" to use regex patterns (e.g. "deploy|release" for alternation).');
|
|
640
|
+
}
|
|
641
|
+
hints.push('Use index_dispatch with action="capabilities" to discover other retrieval methods.');
|
|
642
|
+
return hints;
|
|
643
|
+
}
|
|
644
|
+
// Register the handler
|
|
645
|
+
(0, registry_1.registerHandler)('index_search', handleInstructionsSearch);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const registry_1 = require("../server/registry");
|
|
4
|
+
// Test-only primitive returning handler used by feature flag tests to ensure envelope works with non-object values.
|
|
5
|
+
(0, registry_1.registerHandler)('test_primitive', () => 42);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|