@vfarcic/dot-ai 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +1 -0
  2. package/dist/core/ai-provider.interface.d.ts +12 -8
  3. package/dist/core/ai-provider.interface.d.ts.map +1 -1
  4. package/dist/core/artifacthub.d.ts +1 -1
  5. package/dist/core/artifacthub.d.ts.map +1 -1
  6. package/dist/core/base-vector-service.d.ts +22 -9
  7. package/dist/core/base-vector-service.d.ts.map +1 -1
  8. package/dist/core/base-vector-service.js +106 -37
  9. package/dist/core/capabilities.d.ts.map +1 -1
  10. package/dist/core/capabilities.js +5 -2
  11. package/dist/core/capability-operations.d.ts +55 -7
  12. package/dist/core/capability-operations.d.ts.map +1 -1
  13. package/dist/core/capability-operations.js +1 -3
  14. package/dist/core/capability-scan-workflow.d.ts +64 -8
  15. package/dist/core/capability-scan-workflow.d.ts.map +1 -1
  16. package/dist/core/capability-scan-workflow.js +14 -13
  17. package/dist/core/capability-tools.d.ts +1 -1
  18. package/dist/core/capability-tools.d.ts.map +1 -1
  19. package/dist/core/capability-tools.js +1 -1
  20. package/dist/core/capability-vector-service.d.ts +3 -4
  21. package/dist/core/capability-vector-service.d.ts.map +1 -1
  22. package/dist/core/capability-vector-service.js +2 -2
  23. package/dist/core/command-executor.d.ts +3 -4
  24. package/dist/core/command-executor.d.ts.map +1 -1
  25. package/dist/core/command-executor.js +8 -4
  26. package/dist/core/crd-availability.d.ts +3 -5
  27. package/dist/core/crd-availability.d.ts.map +1 -1
  28. package/dist/core/crd-availability.js +8 -18
  29. package/dist/core/deploy-operation.d.ts +6 -5
  30. package/dist/core/deploy-operation.d.ts.map +1 -1
  31. package/dist/core/deploy-operation.js +16 -10
  32. package/dist/core/discovery.d.ts +6 -14
  33. package/dist/core/discovery.d.ts.map +1 -1
  34. package/dist/core/discovery.js +35 -51
  35. package/dist/core/embedding-service.d.ts.map +1 -1
  36. package/dist/core/embedding-service.js +1 -1
  37. package/dist/core/error-handling.d.ts +13 -13
  38. package/dist/core/error-handling.d.ts.map +1 -1
  39. package/dist/core/error-handling.js +2 -3
  40. package/dist/core/generic-session-manager.d.ts +2 -2
  41. package/dist/core/generic-session-manager.d.ts.map +1 -1
  42. package/dist/core/helm-types.d.ts +5 -5
  43. package/dist/core/helm-types.d.ts.map +1 -1
  44. package/dist/core/index.d.ts +4 -11
  45. package/dist/core/index.d.ts.map +1 -1
  46. package/dist/core/index.js +8 -14
  47. package/dist/core/knowledge-types.d.ts +114 -0
  48. package/dist/core/knowledge-types.d.ts.map +1 -0
  49. package/dist/core/knowledge-types.js +10 -0
  50. package/dist/core/memory.d.ts +12 -12
  51. package/dist/core/memory.d.ts.map +1 -1
  52. package/dist/core/mermaid-tools.d.ts +24 -1
  53. package/dist/core/mermaid-tools.d.ts.map +1 -1
  54. package/dist/core/mermaid-tools.js +10 -8
  55. package/dist/core/packaging.d.ts +23 -1
  56. package/dist/core/packaging.d.ts.map +1 -1
  57. package/dist/core/pattern-operations.d.ts +32 -9
  58. package/dist/core/pattern-operations.d.ts.map +1 -1
  59. package/dist/core/pattern-operations.js +17 -22
  60. package/dist/core/pattern-vector-service.d.ts +3 -4
  61. package/dist/core/pattern-vector-service.d.ts.map +1 -1
  62. package/dist/core/pattern-vector-service.js +2 -2
  63. package/dist/core/platform-utils.d.ts +2 -2
  64. package/dist/core/platform-utils.d.ts.map +1 -1
  65. package/dist/core/plugin-manager.d.ts +6 -2
  66. package/dist/core/plugin-manager.d.ts.map +1 -1
  67. package/dist/core/plugin-manager.js +9 -16
  68. package/dist/core/plugin-registry.d.ts +59 -0
  69. package/dist/core/plugin-registry.d.ts.map +1 -0
  70. package/dist/core/plugin-registry.js +80 -0
  71. package/dist/core/policy-operations.d.ts +101 -21
  72. package/dist/core/policy-operations.d.ts.map +1 -1
  73. package/dist/core/policy-operations.js +45 -47
  74. package/dist/core/policy-vector-service.d.ts +3 -4
  75. package/dist/core/policy-vector-service.d.ts.map +1 -1
  76. package/dist/core/policy-vector-service.js +2 -2
  77. package/dist/core/providers/host-provider.d.ts +1 -1
  78. package/dist/core/providers/host-provider.d.ts.map +1 -1
  79. package/dist/core/providers/host-provider.js +2 -2
  80. package/dist/core/providers/provider-debug-utils.d.ts +2 -2
  81. package/dist/core/providers/provider-debug-utils.d.ts.map +1 -1
  82. package/dist/core/providers/tool-utils.d.ts +10 -2
  83. package/dist/core/providers/tool-utils.d.ts.map +1 -1
  84. package/dist/core/providers/tool-utils.js +2 -2
  85. package/dist/core/providers/vercel-provider.d.ts.map +1 -1
  86. package/dist/core/providers/vercel-provider.js +29 -23
  87. package/dist/core/resource-tools.d.ts +29 -1
  88. package/dist/core/resource-tools.d.ts.map +1 -1
  89. package/dist/core/resource-vector-service.d.ts +3 -4
  90. package/dist/core/resource-vector-service.d.ts.map +1 -1
  91. package/dist/core/resource-vector-service.js +2 -2
  92. package/dist/core/schema.d.ts +15 -14
  93. package/dist/core/schema.d.ts.map +1 -1
  94. package/dist/core/schema.js +32 -34
  95. package/dist/core/shared-prompt-loader.d.ts +1 -1
  96. package/dist/core/shared-prompt-loader.d.ts.map +1 -1
  97. package/dist/core/solution-cr.js +1 -1
  98. package/dist/core/solution-utils.d.ts +22 -3
  99. package/dist/core/solution-utils.d.ts.map +1 -1
  100. package/dist/core/solution-utils.js +1 -1
  101. package/dist/core/telemetry/client.d.ts +0 -6
  102. package/dist/core/telemetry/client.d.ts.map +1 -1
  103. package/dist/core/telemetry/client.js +6 -17
  104. package/dist/core/telemetry/config.js +1 -1
  105. package/dist/core/telemetry/index.d.ts +1 -1
  106. package/dist/core/telemetry/index.d.ts.map +1 -1
  107. package/dist/core/telemetry/index.js +1 -2
  108. package/dist/core/tracing/tool-tracing.d.ts +1 -1
  109. package/dist/core/tracing/tool-tracing.d.ts.map +1 -1
  110. package/dist/core/unified-creation-session.d.ts +15 -8
  111. package/dist/core/unified-creation-session.d.ts.map +1 -1
  112. package/dist/core/unified-creation-session.js +19 -19
  113. package/dist/core/unified-creation-types.d.ts +2 -2
  114. package/dist/core/unified-creation-types.d.ts.map +1 -1
  115. package/dist/core/visualization.d.ts +1 -1
  116. package/dist/core/visualization.d.ts.map +1 -1
  117. package/dist/core/workflow.d.ts +8 -5
  118. package/dist/core/workflow.d.ts.map +1 -1
  119. package/dist/evaluation/dataset-analyzer.d.ts +13 -7
  120. package/dist/evaluation/dataset-analyzer.d.ts.map +1 -1
  121. package/dist/evaluation/dataset-analyzer.js +1 -1
  122. package/dist/evaluation/datasets/loader.d.ts +2 -2
  123. package/dist/evaluation/datasets/loader.d.ts.map +1 -1
  124. package/dist/evaluation/eval-runner.js +7 -5
  125. package/dist/evaluation/evaluators/base-comparative.d.ts +1 -1
  126. package/dist/evaluation/evaluators/base-comparative.d.ts.map +1 -1
  127. package/dist/evaluation/evaluators/base-comparative.js +4 -3
  128. package/dist/evaluation/evaluators/base.d.ts +5 -5
  129. package/dist/evaluation/evaluators/base.d.ts.map +1 -1
  130. package/dist/evaluation/evaluators/capability-comparative.js +1 -1
  131. package/dist/evaluation/platform-synthesizer.d.ts.map +1 -1
  132. package/dist/interfaces/mcp.d.ts.map +1 -1
  133. package/dist/interfaces/mcp.js +26 -15
  134. package/dist/interfaces/openapi-generator.d.ts +116 -12
  135. package/dist/interfaces/openapi-generator.d.ts.map +1 -1
  136. package/dist/interfaces/openapi-generator.js +490 -199
  137. package/dist/interfaces/rest-api.d.ts +28 -6
  138. package/dist/interfaces/rest-api.d.ts.map +1 -1
  139. package/dist/interfaces/rest-api.js +436 -245
  140. package/dist/interfaces/rest-registry.d.ts +4 -4
  141. package/dist/interfaces/rest-registry.d.ts.map +1 -1
  142. package/dist/interfaces/rest-registry.js +6 -5
  143. package/dist/interfaces/rest-route-registry.d.ts +165 -0
  144. package/dist/interfaces/rest-route-registry.d.ts.map +1 -0
  145. package/dist/interfaces/rest-route-registry.js +230 -0
  146. package/dist/interfaces/routes/index.d.ts +22 -0
  147. package/dist/interfaces/routes/index.d.ts.map +1 -0
  148. package/dist/interfaces/routes/index.js +347 -0
  149. package/dist/interfaces/schemas/common.d.ts +177 -0
  150. package/dist/interfaces/schemas/common.d.ts.map +1 -0
  151. package/dist/interfaces/schemas/common.js +102 -0
  152. package/dist/interfaces/schemas/events.d.ts +131 -0
  153. package/dist/interfaces/schemas/events.d.ts.map +1 -0
  154. package/dist/interfaces/schemas/events.js +66 -0
  155. package/dist/interfaces/schemas/index.d.ts +21 -0
  156. package/dist/interfaces/schemas/index.d.ts.map +1 -0
  157. package/dist/interfaces/schemas/index.js +138 -0
  158. package/dist/interfaces/schemas/knowledge.d.ts +210 -0
  159. package/dist/interfaces/schemas/knowledge.d.ts.map +1 -0
  160. package/dist/interfaces/schemas/knowledge.js +117 -0
  161. package/dist/interfaces/schemas/logs.d.ts +82 -0
  162. package/dist/interfaces/schemas/logs.d.ts.map +1 -0
  163. package/dist/interfaces/schemas/logs.js +46 -0
  164. package/dist/interfaces/schemas/prompts.d.ts +191 -0
  165. package/dist/interfaces/schemas/prompts.d.ts.map +1 -0
  166. package/dist/interfaces/schemas/prompts.js +91 -0
  167. package/dist/interfaces/schemas/resources.d.ts +378 -0
  168. package/dist/interfaces/schemas/resources.d.ts.map +1 -0
  169. package/dist/interfaces/schemas/resources.js +173 -0
  170. package/dist/interfaces/schemas/sessions.d.ts +90 -0
  171. package/dist/interfaces/schemas/sessions.d.ts.map +1 -0
  172. package/dist/interfaces/schemas/sessions.js +56 -0
  173. package/dist/interfaces/schemas/tools.d.ts +194 -0
  174. package/dist/interfaces/schemas/tools.d.ts.map +1 -0
  175. package/dist/interfaces/schemas/tools.js +101 -0
  176. package/dist/interfaces/schemas/visualization.d.ts +373 -0
  177. package/dist/interfaces/schemas/visualization.d.ts.map +1 -0
  178. package/dist/interfaces/schemas/visualization.js +134 -0
  179. package/dist/mcp/server.js +5 -4
  180. package/dist/tools/answer-question.d.ts +1 -1
  181. package/dist/tools/answer-question.d.ts.map +1 -1
  182. package/dist/tools/answer-question.js +9 -8
  183. package/dist/tools/deploy-manifests.d.ts +4 -2
  184. package/dist/tools/deploy-manifests.d.ts.map +1 -1
  185. package/dist/tools/deploy-manifests.js +10 -6
  186. package/dist/tools/generate-manifests.d.ts.map +1 -1
  187. package/dist/tools/generate-manifests.js +28 -20
  188. package/dist/tools/index.d.ts +1 -0
  189. package/dist/tools/index.d.ts.map +1 -1
  190. package/dist/tools/index.js +6 -1
  191. package/dist/tools/manage-knowledge.d.ts +77 -0
  192. package/dist/tools/manage-knowledge.d.ts.map +1 -0
  193. package/dist/tools/manage-knowledge.js +573 -0
  194. package/dist/tools/operate-analysis.d.ts +31 -2
  195. package/dist/tools/operate-analysis.d.ts.map +1 -1
  196. package/dist/tools/operate-execution.d.ts +2 -3
  197. package/dist/tools/operate-execution.d.ts.map +1 -1
  198. package/dist/tools/operate-execution.js +7 -7
  199. package/dist/tools/operate.d.ts +7 -2
  200. package/dist/tools/operate.d.ts.map +1 -1
  201. package/dist/tools/operate.js +2 -2
  202. package/dist/tools/organizational-data.d.ts +30 -4
  203. package/dist/tools/organizational-data.d.ts.map +1 -1
  204. package/dist/tools/organizational-data.js +24 -19
  205. package/dist/tools/project-setup/discovery.d.ts.map +1 -1
  206. package/dist/tools/project-setup/generate-scope.d.ts +1 -1
  207. package/dist/tools/project-setup/generate-scope.d.ts.map +1 -1
  208. package/dist/tools/project-setup/types.d.ts +1 -0
  209. package/dist/tools/project-setup/types.d.ts.map +1 -1
  210. package/dist/tools/prompts.d.ts +28 -2
  211. package/dist/tools/prompts.d.ts.map +1 -1
  212. package/dist/tools/query.d.ts +17 -3
  213. package/dist/tools/query.d.ts.map +1 -1
  214. package/dist/tools/query.js +1 -7
  215. package/dist/tools/recommend.d.ts +24 -6
  216. package/dist/tools/recommend.d.ts.map +1 -1
  217. package/dist/tools/recommend.js +18 -15
  218. package/dist/tools/remediate.d.ts +12 -3
  219. package/dist/tools/remediate.d.ts.map +1 -1
  220. package/dist/tools/remediate.js +22 -14
  221. package/dist/tools/version.d.ts +19 -5
  222. package/dist/tools/version.d.ts.map +1 -1
  223. package/dist/tools/version.js +106 -54
  224. package/package.json +11 -5
  225. package/prompts/knowledge-ask.md +29 -0
  226. package/dist/core/vector-db-service.d.ts +0 -108
  227. package/dist/core/vector-db-service.d.ts.map +0 -1
  228. package/dist/core/vector-db-service.js +0 -647
