@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.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +51 -0
- package/dist/core/claude.d.ts +42 -0
- package/dist/core/claude.d.ts.map +1 -0
- package/dist/core/claude.js +229 -0
- package/dist/core/deploy-operation.d.ts +38 -0
- package/dist/core/deploy-operation.d.ts.map +1 -0
- package/dist/core/deploy-operation.js +101 -0
- package/dist/core/discovery.d.ts +162 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +758 -0
- package/dist/core/error-handling.d.ts +167 -0
- package/dist/core/error-handling.d.ts.map +1 -0
- package/dist/core/error-handling.js +399 -0
- package/dist/core/index.d.ts +42 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +123 -0
- package/dist/core/kubernetes-utils.d.ts +38 -0
- package/dist/core/kubernetes-utils.d.ts.map +1 -0
- package/dist/core/kubernetes-utils.js +177 -0
- package/dist/core/memory.d.ts +45 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +113 -0
- package/dist/core/schema.d.ts +187 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +655 -0
- package/dist/core/session-utils.d.ts +29 -0
- package/dist/core/session-utils.d.ts.map +1 -0
- package/dist/core/session-utils.js +121 -0
- package/dist/core/workflow.d.ts +70 -0
- package/dist/core/workflow.d.ts.map +1 -0
- package/dist/core/workflow.js +161 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/interfaces/cli.d.ts +74 -0
- package/dist/interfaces/cli.d.ts.map +1 -0
- package/dist/interfaces/cli.js +769 -0
- package/dist/interfaces/mcp.d.ts +30 -0
- package/dist/interfaces/mcp.d.ts.map +1 -0
- package/dist/interfaces/mcp.js +105 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +151 -0
- package/dist/tools/answer-question.d.ts +27 -0
- package/dist/tools/answer-question.d.ts.map +1 -0
- package/dist/tools/answer-question.js +696 -0
- package/dist/tools/choose-solution.d.ts +23 -0
- package/dist/tools/choose-solution.d.ts.map +1 -0
- package/dist/tools/choose-solution.js +171 -0
- package/dist/tools/deploy-manifests.d.ts +25 -0
- package/dist/tools/deploy-manifests.d.ts.map +1 -0
- package/dist/tools/deploy-manifests.js +74 -0
- package/dist/tools/generate-manifests.d.ts +23 -0
- package/dist/tools/generate-manifests.d.ts.map +1 -0
- package/dist/tools/generate-manifests.js +424 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/recommend.d.ts +23 -0
- package/dist/tools/recommend.d.ts.map +1 -0
- package/dist/tools/recommend.js +332 -0
- package/package.json +124 -0
- package/prompts/intent-validation.md +65 -0
- package/prompts/manifest-generation.md +79 -0
- package/prompts/question-generation.md +128 -0
- package/prompts/resource-analysis.md +127 -0
- package/prompts/resource-selection.md +55 -0
- package/prompts/resource-solution-ranking.md +77 -0
- 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"}
|