@vfarcic/dot-ai 0.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +51 -0
  6. package/dist/core/claude.d.ts +42 -0
  7. package/dist/core/claude.d.ts.map +1 -0
  8. package/dist/core/claude.js +229 -0
  9. package/dist/core/deploy-operation.d.ts +38 -0
  10. package/dist/core/deploy-operation.d.ts.map +1 -0
  11. package/dist/core/deploy-operation.js +101 -0
  12. package/dist/core/discovery.d.ts +162 -0
  13. package/dist/core/discovery.d.ts.map +1 -0
  14. package/dist/core/discovery.js +758 -0
  15. package/dist/core/error-handling.d.ts +167 -0
  16. package/dist/core/error-handling.d.ts.map +1 -0
  17. package/dist/core/error-handling.js +399 -0
  18. package/dist/core/index.d.ts +42 -0
  19. package/dist/core/index.d.ts.map +1 -0
  20. package/dist/core/index.js +123 -0
  21. package/dist/core/kubernetes-utils.d.ts +38 -0
  22. package/dist/core/kubernetes-utils.d.ts.map +1 -0
  23. package/dist/core/kubernetes-utils.js +177 -0
  24. package/dist/core/memory.d.ts +45 -0
  25. package/dist/core/memory.d.ts.map +1 -0
  26. package/dist/core/memory.js +113 -0
  27. package/dist/core/schema.d.ts +187 -0
  28. package/dist/core/schema.d.ts.map +1 -0
  29. package/dist/core/schema.js +655 -0
  30. package/dist/core/session-utils.d.ts +29 -0
  31. package/dist/core/session-utils.d.ts.map +1 -0
  32. package/dist/core/session-utils.js +121 -0
  33. package/dist/core/workflow.d.ts +70 -0
  34. package/dist/core/workflow.d.ts.map +1 -0
  35. package/dist/core/workflow.js +161 -0
  36. package/dist/index.d.ts +15 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +32 -0
  39. package/dist/interfaces/cli.d.ts +74 -0
  40. package/dist/interfaces/cli.d.ts.map +1 -0
  41. package/dist/interfaces/cli.js +769 -0
  42. package/dist/interfaces/mcp.d.ts +30 -0
  43. package/dist/interfaces/mcp.d.ts.map +1 -0
  44. package/dist/interfaces/mcp.js +105 -0
  45. package/dist/mcp/server.d.ts +9 -0
  46. package/dist/mcp/server.d.ts.map +1 -0
  47. package/dist/mcp/server.js +151 -0
  48. package/dist/tools/answer-question.d.ts +27 -0
  49. package/dist/tools/answer-question.d.ts.map +1 -0
  50. package/dist/tools/answer-question.js +696 -0
  51. package/dist/tools/choose-solution.d.ts +23 -0
  52. package/dist/tools/choose-solution.d.ts.map +1 -0
  53. package/dist/tools/choose-solution.js +171 -0
  54. package/dist/tools/deploy-manifests.d.ts +25 -0
  55. package/dist/tools/deploy-manifests.d.ts.map +1 -0
  56. package/dist/tools/deploy-manifests.js +74 -0
  57. package/dist/tools/generate-manifests.d.ts +23 -0
  58. package/dist/tools/generate-manifests.d.ts.map +1 -0
  59. package/dist/tools/generate-manifests.js +424 -0
  60. package/dist/tools/index.d.ts +11 -0
  61. package/dist/tools/index.d.ts.map +1 -0
  62. package/dist/tools/index.js +34 -0
  63. package/dist/tools/recommend.d.ts +23 -0
  64. package/dist/tools/recommend.d.ts.map +1 -0
  65. package/dist/tools/recommend.js +332 -0
  66. package/package.json +124 -0
  67. package/prompts/intent-validation.md +65 -0
  68. package/prompts/manifest-generation.md +79 -0
  69. package/prompts/question-generation.md +128 -0
  70. package/prompts/resource-analysis.md +127 -0
  71. package/prompts/resource-selection.md +55 -0
  72. package/prompts/resource-solution-ranking.md +77 -0
  73. package/prompts/solution-enhancement.md +129 -0