@@ -42,6 +42,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.RestApiRouter = exports.HttpStatus = void 0;
43
43
  const node_url_1 = require("node:url");
44
44
  const openapi_generator_1 = require("./openapi-generator");
45
+ const rest_route_registry_1 = require("./rest-route-registry");
46
+ const routes_1 = require("./routes");
45
47
  const resource_sync_handler_1 = require("./resource-sync-handler");
46
48
  const prompts_1 = require("../tools/prompts");
47
49
  const generic_session_manager_1 = require("../core/generic-session-manager");
@@ -51,6 +53,8 @@ const ai_provider_factory_1 = require("../core/ai-provider-factory");
51
53
  const capability_tools_1 = require("../core/capability-tools");
52
54
  const resource_tools_1 = require("../core/resource-tools");
53
55
  const mermaid_tools_1 = require("../core/mermaid-tools");
56
+ const plugin_registry_1 = require("../core/plugin-registry");
57
+ const manage_knowledge_1 = require("../tools/manage-knowledge");
54
58
  /**
55
59
  * HTTP status codes for REST responses
56
60
  */
@@ -69,6 +73,7 @@ var HttpStatus;
69
73
  */
70
74
  class RestApiRouter {
71
75
  registry;
76
+ routeRegistry;
72
77
  logger;
73
78
  dotAI;
74
79
  config;
@@ -87,14 +92,23 @@ class RestApiRouter {
87
92
  requestTimeout: 1800000, // 30 minutes for long-running operations (capability scan with slower AI providers)
88
93
  ...config
89
94
  };
90
- // Initialize OpenAPI generator
95
+ // Initialize route registry and register all routes (PRD #354)
96
+ this.routeRegistry = new rest_route_registry_1.RestRouteRegistry(logger);
97
+ (0, routes_1.registerAllRoutes)(this.routeRegistry);
98
+ this.logger.info('REST route registry initialized', {
99
+ routeCount: this.routeRegistry.getRouteCount(),
100
+ tags: this.routeRegistry.getTags(),
101
+ });
102
+ // Initialize OpenAPI generator with route registry (PRD #354)
91
103
  this.openApiGenerator = new openapi_generator_1.OpenApiGenerator(registry, logger, {
92
104
  basePath: this.config.basePath,
93
- apiVersion: this.config.version
94
- });
105
+ apiVersion: this.config.version,
106
+ }, this.routeRegistry);
95
107
  }
