@vfarcic/dot-ai 0.165.0 → 0.167.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,6 +105,12 @@ Get started in 3 steps:
105
105
  - **[MCP Setup Guide](docs/setup/mcp-setup.md)** - Complete configuration instructions
106
106
  - **[Tools Overview](docs/guides/mcp-tools-overview.md)** - All available tools and features
107
107
 
108
+ ### Deployment Options
109
+ - **[Docker Setup](docs/setup/docker-setup.md)** - Recommended for local development
110
+ - **[Kubernetes Setup](docs/setup/kubernetes-setup.md)** - Production deployment with Ingress
111
+ - **[ToolHive Setup](docs/setup/kubernetes-toolhive-setup.md)** - Operator-managed deployment
112
+ - **[NPX Setup](docs/setup/npx-setup.md)** - Quick trials with Node.js
113
+
108
114
  ### Feature Guides
109
115
  - **[Resource Provisioning](docs/guides/mcp-recommendation-guide.md)** - AI-powered deployment recommendations
110
116
  - **[Capability Management](docs/guides/mcp-capability-management-guide.md)** - Semantic resource discovery
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Resource Vector Service
3
+ *
4
+ * Vector-based storage and retrieval for Kubernetes cluster resources.
5
+ * Extends BaseVectorService to provide resource-specific operations.
6
+ *
7
+ * This service receives resource data from the dot-ai-controller and stores
8
+ * it in Qdrant for semantic search capabilities.
9
+ */
10
+ import { BaseVectorService } from './base-vector-service';
11
+ import { VectorDBService } from './vector-db-service';
12
+ import { EmbeddingService } from './embedding-service';
13
+ /**
14
+ * Cluster resource data structure
15
+ * Matches the format sent by dot-ai-controller
16
+ * Note: ID is constructed by MCP from namespace/apiVersion/kind/name
17
+ */
18
+ export interface ClusterResource {
19
+ namespace: string;
20
+ name: string;
21
+ kind: string;
22
+ apiVersion: string;
23
+ apiGroup?: string;
24
+ labels: Record<string, string>;
25
+ annotations?: Record<string, string>;
26
+ createdAt: string;
27
+ updatedAt: string;
28
+ }
29
+ /**
30
+ * Resource sync request from controller
31
+ */
32
+ export interface ResourceSyncRequest {
33
+ upserts?: ClusterResource[];
34
+ deletes?: string[];
35
+ isResync?: boolean;
36
+ }
37
+ /**
38
+ * Result of a sync operation
39
+ */
40
+ export interface SyncResult {
41
+ upserted: number;
42
+ deleted: number;
43
+ failures: Array<{
44
+ id: string;
45
+ error: string;
46
+ }>;
47
+ }
48
+ /**
49
+ * Result of a diff and sync operation
50
+ */
51
+ export interface DiffSyncResult {
52
+ inserted: number;
53
+ updated: number;
54
+ deleted: number;
55
+ }
56
+ /**
57
+ * Extract API group from apiVersion
58
+ * e.g., 'apps/v1' -> 'apps', 'v1' -> ''
59
+ */
60
+ export declare function extractApiGroup(apiVersion: string): string;
61
+ /**
62
+ * Build embedding text from resource data
63
+ * Creates a semantic representation for vector search
64
+ */
65
+ export declare function buildEmbeddingText(resource: ClusterResource): string;
66
+ /**
67
+ * Generate resource ID from components
68
+ * Format: namespace:apiVersion:kind:name
69
+ */
70
+ export declare function generateResourceId(namespace: string, apiVersion: string, kind: string, name: string): string;
71
+ /**
72
+ * Generate a deterministic UUID from resource ID for Qdrant storage
73
+ * Qdrant requires UUIDs or positive integers as point IDs
74
+ * The hash is deterministic so the same resource ID always maps to the same UUID
75
+ */
76
+ export declare function generateResourceUuid(resourceId: string): string;
77
+ /**
78
+ * Check if two resources have meaningful differences
79
+ * Used for resync diff logic
80
+ */
81
+ export declare function hasResourceChanged(existing: ClusterResource, incoming: ClusterResource): boolean;
82
+ /**
83
+ * Vector service for storing and searching Kubernetes cluster resources
84
+ */
85
+ export declare class ResourceVectorService extends BaseVectorService<ClusterResource> {
86
+ constructor(collectionName?: string, vectorDB?: VectorDBService, embeddingService?: EmbeddingService);
87
+ /**
88
+ * Create searchable text from resource data for embedding generation
89
+ */
90
+ protected createSearchText(resource: ClusterResource): string;
91
+ /**
92
+ * Extract unique ID from resource data
93
+ * Always constructs from components and hashes to UUID for Qdrant
94
+ */
95
+ protected extractId(resource: ClusterResource): string;
96
+ /**
97
+ * Convert resource to storage payload format
98
+ */
99
+ protected createPayload(resource: ClusterResource): Record<string, any>;
100
+ /**
101
+ * Convert storage payload back to resource object
102
+ */
103
+ protected payloadToData(payload: Record<string, any>): ClusterResource;
104
+ /**
105
+ * Store a resource in the vector database
106
+ */
107
+ storeResource(resource: ClusterResource): Promise<void>;
108
+ /**
109
+ * Upsert a resource (alias for storeResource for API consistency)
110
+ */
111
+ upsertResource(resource: ClusterResource): Promise<void>;
112
+ /**
113
+ * Get a resource by ID
114
+ * Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
115
+ */
116
+ getResource(id: string): Promise<ClusterResource | null>;
117
+ /**
118
+ * Delete a resource by ID (idempotent - ignores not found)
119
+ * Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
120
+ */
121
+ deleteResource(id: string): Promise<void>;
122
+ /**
123
+ * Delete all resources (for testing/reset)
124
+ */
125
+ deleteAllResources(): Promise<void>;
126
+ /**
127
+ * List all resources
128
+ */
129
+ listResources(): Promise<ClusterResource[]>;
130
+ /**
131
+ * Diff incoming resources against Qdrant and sync changes
132
+ * Used for periodic resync operations
133
+ */
134
+ diffAndSync(incoming: ClusterResource[]): Promise<DiffSyncResult>;
135
+ }
136
+ //# sourceMappingURL=resource-vector-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-vector-service.d.ts","sourceRoot":"","sources":["../../src/core/resource-vector-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ /**
3
+ * Resource Vector Service
4
+ *
5
+ * Vector-based storage and retrieval for Kubernetes cluster resources.
6
+ * Extends BaseVectorService to provide resource-specific operations.
7
+ *
8
+ * This service receives resource data from the dot-ai-controller and stores
9
+ * it in Qdrant for semantic search capabilities.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ResourceVectorService = void 0;
13
+ exports.extractApiGroup = extractApiGroup;
14
+ exports.buildEmbeddingText = buildEmbeddingText;
15
+ exports.generateResourceId = generateResourceId;
16
+ exports.generateResourceUuid = generateResourceUuid;
17
+ exports.hasResourceChanged = hasResourceChanged;
18
+ const crypto_1 = require("crypto");
19
+ const base_vector_service_1 = require("./base-vector-service");
20
+ /**
21
+ * Extract API group from apiVersion
22
+ * e.g., 'apps/v1' -> 'apps', 'v1' -> ''
23
+ */
24
+ function extractApiGroup(apiVersion) {
25
+ const parts = apiVersion.split('/');
26
+ return parts.length > 1 ? parts[0] : '';
27
+ }
28
+ /**
29
+ * Build embedding text from resource data
30
+ * Creates a semantic representation for vector search
31
+ */
32
+ function buildEmbeddingText(resource) {
33
+ const parts = [
34
+ `${resource.kind} ${resource.name}`,
35
+ `namespace: ${resource.namespace}`,
36
+ `apiVersion: ${resource.apiVersion}`,
37
+ ];
38
+ // Add API group if present
39
+ const apiGroup = resource.apiGroup || extractApiGroup(resource.apiVersion);
40
+ if (apiGroup) {
41
+ parts.push(`group: ${apiGroup}`);
42
+ }
43
+ // Add meaningful labels (skip standard Kubernetes labels)
44
+ if (resource.labels && Object.keys(resource.labels).length > 0) {
45
+ const meaningfulLabels = Object.entries(resource.labels)
46
+ .filter(([k]) => {
47
+ // Skip standard Kubernetes labels that don't add semantic value
48
+ const skipPrefixes = [
49
+ 'app.kubernetes.io/',
50
+ 'helm.sh/',
51
+ 'kubernetes.io/',
52
+ 'k8s.io/',
53
+ ];
54
+ return !skipPrefixes.some(prefix => k.startsWith(prefix));
55
+ })
56
+ .map(([k, v]) => `${k}=${v}`);
57
+ if (meaningfulLabels.length > 0) {
58
+ parts.push(`labels: ${meaningfulLabels.join(', ')}`);
59
+ }
60
+ // Also include app name from standard labels if present
61
+ const appName = resource.labels['app.kubernetes.io/name'] ||
62
+ resource.labels['app'] ||
63
+ resource.labels['name'];
64
+ if (appName) {
65
+ parts.push(`app: ${appName}`);
66
+ }
67
+ }
68
+ // Add description from annotations if present
69
+ if (resource.annotations?.description) {
70
+ parts.push(`description: ${resource.annotations.description}`);
71
+ }
72
+ return parts.join(' | ');
73
+ }
74
+ /**
75
+ * Generate resource ID from components
76
+ * Format: namespace:apiVersion:kind:name
77
+ */
78
+ function generateResourceId(namespace, apiVersion, kind, name) {
79
+ return `${namespace}:${apiVersion}:${kind}:${name}`;
80
+ }
81
+ /**
82
+ * Generate a deterministic UUID from resource ID for Qdrant storage
83
+ * Qdrant requires UUIDs or positive integers as point IDs
84
+ * The hash is deterministic so the same resource ID always maps to the same UUID
85
+ */
86
+ function generateResourceUuid(resourceId) {
87
+ const hash = (0, crypto_1.createHash)('sha256').update(`resource-${resourceId}`).digest('hex');
88
+ // Convert to UUID format: 8-4-4-4-12
89
+ return `${hash.substring(0, 8)}-${hash.substring(8, 12)}-${hash.substring(12, 16)}-${hash.substring(16, 20)}-${hash.substring(20, 32)}`;
90
+ }
91
+ /**
92
+ * Stringify an object with sorted keys for reliable comparison
93
+ * Ensures consistent ordering regardless of object creation order
94
+ */
95
+ function sortedStringify(obj) {
96
+ if (!obj)
97
+ return '{}';
98
+ const sorted = Object.keys(obj).sort().reduce((acc, key) => {
99
+ acc[key] = obj[key];
100
+ return acc;
101
+ }, {});
102
+ return JSON.stringify(sorted);
103
+ }
104
+ /**
105
+ * Check if two resources have meaningful differences
106
+ * Used for resync diff logic
107
+ */
108
+ function hasResourceChanged(existing, incoming) {
109
+ // Compare updatedAt timestamps
110
+ if (existing.updatedAt !== incoming.updatedAt) {
111
+ return true;
112
+ }
113
+ // Compare labels (with sorted keys for reliable comparison)
114
+ if (sortedStringify(existing.labels) !== sortedStringify(incoming.labels)) {
115
+ return true;
116
+ }
117
+ // Compare annotations (with sorted keys for reliable comparison)
118
+ if (sortedStringify(existing.annotations) !== sortedStringify(incoming.annotations)) {
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+ /**
124
+ * Vector service for storing and searching Kubernetes cluster resources
125
+ */
126
+ class ResourceVectorService extends base_vector_service_1.BaseVectorService {
127
+ constructor(collectionName = 'resources', vectorDB, embeddingService) {
128
+ super(collectionName, vectorDB, embeddingService);
129
+ }
130
+ /**
131
+ * Create searchable text from resource data for embedding generation
132
+ */
133
+ createSearchText(resource) {
134
+ return buildEmbeddingText(resource);
135
+ }
136
+ /**
137
+ * Extract unique ID from resource data
138
+ * Always constructs from components and hashes to UUID for Qdrant
139
+ */
140
+ extractId(resource) {
141
+ // Always construct ID from components (ignore any provided id)
142
+ const resourceId = generateResourceId(resource.namespace, resource.apiVersion, resource.kind, resource.name);
143
+ return generateResourceUuid(resourceId);
144
+ }
145
+ /**
146
+ * Convert resource to storage payload format
147
+ */
148
+ createPayload(resource) {
149
+ return {
150
+ id: generateResourceId(resource.namespace, resource.apiVersion, resource.kind, resource.name),
151
+ namespace: resource.namespace,
152
+ name: resource.name,
153
+ kind: resource.kind,
154
+ apiVersion: resource.apiVersion,
155
+ apiGroup: resource.apiGroup || extractApiGroup(resource.apiVersion),
156
+ labels: resource.labels || {},
157
+ annotations: resource.annotations || {},
158
+ createdAt: resource.createdAt,
159
+ updatedAt: resource.updatedAt
160
+ };
161
+ }
162
+ /**
163
+ * Convert storage payload back to resource object
164
+ */
165
+ payloadToData(payload) {
166
+ return {
167
+ namespace: payload.namespace || '',
168
+ name: payload.name || '',
169
+ kind: payload.kind || '',
170
+ apiVersion: payload.apiVersion || '',
171
+ apiGroup: payload.apiGroup || '',
172
+ labels: payload.labels || {},
173
+ annotations: payload.annotations || {},
174
+ createdAt: payload.createdAt || new Date().toISOString(),
175
+ updatedAt: payload.updatedAt || new Date().toISOString()
176
+ };
177
+ }
178
+ /**
179
+ * Store a resource in the vector database
180
+ */
181
+ async storeResource(resource) {
182
+ await this.storeData(resource);
183
+ }
184
+ /**
185
+ * Upsert a resource (alias for storeResource for API consistency)
186
+ */
187
+ async upsertResource(resource) {
188
+ await this.storeResource(resource);
189
+ }
190
+ /**
191
+ * Get a resource by ID
192
+ * Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
193
+ */
194
+ async getResource(id) {
195
+ const uuid = generateResourceUuid(id);
196
+ return await this.getData(uuid);
197
+ }
198
+ /**
199
+ * Delete a resource by ID (idempotent - ignores not found)
200
+ * Accepts human-readable ID (namespace:apiVersion:kind:name) and converts to UUID
201
+ */
202
+ async deleteResource(id) {
203
+ try {
204
+ // Convert human-readable ID to UUID for Qdrant
205
+ const uuid = generateResourceUuid(id);
206
+ await this.deleteData(uuid);
207
+ }
208
+ catch (error) {
209
+ // Idempotent delete - ignore "not found" errors
210
+ const errorMessage = error instanceof Error ? error.message : String(error);
211
+ if (!errorMessage.toLowerCase().includes('not found')) {
212
+ throw error;
213
+ }
214
+ // Resource already deleted or never existed - this is fine
215
+ }
216
+ }
217
+ /**
218
+ * Delete all resources (for testing/reset)
219
+ */
220
+ async deleteAllResources() {
221
+ await this.deleteAllData();
222
+ }
223
+ /**
224
+ * List all resources
225
+ */
226
+ async listResources() {
227
+ return await this.getAllData();
228
+ }
229
+ /**
230
+ * Diff incoming resources against Qdrant and sync changes
231
+ * Used for periodic resync operations
232
+ */
233
+ async diffAndSync(incoming) {
234
+ // Helper to get human-readable ID from resource
235
+ const getResourceKey = (r) => generateResourceId(r.namespace, r.apiVersion, r.kind, r.name);
236
+ // Get all existing resources from Qdrant
237
+ const existing = await this.listResources();
238
+ const existingMap = new Map(existing.map(r => [getResourceKey(r), r]));
239
+ const incomingMap = new Map(incoming.map(r => [getResourceKey(r), r]));
240
+ const toInsert = [];
241
+ const toUpdate = [];
242
+ const toDelete = [];
243
+ // Find new and changed resources
244
+ for (const resource of incoming) {
245
+ const resourceId = getResourceKey(resource);
246
+ const existingResource = existingMap.get(resourceId);
247
+ if (!existingResource) {
248
+ toInsert.push(resource);
249
+ }
250
+ else if (hasResourceChanged(existingResource, resource)) {
251
+ toUpdate.push(resource);
252
+ }
253
+ }
254
+ // Find deleted resources (in Qdrant but not in incoming)
255
+ for (const id of existingMap.keys()) {
256
+ if (!incomingMap.has(id)) {
257
+ toDelete.push(id);
258
+ }
259
+ }
260
+ // Apply changes
261
+ for (const resource of [...toInsert, ...toUpdate]) {
262
+ await this.storeResource(resource);
263
+ }
264
+ for (const id of toDelete) {
265
+ await this.deleteResource(id);
266
+ }
267
+ return {
268
+ inserted: toInsert.length,
269
+ updated: toUpdate.length,
270
+ deleted: toDelete.length
271
+ };
272
+ }
273
+ }
274
+ exports.ResourceVectorService = ResourceVectorService;
@@ -35,6 +35,7 @@ export declare class VectorDBService {
35
35
  initializeCollection(vectorSize?: number): Promise<void>;
36
36
  /**
37
37
  * Create collection with specified vector size
38
+ * Handles conflict errors gracefully (collection already exists from race condition or restart)
38
39
  */
39
40
  private createCollection;
40
41
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"vector-db-service.d.ts","sourceRoot":"","sources":["../../src/core/vector-db-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,GAAE,cAAmB;IAsBvC,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,sBAAsB;IAM9B;;OAEG;IACG,oBAAoB,CAAC,UAAU,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDnE;;OAEG;YACW,gBAAgB;IAkB9B;;OAEG;IACG,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7D;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAsC1B;;OAEG;IACG,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAuD1B;;OAEG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAqC7D;;OAEG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B/C;;;;OAIG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCzC;;;OAGG;IACG,eAAe,CAAC,KAAK,GAAE,MAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyCvE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC;IAqBvC;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAsBrC;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,SAAS,IAAI,cAAc;CAG5B"}
1
+ {"version":3,"file":"vector-db-service.d.ts","sourceRoot":"","sources":["../../src/core/vector-db-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,cAAc,CAAS;gBAEnB,MAAM,GAAE,cAAmB;IAsBvC,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,sBAAsB;IAM9B;;OAEG;IACG,oBAAoB,CAAC,UAAU,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDnE;;;OAGG;YACW,gBAAgB;IAgC9B;;OAEG;IACG,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7D;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAsC1B;;OAEG;IACG,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC;IAuD1B;;OAEG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAqC7D;;OAEG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B/C;;;;OAIG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCzC;;;OAGG;IACG,eAAe,CAAC,KAAK,GAAE,MAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyCvE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC;IAqBvC;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAsBrC;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,SAAS,IAAI,cAAc;CAG5B"}
@@ -92,22 +92,38 @@ class VectorDBService {
92
92
  }
93
93
  /**
94
94
  * Create collection with specified vector size
95
+ * Handles conflict errors gracefully (collection already exists from race condition or restart)
95
96
  */
96
97
  async createCollection(vectorSize) {
97
98
  if (!this.client) {
98
99
  throw new Error('Vector DB client not initialized');
99
100
  }
100
- await this.client.createCollection(this.collectionName, {
101
- vectors: {
102
- size: vectorSize,
103
- distance: 'Cosine',
104
- on_disk: true // Enable on-disk storage for better performance with large collections
105
- },
106
- // Enable payload indexing for better keyword search performance
107
- optimizers_config: {
108
- default_segment_number: 2
101
+ try {
102
+ await this.client.createCollection(this.collectionName, {
103
+ vectors: {
104
+ size: vectorSize,
105
+ distance: 'Cosine',
106
+ on_disk: true // Enable on-disk storage for better performance with large collections
107
+ },
108
+ // Enable payload indexing for better keyword search performance
109
+ optimizers_config: {
110
+ default_segment_number: 2
111
+ }
112
+ });
113
+ }
114
+ catch (error) {
115
+ // Handle race condition where collection was created between check and create
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ if (errorMessage.toLowerCase().includes('conflict') ||
118
+ errorMessage.toLowerCase().includes('already exists')) {
119
+ // Collection exists - this is fine (race condition or restart)
120
+ if (process.env.DEBUG_DOT_AI) {
121
+ console.debug(`Collection ${this.collectionName} already exists, skipping creation`);
122
+ }
123
+ return;
109
124
  }
110
- });
125
+ throw error;
126
+ }
111
127
  }
112
128
  /**
113
129
  * Store a document with optional vector
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resource Sync Handler
3
+ *
4
+ * Handles POST /api/v1/resources/sync requests from dot-ai-controller.
5
+ * Receives cluster resource data, generates embeddings, and stores in Qdrant.
6
+ */
7
+ import { Logger } from '../core/error-handling';
8
+ import { RestApiResponse } from './rest-api';
9
+ /**
10
+ * Reset initialization state (for testing or recovery)
11
+ * Call this to force re-initialization on the next request
12
+ */
13
+ export declare function resetResourcesCollectionState(): void;
14
+ /**
15
+ * Response data for successful sync operations
16
+ */
17
+ export interface ResourceSyncResponseData {
18
+ upserted: number;
19
+ deleted: number;
20
+ }
21
+ /**
22
+ * Response data for partial failures
23
+ */
24
+ export interface ResourceSyncFailureDetails {
25
+ upserted: number;
26
+ deleted: number;
27
+ failures: Array<{
28
+ id: string;
29
+ error: string;
30
+ }>;
31
+ }
32
+ /**
33
+ * Handle resource sync requests from the controller
34
+ *
35
+ * @param body - The request body containing upserts, deletes, and isResync flag
36
+ * @param logger - Logger instance for request tracking
37
+ * @param requestId - Unique request identifier for correlation
38
+ * @returns RestApiResponse with sync results
39
+ */
40
+ export declare function handleResourceSync(body: unknown, logger: Logger, requestId: string): Promise<RestApiResponse>;
41
+ //# sourceMappingURL=resource-sync-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-sync-handler.d.ts","sourceRoot":"","sources":["../../src/interfaces/resource-sync-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAK7C;;;GAGG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAoID;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA0R1B"}
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ /**
3
+ * Resource Sync Handler
4
+ *
5
+ * Handles POST /api/v1/resources/sync requests from dot-ai-controller.
6
+ * Receives cluster resource data, generates embeddings, and stores in Qdrant.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.resetResourcesCollectionState = resetResourcesCollectionState;
10
+ exports.handleResourceSync = handleResourceSync;
11
+ const resource_vector_service_1 = require("../core/resource-vector-service");
12
+ // Global flag to track if resources collection has been initialized
13
+ let resourcesCollectionInitialized = false;
14
+ /**
15
+ * Reset initialization state (for testing or recovery)
16
+ * Call this to force re-initialization on the next request
17
+ */
18
+ function resetResourcesCollectionState() {
19
+ resourcesCollectionInitialized = false;
20
+ }
21
+ /**
22
+ * Validate a single cluster resource object
23
+ */
24
+ function validateClusterResource(resource, index) {
25
+ const errors = [];
26
+ if (!resource || typeof resource !== 'object') {
27
+ return { valid: false, errors: [`upserts[${index}]: must be an object`] };
28
+ }
29
+ const r = resource;
30
+ if (!r.namespace || typeof r.namespace !== 'string' || r.namespace.length === 0) {
31
+ errors.push(`upserts[${index}].namespace: required string field`);
32
+ }
33
+ if (!r.name || typeof r.name !== 'string' || r.name.length === 0) {
34
+ errors.push(`upserts[${index}].name: required string field`);
35
+ }
36
+ if (!r.kind || typeof r.kind !== 'string' || r.kind.length === 0) {
37
+ errors.push(`upserts[${index}].kind: required string field`);
38
+ }
39
+ if (!r.apiVersion || typeof r.apiVersion !== 'string' || r.apiVersion.length === 0) {
40
+ errors.push(`upserts[${index}].apiVersion: required string field`);
41
+ }
42
+ if (!r.createdAt || typeof r.createdAt !== 'string') {
43
+ errors.push(`upserts[${index}].createdAt: required string field`);
44
+ }
45
+ if (!r.updatedAt || typeof r.updatedAt !== 'string') {
46
+ errors.push(`upserts[${index}].updatedAt: required string field`);
47
+ }
48
+ return { valid: errors.length === 0, errors };
49
+ }
50
+ /**
51
+ * Validate the sync request body
52
+ */
53
+ function validateSyncRequest(body) {
54
+ const errors = [];
55
+ if (!body || typeof body !== 'object') {
56
+ return { valid: false, errors: ['Request body must be an object'] };
57
+ }
58
+ const b = body;
59
+ // Default values
60
+ const upserts = [];
61
+ const deletes = [];
62
+ const isResync = b.isResync === true;
63
+ // Validate upserts array
64
+ if (b.upserts !== undefined) {
65
+ if (!Array.isArray(b.upserts)) {
66
+ errors.push('upserts: must be an array');
67
+ }
68
+ else {
69
+ for (let i = 0; i < b.upserts.length; i++) {
70
+ const resourceValidation = validateClusterResource(b.upserts[i], i);
71
+ if (!resourceValidation.valid) {
72
+ errors.push(...resourceValidation.errors);
73
+ }
74
+ else {
75
+ const r = b.upserts[i];
76
+ upserts.push({
77
+ namespace: r.namespace,
78
+ name: r.name,
79
+ kind: r.kind,
80
+ apiVersion: r.apiVersion,
81
+ apiGroup: r.apiGroup,
82
+ labels: r.labels || {},
83
+ annotations: r.annotations,
84
+ createdAt: r.createdAt,
85
+ updatedAt: r.updatedAt
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ // Validate deletes array (objects with namespace, apiVersion, kind, name)
92
+ if (b.deletes !== undefined) {
93
+ if (!Array.isArray(b.deletes)) {
94
+ errors.push('deletes: must be an array');
95
+ }
96
+ else {
97
+ for (let i = 0; i < b.deletes.length; i++) {
98
+ const del = b.deletes[i];
99
+ if (!del || typeof del !== 'object') {
100
+ errors.push(`deletes[${i}]: must be an object`);
101
+ continue;
102
+ }
103
+ const d = del;
104
+ const delErrors = [];
105
+ if (!d.namespace || typeof d.namespace !== 'string') {
106
+ delErrors.push(`deletes[${i}].namespace: required string field`);
107
+ }
108
+ if (!d.name || typeof d.name !== 'string') {
109
+ delErrors.push(`deletes[${i}].name: required string field`);
110
+ }
111
+ if (!d.kind || typeof d.kind !== 'string') {
112
+ delErrors.push(`deletes[${i}].kind: required string field`);
113
+ }
114
+ if (!d.apiVersion || typeof d.apiVersion !== 'string') {
115
+ delErrors.push(`deletes[${i}].apiVersion: required string field`);
116
+ }
117
+ if (delErrors.length > 0) {
118
+ errors.push(...delErrors);
119
+ }
120
+ else {
121
+ // Construct ID from components
122
+ const deleteId = (0, resource_vector_service_1.generateResourceId)(d.namespace, d.apiVersion, d.kind, d.name);
123
+ deletes.push(deleteId);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ if (errors.length > 0) {
129
+ return { valid: false, errors };
130
+ }
131
+ return {
132
+ valid: true,
133
+ errors: [],
134
+ data: { upserts, deletes, isResync }
135
+ };
136
+ }
137
+ /**
138
+ * Handle resource sync requests from the controller
139
+ *
140
+ * @param body - The request body containing upserts, deletes, and isResync flag
141
+ * @param logger - Logger instance for request tracking
142
+ * @param requestId - Unique request identifier for correlation
143
+ * @returns RestApiResponse with sync results
144
+ */
145
+ async function handleResourceSync(body, logger, requestId) {
146
+ const startTime = Date.now();
147
+ // Validate request body using manual validation
148
+ const validation = validateSyncRequest(body);
149
+ if (!validation.valid) {
150
+ logger.warn('Resource sync request validation failed', {
151
+ requestId,
152
+ errors: validation.errors
153
+ });
154
+ return {
155
+ success: false,
156
+ error: {
157
+ code: 'VALIDATION_ERROR',
158
+ message: 'Invalid request body',
159
+ details: validation.errors.map(e => ({
160
+ path: e.split(':')[0] || 'unknown',
161
+ message: e.split(':').slice(1).join(':').trim() || e
162
+ }))
163
+ },
164
+ meta: {
165
+ timestamp: new Date().toISOString(),
166
+ requestId,
167
+ version: 'v1'
168
+ }
169
+ };
170
+ }
171
+ const syncRequest = validation.data;
172
+ const { upserts = [], deletes = [], isResync = false } = syncRequest;
173
+ logger.info('Processing resource sync request', {
174
+ requestId,
175
+ upsertCount: upserts.length,
176
+ deleteCount: deletes.length,
177
+ isResync
178
+ });
179
+ // Initialize the resource vector service
180
+ let resourceService;
181
+ try {
182
+ resourceService = new resource_vector_service_1.ResourceVectorService();
183
+ }
184
+ catch (error) {
185
+ logger.error('Failed to create ResourceVectorService', error, { requestId });
186
+ return {
187
+ success: false,
188
+ error: {
189
+ code: 'SERVICE_INIT_FAILED',
190
+ message: 'Failed to create resource vector service',
191
+ details: { error: error instanceof Error ? error.message : String(error) }
192
+ },
193
+ meta: {
194
+ timestamp: new Date().toISOString(),
195
+ requestId,
196
+ version: 'v1'
197
+ }
198
+ };
199
+ }
200
+ // Check Vector DB health
201
+ let isHealthy;
202
+ try {
203
+ isHealthy = await resourceService.healthCheck();
204
+ }
205
+ catch (error) {
206
+ logger.error('Health check threw exception', error, { requestId });
207
+ return {
208
+ success: false,
209
+ error: {
210
+ code: 'HEALTH_CHECK_FAILED',
211
+ message: 'Vector DB health check threw exception',
212
+ details: { error: error instanceof Error ? error.message : String(error) }
213
+ },
214
+ meta: {
215
+ timestamp: new Date().toISOString(),
216
+ requestId,
217
+ version: 'v1'
218
+ }
219
+ };
220
+ }
221
+ if (!isHealthy) {
222
+ logger.error('Vector DB health check failed', new Error('Qdrant unavailable'), { requestId });
223
+ return {
224
+ success: false,
225
+ error: {
226
+ code: 'VECTOR_DB_UNAVAILABLE',
227
+ message: 'Vector database is not available',
228
+ details: {
229
+ recommendation: 'Ensure Qdrant is running and accessible'
230
+ }
231
+ },
232
+ meta: {
233
+ timestamp: new Date().toISOString(),
234
+ requestId,
235
+ version: 'v1'
236
+ }
237
+ };
238
+ }
239
+ // Initialize the collection (only on first request, skip thereafter for performance)
240
+ if (!resourcesCollectionInitialized) {
241
+ try {
242
+ await resourceService.initialize();
243
+ resourcesCollectionInitialized = true;
244
+ }
245
+ catch (error) {
246
+ logger.error('Failed to initialize resources collection', error, { requestId });
247
+ return {
248
+ success: false,
249
+ error: {
250
+ code: 'COLLECTION_INIT_FAILED',
251
+ message: 'Failed to initialize resources collection',
252
+ details: {
253
+ error: error instanceof Error ? error.message : String(error)
254
+ }
255
+ },
256
+ meta: {
257
+ timestamp: new Date().toISOString(),
258
+ requestId,
259
+ version: 'v1'
260
+ }
261
+ };
262
+ }
263
+ }
264
+ let upserted = 0;
265
+ let deleted = 0;
266
+ const failures = [];
267
+ // Handle resync mode - use diffAndSync for full reconciliation
268
+ if (isResync && upserts.length > 0) {
269
+ logger.info('Processing resync with diff', {
270
+ requestId,
271
+ incomingResourceCount: upserts.length
272
+ });
273
+ try {
274
+ const diffResult = await resourceService.diffAndSync(upserts);
275
+ logger.info('Resync diff completed', {
276
+ requestId,
277
+ inserted: diffResult.inserted,
278
+ updated: diffResult.updated,
279
+ deleted: diffResult.deleted
280
+ });
281
+ return {
282
+ success: true,
283
+ data: {
284
+ upserted: diffResult.inserted + diffResult.updated,
285
+ deleted: diffResult.deleted,
286
+ resync: {
287
+ inserted: diffResult.inserted,
288
+ updated: diffResult.updated,
289
+ deleted: diffResult.deleted
290
+ }
291
+ },
292
+ meta: {
293
+ timestamp: new Date().toISOString(),
294
+ requestId,
295
+ version: 'v1'
296
+ }
297
+ };
298
+ }
299
+ catch (error) {
300
+ logger.error('Resync diff failed', error, { requestId });
301
+ return {
302
+ success: false,
303
+ error: {
304
+ code: 'RESYNC_FAILED',
305
+ message: 'Failed to perform resync diff',
306
+ details: {
307
+ error: error instanceof Error ? error.message : String(error)
308
+ }
309
+ },
310
+ meta: {
311
+ timestamp: new Date().toISOString(),
312
+ requestId,
313
+ version: 'v1'
314
+ }
315
+ };
316
+ }
317
+ }
318
+ // Handle upserts - process each resource
319
+ for (const resource of upserts) {
320
+ const resourceId = (0, resource_vector_service_1.generateResourceId)(resource.namespace, resource.apiVersion, resource.kind, resource.name);
321
+ try {
322
+ await resourceService.upsertResource(resource);
323
+ upserted++;
324
+ logger.debug('Resource upserted', {
325
+ requestId,
326
+ resourceId,
327
+ kind: resource.kind,
328
+ namespace: resource.namespace
329
+ });
330
+ }
331
+ catch (error) {
332
+ const errorMessage = error instanceof Error ? error.message : String(error);
333
+ failures.push({ id: resourceId, error: errorMessage });
334
+ logger.warn('Failed to upsert resource', {
335
+ requestId,
336
+ resourceId,
337
+ error: errorMessage
338
+ });
339
+ }
340
+ }
341
+ // Handle deletes - idempotent (ignore not found)
342
+ for (const id of deletes) {
343
+ try {
344
+ await resourceService.deleteResource(id);
345
+ deleted++;
346
+ logger.debug('Resource deleted', {
347
+ requestId,
348
+ resourceId: id
349
+ });
350
+ }
351
+ catch (error) {
352
+ // deleteResource is already idempotent, but handle any other errors
353
+ const errorMessage = error instanceof Error ? error.message : String(error);
354
+ failures.push({ id, error: errorMessage });
355
+ logger.warn('Failed to delete resource', {
356
+ requestId,
357
+ resourceId: id,
358
+ error: errorMessage
359
+ });
360
+ }
361
+ }
362
+ const executionTime = Date.now() - startTime;
363
+ // Return appropriate response based on failures
364
+ if (failures.length > 0) {
365
+ logger.warn('Resource sync completed with failures', {
366
+ requestId,
367
+ upserted,
368
+ deleted,
369
+ failureCount: failures.length,
370
+ executionTime
371
+ });
372
+ return {
373
+ success: false,
374
+ error: {
375
+ code: 'SYNC_PARTIAL_FAILURE',
376
+ message: `Failed to process ${failures.length} resource(s)`,
377
+ details: {
378
+ upserted,
379
+ deleted,
380
+ failures
381
+ }
382
+ },
383
+ meta: {
384
+ timestamp: new Date().toISOString(),
385
+ requestId,
386
+ version: 'v1'
387
+ }
388
+ };
389
+ }
390
+ logger.info('Resource sync completed successfully', {
391
+ requestId,
392
+ upserted,
393
+ deleted,
394
+ executionTime
395
+ });
396
+ return {
397
+ success: true,
398
+ data: {
399
+ upserted,
400
+ deleted
401
+ },
402
+ meta: {
403
+ timestamp: new Date().toISOString(),
404
+ requestId,
405
+ version: 'v1'
406
+ }
407
+ };
408
+ }
@@ -16,7 +16,8 @@ export declare enum HttpStatus {
16
16
  BAD_REQUEST = 400,
17
17
  NOT_FOUND = 404,
18
18
  METHOD_NOT_ALLOWED = 405,
19
- INTERNAL_SERVER_ERROR = 500
19
+ INTERNAL_SERVER_ERROR = 500,
20
+ SERVICE_UNAVAILABLE = 503
20
21
  }
21
22
  /**
22
23
  * Standard REST API response format
@@ -96,6 +97,10 @@ export declare class RestApiRouter {
96
97
  * Handle OpenAPI specification requests
97
98
  */
98
99
  private handleOpenApiSpec;
100
+ /**
101
+ * Handle resource sync requests from controller
102
+ */
103
+ private handleResourceSyncRequest;
99
104
  /**
100
105
  * Set CORS headers
101
106
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEtC;;GAEG;AACH,oBAAY,UAAU;IACpB,EAAE,MAAM;IACR,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,kBAAkB,MAAM;IACxB,qBAAqB,MAAM;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,GAAG,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAa;gBAGjC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAoBrC;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA+EzF;;OAEG;IACH,OAAO,CAAC,YAAY;IAmCpB;;OAEG;YACW,mBAAmB;IA2CjC;;OAEG;YACW,mBAAmB;IA+FjC;;OAEG;YACW,iBAAiB;IA8B/B;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;YACW,gBAAgB;IAK9B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIvC;;OAEG;IACH,SAAS,IAAI,aAAa;CAG3B"}
1
+ {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC;;GAEG;AACH,oBAAY,UAAU;IACpB,EAAE,MAAM;IACR,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,kBAAkB,MAAM;IACxB,qBAAqB,MAAM;IAC3B,mBAAmB,MAAM;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,GAAG,CAAC;KACf,CAAC;IACF,IAAI,CAAC,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,GAAG,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAa;gBAGjC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAoBrC;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAyFzF;;OAEG;IACH,OAAO,CAAC,YAAY;IAyCpB;;OAEG;YACW,mBAAmB;IA2CjC;;OAEG;YACW,mBAAmB;IA+FjC;;OAEG;YACW,iBAAiB;IA8B/B;;OAEG;YACW,yBAAyB;IAgEvC;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;YACW,gBAAgB;IAK9B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIvC;;OAEG;IACH,SAAS,IAAI,aAAa;CAG3B"}
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.RestApiRouter = exports.HttpStatus = void 0;
10
10
  const node_url_1 = require("node:url");
11
11
  const openapi_generator_1 = require("./openapi-generator");
12
+ const resource_sync_handler_1 = require("./resource-sync-handler");
12
13
  /**
13
14
  * HTTP status codes for REST responses
14
15
  */
@@ -19,6 +20,7 @@ var HttpStatus;
19
20
  HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
20
21
  HttpStatus[HttpStatus["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
21
22
  HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
23
+ HttpStatus[HttpStatus["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
22
24
  })(HttpStatus || (exports.HttpStatus = HttpStatus = {}));
23
25
  /**
24
26
  * REST API Router for MCP tools
@@ -105,6 +107,17 @@ class RestApiRouter {
105
107
  await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for OpenAPI specification');
106
108
  }
107
109
  break;
110
+ case 'resources':
111
+ if (req.method === 'POST' && pathMatch.action === 'sync') {
112
+ await this.handleResourceSyncRequest(req, res, requestId, body);
113
+ }
114
+ else if (req.method !== 'POST') {
115
+ await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for resource sync');
116
+ }
117
+ else {
118
+ await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown resources endpoint');
119
+ }
120
+ break;
108
121
  default:
109
122
  await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown API endpoint');
110
123
  }
@@ -125,6 +138,7 @@ class RestApiRouter {
125
138
  // /api/v1/tools -> tools discovery
126
139
  // /api/v1/tools/{toolName} -> tool execution
127
140
  // /api/v1/openapi -> OpenAPI spec
141
+ // /api/v1/resources/sync -> resource sync from controller
128
142
  const basePath = `${this.config.basePath}/${this.config.version}`;
129
143
  if (!pathname.startsWith(basePath)) {
130
144
  return null;
@@ -144,6 +158,10 @@ class RestApiRouter {
144
158
  return { endpoint: 'tool', toolName };
145
159
  }
146
160
  }
161
+ // Handle resources/sync endpoint
162
+ if (cleanPath === 'resources/sync') {
163
+ return { endpoint: 'resources', action: 'sync' };
164
+ }
147
165
  return null;
148
166
  }
149
167
  /**
@@ -284,6 +302,50 @@ class RestApiRouter {
284
302
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'OPENAPI_ERROR', 'Failed to generate OpenAPI specification');
285
303
  }
286
304
  }
305
+ /**
306
+ * Handle resource sync requests from controller
307
+ */
308
+ async handleResourceSyncRequest(req, res, requestId, body) {
309
+ try {
310
+ this.logger.info('Processing resource sync request', { requestId });
311
+ // Validate request body exists
312
+ if (!body || typeof body !== 'object') {
313
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_REQUEST', 'Request body must be a JSON object');
314
+ return;
315
+ }
316
+ // Delegate to the resource sync handler
317
+ const response = await (0, resource_sync_handler_1.handleResourceSync)(body, this.logger, requestId);
318
+ // Determine HTTP status based on response and error type
319
+ let httpStatus = HttpStatus.OK;
320
+ if (!response.success) {
321
+ const errorCode = response.error?.code;
322
+ if (errorCode === 'VECTOR_DB_UNAVAILABLE' || errorCode === 'HEALTH_CHECK_FAILED') {
323
+ httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
324
+ }
325
+ else if (errorCode === 'SERVICE_INIT_FAILED' || errorCode === 'COLLECTION_INIT_FAILED' || errorCode === 'RESYNC_FAILED') {
326
+ httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
327
+ }
328
+ else {
329
+ httpStatus = HttpStatus.BAD_REQUEST;
330
+ }
331
+ }
332
+ await this.sendJsonResponse(res, httpStatus, response);
333
+ this.logger.info('Resource sync request completed', {
334
+ requestId,
335
+ success: response.success,
336
+ upserted: response.data?.upserted,
337
+ deleted: response.data?.deleted
338
+ });
339
+ }
340
+ catch (error) {
341
+ const errorMessage = error instanceof Error ? error.message : String(error);
342
+ this.logger.error('Resource sync request failed', error instanceof Error ? error : new Error(String(error)), {
343
+ requestId,
344
+ errorMessage
345
+ });
346
+ await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'SYNC_ERROR', 'Resource sync failed', { error: errorMessage });
347
+ }
348
+ }
287
349
  /**
288
350
  * Set CORS headers
289
351
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vfarcic/dot-ai",
3
- "version": "0.165.0",
3
+ "version": "0.167.0",
4
4
  "description": "AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance",
5
5
  "mcpName": "io.github.vfarcic/dot-ai",
6
6
  "main": "dist/index.js",