@vfarcic/dot-ai 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/.claude/commands/context-load.md +11 -0
  2. package/.claude/commands/context-save.md +16 -0
  3. package/.claude/commands/prd-done.md +115 -0
  4. package/.claude/commands/prd-get.md +25 -0
  5. package/.claude/commands/prd-start.md +87 -0
  6. package/.claude/commands/task-done.md +77 -0
  7. package/.claude/commands/tests-reminder.md +32 -0
  8. package/.claude/settings.local.json +20 -0
  9. package/.eslintrc.json +25 -0
  10. package/.github/workflows/ci.yml +170 -0
  11. package/.prettierrc.json +10 -0
  12. package/.teller.yml +8 -0
  13. package/CLAUDE.md +162 -0
  14. package/assets/images/logo.png +0 -0
  15. package/bin/dot-ai.ts +47 -0
  16. package/destroy.sh +45 -0
  17. package/devbox.json +13 -0
  18. package/devbox.lock +225 -0
  19. package/docs/API.md +449 -0
  20. package/docs/CONTEXT.md +49 -0
  21. package/docs/DEVELOPMENT.md +203 -0
  22. package/docs/NEXT_STEPS.md +97 -0
  23. package/docs/STAGE_BASED_API.md +97 -0
  24. package/docs/cli-guide.md +798 -0
  25. package/docs/design.md +750 -0
  26. package/docs/discovery-engine.md +515 -0
  27. package/docs/error-handling.md +429 -0
  28. package/docs/function-registration.md +157 -0
  29. package/docs/mcp-guide.md +416 -0
  30. package/package.json +2 -121
  31. package/renovate.json +51 -0
  32. package/setup.sh +111 -0
  33. package/{dist/cli.js → src/cli.ts} +26 -19
  34. package/src/core/claude.ts +280 -0
  35. package/src/core/deploy-operation.ts +127 -0
  36. package/src/core/discovery.ts +900 -0
  37. package/src/core/error-handling.ts +562 -0
  38. package/src/core/index.ts +143 -0
  39. package/src/core/kubernetes-utils.ts +218 -0
  40. package/src/core/memory.ts +148 -0
  41. package/src/core/schema.ts +830 -0
  42. package/src/core/session-utils.ts +97 -0
  43. package/src/core/workflow.ts +234 -0
  44. package/src/index.ts +18 -0
  45. package/src/interfaces/cli.ts +872 -0
  46. package/src/interfaces/mcp.ts +183 -0
  47. package/src/mcp/server.ts +131 -0
  48. package/src/tools/answer-question.ts +807 -0
  49. package/src/tools/choose-solution.ts +169 -0
  50. package/src/tools/deploy-manifests.ts +94 -0
  51. package/src/tools/generate-manifests.ts +502 -0
  52. package/src/tools/index.ts +41 -0
  53. package/src/tools/recommend.ts +370 -0
  54. package/tests/__mocks__/@kubernetes/client-node.ts +106 -0
  55. package/tests/build-system.test.ts +345 -0
  56. package/tests/configuration.test.ts +226 -0
  57. package/tests/core/deploy-operation.test.ts +38 -0
  58. package/tests/core/discovery.test.ts +1648 -0
  59. package/tests/core/error-handling.test.ts +632 -0
  60. package/tests/core/schema.test.ts +1658 -0
  61. package/tests/core/session-utils.test.ts +245 -0
  62. package/tests/core.test.ts +439 -0
  63. package/tests/fixtures/configmap-no-labels.yaml +8 -0
  64. package/tests/fixtures/crossplane-app-configuration.yaml +6 -0
  65. package/tests/fixtures/crossplane-providers.yaml +45 -0
  66. package/tests/fixtures/crossplane-rbac.yaml +48 -0
  67. package/tests/fixtures/invalid-configmap.yaml +8 -0
  68. package/tests/fixtures/invalid-deployment.yaml +17 -0
  69. package/tests/fixtures/test-deployment.yaml +28 -0
  70. package/tests/fixtures/valid-configmap.yaml +15 -0
  71. package/tests/infrastructure.test.ts +426 -0
  72. package/tests/interfaces/cli.test.ts +1036 -0
  73. package/tests/interfaces/mcp.test.ts +139 -0
  74. package/tests/kubernetes-utils.test.ts +200 -0
  75. package/tests/mcp/server.test.ts +126 -0
  76. package/tests/setup.ts +31 -0
  77. package/tests/tools/answer-question.test.ts +367 -0
  78. package/tests/tools/choose-solution.test.ts +481 -0
  79. package/tests/tools/deploy-manifests.test.ts +185 -0
  80. package/tests/tools/generate-manifests.test.ts +441 -0
  81. package/tests/tools/index.test.ts +111 -0
  82. package/tests/tools/recommend.test.ts +180 -0
  83. package/tsconfig.json +34 -0
  84. package/dist/cli.d.ts +0 -3
  85. package/dist/cli.d.ts.map +0 -1
  86. package/dist/core/claude.d.ts +0 -42
  87. package/dist/core/claude.d.ts.map +0 -1
  88. package/dist/core/claude.js +0 -229
  89. package/dist/core/deploy-operation.d.ts +0 -38
  90. package/dist/core/deploy-operation.d.ts.map +0 -1
  91. package/dist/core/deploy-operation.js +0 -101
  92. package/dist/core/discovery.d.ts +0 -162
  93. package/dist/core/discovery.d.ts.map +0 -1
  94. package/dist/core/discovery.js +0 -758
  95. package/dist/core/error-handling.d.ts +0 -167
  96. package/dist/core/error-handling.d.ts.map +0 -1
  97. package/dist/core/error-handling.js +0 -399
  98. package/dist/core/index.d.ts +0 -42
  99. package/dist/core/index.d.ts.map +0 -1
  100. package/dist/core/index.js +0 -123
  101. package/dist/core/kubernetes-utils.d.ts +0 -38
  102. package/dist/core/kubernetes-utils.d.ts.map +0 -1
  103. package/dist/core/kubernetes-utils.js +0 -177
  104. package/dist/core/memory.d.ts +0 -45
  105. package/dist/core/memory.d.ts.map +0 -1
  106. package/dist/core/memory.js +0 -113
  107. package/dist/core/schema.d.ts +0 -187
  108. package/dist/core/schema.d.ts.map +0 -1
  109. package/dist/core/schema.js +0 -655
  110. package/dist/core/session-utils.d.ts +0 -29
  111. package/dist/core/session-utils.d.ts.map +0 -1
  112. package/dist/core/session-utils.js +0 -121
  113. package/dist/core/workflow.d.ts +0 -70
  114. package/dist/core/workflow.d.ts.map +0 -1
  115. package/dist/core/workflow.js +0 -161
  116. package/dist/index.d.ts +0 -15
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -32
  119. package/dist/interfaces/cli.d.ts +0 -74
  120. package/dist/interfaces/cli.d.ts.map +0 -1
  121. package/dist/interfaces/cli.js +0 -769
  122. package/dist/interfaces/mcp.d.ts +0 -30
  123. package/dist/interfaces/mcp.d.ts.map +0 -1
  124. package/dist/interfaces/mcp.js +0 -105
  125. package/dist/mcp/server.d.ts +0 -9
  126. package/dist/mcp/server.d.ts.map +0 -1
  127. package/dist/mcp/server.js +0 -151
  128. package/dist/tools/answer-question.d.ts +0 -27
  129. package/dist/tools/answer-question.d.ts.map +0 -1
  130. package/dist/tools/answer-question.js +0 -696
  131. package/dist/tools/choose-solution.d.ts +0 -23
  132. package/dist/tools/choose-solution.d.ts.map +0 -1
  133. package/dist/tools/choose-solution.js +0 -171
  134. package/dist/tools/deploy-manifests.d.ts +0 -25
  135. package/dist/tools/deploy-manifests.d.ts.map +0 -1
  136. package/dist/tools/deploy-manifests.js +0 -74
  137. package/dist/tools/generate-manifests.d.ts +0 -23
  138. package/dist/tools/generate-manifests.d.ts.map +0 -1
  139. package/dist/tools/generate-manifests.js +0 -424
  140. package/dist/tools/index.d.ts +0 -11
  141. package/dist/tools/index.d.ts.map +0 -1
  142. package/dist/tools/index.js +0 -34
  143. package/dist/tools/recommend.d.ts +0 -23
  144. package/dist/tools/recommend.d.ts.map +0 -1
  145. package/dist/tools/recommend.js +0 -332