96
108
  /**
97
109
  * Handle incoming HTTP requests for REST API
110
+ *
111
+ * PRD #354: Uses route registry for matching, dispatches to handlers based on route path.
98
112
  */
99
113
  async handleRequest(req, res, body) {
100
114
  const requestId = this.generateRequestId();
@@ -115,141 +129,35 @@ class RestApiRouter {
115
129
  return;
116
130
  }
117
131
  }
118
- // Parse URL and route
132
+ // Parse URL
119
133
  const url = new node_url_1.URL(req.url || '/', 'http://localhost');
120
- const pathMatch = this.parseApiPath(url.pathname);
121
- if (!pathMatch) {
122
- await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'API endpoint not found');
134
+ const method = req.method || 'GET';
135
+ // PRD #354: Try route registry first
136
+ const routeMatch = this.routeRegistry.findRoute(method, url.pathname);
137
+ if (routeMatch) {
138
+ this.logger.debug('Route matched via registry', {
139
+ requestId,
140
+ path: routeMatch.route.path,
141
+ method: routeMatch.route.method,
142
+ params: routeMatch.params,
143
+ });
144
+ // Dispatch to handler based on route path
145
+ await this.dispatchRoute(req, res, requestId, routeMatch, url.searchParams, body, startTime);
123
146
  return;
124
147
  }