@@ -0,0 +1,655 @@
1
+ "use strict";
2
+ /**
3
+ * Resource Schema Parser and Validator
4
+ *
5
+ * Implements comprehensive schema parsing and validation for Kubernetes resources
6
+ * Fetches structured OpenAPI schemas from Kubernetes API server and validates manifests
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.ResourceRecommender = exports.ManifestValidator = exports.SchemaParser = void 0;
43
+ const kubernetes_utils_1 = require("./kubernetes-utils");
44
+ const claude_1 = require("./claude");
45
+ /**
46
+ * SchemaParser converts kubectl explain output to structured ResourceSchema
47
+ */
48
+ class SchemaParser {
49
+ /**
50
+ * Parse ResourceExplanation from discovery engine into structured schema
51
+ */
52
+ parseResourceExplanation(explanation) {
53
+ const apiVersion = explanation.group
54
+ ? `${explanation.group}/${explanation.version}`
55
+ : explanation.version;
56
+ const properties = new Map();
57
+ const required = [];
58
+ // Process all fields from the explanation
59
+ for (const field of explanation.fields) {
60
+ const parts = field.name.split('.');
61
+ const topLevelField = parts[0];
62
+ // Add to required if marked as required
63
+ if (field.required && !required.includes(topLevelField)) {
64
+ required.push(topLevelField);
65
+ }
66
+ // Create or get the top-level field
67
+ if (!properties.has(topLevelField)) {
68
+ properties.set(topLevelField, {
69
+ name: topLevelField,
70
+ type: this.normalizeType(field.type),
71
+ description: field.description,
72
+ required: field.required,
73
+ constraints: this.parseFieldConstraints(field.type, field.description),
74
+ nested: new Map()
75
+ });
76
+ }
77
+ // Handle nested fields
78
+ if (parts.length > 1) {
79
+ this.addNestedField(properties.get(topLevelField), parts.slice(1), field);
80
+ }
81
+ }
82
+ return {
83
+ apiVersion,
84
+ kind: explanation.kind,
85
+ group: explanation.group,
86
+ version: explanation.version,
87
+ description: explanation.description,
88
+ properties,
89
+ required,
90
+ namespace: true // Default to namespaced, could be enhanced with discovery data
91
+ };
92
+ }
93
+ /**
94
+ * Add nested field to the schema structure
95
+ */
96
+ addNestedField(parentField, fieldParts, field) {
97
+ const currentPart = fieldParts[0];
98
+ if (!parentField.nested.has(currentPart)) {
99
+ parentField.nested.set(currentPart, {
100
+ name: `${parentField.name}.${currentPart}`,
101
+ type: this.normalizeType(field.type),
102
+ description: field.description,
103
+ required: field.required,
104
+ constraints: this.parseFieldConstraints(field.type, field.description),
105
+ nested: new Map()
106
+ });
107
+ }
108
+ // Continue recursively if there are more field parts
109
+ if (fieldParts.length > 1) {
110
+ this.addNestedField(parentField.nested.get(currentPart), fieldParts.slice(1), field);
111
+ }
112
+ }
113
+ /**
114
+ * Normalize field types from kubectl explain output
115
+ */
116
+ normalizeType(type) {
117
+ const lowerType = type.toLowerCase();
118
+ // Map kubectl types to standard types
119
+ const typeMap = {
120
+ 'object': 'object',
121
+ 'string': 'string',
122
+ 'integer': 'integer',
123
+ 'int32': 'integer',
124
+ 'int64': 'integer',
125
+ 'boolean': 'boolean',
126
+ 'array': 'array',
127
+ '[]string': 'array',
128
+ '[]object': 'array',
129
+ 'map[string]string': 'object',
130
+ 'map[string]object': 'object'
131
+ };
132
+ return typeMap[lowerType] || 'string';
133
+ }
134
+ /**
135
+ * Parse field constraints from description text
136
+ */
137
+ parseFieldConstraints(type, description) {
138
+ const constraints = {};
139
+ // Extract minimum/maximum values
140
+ const minMatch = description.match(/(?:minimum|min):\s*(\d+)/i);
141
+ if (minMatch) {
142
+ constraints.minimum = parseInt(minMatch[1]);
143
+ }
144
+ const maxMatch = description.match(/(?:maximum|max):\s*(\d+)/i);
145
+ if (maxMatch) {
146
+ constraints.maximum = parseInt(maxMatch[1]);
147
+ }
148
+ // Extract enum values
149
+ const enumMatch = description.match(/(?:possible values|valid values|values)\s*(?:are)?:\s*([^.]+)/i);
150
+ if (enumMatch) {
151
+ const values = enumMatch[1]
152
+ .split(/,|\s+and\s+/)
153
+ .map(v => v.trim())
154
+ .filter(v => v.length > 0);
155
+ constraints.enum = values;
156
+ }
157
+ // Extract default values - handle multiple patterns: "(default: value)", "defaults to value", ". Default: value"
158
+ const defaultMatch = description.match(/(?:\(default:\s*([^)]+)\)|(?:defaults?\s+to\s+(\w+))|(?:\.\s+default:\s*(\w+)))/i);
159
+ if (defaultMatch) {
160
+ const defaultValue = (defaultMatch[1] || defaultMatch[2] || defaultMatch[3]).trim();
161
+ if (type === 'integer') {
162
+ const parsed = parseInt(defaultValue);
163
+ if (!isNaN(parsed)) {
164
+ constraints.default = parsed;
165
+ }
166
+ }
167
+ else {
168
+ constraints.default = defaultValue;
169
+ }
170
+ }
171
+ // Extract string length constraints
172
+ const minLengthMatch = description.match(/min length:\s*(\d+)/i);
173
+ if (minLengthMatch) {
174
+ constraints.minLength = parseInt(minLengthMatch[1]);
175
+ }
176
+ const maxLengthMatch = description.match(/max length:\s*(\d+)/i);
177
+ if (maxLengthMatch) {
178
+ constraints.maxLength = parseInt(maxLengthMatch[1]);
179
+ }
180
+ return constraints;
181
+ }
182
+ }
183
+ exports.SchemaParser = SchemaParser;
184
+ /**
185
+ * ManifestValidator validates Kubernetes manifests using kubectl dry-run
186
+ */
187
+ class ManifestValidator {
188
+ /**
189
+ * Validate a manifest using kubectl dry-run
190
+ * This uses the actual Kubernetes API server validation for accuracy
191
+ */
192
+ async validateManifest(manifestPath, config) {
193
+ const errors = [];
194
+ const warnings = [];
195
+ try {
196
+ const dryRunMode = config?.dryRunMode || 'server';
197
+ const args = ['apply', '--dry-run=' + dryRunMode, '-f', manifestPath];
198
+ await (0, kubernetes_utils_1.executeKubectl)(args, { kubeconfig: config?.kubeconfig });
199
+ // If we get here, validation passed
200
+ // kubectl dry-run will throw an error if validation fails
201
+ // Add best practice warnings by reading the manifest
202
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
203
+ const yaml = await Promise.resolve().then(() => __importStar(require('yaml')));
204
+ const manifestContent = yaml.parse(fs.readFileSync(manifestPath, 'utf8'));
205
+ this.addBestPracticeWarnings(manifestContent, warnings);
206
+ return {
207
+ valid: true,
208
+ errors,
209
+ warnings
210
+ };
211
+ }
212
+ catch (error) {
213
+ // Parse kubectl error output for validation issues
214
+ const errorMessage = error.message || '';
215
+ if (errorMessage.includes('validation failed')) {
216
+ errors.push('Kubernetes validation failed: ' + errorMessage);
217
+ }
218
+ else if (errorMessage.includes('unknown field')) {
219
+ errors.push('Unknown field in manifest: ' + errorMessage);
220
+ }
221
+ else if (errorMessage.includes('required field')) {
222
+ errors.push('Missing required field: ' + errorMessage);
223
+ }
224
+ else {
225
+ errors.push('Validation error: ' + errorMessage);
226
+ }
227
+ return {
228
+ valid: false,
229
+ errors,
230
+ warnings
231
+ };
232
+ }
233
+ }
234
+ /**
235
+ * Add best practice warnings
236
+ */
237
+ addBestPracticeWarnings(manifest, warnings) {
238
+ // Check for missing labels
239
+ if (!manifest.metadata?.labels) {
240
+ warnings.push('Consider adding labels to metadata for better resource organization');
241
+ }
242
+ // Check for missing namespace in namespaced resources
243
+ if (!manifest.metadata?.namespace && manifest.kind !== 'Namespace') {
244
+ warnings.push('Consider specifying a namespace for better resource isolation');
245
+ }
246
+ }
247
+ }
248
+ exports.ManifestValidator = ManifestValidator;
249
+ /**
250
+ * ResourceRecommender determines which resources best meet user needs using AI
251
+ */
252
+ class ResourceRecommender {
253
+ claudeIntegration;
254
+ config;
255
+ constructor(config) {
256
+ this.config = config;
257
+ this.claudeIntegration = new claude_1.ClaudeIntegration(config.claudeApiKey);
258
+ }
259
+ /**
260
+ * Find the best resource solution(s) for user intent using two-phase analysis
261
+ */
262
+ async findBestSolutions(intent, discoverResources, explainResource) {
263
+ if (!this.claudeIntegration.isInitialized()) {
264
+ throw new Error('Claude integration not initialized. API key required for AI-powered resource ranking.');
265
+ }
266
+ try {
267
+ // Phase 1: Get lightweight resource list and let AI select candidates
268
+ const resourceMap = await discoverResources();
269
+ const allResources = [...resourceMap.resources, ...resourceMap.custom];
270
+ const candidates = await this.selectResourceCandidates(intent, allResources);
271
+ // Phase 2: Fetch detailed schemas for selected candidates and rank
272
+ const schemas = await this.fetchDetailedSchemas(candidates, explainResource);
273
+ return await this.rankWithDetailedSchemas(intent, schemas);
274
+ }
275
+ catch (error) {
276
+ throw new Error(`AI-powered resource solution analysis failed: ${error}`);
277
+ }
278
+ }
279
+ /**
280
+ * Phase 1: AI selects promising resource candidates from lightweight list
281
+ */
282
+ async selectResourceCandidates(intent, resources) {
283
+ // Normalize resource structures between standard resources and CRDs
284
+ const normalizedResources = resources.map(resource => {
285
+ // Handle both standard resources and CRDs
286
+ const apiVersion = resource.apiVersion ||
287
+ (resource.group ? `${resource.group}/${resource.version}` : resource.version);
288
+ const isNamespaced = resource.namespaced !== undefined ?
289
+ resource.namespaced :
290
+ resource.scope === 'Namespaced';
291
+ return {
292
+ ...resource,
293
+ apiVersion,
294
+ namespaced: isNamespaced
295
+ };
296
+ });
297
+ const resourceSummary = normalizedResources.map((resource, index) => `${index}: ${resource.kind} (${resource.apiVersion})
298
+ Group: ${resource.group || 'core'}
299
+ Namespaced: ${resource.namespaced}`).join('\n\n');
300
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
301
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
302
+ const promptPath = path.join(process.cwd(), 'prompts', 'resource-selection.md');
303
+ const template = fs.readFileSync(promptPath, 'utf8');
304
+ const selectionPrompt = template
305
+ .replace('{intent}', intent)
306
+ .replace('{resources}', resourceSummary);
307
+ const response = await this.claudeIntegration.sendMessage(selectionPrompt);
308
+ try {
309
+ // Extract JSON from response with robust parsing
310
+ let jsonContent = response.content;
311
+ // First try to find JSON array wrapped in code blocks
312
+ const codeBlockMatch = response.content.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
313
+ if (codeBlockMatch) {
314
+ jsonContent = codeBlockMatch[1];
315
+ }
316
+ else {
317
+ // Try to find JSON array that starts with [ and find the matching closing ]
318
+ const startIndex = response.content.indexOf('[');
319
+ if (startIndex !== -1) {
320
+ let bracketCount = 0;
321
+ let endIndex = startIndex;
322
+ for (let i = startIndex; i < response.content.length; i++) {
323
+ if (response.content[i] === '[')
324
+ bracketCount++;
325
+ if (response.content[i] === ']')
326
+ bracketCount--;
327
+ if (bracketCount === 0) {
328
+ endIndex = i;
329
+ break;
330
+ }
331
+ }
332
+ if (bracketCount === 0) {
333
+ jsonContent = response.content.substring(startIndex, endIndex + 1);
334
+ }
335
+ }
336
+ }
337
+ const selectedResources = JSON.parse(jsonContent.trim());
338
+ if (!Array.isArray(selectedResources)) {
339
+ throw new Error('AI response is not an array');
340
+ }
341
+ // Validate that each resource has required fields
342
+ for (const resource of selectedResources) {
343
+ if (!resource.kind || !resource.apiVersion) {
344
+ throw new Error(`AI selected invalid resource: ${JSON.stringify(resource)}`);
345
+ }
346
+ }
347
+ return selectedResources;
348
+ }
349
+ catch (error) {
350
+ throw new Error(`AI failed to select resources in valid JSON format. Error: ${error.message}. AI response: "${response.content.substring(0, 200)}..."`);
351
+ }
352
+ }
353
+ /**
354
+ * Phase 2: Fetch detailed schemas for selected candidates
355
+ */
356
+ async fetchDetailedSchemas(candidates, explainResource) {
357
+ const schemas = [];
358
+ const errors = [];
359
+ for (const resource of candidates) {
360
+ try {
361
+ const explanation = await explainResource(resource.kind);
362
+ // Parse GROUP, KIND, VERSION from kubectl explain output
363
+ const lines = explanation.split('\n');
364
+ const groupLine = lines.find((line) => line.startsWith('GROUP:'));
365
+ const kindLine = lines.find((line) => line.startsWith('KIND:'));
366
+ const versionLine = lines.find((line) => line.startsWith('VERSION:'));
367
+ const group = groupLine ? groupLine.replace('GROUP:', '').trim() : '';
368
+ const kind = kindLine ? kindLine.replace('KIND:', '').trim() : resource.kind;
369
+ const version = versionLine ? versionLine.replace('VERSION:', '').trim() : 'v1';
370
+ // Build apiVersion from group and version
371
+ const apiVersion = group ? `${group}/${version}` : version;
372
+ // Create a simple schema with raw explanation for AI processing
373
+ const schema = {
374
+ kind: kind,
375
+ apiVersion: apiVersion,
376
+ group: group,
377
+ description: explanation.split('\n').find((line) => line.startsWith('DESCRIPTION:'))?.replace('DESCRIPTION:', '').trim() || '',
378
+ properties: new Map(),
379
+ rawExplanation: explanation // Include raw explanation for AI
380
+ };
381
+ schemas.push(schema);
382
+ }
383
+ catch (error) {
384
+ errors.push(`${resource.kind}: ${error.message}`);
385
+ }
386
+ }
387
+ if (schemas.length === 0) {
388
+ throw new Error(`Could not fetch schemas for any selected resources. Candidates: ${candidates.map(c => c.kind).join(', ')}. Errors: ${errors.join(', ')}`);
389
+ }
390
+ if (errors.length > 0) {
391
+ console.warn(`Some resources could not be analyzed: ${errors.join(', ')}`);
392
+ console.warn(`Successfully fetched schemas for: ${schemas.map(s => s.kind).join(', ')}`);
393
+ }
394
+ return schemas;
395
+ }
396
+ /**
397
+ * Phase 3: Rank resources with detailed schema information
398
+ */
399
+ async rankWithDetailedSchemas(intent, schemas) {
400
+ const prompt = await this.loadPromptTemplate(intent, schemas);
401
+ const response = await this.claudeIntegration.sendMessage(prompt);
402
+ const solutions = this.parseAISolutionResponse(response.content, schemas);
403
+ // Generate AI-powered questions for each solution
404
+ for (const solution of solutions) {
405
+ solution.questions = await this.generateQuestionsWithAI(intent, solution);
406
+ }
407
+ return solutions;
408
+ }
409
+ /**
410
+ * Load and format prompt template from file
411
+ */
412
+ async loadPromptTemplate(intent, schemas) {
413
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
414
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
415
+ const promptPath = path.join(process.cwd(), 'prompts', 'resource-solution-ranking.md');
416
+ const template = fs.readFileSync(promptPath, 'utf8');
417
+ // Format resources for the prompt
418
+ const resourcesText = schemas.map((schema, index) => `${index}: ${schema.kind} (${schema.apiVersion})
419
+ Group: ${schema.group || 'core'}
420
+ Description: ${schema.description}
421
+ Namespaced: ${schema.namespace}`).join('\n\n');
422
+ return template
423
+ .replace('{intent}', intent)
424
+ .replace('{resources}', resourcesText);
425
+ }
426
+ /**
427
+ * Parse AI response into solution results
428
+ */
429
+ parseAISolutionResponse(aiResponse, schemas) {
430
+ try {
431
+ // Use robust JSON extraction
432
+ const parsed = this.extractJsonFromAIResponse(aiResponse);
433
+ const solutions = parsed.solutions.map((solution) => {
434
+ const isDebugMode = process.env.DOT_AI_DEBUG === 'true';
435
+ if (isDebugMode) {
436
+ console.debug('DEBUG: solution object:', JSON.stringify(solution, null, 2));
437
+ }
438
+ // Find matching schemas for the requested resources
439
+ const resources = [];
440
+ const notFound = [];
441
+ for (const requestedResource of solution.resources || []) {
442
+ const matchingSchema = schemas.find(schema => schema.kind === requestedResource.kind &&
443
+ schema.apiVersion === requestedResource.apiVersion &&
444
+ schema.group === requestedResource.group);
445
+ if (matchingSchema) {
446
+ resources.push(matchingSchema);
447
+ }
448
+ else {
449
+ notFound.push(requestedResource);
450
+ }
451
+ }
452
+ if (resources.length === 0) {
453
+ if (isDebugMode) {
454
+ console.debug('DEBUG: No matching resources found');
455
+ console.debug('DEBUG: Requested resources:', solution.resources);
456
+ console.debug('DEBUG: Available schemas:', schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group })));
457
+ }
458
+ const debugInfo = {
459
+ requestedResources: solution.resources || [],
460
+ notFoundResources: notFound,
461
+ availableSchemas: schemas.map(s => ({ kind: s.kind, apiVersion: s.apiVersion, group: s.group }))
462
+ };
463
+ throw new Error(`No matching resources found: ${JSON.stringify(debugInfo, null, 2)}`);
464
+ }
465
+ if (notFound.length > 0 && isDebugMode) {
466
+ console.debug('DEBUG: Some resources not found:', notFound);
467
+ }
468
+ return {
469
+ type: solution.type,
470
+ resources,
471
+ score: solution.score,
472
+ description: solution.description,
473
+ reasons: solution.reasons || [],
474
+ analysis: solution.analysis || '',
475
+ questions: { required: [], basic: [], advanced: [], open: { question: '', placeholder: '' } }
476
+ };
477
+ });
478
+ // Sort by score descending
479
+ return solutions.sort((a, b) => b.score - a.score);
480
+ }
481
+ catch (error) {
482
+ // Enhanced error message with more context
483
+ const errorMsg = `Failed to parse AI solution response: ${error.message}`;
484
+ const contextMsg = `\nAI Response (first 500 chars): "${aiResponse.substring(0, 500)}..."`;
485
+ const schemasMsg = `\nAvailable schemas: ${schemas.map(s => s.kind).join(', ')} (total: ${schemas.length})`;
486
+ throw new Error(errorMsg + contextMsg + schemasMsg);
487
+ }
488
+ }
489
+ /**
490
+ * Discover cluster options for dynamic question generation
491
+ */
492
+ async discoverClusterOptions() {
493
+ try {
494
+ const { executeKubectl } = await Promise.resolve().then(() => __importStar(require('./kubernetes-utils')));
495
+ // Discover namespaces
496
+ const namespacesResult = await executeKubectl(['get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}']);
497
+ const namespaces = namespacesResult.split(/\s+/).filter(Boolean);
498
+ // Discover storage classes
499
+ let storageClasses = [];
500
+ try {
501
+ const storageResult = await executeKubectl(['get', 'storageclass', '-o', 'jsonpath={.items[*].metadata.name}']);
502
+ storageClasses = storageResult.split(/\s+/).filter(Boolean);
503
+ }
504
+ catch {
505
+ // Storage classes might not be available in all clusters
506
+ storageClasses = [];
507
+ }
508
+ // Discover ingress classes
509
+ let ingressClasses = [];
510
+ try {
511
+ const ingressResult = await executeKubectl(['get', 'ingressclass', '-o', 'jsonpath={.items[*].metadata.name}']);
512
+ ingressClasses = ingressResult.split(/\s+/).filter(Boolean);
513
+ }
514
+ catch {
515
+ // Ingress classes might not be available
516
+ ingressClasses = [];
517
+ }
518
+ // Get common node labels
519
+ let nodeLabels = [];
520
+ try {
521
+ const nodesResult = await executeKubectl(['get', 'nodes', '-o', 'json']);
522
+ const nodes = JSON.parse(nodesResult);
523
+ const labelSet = new Set();
524
+ nodes.items?.forEach((node) => {
525
+ Object.keys(node.metadata?.labels || {}).forEach(label => {
526
+ if (!label.startsWith('kubernetes.io/') && !label.startsWith('node.kubernetes.io/')) {
527
+ labelSet.add(label);
528
+ }
529
+ });
530
+ });
531
+ nodeLabels = Array.from(labelSet);
532
+ }
533
+ catch {
534
+ nodeLabels = [];
535
+ }
536
+ return {
537
+ namespaces,
538
+ storageClasses,
539
+ ingressClasses,
540
+ nodeLabels
541
+ };
542
+ }
543
+ catch (error) {
544
+ console.warn('Failed to discover cluster options, using defaults:', error);
545
+ return {
546
+ namespaces: ['default'],
547
+ storageClasses: [],
548
+ ingressClasses: [],
549
+ nodeLabels: []
550
+ };
551
+ }
552
+ }
553
+ /**
554
+ * Extract JSON object from AI response with robust parsing
555
+ */
556
+ extractJsonFromAIResponse(aiResponse) {
557
+ let jsonContent = aiResponse;
558
+ // First try to find JSON wrapped in code blocks
559
+ const codeBlockMatch = aiResponse.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
560
+ if (codeBlockMatch) {
561
+ jsonContent = codeBlockMatch[1];
562
+ }
563
+ else {
564
+ // Try to find JSON that starts with { and find the matching closing }
565
+ const startIndex = aiResponse.indexOf('{');
566
+ if (startIndex !== -1) {
567
+ let braceCount = 0;
568
+ let endIndex = startIndex;
569
+ for (let i = startIndex; i < aiResponse.length; i++) {
570
+ if (aiResponse[i] === '{')
571
+ braceCount++;
572
+ if (aiResponse[i] === '}')
573
+ braceCount--;
574
+ if (braceCount === 0) {
575
+ endIndex = i;
576
+ break;
577
+ }
578
+ }
579
+ if (braceCount === 0) {
580
+ jsonContent = aiResponse.substring(startIndex, endIndex + 1);
581
+ }
582
+ }
583
+ }
584
+ return JSON.parse(jsonContent.trim());
585
+ }
586
+ /**
587
+ * Generate contextual questions using AI based on user intent and solution resources
588
+ */
589
+ async generateQuestionsWithAI(intent, solution) {
590
+ try {
591
+ // Discover cluster options for dynamic questions
592
+ const clusterOptions = await this.discoverClusterOptions();
593
+ // Format resource details for the prompt using raw explanation when available
594
+ const resourceDetails = solution.resources.map(resource => {
595
+ if (resource.rawExplanation) {
596
+ // Use raw kubectl explain output for comprehensive field information
597
+ return `${resource.kind} (${resource.apiVersion}):
598
+ Description: ${resource.description}
599
+
600
+ Complete Schema Information:
601
+ ${resource.rawExplanation}`;
602
+ }
603
+ else {
604
+ // Fallback to properties map if raw explanation is not available
605
+ const properties = Array.from(resource.properties.entries()).map(([key, field]) => {
606
+ const nestedFields = Array.from(field.nested.entries()).map(([nestedKey, nestedField]) => ` ${nestedKey}: ${nestedField.type} - ${nestedField.description}`).join('\n');
607
+ return ` ${key}: ${field.type} - ${field.description}${field.required ? ' (required)' : ''}${nestedFields ? '\n' + nestedFields : ''}`;
608
+ }).join('\n');
609
+ return `${resource.kind} (${resource.apiVersion}):
610
+ Description: ${resource.description}
611
+ Required fields: ${resource.required?.join(', ') || 'none specified'}
612
+ Properties:
613
+ ${properties}`;
614
+ }
615
+ }).join('\n\n');
616
+ // Format cluster options for the prompt
617
+ const clusterOptionsText = `Available Namespaces: ${clusterOptions.namespaces.join(', ')}
618
+ Available Storage Classes: ${clusterOptions.storageClasses.length > 0 ? clusterOptions.storageClasses.join(', ') : 'None discovered'}
619
+ Available Ingress Classes: ${clusterOptions.ingressClasses.length > 0 ? clusterOptions.ingressClasses.join(', ') : 'None discovered'}
620
+ Available Node Labels: ${clusterOptions.nodeLabels.length > 0 ? clusterOptions.nodeLabels.slice(0, 10).join(', ') : 'None discovered'}`;
621
+ // Load and format the question generation prompt
622
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
623
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
624
+ const promptPath = path.join(process.cwd(), 'prompts', 'question-generation.md');
625
+ const template = fs.readFileSync(promptPath, 'utf8');
626
+ const questionPrompt = template
627
+ .replace('{intent}', intent)
628
+ .replace('{solution_description}', solution.description)
629
+ .replace('{resource_details}', resourceDetails)
630
+ .replace('{cluster_options}', clusterOptionsText);
631
+ const response = await this.claudeIntegration.sendMessage(questionPrompt);
632
+ // Use robust JSON extraction
633
+ const questions = this.extractJsonFromAIResponse(response.content);
634
+ // Validate the response structure
635
+ if (!questions.required || !questions.basic || !questions.advanced || !questions.open) {
636
+ throw new Error('Invalid question structure from AI');
637
+ }
638
+ return questions;
639
+ }
640
+ catch (error) {
641
+ console.warn(`Failed to generate AI questions for solution: ${error}`);
642
+ // Fallback to basic open question
643
+ return {
644
+ required: [],
645
+ basic: [],
646
+ advanced: [],
647
+ open: {
648
+ question: "Is there anything else about your requirements or constraints that would help us provide better recommendations?",
649
+ placeholder: "e.g., specific security requirements, performance needs, existing infrastructure constraints..."
650
+ }
651
+ };
652
+ }
653
+ }
654
+ }
655
+ exports.ResourceRecommender = ResourceRecommender;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Session directory utilities for MCP tools
3
+ * Provides consistent session directory resolution and validation across all tools
4
+ */
5
+ /**
6
+ * Get session directory from CLI args or environment variable
7
+ * CLI parameter takes precedence over environment variable
8
+ *
9
+ * @param args - Tool arguments that may contain sessionDir
10
+ * @returns Resolved session directory path (can be relative or absolute)
11
+ */
12
+ export declare function getSessionDirectory(args: any): string;
13
+ /**
14
+ * Validate session directory exists and is accessible
15
+ * Works with both relative and absolute paths
16
+ *
17
+ * @param sessionDir - Session directory path to validate
18
+ * @param requireWrite - Whether to test write permissions (default: false)
19
+ */
20
+ export declare function validateSessionDirectory(sessionDir: string, requireWrite?: boolean): void;
21
+ /**
22
+ * Get and validate session directory in one call
23
+ *
24
+ * @param args - Tool arguments that may contain sessionDir
25
+ * @param requireWrite - Whether to test write permissions (default: false)
26
+ * @returns Validated session directory path
27
+ */
28
+ export declare function getAndValidateSessionDirectory(args: any, requireWrite?: boolean): string;
29
+ //# sourceMappingURL=session-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-utils.d.ts","sourceRoot":"","sources":["../../src/core/session-utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,CAerD;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,GAAE,OAAe,GAAG,IAAI,CA4ChG;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,GAAE,OAAe,GAAG,MAAM,CAI/F"}