@@ -0,0 +1,830 @@
1
+ /**
2
+ * Resource Schema Parser and Validator
3
+ *
4
+ * Implements comprehensive schema parsing and validation for Kubernetes resources
5
+ * Fetches structured OpenAPI schemas from Kubernetes API server and validates manifests
6
+ */
7
+
8
+ import { ResourceExplanation } from './discovery';
9
+ import {
10
+ executeKubectl
11
+ } from './kubernetes-utils';
12
+ import { ClaudeIntegration } from './claude';
13
+
14
+ // Core type definitions for schema structure
15
+ export interface FieldConstraints {
16
+ minimum?: number;
17
+ maximum?: number;
18
+ minLength?: number;
19
+ maxLength?: number;
20
+ enum?: string[];
21
+ default?: any;
22
+ pattern?: string;
23
+ }
24
+
25
+ export interface SchemaField {
26
+ name: string;
27
+ type: string;
28
+ description: string;
29
+ required: boolean;
30
+ default?: any;
31
+ constraints?: FieldConstraints;
32
+ nested: Map<string, SchemaField>;
33
+ }
34
+
35
+ export interface ResourceSchema {
36
+ apiVersion: string;
37
+ kind: string;
38
+ group: string;
39
+ version?: string;
40
+ description: string;
41
+ properties: Map<string, SchemaField>;
42
+ required?: string[];
43
+ namespace?: boolean;
44
+ rawExplanation?: string; // Raw kubectl explain output for AI processing
45
+ }
46
+
47
+ export interface ValidationResult {
48
+ valid: boolean;
49
+ errors: string[];
50
+ warnings: string[];
51
+ }
52
+
53
+ export interface ResourceMapping {
54
+ resourceKind: string;
55
+ apiVersion: string;
56
+ group?: string;
57
+ fieldPath: string;
58
+ }
59
+
60
+ // Simple answer structure for manifest generation
61
+ export interface AnswerSet {
62
+ [questionId: string]: any;
63
+ }
64
+
65
+ // Enhanced solution for single-pass workflow
66
+ export interface EnhancedSolution extends ResourceSolution {
67
+ answers: AnswerSet;
68
+ openAnswer: string;
69
+ }
70
+
71
+ export interface Question {
72
+ id: string;
73
+ question: string;
74
+ type: 'text' | 'select' | 'multiselect' | 'boolean' | 'number';
75
+ options?: string[];
76
+ placeholder?: string;
77
+ validation?: {
78
+ required?: boolean;
79
+ min?: number;
80
+ max?: number;
81
+ pattern?: string;
82
+ message?: string;
83
+ };
84
+ answer?: any;
85
+ // Note: resourceMapping removed - manifest generator handles field mapping
86
+ }
87
+
88
+ export interface QuestionGroup {
89
+ required: Question[];
90
+ basic: Question[];
91
+ advanced: Question[];
92
+ open: {
93
+ question: string;
94
+ placeholder: string;
95
+ answer?: string;
96
+ };
97
+ }
98
+
99
+ export interface ResourceSolution {
100
+ type: 'single' | 'combination';
101
+ resources: ResourceSchema[];
102
+ score: number;
103
+ description: string;
104
+ reasons: string[];
105
+ analysis: string;
106
+ questions: QuestionGroup;
107
+ }
108
+
109
+ export interface AIRankingConfig {
110
+ claudeApiKey: string;
111
+ }
112
+
113
+ export interface DiscoveryFunctions {
114
+ discoverResources: () => Promise<any>;
115
+ explainResource: (resource: string) => Promise<any>;
116
+ }
117
+
118
+ export interface ClusterOptions {
119
+ namespaces: string[];
120
+ storageClasses: string[];
121
+ ingressClasses: string[];
122
+ nodeLabels: string[];
123
+ serviceAccounts?: { [namespace: string]: string[] };
124
+ }
125
+
126
+ /**
127
+ * SchemaParser converts kubectl explain output to structured ResourceSchema
128
+ */
129
+ export class SchemaParser {
130
+ /**
131
+ * Parse ResourceExplanation from discovery engine into structured schema
132
+ */
133
+ parseResourceExplanation(explanation: ResourceExplanation): ResourceSchema {
134
+ const apiVersion = explanation.group
135
+ ? `${explanation.group}/${explanation.version}`
136
+ : explanation.version;
137
+
138
+ const properties = new Map<string, SchemaField>();
139
+ const required: string[] = [];
140
+
141
+ // Process all fields from the explanation
142
+ for (const field of explanation.fields) {
143
+ const parts = field.name.split('.');
144
+ const topLevelField = parts[0];
145
+
146
+ // Add to required if marked as required
147
+ if (field.required && !required.includes(topLevelField)) {
148
+ required.push(topLevelField);
149
+ }
150
+
151
+ // Create or get the top-level field
152
+ if (!properties.has(topLevelField)) {
153
+ properties.set(topLevelField, {
154
+ name: topLevelField,
155
+ type: this.normalizeType(field.type),
156
+ description: field.description,
157
+ required: field.required,
158
+ constraints: this.parseFieldConstraints(field.type, field.description),
159
+ nested: new Map()
160
+ });
161
+ }
162
+
163
+ // Handle nested fields
164
+ if (parts.length > 1) {
165
+ this.addNestedField(properties.get(topLevelField)!, parts.slice(1), field);
166
+ }
167
+ }
168
+
169
+ return {
170
+ apiVersion,
171
+ kind: explanation.kind,
172
+ group: explanation.group,
173
+ version: explanation.version,
174
+ description: explanation.description,
175
+ properties,
176
+ required,
177
+ namespace: true // Default to namespaced, could be enhanced with discovery data
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Add nested field to the schema structure
183
+ */
184
+ private addNestedField(parentField: SchemaField, fieldParts: string[], field: any): void {
185
+ const currentPart = fieldParts[0];
186
+
187
+ if (!parentField.nested.has(currentPart)) {
188
+ parentField.nested.set(currentPart, {
189
+ name: `${parentField.name}.${currentPart}`,
190
+ type: this.normalizeType(field.type),
191
+ description: field.description,
192
+ required: field.required,
193
+ constraints: this.parseFieldConstraints(field.type, field.description),
194
+ nested: new Map()
195
+ });
196
+ }
197
+
198
+ // Continue recursively if there are more field parts
199
+ if (fieldParts.length > 1) {
200
+ this.addNestedField(parentField.nested.get(currentPart)!, fieldParts.slice(1), field);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Normalize field types from kubectl explain output
206
+ */
207
+ private normalizeType(type: string): string {
208
+ const lowerType = type.toLowerCase();
209
+
210
+ // Map kubectl types to standard types
211
+ const typeMap: { [key: string]: string } = {
212
+ 'object': 'object',
213
+ 'string': 'string',
214
+ 'integer': 'integer',
215
+ 'int32': 'integer',
216
+ 'int64': 'integer',
217
+ 'boolean': 'boolean',
218
+ 'array': 'array',
219
+ '[]string': 'array',
220
+ '[]object': 'array',
221
+ 'map[string]string': 'object',
222
+ 'map[string]object': 'object'
223
+ };
224
+
225
+ return typeMap[lowerType] || 'string';
226
+ }
227
+
228
+ /**
229
+ * Parse field constraints from description text
230
+ */
231
+ parseFieldConstraints(type: string, description: string): FieldConstraints {
232
+ const constraints: FieldConstraints = {};
233
+
234
+ // Extract minimum/maximum values
235
+ const minMatch = description.match(/(?:minimum|min):\s*(\d+)/i);
236
+ if (minMatch) {
237
+ constraints.minimum = parseInt(minMatch[1]);
238
+ }
239
+
240
+ const maxMatch = description.match(/(?:maximum|max):\s*(\d+)/i);
241
+ if (maxMatch) {
242
+ constraints.maximum = parseInt(maxMatch[1]);
243
+ }
244
+
245
+ // Extract enum values
246
+ const enumMatch = description.match(/(?:possible values|valid values|values)\s*(?:are)?:\s*([^.]+)/i);
247
+ if (enumMatch) {
248
+ const values = enumMatch[1]
249
+ .split(/,|\s+and\s+/)
250
+ .map(v => v.trim())
251
+ .filter(v => v.length > 0);
252
+ constraints.enum = values;
253
+ }
254
+
255
+ // Extract default values - handle multiple patterns: "(default: value)", "defaults to value", ". Default: value"
256
+ const defaultMatch = description.match(/(?:\(default:\s*([^)]+)\)|(?:defaults?\s+to\s+(\w+))|(?:\.\s+default:\s*(\w+)))/i);
257
+ if (defaultMatch) {
258
+ const defaultValue = (defaultMatch[1] || defaultMatch[2] || defaultMatch[3]).trim();
259
+ if (type === 'integer') {
260
+ const parsed = parseInt(defaultValue);
261
+ if (!isNaN(parsed)) {
262
+ constraints.default = parsed;
263
+ }
264
+ } else {
265
+ constraints.default = defaultValue;
266
+ }
267
+ }
268
+
269
+ // Extract string length constraints
270
+ const minLengthMatch = description.match(/min length:\s*(\d+)/i);
271
+ if (minLengthMatch) {
272
+ constraints.minLength = parseInt(minLengthMatch[1]);
273
+ }
274
+
275
+ const maxLengthMatch = description.match(/max length:\s*(\d+)/i);
276
+ if (maxLengthMatch) {
277
+ constraints.maxLength = parseInt(maxLengthMatch[1]);
278
+ }
279
+
280
+ return constraints;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * ManifestValidator validates Kubernetes manifests using kubectl dry-run
286
+ */
287
+ export class ManifestValidator {
288
+ /**
289
+ * Validate a manifest using kubectl dry-run
290
+ * This uses the actual Kubernetes API server validation for accuracy
291
+ */
292
+ async validateManifest(manifestPath: string, config?: { kubeconfig?: string; dryRunMode?: 'client' | 'server' }): Promise<ValidationResult> {
293
+ const errors: string[] = [];
294
+ const warnings: string[] = [];
295
+
296
+ try {
297
+ const dryRunMode = config?.dryRunMode || 'server';
298
+ const args = ['apply', '--dry-run=' + dryRunMode, '-f', manifestPath];
299
+
300
+ await executeKubectl(args, { kubeconfig: config?.kubeconfig });
301
+
302
+ // If we get here, validation passed
303
+ // kubectl dry-run will throw an error if validation fails
304
+
305
+ // Add best practice warnings by reading the manifest
306
+ const fs = await import('fs');
307
+ const yaml = await import('yaml');
308
+ const manifestContent = yaml.parse(fs.readFileSync(manifestPath, 'utf8')) as any;
309
+ this.addBestPracticeWarnings(manifestContent, warnings);
310
+
311
+ return {
312
+ valid: true,
313
+ errors,
314
+ warnings
315
+ };
316
+
317
+ } catch (error: any) {
318
+ // Parse kubectl error output for validation issues
319
+ const errorMessage = error.message || '';
320
+
321
+ if (errorMessage.includes('validation failed')) {
322
+ errors.push('Kubernetes validation failed: ' + errorMessage);
323
+ } else if (errorMessage.includes('unknown field')) {
324
+ errors.push('Unknown field in manifest: ' + errorMessage);
325
+ } else if (errorMessage.includes('required field')) {
326
+ errors.push('Missing required field: ' + errorMessage);
327
+ } else {
328
+ errors.push('Validation error: ' + errorMessage);
329
+ }
330
+
331
+ return {
332
+ valid: false,
333
+ errors,
334
+ warnings
335
+ };
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Add best practice warnings
341
+ */
342
+ private addBestPracticeWarnings(manifest: any, warnings: string[]): void {
343
+ // Check for missing labels
344
+ if (!manifest.metadata?.labels) {
345
+ warnings.push('Consider adding labels to metadata for better resource organization');
346
+ }
347
+
348
+ // Check for missing namespace in namespaced resources
349
+ if (!manifest.metadata?.namespace && manifest.kind !== 'Namespace') {
350
+ warnings.push('Consider specifying a namespace for better resource isolation');
351
+ }
352
+ }
353
+ }
354
+
355
+ /**
356
+ * ResourceRecommender determines which resources best meet user needs using AI
357
+ */
358
+ export class ResourceRecommender {
359
+ private claudeIntegration: ClaudeIntegration;
360
+ private config: AIRankingConfig;
361
+
362
+ constructor(config: AIRankingConfig) {
363
+ this.config = config;
364
+ this.claudeIntegration = new ClaudeIntegration(config.claudeApiKey);
365
+ }
366
+
367
+ /**
368
+ * Find the best resource solution(s) for user intent using two-phase analysis
369
+ */
370
+ async findBestSolutions(
371
+ intent: string,
372
+ discoverResources: () => Promise<any>,
373
+ explainResource: (resource: string) => Promise<any>
374
+ ): Promise<ResourceSolution[]> {
375
+ if (!this.claudeIntegration.isInitialized()) {
376
+ throw new Error('Claude integration not initialized. API key required for AI-powered resource ranking.');
377
+ }
378
+
379
+ try {
380
+ // Phase 1: Get lightweight resource list and let AI select candidates
381
+ const resourceMap = await discoverResources();
382
+ const allResources = [...resourceMap.resources, ...resourceMap.custom];
383
+ const candidates = await this.selectResourceCandidates(intent, allResources);
384
+
385
+ // Phase 2: Fetch detailed schemas for selected candidates and rank
386
+ const schemas = await this.fetchDetailedSchemas(candidates, explainResource);
387
+ return await this.rankWithDetailedSchemas(intent, schemas);
388
+ } catch (error) {
389
+ throw new Error(`AI-powered resource solution analysis failed: ${error}`);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Phase 1: AI selects promising resource candidates from lightweight list
395
+ */
396
+ private async selectResourceCandidates(intent: string, resources: any[]): Promise<any[]> {
397
+ // Normalize resource structures between standard resources and CRDs
398
+ const normalizedResources = resources.map(resource => {
399
+ // Handle both standard resources and CRDs
400
+ const apiVersion = resource.apiVersion ||
401
+ (resource.group ? `${resource.group}/${resource.version}` : resource.version);
402
+ const isNamespaced = resource.namespaced !== undefined ?
403
+ resource.namespaced :
404
+ resource.scope === 'Namespaced';
405
+
406
+ return {
407
+ ...resource,
408
+ apiVersion,
409
+ namespaced: isNamespaced
410
+ };
411
+ });
412
+
413
+ const resourceSummary = normalizedResources.map((resource, index) =>
414
+ `${index}: ${resource.kind} (${resource.apiVersion})
415
+ Group: ${resource.group || 'core'}
416
+ Namespaced: ${resource.namespaced}`
417
+ ).join('\n\n');
418
+
419
+ const fs = await import('fs');
420
+ const path = await import('path');
421
+
422
+ const promptPath = path.join(process.cwd(), 'prompts', 'resource-selection.md');
423
+ const template = fs.readFileSync(promptPath, 'utf8');
424
+
425
+ const selectionPrompt = template
426
+ .replace('{intent}', intent)
427
+ .replace('{resources}', resourceSummary);
428
+
429
+ const response = await this.claudeIntegration.sendMessage(selectionPrompt);
430
+
431
+ try {
432
+ // Extract JSON from response with robust parsing
433
+ let jsonContent = response.content;
434
+
435
+ // First try to find JSON array wrapped in code blocks
436
+ const codeBlockMatch = response.content.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
437
+ if (codeBlockMatch) {
438
+ jsonContent = codeBlockMatch[1];
439
+ } else {
440
+ // Try to find JSON array that starts with [ and find the matching closing ]
441
+ const startIndex = response.content.indexOf('[');
442
+ if (startIndex !== -1) {
443
+ let bracketCount = 0;
444
+ let endIndex = startIndex;
445
+
446
+ for (let i = startIndex; i < response.content.length; i++) {
447
+ if (response.content[i] === '[') bracketCount++;
448
+ if (response.content[i] === ']') bracketCount--;
449
+ if (bracketCount === 0) {
450
+ endIndex = i;
451
+ break;
452
+ }
453
+ }
454
+
455
+ if (bracketCount === 0) {
456
+ jsonContent = response.content.substring(startIndex, endIndex + 1);
457
+ }
458
+ }
459
+ }
460
+
461
+ const selectedResources = JSON.parse(jsonContent.trim());
462
+
463
+ if (!Array.isArray(selectedResources)) {
464
+ throw new Error('AI response is not an array');
465
+ }
466
+
467
+ // Validate that each resource has required fields
468
+ for (const resource of selectedResources) {
469
+ if (!resource.kind || !resource.apiVersion) {
470
+ throw new Error(`AI selected invalid resource: ${JSON.stringify(resource)}`);
471
+ }
472
+ }
473
+
474
+ return selectedResources;
475
+ } catch (error) {
476
+ throw new Error(`AI failed to select resources in valid JSON format. Error: ${(error as Error).message}. AI response: "${response.content.substring(0, 200)}..."`);
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Phase 2: Fetch detailed schemas for selected candidates
482
+ */
483
+ private async fetchDetailedSchemas(candidates: any[], explainResource: (resource: string) => Promise<any>): Promise<ResourceSchema[]> {
484
+ const schemas: ResourceSchema[] = [];
485
+ const errors: string[] = [];
486
+
487
+ for (const resource of candidates) {
488
+ try {
489
+ const explanation = await explainResource(resource.kind);
490
+
491
+ // Parse GROUP, KIND, VERSION from kubectl explain output
492
+ const lines = explanation.split('\n');
493
+ const groupLine = lines.find((line: string) => line.startsWith('GROUP:'));
494
+ const kindLine = lines.find((line: string) => line.startsWith('KIND:'));
495
+ const versionLine = lines.find((line: string) => line.startsWith('VERSION:'));
496
+
497
+ const group = groupLine ? groupLine.replace('GROUP:', '').trim() : '';
498
+ const kind = kindLine ? kindLine.replace('KIND:', '').trim() : resource.kind;
499
+ const version = versionLine ? versionLine.replace('VERSION:', '').trim() : 'v1';
500
+
501
+ // Build apiVersion from group and version
502
+ const apiVersion = group ? `${group}/${version}` : version;
503
+
504
+ // Create a simple schema with raw explanation for AI processing
505
+ const schema: ResourceSchema = {
506
+ kind: kind,
507
+ apiVersion: apiVersion,
508
+ group: group,
509
+ description: explanation.split('\n').find((line: string) => line.startsWith('DESCRIPTION:'))?.replace('DESCRIPTION:', '').trim() || '',
510
+ properties: new Map<string, SchemaField>(),
511
+ rawExplanation: explanation // Include raw explanation for AI
512
+ };
513
+ schemas.push(schema);
514
+ } catch (error) {
515
+ errors.push(`${resource.kind}: ${(error as Error).message}`);
516
+ }
517
+ }
518
+
519
+ if (schemas.length === 0) {
520
+ throw new Error(`Could not fetch schemas for any selected resources. Candidates: ${candidates.map(c => c.kind).join(', ')}. Errors: ${errors.join(', ')}`);
521
+ }
522
+
523
+ if (errors.length > 0) {
524
+ console.warn(`Some resources could not be analyzed: ${errors.join(', ')}`);
525
+ console.warn(`Successfully fetched schemas for: ${schemas.map(s => s.kind).join(', ')}`);
526
+ }
527
+
528
+ return schemas;
529
+ }
530
+
531
+ /**
532
+ * Phase 3: Rank resources with detailed schema information
533
+ */
534
+ private async rankWithDetailedSchemas(intent: string, schemas: ResourceSchema[]): Promise<ResourceSolution[]> {
535
+ const prompt = await this.loadPromptTemplate(intent, schemas);
536
+ const response = await this.claudeIntegration.sendMessage(prompt);
537
+ const solutions = this.parseAISolutionResponse(response.content, schemas);
538
+
539
+ // Generate AI-powered questions for each solution
540
+ for (const solution of solutions) {
541
+ solution.questions = await this.generateQuestionsWithAI(intent, solution);
542
+ }
543
+
544
+ return solutions;
545
+ }
546
+
547
+ /**
548
+ * Load and format prompt template from file
549
+ */
550
+ private async loadPromptTemplate(intent: string, schemas: ResourceSchema[]): Promise<string> {
551
+ const fs = await import('fs');
552
+ const path = await import('path');
553
+
554
+ const promptPath = path.join(process.cwd(), 'prompts', 'resource-solution-ranking.md');
555
+ const template = fs.readFileSync(promptPath, 'utf8');
556
+
557
+ // Format resources for the prompt
558
+ const resourcesText = schemas.map((schema, index) =>
559
+ `${index}: ${schema.kind} (${schema.apiVersion})
560
+ Group: ${schema.group || 'core'}
561
+ Description: ${schema.description}
562
+ Namespaced: ${schema.namespace}`
563
+ ).join('\n\n');
564
+
565
+ return template
566
+ .replace('{intent}', intent)
567
+ .replace('{resources}', resourcesText);
568
+ }
569
+
570
+ /**
571
+ * Parse AI response into solution results
572
+ */
573
+ private parseAISolutionResponse(aiResponse: string, schemas: ResourceSchema[]): ResourceSolution[] {
574
+ try {
575
+ // Use robust JSON extraction
576
+ const parsed = this.extractJsonFromAIResponse(aiResponse);
577
+
578
+ const solutions: ResourceSolution[] = parsed.solutions.map((solution: any) => {
579
+ const isDebugMode = process.env.DOT_AI_DEBUG === 'true';
580
+
581
+ if (isDebugMode) {
582
+ console.debug('DEBUG: solution object:', JSON.stringify(solution, null, 2));
583
+ }
584
+
585
+ // Find matching schemas for the requested resources
586
+ const resources: ResourceSchema[] = [];
587
+ const notFound: any[] = [];
588
+
589
+ for (const requestedResource of solution.resources || []) {
590
+ const matchingSchema = schemas.find(schema =>
591
+ schema.kind === requestedResource.kind &&
592
+ schema.apiVersion === requestedResource.apiVersion &&
593
+ schema.group === requestedResource.group
594
+ );
595
+
596
+ if (matchingSchema) {
597
+ resources.push(matchingSchema);
598
+ } else {
599
+ notFound.push(requestedResource);
600
+ }
601
+ }
602
+
603
+ if (resources.length === 0) {
604
+ if (isDebugMode) {
605
+ console.debug('DEBUG: No matching resources found');
606
+ console.debug('DEBUG: Requested resources:', solution.resources);
607
+ console.debug('DEBUG: Available schemas:', schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group })));
608
+ }
609
+
610
+ const debugInfo = {
611
+ requestedResources: solution.resources || [],
612
+ notFoundResources: notFound,
613
+ availableSchemas: schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group }))
614
+ };
615
+ throw new Error(`No matching resources found: ${JSON.stringify(debugInfo, null, 2)}`);
616
+ }
617
+
618
+ if (notFound.length > 0 && isDebugMode) {
619
+ console.debug('DEBUG: Some resources not found:', notFound);
620
+ }
621
+
622
+ return {
623
+ type: solution.type,
624
+ resources,
625
+ score: solution.score,
626
+ description: solution.description,
627
+ reasons: solution.reasons || [],
628
+ analysis: solution.analysis || '',
629
+ questions: { required: [], basic: [], advanced: [], open: { question: '', placeholder: '' } }
630
+ };
631
+ });
632
+
633
+ // Sort by score descending
634
+ return solutions.sort((a, b) => b.score - a.score);
635
+
636
+ } catch (error) {
637
+ // Enhanced error message with more context
638
+ const errorMsg = `Failed to parse AI solution response: ${(error as Error).message}`;
639
+ const contextMsg = `\nAI Response (first 500 chars): "${aiResponse.substring(0, 500)}..."`;
640
+ const schemasMsg = `\nAvailable schemas: ${schemas.map(s => s.kind).join(', ')} (total: ${schemas.length})`;
641
+ throw new Error(errorMsg + contextMsg + schemasMsg);
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Discover cluster options for dynamic question generation
647
+ */
648
+ private async discoverClusterOptions(): Promise<ClusterOptions> {
649
+ try {
650
+ const { executeKubectl } = await import('./kubernetes-utils');
651
+
652
+ // Discover namespaces
653
+ const namespacesResult = await executeKubectl(['get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}']);
654
+ const namespaces = namespacesResult.split(/\s+/).filter(Boolean);
655
+
656
+ // Discover storage classes
657
+ let storageClasses: string[] = [];
658
+ try {
659
+ const storageResult = await executeKubectl(['get', 'storageclass', '-o', 'jsonpath={.items[*].metadata.name}']);
660
+ storageClasses = storageResult.split(/\s+/).filter(Boolean);
661
+ } catch {
662
+ // Storage classes might not be available in all clusters
663
+ storageClasses = [];
664
+ }
665
+
666
+ // Discover ingress classes
667
+ let ingressClasses: string[] = [];
668
+ try {
669
+ const ingressResult = await executeKubectl(['get', 'ingressclass', '-o', 'jsonpath={.items[*].metadata.name}']);
670
+ ingressClasses = ingressResult.split(/\s+/).filter(Boolean);
671
+ } catch {
672
+ // Ingress classes might not be available
673
+ ingressClasses = [];
674
+ }
675
+
676
+ // Get common node labels
677
+ let nodeLabels: string[] = [];
678
+ try {
679
+ const nodesResult = await executeKubectl(['get', 'nodes', '-o', 'json']);
680
+ const nodes = JSON.parse(nodesResult);
681
+ const labelSet = new Set<string>();
682
+
683
+ nodes.items?.forEach((node: any) => {
684
+ Object.keys(node.metadata?.labels || {}).forEach(label => {
685
+ if (!label.startsWith('kubernetes.io/') && !label.startsWith('node.kubernetes.io/')) {
686
+ labelSet.add(label);
687
+ }
688
+ });
689
+ });
690
+
691
+ nodeLabels = Array.from(labelSet);
692
+ } catch {
693
+ nodeLabels = [];
694
+ }
695
+
696
+ return {
697
+ namespaces,
698
+ storageClasses,
699
+ ingressClasses,
700
+ nodeLabels
701
+ };
702
+ } catch (error) {
703
+ console.warn('Failed to discover cluster options, using defaults:', error);
704
+ return {
705
+ namespaces: ['default'],
706
+ storageClasses: [],
707
+ ingressClasses: [],
708
+ nodeLabels: []
709
+ };
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Extract JSON object from AI response with robust parsing
715
+ */
716
+ private extractJsonFromAIResponse(aiResponse: string): any {
717
+ let jsonContent = aiResponse;
718
+
719
+ // First try to find JSON wrapped in code blocks
720
+ const codeBlockMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
721
+ if (codeBlockMatch) {
722
+ jsonContent = codeBlockMatch[1];
723
+ } else {
724
+ // Try to find JSON that starts with { and find the matching closing }
725
+ const startIndex = aiResponse.indexOf('{');
726
+ if (startIndex !== -1) {
727
+ let braceCount = 0;
728
+ let endIndex = startIndex;
729
+
730
+ for (let i = startIndex; i < aiResponse.length; i++) {
731
+ if (aiResponse[i] === '{') braceCount++;
732
+ if (aiResponse[i] === '}') braceCount--;
733
+ if (braceCount === 0) {
734
+ endIndex = i;
735
+ break;
736
+ }
737
+ }
738
+
739
+ if (braceCount === 0) {
740
+ jsonContent = aiResponse.substring(startIndex, endIndex + 1);
741
+ }
742
+ }
743
+ }
744
+
745
+ return JSON.parse(jsonContent.trim());
746
+ }
747
+
748
+ /**
749
+ * Generate contextual questions using AI based on user intent and solution resources
750
+ */
751
+ private async generateQuestionsWithAI(intent: string, solution: ResourceSolution): Promise<QuestionGroup> {
752
+ try {
753
+ // Discover cluster options for dynamic questions
754
+ const clusterOptions = await this.discoverClusterOptions();
755
+
756
+ // Format resource details for the prompt using raw explanation when available
757
+ const resourceDetails = solution.resources.map(resource => {
758
+ if (resource.rawExplanation) {
759
+ // Use raw kubectl explain output for comprehensive field information
760
+ return `${resource.kind} (${resource.apiVersion}):
761
+ Description: ${resource.description}
762
+
763
+ Complete Schema Information:
764
+ ${resource.rawExplanation}`;
765
+ } else {
766
+ // Fallback to properties map if raw explanation is not available
767
+ const properties = Array.from(resource.properties.entries()).map(([key, field]) => {
768
+ const nestedFields = Array.from(field.nested.entries()).map(([nestedKey, nestedField]) =>
769
+ ` ${nestedKey}: ${nestedField.type} - ${nestedField.description}`
770
+ ).join('\n');
771
+
772
+ return ` ${key}: ${field.type} - ${field.description}${field.required ? ' (required)' : ''}${nestedFields ? '\n' + nestedFields : ''}`;
773
+ }).join('\n');
774
+
775
+ return `${resource.kind} (${resource.apiVersion}):
776
+ Description: ${resource.description}
777
+ Required fields: ${resource.required?.join(', ') || 'none specified'}
778
+ Properties:
779
+ ${properties}`;
780
+ }
781
+ }).join('\n\n');
782
+
783
+ // Format cluster options for the prompt
784
+ const clusterOptionsText = `Available Namespaces: ${clusterOptions.namespaces.join(', ')}
785
+ Available Storage Classes: ${clusterOptions.storageClasses.length > 0 ? clusterOptions.storageClasses.join(', ') : 'None discovered'}
786
+ Available Ingress Classes: ${clusterOptions.ingressClasses.length > 0 ? clusterOptions.ingressClasses.join(', ') : 'None discovered'}
787
+ Available Node Labels: ${clusterOptions.nodeLabels.length > 0 ? clusterOptions.nodeLabels.slice(0, 10).join(', ') : 'None discovered'}`;
788
+
789
+ // Load and format the question generation prompt
790
+ const fs = await import('fs');
791
+ const path = await import('path');
792
+
793
+ const promptPath = path.join(process.cwd(), 'prompts', 'question-generation.md');
794
+ const template = fs.readFileSync(promptPath, 'utf8');
795
+
796
+ const questionPrompt = template
797
+ .replace('{intent}', intent)
798
+ .replace('{solution_description}', solution.description)
799
+ .replace('{resource_details}', resourceDetails)
800
+ .replace('{cluster_options}', clusterOptionsText);
801
+
802
+ const response = await this.claudeIntegration.sendMessage(questionPrompt);
803
+
804
+ // Use robust JSON extraction
805
+ const questions = this.extractJsonFromAIResponse(response.content);
806
+
807
+ // Validate the response structure
808
+ if (!questions.required || !questions.basic || !questions.advanced || !questions.open) {
809
+ throw new Error('Invalid question structure from AI');
810
+ }
811
+
812
+ return questions as QuestionGroup;
813
+ } catch (error) {
814
+ console.warn(`Failed to generate AI questions for solution: ${error}`);
815
+
816
+ // Fallback to basic open question
817
+ return {
818
+ required: [],
819
+ basic: [],
820
+ advanced: [],
821
+ open: {
822
+ question: "Is there anything else about your requirements or constraints that would help us provide better recommendations?",
823
+ placeholder: "e.g., specific security requirements, performance needs, existing infrastructure constraints..."
824
+ }
825
+ };
826
+ }
827
+ }
828
+ }
829
+
830
+