125
- // Route to appropriate handler
126
- switch (pathMatch.endpoint) {
127
- case 'tools':
128
- if (req.method === 'GET') {
129
- await this.handleToolDiscovery(req, res, requestId, url.searchParams);
130
- }
131
- else {
132
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for tool discovery');
133
- }
134
- break;
135
- case 'tool':
136
- if (req.method === 'POST' && pathMatch.toolName) {
137
- await this.handleToolExecution(req, res, requestId, pathMatch.toolName, body, startTime);
138
- }
139
- else if (req.method !== 'POST') {
140
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for tool execution');
141
- }
142
- else {
143
- await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Tool name is required');
144
- }
145
- break;
146
- case 'openapi':
147
- if (req.method === 'GET') {
148
- await this.handleOpenApiSpec(req, res, requestId);
149
- }
150
- else {
151
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for OpenAPI specification');
152
- }
153
- break;
154
- case 'resources':
155
- if (req.method === 'POST' && pathMatch.action === 'sync') {
156
- await this.handleResourceSyncRequest(req, res, requestId, body);
157
- }
158
- else if (req.method === 'GET' && pathMatch.action === 'kinds') {
159
- await this.handleGetResourceKinds(req, res, requestId, url.searchParams);
160
- }
161
- else if (req.method === 'GET' && pathMatch.action === 'search') {
162
- await this.handleSearchResources(req, res, requestId, url.searchParams);
163
- }
164
- else if (req.method === 'GET' && pathMatch.action === 'list') {
165
- await this.handleListResources(req, res, requestId, url.searchParams);
166
- }
167
- else if (pathMatch.action === 'sync' && req.method !== 'POST') {
168
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for resource sync');
169
- }
170
- else if ((pathMatch.action === 'kinds' || pathMatch.action === 'list' || pathMatch.action === 'search') && req.method !== 'GET') {
171
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for this endpoint');
172
- }
173
- else {
174
- await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown resources endpoint');
175
- }
176
- break;
177
- case 'namespaces':
178
- if (req.method === 'GET') {
179
- await this.handleGetNamespaces(req, res, requestId);
180
- }
181
- else {
182
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for namespaces');
183
- }
184
- break;
185
- case 'resource':
186
- if (req.method === 'GET') {
187
- await this.handleGetResource(req, res, requestId, url.searchParams);
188
- }
189
- else {
190
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for resource');
191
- }
192
- break;
193
- case 'events':
194
- if (req.method === 'GET') {
195
- await this.handleGetEvents(req, res, requestId, url.searchParams);
196
- }
197
- else {
198
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for events');
199
- }
200
- break;
201
- case 'logs':
202
- if (req.method === 'GET') {
203
- await this.handleGetLogs(req, res, requestId, url.searchParams);
204
- }
205
- else {
206
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for logs');
207
- }
208
- break;
209
- case 'prompts':
210
- if (req.method === 'GET') {
211
- await this.handlePromptsListRequest(req, res, requestId);
212
- }
213
- else {
214
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for prompts list');
215
- }
216
- break;
217
- case 'prompt':
218
- if (req.method === 'POST' && pathMatch.promptName) {
219
- await this.handlePromptsGetRequest(req, res, requestId, pathMatch.promptName, body);
220
- }
221
- else if (req.method !== 'POST') {
222
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for prompt get');
223
- }
224
- else {
225
- await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Prompt name is required');
226
- }
227
- break;
228
- case 'visualize':
229
- if (req.method === 'GET' && pathMatch.sessionId) {
230
- await this.handleVisualize(req, res, requestId, pathMatch.sessionId, url.searchParams);
231
- }
232
- else if (req.method !== 'GET') {
233
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for visualization');
234
- }
235
- else {
236
- await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Session ID is required');
237
- }
238
- break;
239
- case 'sessions':
240
- if (req.method === 'GET' && pathMatch.sessionId) {
241
- await this.handleSessionRetrieval(req, res, requestId, pathMatch.sessionId);
242
- }
243
- else if (req.method !== 'GET') {
244
- await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for session retrieval');
245
- }
246
- else {
247
- await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Session ID is required');
248
- }
249
- break;
250
- default:
251
- await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown API endpoint');
148
+ // Check if path matches but method is wrong (HTTP 405 per RFC 7231)
149
+ const allowedMethods = this.routeRegistry.findAllowedMethods(url.pathname);
150
+ if (allowedMethods.length > 0) {
151
+ res.setHeader('Allow', allowedMethods.join(', '));
152
+ const methodList = allowedMethods.join(', ');
153
+ const message = allowedMethods.length === 1
154
+ ? `Only ${methodList} method allowed`
155
+ : `Only ${methodList} methods allowed`;
156
+ await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', message);
157
+ return;
252
158
  }
159
+ // No match found
160
+ await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'API endpoint not found');
253
161
  }
254
162
  catch (error) {
255
163
  this.logger.error('REST API request failed', error instanceof Error ? error : new Error(String(error)), {
@@ -260,96 +168,40 @@ class RestApiRouter {
260
168
  }
261
169
  }
262
170
  /**
263
- * Parse API path and extract route information
171
+ * Dispatch request to appropriate handler based on matched route
172
+ * PRD #354: Central dispatch using handler map for registry-matched routes.
264
173
  */
265
- parseApiPath(pathname) {
266
- // Expected patterns:
267
- // /api/v1/tools -> tools discovery
268
- // /api/v1/tools/{toolName} -> tool execution
269
- // /api/v1/openapi -> OpenAPI spec
270
- // /api/v1/resources/sync -> resource sync from controller
271
- // /api/v1/resources/kinds -> list resource kinds (PRD #328)
272
- // /api/v1/resources/search -> semantic search for resources (PRD #328)
273
- // /api/v1/resource -> get single resource with full details (PRD #328)
274
- // /api/v1/resources -> list resources with filtering (PRD #328)
275
- // /api/v1/namespaces -> list namespaces (PRD #328)
276
- // /api/v1/events -> get events for a resource (PRD #328)
277
- // /api/v1/logs -> get pod logs (PRD #328)
278
- // /api/v1/prompts -> prompts list
279
- // /api/v1/prompts/{promptName} -> prompt get
280
- const basePath = `${this.config.basePath}/${this.config.version}`;
281
- if (!pathname.startsWith(basePath)) {
282
- return null;
283
- }
284
- const pathSuffix = pathname.substring(basePath.length);
285
- // Remove leading slash
286
- const cleanPath = pathSuffix.startsWith('/') ? pathSuffix.substring(1) : pathSuffix;
287
- if (cleanPath === 'tools') {
288
- return { endpoint: 'tools' };
289
- }
290
- if (cleanPath === 'openapi') {
291
- return { endpoint: 'openapi' };
292
- }
293
- if (cleanPath.startsWith('tools/')) {
294
- const toolName = cleanPath.substring(6); // Remove 'tools/'
295
- if (toolName) {
296
- return { endpoint: 'tool', toolName };
297
- }
298
- }
299
- // Handle resources endpoints (PRD #328)
300
- if (cleanPath === 'resources/kinds') {
301
- return { endpoint: 'resources', action: 'kinds' };
302
- }
303
- if (cleanPath === 'resources/search') {
304
- return { endpoint: 'resources', action: 'search' };
305
- }
306
- if (cleanPath === 'resources/sync') {
307
- return { endpoint: 'resources', action: 'sync' };
308
- }
309
- if (cleanPath === 'resources') {
310
- return { endpoint: 'resources', action: 'list' };
311
- }
312
- // Handle single resource endpoint (PRD #328)
313
- if (cleanPath === 'resource') {
314
- return { endpoint: 'resource' };
315
- }
316
- // Handle namespaces endpoint (PRD #328)
317
- if (cleanPath === 'namespaces') {
318
- return { endpoint: 'namespaces' };
319
- }
320
- // Handle events endpoint (PRD #328)
321
- if (cleanPath === 'events') {
322
- return { endpoint: 'events' };
323
- }
324
- // Handle logs endpoint (PRD #328)
325
- if (cleanPath === 'logs') {
326
- return { endpoint: 'logs' };
327
- }
328
- // Handle prompts endpoints
329
- if (cleanPath === 'prompts') {
330
- return { endpoint: 'prompts' };
331
- }
332
- if (cleanPath.startsWith('prompts/')) {
333
- const promptName = cleanPath.substring(8); // Remove 'prompts/'
334
- if (promptName) {
335
- return { endpoint: 'prompt', promptName };
336
- }
337
- }
338
- // Handle visualize endpoint (PRD #317)
339
- if (cleanPath.startsWith('visualize/')) {
340
- const sessionId = cleanPath.substring(10); // Remove 'visualize/'
341
- if (sessionId) {
342
- return { endpoint: 'visualize', sessionId };
343
- }
174
+ async dispatchRoute(req, res, requestId, routeMatch, searchParams, body, startTime) {
175
+ const { route, params } = routeMatch;
176
+ const routeKey = `${route.method}:${route.path}`;
177
+ // Handler map: route key -> handler function
178
+ const handlers = {
179
+ 'GET:/api/v1/tools': () => this.handleToolDiscovery(req, res, requestId, searchParams),
180
+ 'POST:/api/v1/tools/:toolName': () => this.handleToolExecution(req, res, requestId, params.toolName, body, startTime),
181
+ 'GET:/api/v1/openapi': () => this.handleOpenApiSpec(req, res, requestId),
182
+ 'GET:/api/v1/resources': () => this.handleListResources(req, res, requestId, searchParams),
183
+ 'GET:/api/v1/resources/kinds': () => this.handleGetResourceKinds(req, res, requestId, searchParams),
184
+ 'GET:/api/v1/resources/search': () => this.handleSearchResources(req, res, requestId, searchParams),
185
+ 'POST:/api/v1/resources/sync': () => this.handleResourceSyncRequest(req, res, requestId, body),
186
+ 'GET:/api/v1/resource': () => this.handleGetResource(req, res, requestId, searchParams),
187
+ 'GET:/api/v1/namespaces': () => this.handleGetNamespaces(req, res, requestId),
188
+ 'GET:/api/v1/events': () => this.handleGetEvents(req, res, requestId, searchParams),
189
+ 'GET:/api/v1/logs': () => this.handleGetLogs(req, res, requestId, searchParams),
190
+ 'GET:/api/v1/prompts': () => this.handlePromptsListRequest(req, res, requestId),
191
+ 'POST:/api/v1/prompts/:promptName': () => this.handlePromptsGetRequest(req, res, requestId, params.promptName, body),
192
+ 'GET:/api/v1/visualize/:sessionId': () => this.handleVisualize(req, res, requestId, params.sessionId, searchParams),
193
+ 'GET:/api/v1/sessions/:sessionId': () => this.handleSessionRetrieval(req, res, requestId, params.sessionId),
194
+ 'DELETE:/api/v1/knowledge/source/:sourceIdentifier': () => this.handleDeleteKnowledgeSource(req, res, requestId, params.sourceIdentifier),
195
+ 'POST:/api/v1/knowledge/ask': () => this.handleKnowledgeAsk(req, res, requestId, body),
196
+ };
197
+ const handler = handlers[routeKey];
198
+ if (handler) {
199
+ await handler();
344
200
  }
345
- // Handle generic session retrieval endpoint (works for any tool: remediate, query, recommend, etc.)
346
- if (cleanPath.startsWith('sessions/')) {
347
- const sessionId = cleanPath.substring(9); // Remove 'sessions/'
348
- if (sessionId) {
349
- return { endpoint: 'sessions', sessionId };
350
- }
201
+ else {
202
+ this.logger.warn('Route matched but no handler found', { requestId, routeKey });
203
+ await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Handler not found for route');
351
204
  }
352
- return null;
353
205
  }
354
206
  /**
355
207
  * Handle tool discovery requests
@@ -383,7 +235,7 @@ class RestApiRouter {
383
235
  filters: { category, tag, search }
384
236
  });
385
237
  }
386
- catch (error) {
238
+ catch {
387
239
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DISCOVERY_ERROR', 'Failed to retrieve tool information');
388
240
  }
389
241
  }
@@ -721,13 +573,14 @@ class RestApiRouter {
721
573
  // Fetch status via plugin separately if requested
722
574
  const result = await (0, resource_tools_1.listResources)({ kind, apiVersion, namespace, limit, offset });
723
575
  // Enrich with live status via plugin if requested
724
- if (includeStatus && result.resources.length > 0 && this.pluginManager?.isPluginTool('kubectl_get_resource_json')) {
576
+ // PRD #359: Use unified plugin registry
577
+ if (includeStatus && result.resources.length > 0 && (0, plugin_registry_1.isPluginInitialized)()) {
725
578
  const statusPromises = result.resources.map(async (resource) => {
726
579
  const resourceType = resource.apiGroup
727
580
  ? `${resource.kind.toLowerCase()}.${resource.apiGroup}`
728
581
  : resource.kind.toLowerCase();
729
582
  const resourceId = `${resourceType}/${resource.name}`;
730
- const pluginResponse = await this.pluginManager.invokeTool('kubectl_get_resource_json', {
583
+ const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_get_resource_json', {
731
584
  resource: resourceId,
732
585
  namespace: resource.namespace,
733
586
  field: 'status'
@@ -833,15 +686,16 @@ class RestApiRouter {
833
686
  this.logger.info('Processing get resource request', { requestId, kind, apiVersion, name, namespace });
834
687
  // Extract apiGroup from apiVersion (e.g., "apps/v1" -> "apps", "v1" -> "")
835
688
  const apiGroup = apiVersion.includes('/') ? apiVersion.split('/')[0] : '';
836
- // PRD #343: Use plugin for kubectl operations
837
- if (!this.pluginManager?.isPluginTool('kubectl_get_resource_json')) {
838
- await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Kubernetes plugin not available');
689
+ // PRD #359: Use unified plugin registry
690
+ if (!(0, plugin_registry_1.isPluginInitialized)()) {
691
+ await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized');
839
692
  return;
840
693
  }
841
694
  // Build resource identifier (kind.group/name or kind/name for core resources)
842
695
  const resourceType = apiGroup ? `${kind.toLowerCase()}.${apiGroup}` : kind.toLowerCase();
843
696
  const resourceId = `${resourceType}/${name}`;
844
- const pluginResponse = await this.pluginManager.invokeTool('kubectl_get_resource_json', {
697
+ // PRD #359: Use unified plugin registry for kubectl operations
698
+ const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_get_resource_json', {
845
699
  resource: resourceId,
846
700
  namespace: namespace
847
701
  });
@@ -925,9 +779,9 @@ class RestApiRouter {
925
779
  return;
926
780
  }
927
781
  this.logger.info('Processing get events request', { requestId, name, kind, namespace, uid });
928
- // PRD #343: Use plugin for kubectl operations
929
- if (!this.pluginManager?.isPluginTool('kubectl_events')) {
930
- await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Kubernetes plugin not available');
782
+ // PRD #359: Use unified plugin registry
783
+ if (!(0, plugin_registry_1.isPluginInitialized)()) {
784
+ await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized');
931
785
  return;
932
786
  }
933
787
  // Build field selector for involvedObject filtering
@@ -938,11 +792,12 @@ class RestApiRouter {
938
792
  if (uid) {
939
793
  fieldSelectors.push(`involvedObject.uid=${uid}`);
940
794
  }
941
- const pluginResponse = await this.pluginManager.invokeTool('kubectl_events', {
795
+ // PRD #359: Use unified plugin registry for kubectl operations
796
+ const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_events', {
942
797
  namespace: namespace,
943
798
  args: [`--field-selector=${fieldSelectors.join(',')}`]
944
799
  });
945
- let events = [];
800
+ const events = [];
946
801
  if (pluginResponse.success && pluginResponse.result) {
947
802
  const pluginResult = pluginResponse.result;
948
803
  if (pluginResult.success && pluginResult.data) {
@@ -1025,9 +880,9 @@ class RestApiRouter {
1025
880
  }
1026
881
  }
1027
882
  this.logger.info('Processing get logs request', { requestId, name, namespace, container, tailLines });
1028
- // PRD #343: Use plugin for kubectl operations
1029
- if (!this.pluginManager?.isPluginTool('kubectl_logs')) {
1030
- await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Kubernetes plugin not available');
883
+ // PRD #359: Use unified plugin registry
884
+ if (!(0, plugin_registry_1.isPluginInitialized)()) {
885
+ await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized');
1031
886
  return;
1032
887
  }
1033
888
  // Build args for kubectl_logs
@@ -1038,7 +893,8 @@ class RestApiRouter {
1038
893
  if (container) {
1039
894
  args.push('-c', container);
1040
895
  }
1041
- const pluginResponse = await this.pluginManager.invokeTool('kubectl_logs', {
896
+ // PRD #359: Use unified plugin registry for kubectl operations
897
+ const pluginResponse = await (0, plugin_registry_1.invokePluginTool)('agentic-tools', 'kubectl_logs', {
1042
898
  resource: name,
1043
899
  namespace: namespace,
1044
900
  args: args.length > 0 ? args : undefined
@@ -1119,7 +975,8 @@ class RestApiRouter {
1119
975
  async handlePromptsGetRequest(req, res, requestId, promptName, body) {
1120
976
  try {
1121
977
  this.logger.info('Processing prompt get request', { requestId, promptName });
1122
- const result = await (0, prompts_1.handlePromptsGetRequest)({ name: promptName, arguments: body?.arguments }, this.logger, requestId);
978
+ const bodyObj = body;
979
+ const result = await (0, prompts_1.handlePromptsGetRequest)({ name: promptName, arguments: bodyObj?.arguments }, this.logger, requestId);
1123
980
  const response = {
1124
981
  success: true,
1125
982
  data: result,
@@ -1210,29 +1067,31 @@ class RestApiRouter {
1210
1067
  return;
1211
1068
  }
1212
1069
  // PRD #320: Select prompt based on tool name (defaults to 'query' for backwards compatibility)
1213
- const toolName = primarySession.data.toolName || 'query';
1070
+ const toolName = (primarySession.data.toolName || 'query');
1214
1071
  const promptName = (0, visualization_1.getPromptForTool)(toolName);
1215
1072
  this.logger.info('Loading visualization prompt', { requestId, sessionIds, toolName, promptName });
1216
1073
  // Load system prompt with session context
1217
1074
  // PRD #320: Unified visualization prompt with tool-aware data selection
1218
1075
  let intent;
1219
1076
  let data;
1077
+ // Cast to allow access to tool-specific properties
1078
+ const sessionData = primarySession.data;
1220
1079
  switch (toolName) {
1221
1080
  case 'recommend':
1222
- intent = primarySession.data.intent || '';
1081
+ intent = sessionData.intent || '';
1223
1082
  data = isMultiSession ? sessions.map(s => s.data) : primarySession.data;
1224
1083
  break;
1225
1084
  case 'remediate':
1226
- intent = primarySession.data.issue || '';
1227
- data = primarySession.data.finalAnalysis || primarySession.data;
1085
+ intent = sessionData.issue || '';
1086
+ data = sessionData.finalAnalysis || primarySession.data;
1228
1087
  break;
1229
1088
  case 'operate':
1230
- intent = primarySession.data.intent || '';
1089
+ intent = sessionData.intent || '';
1231
1090
  data = primarySession.data;
1232
1091
  break;
1233
1092
  case 'version':
1234
1093
  // PRD #320: Version tool provides system health status
1235
- intent = `System health: ${primarySession.data.summary?.overall || 'unknown'}`;
1094
+ intent = `System health: ${sessionData.summary?.overall || 'unknown'}`;
1236
1095
  data = primarySession.data;
1237
1096
  break;
1238
1097
  default:
@@ -1423,12 +1282,337 @@ class RestApiRouter {
1423
1282
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SESSION_RETRIEVAL_ERROR', 'Failed to retrieve session', { error: errorMessage });
1424
1283
  }
1425
1284
  }
1285
+ /**
1286
+ * Handle DELETE /api/v1/knowledge/source/:sourceIdentifier (PRD #356)
1287
+ * Delete all knowledge base chunks for a source identifier
1288
+ * Used by controller for GitKnowledgeSource cleanup
1289
+ */
1290
+ async handleDeleteKnowledgeSource(req, res, requestId, sourceIdentifier) {
1291
+ try {
1292
+ // URL-decode the sourceIdentifier (it may contain / encoded as %2F)
1293
+ const decodedSourceIdentifier = decodeURIComponent(sourceIdentifier);
1294
+ this.logger.info('Processing delete knowledge source request', {
1295
+ requestId,
1296
+ sourceIdentifier: decodedSourceIdentifier,
1297
+ });
1298
+ // Check plugin availability
1299
+ if (!(0, plugin_registry_1.isPluginInitialized)()) {
1300
+ await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized');
1301
+ return;
1302
+ }
1303
+ const KNOWLEDGE_COLLECTION = 'knowledge-base';
1304
+ const PLUGIN_NAME = 'agentic-tools';
1305
+ // Step 1: Query all chunks matching the sourceIdentifier in metadata
1306
+ const queryResponse = await (0, plugin_registry_1.invokePluginTool)(PLUGIN_NAME, 'vector_query', {
1307
+ collection: KNOWLEDGE_COLLECTION,
1308
+ filter: {
1309
+ must: [{ key: 'metadata.sourceIdentifier', match: { value: decodedSourceIdentifier } }],
1310
+ },
1311
+ limit: 10000, // High limit to get all chunks for a source
1312
+ });
1313
+ if (!queryResponse.success) {
1314
+ const error = queryResponse.error;
1315
+ const errorMessage = error?.message || error?.error || 'Query failed';
1316
+ // If collection doesn't exist (Not Found), return success with 0 deleted
1317
+ if (errorMessage.includes('Not Found') || errorMessage.includes('not found')) {
1318
+ this.logger.info('Collection not found - returning success with 0 deleted', {
1319
+ requestId,
1320
+ sourceIdentifier: decodedSourceIdentifier,
1321
+ });
1322
+ const response = {
1323
+ success: true,
1324
+ data: {
1325
+ sourceIdentifier: decodedSourceIdentifier,
1326
+ chunksDeleted: 0,
1327
+ },
1328
+ meta: {
1329
+ timestamp: new Date().toISOString(),
1330
+ requestId,
1331
+ version: this.config.version,
1332
+ },
1333
+ };
1334
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
1335
+ return;
1336
+ }
1337
+ this.logger.error('Plugin query failed', new Error(errorMessage), {
1338
+ requestId,
1339
+ sourceIdentifier: decodedSourceIdentifier,
1340
+ });
1341
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DELETE_SOURCE_ERROR', 'Failed to query chunks for deletion', { error: errorMessage });
1342
+ return;
1343
+ }
1344
+ // Extract results from plugin response
1345
+ const queryResult = queryResponse.result;
1346
+ if (!queryResult.success) {
1347
+ const errorMessage = queryResult.error || queryResult.message;
1348
+ // If collection doesn't exist, return success with 0 deleted
1349
+ if (errorMessage.includes('Not Found') || errorMessage.includes('not found')) {
1350
+ this.logger.info('Collection not found - returning success with 0 deleted', {
1351
+ requestId,
1352
+ sourceIdentifier: decodedSourceIdentifier,
1353
+ });
1354
+ const response = {
1355
+ success: true,
1356
+ data: {
1357
+ sourceIdentifier: decodedSourceIdentifier,
1358
+ chunksDeleted: 0,
1359
+ },
1360
+ meta: {
1361
+ timestamp: new Date().toISOString(),
1362
+ requestId,
1363
+ version: this.config.version,
1364
+ },
1365
+ };
1366
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
1367
+ return;
1368
+ }
1369
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DELETE_SOURCE_ERROR', 'Failed to query chunks for deletion', { error: errorMessage });
1370
+ return;
1371
+ }
1372
+ const chunksToDelete = queryResult.data || [];
1373
+ // If no chunks found, return success with 0 deleted
1374
+ if (chunksToDelete.length === 0) {
1375
+ this.logger.info('No chunks found for source identifier', {
1376
+ requestId,
1377
+ sourceIdentifier: decodedSourceIdentifier,
1378
+ });
1379
+ const response = {
1380
+ success: true,
1381
+ data: {
1382
+ sourceIdentifier: decodedSourceIdentifier,
1383
+ chunksDeleted: 0,
1384
+ },
1385
+ meta: {
1386
+ timestamp: new Date().toISOString(),
1387
+ requestId,
1388
+ version: this.config.version,
1389
+ },
1390
+ };
1391
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
1392
+ return;
1393
+ }
1394
+ // Step 2: Delete each chunk by ID
1395
+ let deletedCount = 0;
1396
+ for (const chunk of chunksToDelete) {
1397
+ this.logger.debug('Deleting chunk', { requestId, chunkId: chunk.id });
1398
+ const deleteResponse = await (0, plugin_registry_1.invokePluginTool)(PLUGIN_NAME, 'vector_delete', {
1399
+ collection: KNOWLEDGE_COLLECTION,
1400
+ id: chunk.id,
1401
+ });
1402
+ if (!deleteResponse.success) {
1403
+ const error = deleteResponse.error;
1404
+ const errorMessage = error?.message || 'Delete failed';
1405
+ this.logger.error('Failed to delete chunk', new Error(errorMessage), {
1406
+ requestId,
1407
+ chunkId: chunk.id,
1408
+ deletedSoFar: deletedCount,
1409
+ });
1410
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DELETE_SOURCE_ERROR', 'Failed to delete chunk', { chunkId: chunk.id, error: errorMessage, chunksDeletedBeforeFailure: deletedCount });
1411
+ return;
1412
+ }
1413
+ deletedCount++;
1414
+ }
1415
+ this.logger.info('Delete knowledge source operation completed', {
1416
+ requestId,
1417
+ sourceIdentifier: decodedSourceIdentifier,
1418
+ chunksDeleted: deletedCount,
1419
+ });
1420
+ const response = {
1421
+ success: true,
1422
+ data: {
1423
+ sourceIdentifier: decodedSourceIdentifier,
1424
+ chunksDeleted: deletedCount,
1425
+ },
1426
+ meta: {
1427
+ timestamp: new Date().toISOString(),
1428
+ requestId,
1429
+ version: this.config.version,
1430
+ },
1431
+ };
1432
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
1433
+ }
1434
+ catch (error) {
1435
+ const errorMessage = error instanceof Error ? error.message : String(error);
1436
+ this.logger.error('Delete knowledge source request failed', error instanceof Error ? error : new Error(String(error)), {
1437
+ requestId,
1438
+ sourceIdentifier,
1439
+ });
1440
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DELETE_SOURCE_ERROR', 'Failed to delete knowledge source', { error: errorMessage });
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Handle POST /api/v1/knowledge/ask (PRD #356)
1445
+ * Ask a question and receive an AI-synthesized answer from the knowledge base.
1446
+ * Uses an agentic approach with toolLoop to allow multiple searches if needed.
1447
+ */
1448
+ async handleKnowledgeAsk(req, res, requestId, body) {
1449
+ try {
1450
+ // Validate request body
1451
+ if (!body || typeof body !== 'object') {
1452
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Request body must be a JSON object');
1453
+ return;
1454
+ }
1455
+ const { query, limit = 20, uriFilter } = body;
1456
+ // Validate limit parameter (must be positive integer)
1457
+ if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1) {
1458
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_PARAMETER', 'The "limit" parameter must be a positive integer');
1459
+ return;
1460
+ }
1461
+ // Validate required query parameter
1462
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
1463
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Missing or empty required parameter: query');
1464
+ return;
1465
+ }
1466
+ this.logger.info('Processing knowledge ask request', {
1467
+ requestId,
1468
+ queryLength: query.length,
1469
+ limit,
1470
+ hasUriFilter: !!uriFilter,
1471
+ });
1472
+ // Check plugin availability (needed for search)
1473
+ if (!(0, plugin_registry_1.isPluginInitialized)()) {
1474
+ await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'PLUGIN_UNAVAILABLE', 'Plugin system not initialized');
1475
+ return;
1476
+ }
1477
+ // Check AI provider availability
1478
+ const aiProvider = (0, ai_provider_factory_1.createAIProvider)();
1479
+ if (!aiProvider.isInitialized()) {
1480
+ await this.sendErrorResponse(res, requestId, HttpStatus.SERVICE_UNAVAILABLE, 'AI_NOT_CONFIGURED', 'AI provider not configured. Set ANTHROPIC_API_KEY or another provider API key.');
1481
+ return;
1482
+ }
1483
+ // Define the search tool for the AI
1484
+ const searchTool = {
1485
+ name: 'search_knowledge_base',
1486
+ description: 'Search the knowledge base for relevant information. Returns chunks of text from documents that match the query semantically.',
1487
+ inputSchema: {
1488
+ type: 'object',
1489
+ properties: {
1490
+ query: {
1491
+ type: 'string',
1492
+ description: 'The search query - can be a question or keywords',
1493
+ },
1494
+ },
1495
+ required: ['query'],
1496
+ },
1497
+ };
1498
+ // Collect all chunks from search results across iterations
1499
+ const allChunks = [];
1500
+ // Tool executor that calls searchKnowledgeBase
1501
+ const toolExecutor = async (toolName, input) => {
1502
+ if (toolName !== 'search_knowledge_base') {
1503
+ return {
1504
+ success: false,
1505
+ error: `Unknown tool: ${toolName}`,
1506
+ };
1507
+ }
1508
+ const searchInput = input;
1509
+ const result = await (0, manage_knowledge_1.searchKnowledgeBase)({
1510
+ query: searchInput.query,
1511
+ limit,
1512
+ uriFilter,
1513
+ });
1514
+ if (!result.success) {
1515
+ return {
1516
+ success: false,
1517
+ error: result.error,
1518
+ message: 'Search failed',
1519
+ };
1520
+ }
1521
+ // Collect chunks for the response
1522
+ for (const chunk of result.chunks) {
1523
+ // Avoid duplicates (same id)
1524
+ if (!allChunks.some(c => c.uri === chunk.uri && c.chunkIndex === chunk.chunkIndex)) {
1525
+ allChunks.push({
1526
+ content: chunk.content,
1527
+ uri: chunk.uri,
1528
+ score: chunk.score,
1529
+ chunkIndex: chunk.chunkIndex,
1530
+ });
1531
+ }
1532
+ }
1533
+ // Return results to the AI
1534
+ if (result.chunks.length === 0) {
1535
+ return {
1536
+ success: true,
1537
+ message: 'No matching documents found in the knowledge base.',
1538
+ chunks: [],
1539
+ };
1540
+ }
1541
+ return {
1542
+ success: true,
1543
+ message: `Found ${result.chunks.length} relevant chunks.`,
1544
+ chunks: result.chunks.map(c => ({
1545
+ content: c.content,
1546
+ uri: c.uri,
1547
+ score: c.score,
1548
+ })),
1549
+ };
1550
+ };
1551
+ // Load system prompt
1552
+ const systemPrompt = (0, shared_prompt_loader_1.loadPrompt)('knowledge-ask');
1553
+ // Execute tool loop
1554
+ this.logger.info('Starting knowledge ask tool loop', { requestId });
1555
+ const result = await aiProvider.toolLoop({
1556
+ systemPrompt,
1557
+ userMessage: query,
1558
+ tools: [searchTool],
1559
+ toolExecutor,
1560
+ maxIterations: 5,
1561
+ operation: 'knowledge-ask',
1562
+ evaluationContext: {
1563
+ user_intent: query,
1564
+ },
1565
+ });
1566
+ this.logger.info('Knowledge ask tool loop completed', {
1567
+ requestId,
1568
+ iterations: result.iterations,
1569
+ chunksCollected: allChunks.length,
1570
+ toolCallsCount: result.toolCallsExecuted.length,
1571
+ });
1572
+ // Deduplicate sources from collected chunks
1573
+ const sourceMap = new Map();
1574
+ for (const chunk of allChunks) {
1575
+ if (!sourceMap.has(chunk.uri)) {
1576
+ sourceMap.set(chunk.uri, { uri: chunk.uri });
1577
+ }
1578
+ }
1579
+ const sources = Array.from(sourceMap.values());
1580
+ // Build response
1581
+ const response = {
1582
+ success: true,
1583
+ data: {
1584
+ answer: result.finalMessage,
1585
+ sources,
1586
+ chunks: allChunks,
1587
+ },
1588
+ meta: {
1589
+ timestamp: new Date().toISOString(),
1590
+ requestId,
1591
+ version: this.config.version,
1592
+ },
1593
+ };
1594
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
1595
+ this.logger.info('Knowledge ask completed', {
1596
+ requestId,
1597
+ sourcesFound: sources.length,
1598
+ chunksReturned: allChunks.length,
1599
+ answerLength: result.finalMessage.length,
1600
+ });
1601
+ }
1602
+ catch (error) {
1603
+ const errorMessage = error instanceof Error ? error.message : String(error);
1604
+ this.logger.error('Knowledge ask request failed', error instanceof Error ? error : new Error(String(error)), {
1605
+ requestId,
1606
+ });
1607
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SYNTHESIS_ERROR', 'Failed to process knowledge ask request', { error: errorMessage });
1608
+ }
1609
+ }
1426
1610
  /**
1427
1611
  * Set CORS headers
1428
1612
  */
1429
1613
  setCorsHeaders(res) {
1430
1614
  res.setHeader('Access-Control-Allow-Origin', '*');
1431
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1615
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
1432
1616
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
1433
1617
  res.setHeader('Access-Control-Max-Age', '86400');
1434
1618
  }
@@ -1476,5 +1660,12 @@ class RestApiRouter {
1476
1660
  getConfig() {
1477
1661
  return { ...this.config };
1478
1662
  }
1663
+ /**
1664
+ * Get the route registry for OpenAPI generation and fixture validation
1665
+ * PRD #354: Exposes route registry for downstream consumers
1666
+ */
1667
+ getRouteRegistry() {
1668
+ return this.routeRegistry;
1669
+ }
1479
1670
  }
1480
1671
  exports.RestApiRouter = RestApiRouter;